From b2990e9a3c3d880109241169fc083e5d35c5800d Mon Sep 17 00:00:00 2001 From: Victorien Gauch <85494462+VGau@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:44:53 +0200 Subject: [PATCH] Feat: Add metrics to postman (#871) * feat: add metrics to postman * fix: issue with get message to claim query * fix: import issues * fix: update unit tests and fix minor issues * fix: refator metrics * fix: update typeorm query * fix: update postman docker config --- docker/compose-spec-l2-services.yml | 2 + docker/config/postman/env | 3 +- pnpm-lock.yaml | 635 +++++++++++++++--- postman/.env.sample | 1 + postman/jest.config.js | 2 + postman/package.json | 3 + postman/scripts/runPostman.ts | 5 +- postman/src/application/postman/api/Api.ts | 65 ++ .../postman/api/__tests__/Api.test.ts | 47 ++ .../api/metrics/MessageMetricsService.ts | 71 ++ .../postman/api/metrics/MetricsService.ts | 152 +++++ .../__tests__/MessageMetricsService.test.ts | 64 ++ .../metrics/__tests__/MetricsService.test.ts | 60 ++ .../postman/app/PostmanServiceClient.ts | 58 +- .../__tests__/PostmanServiceClient.test.ts | 31 +- .../app/config/__tests__/utils.test.ts | 9 + .../application/postman/app/config/config.ts | 8 + .../application/postman/app/config/utils.ts | 4 + .../repositories/TypeOrmMessageRepository.ts | 7 + .../subscribers/MessageStatusSubscriber.ts | 124 ++++ postman/src/core/metrics/IMetricsService.ts | 16 + .../processors/MessageClaimingProcessor.ts | 20 +- 22 files changed, 1263 insertions(+), 124 deletions(-) create mode 100644 postman/src/application/postman/api/Api.ts create mode 100644 postman/src/application/postman/api/__tests__/Api.test.ts create mode 100644 postman/src/application/postman/api/metrics/MessageMetricsService.ts create mode 100644 postman/src/application/postman/api/metrics/MetricsService.ts create mode 100644 postman/src/application/postman/api/metrics/__tests__/MessageMetricsService.test.ts create mode 100644 postman/src/application/postman/api/metrics/__tests__/MetricsService.test.ts create mode 100644 postman/src/application/postman/persistence/subscribers/MessageStatusSubscriber.ts create mode 100644 postman/src/core/metrics/IMetricsService.ts diff --git a/docker/compose-spec-l2-services.yml b/docker/compose-spec-l2-services.yml index 90bfcef6..f6885086 100644 --- a/docker/compose-spec-l2-services.yml +++ b/docker/compose-spec-l2-services.yml @@ -251,6 +251,8 @@ services: profiles: [ "l2", "debug" ] platform: linux/amd64 restart: on-failure + ports: + - "9090:3000" depends_on: sequencer: condition: service_healthy diff --git a/docker/config/postman/env b/docker/config/postman/env index 61111afa..cb34f5d0 100644 --- a/docker/config/postman/env +++ b/docker/config/postman/env @@ -39,6 +39,7 @@ POSTGRES_PASSWORD=postgres POSTGRES_DB=postman_db DB_CLEANER_ENABLED=false ENABLE_LINEA_ESTIMATE_GAS=false +API_PORT=3000 L1_L2_ENABLE_POSTMAN_SPONSORING=true L2_L1_ENABLE_POSTMAN_SPONSORING=false -MAX_POSTMAN_SPONSOR_GAS_LIMIT=250000 \ No newline at end of file +MAX_POSTMAN_SPONSOR_GAS_LIMIT=250000 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3933d5e7..dff8c0ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,10 +100,10 @@ importers: version: 1.9.2 next: specifier: 14.2.25 - version: 14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0) + version: 14.2.25(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0) next-seo: specifier: 6.6.0 - version: 6.6.0(next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 6.6.0(next@14.2.25(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pino-pretty: specifier: 13.0.0 version: 13.0.0 @@ -362,12 +362,18 @@ importers: ethers: specifier: 6.13.4 version: 6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) + express: + specifier: 5.1.0 + version: 5.1.0 filtrex: specifier: 3.1.0 version: 3.1.0 pg: specifier: 8.13.1 version: 8.13.1 + prom-client: + specifier: 15.1.3 + version: 15.1.3 typeorm: specifier: 0.3.20 version: 0.3.20(better-sqlite3@11.6.0)(pg@8.13.1)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5)) @@ -381,6 +387,9 @@ importers: '@jest/globals': specifier: 29.7.0 version: 29.7.0 + '@types/express': + specifier: 5.0.1 + version: 5.0.1 '@types/jest': specifier: 29.5.14 version: 29.5.14 @@ -2729,6 +2738,10 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@openzeppelin/contracts-upgradeable@4.9.6': resolution: {integrity: sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==} @@ -3636,6 +3649,9 @@ packages: '@types/bn.js@5.1.6': resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} @@ -3660,6 +3676,12 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@5.0.1': + resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} + '@types/ffi-napi@4.0.10': resolution: {integrity: sha512-Q6TimLDxdg+Obp4W9tgzIhSA25THD6AmR8eM+ZB7bbU96xnkpd7PRXwG4az2rKpreuF90IPr/LlQtetmIfQL2g==} @@ -3678,6 +3700,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -3711,6 +3736,9 @@ packages: '@types/lru-cache@5.1.1': resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -3759,6 +3787,9 @@ packages: '@types/qs@6.9.16': resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} @@ -3785,6 +3816,12 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -4106,6 +4143,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4499,6 +4540,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bip174@3.0.0-rc.1: resolution: {integrity: sha512-+8P3BpSairVNF2Nee6Ksdc1etIjWjBOi/MH0MwKtq9YaYp+S2Hk2uvup0e8hCT4IKlS58nXJyyQVmW92zPoD4Q==} engines: {node: '>=18.0.0'} @@ -4532,6 +4576,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -4678,10 +4726,18 @@ packages: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caller-callsite@2.0.0: resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} engines: {node: '>=4'} @@ -5004,6 +5060,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-hash@2.5.2: resolution: {integrity: sha512-FvIQKy0S1JaWV10sMsA7TRx8bpU+pqPkhbsfvOJAdjRXvYxEckAwQWGwtRjiaJfh+E0DvcWUGqcdjwMGFjsSdw==} @@ -5023,6 +5083,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} @@ -5190,6 +5254,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -5364,6 +5437,10 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} @@ -5481,8 +5558,8 @@ packages: resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} engines: {node: '>= 0.4'} - es-define-property@1.0.0: - resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} es-errors@1.3.0: @@ -5500,6 +5577,10 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} @@ -5853,6 +5934,10 @@ packages: resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -5959,6 +6044,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-cache-dir@2.1.0: resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} engines: {node: '>=6'} @@ -6084,6 +6173,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -6167,6 +6260,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -6182,6 +6279,10 @@ packages: resolution: {integrity: sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -6279,6 +6380,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} @@ -6378,6 +6483,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} @@ -6761,6 +6870,9 @@ packages: resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -7376,6 +7488,10 @@ packages: match-all@1.2.6: resolution: {integrity: sha512-0EESkXiTkWzrQQntBu2uzKvLu6vVkUGz40nGPbSZuegcfE5UuSzNjLaIu76zJWuaT/2I3Z/8M06OlUOZLGwLlQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} @@ -7389,6 +7505,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -7399,6 +7519,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-options@3.0.4: resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} engines: {node: '>=10'} @@ -7486,14 +7610,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.53.0: - resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -7708,6 +7836,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -7950,8 +8082,8 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.2: - resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} object-is@1.1.6: @@ -8189,6 +8321,10 @@ packages: path-to-regexp@0.1.10: resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8429,6 +8565,10 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} @@ -8502,6 +8642,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -8549,6 +8693,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -8904,6 +9052,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rpc-websockets@9.1.1: resolution: {integrity: sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA==} @@ -8977,6 +9129,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -8988,6 +9144,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + servify@0.1.12: resolution: {integrity: sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw==} engines: {node: '>=6'} @@ -9052,10 +9212,26 @@ packages: engines: {node: '>=6'} hasBin: true + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -9450,6 +9626,9 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -9706,6 +9885,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -10641,7 +10824,7 @@ snapshots: '@babel/traverse': 7.25.7 '@babel/types': 7.25.7 convert-source-map: 2.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10699,7 +10882,7 @@ snapshots: '@babel/core': 7.25.7 '@babel/helper-compilation-targets': 7.25.7 '@babel/helper-plugin-utils': 7.25.7 - debug: 4.3.7 + debug: 4.4.0 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -11576,7 +11759,7 @@ snapshots: '@babel/parser': 7.25.7 '@babel/template': 7.25.7 '@babel/types': 7.25.7 - debug: 4.3.7 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12386,7 +12569,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.0 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -12765,7 +12948,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13302,7 +13485,7 @@ snapshots: bufferutil: 4.0.8 cross-fetch: 4.0.0(encoding@0.1.13) date-fns: 2.30.0 - debug: 4.3.7 + debug: 4.4.0 eciesjs: 0.4.13 eventemitter2: 6.4.9 readable-stream: 3.6.2 @@ -13326,7 +13509,7 @@ snapshots: '@paulmillr/qr': 0.2.1 bowser: 2.11.0 cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) eciesjs: 0.4.13 eth-rpc-errors: 4.0.3 eventemitter2: 6.4.9 @@ -13349,7 +13532,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.4.0 semver: 7.6.3 superstruct: 1.0.4 transitivePeerDependencies: @@ -13362,7 +13545,7 @@ snapshots: '@noble/hashes': 1.7.1 '@scure/base': 1.2.4 '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.4.0 pony-cause: 2.1.11 semver: 7.6.3 uuid: 9.0.1 @@ -13376,7 +13559,7 @@ snapshots: '@noble/hashes': 1.7.1 '@scure/base': 1.2.4 '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.4.0 pony-cause: 2.1.11 semver: 7.6.3 uuid: 9.0.1 @@ -13835,6 +14018,8 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api@1.9.0': {} + '@openzeppelin/contracts-upgradeable@4.9.6': {} '@openzeppelin/contracts@4.9.6': {} @@ -15273,6 +15458,11 @@ snapshots: dependencies: '@types/node': 20.12.7 + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.12.7 + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 @@ -15302,6 +15492,19 @@ snapshots: '@types/estree@1.0.6': {} + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 20.12.7 + '@types/qs': 6.9.16 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@5.0.1': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.6 + '@types/serve-static': 1.15.7 + '@types/ffi-napi@4.0.10': dependencies: '@types/node': 20.12.7 @@ -15328,6 +15531,8 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} + '@types/http-errors@2.0.4': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -15364,6 +15569,8 @@ snapshots: '@types/lru-cache@5.1.1': {} + '@types/mime@1.3.5': {} + '@types/minimatch@5.1.2': {} '@types/mocha@10.0.9': {} @@ -15404,6 +15611,8 @@ snapshots: '@types/qs@6.9.16': {} + '@types/range-parser@1.2.7': {} + '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.11 @@ -15435,6 +15644,17 @@ snapshots: '@types/semver@7.5.8': {} + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.12.7 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 20.12.7 + '@types/send': 0.17.4 + '@types/stack-utils@2.0.3': {} '@types/tinycolor2@1.4.6': {} @@ -15481,7 +15701,7 @@ snapshots: '@typescript-eslint/type-utils': 7.6.0(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.6.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -15499,7 +15719,7 @@ snapshots: '@typescript-eslint/types': 7.6.0 '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.6.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 optionalDependencies: typescript: 5.4.5 @@ -15515,7 +15735,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.5) '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.4.5) - debug: 4.3.7 + debug: 4.4.0 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.5) optionalDependencies: @@ -15529,7 +15749,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.6.0 '@typescript-eslint/visitor-keys': 7.6.0 - debug: 4.3.7 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -16240,6 +16460,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -16258,13 +16483,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color agent-base@7.1.1: dependencies: - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -16442,7 +16667,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.3 es-errors: 1.3.0 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 @@ -16524,7 +16749,7 @@ snapshots: axios@1.6.7: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16684,6 +16909,8 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 + bintrees@1.0.2: {} + bip174@3.0.0-rc.1: dependencies: uint8array-tools: 0.0.9 @@ -16736,6 +16963,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} borsh@0.7.0: @@ -16924,14 +17165,24 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 - call-bind@1.0.7: + call-bind-apply-helpers@1.0.2: dependencies: - es-define-property: 1.0.0 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.4 + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 set-function-length: 1.2.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caller-callsite@2.0.0: dependencies: callsites: 2.0.0 @@ -17219,7 +17470,7 @@ snapshots: compressible@2.0.18: dependencies: - mime-db: 1.53.0 + mime-db: 1.54.0 compression@1.7.4: dependencies: @@ -17268,6 +17519,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-hash@2.5.2: dependencies: cids: 0.7.5 @@ -17284,6 +17539,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.4.2: {} cookie@0.7.1: {} @@ -17481,16 +17738,16 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.7: - dependencies: - ms: 2.1.3 - debug@4.3.7(supports-color@8.1.1): dependencies: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decamelize@4.0.0: {} @@ -17522,7 +17779,7 @@ snapshots: array-buffer-byte-length: 1.0.1 call-bind: 1.0.7 es-get-iterator: 1.1.3 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-arguments: 1.1.1 is-array-buffer: 3.0.4 is-date-object: 1.0.5 @@ -17533,7 +17790,7 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.3 - side-channel: 1.0.6 + side-channel: 1.1.0 which-boxed-primitive: 1.0.2 which-collection: 1.0.2 which-typed-array: 1.1.15 @@ -17554,9 +17811,9 @@ snapshots: define-data-property@1.1.4: dependencies: - es-define-property: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 - gopd: 1.0.1 + gopd: 1.2.0 define-properties@1.2.1: dependencies: @@ -17646,6 +17903,12 @@ snapshots: dotenv@16.4.7: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer2@0.1.4: dependencies: readable-stream: 2.3.8 @@ -17745,7 +18008,7 @@ snapshots: engine.io-client@6.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) xmlhttprequest-ssl: 2.1.1 @@ -17796,19 +18059,19 @@ snapshots: data-view-buffer: 1.0.1 data-view-byte-length: 1.0.1 data-view-byte-offset: 1.0.0 - es-define-property: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.0.0 + es-object-atoms: 1.1.1 es-set-tostringtag: 2.0.3 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 get-symbol-description: 1.0.2 globalthis: 1.0.4 - gopd: 1.0.1 + gopd: 1.2.0 has-property-descriptors: 1.0.2 has-proto: 1.0.3 - has-symbols: 1.0.3 + has-symbols: 1.1.0 hasown: 2.0.2 internal-slot: 1.0.7 is-array-buffer: 3.0.4 @@ -17820,7 +18083,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.13 is-weakref: 1.0.2 - object-inspect: 1.13.2 + object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.3 @@ -17836,17 +18099,15 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.15 - es-define-property@1.0.0: - dependencies: - get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} es-errors@1.3.0: {} es-get-iterator@1.1.3: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 is-arguments: 1.1.1 is-map: 2.0.3 is-set: 2.0.3 @@ -17875,9 +18136,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.3: dependencies: - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -18023,7 +18288,7 @@ snapshots: eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) @@ -18154,7 +18419,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -18553,6 +18818,38 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext@1.7.0: dependencies: type: 2.7.3 @@ -18658,6 +18955,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-cache-dir@2.1.0: dependencies: commondir: 1.0.1 @@ -18710,8 +19018,6 @@ snapshots: dependencies: tslib: 2.7.0 - follow-redirects@1.15.9: {} - follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: debug: 4.3.7(supports-color@8.1.1) @@ -18776,6 +19082,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -18866,9 +19174,22 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 has-proto: 1.0.3 - has-symbols: 1.0.3 + has-symbols: 1.1.0 hasown: 2.0.2 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-package-type@0.1.0: {} get-port-please@3.1.2: {} @@ -18877,6 +19198,11 @@ snapshots: get-port@6.1.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@5.2.0: dependencies: pump: 3.0.2 @@ -18889,7 +19215,7 @@ snapshots: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 get-tsconfig@4.8.1: dependencies: @@ -18989,7 +19315,7 @@ snapshots: globalthis@1.0.4: dependencies: define-properties: 1.2.1 - gopd: 1.0.1 + gopd: 1.2.0 globby@10.0.2: dependencies: @@ -19013,7 +19339,9 @@ snapshots: gopd@1.0.1: dependencies: - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 + + gopd@1.2.0: {} got@11.8.6: dependencies: @@ -19224,15 +19552,17 @@ snapshots: has-property-descriptors@1.0.2: dependencies: - es-define-property: 1.0.0 + es-define-property: 1.0.1 has-proto@1.0.3: {} has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: dependencies: - has-symbols: 1.0.3 + has-symbols: 1.1.0 hash-base@3.1.0: dependencies: @@ -19326,14 +19656,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.3.7) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -19363,14 +19693,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -19403,7 +19733,6 @@ snapshots: iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - optional: true idb-keyval@6.2.1: {} @@ -19457,7 +19786,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 interpret@1.4.0: {} @@ -19486,7 +19815,7 @@ snapshots: is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-arrayish@0.2.1: {} @@ -19587,6 +19916,8 @@ snapshots: is-primitive@3.0.1: {} + is-promise@4.0.0: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -19608,7 +19939,7 @@ snapshots: is-symbol@1.0.4: dependencies: - has-symbols: 1.0.3 + has-symbols: 1.1.0 is-typed-array@1.1.13: dependencies: @@ -19627,7 +19958,7 @@ snapshots: is-weakset@2.0.3: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-wsl@1.1.0: {} @@ -19708,7 +20039,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -19724,8 +20055,8 @@ snapshots: iterator.prototype@1.1.3: dependencies: define-properties: 1.2.1 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 reflect.getprototypeof: 1.0.6 set-function-name: 2.0.2 @@ -20520,6 +20851,8 @@ snapshots: match-all@1.2.6: {} + math-intrinsics@1.1.0: {} + md5.js@1.3.5: dependencies: hash-base: 3.1.0 @@ -20532,12 +20865,16 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memoize-one@5.2.1: {} memorystream@0.3.1: {} merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-options@3.0.4: dependencies: is-plain-obj: 2.1.0 @@ -20738,12 +21075,16 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.53.0: {} + mime-db@1.54.0: {} mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.6.0: {} @@ -20952,17 +21293,19 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} - next-seo@6.6.0(next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-seo@6.6.0(next@14.2.25(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - next: 14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0) + next: 14.2.25(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) next-tick@1.1.0: {} - next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0): + next@14.2.25(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0): dependencies: '@next/env': 14.2.25 '@swc/helpers': 0.5.5 @@ -20983,6 +21326,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.25 '@next/swc-win32-ia32-msvc': 14.2.25 '@next/swc-win32-x64-msvc': 14.2.25 + '@opentelemetry/api': 1.9.0 '@playwright/test': 1.51.1 sass: 1.86.0 transitivePeerDependencies: @@ -21117,7 +21461,7 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.2: {} + object-inspect@1.13.4: {} object-is@1.1.6: dependencies: @@ -21132,7 +21476,7 @@ snapshots: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - has-symbols: 1.0.3 + has-symbols: 1.1.0 object-keys: 1.1.1 object.entries@1.1.8: @@ -21379,6 +21723,8 @@ snapshots: path-to-regexp@0.1.10: {} + path-to-regexp@8.2.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -21610,6 +21956,11 @@ snapshots: progress@2.0.3: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.0 + tdigest: 0.1.2 + promise-retry@2.0.1: dependencies: err-code: 2.0.3 @@ -21688,6 +22039,10 @@ snapshots: dependencies: side-channel: 1.0.6 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + qs@6.5.3: {} query-string@5.1.1: @@ -21734,6 +22089,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -21975,7 +22337,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.3 es-errors: 1.3.0 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 globalthis: 1.0.4 which-builtin-type: 1.1.4 @@ -22162,6 +22524,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + rpc-websockets@9.1.1: dependencies: '@swc/helpers': 0.5.13 @@ -22182,8 +22554,8 @@ snapshots: safe-array-concat@1.1.2: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 isarray: 2.0.5 safe-buffer@5.1.2: {} @@ -22272,6 +22644,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serialize-javascript@6.0.2: @@ -22287,6 +22675,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + servify@0.1.12: dependencies: body-parser: 1.20.3 @@ -22304,8 +22701,8 @@ snapshots: define-data-property: 1.1.4 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.4 - gopd: 1.0.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 has-property-descriptors: 1.0.2 set-function-name@2.0.2: @@ -22385,12 +22782,40 @@ snapshots: minimist: 1.2.8 shelljs: 0.8.5 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + side-channel@1.0.6: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 - get-intrinsic: 1.2.4 - object-inspect: 1.13.2 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 signal-exit@3.0.7: {} @@ -22442,7 +22867,7 @@ snapshots: socket.io-client@4.8.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) engine.io-client: 6.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -22453,14 +22878,14 @@ snapshots: socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 - debug: 4.3.7(supports-color@8.1.1) + debug: 4.4.0 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -22673,7 +23098,7 @@ snapshots: internal-slot: 1.0.7 regexp.prototype.flags: 1.5.3 set-function-name: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 string.prototype.repeat@1.0.0: dependencies: @@ -22685,7 +23110,7 @@ snapshots: call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.23.3 - es-object-atoms: 1.0.0 + es-object-atoms: 1.1.1 string.prototype.trimend@1.0.8: dependencies: @@ -22697,7 +23122,7 @@ snapshots: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-object-atoms: 1.0.0 + es-object-atoms: 1.1.1 string_decoder@1.1.1: dependencies: @@ -22884,6 +23309,10 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + temp-dir@2.0.0: {} temp@0.8.4: @@ -22921,7 +23350,7 @@ snapshots: http-basic: 8.1.3 http-response-object: 3.0.2 promise: 8.3.0 - qs: 6.13.0 + qs: 6.14.0 thenify-all@1.6.0: dependencies: @@ -23112,7 +23541,7 @@ snapshots: bundle-require: 4.2.1(esbuild@0.19.12) cac: 6.7.14 chokidar: 3.6.0 - debug: 4.3.7 + debug: 4.4.0 esbuild: 0.19.12 execa: 5.1.1 globby: 11.1.0 @@ -23165,6 +23594,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + type@2.7.3: {} typechain@8.3.2(typescript@5.4.5): @@ -23193,7 +23628,7 @@ snapshots: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-proto: 1.0.3 is-typed-array: 1.1.13 @@ -23202,7 +23637,7 @@ snapshots: available-typed-arrays: 1.0.7 call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-proto: 1.0.3 is-typed-array: 1.1.13 @@ -23210,7 +23645,7 @@ snapshots: dependencies: call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-proto: 1.0.3 is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 @@ -23261,7 +23696,7 @@ snapshots: chalk: 4.1.2 cli-highlight: 2.1.11 dayjs: 1.11.13 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) dotenv: 16.4.5 glob: 10.4.5 mkdirp: 2.1.6 @@ -23304,7 +23739,7 @@ snapshots: dependencies: call-bind: 1.0.7 has-bigints: 1.0.2 - has-symbols: 1.0.3 + has-symbols: 1.1.0 which-boxed-primitive: 1.0.2 uncrypto@0.1.3: {} @@ -23898,7 +24333,7 @@ snapshots: available-typed-arrays: 1.0.7 call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-tostringtag: 1.0.2 which@1.3.1: diff --git a/postman/.env.sample b/postman/.env.sample index 7953d518..a8cf0468 100644 --- a/postman/.env.sample +++ b/postman/.env.sample @@ -42,6 +42,7 @@ DB_CLEANER_ENABLED=false DB_CLEANING_INTERVAL=10000 DB_DAYS_BEFORE_NOW_TO_DELETE=1 ENABLE_LINEA_ESTIMATE_GAS=false +API_PORT=3000 L1_L2_ENABLE_POSTMAN_SPONSORING=true L2_L1_ENABLE_POSTMAN_SPONSORING=false MAX_POSTMAN_SPONSOR_GAS_LIMIT=250000 diff --git a/postman/jest.config.js b/postman/jest.config.js index f4837d56..2c7710f2 100644 --- a/postman/jest.config.js +++ b/postman/jest.config.js @@ -11,6 +11,7 @@ module.exports = { "src/clients/blockchain/typechain", "src/application/postman/persistence/migrations/", "src/application/postman/persistence/repositories/", + "src/application/postman/persistence/subscribers/", "src/index.ts", "src/utils/WinstonLogger.ts", ], @@ -18,6 +19,7 @@ module.exports = { "src/clients/blockchain/typechain", "src/application/postman/persistence/migrations/", "src/application/postman/persistence/repositories/", + "src/application/postman/persistence/subscribers/", "src/index.ts", "src/utils/WinstonLogger.ts", "src/utils/testing/", diff --git a/postman/package.json b/postman/package.json index b9779281..ad5095a4 100644 --- a/postman/package.json +++ b/postman/package.json @@ -24,14 +24,17 @@ "class-validator": "0.14.1", "dotenv": "16.4.5", "ethers": "6.13.4", + "express": "5.1.0", "filtrex": "3.1.0", "pg": "8.13.1", + "prom-client": "15.1.3", "typeorm": "0.3.20", "typeorm-naming-strategies": "4.1.0", "winston": "3.17.0" }, "devDependencies": { "@jest/globals": "29.7.0", + "@types/express": "5.0.1", "@types/jest": "29.5.14", "@types/yargs": "17.0.33", "jest": "29.7.0", diff --git a/postman/scripts/runPostman.ts b/postman/scripts/runPostman.ts index a908c371..d8c3b9f6 100644 --- a/postman/scripts/runPostman.ts +++ b/postman/scripts/runPostman.ts @@ -153,8 +153,11 @@ async function main() { ? parseInt(process.env.DB_DAYS_BEFORE_NOW_TO_DELETE) : undefined, }, + apiOptions: { + port: process.env.API_PORT ? parseInt(process.env.API_PORT) : undefined, + }, }); - await client.connectDatabase(); + await client.connectServices(); client.startAllServices(); } diff --git a/postman/src/application/postman/api/Api.ts b/postman/src/application/postman/api/Api.ts new file mode 100644 index 00000000..6fa93bd7 --- /dev/null +++ b/postman/src/application/postman/api/Api.ts @@ -0,0 +1,65 @@ +import express, { Express, Request, Response } from "express"; +import { IMetricsService } from "../../../core/metrics/IMetricsService"; +import { ILogger } from "../../../core/utils/logging/ILogger"; + +type ApiConfig = { + port: number; +}; + +export class Api { + private readonly app: Express; + private server?: ReturnType; + + constructor( + private readonly config: ApiConfig, + private readonly metricsService: IMetricsService, + private readonly logger: ILogger, + ) { + this.app = express(); + + this.setupMiddleware(); + this.setupRoutes(); + } + + private setupMiddleware(): void { + this.app.use(express.json()); + } + + private setupRoutes(): void { + this.app.get("/metrics", this.handleMetrics.bind(this)); + } + + private async handleMetrics(_req: Request, res: Response): Promise { + try { + const registry = this.metricsService.getRegistry(); + res.set("Content-Type", registry.contentType); + res.end(await registry.metrics()); + } catch (error) { + res.status(500).json({ error: "Failed to collect metrics" }); + } + } + + public async start(): Promise { + this.server = this.app.listen(this.config.port); + + await new Promise((resolve) => { + this.server?.on("listening", () => { + this.logger.info(`Listening on port ${this.config.port}`); + resolve(); + }); + }); + } + + public async stop(): Promise { + if (!this.server) return; + + await new Promise((resolve, reject) => { + this.server?.close((err) => { + if (err) return reject(err); + this.logger.info(`Closing API server on port ${this.config.port}`); + this.server = undefined; + resolve(); + }); + }); + } +} diff --git a/postman/src/application/postman/api/__tests__/Api.test.ts b/postman/src/application/postman/api/__tests__/Api.test.ts new file mode 100644 index 00000000..f4cc3437 --- /dev/null +++ b/postman/src/application/postman/api/__tests__/Api.test.ts @@ -0,0 +1,47 @@ +import { Registry } from "prom-client"; +import { mock } from "jest-mock-extended"; +import { Api } from "../Api"; +import { IMetricsService } from "../../../../core/metrics/IMetricsService"; +import { ILogger } from "../../../../core/utils/logging/ILogger"; + +describe("Api", () => { + let api: Api; + const mockConfig = { port: 3000 }; + const mockMetricService = mock(); + const mockLogger = mock(); + + beforeEach(async () => { + mockMetricService.getRegistry.mockReturnValue({ + contentType: "text/plain; version=0.0.4; charset=utf-8", + metrics: async () => "mocked metrics", + } as Registry); + api = new Api(mockConfig, mockMetricService, mockLogger); + }); + + afterEach(async () => { + await api.stop(); + }); + + it("should initialize the API", () => { + expect(api).toBeDefined(); + }); + + it("should return metrics from the metric service", async () => { + await api.start(); + + const registry = api["metricsService"].getRegistry(); + expect(registry.contentType).toBe("text/plain; version=0.0.4; charset=utf-8"); + expect(await registry.metrics()).toBe("mocked metrics"); + }); + + it("should start the server", async () => { + await api.start(); + expect(api["server"]).toBeDefined(); + }); + + it("should stop the server", async () => { + await api.start(); + await api.stop(); + expect(api["server"]).toBeUndefined(); + }); +}); diff --git a/postman/src/application/postman/api/metrics/MessageMetricsService.ts b/postman/src/application/postman/api/metrics/MessageMetricsService.ts new file mode 100644 index 00000000..89a4c002 --- /dev/null +++ b/postman/src/application/postman/api/metrics/MessageMetricsService.ts @@ -0,0 +1,71 @@ +import { EntityManager } from "typeorm"; +import { Direction } from "@consensys/linea-sdk"; +import { MetricsService } from "./MetricsService"; +import { MessageEntity } from "../../persistence/entities/Message.entity"; +import { MessageStatus } from "../../../../core/enums"; +import { LineaPostmanMetrics } from "../../../../core/metrics/IMetricsService"; + +export class MessageMetricsService extends MetricsService { + constructor(private readonly entityManager: EntityManager) { + super(); + this.createGauge(LineaPostmanMetrics.Messages, "Current number of messages by status and direction", [ + "status", + "direction", + ]); + } + + public async initialize(): Promise { + const fullResult = await this.getMessagesCountFromDatabase(); + this.initializeGaugeValues(fullResult); + } + + private async getMessagesCountFromDatabase(): Promise< + { status: MessageStatus; direction: Direction; count: number }[] + > { + const totalNumberOfMessagesByStatusAndDirection = await this.entityManager + .createQueryBuilder(MessageEntity, "message") + .select("message.status", "status") + .addSelect("message.direction", "direction") + .addSelect("COUNT(message.id)", "count") + .groupBy("message.status") + .addGroupBy("message.direction") + .getRawMany(); + + // MessageStatus => MessageDirection => Count + const resultMap = new Map>(); + + totalNumberOfMessagesByStatusAndDirection.forEach((r) => { + if (!resultMap.has(r.status)) { + resultMap.set(r.status, new Map()); + } + resultMap.get(r.status)!.set(r.direction, Number(r.count)); + }); + + const results: { status: MessageStatus; direction: Direction; count: number }[] = []; + + for (const status of Object.values(MessageStatus)) { + for (const direction of Object.values(Direction)) { + results.push({ + status, + direction, + count: resultMap.get(status)?.get(direction) || 0, + }); + } + } + + return results; + } + + private initializeGaugeValues(fullResult: { status: MessageStatus; direction: Direction; count: number }[]): void { + for (const { status, count, direction } of fullResult) { + this.incrementGauge( + LineaPostmanMetrics.Messages, + { + status, + direction, + }, + count, + ); + } + } +} diff --git a/postman/src/application/postman/api/metrics/MetricsService.ts b/postman/src/application/postman/api/metrics/MetricsService.ts new file mode 100644 index 00000000..fbb1ba69 --- /dev/null +++ b/postman/src/application/postman/api/metrics/MetricsService.ts @@ -0,0 +1,152 @@ +import { Counter, Gauge, MetricObjectWithValues, MetricValue, Registry } from "prom-client"; +import { IMetricsService, LineaPostmanMetrics } from "../../../../core/metrics/IMetricsService"; + +/** + * MetricsService class that implements the IMetricsService interface. + * This class provides methods to create and manage Prometheus metrics. + */ +export abstract class MetricsService implements IMetricsService { + private readonly registry: Registry; + private readonly counters: Map>; + private readonly gauges: Map>; + + constructor() { + this.registry = new Registry(); + this.registry.setDefaultLabels({ app: "postman" }); + + this.counters = new Map(); + this.gauges = new Map(); + } + + /** + * Returns the registry + * @returns {Registry} The registry instance + */ + public getRegistry(): Registry { + return this.registry; + } + + /** + * Creates counter metric + */ + public createCounter(name: LineaPostmanMetrics, help: string, labelNames: string[] = []): Counter { + if (!this.counters.has(name)) { + this.counters.set( + name, + new Counter({ + name, + help, + labelNames, + registers: [this.registry], + }), + ); + } + return this.counters.get(name) as Counter; + } + + /** + * Get counter metric value + * @param name - Name of the metric + * @param labels - Labels for the metric + * @returns Value of the counter metric + */ + public async getCounterValue(name: LineaPostmanMetrics, labels: Record): Promise { + const counter = this.counters.get(name); + if (counter === undefined) { + return undefined; + } + + const metricData = await counter.get(); + const metricValueWithMatchingLabels = this.findMetricValueWithExactMatchingLabels(metricData, labels); + return metricValueWithMatchingLabels?.value; + } + + /** + * Creates gauge metric + * @param name - Name of the metric + * @param help - Help text for the metric + * @param labelNames - Array of label names for the metric + * @returns Gauge metric + */ + public createGauge(name: LineaPostmanMetrics, help: string, labelNames: string[] = []): Gauge { + if (!this.gauges.has(name)) { + this.gauges.set( + name, + new Gauge({ + name, + help, + labelNames, + registers: [this.registry], + }), + ); + } + return this.gauges.get(name) as Gauge; + } + + /** + * Get gauge metric value + * @param name - Name of the metric + * @param labels - Labels for the metric + * @returns Value of the gauge metric + */ + public async getGaugeValue(name: LineaPostmanMetrics, labels: Record): Promise { + const gauge = this.gauges.get(name); + + if (gauge === undefined) { + return undefined; + } + + const metricData = await gauge.get(); + const metricValueWithMatchingLabels = this.findMetricValueWithExactMatchingLabels(metricData, labels); + return metricValueWithMatchingLabels?.value; + } + + /** + * Increments a counter metric + * @param name - Name of the metric + * @param labels - Labels for the metric + * @param value - Value to increment by (default is 1) + * @returns void + */ + public incrementCounter(name: LineaPostmanMetrics, labels: Record = {}, value?: number): void { + const counter = this.counters.get(name); + if (counter !== undefined) { + counter.inc(labels, value); + } + } + + /** + * Increment a gauge metric value + * @param name - Name of the metric + * @param labels - Labels for the metric + * @param value - Value to increment by (default is 1) + * @returns void + */ + public incrementGauge(name: LineaPostmanMetrics, labels: Record = {}, value?: number): void { + const gauge = this.gauges.get(name); + if (gauge !== undefined) { + gauge.inc(labels, value); + } + } + + /** + * Decrement a gauge metric value + * @param name - Name of the metric + * @param value - Value to decrement by (default is 1) + * @param labels - Labels for the metric + * @returns void + */ + public decrementGauge(name: LineaPostmanMetrics, labels: Record = {}, value?: number): void { + const gauge = this.gauges.get(name); + if (gauge !== undefined) { + gauge.dec(labels, value); + } + } + + private findMetricValueWithExactMatchingLabels( + metricData: MetricObjectWithValues>, + labels: Record, + ): MetricValue | undefined { + return metricData.values.find((value) => Object.entries(labels).every(([key, val]) => value.labels[key] === val)); + } +} diff --git a/postman/src/application/postman/api/metrics/__tests__/MessageMetricsService.test.ts b/postman/src/application/postman/api/metrics/__tests__/MessageMetricsService.test.ts new file mode 100644 index 00000000..2665c931 --- /dev/null +++ b/postman/src/application/postman/api/metrics/__tests__/MessageMetricsService.test.ts @@ -0,0 +1,64 @@ +import { EntityManager, SelectQueryBuilder } from "typeorm"; +import { Direction } from "@consensys/linea-sdk"; +import { MessageMetricsService } from "../MessageMetricsService"; +import { mock, MockProxy } from "jest-mock-extended"; +import { MessageStatus } from "../../../../../core/enums"; +import { LineaPostmanMetrics } from "../../../../../core/metrics/IMetricsService"; + +describe("MessageMetricsService", () => { + let messageMetricsService: MessageMetricsService; + let mockEntityManager: MockProxy; + + beforeEach(() => { + mockEntityManager = mock(); + messageMetricsService = new MessageMetricsService(mockEntityManager); + }); + + it("should update gauges based on message status", async () => { + jest.spyOn(mockEntityManager, "maximum").mockResolvedValue(10); + jest.spyOn(mockEntityManager, "createQueryBuilder").mockReturnValue({ + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + addGroupBy: jest.fn().mockReturnThis(), + getRawMany: jest.fn().mockResolvedValue([ + { status: MessageStatus.SENT, direction: Direction.L1_TO_L2, count: 5 }, + { status: MessageStatus.CLAIMED_SUCCESS, direction: Direction.L1_TO_L2, count: 10 }, + ]), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as unknown as SelectQueryBuilder); + + await messageMetricsService.initialize(); + + // Check if the gauge was updated + expect( + await messageMetricsService.getGaugeValue(LineaPostmanMetrics.Messages, { + status: MessageStatus.SENT, + direction: Direction.L1_TO_L2, + }), + ).toBe(5); + + expect( + await messageMetricsService.getGaugeValue(LineaPostmanMetrics.Messages, { + status: MessageStatus.CLAIMED_SUCCESS, + direction: Direction.L1_TO_L2, + }), + ).toBe(10); + }); + + it("should return the correct gauge value", async () => { + messageMetricsService.incrementGauge( + LineaPostmanMetrics.Messages, + { + status: "processed", + direction: Direction.L1_TO_L2, + }, + 10, + ); + const gaugeValue = await messageMetricsService.getGaugeValue(LineaPostmanMetrics.Messages, { + status: "processed", + direction: Direction.L1_TO_L2, + }); + expect(gaugeValue).toBe(10); + }); +}); diff --git a/postman/src/application/postman/api/metrics/__tests__/MetricsService.test.ts b/postman/src/application/postman/api/metrics/__tests__/MetricsService.test.ts new file mode 100644 index 00000000..03915602 --- /dev/null +++ b/postman/src/application/postman/api/metrics/__tests__/MetricsService.test.ts @@ -0,0 +1,60 @@ +import { Counter, Gauge } from "prom-client"; +import { LineaPostmanMetrics } from "../../../../../core/metrics/IMetricsService"; +import { MetricsService } from "../MetricsService"; + +class TestMetricService extends MetricsService { + constructor() { + super(); + } +} + +describe("MetricsService", () => { + let metricService: TestMetricService; + + beforeEach(() => { + metricService = new TestMetricService(); + }); + + it("should create a counter", () => { + const counter = metricService.createCounter(LineaPostmanMetrics.Messages, "A test counter"); + expect(counter).toBeInstanceOf(Counter); + }); + + it("should increment a counter", async () => { + const counter = metricService.createCounter(LineaPostmanMetrics.Messages, "A test counter"); + metricService.incrementCounter(LineaPostmanMetrics.Messages, {}, 1); + expect((await counter.get()).values[0].value).toBe(1); + }); + + it("should create a gauge", () => { + const gauge = metricService.createGauge(LineaPostmanMetrics.Messages, "A test gauge"); + expect(gauge).toBeInstanceOf(Gauge); + }); + + it("should increment a gauge", async () => { + const gauge = metricService.createGauge(LineaPostmanMetrics.Messages, "A test gauge"); + metricService.incrementGauge(LineaPostmanMetrics.Messages, {}, 5); + expect((await gauge.get()).values[0].value).toBe(5); + }); + + it("should decrement a gauge", async () => { + metricService.createGauge(LineaPostmanMetrics.Messages, "A test gauge"); + metricService.incrementGauge(LineaPostmanMetrics.Messages, {}, 5); + metricService.decrementGauge(LineaPostmanMetrics.Messages, {}, 2); + expect(await metricService.getGaugeValue(LineaPostmanMetrics.Messages, {})).toBe(3); + }); + + it("should return the correct counter value", async () => { + metricService.createCounter(LineaPostmanMetrics.Messages, "A test counter"); + metricService.incrementCounter(LineaPostmanMetrics.Messages, {}, 5); + const counterValue = await metricService.getCounterValue(LineaPostmanMetrics.Messages, {}); + expect(counterValue).toBe(5); + }); + + it("should return the correct gauge value", async () => { + metricService.createGauge(LineaPostmanMetrics.Messages, "A test gauge"); + metricService.incrementGauge(LineaPostmanMetrics.Messages, {}, 10); + const gaugeValue = await metricService.getGaugeValue(LineaPostmanMetrics.Messages, {}); + expect(gaugeValue).toBe(10); + }); +}); diff --git a/postman/src/application/postman/app/PostmanServiceClient.ts b/postman/src/application/postman/app/PostmanServiceClient.ts index 7204cdf7..27ae2995 100644 --- a/postman/src/application/postman/app/PostmanServiceClient.ts +++ b/postman/src/application/postman/app/PostmanServiceClient.ts @@ -11,7 +11,7 @@ import { MessageSentEventProcessor, L2ClaimMessageTransactionSizeProcessor, } from "../../../services/processors"; -import { PostmanOptions } from "./config/config"; +import { PostmanConfig, PostmanOptions } from "./config/config"; import { DB } from "../persistence/dataSource"; import { MessageSentEventPoller, @@ -26,6 +26,9 @@ import { L2ClaimTransactionSizeCalculator } from "../../../services/L2ClaimTrans import { LineaTransactionValidationService } from "../../../services/LineaTransactionValidationService"; import { EthereumTransactionValidationService } from "../../../services/EthereumTransactionValidationService"; import { getConfig } from "./config/utils"; +import { Api } from "../api/Api"; +import { MessageStatusSubscriber } from "../persistence/subscribers/MessageStatusSubscriber"; +import { MessageMetricsService } from "../api/metrics/MessageMetricsService"; export class PostmanServiceClient { // L1 -> L2 flow @@ -49,6 +52,8 @@ export class PostmanServiceClient { private l1L2AutoClaimEnabled: boolean; private l2L1AutoClaimEnabled: boolean; + private api: Api; + private config: PostmanConfig; /** * Initializes a new instance of the PostmanServiceClient. @@ -57,6 +62,7 @@ export class PostmanServiceClient { */ constructor(options: PostmanOptions) { const config = getConfig(options); + this.config = config; this.logger = new WinstonLogger(PostmanServiceClient.name, config.loggerOptions); this.l1L2AutoClaimEnabled = config.l1L2AutoClaimEnabled; @@ -359,10 +365,50 @@ export class PostmanServiceClient { } /** - * Initializes the database connection using the configuration provided. + * Initializes the database connection. */ - public async connectDatabase() { - await this.db.initialize(); + public async initializeDatabase(): Promise { + try { + await this.db.initialize(); + this.logger.info("Database connection established successfully."); + } catch (error) { + this.logger.error("Failed to connect to the database.", error); + throw error; + } + } + + /** + * Initializes metrics, registers subscribers, and configures the API. + * This method expects the database to be connected. + */ + public async initializeMetricsAndApi(): Promise { + try { + const metricService = new MessageMetricsService(this.db.manager); + await metricService.initialize(); + + const messageStatusSubscriber = new MessageStatusSubscriber( + metricService, + new WinstonLogger(MessageStatusSubscriber.name), + ); + this.db.subscribers.push(messageStatusSubscriber); + + // Initialize or reinitialize the API using the metrics service. + this.api = new Api({ port: this.config.apiConfig.port }, metricService, new WinstonLogger(Api.name)); + + this.logger.info("Metrics and API have been initialized successfully."); + } catch (error) { + this.logger.error("Failed to initialize metrics or API.", error); + throw error; + } + } + + /** + * Connects services by first initializing the database and then setting up metrics and the API. + */ + public async connectServices(): Promise { + // Database initialization must happen before metrics initialization + await this.initializeDatabase(); + await this.initializeMetricsAndApi(); } /** @@ -389,6 +435,8 @@ export class PostmanServiceClient { // Database Cleaner this.databaseCleaningPoller.start(); + this.api.start(); + this.logger.info("All listeners and message deliverers have been started."); } @@ -416,6 +464,8 @@ export class PostmanServiceClient { // Database Cleaner this.databaseCleaningPoller.stop(); + this.api.stop(); + this.logger.info("All listeners and message deliverers have been stopped."); } } diff --git a/postman/src/application/postman/app/__tests__/PostmanServiceClient.test.ts b/postman/src/application/postman/app/__tests__/PostmanServiceClient.test.ts index e978b25c..5d69e4ce 100644 --- a/postman/src/application/postman/app/__tests__/PostmanServiceClient.test.ts +++ b/postman/src/application/postman/app/__tests__/PostmanServiceClient.test.ts @@ -26,6 +26,9 @@ import { DatabaseCleaningPoller } from "../../../../services/pollers/DatabaseCle import { TypeOrmMessageRepository } from "../../persistence/repositories/TypeOrmMessageRepository"; import { L2ClaimMessageTransactionSizePoller } from "../../../../services/pollers/L2ClaimMessageTransactionSizePoller"; import { DEFAULT_MAX_CLAIM_GAS_LIMIT } from "../../../../core/constants"; +import { MessageStatusSubscriber } from "../../persistence/subscribers/MessageStatusSubscriber"; +import { MessageMetricsService } from "../../api/metrics/MessageMetricsService"; +import { Api } from "../../api/Api"; jest.mock("ethers", () => { const allAutoMocked = jest.createMockFromModule("ethers"); @@ -171,8 +174,9 @@ describe("PostmanServiceClient", () => { }); }); - describe("connectDatabase", () => { - it("should initialize the db", async () => { + describe("connectServices", () => { + it("should initialize API and database", async () => { + jest.spyOn(MessageMetricsService.prototype, "initialize").mockResolvedValueOnce(); const initializeSpy = jest.spyOn(DataSource.prototype, "initialize").mockResolvedValue( new DataSource({ type: "postgres", @@ -190,19 +194,20 @@ describe("PostmanServiceClient", () => { RemoveUniqueConstraint1689084924789, AddNewIndexes1701265652528, ], + subscribers: [MessageStatusSubscriber], migrationsTableName: "migrations", logging: ["error"], migrationsRun: true, }), ); - await postmanServiceClient.connectDatabase(); + await postmanServiceClient.connectServices(); expect(initializeSpy).toHaveBeenCalledTimes(1); }); }); describe("startAllServices", () => { - it("should start all postman services", () => { + it("should start all postman services", async () => { jest.spyOn(MessageSentEventPoller.prototype, "start").mockImplementationOnce(jest.fn()); jest.spyOn(MessageAnchoringPoller.prototype, "start").mockImplementationOnce(jest.fn()); jest.spyOn(MessageClaimingPoller.prototype, "start").mockImplementationOnce(jest.fn()); @@ -210,27 +215,35 @@ describe("PostmanServiceClient", () => { jest.spyOn(MessagePersistingPoller.prototype, "start").mockImplementationOnce(jest.fn()); jest.spyOn(DatabaseCleaningPoller.prototype, "start").mockImplementationOnce(jest.fn()); jest.spyOn(TypeOrmMessageRepository.prototype, "getLatestMessageSent").mockImplementationOnce(jest.fn()); + jest.spyOn(Api.prototype, "start").mockImplementationOnce(jest.fn()); + + jest.spyOn(MessageMetricsService.prototype, "initialize").mockResolvedValueOnce(); + await postmanServiceClient.initializeMetricsAndApi(); postmanServiceClient.startAllServices(); - expect(loggerSpy).toHaveBeenCalledTimes(5); - expect(loggerSpy).toHaveBeenCalledWith("All listeners and message deliverers have been started."); + expect(loggerSpy).toHaveBeenCalledTimes(6); + expect(loggerSpy).toHaveBeenLastCalledWith("All listeners and message deliverers have been started."); postmanServiceClient.stopAllServices(); }); - it("should stop all postman services", () => { + it("should stop all postman services", async () => { jest.spyOn(MessageSentEventPoller.prototype, "stop").mockImplementationOnce(jest.fn()); jest.spyOn(MessageAnchoringPoller.prototype, "stop").mockImplementationOnce(jest.fn()); jest.spyOn(MessageClaimingPoller.prototype, "stop").mockImplementationOnce(jest.fn()); jest.spyOn(L2ClaimMessageTransactionSizePoller.prototype, "stop").mockImplementationOnce(jest.fn()); jest.spyOn(MessagePersistingPoller.prototype, "stop").mockImplementationOnce(jest.fn()); jest.spyOn(DatabaseCleaningPoller.prototype, "stop").mockImplementationOnce(jest.fn()); + jest.spyOn(Api.prototype, "stop").mockImplementationOnce(jest.fn()); + + jest.spyOn(MessageMetricsService.prototype, "initialize").mockResolvedValueOnce(); + await postmanServiceClient.initializeMetricsAndApi(); postmanServiceClient.stopAllServices(); - expect(loggerSpy).toHaveBeenCalledTimes(9); - expect(loggerSpy).toHaveBeenCalledWith("All listeners and message deliverers have been stopped."); + expect(loggerSpy).toHaveBeenCalledTimes(10); + expect(loggerSpy).toHaveBeenLastCalledWith("All listeners and message deliverers have been stopped."); }); }); }); diff --git a/postman/src/application/postman/app/config/__tests__/utils.test.ts b/postman/src/application/postman/app/config/__tests__/utils.test.ts index cfe6aa31..004cc966 100644 --- a/postman/src/application/postman/app/config/__tests__/utils.test.ts +++ b/postman/src/application/postman/app/config/__tests__/utils.test.ts @@ -133,6 +133,9 @@ describe("Config utils", () => { }, l2L1AutoClaimEnabled: false, loggerOptions: undefined, + apiConfig: { + port: 3000, + }, }); }); @@ -169,6 +172,9 @@ describe("Config utils", () => { databaseCleanerOptions: { enabled: true, }, + apiOptions: { + port: 9090, + }, }); expect(config).toStrictEqual({ databaseCleanerConfig: { @@ -242,6 +248,9 @@ describe("Config utils", () => { }, l2L1AutoClaimEnabled: true, loggerOptions: undefined, + apiConfig: { + port: 9090, + }, }); }); }); diff --git a/postman/src/application/postman/app/config/config.ts b/postman/src/application/postman/app/config/config.ts index 04572ea9..66a4fb9b 100644 --- a/postman/src/application/postman/app/config/config.ts +++ b/postman/src/application/postman/app/config/config.ts @@ -16,6 +16,7 @@ export type PostmanOptions = { databaseOptions: DBOptions; databaseCleanerOptions?: DBCleanerOptions; loggerOptions?: LoggerOptions; + apiOptions?: ApiOptions; }; /** @@ -29,6 +30,7 @@ export type PostmanConfig = { databaseOptions: DBOptions; databaseCleanerConfig: DBCleanerConfig; loggerOptions?: LoggerOptions; + apiConfig: ApiConfig; }; /** @@ -113,3 +115,9 @@ export type ListenerOptions = { export type ListenerConfig = Required> & Partial>; + +export type ApiOptions = { + port?: number; +}; + +export type ApiConfig = Required; diff --git a/postman/src/application/postman/app/config/utils.ts b/postman/src/application/postman/app/config/utils.ts index edfac096..79aa4c7a 100644 --- a/postman/src/application/postman/app/config/utils.ts +++ b/postman/src/application/postman/app/config/utils.ts @@ -38,6 +38,7 @@ export function getConfig(postmanOptions: PostmanOptions): PostmanConfig { databaseOptions, databaseCleanerOptions, loggerOptions, + apiOptions, } = postmanOptions; if (l1Options.listener.eventFilters) { @@ -124,6 +125,9 @@ export function getConfig(postmanOptions: PostmanOptions): PostmanConfig { daysBeforeNowToDelete: databaseCleanerOptions?.daysBeforeNowToDelete ?? 14, }, loggerOptions, + apiConfig: { + port: apiOptions?.port ?? 3000, + }, }; } diff --git a/postman/src/application/postman/persistence/repositories/TypeOrmMessageRepository.ts b/postman/src/application/postman/persistence/repositories/TypeOrmMessageRepository.ts index 82bc894a..18e39910 100644 --- a/postman/src/application/postman/persistence/repositories/TypeOrmMessageRepository.ts +++ b/postman/src/application/postman/persistence/repositories/TypeOrmMessageRepository.ts @@ -302,6 +302,13 @@ export class TypeOrmMessageRepository { + constructor( + private readonly metricsService: IMetricsService, + private readonly logger: ILogger, + ) {} + + listenTo() { + return MessageEntity; + } + + async afterInsert(event: InsertEvent): Promise { + const { status, direction } = event.entity; + this.updateMessageMetricsOnInsert(status, direction); + } + + async afterUpdate(event: UpdateEvent): Promise { + if (!event.entity || !event.databaseEntity) return; + + const prevStatus = event.databaseEntity.status; + const newStatus = event.entity.status; + const direction = event.databaseEntity.direction; + + if (prevStatus !== newStatus) { + await this.swapStatus( + LineaPostmanMetrics.Messages, + { status: prevStatus, direction }, + { status: newStatus, direction }, + ); + } + } + + async afterRemove(event: RemoveEvent): Promise { + if (event.entity) { + await this.updateMessageMetricsOnRemove(event.databaseEntity.status, event.databaseEntity.direction); + } + } + + async afterTransactionCommit(event: TransactionCommitEvent): Promise { + const updatedEntity = event.queryRunner?.data?.updatedEntity; + if (updatedEntity) { + await this.swapStatus( + LineaPostmanMetrics.Messages, + { status: updatedEntity.previousStatus, direction: updatedEntity.direction }, + { status: updatedEntity.newStatus, direction: updatedEntity.direction }, + ); + } + } + + private async updateMessageMetricsOnInsert(messageStatus: MessageStatus, messageDirection: Direction): Promise { + try { + const prevGaugeValue = await this.metricsService.getGaugeValue(LineaPostmanMetrics.Messages, { + status: messageStatus, + direction: messageDirection, + }); + + if (prevGaugeValue === undefined) { + return; + } + + this.metricsService.incrementGauge(LineaPostmanMetrics.Messages, { + status: messageStatus, + direction: messageDirection, + }); + } catch (error) { + this.logger.error("Failed to update metrics:", error); + } + } + + private async updateMessageMetricsOnRemove(messageStatus: MessageStatus, messageDirection: Direction): Promise { + try { + const prevGaugeValue = await this.metricsService.getGaugeValue(LineaPostmanMetrics.Messages, { + status: messageStatus, + direction: messageDirection, + }); + + if (prevGaugeValue && prevGaugeValue > 0) { + this.metricsService.decrementGauge(LineaPostmanMetrics.Messages, { + status: messageStatus, + direction: messageDirection, + }); + } + } catch (error) { + this.logger.error("Failed to update metrics:", error); + } + } + + private async swapStatus( + name: LineaPostmanMetrics, + previous: Record, + next: Record, + ): Promise { + try { + const [prevVal, newVal] = await Promise.all([ + this.metricsService.getGaugeValue(name, previous), + this.metricsService.getGaugeValue(name, next), + ]); + + if (prevVal && prevVal > 0) { + this.metricsService.decrementGauge(name, previous); + } + + if (newVal !== undefined) { + this.metricsService.incrementGauge(name, next); + } + } catch (error) { + this.logger.error("Metrics swap failed:", error); + } + } +} diff --git a/postman/src/core/metrics/IMetricsService.ts b/postman/src/core/metrics/IMetricsService.ts new file mode 100644 index 00000000..4f5bfb54 --- /dev/null +++ b/postman/src/core/metrics/IMetricsService.ts @@ -0,0 +1,16 @@ +import { Counter, Gauge, Registry } from "prom-client"; + +export enum LineaPostmanMetrics { + Messages = "linea_postman_messages", +} + +export interface IMetricsService { + getRegistry(): Registry; + createCounter(name: LineaPostmanMetrics, help: string, labelNames?: string[]): Counter; + createGauge(name: LineaPostmanMetrics, help: string, labelNames?: string[]): Gauge; + incrementCounter(name: LineaPostmanMetrics, labels?: Record, value?: number): void; + incrementGauge(name: LineaPostmanMetrics, labels?: Record, value?: number): void; + decrementGauge(name: LineaPostmanMetrics, labels?: Record, value?: number): void; + getGaugeValue(name: LineaPostmanMetrics, labels: Record): Promise; + getCounterValue(name: LineaPostmanMetrics, labels: Record): Promise; +} diff --git a/postman/src/services/processors/MessageClaimingProcessor.ts b/postman/src/services/processors/MessageClaimingProcessor.ts index b5ccf4f4..351dfb57 100644 --- a/postman/src/services/processors/MessageClaimingProcessor.ts +++ b/postman/src/services/processors/MessageClaimingProcessor.ts @@ -236,15 +236,17 @@ export class MessageClaimingProcessor implements IMessageClaimingProcessor { maxFeePerGas: bigint, ): Promise { if (isUnderPriced) { - this.logger.warn( - "Fee underpriced found in this message: messageHash=%s messageInfo=%s transactionGasLimit=%s maxFeePerGas=%s", - message.messageHash, - message.toString(), - estimatedGasLimit?.toString(), - maxFeePerGas.toString(), - ); - message.edit({ status: MessageStatus.FEE_UNDERPRICED }); - await this.databaseService.updateMessage(message); + if (message.status !== MessageStatus.FEE_UNDERPRICED) { + this.logger.warn( + "Fee underpriced found in this message: messageHash=%s messageInfo=%s transactionGasLimit=%s maxFeePerGas=%s", + message.messageHash, + message.toString(), + estimatedGasLimit?.toString(), + maxFeePerGas.toString(), + ); + message.edit({ status: MessageStatus.FEE_UNDERPRICED }); + await this.databaseService.updateMessage(message); + } return true; } return false;