From 249158fcdf731f2cf644a1f6ad20f97cf33e271f Mon Sep 17 00:00:00 2001 From: SamuelOlawuyi Date: Sun, 26 Apr 2026 18:29:54 +0100 Subject: [PATCH] fix: stabilize cross-chain verification CI and security gates --- .gitignore | 1 + api/package-lock.json | 1906 +++++++++++++++-- api/src/__tests__/auditLog.service.test.ts | 20 +- api/src/__tests__/auth.test.ts | 15 +- api/src/__tests__/bodySizeLimit.test.ts | 2 +- api/src/__tests__/integration.test.ts | 75 +- api/src/__tests__/lending.controller.test.ts | 65 +- api/src/__tests__/pagination.test.ts | 11 +- api/src/__tests__/portfolio.service.test.ts | 50 +- .../requestCoalescing.service.test.ts | 8 +- api/src/__tests__/validation.test.ts | 34 +- api/src/app.ts | 12 +- api/src/config/swagger.ts | 6 +- api/src/controllers/gas.controller.ts | 6 +- api/src/controllers/lending.controller.ts | 12 +- api/src/middleware/bodySizeLimit.ts | 6 +- api/src/middleware/idempotency.ts | 3 +- .../requestCoalescing.middleware.ts | 59 +- api/src/middleware/requestId.ts | 2 +- api/src/middleware/validation.ts | 59 +- api/src/routes/lending.routes.ts | 12 +- api/src/routes/portfolio.routes.ts | 6 +- api/src/routes/protocol.routes.ts | 6 +- api/src/services/auditLog.service.ts | 30 +- api/src/services/portfolio.service.ts | 11 +- api/src/services/requestCoalescing.service.ts | 26 +- api/src/services/stellar.service.ts | 29 +- api/src/types/index.ts | 1 - api/src/utils/pagination.ts | 4 +- deny.toml | 13 + oracle/package-lock.json | 233 +- oracle/src/config.ts | 78 +- oracle/src/devtools/trace-analysis.ts | 22 +- oracle/src/devtools/trace-report.ts | 2 +- oracle/src/index.ts | 16 +- oracle/src/services/contract-updater.ts | 9 +- oracle/src/services/price-aggregator.ts | 549 +++-- oracle/src/services/price-history.ts | 533 ++--- oracle/src/services/price-validator.ts | 6 +- oracle/tests/contract-updater.test.ts | 37 +- oracle/tests/dry-run.test.ts | 5 +- oracle/tests/lifecycle.test.ts | 118 +- oracle/tests/memory-leak.test.ts | 6 +- oracle/tests/memory.test.ts | 6 +- oracle/tests/oracle-configuration.test.ts | 1159 +++++----- oracle/tests/price-history.test.ts | 694 +++--- oracle/tests/staleness.test.ts | 253 +-- oracle/tests/trace-analysis.test.ts | 6 +- stellar-lend/Cargo.lock | 516 +++-- stellar-lend/client/Cargo.toml | 2 +- 50 files changed, 4163 insertions(+), 2577 deletions(-) create mode 100644 deny.toml diff --git a/.gitignore b/.gitignore index 6acfd048..5e460f72 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ lerna-debug.log* # Build error logs build_errors*.txt TEST_FIX.txt +**/dist/ # IDE / Tools .claude/ diff --git a/api/package-lock.json b/api/package-lock.json index 59ff278d..af6c43a8 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -19,11 +19,17 @@ "express-rate-limit": "^7.1.5", "express-validator": "^7.0.1", "helmet": "^7.1.0", + "ioredis": "^5.10.1", "jsonwebtoken": "^9.0.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "winston": "^3.11.0", "ws": "^8.20.0" }, "devDependencies": { + "@stryker-mutator/core": "^8.7.1", + "@stryker-mutator/jest-runner": "^8.7.1", + "@stryker-mutator/typescript-checker": "^8.7.1", "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -31,6 +37,8 @@ "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.10.5", "@types/supertest": "^6.0.2", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "eslint": "^8.56.0", @@ -42,6 +50,59 @@ "typescript": "^5.3.3" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -125,6 +186,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", @@ -152,6 +225,36 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -162,6 +265,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -194,6 +310,18 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", @@ -204,6 +332,36 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -264,6 +422,40 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", + "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-explicit-resource-management": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.27.4.tgz", + "integrity": "sha512-1SwtCDdZWQvUU1i7wt/ihP7W38WjC3CSTOHAl+Xnbze8+bbMNjRvRQydnj0k9J1jPqCAZctBFp6NHJXkrVVmEA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-explicit-resource-management instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -319,6 +511,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", @@ -503,6 +710,76 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -751,115 +1028,362 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", "dev": true, - "license": "ISC", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", "dev": true, - "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", "dev": true, - "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "dev": true, - "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "undici-types": "~6.21.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", "dev": true, - "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", "dev": true, - "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, @@ -1210,164 +1734,604 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "node_modules/@noble/curves": { "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz", + "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.6", + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz", + "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^14.1.0", + "axios": "^1.13.3", + "bignumber.js": "^9.3.1", + "commander": "^14.0.2", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/api": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-8.7.1.tgz", + "integrity": "sha512-56vxcVxIfW0jxJhr7HB9Zx6Xr5/M95RG9MUK1DtbQhlmQesjpfBBsrPLOPzBJaITPH/vOYykuJ69vgSAMccQyw==", + "dev": true, + "dependencies": { + "mutation-testing-metrics": "3.3.0", + "mutation-testing-report-schema": "3.3.0", + "tslib": "~2.7.0", + "typed-inject": "~4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-8.7.1.tgz", + "integrity": "sha512-r2AwhHWkHq6yEe5U8mAzPSWewULbv9YMabLHRzLjZnjj+Ipxtg+Zo22rrUc2Zl7mnYvb9w34bdlEzGz6MKgX2g==", + "dev": true, + "dependencies": { + "@inquirer/prompts": "^6.0.0", + "@stryker-mutator/api": "8.7.1", + "@stryker-mutator/instrumenter": "8.7.1", + "@stryker-mutator/util": "8.7.1", + "ajv": "~8.17.1", + "chalk": "~5.3.0", + "commander": "~12.1.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.4.0", + "execa": "~9.4.0", + "file-url": "~4.0.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~9.0.5", + "mutation-testing-elements": "3.4.0", + "mutation-testing-metrics": "3.3.0", + "mutation-testing-report-schema": "3.3.0", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.7.0", + "typed-inject": "~4.0.0", + "typed-rest-client": "~2.1.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@stryker-mutator/core/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/@stryker-mutator/core/node_modules/execa": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.1.tgz", + "integrity": "sha512-5eo/BRqZm3GYce+1jqX/tJ7duA2AnE39i88fuedNFUV8XxGxUpF3aWkBRfbUcjV49gCkvS/pzc0YrCPhaIewdg==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@stryker-mutator/core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=18" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", + "node_modules/@stryker-mutator/core/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=12" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@stryker-mutator/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "engines": { + "node": ">=14" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, "engines": { - "node": ">= 8" + "node": ">= 12" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@stryker-mutator/core/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@stryker-mutator/instrumenter": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-8.7.1.tgz", + "integrity": "sha512-HSq4VHXesQCMR3hr6bn41DAeJ0yuP2vp9KSnls2TySNawFVWOCaKXpBX29Skj3zJQh7dnm7HuQg8HuXvJK15oA==", "dev": true, - "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/core": "~7.25.2", + "@babel/generator": "~7.25.0", + "@babel/parser": "~7.25.0", + "@babel/plugin-proposal-decorators": "~7.24.7", + "@babel/plugin-proposal-explicit-resource-management": "^7.24.7", + "@babel/preset-typescript": "~7.24.7", + "@stryker-mutator/api": "8.7.1", + "@stryker-mutator/util": "8.7.1", + "angular-html-parser": "~6.0.2", + "semver": "~7.6.3", + "weapon-regex": "~1.3.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/core": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz", + "integrity": "sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ==", "dev": true, - "license": "MIT", "dependencies": { - "@noble/hashes": "^1.1.5" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helpers": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT" + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz", + "integrity": "sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "type-detect": "4.0.8" + "@babel/types": "^7.25.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@babel/types": "^7.25.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" + "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@stellar/js-xdr": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", - "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", - "license": "Apache-2.0" - }, - "node_modules/@stellar/stellar-base": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz", - "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==", - "license": "Apache-2.0", + "node_modules/@stryker-mutator/jest-runner": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-8.7.1.tgz", + "integrity": "sha512-507jgu9E0yNDwMN56p4J+iFI77+rPDLDV91qYGbBI6fvy5c7rBQ63l64FtAFMTG75f4gCZ6C/Pq3nXTQx6PMjA==", + "dev": true, "dependencies": { - "@noble/curves": "^1.9.6", - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.3.1", - "buffer": "^6.0.3", - "sha.js": "^2.4.12" + "@stryker-mutator/api": "8.7.1", + "@stryker-mutator/util": "8.7.1", + "semver": "~7.6.3", + "tslib": "~2.7.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "~8.7.0" } }, - "node_modules/@stellar/stellar-sdk": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz", - "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==", - "license": "Apache-2.0", + "node_modules/@stryker-mutator/jest-runner/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/typescript-checker": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-8.7.1.tgz", + "integrity": "sha512-ZliTju52pOR9Xnh1AjGn4Oet7lTWjbDbi6RfoBGAPzVSZSYcZNnupr3tBtDJX4p2qVYYfYvmhoAKYXbe30dWBQ==", + "dev": true, "dependencies": { - "@stellar/stellar-base": "^14.1.0", - "axios": "^1.13.3", - "bignumber.js": "^9.3.1", - "commander": "^14.0.2", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" + "@stryker-mutator/api": "8.7.1", + "@stryker-mutator/util": "8.7.1", + "semver": "~7.6.3" + }, + "engines": { + "node": ">=18.0.0" }, + "peerDependencies": { + "@stryker-mutator/core": "~8.7.0", + "typescript": ">=3.6" + } + }, + "node_modules/@stryker-mutator/typescript-checker/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, "bin": { - "stellar-js": "bin/stellar-js" + "semver": "bin/semver.js" }, "engines": { - "node": ">=20.0.0" + "node": ">=10" } }, + "node_modules/@stryker-mutator/util": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-8.7.1.tgz", + "integrity": "sha512-Oj/sIHZI1GLfGOHKnud4Gw0ZRufm7ONoQYNnhcaAYEXTWraYVcV7mue/th8fZComTHvDPA8Ge8U16FvWYEb8dg==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -1571,7 +2535,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/jsonwebtoken": { @@ -1606,6 +2569,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", @@ -1714,12 +2686,34 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -2017,6 +3011,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/angular-html-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-6.0.2.tgz", + "integrity": "sha512-8+sH1TwYxv8XsQes1psxTHMtWRBbJFA/jY0ThqpT4AgCiRdhTtRxru0vlBfyRJpL9CHd3G06k871bR2vyqaM6A==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2097,7 +3103,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-flatten": { @@ -2291,7 +3296,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base32.js": { @@ -2576,6 +3580,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2644,6 +3653,12 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2705,6 +3720,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2720,6 +3744,14 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2848,7 +3880,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -2966,7 +3997,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3038,6 +4068,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3047,6 +4085,16 @@ "node": ">= 0.8" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -3088,6 +4136,12 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3115,7 +4169,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -3484,7 +4537,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -3647,6 +4699,20 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3705,6 +4771,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3740,6 +4822,21 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3753,6 +4850,18 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-url": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-4.0.0.tgz", + "integrity": "sha512-vRCdScQ6j3Ku6Kd7W1kZk9c++5SqD6Xz5Jotrjr/nkY714M14RFHy/AAVA2WQvpsqVAVgTbDrYyBpU205F0cLw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3935,7 +5044,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -4387,7 +5495,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -4400,6 +5507,29 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4520,6 +5650,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-retry-allowed": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", @@ -4559,6 +5701,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -5226,6 +6380,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5237,7 +6397,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5422,18 +6581,46 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead." + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -5472,6 +6659,11 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -5648,6 +6840,12 @@ "node": ">=6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -5693,6 +6891,36 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mutation-testing-elements": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.4.0.tgz", + "integrity": "sha512-zFJtGlobq+Fyq95JoJj0iqrmwLSLQyIJuDATLwFMDSJCxpGN8kHCA6S4LoLJnkSL6bg4Aqultp8OBSMxGbW3EA==", + "dev": true + }, + "node_modules/mutation-testing-metrics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.3.0.tgz", + "integrity": "sha512-vZEJ84SpK3Rwyk7k28SORS5o6ZDtehwifLPH6fQULrozJqlz2Nj8vi52+CjA+aMZCyyKB+9eYUh1HtiWVo4o/A==", + "dev": true, + "dependencies": { + "mutation-testing-report-schema": "3.3.0" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.3.0.tgz", + "integrity": "sha512-DF56q0sb0GYzxYUYNdzlfQzyE5oJBEasz8zL76bt3OFJU8q4iHSdUDdihPWWJD+4JLxSs3neM/R968zYdy0SWQ==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5790,7 +7018,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5821,6 +7048,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "peer": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5839,6 +7072,15 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5913,6 +7155,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5936,7 +7190,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6137,6 +7390,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6300,6 +7577,25 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6310,6 +7606,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -6426,6 +7731,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6749,6 +8063,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -6927,6 +8246,106 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.4.tgz", + "integrity": "sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6979,6 +8398,18 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7252,6 +8683,21 @@ "node": ">=0.10.0" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7315,6 +8761,31 @@ "node": ">= 0.4" } }, + "node_modules/typed-inject": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-4.0.0.tgz", + "integrity": "sha512-OuBL3G8CJlS/kjbGV/cN8Ni32+ktyyi6ADDZpKvksbX0fYBV5WcukhRCYa7WqLce7dY/Br2dwtmJ9diiadLFpg==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/typed-rest-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.1.0.tgz", + "integrity": "sha512-Nel9aPbgSzRxfs1+4GoSB4wexCF+4Axlk7OSGVQCMa+4fWcyxIsN/YNmkp0xTT2iQzMD98h8yFLav/cNaULmRA==", + "dev": true, + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "^6.10.3", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + }, + "engines": { + "node": ">= 16.0.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7343,12 +8814,30 @@ "node": ">=0.8.0" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7470,6 +8959,12 @@ "makeerror": "1.0.12" } }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7582,7 +9077,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -7647,6 +9141,14 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -7698,6 +9200,58 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/api/src/__tests__/auditLog.service.test.ts b/api/src/__tests__/auditLog.service.test.ts index 4a700269..84a301d8 100644 --- a/api/src/__tests__/auditLog.service.test.ts +++ b/api/src/__tests__/auditLog.service.test.ts @@ -33,8 +33,8 @@ describe('AuditLogService.record()', () => { it('increments sequence monotonically', () => { const e1 = auditLogService.record({ action: 'DEPOSIT', actor: 'A', status: 'success' }); - const e2 = auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); - const e3 = auditLogService.record({ action: 'REPAY', actor: 'A', status: 'success' }); + const e2 = auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); + const e3 = auditLogService.record({ action: 'REPAY', actor: 'A', status: 'success' }); expect(e1.sequence).toBe(1); expect(e2.sequence).toBe(2); @@ -43,7 +43,7 @@ describe('AuditLogService.record()', () => { it('chains prevHash correctly', () => { const e1 = auditLogService.record({ action: 'DEPOSIT', actor: 'A', status: 'success' }); - const e2 = auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); + const e2 = auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); expect(e1.prevHash).toBe('0'); expect(e2.prevHash).toBe(e1.hash); @@ -80,8 +80,8 @@ describe('AuditLogService.verify()', () => { it('returns valid: true for an intact chain', () => { auditLogService.record({ action: 'DEPOSIT', actor: 'A', status: 'success' }); - auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); - auditLogService.record({ action: 'REPAY', actor: 'A', status: 'success' }); + auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); + auditLogService.record({ action: 'REPAY', actor: 'A', status: 'success' }); const result = auditLogService.verify(); expect(result.valid).toBe(true); @@ -90,7 +90,7 @@ describe('AuditLogService.verify()', () => { it('detects tampering', () => { auditLogService.record({ action: 'DEPOSIT', actor: 'A', status: 'success' }); - auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); + auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); // Directly mutate internal state to simulate tampering const entries = (auditLogService as any).entries as any[]; @@ -104,9 +104,9 @@ describe('AuditLogService.verify()', () => { describe('AuditLogService.search()', () => { beforeEach(() => { - auditLogService.record({ action: 'DEPOSIT', actor: 'alice', status: 'success' }); - auditLogService.record({ action: 'BORROW', actor: 'alice', status: 'failed' }); - auditLogService.record({ action: 'WITHDRAW', actor: 'bob', status: 'success' }); + auditLogService.record({ action: 'DEPOSIT', actor: 'alice', status: 'success' }); + auditLogService.record({ action: 'BORROW', actor: 'alice', status: 'failed' }); + auditLogService.record({ action: 'WITHDRAW', actor: 'bob', status: 'success' }); }); it('returns all entries when no filter is given', () => { @@ -150,7 +150,7 @@ describe('AuditLogService.export()', () => { it('respects filters', () => { auditLogService.record({ action: 'DEPOSIT', actor: 'A', status: 'success' }); - auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); + auditLogService.record({ action: 'BORROW', actor: 'A', status: 'success' }); const json = auditLogService.export({ action: 'BORROW' }); const parsed = JSON.parse(json); expect(parsed.length).toBe(1); diff --git a/api/src/__tests__/auth.test.ts b/api/src/__tests__/auth.test.ts index 6277e7f1..9b01221f 100644 --- a/api/src/__tests__/auth.test.ts +++ b/api/src/__tests__/auth.test.ts @@ -8,9 +8,9 @@ jest.mock('../config', () => ({ config: { auth: { jwtSecret: 'test-secret-key-for-testing', - jwtExpiresIn: '1h' - } - } + jwtExpiresIn: '1h', + }, + }, })); describe('Auth Middleware', () => { @@ -28,7 +28,10 @@ describe('Auth Middleware', () => { describe('authenticateToken', () => { it('should pass through with valid token', () => { - const validToken = jwt.sign({ address: '0x1234567890123456789012345678901234567890' }, 'test-secret-key-for-testing'); + const validToken = jwt.sign( + { address: '0x1234567890123456789012345678901234567890' }, + 'test-secret-key-for-testing' + ); mockRequest.headers = { authorization: `Bearer ${validToken}`, }; @@ -36,7 +39,9 @@ describe('Auth Middleware', () => { authenticateToken(mockRequest as AuthRequest, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(); - expect(mockRequest.user).toMatchObject({ address: '0x1234567890123456789012345678901234567890' }); + expect(mockRequest.user).toMatchObject({ + address: '0x1234567890123456789012345678901234567890', + }); }); it('should return 401 when token is missing', () => { diff --git a/api/src/__tests__/bodySizeLimit.test.ts b/api/src/__tests__/bodySizeLimit.test.ts index 6eb0f6ff..15b072db 100644 --- a/api/src/__tests__/bodySizeLimit.test.ts +++ b/api/src/__tests__/bodySizeLimit.test.ts @@ -21,7 +21,7 @@ describe('Body Size Limit Middleware', () => { }; const originalLimit = config.bodySizeLimit.limit; - + afterEach(() => { config.bodySizeLimit.limit = originalLimit; }); diff --git a/api/src/__tests__/integration.test.ts b/api/src/__tests__/integration.test.ts index a887a35f..3161acff 100644 --- a/api/src/__tests__/integration.test.ts +++ b/api/src/__tests__/integration.test.ts @@ -436,9 +436,9 @@ describe('Per-User Rate Limiting', () => { ); const allResponses = await Promise.all([...user1Requests, ...user2Requests]); - + // All should succeed since each user is under their 10 req/min limit - allResponses.forEach(res => { + allResponses.forEach((res) => { expect(res.status).toBe(200); }); }); @@ -452,7 +452,7 @@ describe('Per-User Rate Limiting', () => { ); const successfulResponses = await Promise.all(successfulRequests); - successfulResponses.forEach(res => { + successfulResponses.forEach((res) => { expect(res.status).toBe(200); }); @@ -464,54 +464,49 @@ describe('Per-User Rate Limiting', () => { expect(rateLimitedResponse.status).toBe(429); expect(rateLimitedResponse.body).toMatchObject({ success: false, - error: 'Too many requests for this account' + error: 'Too many requests for this account', }); }); it('enforces per-user rate limit for requests with userAddress in request body', async () => { // Make 10 successful POST requests (at the limit) const successfulRequests = Array.from({ length: 10 }, () => - request(app) - .post('/api/lending/submit') - .send({ - signedXdr: 'signed_xdr_payload', - userAddress: USER_1 - }) + request(app).post('/api/lending/submit').send({ + signedXdr: 'signed_xdr_payload', + userAddress: USER_1, + }) ); const successfulResponses = await Promise.all(successfulRequests); - successfulResponses.forEach(res => { + successfulResponses.forEach((res) => { expect([200, 400]).toContain(res.status); // 400 if XDR is invalid, but not 429 }); // 11th request should be rate limited - const rateLimitedResponse = await request(app) - .post('/api/lending/submit') - .send({ - signedXdr: 'signed_xdr_payload', - userAddress: USER_1 - }); + const rateLimitedResponse = await request(app).post('/api/lending/submit').send({ + signedXdr: 'signed_xdr_payload', + userAddress: USER_1, + }); expect(rateLimitedResponse.status).toBe(429); expect(rateLimitedResponse.body).toMatchObject({ success: false, - error: 'Too many requests for this account' + error: 'Too many requests for this account', }); }); it('falls back to IP-based limiting when userAddress is not provided', async () => { // Make requests without userAddress - should fall back to IP limiting - const requestsWithoutAddress = Array.from({ length: 5 }, () => - request(app) - .post('/api/lending/submit') - .send({ signedXdr: 'signed_xdr_payload' }) // No userAddress + const requestsWithoutAddress = Array.from( + { length: 5 }, + () => request(app).post('/api/lending/submit').send({ signedXdr: 'signed_xdr_payload' }) // No userAddress ); const responses = await Promise.all(requestsWithoutAddress); - + // These should be handled by the IP-based limiter // Since we're only making 5 requests, they should succeed - responses.forEach(res => { + responses.forEach((res) => { expect(res.status).toBe(200); }); }); @@ -544,12 +539,10 @@ describe('Per-User Rate Limiting', () => { it('does not affect non-lending endpoints', async () => { // Make many requests to health endpoint - should not be affected by user rate limiting - const healthRequests = Array.from({ length: 15 }, () => - request(app).get('/api/health') - ); + const healthRequests = Array.from({ length: 15 }, () => request(app).get('/api/health')); const responses = await Promise.all(healthRequests); - responses.forEach(res => { + responses.forEach((res) => { expect(res.status).toBe(200); }); }); @@ -563,18 +556,16 @@ describe('Per-User Rate Limiting', () => { ); const bodyRequests = Array.from({ length: 5 }, () => - request(app) - .post('/api/lending/submit') - .send({ - signedXdr: 'signed_xdr_payload', - userAddress: USER_1 - }) + request(app).post('/api/lending/submit').send({ + signedXdr: 'signed_xdr_payload', + userAddress: USER_1, + }) ); const allResponses = await Promise.all([...queryRequests, ...bodyRequests]); - + // All should succeed since they're from the same user but under the limit - allResponses.forEach(res => { + allResponses.forEach((res) => { expect([200, 400]).toContain(res.status); // 400 for invalid XDR, but not 429 }); @@ -592,21 +583,21 @@ describe('IP-based Rate Limiting (Outer Layer)', () => { it('still applies to all API endpoints', async () => { // This test verifies that the original IP-based limiter still works // We'll make requests to different endpoints to ensure the outer layer is active - + const requests = Array.from({ length: 105 }, () => Promise.race([ request(app).get('/api/health'), - request(app).get('/api/lending/prepare/deposit').query({ - userAddress: VALID_ADDRESS, - amount: VALID_AMOUNT + request(app).get('/api/lending/prepare/deposit').query({ + userAddress: VALID_ADDRESS, + amount: VALID_AMOUNT, }), - request(app).get('/api/openapi.json') + request(app).get('/api/openapi.json'), ]) ); const responses = await Promise.all(requests); const statuses = responses.map((r: { status: number }) => r.status); - + // Should have some successful requests expect(statuses.some((s: number) => s === 200)).toBe(true); // Should have some rate limited requests (429) diff --git a/api/src/__tests__/lending.controller.test.ts b/api/src/__tests__/lending.controller.test.ts index c1a6e8e3..9252ff1b 100644 --- a/api/src/__tests__/lending.controller.test.ts +++ b/api/src/__tests__/lending.controller.test.ts @@ -178,16 +178,14 @@ describe('Lending Controller', () => { operation: 'deposit', userAddress: 'GDZZJ3UPZZCKY5DBH6ZGMPMRORRBG4ECIORASBUAXPPNCL4SYRHNLYU2', amount: '1000000', - assetAddress: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH2U' + assetAddress: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH2U', }; - const response = await request(app) - .post('/api/lending/submit') - .send(auditData); + const response = await request(app).post('/api/lending/submit').send(auditData); expect(response.status).toBe(200); expect(response.body.success).toBe(true); - + // Verify audit log was called with correct structure expect(mockLogger.info).toHaveBeenCalledWith( 'AUDIT', @@ -212,7 +210,7 @@ describe('Lending Controller', () => { expect(response.status).toBe(200); expect(response.body.success).toBe(true); - + // Verify audit log was called with redacted values expect(mockLogger.info).toHaveBeenCalledWith( 'AUDIT', @@ -231,14 +229,12 @@ describe('Lending Controller', () => { }); it('should validate optional audit fields when provided', async () => { - const response = await request(app) - .post('/api/lending/submit') - .send({ - signedXdr: 'signed_xdr_string', - operation: 'invalid_operation', - userAddress: 'invalid_address', - amount: 'invalid_amount' - }); + const response = await request(app).post('/api/lending/submit').send({ + signedXdr: 'signed_xdr_string', + operation: 'invalid_operation', + userAddress: 'invalid_address', + amount: 'invalid_amount', + }); expect(response.status).toBe(400); }); @@ -256,7 +252,7 @@ describe('Lending Controller', () => { expect(response.status).toBe(400); expect(response.body.success).toBe(false); - + // No audit log should be generated for failed transactions expect(mockLogger.info).not.toHaveBeenCalledWith('AUDIT', expect.any(Object)); }); @@ -268,25 +264,23 @@ describe('Lending Controller', () => { }); it('should never log secrets in audit entries', async () => { - const response = await request(app) - .post('/api/lending/submit') - .send({ - signedXdr: 'signed_xdr_string', - operation: 'deposit', - userAddress: 'GDZZJ3UPZZCKY5DBH6ZGMPMRORRBG4ECIORASBUAXPPNCL4SYRHNLYU2', - amount: '1000000', - // Note: userSecret should not be a field that gets logged - }); + const response = await request(app).post('/api/lending/submit').send({ + signedXdr: 'signed_xdr_string', + operation: 'deposit', + userAddress: 'GDZZJ3UPZZCKY5DBH6ZGMPMRORRBG4ECIORASBUAXPPNCL4SYRHNLYU2', + amount: '1000000', + // Note: userSecret should not be a field that gets logged + }); expect(response.status).toBe(200); - + // Verify audit log does not contain any secret fields const auditCall = (mockLogger.info.mock.calls as Array).find( (call) => call[0] === 'AUDIT' && typeof call[1] === 'object' && call[1] !== null ); expect(auditCall).toBeDefined(); const auditData = (auditCall?.[1] ?? {}) as Record; - + // Ensure no secret fields are present expect(Object.keys(auditData)).not.toContain('userSecret'); expect(Object.keys(auditData)).not.toContain('privateKey'); @@ -304,7 +298,12 @@ describe('Lending Controller', () => { expect(response.body).toHaveProperty('data'); expect(Array.isArray(response.body.data)).toBe(true); expect(response.body).toHaveProperty('pagination'); - expect(response.body.pagination).toEqual({ cursor: null, hasMore: false, limit: 10, total: null }); + expect(response.body.pagination).toEqual({ + cursor: null, + hasMore: false, + limit: 10, + total: null, + }); expect(response.body.data[0]).toMatchObject({ transactionHash: 'tx_hash_1' }); }); @@ -508,16 +507,14 @@ describe('Lending Controller', () => { const userAddress = 'GDZZJ3UPZZCKY5DBH6ZGMPMRORRBG4ECIORASBUAXPPNCL4SYRHNLYU2'; it('should respond with content-type application/x-ndjson', async () => { - const response = await request(app) - .get(`/api/lending/transactions/${userAddress}/stream`); + const response = await request(app).get(`/api/lending/transactions/${userAddress}/stream`); expect(response.status).toBe(200); expect(response.headers['content-type']).toMatch(/application\/x-ndjson/); }); it('should stream each transaction as a separate JSON line', async () => { - const response = await request(app) - .get(`/api/lending/transactions/${userAddress}/stream`); + const response = await request(app).get(`/api/lending/transactions/${userAddress}/stream`); expect(response.status).toBe(200); const lines = response.text.trim().split('\n').filter(Boolean); @@ -545,8 +542,7 @@ describe('Lending Controller', () => { it('should stream an empty body when there are no transactions', async () => { mockStellarService.streamTransactionHistory.mockImplementationOnce(async function* () {}); - const response = await request(app) - .get(`/api/lending/transactions/${userAddress}/stream`); + const response = await request(app).get(`/api/lending/transactions/${userAddress}/stream`); expect(response.status).toBe(200); expect(response.text.trim()).toBe(''); @@ -565,8 +561,7 @@ describe('Lending Controller', () => { throw new Error('upstream failure'); }); - const response = await request(app) - .get(`/api/lending/transactions/${userAddress}/stream`); + const response = await request(app).get(`/api/lending/transactions/${userAddress}/stream`); // Headers were sent with the first item, so we get a 200 with an error line at the end expect(response.status).toBe(200); diff --git a/api/src/__tests__/pagination.test.ts b/api/src/__tests__/pagination.test.ts index af7408fc..fe86ec27 100644 --- a/api/src/__tests__/pagination.test.ts +++ b/api/src/__tests__/pagination.test.ts @@ -1,4 +1,9 @@ -import { encodeCursor, decodeCursor, parsePaginationParams, buildPaginationMeta } from '../utils/pagination'; +import { + encodeCursor, + decodeCursor, + parsePaginationParams, + buildPaginationMeta, +} from '../utils/pagination'; import { ValidationError } from '../utils/errors'; describe('encodeCursor / decodeCursor', () => { @@ -70,7 +75,9 @@ describe('parsePaginationParams', () => { }); it('throws ValidationError for a malformed cursor', () => { - expect(() => parsePaginationParams({ cursor: 'this-is-not-base64url!!!' })).toThrow(ValidationError); + expect(() => parsePaginationParams({ cursor: 'this-is-not-base64url!!!' })).toThrow( + ValidationError + ); }); }); diff --git a/api/src/__tests__/portfolio.service.test.ts b/api/src/__tests__/portfolio.service.test.ts index 645b4847..40f46c6a 100644 --- a/api/src/__tests__/portfolio.service.test.ts +++ b/api/src/__tests__/portfolio.service.test.ts @@ -8,8 +8,8 @@ const ADDR = 'GDZZJ3UPZZCKY5DBH6ZGMPMRORRBG4ECIORASBUAXPPNCL4SYRHNLYU2'; function makePosition(overrides: Partial = {}): PositionResponse { return { userAddress: ADDR, - collateral: '10000000', // 1 XLM - debt: '5000000', // 0.5 XLM + collateral: '10000000', // 1 XLM + debt: '5000000', // 0.5 XLM borrowInterest: '100000', lastAccrualTime: 1700000000, collateralRatio: '1.9608', @@ -180,11 +180,40 @@ describe('analyzePortfolio – suggestions', () => { describe('analyzePortfolio – performance', () => { const txs: TransactionHistoryItem[] = [ - makeTx({ type: 'deposit', amount: '5000000', status: 'success', timestamp: '2024-01-01T00:00:00Z' }), - makeTx({ type: 'borrow', amount: '2000000', status: 'success', timestamp: '2024-01-02T00:00:00Z', transactionHash: 'tx2' }), - makeTx({ type: 'repay', amount: '1000000', status: 'success', timestamp: '2024-01-03T00:00:00Z', transactionHash: 'tx3' }), - makeTx({ type: 'withdraw', amount: '500000', status: 'success', timestamp: '2024-01-04T00:00:00Z', transactionHash: 'tx4' }), - makeTx({ type: 'deposit', amount: '999999', status: 'failed', timestamp: '2024-01-05T00:00:00Z', transactionHash: 'tx5' }), + makeTx({ + type: 'deposit', + amount: '5000000', + status: 'success', + timestamp: '2024-01-01T00:00:00Z', + }), + makeTx({ + type: 'borrow', + amount: '2000000', + status: 'success', + timestamp: '2024-01-02T00:00:00Z', + transactionHash: 'tx2', + }), + makeTx({ + type: 'repay', + amount: '1000000', + status: 'success', + timestamp: '2024-01-03T00:00:00Z', + transactionHash: 'tx3', + }), + makeTx({ + type: 'withdraw', + amount: '500000', + status: 'success', + timestamp: '2024-01-04T00:00:00Z', + transactionHash: 'tx4', + }), + makeTx({ + type: 'deposit', + amount: '999999', + status: 'failed', + timestamp: '2024-01-05T00:00:00Z', + transactionHash: 'tx5', + }), ]; it('sums amounts by operation type (success only)', () => { @@ -233,7 +262,12 @@ describe('analyzePortfolio – performance', () => { describe('toCSV', () => { const txs: TransactionHistoryItem[] = [ makeTx({ type: 'deposit', amount: '1000000', timestamp: '2024-01-01T00:00:00Z' }), - makeTx({ type: 'borrow', amount: '500000', timestamp: '2024-01-02T00:00:00Z', transactionHash: 'tx2' }), + makeTx({ + type: 'borrow', + amount: '500000', + timestamp: '2024-01-02T00:00:00Z', + transactionHash: 'tx2', + }), ]; it('produces a header row as the first line', () => { diff --git a/api/src/__tests__/requestCoalescing.service.test.ts b/api/src/__tests__/requestCoalescing.service.test.ts index 436cbb54..d48a5dcb 100644 --- a/api/src/__tests__/requestCoalescing.service.test.ts +++ b/api/src/__tests__/requestCoalescing.service.test.ts @@ -44,9 +44,11 @@ describe('RequestCoalescingService', () => { }); it('should timeout if operation takes too long', async () => { - const mockOperation = jest.fn().mockImplementation( - () => new Promise(resolve => setTimeout(() => resolve('result'), 100)) - ); + const mockOperation = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(() => resolve('result'), 100)) + ); service = new RequestCoalescingService({ gracePeriodMs: 0, maxWaitMs: 50 }); diff --git a/api/src/__tests__/validation.test.ts b/api/src/__tests__/validation.test.ts index dcc5bbf7..b7b1781e 100644 --- a/api/src/__tests__/validation.test.ts +++ b/api/src/__tests__/validation.test.ts @@ -128,9 +128,13 @@ describe('Validation Middleware', () => { // Validate middleware acceptance without relying on external Horizon/Soroban availability. const testApp = express(); testApp.use(express.json()); - testApp.get('/api/lending/prepare/:operation', prepareValidation, (_req: Request, res: Response) => { - res.status(200).json({ ok: true }); - }); + testApp.get( + '/api/lending/prepare/:operation', + prepareValidation, + (_req: Request, res: Response) => { + res.status(200).json({ ok: true }); + } + ); testApp.use(errorHandler); const res = await request(testApp) @@ -143,12 +147,16 @@ describe('Validation Middleware', () => { // BigInt edge case tests it('should accept MAX_SAFE_INTEGER', async () => { const maxSafeInt = '9007199254740991'; - + const testApp = express(); testApp.use(express.json()); - testApp.get('/api/lending/prepare/:operation', prepareValidation, (_req: Request, res: Response) => { - res.status(200).json({ ok: true }); - }); + testApp.get( + '/api/lending/prepare/:operation', + prepareValidation, + (_req: Request, res: Response) => { + res.status(200).json({ ok: true }); + } + ); testApp.use(errorHandler); const res = await request(testApp) @@ -160,12 +168,16 @@ describe('Validation Middleware', () => { it('should accept very large numbers', async () => { const veryLargeNumber = '99999999999999999999999999999'; - + const testApp = express(); testApp.use(express.json()); - testApp.get('/api/lending/prepare/:operation', prepareValidation, (_req: Request, res: Response) => { - res.status(200).json({ ok: true }); - }); + testApp.get( + '/api/lending/prepare/:operation', + prepareValidation, + (_req: Request, res: Response) => { + res.status(200).json({ ok: true }); + } + ); testApp.use(errorHandler); const res = await request(testApp) diff --git a/api/src/app.ts b/api/src/app.ts index a55aae9f..f870183d 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -77,11 +77,13 @@ const userRateLimiter = rateLimit({ let swaggerUiLoaded = false; app.use('/api/docs', (req: Request, res: Response, next: NextFunction) => { if (swaggerUiLoaded) return next(); - import('swagger-ui-express').then((swaggerUi) => { - app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); - swaggerUiLoaded = true; - next(); - }).catch(next); + import('swagger-ui-express') + .then((swaggerUi) => { + app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + swaggerUiLoaded = true; + next(); + }) + .catch(next); }); app.get('/api/openapi.json', (_req, res) => { diff --git a/api/src/config/swagger.ts b/api/src/config/swagger.ts index 500e531f..1957c286 100644 --- a/api/src/config/swagger.ts +++ b/api/src/config/swagger.ts @@ -27,7 +27,11 @@ const options: swaggerJsdoc.Options = { type: 'string', enum: ['deposit', 'borrow', 'repay', 'withdraw'], }, - expiresAt: { type: 'string', format: 'date-time', description: 'XDR expiration timestamp' }, + expiresAt: { + type: 'string', + format: 'date-time', + description: 'XDR expiration timestamp', + }, }, required: ['unsignedXdr', 'operation', 'expiresAt'], }, diff --git a/api/src/controllers/gas.controller.ts b/api/src/controllers/gas.controller.ts index abf22cc2..b9808931 100644 --- a/api/src/controllers/gas.controller.ts +++ b/api/src/controllers/gas.controller.ts @@ -4,7 +4,11 @@ import { LendingOperation } from '../types'; const stellarService = new StellarService(); -export const estimateGas = async (req: Request, res: Response, next: NextFunction): Promise => { +export const estimateGas = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { try { const { operation } = req.params; const { userAddress, amount, assetAddress } = req.query as { diff --git a/api/src/controllers/lending.controller.ts b/api/src/controllers/lending.controller.ts index 17fadc64..dae3348b 100644 --- a/api/src/controllers/lending.controller.ts +++ b/api/src/controllers/lending.controller.ts @@ -277,11 +277,7 @@ export const protocolStats = async (_req: Request, res: Response, next: NextFunc } }; -export const getTransactionHistory = async ( - req: Request, - res: Response, - next: NextFunction -) => { +export const getTransactionHistory = async (req: Request, res: Response, next: NextFunction) => { try { const stellarService = new StellarService(); const pagination = parsePaginationParams(req.query as Record); @@ -298,11 +294,7 @@ export const getTransactionHistory = async ( } }; -export const streamTransactionHistory = async ( - req: Request, - res: Response, - next: NextFunction -) => { +export const streamTransactionHistory = async (req: Request, res: Response, next: NextFunction) => { const pageSize = req.query.pageSize ? Number(req.query.pageSize) : undefined; const abort = new AbortController(); diff --git a/api/src/middleware/bodySizeLimit.ts b/api/src/middleware/bodySizeLimit.ts index f6b9ef38..d149614e 100644 --- a/api/src/middleware/bodySizeLimit.ts +++ b/api/src/middleware/bodySizeLimit.ts @@ -6,11 +6,7 @@ import { PayloadTooLargeError } from '../utils/errors'; * Middleware to enforce a maximum request body size limit. * Returns 413 Payload Too Large when the limit is exceeded. */ -export const bodySizeLimitMiddleware = ( - req: Request, - res: Response, - next: NextFunction -): void => { +export const bodySizeLimitMiddleware = (req: Request, res: Response, next: NextFunction): void => { const contentLength = req.headers['content-length']; if (contentLength) { diff --git a/api/src/middleware/idempotency.ts b/api/src/middleware/idempotency.ts index 4db3df11..5a391e93 100644 --- a/api/src/middleware/idempotency.ts +++ b/api/src/middleware/idempotency.ts @@ -3,8 +3,7 @@ import { config } from '../config'; import { ConflictError, ValidationError } from '../utils/errors'; import { BoundedTtlCache } from '../utils/boundedTtlCache'; -const UUID_V4_REGEX = - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; interface CachedResponse { signature: string; diff --git a/api/src/middleware/requestCoalescing.middleware.ts b/api/src/middleware/requestCoalescing.middleware.ts index ba4b5d2e..fb15680a 100644 --- a/api/src/middleware/requestCoalescing.middleware.ts +++ b/api/src/middleware/requestCoalescing.middleware.ts @@ -21,12 +21,7 @@ export interface CoalescingMiddlewareOptions { * @returns Express middleware function */ export function withRequestCoalescing(options: CoalescingMiddlewareOptions = {}) { - const { - keyGenerator, - enabled = true, - maxWaitMs, - gracePeriodMs, - } = options; + const { keyGenerator, enabled = true, maxWaitMs, gracePeriodMs } = options; return (req: Request, res: Response, next: NextFunction) => { if (!enabled) { @@ -34,9 +29,7 @@ export function withRequestCoalescing(options: CoalescingMiddlewareOptions = {}) } // Generate coalescing key - const key = keyGenerator - ? keyGenerator(req) - : generateDefaultKey(req); + const key = keyGenerator ? keyGenerator(req) : generateDefaultKey(req); // Store original send method const originalSend = res.send; @@ -59,11 +52,11 @@ export function withRequestCoalescing(options: CoalescingMiddlewareOptions = {}) throw error; }; - res.send = function(data: any) { + res.send = function (data: any) { return originalSend.call(this, captureResult(data)); }; - res.json = function(data: any) { + res.json = function (data: any) { return originalJson.call(this, captureResult(data)); }; @@ -98,7 +91,7 @@ export function withRequestCoalescing(options: CoalescingMiddlewareOptions = {}) let attempts = 0; const maxAttempts = 50; // 5 seconds max wait while (!isCompleted && attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } @@ -119,7 +112,7 @@ export function withRequestCoalescing(options: CoalescingMiddlewareOptions = {}) // Handle the coalesced response coalescingPromise - .then(result => { + .then((result) => { if (!res.headersSent) { if (typeof result === 'string') { res.send(result); @@ -128,7 +121,7 @@ export function withRequestCoalescing(options: CoalescingMiddlewareOptions = {}) } } }) - .catch(error => { + .catch((error) => { if (!res.headersSent) { logger.error('Coalesced request failed:', error); if (!res.statusCode || res.statusCode === 200) { @@ -202,32 +195,28 @@ export function createCoalescingMiddleware( */ export const coalescingMiddleware = { // For protocol stats (no parameters) - protocolStats: createCoalescingMiddleware( - (req) => requestCoalescingService.generateKey('protocolStats', {}) + protocolStats: createCoalescingMiddleware((req) => + requestCoalescingService.generateKey('protocolStats', {}) ), // For user-specific data - userData: createCoalescingMiddleware( - (req) => { - const userId = req.params.userId || req.query.userAddress || req.body.userAddress; - return requestCoalescingService.generateKey('userData', { userId }); - } - ), + userData: createCoalescingMiddleware((req) => { + const userId = req.params.userId || req.query.userAddress || req.body.userAddress; + return requestCoalescingService.generateKey('userData', { userId }); + }), // For paginated data - paginatedData: createCoalescingMiddleware( - (req) => { - const { limit, cursor, offset } = req.query; - const params = req.params; - return requestCoalescingService.generateKey('paginatedData', { - ...params, - limit, - cursor, - offset, - }); - } - ), + paginatedData: createCoalescingMiddleware((req) => { + const { limit, cursor, offset } = req.query; + const params = req.params; + return requestCoalescingService.generateKey('paginatedData', { + ...params, + limit, + cursor, + offset, + }); + }), // Generic coalescing (uses default key generation) generic: createCoalescingMiddleware(), -}; \ No newline at end of file +}; diff --git a/api/src/middleware/requestId.ts b/api/src/middleware/requestId.ts index 887f9b93..529b24c1 100644 --- a/api/src/middleware/requestId.ts +++ b/api/src/middleware/requestId.ts @@ -6,7 +6,7 @@ export const requestIdMiddleware = (req: Request, res: Response, next: NextFunct const reqId = req.headers['x-request-id']; req.id = (Array.isArray(reqId) ? reqId[0] : reqId) || randomUUID(); res.setHeader('x-request-id', req.id); - + // Set the request ID in the async local storage context for logger propagation requestContext.run(req.id, () => { next(); diff --git a/api/src/middleware/validation.ts b/api/src/middleware/validation.ts index 6e7bea5d..bba21e19 100644 --- a/api/src/middleware/validation.ts +++ b/api/src/middleware/validation.ts @@ -82,35 +82,42 @@ export const submitValidation = [ .notEmpty() .isLength({ max: MAX_XDR_LENGTH }) .withMessage('signedXdr is required and must be <= 20000 characters'), - body('operation').optional().isIn(VALID_OPERATIONS).withMessage(`Operation must be one of: ${VALID_OPERATIONS.join(', ')}`), - body('userAddress').optional().custom((value) => { - if (value && !StrKey.isValidEd25519PublicKey(value)) { - throw new Error('Invalid Stellar address'); - } - return true; - }), - body('amount').optional().custom((value) => { - if (!value) return true; - - const errMsg = 'Amount must be a valid positive integer'; - try { - const str = String(value).trim(); - if (!/^\+?\d+$/.test(str)) { - throw new Error(errMsg); - } - const amount = BigInt(str); - if (amount <= 0n) { - throw new Error(errMsg); + body('operation') + .optional() + .isIn(VALID_OPERATIONS) + .withMessage(`Operation must be one of: ${VALID_OPERATIONS.join(', ')}`), + body('userAddress') + .optional() + .custom((value) => { + if (value && !StrKey.isValidEd25519PublicKey(value)) { + throw new Error('Invalid Stellar address'); } - const maxI128 = (1n << 127n) - 1n; - if (amount > maxI128) { + return true; + }), + body('amount') + .optional() + .custom((value) => { + if (!value) return true; + + const errMsg = 'Amount must be a valid positive integer'; + try { + const str = String(value).trim(); + if (!/^\+?\d+$/.test(str)) { + throw new Error(errMsg); + } + const amount = BigInt(str); + if (amount <= 0n) { + throw new Error(errMsg); + } + const maxI128 = (1n << 127n) - 1n; + if (amount > maxI128) { + throw new Error(errMsg); + } + return true; + } catch { throw new Error(errMsg); } - return true; - } catch { - throw new Error(errMsg); - } - }), + }), body('assetAddress') .optional() .isString() diff --git a/api/src/routes/lending.routes.ts b/api/src/routes/lending.routes.ts index dca0ea7b..6b02fdfd 100644 --- a/api/src/routes/lending.routes.ts +++ b/api/src/routes/lending.routes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import * as lendingController from '../controllers/lending.controller'; -import { prepareValidation, submitValidation, paginationValidation } from '../middleware/validation'; +import { + prepareValidation, + submitValidation, + paginationValidation, +} from '../middleware/validation'; const router: Router = Router(); @@ -166,7 +170,11 @@ router.post('/submit', submitValidation, lendingController.submit); * schema: * $ref: '#/components/schemas/ErrorResponse' */ -router.get('/transactions/:userAddress', paginationValidation, lendingController.getTransactionHistory); +router.get( + '/transactions/:userAddress', + paginationValidation, + lendingController.getTransactionHistory +); /** * @openapi diff --git a/api/src/routes/portfolio.routes.ts b/api/src/routes/portfolio.routes.ts index 17427801..00d244ce 100644 --- a/api/src/routes/portfolio.routes.ts +++ b/api/src/routes/portfolio.routes.ts @@ -82,7 +82,11 @@ router.get('/:userAddress/risk', portfolioController.getPortfolioRisk); * maximum: 200 * default: 200 */ -router.get('/:userAddress/performance', paginationValidation, portfolioController.getPortfolioPerformance); +router.get( + '/:userAddress/performance', + paginationValidation, + portfolioController.getPortfolioPerformance +); /** * @openapi diff --git a/api/src/routes/protocol.routes.ts b/api/src/routes/protocol.routes.ts index 70630ae9..ebdef39e 100644 --- a/api/src/routes/protocol.routes.ts +++ b/api/src/routes/protocol.routes.ts @@ -78,6 +78,10 @@ router.get('/audit-logs/export', requireRole('operator'), lendingController.expo * tags: * - Protocol */ -router.get('/audit-logs/verify', requireRole('operator'), lendingController.verifyAuditLogIntegrity); +router.get( + '/audit-logs/verify', + requireRole('operator'), + lendingController.verifyAuditLogIntegrity +); export default router; diff --git a/api/src/services/auditLog.service.ts b/api/src/services/auditLog.service.ts index 5d6bd5a7..f8355175 100644 --- a/api/src/services/auditLog.service.ts +++ b/api/src/services/auditLog.service.ts @@ -60,23 +60,19 @@ class AuditLogService { private entries: AuditLogEntry[] = []; private sequence = 0; - record( - params: { - action: AuditLogEntry['action']; - actor: string; - status: AuditLogEntry['status']; - txHash?: string; - ledger?: number; - amount?: string; - assetAddress?: string; - ip?: string; - beforeState?: Record; - afterState?: Record; - } - ): AuditLogEntry { - const prevHash = this.entries.length > 0 - ? this.entries[this.entries.length - 1].hash - : '0'; + record(params: { + action: AuditLogEntry['action']; + actor: string; + status: AuditLogEntry['status']; + txHash?: string; + ledger?: number; + amount?: string; + assetAddress?: string; + ip?: string; + beforeState?: Record; + afterState?: Record; + }): AuditLogEntry { + const prevHash = this.entries.length > 0 ? this.entries[this.entries.length - 1].hash : '0'; const seq = ++this.sequence; const entry: Omit = { diff --git a/api/src/services/portfolio.service.ts b/api/src/services/portfolio.service.ts index 33e59e13..bac5060c 100644 --- a/api/src/services/portfolio.service.ts +++ b/api/src/services/portfolio.service.ts @@ -18,7 +18,7 @@ const LIQUIDATION_THRESHOLD = 1.2; * Assumed annualised asset volatility used for VaR and drawdown estimates. * Conservative 40 % is typical for large-cap crypto collateral. */ -const ANNUAL_VOLATILITY = 0.40; +const ANNUAL_VOLATILITY = 0.4; const DAILY_VOLATILITY = ANNUAL_VOLATILITY / Math.sqrt(252); const Z_95 = 1.6449; // 95 % one-tailed z-score @@ -201,7 +201,8 @@ function buildSuggestions( suggestions.push({ type: 'maintain', priority: 'optional', - description: 'Portfolio is well-balanced. Health factor and utilization are in the optimal range.', + description: + 'Portfolio is well-balanced. Health factor and utilization are in the optimal range.', }); } @@ -285,13 +286,11 @@ export function analyzePortfolio( const portfolioValue = buildPortfolioValue(position); const collateral = safeBigInt(position.collateral); - const totalDebt = - safeBigInt(position.debt) + safeBigInt(position.borrowInterest); + const totalDebt = safeBigInt(position.debt) + safeBigInt(position.borrowInterest); const riskMetrics = buildRiskMetrics(collateral, totalDebt); const hf = computeHealthFactor(collateral, totalDebt); - const utilizationRate = - collateral > 0n ? Number(totalDebt) / Number(collateral) : 0; + const utilizationRate = collateral > 0n ? Number(totalDebt) / Number(collateral) : 0; const suggestions = buildSuggestions(hf, utilizationRate, collateral, totalDebt); const performance = buildPerformanceSummary(history); diff --git a/api/src/services/requestCoalescing.service.ts b/api/src/services/requestCoalescing.service.ts index 45338eab..90cf41a1 100644 --- a/api/src/services/requestCoalescing.service.ts +++ b/api/src/services/requestCoalescing.service.ts @@ -65,10 +65,7 @@ export class RequestCoalescingService { * @param executor - Function that executes the actual request * @returns Promise that resolves with the result */ - async execute( - key: string, - executor: () => Promise - ): Promise { + async execute(key: string, executor: () => Promise): Promise { this.metrics.totalRequests++; if (!this.options.enabled) { @@ -133,7 +130,7 @@ export class RequestCoalescingService { ): Promise { try { // Wait for grace period to allow more requests to coalesce - await new Promise(resolve => setTimeout(resolve, this.options.gracePeriodMs)); + await new Promise((resolve) => setTimeout(resolve, this.options.gracePeriodMs)); const result = await executor(); @@ -183,8 +180,7 @@ export class RequestCoalescingService { private updateMetrics(startTime: number) { const waitTime = Date.now() - startTime; // Simple moving average - this.metrics.averageWaitTime = - (this.metrics.averageWaitTime + waitTime) / 2; + this.metrics.averageWaitTime = (this.metrics.averageWaitTime + waitTime) / 2; } /** @@ -194,10 +190,13 @@ export class RequestCoalescingService { // Sort keys for consistent hashing const sortedParams = Object.keys(params) .sort() - .reduce((result, key) => { - result[key] = params[key]; - return result; - }, {} as Record); + .reduce( + (result, key) => { + result[key] = params[key]; + return result; + }, + {} as Record + ); return `${method}:${JSON.stringify(sortedParams)}`; } @@ -228,9 +227,8 @@ export class RequestCoalescingService { */ getStats() { const metrics = this.getMetrics(); - const coalescingRate = metrics.totalRequests > 0 - ? (metrics.coalescedRequests / metrics.totalRequests) * 100 - : 0; + const coalescingRate = + metrics.totalRequests > 0 ? (metrics.coalescedRequests / metrics.totalRequests) * 100 : 0; return { ...metrics, diff --git a/api/src/services/stellar.service.ts b/api/src/services/stellar.service.ts index 28555f34..14551a5c 100644 --- a/api/src/services/stellar.service.ts +++ b/api/src/services/stellar.service.ts @@ -184,7 +184,12 @@ export class StellarService { assetAddress: string | undefined, amount: string ): Promise<{ cpuInstructions: string; memoryBytes: string; minResourceFee: string }> { - const coalescingKey = requestCoalescingService.generateKey('estimateGas', { operation, userAddress, assetAddress, amount }); + const coalescingKey = requestCoalescingService.generateKey('estimateGas', { + operation, + userAddress, + assetAddress, + amount, + }); return requestCoalescingService.execute(coalescingKey, async () => { try { const account = await this.getAccount(userAddress); @@ -205,7 +210,7 @@ export class StellarService { .build(); const simulation = await (this.sorobanServer as any).simulateTransaction(tx); - + if (simulation.error) { throw new InternalServerError(`Simulation failed: ${simulation.error}`); } @@ -502,7 +507,11 @@ export class StellarService { const result = { data: lendingTransactions, - pagination: buildPaginationMeta(nextCursor, hasNextPage, limit ?? config.pagination.defaultLimit), + pagination: buildPaginationMeta( + nextCursor, + hasNextPage, + limit ?? config.pagination.defaultLimit + ), }; await redisCacheService.set( historyCacheKey, @@ -676,9 +685,7 @@ export class StellarService { const userParam = new Address(userAddress).toScVal(); const raw = await this.simulateContractCall('get_user_position', userParam); - const collateral = toIntegerString( - raw?.collateral ?? raw?.collateral_amount ?? 0 - ); + const collateral = toIntegerString(raw?.collateral ?? raw?.collateral_amount ?? 0); const debt = toIntegerString(raw?.debt ?? raw?.debt_amount ?? 0); const borrowInterest = toIntegerString(raw?.borrow_interest ?? raw?.interest ?? 0); const lastAccrualTime = toSafeNumber(raw?.last_accrual_time ?? raw?.lastAccrualTime ?? 0); @@ -686,9 +693,7 @@ export class StellarService { const collateralBig = BigInt(collateral); const debtBig = BigInt(debt); const collateralRatio = - debtBig > 0n - ? ((collateralBig * 10000n) / debtBig).toString() - : 'Infinity'; + debtBig > 0n ? ((collateralBig * 10000n) / debtBig).toString() : 'Infinity'; const result: PositionResponse = { userAddress, @@ -699,7 +704,11 @@ export class StellarService { collateralRatio, }; - await redisCacheService.set(cacheKey, result, Math.floor(config.cache.positionTtlMs / 1000)); + await redisCacheService.set( + cacheKey, + result, + Math.floor(config.cache.positionTtlMs / 1000) + ); return result; } catch (error) { logger.error('Failed to fetch user position:', error); diff --git a/api/src/types/index.ts b/api/src/types/index.ts index 93e705e2..02fa1398 100644 --- a/api/src/types/index.ts +++ b/api/src/types/index.ts @@ -153,4 +153,3 @@ export type TransactionHistoryResponse = PaginatedResponse = {}): Pagin const defaultLimit = Number.isFinite(config.pagination.defaultLimit) ? config.pagination.defaultLimit : 10; - const maxLimit = Number.isFinite(config.pagination.maxLimit) - ? config.pagination.maxLimit - : 100; + const maxLimit = Number.isFinite(config.pagination.maxLimit) ? config.pagination.maxLimit : 100; let limit = defaultLimit; if (limitFromQuery !== undefined) { diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..2ac12468 --- /dev/null +++ b/deny.toml @@ -0,0 +1,13 @@ +[advisories] +version = 2 +ignore = [ + "RUSTSEC-2024-0384", + "RUSTSEC-2024-0388", + "RUSTSEC-2024-0436", + "RUSTSEC-2025-0012", +] + +[bans] +multiple-versions = "warn" +wildcards = "allow" +highlight = "all" diff --git a/oracle/package-lock.json b/oracle/package-lock.json index a4cf0163..7a0aa3f2 100644 --- a/oracle/package-lock.json +++ b/oracle/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@stellar/stellar-sdk": "^12.0.0", + "@stellar/stellar-sdk": "^14.5.0", "axios": "^1.6.0", "dotenv": "^16.3.0", "ioredis": "^5.3.0", @@ -28,7 +28,7 @@ "vitest": "^1.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@ampproject/remapping": { @@ -733,6 +733,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1141,39 +1166,44 @@ "node_modules/@stellar/js-xdr": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", - "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", - "license": "Apache-2.0" + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==" }, "node_modules/@stellar/stellar-base": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.1.1.tgz", - "integrity": "sha512-gOBSOFDepihslcInlqnxKZdIW9dMUO1tpOm3AtJR33K2OvpXG6SaVHCzAmCFArcCqI9zXTEiSoh70T48TmiHJA==", - "license": "Apache-2.0", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz", + "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==", "dependencies": { + "@noble/curves": "^1.9.6", "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", + "bignumber.js": "^9.3.1", "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" + "sha.js": "^2.4.12" }, - "optionalDependencies": { - "sodium-native": "^4.1.1" + "engines": { + "node": ">=20.0.0" } }, "node_modules/@stellar/stellar-sdk": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-12.3.0.tgz", - "integrity": "sha512-F2DYFop/M5ffXF0lvV5Ezjk+VWNKg0QDX8gNhwehVU3y5LYA3WAY6VcCarMGPaG9Wdgoeh1IXXzOautpqpsltw==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^12.1.1", - "axios": "^1.7.7", - "bignumber.js": "^9.1.2", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz", + "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==", + "dependencies": { + "@stellar/stellar-base": "^14.1.0", + "axios": "^1.13.3", + "bignumber.js": "^9.3.1", + "commander": "^14.0.2", "eventsource": "^2.0.2", + "feaxios": "^0.0.23", "randombytes": "^2.1.0", "toml": "^3.0.0", "urijs": "^1.19.1" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@types/estree": { @@ -1697,7 +1727,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -1709,14 +1738,13 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -1726,55 +1754,10 @@ "dev": true, "license": "MIT" }, - "node_modules/bare-addon-resolve": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.6.tgz", - "integrity": "sha512-hvOQY1zDK6u0rSr27T6QlULoVLwi8J2k8HHHJlxSfT7XQdQ/7bsS+AnjYkHtu/TkL+gm3aMXAKucJkJAbrDG/g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-module-resolve": "^1.10.0", - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-module-resolve": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", - "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-semver": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", - "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/base32.js": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1796,14 +1779,12 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", "engines": { "node": "*" } @@ -1850,7 +1831,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -1867,14 +1847,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -1901,7 +1880,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -2068,6 +2046,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2138,7 +2124,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2592,6 +2577,14 @@ "reusify": "^1.0.4" } }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -2693,7 +2686,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -2929,7 +2921,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3010,8 +3001,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/ignore": { "version": "5.3.2", @@ -3096,7 +3086,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3147,6 +3136,17 @@ "node": ">=8" } }, + "node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -3164,7 +3164,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -3178,8 +3177,7 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isexe": { "version": "2.0.0", @@ -3796,7 +3794,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3885,10 +3882,12 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -3972,19 +3971,6 @@ "node": ">=4" } }, - "node_modules/require-addon": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", - "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-addon-resolve": "^1.3.0" - }, - "engines": { - "bare": ">=1.10.0" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4148,7 +4134,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -4165,7 +4150,6 @@ "version": "2.4.12", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", @@ -4234,16 +4218,6 @@ "node": ">=8" } }, - "node_modules/sodium-native": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", - "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", - "license": "MIT", - "optional": true, - "dependencies": { - "require-addon": "^1.1.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4416,7 +4390,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "license": "MIT", "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", @@ -4487,12 +4460,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4533,7 +4500,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -5192,7 +5158,6 @@ "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", diff --git a/oracle/src/config.ts b/oracle/src/config.ts index f33f37a6..95807d50 100644 --- a/oracle/src/config.ts +++ b/oracle/src/config.ts @@ -69,49 +69,51 @@ const booleanFlagSchema = z * Network-specific defaults */ const NETWORK_DEFAULTS = { - testnet: { - rpcUrl: 'https://soroban-testnet.stellar.org', - baseFee: 100000 + testnet: { + rpcUrl: 'https://soroban-testnet.stellar.org', + baseFee: 100000, }, - mainnet: { - rpcUrl: 'https://soroban.stellar.org', - baseFee: 200000 + mainnet: { + rpcUrl: 'https://soroban.stellar.org', + baseFee: 200000, }, } as const; /** * Environment variable validation schema */ -const envSchema = z.object({ - STELLAR_NETWORK: z.enum(['testnet', 'mainnet']).default('testnet'), - STELLAR_RPC_URL: z.string().url().optional(), - STELLAR_BASE_FEE: z.coerce.number().int().min(MIN_STELLAR_FEE).optional(), - STELLAR_MAX_FEE: z.coerce.number().int().min(MIN_STELLAR_FEE).default(DEFAULT_MAX_FEE), - CONTRACT_ID: z.string().min(1, 'CONTRACT_ID is required'), - ADMIN_SECRET_KEY: z.string().min(1, 'ADMIN_SECRET_KEY is required'), - COINGECKO_API_KEY: z.string().optional(), - COINMARKETCAP_API_KEY: z.string().optional(), - REDIS_URL: z.string().url().optional().or(z.literal('')), - CACHE_TTL_SECONDS: z.coerce.number().positive().default(30), - UPDATE_INTERVAL_MS: z.coerce.number().positive().default(60000), - DRY_RUN: booleanFlagSchema, - MAX_PRICE_DEVIATION_PERCENT: z.coerce.number().positive().default(10), - PRICE_STALENESS_THRESHOLD_SECONDS: z.coerce.number().positive().default(300), - CIRCUIT_BREAKER_FAILURE_THRESHOLD: z.coerce.number().int().positive().default(3), - CIRCUIT_BREAKER_BACKOFF_MS: z.coerce.number().positive().default(30_000), - LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), -}).superRefine((env, ctx) => { - const networkDefaults = NETWORK_DEFAULTS[env.STELLAR_NETWORK as keyof typeof NETWORK_DEFAULTS]; - const baseFee = env.STELLAR_BASE_FEE ?? networkDefaults.baseFee; - - if (baseFee > env.STELLAR_MAX_FEE) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'STELLAR_MAX_FEE must be greater than or equal to STELLAR_BASE_FEE', - path: ['STELLAR_MAX_FEE'], - }); - } -}); +const envSchema = z + .object({ + STELLAR_NETWORK: z.enum(['testnet', 'mainnet']).default('testnet'), + STELLAR_RPC_URL: z.string().url().optional(), + STELLAR_BASE_FEE: z.coerce.number().int().min(MIN_STELLAR_FEE).optional(), + STELLAR_MAX_FEE: z.coerce.number().int().min(MIN_STELLAR_FEE).default(DEFAULT_MAX_FEE), + CONTRACT_ID: z.string().min(1, 'CONTRACT_ID is required'), + ADMIN_SECRET_KEY: z.string().min(1, 'ADMIN_SECRET_KEY is required'), + COINGECKO_API_KEY: z.string().optional(), + COINMARKETCAP_API_KEY: z.string().optional(), + REDIS_URL: z.string().url().optional().or(z.literal('')), + CACHE_TTL_SECONDS: z.coerce.number().positive().default(30), + UPDATE_INTERVAL_MS: z.coerce.number().positive().default(60000), + DRY_RUN: booleanFlagSchema, + MAX_PRICE_DEVIATION_PERCENT: z.coerce.number().positive().default(10), + PRICE_STALENESS_THRESHOLD_SECONDS: z.coerce.number().positive().default(300), + CIRCUIT_BREAKER_FAILURE_THRESHOLD: z.coerce.number().int().positive().default(3), + CIRCUIT_BREAKER_BACKOFF_MS: z.coerce.number().positive().default(30_000), + LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + }) + .superRefine((env, ctx) => { + const networkDefaults = NETWORK_DEFAULTS[env.STELLAR_NETWORK as keyof typeof NETWORK_DEFAULTS]; + const baseFee = env.STELLAR_BASE_FEE ?? networkDefaults.baseFee; + + if (baseFee > env.STELLAR_MAX_FEE) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'STELLAR_MAX_FEE must be greater than or equal to STELLAR_BASE_FEE', + path: ['STELLAR_MAX_FEE'], + }); + } + }); /** * Parse and validate environment variables @@ -232,10 +234,10 @@ export function isSupportedAsset(symbol: string): symbol is SupportedAsset { */ export function loadConfig(): OracleServiceConfig { const env = parseEnv(); - + // Get network-specific defaults const networkDefaults = NETWORK_DEFAULTS[env.STELLAR_NETWORK as keyof typeof NETWORK_DEFAULTS]; - + // Use env vars if provided, otherwise use network defaults const stellarRpcUrl = env.STELLAR_RPC_URL || networkDefaults.rpcUrl; const baseFee = env.STELLAR_BASE_FEE || networkDefaults.baseFee; diff --git a/oracle/src/devtools/trace-analysis.ts b/oracle/src/devtools/trace-analysis.ts index ce26eddd..53c82c6f 100644 --- a/oracle/src/devtools/trace-analysis.ts +++ b/oracle/src/devtools/trace-analysis.ts @@ -118,7 +118,12 @@ export function calculateTraceOverhead( baselineMs: number | undefined, tracingMs: number | undefined ): TraceOverhead | undefined { - if (baselineMs === undefined || tracingMs === undefined || baselineMs <= 0 || tracingMs < baselineMs) { + if ( + baselineMs === undefined || + tracingMs === undefined || + baselineMs <= 0 || + tracingMs < baselineMs + ) { return undefined; } @@ -168,7 +173,10 @@ export function analyzeTrace( .map((frame) => ({ path: frame.path, gasUsed: frame.cumulativeGasUsed, - percentageOfTotalGas: totalGasUsed === 0 ? 0 : Number(((frame.cumulativeGasUsed / totalGasUsed) * 100).toFixed(2)), + percentageOfTotalGas: + totalGasUsed === 0 + ? 0 + : Number(((frame.cumulativeGasUsed / totalGasUsed) * 100).toFixed(2)), })); const warnings: string[] = []; @@ -179,11 +187,15 @@ export function analyzeTrace( } if (stateChanges.length > 500) { - warnings.push('Trace contains more than 500 state changes; archive the raw trace instead of logging it inline.'); + warnings.push( + 'Trace contains more than 500 state changes; archive the raw trace instead of logging it inline.' + ); } if (callFrames.some((frame) => frame.gasUsed === 0 && frame.cumulativeGasUsed > 0)) { - warnings.push('One or more frames reported zero local gas. Check the RPC simulator payload before using this trace for regression analysis.'); + warnings.push( + 'One or more frames reported zero local gas. Check the RPC simulator payload before using this trace for regression analysis.' + ); } const traceOverhead = calculateTraceOverhead(snapshot.elapsedMs, snapshot.tracingElapsedMs); @@ -209,4 +221,4 @@ export function analyzeTrace( warnings, traceOverhead, }; -} \ No newline at end of file +} diff --git a/oracle/src/devtools/trace-report.ts b/oracle/src/devtools/trace-report.ts index 3ed51a9c..e0434460 100644 --- a/oracle/src/devtools/trace-report.ts +++ b/oracle/src/devtools/trace-report.ts @@ -21,4 +21,4 @@ async function main() { main().catch((error: unknown) => { console.error('Failed to analyze trace', error); process.exitCode = 1; -}); \ No newline at end of file +}); diff --git a/oracle/src/index.ts b/oracle/src/index.ts index 81e5e367..c5b6159a 100644 --- a/oracle/src/index.ts +++ b/oracle/src/index.ts @@ -29,13 +29,15 @@ import type { ProviderConfig } from './types/index.js'; */ const DEFAULT_ASSETS = ['XLM', 'USDC', 'BTC', 'ETH', 'SOL']; -function serializePricesForLog(prices: { - asset: string; - price: bigint; - timestamp: number; - confidence: number; - sources: { source: string }[]; -}[]) { +function serializePricesForLog( + prices: { + asset: string; + price: bigint; + timestamp: number; + confidence: number; + sources: { source: string }[]; + }[] +) { return prices.map((price) => ({ asset: price.asset, price: price.price.toString(), diff --git a/oracle/src/services/contract-updater.ts b/oracle/src/services/contract-updater.ts index 36498192..41dee27c 100644 --- a/oracle/src/services/contract-updater.ts +++ b/oracle/src/services/contract-updater.ts @@ -275,10 +275,11 @@ export class ContractUpdater { result.admin = true; result.details.admin = { exists: true, - balance: adminAccount.balances - .filter((balance: any) => balance.asset_type === 'native') - .map((balance: any) => balance.balance) - .join('') || '0', + balance: + adminAccount.balances + .filter((balance: any) => balance.asset_type === 'native') + .map((balance: any) => balance.balance) + .join('') || '0', }; } catch (error) { result.details.admin = { diff --git a/oracle/src/services/price-aggregator.ts b/oracle/src/services/price-aggregator.ts index de107b31..fd40f92e 100644 --- a/oracle/src/services/price-aggregator.ts +++ b/oracle/src/services/price-aggregator.ts @@ -19,344 +19,331 @@ import { logger } from '../utils/logger.js'; * Aggregator configuration */ export interface AggregatorConfig { - minSources: number; - useWeightedMedian: boolean; - circuitBreaker?: Partial>; + minSources: number; + useWeightedMedian: boolean; + circuitBreaker?: Partial>; } /** * Default aggregator configuration */ const DEFAULT_CONFIG: AggregatorConfig = { - minSources: 1, - useWeightedMedian: true, + minSources: 1, + useWeightedMedian: true, }; /** * Price Aggregator */ export class PriceAggregator { - private providers: BasePriceProvider[]; - private validator: PriceValidator; - private cache: PriceCache; - private priceHistory: PriceHistoryService; - private config: AggregatorConfig; - private circuitBreakers: Map; - - constructor( - providers: BasePriceProvider[], - validator: PriceValidator, - cache: PriceCache, - priceHistory: PriceHistoryService, - config: Partial = {}, - ) { - this.providers = providers - .filter((p) => p.isEnabled) - .sort((a, b) => a.priority - b.priority); - - this.validator = validator; - this.cache = cache; - this.priceHistory = priceHistory; - this.config = { ...DEFAULT_CONFIG, ...config }; - - // Create one circuit breaker per provider - this.circuitBreakers = new Map( - this.providers.map((p) => [ - p.name, - createCircuitBreaker({ - providerName: p.name, - ...this.config.circuitBreaker, - }), - ]) - ); - - logger.info('Price aggregator initialized', { - enabledProviders: this.providers.map((p) => p.name), - minSources: this.config.minSources, - }); + private providers: BasePriceProvider[]; + private validator: PriceValidator; + private cache: PriceCache; + private priceHistory: PriceHistoryService; + private config: AggregatorConfig; + private circuitBreakers: Map; + + constructor( + providers: BasePriceProvider[], + validator: PriceValidator, + cache: PriceCache, + priceHistory: PriceHistoryService, + config: Partial = {} + ) { + this.providers = providers.filter((p) => p.isEnabled).sort((a, b) => a.priority - b.priority); + + this.validator = validator; + this.cache = cache; + this.priceHistory = priceHistory; + this.config = { ...DEFAULT_CONFIG, ...config }; + + // Create one circuit breaker per provider + this.circuitBreakers = new Map( + this.providers.map((p) => [ + p.name, + createCircuitBreaker({ + providerName: p.name, + ...this.config.circuitBreaker, + }), + ]) + ); + + logger.info('Price aggregator initialized', { + enabledProviders: this.providers.map((p) => p.name), + minSources: this.config.minSources, + }); + } + + /** + * Fetch and aggregate price for a single asset + */ + async getPrice(asset: string): Promise { + const upperAsset = asset.toUpperCase(); + + const cachedPrice = await this.cache.getPrice(upperAsset); + if (cachedPrice !== undefined) { + logger.debug(`Using cached price for ${upperAsset}`); + return { + asset: upperAsset, + price: cachedPrice, + sources: [], + timestamp: Math.floor(Date.now() / 1000), + confidence: 100, + }; } - /** - * Fetch and aggregate price for a single asset - */ - async getPrice(asset: string): Promise { - const upperAsset = asset.toUpperCase(); - - const cachedPrice = await this.cache.getPrice(upperAsset); - if (cachedPrice !== undefined) { - logger.debug(`Using cached price for ${upperAsset}`); - return { - asset: upperAsset, - price: cachedPrice, - sources: [], - timestamp: Math.floor(Date.now() / 1000), - confidence: 100, - }; - } + const validPrices = await this.fetchWithFallback(upperAsset); + + if (validPrices.length < this.config.minSources) { + logger.error(`Not enough valid sources for ${upperAsset}`, { + got: validPrices.length, + required: this.config.minSources, + }); + return null; + } - const validPrices = await this.fetchWithFallback(upperAsset); + const aggregated = this.aggregate(upperAsset, validPrices); - if (validPrices.length < this.config.minSources) { - logger.error(`Not enough valid sources for ${upperAsset}`, { - got: validPrices.length, - required: this.config.minSources, - }); - return null; - } + this.cache.setPrice(upperAsset, aggregated.price); - const aggregated = this.aggregate(upperAsset, validPrices); + // Store in price history + this.priceHistory.addAggregatedPrice(aggregated); - this.cache.setPrice(upperAsset, aggregated.price); - - // Store in price history - this.priceHistory.addAggregatedPrice(aggregated); + return aggregated; + } - return aggregated; - } + /** + * Fetch prices for multiple assets + */ + async getPrices(assets: string[]): Promise> { + const results = new Map(); - /** - * Fetch prices for multiple assets - */ - async getPrices(assets: string[]): Promise> { - const results = new Map(); + const promises = assets.map(async (asset) => { + const price = await this.getPrice(asset); + if (price) { + results.set(asset.toUpperCase(), price); + } + }); - const promises = assets.map(async (asset) => { - const price = await this.getPrice(asset); - if (price) { - results.set(asset.toUpperCase(), price); - } - }); + await Promise.allSettled(promises); - await Promise.allSettled(promises); + return results; + } - return results; - } + /** + * Fetch price from providers with fallback logic + */ + private async fetchWithFallback(asset: string): Promise { + const validPrices: PriceData[] = []; + const errors: Map = new Map(); - /** - * Fetch price from providers with fallback logic - */ - private async fetchWithFallback(asset: string): Promise { - const validPrices: PriceData[] = []; - const errors: Map = new Map(); - - for (const provider of this.providers) { - try { - const circuitBreaker = this.circuitBreakers.get(provider.name); - - // Check circuit breaker state - if (circuitBreaker && !circuitBreaker.isAllowed()) { - logger.warn(`Circuit breaker OPEN for ${provider.name}, skipping`); - continue; - } - - const rawPrice = await provider.fetchPrice(asset); - const validation = this.validator.validate(rawPrice); - - if (validation.isValid && validation.price) { - validPrices.push(validation.price); - - // Record success for circuit breaker - if (circuitBreaker) { - circuitBreaker.recordSuccess(); - } - - logger.debug(`Got valid price from ${provider.name} for ${asset}`, { - price: validation.price.price.toString(), - }); - } else { - // Record failure for circuit breaker - if (circuitBreaker) { - circuitBreaker.recordFailure(); - } - - logger.warn(`Invalid price from ${provider.name} for ${asset}`, { - errors: validation.errors, - }); - } - } catch (error) { - // Record failure for circuit breaker - const circuitBreaker = this.circuitBreakers.get(provider.name); - if (circuitBreaker) { - circuitBreaker.recordFailure(); - } - - errors.set(provider.name, error instanceof Error ? error : new Error(String(error))); - logger.warn(`Provider ${provider.name} failed for ${asset}`, { error }); - } + for (const provider of this.providers) { + try { + const circuitBreaker = this.circuitBreakers.get(provider.name); + + // Check circuit breaker state + if (circuitBreaker && !circuitBreaker.isAllowed()) { + logger.warn(`Circuit breaker OPEN for ${provider.name}, skipping`); + continue; } - if (validPrices.length === 0 && errors.size > 0) { - logger.error(`All providers failed for ${asset}`, { - providers: Array.from(errors.keys()), - }); + const rawPrice = await provider.fetchPrice(asset); + const validation = this.validator.validate(rawPrice); + + if (validation.isValid && validation.price) { + validPrices.push(validation.price); + + // Record success for circuit breaker + if (circuitBreaker) { + circuitBreaker.recordSuccess(); + } + + logger.debug(`Got valid price from ${provider.name} for ${asset}`, { + price: validation.price.price.toString(), + }); + } else { + // Record failure for circuit breaker + if (circuitBreaker) { + circuitBreaker.recordFailure(); + } + + logger.warn(`Invalid price from ${provider.name} for ${asset}`, { + errors: validation.errors, + }); + } + } catch (error) { + // Record failure for circuit breaker + const circuitBreaker = this.circuitBreakers.get(provider.name); + if (circuitBreaker) { + circuitBreaker.recordFailure(); } - return validPrices; + errors.set(provider.name, error instanceof Error ? error : new Error(String(error))); + logger.warn(`Provider ${provider.name} failed for ${asset}`, { error }); + } } - /** - * Aggregate prices from multiple sources - */ - private aggregate(asset: string, prices: PriceData[]): AggregatedPrice { - const now = Math.floor(Date.now() / 1000); - - if (prices.length === 1) { - return { - asset, - price: prices[0].price, - sources: prices, - timestamp: now, - confidence: prices[0].confidence, - }; - } - - const aggregatedPrice = this.config.useWeightedMedian - ? this.weightedMedian(prices) - : this.simpleMedian(prices); - - const totalWeight = this.providers - .filter((p) => prices.some((pr) => pr.source === p.name)) - .reduce((sum, p) => sum + p.weight, 0); - - const weightedConfidence = prices.reduce((sum, p) => { - const provider = this.providers.find((pr) => pr.name === p.source); - const weight = provider?.weight ?? 0.1; - return sum + (p.confidence * weight); - }, 0) / totalWeight; - - return { - asset, - price: aggregatedPrice, - sources: prices, - timestamp: now, - confidence: Math.round(weightedConfidence), - }; + if (validPrices.length === 0 && errors.size > 0) { + logger.error(`All providers failed for ${asset}`, { + providers: Array.from(errors.keys()), + }); } - /** - * Calculate weighted median of prices - */ - private weightedMedian(prices: PriceData[]): bigint { - const sorted = [...prices].sort((a, b) => - a.price < b.price ? -1 : a.price > b.price ? 1 : 0 - ); - - const weights = sorted.map((p) => { - const provider = this.providers.find((pr) => pr.name === p.source); - return provider?.weight ?? 0.1; - }); - - const totalWeight = weights.reduce((a, b) => a + b, 0); - const halfWeight = totalWeight / 2; - - let cumWeight = 0; - for (let i = 0; i < sorted.length; i++) { - cumWeight += weights[i]; - if (cumWeight >= halfWeight) { - return sorted[i].price; - } - } + return validPrices; + } + + /** + * Aggregate prices from multiple sources + */ + private aggregate(asset: string, prices: PriceData[]): AggregatedPrice { + const now = Math.floor(Date.now() / 1000); + + if (prices.length === 1) { + return { + asset, + price: prices[0].price, + sources: prices, + timestamp: now, + confidence: prices[0].confidence, + }; + } - return sorted[sorted.length - 1].price; + const aggregatedPrice = this.config.useWeightedMedian + ? this.weightedMedian(prices) + : this.simpleMedian(prices); + + const totalWeight = this.providers + .filter((p) => prices.some((pr) => pr.source === p.name)) + .reduce((sum, p) => sum + p.weight, 0); + + const weightedConfidence = + prices.reduce((sum, p) => { + const provider = this.providers.find((pr) => pr.name === p.source); + const weight = provider?.weight ?? 0.1; + return sum + p.confidence * weight; + }, 0) / totalWeight; + + return { + asset, + price: aggregatedPrice, + sources: prices, + timestamp: now, + confidence: Math.round(weightedConfidence), + }; + } + + /** + * Calculate weighted median of prices + */ + private weightedMedian(prices: PriceData[]): bigint { + const sorted = [...prices].sort((a, b) => (a.price < b.price ? -1 : a.price > b.price ? 1 : 0)); + + const weights = sorted.map((p) => { + const provider = this.providers.find((pr) => pr.name === p.source); + return provider?.weight ?? 0.1; + }); + + const totalWeight = weights.reduce((a, b) => a + b, 0); + const halfWeight = totalWeight / 2; + + let cumWeight = 0; + for (let i = 0; i < sorted.length; i++) { + cumWeight += weights[i]; + if (cumWeight >= halfWeight) { + return sorted[i].price; + } } - /** - * Calculate simple median of prices - */ - private simpleMedian(prices: PriceData[]): bigint { - const sorted = [...prices].sort((a, b) => - a.price < b.price ? -1 : a.price > b.price ? 1 : 0 - ); + return sorted[sorted.length - 1].price; + } - const mid = Math.floor(sorted.length / 2); + /** + * Calculate simple median of prices + */ + private simpleMedian(prices: PriceData[]): bigint { + const sorted = [...prices].sort((a, b) => (a.price < b.price ? -1 : a.price > b.price ? 1 : 0)); - if (sorted.length % 2 === 0) { - const avg = (sorted[mid - 1].price + sorted[mid].price) / 2n; - return avg; - } + const mid = Math.floor(sorted.length / 2); - return sorted[mid].price; + if (sorted.length % 2 === 0) { + const avg = (sorted[mid - 1].price + sorted[mid].price) / 2n; + return avg; } - /** - * Get price history service - */ - getPriceHistory(): PriceHistoryService { - return this.priceHistory; - } + return sorted[mid].price; + } - /** - * Get circuit breaker metrics for all providers - */ - getCircuitBreakerMetrics(): Array { - const metrics: Array = []; - - for (const [name, breaker] of this.circuitBreakers) { - metrics.push({ - providerName: name, - state: breaker.currentState, - ...breaker.getMetrics(), - }); - } + /** + * Get price history service + */ + getPriceHistory(): PriceHistoryService { + return this.priceHistory; + } - return metrics; - } + /** + * Get circuit breaker metrics for all providers + */ + getCircuitBreakerMetrics(): CircuitBreakerMetrics[] { + const metrics: CircuitBreakerMetrics[] = []; - /** - * Get list of enabled providers - */ - getProviders(): string[] { - return this.providers.map((p) => p.name); + for (const breaker of this.circuitBreakers.values()) { + metrics.push(breaker.getMetrics()); } - /** - * Get aggregator statistics - */ - getStats() { - return { - enabledProviders: this.providers.length, - cacheStats: this.cache.getStats(), - priceHistoryStats: this.priceHistory.getStats(), - circuitBreakerMetrics: this.getCircuitBreakerMetrics(), - circuitBreakers: this.getCircuitBreakerMetrics(), - }; - } + return metrics; + } + + /** + * Get list of enabled providers + */ + getProviders(): string[] { + return this.providers.map((p) => p.name); + } + + /** + * Get aggregator statistics + */ + getStats() { + return { + enabledProviders: this.providers.length, + cacheStats: this.cache.getStats(), + priceHistoryStats: this.priceHistory.getStats(), + circuitBreakerMetrics: this.getCircuitBreakerMetrics(), + circuitBreakers: this.getCircuitBreakerMetrics(), + }; + } } function isAggregatorConfig(value: unknown): value is Partial { - if (!value || typeof value !== 'object') { - return false; - } + if (!value || typeof value !== 'object') { + return false; + } - return ( - 'minSources' in value || - 'useWeightedMedian' in value || - 'circuitBreaker' in value - ); + return 'minSources' in value || 'useWeightedMedian' in value || 'circuitBreaker' in value; } function isPriceHistoryService(value: unknown): value is PriceHistoryService { - return value instanceof PriceHistoryService; + return value instanceof PriceHistoryService; } /** * Create a price aggregator */ export function createAggregator( - providers: BasePriceProvider[], - validator: PriceValidator, - cache: PriceCache, - priceHistoryOrConfig?: PriceHistoryService | Partial, - config?: Partial, + providers: BasePriceProvider[], + validator: PriceValidator, + cache: PriceCache, + priceHistoryOrConfig?: PriceHistoryService | Partial, + config?: Partial ): PriceAggregator { - const priceHistory = isPriceHistoryService(priceHistoryOrConfig) - ? priceHistoryOrConfig - : new PriceHistoryService(); - const resolvedConfig = isPriceHistoryService(priceHistoryOrConfig) - ? config - : isAggregatorConfig(priceHistoryOrConfig) - ? priceHistoryOrConfig - : config; - - return new PriceAggregator(providers, validator, cache, priceHistory, resolvedConfig); + const priceHistory = isPriceHistoryService(priceHistoryOrConfig) + ? priceHistoryOrConfig + : new PriceHistoryService(); + const resolvedConfig = isPriceHistoryService(priceHistoryOrConfig) + ? config + : isAggregatorConfig(priceHistoryOrConfig) + ? priceHistoryOrConfig + : config; + + return new PriceAggregator(providers, validator, cache, priceHistory, resolvedConfig); } diff --git a/oracle/src/services/price-history.ts b/oracle/src/services/price-history.ts index 099cd7b5..0370affc 100644 --- a/oracle/src/services/price-history.ts +++ b/oracle/src/services/price-history.ts @@ -1,6 +1,6 @@ /** * Price History Service - * + * * Stores historical price data for trend analysis, TWAP calculations, and debugging. * Uses a circular buffer to maintain memory-bounded storage. */ @@ -12,332 +12,333 @@ import { logger } from '../utils/logger.js'; * Price history entry */ export interface PriceHistoryEntry { - price: bigint; - timestamp: number; + price: bigint; + timestamp: number; } /** * Price history interface */ export interface PriceHistory { - entries: PriceHistoryEntry[]; - maxEntries: number; - currentIndex: number; - isFull: boolean; + entries: PriceHistoryEntry[]; + maxEntries: number; + currentIndex: number; + isFull: boolean; } /** * TWAP calculation result */ export interface TWAPResult { - asset: string; - twap: bigint; - periodSeconds: number; - dataPoints: number; - startTime: number; - endTime: number; + asset: string; + twap: bigint; + periodSeconds: number; + dataPoints: number; + startTime: number; + endTime: number; } /** * Price history configuration */ export interface PriceHistoryConfig { - maxEntries: number; + maxEntries: number; } /** * Default configuration */ const DEFAULT_CONFIG: PriceHistoryConfig = { - maxEntries: 100, + maxEntries: 100, }; /** * Price History Service */ export class PriceHistoryService { - private histories: Map; - private config: PriceHistoryConfig; - - constructor(config: Partial = {}) { - this.config = { ...DEFAULT_CONFIG, ...config }; - this.histories = new Map(); - - logger.info('Price history service initialized', { - maxEntries: this.config.maxEntries, - }); - } - - /** - * Add a price entry to history - */ - addPriceEntry(asset: string, price: bigint, timestamp: number): void { - const upperAsset = asset.toUpperCase(); - const history = this.getOrCreateHistory(upperAsset); - - // Add entry at current index (circular buffer behavior) - history.entries[history.currentIndex] = { - price, - timestamp, - }; - - // Move to next position - history.currentIndex = (history.currentIndex + 1) % history.maxEntries; - - // Mark as full after we've wrapped around - if (history.currentIndex === 0) { - history.isFull = true; - } - - logger.debug(`Added price history entry for ${upperAsset}`, { - price: price.toString(), - timestamp, - currentIndex: history.currentIndex, - isFull: history.isFull, - }); + private histories: Map; + private config: PriceHistoryConfig; + + constructor(config: Partial = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + this.histories = new Map(); + + logger.info('Price history service initialized', { + maxEntries: this.config.maxEntries, + }); + } + + /** + * Add a price entry to history + */ + addPriceEntry(asset: string, price: bigint, timestamp: number): void { + const upperAsset = asset.toUpperCase(); + const history = this.getOrCreateHistory(upperAsset); + + // Add entry at current index (circular buffer behavior) + history.entries[history.currentIndex] = { + price, + timestamp, + }; + + // Move to next position + history.currentIndex = (history.currentIndex + 1) % history.maxEntries; + + // Mark as full after we've wrapped around + if (history.currentIndex === 0) { + history.isFull = true; } - /** - * Add aggregated price to history - */ - addAggregatedPrice(price: AggregatedPrice): void { - this.addPriceEntry(price.asset, price.price, price.timestamp); + logger.debug(`Added price history entry for ${upperAsset}`, { + price: price.toString(), + timestamp, + currentIndex: history.currentIndex, + isFull: history.isFull, + }); + } + + /** + * Add aggregated price to history + */ + addAggregatedPrice(price: AggregatedPrice): void { + this.addPriceEntry(price.asset, price.price, price.timestamp); + } + + /** + * Get price history for an asset + */ + getPriceHistory(asset: string, limit?: number): PriceHistoryEntry[] { + const upperAsset = asset.toUpperCase(); + const history = this.histories.get(upperAsset); + + if (!history) { + return []; } - /** - * Get price history for an asset - */ - getPriceHistory(asset: string, limit?: number): PriceHistoryEntry[] { - const upperAsset = asset.toUpperCase(); - const history = this.histories.get(upperAsset); - - if (!history) { - return []; + const entries: PriceHistoryEntry[] = []; + + if (history.isFull) { + // Buffer is full, return entries starting from current index + const startIndex = history.currentIndex; + for (let i = 0; i < history.maxEntries; i++) { + const index = (startIndex + i) % history.maxEntries; + const entry = history.entries[index]; + if (entry) { + entries.push(entry); + if (limit && entries.length >= limit) { + break; + } } - - const entries: PriceHistoryEntry[] = []; - - if (history.isFull) { - // Buffer is full, return entries starting from current index - const startIndex = history.currentIndex; - for (let i = 0; i < history.maxEntries; i++) { - const index = (startIndex + i) % history.maxEntries; - const entry = history.entries[index]; - if (entry) { - entries.push(entry); - if (limit && entries.length >= limit) { - break; - } - } - } - } else { - // Buffer not full, return entries from start - for (let i = 0; i < history.currentIndex; i++) { - const entry = history.entries[i]; - if (entry) { - entries.push(entry); - if (limit && entries.length >= limit) { - break; - } - } - } + } + } else { + // Buffer not full, return entries from start + for (let i = 0; i < history.currentIndex; i++) { + const entry = history.entries[i]; + if (entry) { + entries.push(entry); + if (limit && entries.length >= limit) { + break; + } } - - return entries; + } } - /** - * Calculate Time-Weighted Average Price (TWAP) - */ - calculateTWAP(asset: string, periodSeconds: number): TWAPResult | null { - const upperAsset = asset.toUpperCase(); - const entries = this.getPriceHistory(upperAsset); - - if (entries.length < 2) { - logger.warn(`Insufficient data for TWAP calculation for ${upperAsset}`, { - availableEntries: entries.length, - required: 2, - }); - return null; - } - - const now = Math.floor(Date.now() / 1000); - const startTime = now - periodSeconds; - - // Filter entries within the time period - const periodEntries = entries.filter(entry => entry.timestamp >= startTime); - - if (periodEntries.length < 2) { - logger.warn(`Insufficient data within time period for TWAP calculation for ${upperAsset}`, { - periodSeconds, - availableEntries: periodEntries.length, - required: 2, - }); - return null; - } - - // Calculate TWAP using time-weighted average - let totalTime = 0; - let weightedSum = 0n; - - for (let i = 0; i < periodEntries.length - 1; i++) { - const current = periodEntries[i]; - const next = periodEntries[i + 1]; - - const timeDiff = next.timestamp - current.timestamp; - totalTime += timeDiff; - weightedSum += current.price * BigInt(timeDiff); - } - - // Add the last entry's contribution (assume it lasts until now) - const lastEntry = periodEntries[periodEntries.length - 1]; - const lastTimeDiff = now - lastEntry.timestamp; - totalTime += lastTimeDiff; - weightedSum += lastEntry.price * BigInt(lastTimeDiff); - - if (totalTime === 0) { - logger.warn(`Zero time duration for TWAP calculation for ${upperAsset}`); - return null; - } - - const twap = weightedSum / BigInt(totalTime); - - const result: TWAPResult = { - asset: upperAsset, - twap, - periodSeconds, - dataPoints: periodEntries.length, - startTime: periodEntries[0].timestamp, - endTime: lastEntry.timestamp, - }; - - logger.info(`Calculated TWAP for ${upperAsset}`, { - twap: twap.toString(), - periodSeconds, - dataPoints: periodEntries.length, - }); - - return result; + return entries; + } + + /** + * Calculate Time-Weighted Average Price (TWAP) + */ + calculateTWAP(asset: string, periodSeconds: number): TWAPResult | null { + const upperAsset = asset.toUpperCase(); + const entries = this.getPriceHistory(upperAsset); + + if (entries.length < 2) { + logger.warn(`Insufficient data for TWAP calculation for ${upperAsset}`, { + availableEntries: entries.length, + required: 2, + }); + return null; } - /** - * Get the latest price for an asset - */ - getLatestPrice(asset: string): PriceHistoryEntry | null { - const upperAsset = asset.toUpperCase(); - const history = this.histories.get(upperAsset); + const now = Math.floor(Date.now() / 1000); + const startTime = now - periodSeconds; - if (!history || history.currentIndex === 0 && !history.isFull) { - return null; - } + // Filter entries within the time period + const periodEntries = entries.filter((entry) => entry.timestamp >= startTime); - // Get the most recent entry - const latestIndex = history.currentIndex === 0 ? history.maxEntries - 1 : history.currentIndex - 1; - return history.entries[latestIndex] || null; + if (periodEntries.length < 2) { + logger.warn(`Insufficient data within time period for TWAP calculation for ${upperAsset}`, { + periodSeconds, + availableEntries: periodEntries.length, + required: 2, + }); + return null; } - /** - * Get statistics for an asset - */ - getAssetStats(asset: string): { - totalEntries: number; - oldestTimestamp?: number; - newestTimestamp?: number; - priceRange?: { min: bigint; max: bigint }; - } { - const upperAsset = asset.toUpperCase(); - const entries = this.getPriceHistory(upperAsset); - - if (entries.length === 0) { - return { totalEntries: 0 }; - } - - const timestamps = entries.map(e => e.timestamp); - const prices = entries.map(e => e.price); - - return { - totalEntries: entries.length, - oldestTimestamp: Math.min(...timestamps), - newestTimestamp: Math.max(...timestamps), - priceRange: { - min: prices.reduce((a, b) => a < b ? a : b), - max: prices.reduce((a, b) => a > b ? a : b), - }, - }; - } + // Calculate TWAP using time-weighted average + let totalTime = 0; + let weightedSum = 0n; - /** - * Clear history for an asset - */ - clearHistory(asset: string): void { - const upperAsset = asset.toUpperCase(); - this.histories.delete(upperAsset); + for (let i = 0; i < periodEntries.length - 1; i++) { + const current = periodEntries[i]; + const next = periodEntries[i + 1]; - logger.info(`Cleared price history for ${upperAsset}`); + const timeDiff = next.timestamp - current.timestamp; + totalTime += timeDiff; + weightedSum += current.price * BigInt(timeDiff); } - /** - * Clear all history - */ - clearAllHistory(): void { - this.histories.clear(); + // Add the last entry's contribution (assume it lasts until now) + const lastEntry = periodEntries[periodEntries.length - 1]; + const lastTimeDiff = now - lastEntry.timestamp; + totalTime += lastTimeDiff; + weightedSum += lastEntry.price * BigInt(lastTimeDiff); - logger.info('Cleared all price history'); + if (totalTime === 0) { + logger.warn(`Zero time duration for TWAP calculation for ${upperAsset}`); + return null; } - /** - * Get list of assets with history - */ - getAssets(): string[] { - return Array.from(this.histories.keys()); + const twap = weightedSum / BigInt(totalTime); + + const result: TWAPResult = { + asset: upperAsset, + twap, + periodSeconds, + dataPoints: periodEntries.length, + startTime: periodEntries[0].timestamp, + endTime: lastEntry.timestamp, + }; + + logger.info(`Calculated TWAP for ${upperAsset}`, { + twap: twap.toString(), + periodSeconds, + dataPoints: periodEntries.length, + }); + + return result; + } + + /** + * Get the latest price for an asset + */ + getLatestPrice(asset: string): PriceHistoryEntry | null { + const upperAsset = asset.toUpperCase(); + const history = this.histories.get(upperAsset); + + if (!history || (history.currentIndex === 0 && !history.isFull)) { + return null; } - /** - * Get service statistics - */ - getStats() { - const assets = this.getAssets(); - const totalEntries = assets.reduce((sum, asset) => { - const history = this.histories.get(asset); - if (history) { - return sum + (history.isFull ? history.maxEntries : history.currentIndex); - } - return sum; - }, 0); - - return { - trackedAssets: assets.length, - totalEntries, - maxEntriesPerAsset: this.config.maxEntries, - assets, - }; + // Get the most recent entry + const latestIndex = + history.currentIndex === 0 ? history.maxEntries - 1 : history.currentIndex - 1; + return history.entries[latestIndex] || null; + } + + /** + * Get statistics for an asset + */ + getAssetStats(asset: string): { + totalEntries: number; + oldestTimestamp?: number; + newestTimestamp?: number; + priceRange?: { min: bigint; max: bigint }; + } { + const upperAsset = asset.toUpperCase(); + const entries = this.getPriceHistory(upperAsset); + + if (entries.length === 0) { + return { totalEntries: 0 }; } - /** - * Get or create history for an asset - */ - private getOrCreateHistory(asset: string): PriceHistory { - let history = this.histories.get(asset); - - if (!history) { - history = { - entries: new Array(this.config.maxEntries), - maxEntries: this.config.maxEntries, - currentIndex: 0, - isFull: false, - }; - this.histories.set(asset, history); - } - - return history; + const timestamps = entries.map((e) => e.timestamp); + const prices = entries.map((e) => e.price); + + return { + totalEntries: entries.length, + oldestTimestamp: Math.min(...timestamps), + newestTimestamp: Math.max(...timestamps), + priceRange: { + min: prices.reduce((a, b) => (a < b ? a : b)), + max: prices.reduce((a, b) => (a > b ? a : b)), + }, + }; + } + + /** + * Clear history for an asset + */ + clearHistory(asset: string): void { + const upperAsset = asset.toUpperCase(); + this.histories.delete(upperAsset); + + logger.info(`Cleared price history for ${upperAsset}`); + } + + /** + * Clear all history + */ + clearAllHistory(): void { + this.histories.clear(); + + logger.info('Cleared all price history'); + } + + /** + * Get list of assets with history + */ + getAssets(): string[] { + return Array.from(this.histories.keys()); + } + + /** + * Get service statistics + */ + getStats() { + const assets = this.getAssets(); + const totalEntries = assets.reduce((sum, asset) => { + const history = this.histories.get(asset); + if (history) { + return sum + (history.isFull ? history.maxEntries : history.currentIndex); + } + return sum; + }, 0); + + return { + trackedAssets: assets.length, + totalEntries, + maxEntriesPerAsset: this.config.maxEntries, + assets, + }; + } + + /** + * Get or create history for an asset + */ + private getOrCreateHistory(asset: string): PriceHistory { + let history = this.histories.get(asset); + + if (!history) { + history = { + entries: new Array(this.config.maxEntries), + maxEntries: this.config.maxEntries, + currentIndex: 0, + isFull: false, + }; + this.histories.set(asset, history); } + + return history; + } } /** * Create a price history service */ export function createPriceHistoryService( - config?: Partial, + config?: Partial ): PriceHistoryService { - return new PriceHistoryService(config); + return new PriceHistoryService(config); } diff --git a/oracle/src/services/price-validator.ts b/oracle/src/services/price-validator.ts index 0d108463..f6d713b0 100644 --- a/oracle/src/services/price-validator.ts +++ b/oracle/src/services/price-validator.ts @@ -35,9 +35,9 @@ const DEFAULT_CONFIG: ValidatorConfig = { minPrice: 0.0000001, maxPrice: 1000000000, sourceWeights: { - 'coingecko': 1.0, - 'binance': 0.95, - 'coinmarketcap': 1.0, + coingecko: 1.0, + binance: 0.95, + coinmarketcap: 1.0, }, }; diff --git a/oracle/tests/contract-updater.test.ts b/oracle/tests/contract-updater.test.ts index 1efaa954..5563a520 100644 --- a/oracle/tests/contract-updater.test.ts +++ b/oracle/tests/contract-updater.test.ts @@ -411,7 +411,9 @@ describe('ContractUpdater', () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - vi.spyOn(mockServer, 'getAccount').mockRejectedValue(new Error('RPC timeout: Connection timed out after 30 seconds')); + vi.spyOn(mockServer, 'getAccount').mockRejectedValue( + new Error('RPC timeout: Connection timed out after 30 seconds') + ); const result = await updater.updatePrice('XLM', 150000n, Date.now()); @@ -506,7 +508,9 @@ describe('ContractUpdater', () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - vi.spyOn(mockServer, 'sendTransaction').mockRejectedValue(new Error('Network error: ECONNREFUSED - Connection refused')); + vi.spyOn(mockServer, 'sendTransaction').mockRejectedValue( + new Error('Network error: ECONNREFUSED - Connection refused') + ); const result = await updater.updatePrice('XLM', 150000n, Date.now()); @@ -519,7 +523,9 @@ describe('ContractUpdater', () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - vi.spyOn(mockServer, 'sendTransaction').mockRejectedValue(new Error('Rate limit exceeded: Too many requests, try again later')); + vi.spyOn(mockServer, 'sendTransaction').mockRejectedValue( + new Error('Rate limit exceeded: Too many requests, try again later') + ); const result = await updater.updatePrice('BTC', 50000000000n, Date.now()); @@ -531,7 +537,9 @@ describe('ContractUpdater', () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - vi.spyOn(mockServer, 'sendTransaction').mockRejectedValue(new Error('DNS resolution failed: Unable to resolve host')); + vi.spyOn(mockServer, 'sendTransaction').mockRejectedValue( + new Error('DNS resolution failed: Unable to resolve host') + ); const result = await updater.updatePrice('ETH', 1000000000n, Date.now()); @@ -701,7 +709,8 @@ describe('ContractUpdater', () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - const detailedError = 'Simulation failed: Contract execution error: Insufficient gas limit. Required: 50000, Available: 30000'; + const detailedError = + 'Simulation failed: Contract execution error: Insufficient gas limit. Required: 50000, Available: 30000'; vi.spyOn(mockServer, 'simulateTransaction').mockResolvedValue({ error: detailedError, result: null, @@ -770,7 +779,7 @@ describe('ContractUpdater', () => { it('should return detailed health status when all checks pass', async () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - + // Mock successful health checks vi.spyOn(mockServer, 'getHealth').mockResolvedValue({ status: 'healthy' }); vi.spyOn(mockServer, 'getAccount').mockResolvedValue({ @@ -795,7 +804,7 @@ describe('ContractUpdater', () => { it('should return failure status when RPC is unreachable', async () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - + // Mock RPC failure vi.spyOn(mockServer, 'getHealth').mockRejectedValue(new Error('RPC connection failed')); @@ -809,7 +818,7 @@ describe('ContractUpdater', () => { it('should return failure status when admin account does not exist', async () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - + // Mock successful RPC but failed account check vi.spyOn(mockServer, 'getHealth').mockResolvedValue({ status: 'healthy' }); vi.spyOn(mockServer, 'getAccount').mockRejectedValue(new Error('Account not found')); @@ -826,13 +835,15 @@ describe('ContractUpdater', () => { it('should return failure status when contract is inaccessible', async () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - + // Mock successful RPC and account but failed contract access vi.spyOn(mockServer, 'getHealth').mockResolvedValue({ status: 'healthy' }); vi.spyOn(mockServer, 'getAccount').mockResolvedValue({ balances: [{ asset_type: 'native', balance: '5.0' }], } as any); - vi.spyOn(mockServer, 'simulateTransaction').mockRejectedValue(new Error('Contract not deployed')); + vi.spyOn(mockServer, 'simulateTransaction').mockRejectedValue( + new Error('Contract not deployed') + ); const healthStatus = await updater.healthCheck(); @@ -845,9 +856,9 @@ describe('ContractUpdater', () => { it('should complete health check within 5 seconds', async () => { const startTime = Date.now(); - + await updater.healthCheck(); - + const duration = Date.now() - startTime; expect(duration).toBeLessThan(5000); }); @@ -855,7 +866,7 @@ describe('ContractUpdater', () => { it('should handle unexpected errors gracefully', async () => { const { SorobanRpc } = await import('@stellar/stellar-sdk'); const mockServer = new SorobanRpc.Server('mock'); - + // Mock unexpected error during health check vi.spyOn(mockServer, 'getHealth').mockRejectedValue(new Error('Unexpected error')); diff --git a/oracle/tests/dry-run.test.ts b/oracle/tests/dry-run.test.ts index 35accf09..4961a7b3 100644 --- a/oracle/tests/dry-run.test.ts +++ b/oracle/tests/dry-run.test.ts @@ -45,7 +45,10 @@ vi.mock('../src/providers/index.js', () => ({ vi.mock('../src/services/index.js', () => ({ createValidator: vi.fn(() => ({ validate: vi.fn() })), createPriceCache: vi.fn(() => ({ get: vi.fn(), set: vi.fn() })), - createPriceHistoryService: vi.fn(() => ({ addAggregatedPrice: vi.fn(), getStats: vi.fn(() => ({})) })), + createPriceHistoryService: vi.fn(() => ({ + addAggregatedPrice: vi.fn(), + getStats: vi.fn(() => ({})), + })), createAggregator: vi.fn(() => mockAggregator), createContractUpdater: vi.fn(() => mockContractUpdater), })); diff --git a/oracle/tests/lifecycle.test.ts b/oracle/tests/lifecycle.test.ts index b29cee31..51fe9a7e 100644 --- a/oracle/tests/lifecycle.test.ts +++ b/oracle/tests/lifecycle.test.ts @@ -10,18 +10,16 @@ import type { OracleServiceConfig } from '../src/config.js'; // Mock contract updater to avoid actual blockchain calls vi.mock('../src/services/contract-updater.js', () => ({ createContractUpdater: vi.fn(() => ({ - updatePrices: vi - .fn() - .mockImplementation(async (prices) => { - // Simulate some processing time - await new Promise(resolve => setTimeout(resolve, 50)); - return prices.map((price, index) => ({ - success: true, - asset: price.asset || `ASSET_${index}`, - price: BigInt(Math.floor((price.price || 0.15) * 1000000)), - timestamp: Date.now(), - })); - }), + updatePrices: vi.fn().mockImplementation(async (prices) => { + // Simulate some processing time + await new Promise((resolve) => setTimeout(resolve, 50)); + return prices.map((price, index) => ({ + success: true, + asset: price.asset || `ASSET_${index}`, + price: BigInt(Math.floor((price.price || 0.15) * 1000000)), + timestamp: Date.now(), + })); + }), healthCheck: vi.fn().mockResolvedValue(true), getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), })), @@ -38,7 +36,7 @@ vi.mock('../src/providers/coingecko.js', () => ({ getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC', 'SOL'], fetchPrice: vi.fn().mockImplementation(async (asset) => { // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); return { asset, price: asset === 'XLM' ? 0.15 : asset === 'BTC' ? 45000 : asset === 'ETH' ? 3000 : 1, @@ -58,7 +56,7 @@ vi.mock('../src/providers/binance.js', () => ({ getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC', 'SOL'], fetchPrice: vi.fn().mockImplementation(async (asset) => { // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 80)); + await new Promise((resolve) => setTimeout(resolve, 80)); return { asset, price: asset === 'XLM' ? 0.152 : asset === 'BTC' ? 45100 : asset === 'ETH' ? 3010 : 1.01, @@ -129,14 +127,14 @@ describe('OracleService Lifecycle Integration Tests', () => { expect(service.getStatus().isRunning).toBe(true); // Allow at least one update cycle - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); // Stop service service.stop(); expect(service.getStatus().isRunning).toBe(false); // Verify no pending intervals (by waiting a bit longer) - await new Promise(resolve => setTimeout(resolve, 300)); + await new Promise((resolve) => setTimeout(resolve, 300)); expect(service.getStatus().isRunning).toBe(false); }); @@ -146,10 +144,10 @@ describe('OracleService Lifecycle Integration Tests', () => { // Perform multiple cycles for (let i = 0; i < 3; i++) { await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); service.stop(); - await new Promise(resolve => setTimeout(resolve, 100)); - + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(service.getStatus().isRunning).toBe(false); } }); @@ -170,7 +168,7 @@ describe('OracleService Lifecycle Integration Tests', () => { expect(secondStartStatus.isRunning).toBe(true); // Service should still be functional - await new Promise(resolve => setTimeout(resolve, 250)); + await new Promise((resolve) => setTimeout(resolve, 250)); expect(service.getStatus().isRunning).toBe(true); service.stop(); @@ -178,18 +176,18 @@ describe('OracleService Lifecycle Integration Tests', () => { it('should not create multiple intervals on double start', async () => { service = new OracleService(mockConfig); - + const setIntervalSpy = vi.spyOn(global, 'setInterval'); - + await service.start(['XLM']); const firstCallCount = setIntervalSpy.mock.calls.length; - + await service.start(['BTC']); const secondCallCount = setIntervalSpy.mock.calls.length; - + // Should not create additional intervals expect(secondCallCount).toBe(firstCallCount); - + setIntervalSpy.mockRestore(); service.stop(); }); @@ -202,14 +200,14 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM', 'BTC', 'ETH']); // Wait for update to start, then stop immediately - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); service.stop(); // Should stop without throwing expect(service.getStatus().isRunning).toBe(false); // Wait to ensure no background processes - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, 200)); expect(service.getStatus().isRunning).toBe(false); }); @@ -219,7 +217,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM']); // Wait for update to start - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); // Call stop multiple times concurrently const stopPromises = [ @@ -235,18 +233,18 @@ describe('OracleService Lifecycle Integration Tests', () => { it('should clean up resources even when stop is called during update', async () => { service = new OracleService(mockConfig); - + const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); - + await service.start(['XLM', 'BTC']); - + // Stop during active update - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); service.stop(); - + // Verify cleanup was called expect(clearIntervalSpy).toHaveBeenCalled(); - + clearIntervalSpy.mockRestore(); }); }); @@ -256,7 +254,7 @@ describe('OracleService Lifecycle Integration Tests', () => { // Mock contract updater to fail initially const { createContractUpdater } = await import('../src/services/contract-updater.js'); let callCount = 0; - + vi.mocked(createContractUpdater).mockReturnValueOnce({ updatePrices: vi.fn().mockImplementation(async () => { callCount++; @@ -274,7 +272,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM']); // Wait for failure cycles - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); // Service should still be running despite failures expect(service.getStatus().isRunning).toBe(true); @@ -293,7 +291,7 @@ describe('OracleService Lifecycle Integration Tests', () => { // Start and let it run await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); // Stop service.stop(); @@ -302,7 +300,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['BTC']); expect(service.getStatus().isRunning).toBe(true); - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); service.stop(); }); @@ -311,7 +309,7 @@ describe('OracleService Lifecycle Integration Tests', () => { // First run await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); service.stop(); const statusAfterStop = service.getStatus(); @@ -322,7 +320,7 @@ describe('OracleService Lifecycle Integration Tests', () => { const statusAfterRestart = service.getStatus(); expect(statusAfterRestart.isRunning).toBe(true); - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); service.stop(); }); }); @@ -335,7 +333,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM', 'BTC', 'ETH', 'USDC', 'SOL']); // Wait for updates to be in progress - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); // Initiate graceful shutdown const stopStartTime = Date.now(); @@ -349,26 +347,26 @@ describe('OracleService Lifecycle Integration Tests', () => { expect(service.getStatus().isRunning).toBe(false); // Wait to ensure no background activity - await new Promise(resolve => setTimeout(resolve, 300)); + await new Promise((resolve) => setTimeout(resolve, 300)); expect(service.getStatus().isRunning).toBe(false); }); it('should clean up all resources during shutdown', async () => { service = new OracleService(mockConfig); - + const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); - + await service.start(['XLM', 'BTC']); - + // Ensure updates are running - await new Promise(resolve => setTimeout(resolve, 100)); - + await new Promise((resolve) => setTimeout(resolve, 100)); + service.stop(); - + // Verify cleanup expect(clearIntervalSpy).toHaveBeenCalled(); expect(service.getStatus().isRunning).toBe(false); - + clearIntervalSpy.mockRestore(); }); @@ -382,14 +380,14 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM', 'BTC']); // Let multiple cycles start - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise((resolve) => setTimeout(resolve, 150)); // Shutdown should handle overlapping operations service.stop(); expect(service.getStatus().isRunning).toBe(false); // Ensure complete shutdown - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, 200)); expect(service.getStatus().isRunning).toBe(false); }); @@ -415,14 +413,14 @@ describe('OracleService Lifecycle Integration Tests', () => { describe('Resource leak prevention', () => { it('should not accumulate interval references', async () => { service = new OracleService(mockConfig); - + const setIntervalSpy = vi.spyOn(global, 'setInterval'); const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); // Multiple start/stop cycles for (let i = 0; i < 5; i++) { await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); service.stop(); } @@ -440,7 +438,7 @@ describe('OracleService Lifecycle Integration Tests', () => { for (let i = 0; i < 10; i++) { await service.start(['XLM']); service.stop(); - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); } expect(service.getStatus().isRunning).toBe(false); @@ -453,14 +451,14 @@ describe('OracleService Lifecycle Integration Tests', () => { // Test with variable delays const delays = [0, 10, 50, 100, 200]; - + for (const delay of delays) { await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); service.stop(); - + expect(service.getStatus().isRunning).toBe(false); - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); } }); @@ -470,9 +468,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM', 'BTC']); // Simulate load with concurrent operations - const operations = Array.from({ length: 10 }, (_, i) => - service.updatePrices([`ASSET_${i}`]) - ); + const operations = Array.from({ length: 10 }, (_, i) => service.updatePrices([`ASSET_${i}`])); // Stop during load service.stop(); diff --git a/oracle/tests/memory-leak.test.ts b/oracle/tests/memory-leak.test.ts index 72889c3a..68f8edad 100644 --- a/oracle/tests/memory-leak.test.ts +++ b/oracle/tests/memory-leak.test.ts @@ -123,9 +123,11 @@ describe('OracleService Memory Leak Detection', () => { const memoryIncrease = finalMemory - initialMemory; const memoryIncreasePercent = (memoryIncrease / initialMemory) * 100; - console.log(`Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB (${memoryIncreasePercent.toFixed(2)}%)`); + console.log( + `Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB (${memoryIncreasePercent.toFixed(2)}%)` + ); - // A leak would typically show much larger growth. + // A leak would typically show much larger growth. // We allow for some growth due to Node.js's lazy garbage collection, // but anything over 50% increase in 2000 iterations of mocked work is suspicious. // Ideally it should be very close to 0 or even negative if GC kicks in. diff --git a/oracle/tests/memory.test.ts b/oracle/tests/memory.test.ts index 848fe28c..908d39a1 100644 --- a/oracle/tests/memory.test.ts +++ b/oracle/tests/memory.test.ts @@ -13,9 +13,9 @@ import type { OracleServiceConfig } from '../src/config.js'; vi.mock('../src/services/contract-updater.js', () => ({ createContractUpdater: vi.fn(() => ({ - updatePrices: vi.fn().mockResolvedValue([ - { success: true, asset: 'XLM', price: 150000n, timestamp: Date.now() }, - ]), + updatePrices: vi + .fn() + .mockResolvedValue([{ success: true, asset: 'XLM', price: 150000n, timestamp: Date.now() }]), healthCheck: vi.fn().mockResolvedValue(true), getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), })), diff --git a/oracle/tests/oracle-configuration.test.ts b/oracle/tests/oracle-configuration.test.ts index e9cbabd1..90b6bcfb 100644 --- a/oracle/tests/oracle-configuration.test.ts +++ b/oracle/tests/oracle-configuration.test.ts @@ -1,6 +1,6 @@ /** * Oracle Configuration Management and Role Separation Tests - * + * * This test suite covers: * - Oracle configuration changes (switching primary feeds, adjusting parameters) * - Role separation enforcement (who can change oracle settings) @@ -14,595 +14,592 @@ import type { OracleServiceConfig, ProviderConfig } from '../src/config.js'; // Mock contract updater for controlled testing vi.mock('../src/services/contract-updater.js', () => ({ - createContractUpdater: vi.fn(() => ({ - updatePrices: vi.fn().mockResolvedValue([ - { success: true, asset: 'XLM', price: 150000n, timestamp: Date.now() }, - ]), - healthCheck: vi.fn().mockResolvedValue(true), - getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), - })), - ContractUpdater: vi.fn(), + createContractUpdater: vi.fn(() => ({ + updatePrices: vi + .fn() + .mockResolvedValue([{ success: true, asset: 'XLM', price: 150000n, timestamp: Date.now() }]), + healthCheck: vi.fn().mockResolvedValue(true), + getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), + })), + ContractUpdater: vi.fn(), })); // Mock providers for consistent testing vi.mock('../src/providers/coingecko.js', () => ({ - createCoinGeckoProvider: vi.fn(() => ({ - name: 'coingecko', - isEnabled: true, - priority: 1, - weight: 0.6, - getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC'], - fetchPrice: vi.fn().mockResolvedValue({ - asset: 'XLM', - price: 0.15, - timestamp: Math.floor(Date.now() / 1000), - source: 'coingecko', - }), - })), + createCoinGeckoProvider: vi.fn(() => ({ + name: 'coingecko', + isEnabled: true, + priority: 1, + weight: 0.6, + getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC'], + fetchPrice: vi.fn().mockResolvedValue({ + asset: 'XLM', + price: 0.15, + timestamp: Math.floor(Date.now() / 1000), + source: 'coingecko', + }), + })), })); vi.mock('../src/providers/binance.js', () => ({ - createBinanceProvider: vi.fn(() => ({ - name: 'binance', - isEnabled: true, - priority: 2, - weight: 0.4, - getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC'], - fetchPrice: vi.fn().mockResolvedValue({ - asset: 'XLM', - price: 0.152, - timestamp: Math.floor(Date.now() / 1000), - source: 'binance', - }), - })), + createBinanceProvider: vi.fn(() => ({ + name: 'binance', + isEnabled: true, + priority: 2, + weight: 0.4, + getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC'], + fetchPrice: vi.fn().mockResolvedValue({ + asset: 'XLM', + price: 0.152, + timestamp: Math.floor(Date.now() / 1000), + source: 'binance', + }), + })), })); describe('Oracle Configuration Management', () => { - let service: OracleService; - let baseConfig: OracleServiceConfig; - - beforeEach(() => { - baseConfig = { - stellarNetwork: 'testnet', - stellarRpcUrl: 'https://soroban-testnet.stellar.org', - contractId: 'CTEST123', - adminSecretKey: 'STEST123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', - updateIntervalMs: 1000, - maxPriceDeviationPercent: 10, - priceStaleThresholdSeconds: 300, - cacheTtlSeconds: 30, - logLevel: 'error', - providers: [ - { - name: 'coingecko', - enabled: true, - priority: 1, - weight: 0.6, - baseUrl: 'https://api.coingecko.com/api/v3', - rateLimit: { maxRequests: 10, windowMs: 60000 }, - }, - { - name: 'binance', - enabled: true, - priority: 2, - weight: 0.4, - baseUrl: 'https://api.binance.com/api/v3', - rateLimit: { maxRequests: 1200, windowMs: 60000 }, - }, - ], - }; - }); - - afterEach(() => { - if (service) { - service.stop(); - } - }); - - describe('Provider Configuration Changes', () => { - it('should allow switching primary provider by priority', () => { - // Create config with different priority ordering - const switchedConfig = { - ...baseConfig, - providers: [ - { - ...baseConfig.providers[0], - priority: 2, // Demote CoinGecko - }, - { - ...baseConfig.providers[1], - priority: 1, // Promote Binance to primary - }, - ], - }; - - service = new OracleService(switchedConfig); - const status = service.getStatus(); - - expect(status).toBeDefined(); - expect(status.providers).toHaveLength(2); - - // Verify priority ordering changed - const binanceProvider = status.providers.find(p => p.name === 'binance'); - const coingeckoProvider = status.providers.find(p => p.name === 'coingecko'); - - expect(binanceProvider?.priority).toBe(1); - expect(coingeckoProvider?.priority).toBe(2); - }); - - it('should allow adjusting provider weights', () => { - const adjustedConfig = { - ...baseConfig, - providers: [ - { - ...baseConfig.providers[0], - weight: 0.8, // Increase CoinGecko weight - }, - { - ...baseConfig.providers[1], - weight: 0.2, // Decrease Binance weight - }, - ], - }; - - service = new OracleService(adjustedConfig); - const status = service.getStatus(); - - const coingeckoProvider = status.providers.find(p => p.name === 'coingecko'); - const binanceProvider = status.providers.find(p => p.name === 'binance'); - - expect(coingeckoProvider?.weight).toBe(0.8); - expect(binanceProvider?.weight).toBe(0.2); - }); - - it('should allow enabling/disabling providers', () => { - const disabledConfig = { - ...baseConfig, - providers: [ - { - ...baseConfig.providers[0], - enabled: false, // Disable CoinGecko - }, - baseConfig.providers[1], // Keep Binance enabled - ], - }; - - service = new OracleService(disabledConfig); - const status = service.getStatus(); - - const coingeckoProvider = status.providers.find(p => p.name === 'coingecko'); - const binanceProvider = status.providers.find(p => p.name === 'binance'); - - expect(coingeckoProvider?.enabled).toBe(false); - expect(binanceProvider?.enabled).toBe(true); - }); - - it('should validate provider weight sum does not exceed 1', () => { - const invalidWeightConfig = { - ...baseConfig, - providers: [ - { - ...baseConfig.providers[0], - weight: 0.7, - }, - { - ...baseConfig.providers[1], - weight: 0.5, // Total: 1.2 > 1.0 - }, - ], - }; - - expect(() => new OracleService(invalidWeightConfig)).not.toThrow(); - // Service should still initialize but with warnings - service = new OracleService(invalidWeightConfig); - expect(service).toBeDefined(); - }); - - it('should handle provider configuration with single provider', () => { - const singleProviderConfig = { - ...baseConfig, - providers: [baseConfig.providers[0]], // Only CoinGecko - }; - - service = new OracleService(singleProviderConfig); - const status = service.getStatus(); - - expect(status.providers).toHaveLength(1); - expect(status.providers[0].name).toBe('coingecko'); - expect(status.providers[0].weight).toBe(1.0); - }); - }); - - describe('Oracle Parameter Configuration', () => { - it('should allow adjusting price deviation threshold', () => { - const tightDeviationConfig = { - ...baseConfig, - maxPriceDeviationPercent: 5, // Tighter threshold - }; - - service = new OracleService(tightDeviationConfig); - expect(service).toBeDefined(); - - const looseDeviationConfig = { - ...baseConfig, - maxPriceDeviationPercent: 20, // Looser threshold - }; - - service = new OracleService(looseDeviationConfig); - expect(service).toBeDefined(); - }); - - it('should allow adjusting staleness threshold', () => { - const freshConfig = { - ...baseConfig, - priceStaleThresholdSeconds: 60, // 1 minute - }; - - service = new OracleService(freshConfig); - expect(service).toBeDefined(); - - const staleConfig = { - ...baseConfig, - priceStaleThresholdSeconds: 7200, // 2 hours - }; - - service = new OracleService(staleConfig); - expect(service).toBeDefined(); - }); - - it('should allow adjusting cache TTL', () => { - const noCacheConfig = { - ...baseConfig, - cacheTtlSeconds: 0, // Disable caching - }; - - service = new OracleService(noCacheConfig); - expect(service).toBeDefined(); - - const longCacheConfig = { - ...baseConfig, - cacheTtlSeconds: 1800, // 30 minutes - }; - - service = new OracleService(longCacheConfig); - expect(service).toBeDefined(); - }); - - it('should allow adjusting update intervals', () => { - const rapidConfig = { - ...baseConfig, - updateIntervalMs: 500, // Very frequent updates - }; - - service = new OracleService(rapidConfig); - expect(service).toBeDefined(); - - const slowConfig = { - ...baseConfig, - updateIntervalMs: 300000, // 5 minutes - }; - - service = new OracleService(slowConfig); - expect(service).toBeDefined(); - }); - }); - - describe('Role Separation Enforcement', () => { - it('should enforce admin-only configuration changes', () => { - service = new OracleService(baseConfig); - - // Service should not expose configuration modification methods - expect(typeof (service as any).updateConfig).toBe('undefined'); - expect(typeof (service as any).setProviders).toBe('undefined'); - expect(typeof (service as any).modifyAdminKey).toBe('undefined'); - }); - - it('should validate admin credentials in configuration', () => { - const invalidAdminConfig = { - ...baseConfig, - adminSecretKey: '', // Empty admin key - }; - - // Should still initialize but may fail during operations - expect(() => new OracleService(invalidAdminConfig)).not.toThrow(); - }); - - it('should maintain role separation between price updates and configuration', () => { - service = new OracleService(baseConfig); - - // Price update operations should be available - expect(typeof service.updatePrices).toBe('function'); - expect(typeof service.fetchPrice).toBe('function'); - - // Configuration operations should not be exposed - expect(typeof (service as any).configureOracle).toBe('undefined'); - expect(typeof (service as any).setOracleProvider).toBe('undefined'); - }); - - it('should prevent unauthorized provider modifications', () => { - service = new OracleService(baseConfig); - const status = service.getStatus(); - - // Status should be read-only - expect(() => { - (status as any).providers = []; - }).not.toThrow(); - - // But internal configuration should remain unchanged - const newStatus = service.getStatus(); - expect(newStatus.providers).toHaveLength(2); - }); - }); - - describe('Configuration Validation', () => { - it('should reject invalid network configuration', () => { - const invalidNetworkConfig = { - ...baseConfig, - stellarNetwork: 'invalid' as any, - }; - - expect(() => new OracleService(invalidNetworkConfig)).toThrow(); - }); - - it('should reject invalid RPC URL', () => { - const invalidRpcConfig = { - ...baseConfig, - stellarRpcUrl: 'not-a-url', - }; - - expect(() => new OracleService(invalidRpcConfig)).toThrow(); - }); - - it('should reject empty contract ID', () => { - const emptyContractConfig = { - ...baseConfig, - contractId: '', - }; - - expect(() => new OracleService(emptyContractConfig)).toThrow(); - }); - - it('should reject negative price deviation threshold', () => { - const negativeDeviationConfig = { - ...baseConfig, - maxPriceDeviationPercent: -5, - }; - - // Should handle gracefully or throw - expect(() => new OracleService(negativeDeviationConfig)).not.toThrow(); - }); - - it('should reject zero staleness threshold', () => { - const zeroStalenessConfig = { - ...baseConfig, - priceStaleThresholdSeconds: 0, - }; - - // Should handle gracefully or throw - expect(() => new OracleService(zeroStalenessConfig)).not.toThrow(); - }); - - it('should reject negative cache TTL', () => { - const negativeCacheConfig = { - ...baseConfig, - cacheTtlSeconds: -30, - }; - - // Should handle gracefully or throw - expect(() => new OracleService(negativeCacheConfig)).not.toThrow(); - }); - - it('should reject negative update interval', () => { - const negativeIntervalConfig = { - ...baseConfig, - updateIntervalMs: -1000, - }; - - // Should handle gracefully or throw - expect(() => new OracleService(negativeIntervalConfig)).not.toThrow(); - }); - }); - - describe('Security Edge Cases', () => { - it('should handle configuration with no providers', () => { - const noProvidersConfig = { - ...baseConfig, - providers: [], - }; - - service = new OracleService(noProvidersConfig); - expect(service).toBeDefined(); - - const status = service.getStatus(); - expect(status.providers).toHaveLength(0); - }); - - it('should handle configuration with all providers disabled', () => { - const allDisabledConfig = { - ...baseConfig, - providers: baseConfig.providers.map(p => ({ ...p, enabled: false })), - }; - - service = new OracleService(allDisabledConfig); - expect(service).toBeDefined(); - - const status = service.getStatus(); - expect(status.providers.every(p => !p.enabled)).toBe(true); - }); - - it('should handle provider with zero weight', () => { - const zeroWeightConfig = { - ...baseConfig, - providers: [ - { ...baseConfig.providers[0], weight: 0 }, - { ...baseConfig.providers[1], weight: 1 }, - ], - }; - - service = new OracleService(zeroWeightConfig); - expect(service).toBeDefined(); - }); - - it('should handle provider with negative priority', () => { - const negativePriorityConfig = { - ...baseConfig, - providers: [ - { ...baseConfig.providers[0], priority: -1 }, - { ...baseConfig.providers[1], priority: 1 }, - ], - }; - - service = new OracleService(negativePriorityConfig); - expect(service).toBeDefined(); - }); - - it('should handle extremely large configuration values', () => { - const extremeConfig = { - ...baseConfig, - maxPriceDeviationPercent: 1000, - priceStaleThresholdSeconds: Number.MAX_SAFE_INTEGER, - cacheTtlSeconds: Number.MAX_SAFE_INTEGER, - updateIntervalMs: Number.MAX_SAFE_INTEGER, - }; - - service = new OracleService(extremeConfig); - expect(service).toBeDefined(); - }); - - it('should handle configuration with duplicate provider names', () => { - const duplicateConfig = { - ...baseConfig, - providers: [ - { ...baseConfig.providers[0] }, - { ...baseConfig.providers[0], name: 'coingecko' }, // Duplicate name - ], - }; - - service = new OracleService(duplicateConfig); - expect(service).toBeDefined(); - }); - }); - - describe('Configuration Persistence', () => { - it('should maintain configuration across service restarts', () => { - // Create service with custom config - const customConfig = { - ...baseConfig, - maxPriceDeviationPercent: 15, - priceStaleThresholdSeconds: 600, - }; - - service = new OracleService(customConfig); - const initialStatus = service.getStatus(); - - service.stop(); - - // Create new service with same config - const newService = new OracleService(customConfig); - const newStatus = newService.getStatus(); - - expect(newStatus).toBeDefined(); - newService.stop(); - }); - - it('should allow configuration updates through service recreation', () => { - service = new OracleService(baseConfig); - service.stop(); - - // Update configuration - const updatedConfig = { - ...baseConfig, - maxPriceDeviationPercent: 25, - providers: [ - { ...baseConfig.providers[0], enabled: false }, - baseConfig.providers[1], - ], - }; - - const newService = new OracleService(updatedConfig); - const status = newService.getStatus(); - - expect(status).toBeDefined(); - newService.stop(); - }); - }); - - describe('Error Handling in Configuration', () => { - it('should handle malformed provider configuration gracefully', () => { - const malformedConfig = { - ...baseConfig, - providers: [ - { - ...baseConfig.providers[0], - baseUrl: undefined as any, - rateLimit: null as any, - }, - ], - }; - - expect(() => new OracleService(malformedConfig)).not.toThrow(); - }); - - it('should handle missing optional configuration fields', () => { - const minimalConfig = { - stellarNetwork: 'testnet' as const, - stellarRpcUrl: 'https://soroban-testnet.stellar.org', - contractId: 'CTEST123', - adminSecretKey: 'STEST123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', - updateIntervalMs: 60000, - maxPriceDeviationPercent: 10, - priceStaleThresholdSeconds: 300, - cacheTtlSeconds: 30, - logLevel: 'info' as const, - providers: [], - }; - - service = new OracleService(minimalConfig); - expect(service).toBeDefined(); - }); - - it('should handle configuration with circular references', () => { - const config: any = { ...baseConfig }; - config.self = config; // Create circular reference - - expect(() => new OracleService(config)).not.toThrow(); - }); - }); - - describe('Performance Impact of Configuration', () => { - it('should handle rapid configuration changes', async () => { - const configs = Array.from({ length: 10 }, (_, i) => ({ - ...baseConfig, - maxPriceDeviationPercent: 5 + i, - })); - - const services = configs.map(config => new OracleService(config)); - - // All services should initialize successfully - services.forEach(s => expect(s).toBeDefined()); - - // Clean up - services.forEach(s => s.stop()); - }); - - it('should handle configuration with many providers', () => { - const manyProvidersConfig = { - ...baseConfig, - providers: Array.from({ length: 20 }, (_, i) => ({ - name: `provider_${i}`, - enabled: true, - priority: i + 1, - weight: 0.05, - baseUrl: `https://provider${i}.example.com`, - rateLimit: { maxRequests: 100, windowMs: 60000 }, - })), - }; - - service = new OracleService(manyProvidersConfig); - expect(service).toBeDefined(); - - const status = service.getStatus(); - expect(status.providers).toHaveLength(20); - }); + let service: OracleService; + let baseConfig: OracleServiceConfig; + + beforeEach(() => { + baseConfig = { + stellarNetwork: 'testnet', + stellarRpcUrl: 'https://soroban-testnet.stellar.org', + contractId: 'CTEST123', + adminSecretKey: 'STEST123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', + updateIntervalMs: 1000, + maxPriceDeviationPercent: 10, + priceStaleThresholdSeconds: 300, + cacheTtlSeconds: 30, + logLevel: 'error', + providers: [ + { + name: 'coingecko', + enabled: true, + priority: 1, + weight: 0.6, + baseUrl: 'https://api.coingecko.com/api/v3', + rateLimit: { maxRequests: 10, windowMs: 60000 }, + }, + { + name: 'binance', + enabled: true, + priority: 2, + weight: 0.4, + baseUrl: 'https://api.binance.com/api/v3', + rateLimit: { maxRequests: 1200, windowMs: 60000 }, + }, + ], + }; + }); + + afterEach(() => { + if (service) { + service.stop(); + } + }); + + describe('Provider Configuration Changes', () => { + it('should allow switching primary provider by priority', () => { + // Create config with different priority ordering + const switchedConfig = { + ...baseConfig, + providers: [ + { + ...baseConfig.providers[0], + priority: 2, // Demote CoinGecko + }, + { + ...baseConfig.providers[1], + priority: 1, // Promote Binance to primary + }, + ], + }; + + service = new OracleService(switchedConfig); + const status = service.getStatus(); + + expect(status).toBeDefined(); + expect(status.providers).toHaveLength(2); + + // Verify priority ordering changed + const binanceProvider = status.providers.find((p) => p.name === 'binance'); + const coingeckoProvider = status.providers.find((p) => p.name === 'coingecko'); + + expect(binanceProvider?.priority).toBe(1); + expect(coingeckoProvider?.priority).toBe(2); }); + + it('should allow adjusting provider weights', () => { + const adjustedConfig = { + ...baseConfig, + providers: [ + { + ...baseConfig.providers[0], + weight: 0.8, // Increase CoinGecko weight + }, + { + ...baseConfig.providers[1], + weight: 0.2, // Decrease Binance weight + }, + ], + }; + + service = new OracleService(adjustedConfig); + const status = service.getStatus(); + + const coingeckoProvider = status.providers.find((p) => p.name === 'coingecko'); + const binanceProvider = status.providers.find((p) => p.name === 'binance'); + + expect(coingeckoProvider?.weight).toBe(0.8); + expect(binanceProvider?.weight).toBe(0.2); + }); + + it('should allow enabling/disabling providers', () => { + const disabledConfig = { + ...baseConfig, + providers: [ + { + ...baseConfig.providers[0], + enabled: false, // Disable CoinGecko + }, + baseConfig.providers[1], // Keep Binance enabled + ], + }; + + service = new OracleService(disabledConfig); + const status = service.getStatus(); + + const coingeckoProvider = status.providers.find((p) => p.name === 'coingecko'); + const binanceProvider = status.providers.find((p) => p.name === 'binance'); + + expect(coingeckoProvider?.enabled).toBe(false); + expect(binanceProvider?.enabled).toBe(true); + }); + + it('should validate provider weight sum does not exceed 1', () => { + const invalidWeightConfig = { + ...baseConfig, + providers: [ + { + ...baseConfig.providers[0], + weight: 0.7, + }, + { + ...baseConfig.providers[1], + weight: 0.5, // Total: 1.2 > 1.0 + }, + ], + }; + + expect(() => new OracleService(invalidWeightConfig)).not.toThrow(); + // Service should still initialize but with warnings + service = new OracleService(invalidWeightConfig); + expect(service).toBeDefined(); + }); + + it('should handle provider configuration with single provider', () => { + const singleProviderConfig = { + ...baseConfig, + providers: [baseConfig.providers[0]], // Only CoinGecko + }; + + service = new OracleService(singleProviderConfig); + const status = service.getStatus(); + + expect(status.providers).toHaveLength(1); + expect(status.providers[0].name).toBe('coingecko'); + expect(status.providers[0].weight).toBe(1.0); + }); + }); + + describe('Oracle Parameter Configuration', () => { + it('should allow adjusting price deviation threshold', () => { + const tightDeviationConfig = { + ...baseConfig, + maxPriceDeviationPercent: 5, // Tighter threshold + }; + + service = new OracleService(tightDeviationConfig); + expect(service).toBeDefined(); + + const looseDeviationConfig = { + ...baseConfig, + maxPriceDeviationPercent: 20, // Looser threshold + }; + + service = new OracleService(looseDeviationConfig); + expect(service).toBeDefined(); + }); + + it('should allow adjusting staleness threshold', () => { + const freshConfig = { + ...baseConfig, + priceStaleThresholdSeconds: 60, // 1 minute + }; + + service = new OracleService(freshConfig); + expect(service).toBeDefined(); + + const staleConfig = { + ...baseConfig, + priceStaleThresholdSeconds: 7200, // 2 hours + }; + + service = new OracleService(staleConfig); + expect(service).toBeDefined(); + }); + + it('should allow adjusting cache TTL', () => { + const noCacheConfig = { + ...baseConfig, + cacheTtlSeconds: 0, // Disable caching + }; + + service = new OracleService(noCacheConfig); + expect(service).toBeDefined(); + + const longCacheConfig = { + ...baseConfig, + cacheTtlSeconds: 1800, // 30 minutes + }; + + service = new OracleService(longCacheConfig); + expect(service).toBeDefined(); + }); + + it('should allow adjusting update intervals', () => { + const rapidConfig = { + ...baseConfig, + updateIntervalMs: 500, // Very frequent updates + }; + + service = new OracleService(rapidConfig); + expect(service).toBeDefined(); + + const slowConfig = { + ...baseConfig, + updateIntervalMs: 300000, // 5 minutes + }; + + service = new OracleService(slowConfig); + expect(service).toBeDefined(); + }); + }); + + describe('Role Separation Enforcement', () => { + it('should enforce admin-only configuration changes', () => { + service = new OracleService(baseConfig); + + // Service should not expose configuration modification methods + expect(typeof (service as any).updateConfig).toBe('undefined'); + expect(typeof (service as any).setProviders).toBe('undefined'); + expect(typeof (service as any).modifyAdminKey).toBe('undefined'); + }); + + it('should validate admin credentials in configuration', () => { + const invalidAdminConfig = { + ...baseConfig, + adminSecretKey: '', // Empty admin key + }; + + // Should still initialize but may fail during operations + expect(() => new OracleService(invalidAdminConfig)).not.toThrow(); + }); + + it('should maintain role separation between price updates and configuration', () => { + service = new OracleService(baseConfig); + + // Price update operations should be available + expect(typeof service.updatePrices).toBe('function'); + expect(typeof service.fetchPrice).toBe('function'); + + // Configuration operations should not be exposed + expect(typeof (service as any).configureOracle).toBe('undefined'); + expect(typeof (service as any).setOracleProvider).toBe('undefined'); + }); + + it('should prevent unauthorized provider modifications', () => { + service = new OracleService(baseConfig); + const status = service.getStatus(); + + // Status should be read-only + expect(() => { + (status as any).providers = []; + }).not.toThrow(); + + // But internal configuration should remain unchanged + const newStatus = service.getStatus(); + expect(newStatus.providers).toHaveLength(2); + }); + }); + + describe('Configuration Validation', () => { + it('should reject invalid network configuration', () => { + const invalidNetworkConfig = { + ...baseConfig, + stellarNetwork: 'invalid' as any, + }; + + expect(() => new OracleService(invalidNetworkConfig)).toThrow(); + }); + + it('should reject invalid RPC URL', () => { + const invalidRpcConfig = { + ...baseConfig, + stellarRpcUrl: 'not-a-url', + }; + + expect(() => new OracleService(invalidRpcConfig)).toThrow(); + }); + + it('should reject empty contract ID', () => { + const emptyContractConfig = { + ...baseConfig, + contractId: '', + }; + + expect(() => new OracleService(emptyContractConfig)).toThrow(); + }); + + it('should reject negative price deviation threshold', () => { + const negativeDeviationConfig = { + ...baseConfig, + maxPriceDeviationPercent: -5, + }; + + // Should handle gracefully or throw + expect(() => new OracleService(negativeDeviationConfig)).not.toThrow(); + }); + + it('should reject zero staleness threshold', () => { + const zeroStalenessConfig = { + ...baseConfig, + priceStaleThresholdSeconds: 0, + }; + + // Should handle gracefully or throw + expect(() => new OracleService(zeroStalenessConfig)).not.toThrow(); + }); + + it('should reject negative cache TTL', () => { + const negativeCacheConfig = { + ...baseConfig, + cacheTtlSeconds: -30, + }; + + // Should handle gracefully or throw + expect(() => new OracleService(negativeCacheConfig)).not.toThrow(); + }); + + it('should reject negative update interval', () => { + const negativeIntervalConfig = { + ...baseConfig, + updateIntervalMs: -1000, + }; + + // Should handle gracefully or throw + expect(() => new OracleService(negativeIntervalConfig)).not.toThrow(); + }); + }); + + describe('Security Edge Cases', () => { + it('should handle configuration with no providers', () => { + const noProvidersConfig = { + ...baseConfig, + providers: [], + }; + + service = new OracleService(noProvidersConfig); + expect(service).toBeDefined(); + + const status = service.getStatus(); + expect(status.providers).toHaveLength(0); + }); + + it('should handle configuration with all providers disabled', () => { + const allDisabledConfig = { + ...baseConfig, + providers: baseConfig.providers.map((p) => ({ ...p, enabled: false })), + }; + + service = new OracleService(allDisabledConfig); + expect(service).toBeDefined(); + + const status = service.getStatus(); + expect(status.providers.every((p) => !p.enabled)).toBe(true); + }); + + it('should handle provider with zero weight', () => { + const zeroWeightConfig = { + ...baseConfig, + providers: [ + { ...baseConfig.providers[0], weight: 0 }, + { ...baseConfig.providers[1], weight: 1 }, + ], + }; + + service = new OracleService(zeroWeightConfig); + expect(service).toBeDefined(); + }); + + it('should handle provider with negative priority', () => { + const negativePriorityConfig = { + ...baseConfig, + providers: [ + { ...baseConfig.providers[0], priority: -1 }, + { ...baseConfig.providers[1], priority: 1 }, + ], + }; + + service = new OracleService(negativePriorityConfig); + expect(service).toBeDefined(); + }); + + it('should handle extremely large configuration values', () => { + const extremeConfig = { + ...baseConfig, + maxPriceDeviationPercent: 1000, + priceStaleThresholdSeconds: Number.MAX_SAFE_INTEGER, + cacheTtlSeconds: Number.MAX_SAFE_INTEGER, + updateIntervalMs: Number.MAX_SAFE_INTEGER, + }; + + service = new OracleService(extremeConfig); + expect(service).toBeDefined(); + }); + + it('should handle configuration with duplicate provider names', () => { + const duplicateConfig = { + ...baseConfig, + providers: [ + { ...baseConfig.providers[0] }, + { ...baseConfig.providers[0], name: 'coingecko' }, // Duplicate name + ], + }; + + service = new OracleService(duplicateConfig); + expect(service).toBeDefined(); + }); + }); + + describe('Configuration Persistence', () => { + it('should maintain configuration across service restarts', () => { + // Create service with custom config + const customConfig = { + ...baseConfig, + maxPriceDeviationPercent: 15, + priceStaleThresholdSeconds: 600, + }; + + service = new OracleService(customConfig); + const initialStatus = service.getStatus(); + + service.stop(); + + // Create new service with same config + const newService = new OracleService(customConfig); + const newStatus = newService.getStatus(); + + expect(newStatus).toBeDefined(); + newService.stop(); + }); + + it('should allow configuration updates through service recreation', () => { + service = new OracleService(baseConfig); + service.stop(); + + // Update configuration + const updatedConfig = { + ...baseConfig, + maxPriceDeviationPercent: 25, + providers: [{ ...baseConfig.providers[0], enabled: false }, baseConfig.providers[1]], + }; + + const newService = new OracleService(updatedConfig); + const status = newService.getStatus(); + + expect(status).toBeDefined(); + newService.stop(); + }); + }); + + describe('Error Handling in Configuration', () => { + it('should handle malformed provider configuration gracefully', () => { + const malformedConfig = { + ...baseConfig, + providers: [ + { + ...baseConfig.providers[0], + baseUrl: undefined as any, + rateLimit: null as any, + }, + ], + }; + + expect(() => new OracleService(malformedConfig)).not.toThrow(); + }); + + it('should handle missing optional configuration fields', () => { + const minimalConfig = { + stellarNetwork: 'testnet' as const, + stellarRpcUrl: 'https://soroban-testnet.stellar.org', + contractId: 'CTEST123', + adminSecretKey: 'STEST123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', + updateIntervalMs: 60000, + maxPriceDeviationPercent: 10, + priceStaleThresholdSeconds: 300, + cacheTtlSeconds: 30, + logLevel: 'info' as const, + providers: [], + }; + + service = new OracleService(minimalConfig); + expect(service).toBeDefined(); + }); + + it('should handle configuration with circular references', () => { + const config: any = { ...baseConfig }; + config.self = config; // Create circular reference + + expect(() => new OracleService(config)).not.toThrow(); + }); + }); + + describe('Performance Impact of Configuration', () => { + it('should handle rapid configuration changes', async () => { + const configs = Array.from({ length: 10 }, (_, i) => ({ + ...baseConfig, + maxPriceDeviationPercent: 5 + i, + })); + + const services = configs.map((config) => new OracleService(config)); + + // All services should initialize successfully + services.forEach((s) => expect(s).toBeDefined()); + + // Clean up + services.forEach((s) => s.stop()); + }); + + it('should handle configuration with many providers', () => { + const manyProvidersConfig = { + ...baseConfig, + providers: Array.from({ length: 20 }, (_, i) => ({ + name: `provider_${i}`, + enabled: true, + priority: i + 1, + weight: 0.05, + baseUrl: `https://provider${i}.example.com`, + rateLimit: { maxRequests: 100, windowMs: 60000 }, + })), + }; + + service = new OracleService(manyProvidersConfig); + expect(service).toBeDefined(); + + const status = service.getStatus(); + expect(status.providers).toHaveLength(20); + }); + }); }); diff --git a/oracle/tests/price-history.test.ts b/oracle/tests/price-history.test.ts index ca411f33..a3a555e7 100644 --- a/oracle/tests/price-history.test.ts +++ b/oracle/tests/price-history.test.ts @@ -7,354 +7,354 @@ import { PriceHistoryService, createPriceHistoryService } from '../src/services/ import type { AggregatedPrice } from '../src/types/index.js'; describe('PriceHistoryService', () => { - let service: PriceHistoryService; + let service: PriceHistoryService; + + beforeEach(() => { + vi.clearAllMocks(); + service = createPriceHistoryService({ maxEntries: 5 }); + }); + + describe('initialization', () => { + it('should create service with default config', () => { + const defaultService = createPriceHistoryService(); + expect(defaultService).toBeDefined(); + expect(defaultService).toBeInstanceOf(PriceHistoryService); + }); + + it('should create service with custom config', () => { + const customService = createPriceHistoryService({ maxEntries: 10 }); + expect(customService).toBeDefined(); + + const stats = customService.getStats(); + expect(stats.maxEntriesPerAsset).toBe(10); + }); + + it('should start with empty history', () => { + const stats = service.getStats(); + expect(stats.trackedAssets).toBe(0); + expect(stats.totalEntries).toBe(0); + expect(stats.assets).toEqual([]); + }); + }); + + describe('addPriceEntry', () => { + it('should add price entry for new asset', () => { + const timestamp = Date.now(); + service.addPriceEntry('XLM', 150000n, timestamp); + + const history = service.getPriceHistory('XLM'); + expect(history).toHaveLength(1); + expect(history[0]).toEqual({ + price: 150000n, + timestamp, + }); + + const stats = service.getStats(); + expect(stats.trackedAssets).toBe(1); + expect(stats.totalEntries).toBe(1); + expect(stats.assets).toContain('XLM'); + }); + + it('should handle asset case normalization', () => { + service.addPriceEntry('xlm', 150000n, Date.now()); + service.addPriceEntry('Xlm', 160000n, Date.now()); + + const history = service.getPriceHistory('XLM'); + expect(history).toHaveLength(2); + + const assets = service.getAssets(); + expect(assets).toContain('XLM'); + expect(assets).not.toContain('xlm'); + expect(assets).not.toContain('Xlm'); + }); + + it('should add multiple entries for same asset', () => { + const baseTime = Date.now(); + for (let i = 0; i < 3; i++) { + service.addPriceEntry('BTC', 50000000000n + BigInt(i * 1000), baseTime + i * 1000); + } + + const history = service.getPriceHistory('BTC'); + expect(history).toHaveLength(3); + expect(history[0].price).toBe(50000000000n); + expect(history[2].price).toBe(50000002000n); + }); + + it('should handle circular buffer behavior', () => { + // Fill beyond capacity + for (let i = 0; i < 7; i++) { + service.addPriceEntry('ETH', 1000000000n + BigInt(i * 1000), Date.now() + i * 1000); + } + + const history = service.getPriceHistory('ETH'); + expect(history).toHaveLength(5); // Should be limited to maxEntries + + // Should contain the last 5 entries (circular buffer) + expect(history[0].price).toBe(1000002000n); // Entry at index 2 + expect(history[4].price).toBe(1000006000n); // Entry at index 6 + }); + + it('should add aggregated price entry', () => { + const aggregatedPrice: AggregatedPrice = { + asset: 'USDC', + price: 1000000n, + sources: [], + timestamp: Math.floor(Date.now() / 1000), + confidence: 95, + }; + + service.addAggregatedPrice(aggregatedPrice); + + const history = service.getPriceHistory('USDC'); + expect(history).toHaveLength(1); + expect(history[0]).toEqual({ + price: 1000000n, + timestamp: aggregatedPrice.timestamp, + }); + }); + }); + + describe('getPriceHistory', () => { + beforeEach(() => { + const baseTime = Date.now(); + for (let i = 0; i < 5; i++) { + service.addPriceEntry('XLM', 150000n + BigInt(i * 1000), baseTime + i * 1000); + } + }); + it('should return all entries for asset', () => { + const history = service.getPriceHistory('XLM'); + expect(history).toHaveLength(5); + expect(history[0].price).toBe(150000n); + expect(history[4].price).toBe(154000n); + }); + + it('should return limited entries', () => { + const history = service.getPriceHistory('XLM', 3); + expect(history).toHaveLength(3); + }); + + it('should return empty array for non-existent asset', () => { + const history = service.getPriceHistory('NONEXISTENT'); + expect(history).toHaveLength(0); + }); + + it('should return entries in chronological order for circular buffer', () => { + // Fill beyond capacity to test circular buffer ordering + for (let i = 5; i < 8; i++) { + service.addPriceEntry('XLM', 150000n + BigInt(i * 1000), Date.now() + i * 1000); + } + + const history = service.getPriceHistory('XLM'); + expect(history).toHaveLength(5); + + // Should be in chronological order (oldest to newest) + for (let i = 1; i < history.length; i++) { + expect(history[i].timestamp).toBeGreaterThan(history[i - 1].timestamp); + } + }); + }); + + describe('calculateTWAP', () => { beforeEach(() => { - vi.clearAllMocks(); - service = createPriceHistoryService({ maxEntries: 5 }); - }); - - describe('initialization', () => { - it('should create service with default config', () => { - const defaultService = createPriceHistoryService(); - expect(defaultService).toBeDefined(); - expect(defaultService).toBeInstanceOf(PriceHistoryService); - }); - - it('should create service with custom config', () => { - const customService = createPriceHistoryService({ maxEntries: 10 }); - expect(customService).toBeDefined(); - - const stats = customService.getStats(); - expect(stats.maxEntriesPerAsset).toBe(10); - }); - - it('should start with empty history', () => { - const stats = service.getStats(); - expect(stats.trackedAssets).toBe(0); - expect(stats.totalEntries).toBe(0); - expect(stats.assets).toEqual([]); - }); - }); - - describe('addPriceEntry', () => { - it('should add price entry for new asset', () => { - const timestamp = Date.now(); - service.addPriceEntry('XLM', 150000n, timestamp); - - const history = service.getPriceHistory('XLM'); - expect(history).toHaveLength(1); - expect(history[0]).toEqual({ - price: 150000n, - timestamp, - }); - - const stats = service.getStats(); - expect(stats.trackedAssets).toBe(1); - expect(stats.totalEntries).toBe(1); - expect(stats.assets).toContain('XLM'); - }); - - it('should handle asset case normalization', () => { - service.addPriceEntry('xlm', 150000n, Date.now()); - service.addPriceEntry('Xlm', 160000n, Date.now()); - - const history = service.getPriceHistory('XLM'); - expect(history).toHaveLength(2); - - const assets = service.getAssets(); - expect(assets).toContain('XLM'); - expect(assets).not.toContain('xlm'); - expect(assets).not.toContain('Xlm'); - }); - - it('should add multiple entries for same asset', () => { - const baseTime = Date.now(); - for (let i = 0; i < 3; i++) { - service.addPriceEntry('BTC', 50000000000n + BigInt(i * 1000), baseTime + i * 1000); - } - - const history = service.getPriceHistory('BTC'); - expect(history).toHaveLength(3); - expect(history[0].price).toBe(50000000000n); - expect(history[2].price).toBe(50000002000n); - }); - - it('should handle circular buffer behavior', () => { - // Fill beyond capacity - for (let i = 0; i < 7; i++) { - service.addPriceEntry('ETH', 1000000000n + BigInt(i * 1000), Date.now() + i * 1000); - } - - const history = service.getPriceHistory('ETH'); - expect(history).toHaveLength(5); // Should be limited to maxEntries - - // Should contain the last 5 entries (circular buffer) - expect(history[0].price).toBe(1000002000n); // Entry at index 2 - expect(history[4].price).toBe(1000006000n); // Entry at index 6 - }); - - it('should add aggregated price entry', () => { - const aggregatedPrice: AggregatedPrice = { - asset: 'USDC', - price: 1000000n, - sources: [], - timestamp: Math.floor(Date.now() / 1000), - confidence: 95, - }; - - service.addAggregatedPrice(aggregatedPrice); - - const history = service.getPriceHistory('USDC'); - expect(history).toHaveLength(1); - expect(history[0]).toEqual({ - price: 1000000n, - timestamp: aggregatedPrice.timestamp, - }); - }); - }); - - describe('getPriceHistory', () => { - beforeEach(() => { - const baseTime = Date.now(); - for (let i = 0; i < 5; i++) { - service.addPriceEntry('XLM', 150000n + BigInt(i * 1000), baseTime + i * 1000); - } - }); - - it('should return all entries for asset', () => { - const history = service.getPriceHistory('XLM'); - expect(history).toHaveLength(5); - expect(history[0].price).toBe(150000n); - expect(history[4].price).toBe(154000n); - }); - - it('should return limited entries', () => { - const history = service.getPriceHistory('XLM', 3); - expect(history).toHaveLength(3); - }); - - it('should return empty array for non-existent asset', () => { - const history = service.getPriceHistory('NONEXISTENT'); - expect(history).toHaveLength(0); - }); - - it('should return entries in chronological order for circular buffer', () => { - // Fill beyond capacity to test circular buffer ordering - for (let i = 5; i < 8; i++) { - service.addPriceEntry('XLM', 150000n + BigInt(i * 1000), Date.now() + i * 1000); - } - - const history = service.getPriceHistory('XLM'); - expect(history).toHaveLength(5); - - // Should be in chronological order (oldest to newest) - for (let i = 1; i < history.length; i++) { - expect(history[i].timestamp).toBeGreaterThan(history[i - 1].timestamp); - } - }); - }); - - describe('calculateTWAP', () => { - beforeEach(() => { - const baseTime = Math.floor(Date.now() / 1000); - // Add entries with 1-second intervals - for (let i = 0; i < 5; i++) { - service.addPriceEntry('BTC', 50000000000n + BigInt(i * 1000000), baseTime - (4 - i) * 1000); - } - }); - - it('should calculate TWAP for time period', () => { - const twap = service.calculateTWAP('BTC', 3000); // 3 seconds - - expect(twap).toBeDefined(); - expect(twap!.asset).toBe('BTC'); - expect(twap!.periodSeconds).toBe(3000); - expect(twap!.dataPoints).toBeGreaterThan(1); - expect(twap!.startTime).toBeDefined(); - expect(twap!.endTime).toBeDefined(); - expect(twap!.twap).toBeGreaterThan(0n); - }); - - it('should return null for insufficient data', () => { - service.addPriceEntry('NEW', 100000n, Date.now()); - - const twap = service.calculateTWAP('NEW', 3000); - expect(twap).toBeNull(); - }); - - it('should return null for non-existent asset', () => { - const twap = service.calculateTWAP('NONEXISTENT', 3000); - expect(twap).toBeNull(); - }); - - it('should handle large time periods gracefully', () => { - const twap = service.calculateTWAP('BTC', 999999); // Very large period - expect(twap).toBeDefined(); - expect(twap!.dataPoints).toBeGreaterThan(0); - }); - - it('should calculate reasonable TWAP values', () => { - const twap = service.calculateTWAP('BTC', 5000); - - // TWAP should be between min and max prices - const history = service.getPriceHistory('BTC'); - const prices = history.map(h => h.price); - const minPrice = prices.reduce((a, b) => a < b ? a : b); - const maxPrice = prices.reduce((a, b) => a > b ? a : b); - - expect(twap!.twap).toBeGreaterThanOrEqual(minPrice); - expect(twap!.twap).toBeLessThanOrEqual(maxPrice); - }); - }); - - describe('getLatestPrice', () => { - beforeEach(() => { - const baseTime = Date.now(); - for (let i = 0; i < 3; i++) { - service.addPriceEntry('ETH', 1000000000n + BigInt(i * 1000), baseTime + i * 1000); - } - }); - - it('should return latest price for asset', () => { - const latest = service.getLatestPrice('ETH'); - expect(latest).toBeDefined(); - expect(latest!.price).toBe(1000002000n); // Last entry - }); - - it('should return null for non-existent asset', () => { - const latest = service.getLatestPrice('NONEXISTENT'); - expect(latest).toBeNull(); - }); - - it('should return null for empty history', () => { - const latest = service.getLatestPrice('EMPTY'); - expect(latest).toBeNull(); - }); - }); - - describe('getAssetStats', () => { - beforeEach(() => { - const baseTime = Date.now(); - service.addPriceEntry('XLM', 150000n, baseTime); - service.addPriceEntry('XLM', 160000n, baseTime + 1000); - service.addPriceEntry('XLM', 140000n, baseTime + 2000); - }); - - it('should return asset statistics', () => { - const stats = service.getAssetStats('XLM'); - - expect(stats.totalEntries).toBe(3); - expect(stats.oldestTimestamp).toBeDefined(); - expect(stats.newestTimestamp).toBeDefined(); - expect(stats.priceRange).toBeDefined(); - expect(stats.priceRange!.min).toBe(140000n); - expect(stats.priceRange!.max).toBe(160000n); - }); - - it('should return empty stats for non-existent asset', () => { - const stats = service.getAssetStats('NONEXISTENT'); - expect(stats.totalEntries).toBe(0); - expect(stats.oldestTimestamp).toBeUndefined(); - expect(stats.newestTimestamp).toBeUndefined(); - expect(stats.priceRange).toBeUndefined(); - }); - }); - - describe('clearHistory', () => { - beforeEach(() => { - service.addPriceEntry('XLM', 150000n, Date.now()); - service.addPriceEntry('BTC', 50000000000n, Date.now()); - }); - - it('should clear history for specific asset', () => { - service.clearHistory('XLM'); - - expect(service.getPriceHistory('XLM')).toHaveLength(0); - expect(service.getPriceHistory('BTC')).toHaveLength(1); - expect(service.getAssets()).not.toContain('XLM'); - expect(service.getAssets()).toContain('BTC'); - }); - - it('should clear all history', () => { - service.clearAllHistory(); - - expect(service.getPriceHistory('XLM')).toHaveLength(0); - expect(service.getPriceHistory('BTC')).toHaveLength(0); - expect(service.getAssets()).toHaveLength(0); - - const stats = service.getStats(); - expect(stats.trackedAssets).toBe(0); - expect(stats.totalEntries).toBe(0); - }); - }); - - describe('getAssets', () => { - it('should return list of tracked assets', () => { - service.addPriceEntry('XLM', 150000n, Date.now()); - service.addPriceEntry('BTC', 50000000000n, Date.now()); - service.addPriceEntry('ETH', 1000000000n, Date.now()); - - const assets = service.getAssets(); - expect(assets).toHaveLength(3); - expect(assets).toContain('XLM'); - expect(assets).toContain('BTC'); - expect(assets).toContain('ETH'); - }); - - it('should return empty list when no assets tracked', () => { - const assets = service.getAssets(); - expect(assets).toHaveLength(0); - }); - }); - - describe('getStats', () => { - it('should return comprehensive statistics', () => { - service.addPriceEntry('XLM', 150000n, Date.now()); - service.addPriceEntry('BTC', 50000000000n, Date.now()); - - // Fill XLM beyond capacity to test circular buffer counting - for (let i = 0; i < 5; i++) { - service.addPriceEntry('XLM', 150000n + BigInt(i * 1000), Date.now() + i * 1000); - } - - const stats = service.getStats(); - - expect(stats.trackedAssets).toBe(2); - expect(stats.totalEntries).toBeGreaterThan(0); - expect(stats.maxEntriesPerAsset).toBe(5); - expect(stats.assets).toContain('XLM'); - expect(stats.assets).toContain('BTC'); - }); - }); - - describe('edge cases', () => { - it('should handle zero price values', () => { - service.addPriceEntry('ZERO', 0n, Date.now()); - - const history = service.getPriceHistory('ZERO'); - expect(history).toHaveLength(1); - expect(history[0].price).toBe(0n); - }); - - it('should handle very large price values', () => { - const largePrice = 999999999999999999n; - service.addPriceEntry('LARGE', largePrice, Date.now()); - - const history = service.getPriceHistory('LARGE'); - expect(history).toHaveLength(1); - expect(history[0].price).toBe(largePrice); - }); - - it('should handle duplicate timestamps', () => { - const timestamp = Date.now(); - service.addPriceEntry('DUP', 100000n, timestamp); - service.addPriceEntry('DUP', 110000n, timestamp); - - const history = service.getPriceHistory('DUP'); - expect(history).toHaveLength(2); - expect(history[0].timestamp).toBe(timestamp); - expect(history[1].timestamp).toBe(timestamp); - }); + const baseTime = Math.floor(Date.now() / 1000); + // Add entries with 1-second intervals + for (let i = 0; i < 5; i++) { + service.addPriceEntry('BTC', 50000000000n + BigInt(i * 1000000), baseTime - (4 - i) * 1000); + } + }); + + it('should calculate TWAP for time period', () => { + const twap = service.calculateTWAP('BTC', 3000); // 3 seconds + + expect(twap).toBeDefined(); + expect(twap!.asset).toBe('BTC'); + expect(twap!.periodSeconds).toBe(3000); + expect(twap!.dataPoints).toBeGreaterThan(1); + expect(twap!.startTime).toBeDefined(); + expect(twap!.endTime).toBeDefined(); + expect(twap!.twap).toBeGreaterThan(0n); + }); + + it('should return null for insufficient data', () => { + service.addPriceEntry('NEW', 100000n, Date.now()); + + const twap = service.calculateTWAP('NEW', 3000); + expect(twap).toBeNull(); + }); + + it('should return null for non-existent asset', () => { + const twap = service.calculateTWAP('NONEXISTENT', 3000); + expect(twap).toBeNull(); + }); + + it('should handle large time periods gracefully', () => { + const twap = service.calculateTWAP('BTC', 999999); // Very large period + expect(twap).toBeDefined(); + expect(twap!.dataPoints).toBeGreaterThan(0); + }); + + it('should calculate reasonable TWAP values', () => { + const twap = service.calculateTWAP('BTC', 5000); + + // TWAP should be between min and max prices + const history = service.getPriceHistory('BTC'); + const prices = history.map((h) => h.price); + const minPrice = prices.reduce((a, b) => (a < b ? a : b)); + const maxPrice = prices.reduce((a, b) => (a > b ? a : b)); + + expect(twap!.twap).toBeGreaterThanOrEqual(minPrice); + expect(twap!.twap).toBeLessThanOrEqual(maxPrice); + }); + }); + + describe('getLatestPrice', () => { + beforeEach(() => { + const baseTime = Date.now(); + for (let i = 0; i < 3; i++) { + service.addPriceEntry('ETH', 1000000000n + BigInt(i * 1000), baseTime + i * 1000); + } + }); + + it('should return latest price for asset', () => { + const latest = service.getLatestPrice('ETH'); + expect(latest).toBeDefined(); + expect(latest!.price).toBe(1000002000n); // Last entry + }); + + it('should return null for non-existent asset', () => { + const latest = service.getLatestPrice('NONEXISTENT'); + expect(latest).toBeNull(); + }); + + it('should return null for empty history', () => { + const latest = service.getLatestPrice('EMPTY'); + expect(latest).toBeNull(); + }); + }); + + describe('getAssetStats', () => { + beforeEach(() => { + const baseTime = Date.now(); + service.addPriceEntry('XLM', 150000n, baseTime); + service.addPriceEntry('XLM', 160000n, baseTime + 1000); + service.addPriceEntry('XLM', 140000n, baseTime + 2000); + }); + + it('should return asset statistics', () => { + const stats = service.getAssetStats('XLM'); + + expect(stats.totalEntries).toBe(3); + expect(stats.oldestTimestamp).toBeDefined(); + expect(stats.newestTimestamp).toBeDefined(); + expect(stats.priceRange).toBeDefined(); + expect(stats.priceRange!.min).toBe(140000n); + expect(stats.priceRange!.max).toBe(160000n); + }); + + it('should return empty stats for non-existent asset', () => { + const stats = service.getAssetStats('NONEXISTENT'); + expect(stats.totalEntries).toBe(0); + expect(stats.oldestTimestamp).toBeUndefined(); + expect(stats.newestTimestamp).toBeUndefined(); + expect(stats.priceRange).toBeUndefined(); + }); + }); + + describe('clearHistory', () => { + beforeEach(() => { + service.addPriceEntry('XLM', 150000n, Date.now()); + service.addPriceEntry('BTC', 50000000000n, Date.now()); + }); + + it('should clear history for specific asset', () => { + service.clearHistory('XLM'); + + expect(service.getPriceHistory('XLM')).toHaveLength(0); + expect(service.getPriceHistory('BTC')).toHaveLength(1); + expect(service.getAssets()).not.toContain('XLM'); + expect(service.getAssets()).toContain('BTC'); + }); + + it('should clear all history', () => { + service.clearAllHistory(); + + expect(service.getPriceHistory('XLM')).toHaveLength(0); + expect(service.getPriceHistory('BTC')).toHaveLength(0); + expect(service.getAssets()).toHaveLength(0); + + const stats = service.getStats(); + expect(stats.trackedAssets).toBe(0); + expect(stats.totalEntries).toBe(0); + }); + }); + + describe('getAssets', () => { + it('should return list of tracked assets', () => { + service.addPriceEntry('XLM', 150000n, Date.now()); + service.addPriceEntry('BTC', 50000000000n, Date.now()); + service.addPriceEntry('ETH', 1000000000n, Date.now()); + + const assets = service.getAssets(); + expect(assets).toHaveLength(3); + expect(assets).toContain('XLM'); + expect(assets).toContain('BTC'); + expect(assets).toContain('ETH'); + }); + + it('should return empty list when no assets tracked', () => { + const assets = service.getAssets(); + expect(assets).toHaveLength(0); + }); + }); + + describe('getStats', () => { + it('should return comprehensive statistics', () => { + service.addPriceEntry('XLM', 150000n, Date.now()); + service.addPriceEntry('BTC', 50000000000n, Date.now()); + + // Fill XLM beyond capacity to test circular buffer counting + for (let i = 0; i < 5; i++) { + service.addPriceEntry('XLM', 150000n + BigInt(i * 1000), Date.now() + i * 1000); + } + + const stats = service.getStats(); + + expect(stats.trackedAssets).toBe(2); + expect(stats.totalEntries).toBeGreaterThan(0); + expect(stats.maxEntriesPerAsset).toBe(5); + expect(stats.assets).toContain('XLM'); + expect(stats.assets).toContain('BTC'); + }); + }); + + describe('edge cases', () => { + it('should handle zero price values', () => { + service.addPriceEntry('ZERO', 0n, Date.now()); + + const history = service.getPriceHistory('ZERO'); + expect(history).toHaveLength(1); + expect(history[0].price).toBe(0n); + }); + + it('should handle very large price values', () => { + const largePrice = 999999999999999999n; + service.addPriceEntry('LARGE', largePrice, Date.now()); + + const history = service.getPriceHistory('LARGE'); + expect(history).toHaveLength(1); + expect(history[0].price).toBe(largePrice); + }); + + it('should handle duplicate timestamps', () => { + const timestamp = Date.now(); + service.addPriceEntry('DUP', 100000n, timestamp); + service.addPriceEntry('DUP', 110000n, timestamp); + + const history = service.getPriceHistory('DUP'); + expect(history).toHaveLength(2); + expect(history[0].timestamp).toBe(timestamp); + expect(history[1].timestamp).toBe(timestamp); }); + }); }); diff --git a/oracle/tests/staleness.test.ts b/oracle/tests/staleness.test.ts index f77100fa..b6cf1274 100644 --- a/oracle/tests/staleness.test.ts +++ b/oracle/tests/staleness.test.ts @@ -8,139 +8,148 @@ import { logger, logStalenessAlert } from '../src/utils/logger.js'; // Mock logger to verify calls vi.mock('../src/utils/logger.js', async () => { - const actual = await vi.importActual('../src/utils/logger.js'); - return { - ...actual, - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, - logStalenessAlert: vi.fn(), - }; + const actual = await vi.importActual('../src/utils/logger.js'); + return { + ...actual, + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + logStalenessAlert: vi.fn(), + }; }); // Mock contract updater vi.mock('../src/services/contract-updater.js', () => ({ - createContractUpdater: vi.fn(() => ({ - updatePrices: vi.fn().mockResolvedValue([{ success: true, asset: 'XLM', price: 100n, timestamp: Date.now() }]), - healthCheck: vi.fn().mockResolvedValue(true), - getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), - })), - ContractUpdater: vi.fn(), + createContractUpdater: vi.fn(() => ({ + updatePrices: vi + .fn() + .mockResolvedValue([{ success: true, asset: 'XLM', price: 100n, timestamp: Date.now() }]), + healthCheck: vi.fn().mockResolvedValue(true), + getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), + })), + ContractUpdater: vi.fn(), })); const mockAggregator = { - getPrices: vi.fn().mockResolvedValue(new Map([['XLM', { asset: 'XLM', price: 100n, timestamp: Date.now() }]])), - getPrice: vi.fn(), - getProviders: vi.fn().mockReturnValue([]), - getStats: vi.fn().mockReturnValue({}), - getCircuitBreakerMetrics: vi.fn().mockReturnValue([]), + getPrices: vi + .fn() + .mockResolvedValue(new Map([['XLM', { asset: 'XLM', price: 100n, timestamp: Date.now() }]])), + getPrice: vi.fn(), + getProviders: vi.fn().mockReturnValue([]), + getStats: vi.fn().mockReturnValue({}), + getCircuitBreakerMetrics: vi.fn().mockReturnValue([]), }; vi.mock('../src/services/index.js', () => ({ - createValidator: vi.fn(() => ({ validate: vi.fn() })), - createPriceCache: vi.fn(() => ({ get: vi.fn(), set: vi.fn(), getStats: vi.fn(() => ({})) })), - createPriceHistoryService: vi.fn(() => ({ addAggregatedPrice: vi.fn(), getStats: vi.fn(() => ({})) })), - createAggregator: vi.fn(() => mockAggregator), - createContractUpdater: vi.fn(() => ({ - updatePrices: vi.fn().mockResolvedValue([{ success: true, asset: 'XLM', price: 100n, timestamp: Date.now() }]), - healthCheck: vi.fn().mockResolvedValue(true), - getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), - })), + createValidator: vi.fn(() => ({ validate: vi.fn() })), + createPriceCache: vi.fn(() => ({ get: vi.fn(), set: vi.fn(), getStats: vi.fn(() => ({})) })), + createPriceHistoryService: vi.fn(() => ({ + addAggregatedPrice: vi.fn(), + getStats: vi.fn(() => ({})), + })), + createAggregator: vi.fn(() => mockAggregator), + createContractUpdater: vi.fn(() => ({ + updatePrices: vi + .fn() + .mockResolvedValue([{ success: true, asset: 'XLM', price: 100n, timestamp: Date.now() }]), + healthCheck: vi.fn().mockResolvedValue(true), + getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), + })), })); describe('Oracle Price Staleness Detection', () => { - let service: OracleService; - const STALE_THRESHOLD = 300; // 5 minutes - - const mockConfig: any = { - stellarNetwork: 'testnet', - stellarRpcUrl: 'http://localhost:8000', - contractId: 'CTEST123', - adminSecretKey: 'S123', - updateIntervalMs: 60000, - maxPriceDeviationPercent: 10, - priceStaleThresholdSeconds: STALE_THRESHOLD, - cacheTtlSeconds: 30, - logLevel: 'info', - providers: [], - }; - - beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-03-24T12:00:00Z')); - service = new OracleService(mockConfig); - }); - - afterEach(() => { - vi.useRealTimers(); - vi.restoreAllMocks(); - }); - - it('should not log staleness alert on first update', async () => { - await service.updatePrices(['XLM']); - expect(logStalenessAlert).not.toHaveBeenCalled(); - }); - - it('should not log staleness alert if update happens within threshold', async () => { - // First successful update - await service.updatePrices(['XLM']); - - // Advance time by 4 minutes (less than 5m threshold) - vi.advanceTimersByTime(4 * 60 * 1000); - - await service.updatePrices(['XLM']); - expect(logStalenessAlert).not.toHaveBeenCalled(); - }); - - it('should log staleness alert if update age exceeds threshold', async () => { - // First successful update - await service.updatePrices(['XLM']); - const firstUpdate = (service as any).lastSuccessfulUpdate; - - // Advance time by 6 minutes (more than 5m threshold) - vi.advanceTimersByTime(6 * 60 * 1000); - - await service.updatePrices(['XLM']); - expect(logger.info).toHaveBeenCalled(); - }); - - it('should update lastSuccessfulUpdate after a successful cycle', async () => { - // First update - await service.updatePrices(['XLM']); - - // Advance time by 4 minutes - vi.advanceTimersByTime(4 * 60 * 1000); - await service.updatePrices(['XLM']); - - // Advance another 4 minutes (total 8 from start, but only 4 from last update) - vi.advanceTimersByTime(4 * 60 * 1000); - await service.updatePrices(['XLM']); - - expect(logStalenessAlert).not.toHaveBeenCalled(); - }); - - it('should log alert even if price fetching fails but cycle starts', async () => { - // First success - await service.updatePrices(['XLM']); - const firstUpdate = (service as any).lastSuccessfulUpdate; - - // Advance beyond threshold - vi.advanceTimersByTime(6 * 60 * 1000); - - // Mock failure for the NEXT getPrices call - mockAggregator.getPrices.mockRejectedValueOnce(new Error('API Down')); - - await service.updatePrices(['XLM']); - - expect((service as any).lastSuccessfulUpdate).toBe(firstUpdate); - expect(logger.error).toHaveBeenCalledWith( - 'Price update cycle failed', - expect.objectContaining({ - error: expect.any(Error), - }) - ); - }); + let service: OracleService; + const STALE_THRESHOLD = 300; // 5 minutes + + const mockConfig: any = { + stellarNetwork: 'testnet', + stellarRpcUrl: 'http://localhost:8000', + contractId: 'CTEST123', + adminSecretKey: 'S123', + updateIntervalMs: 60000, + maxPriceDeviationPercent: 10, + priceStaleThresholdSeconds: STALE_THRESHOLD, + cacheTtlSeconds: 30, + logLevel: 'info', + providers: [], + }; + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-03-24T12:00:00Z')); + service = new OracleService(mockConfig); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('should not log staleness alert on first update', async () => { + await service.updatePrices(['XLM']); + expect(logStalenessAlert).not.toHaveBeenCalled(); + }); + + it('should not log staleness alert if update happens within threshold', async () => { + // First successful update + await service.updatePrices(['XLM']); + + // Advance time by 4 minutes (less than 5m threshold) + vi.advanceTimersByTime(4 * 60 * 1000); + + await service.updatePrices(['XLM']); + expect(logStalenessAlert).not.toHaveBeenCalled(); + }); + + it('should log staleness alert if update age exceeds threshold', async () => { + // First successful update + await service.updatePrices(['XLM']); + const firstUpdate = (service as any).lastSuccessfulUpdate; + + // Advance time by 6 minutes (more than 5m threshold) + vi.advanceTimersByTime(6 * 60 * 1000); + + await service.updatePrices(['XLM']); + expect(logger.info).toHaveBeenCalled(); + }); + + it('should update lastSuccessfulUpdate after a successful cycle', async () => { + // First update + await service.updatePrices(['XLM']); + + // Advance time by 4 minutes + vi.advanceTimersByTime(4 * 60 * 1000); + await service.updatePrices(['XLM']); + + // Advance another 4 minutes (total 8 from start, but only 4 from last update) + vi.advanceTimersByTime(4 * 60 * 1000); + await service.updatePrices(['XLM']); + + expect(logStalenessAlert).not.toHaveBeenCalled(); + }); + + it('should log alert even if price fetching fails but cycle starts', async () => { + // First success + await service.updatePrices(['XLM']); + const firstUpdate = (service as any).lastSuccessfulUpdate; + + // Advance beyond threshold + vi.advanceTimersByTime(6 * 60 * 1000); + + // Mock failure for the NEXT getPrices call + mockAggregator.getPrices.mockRejectedValueOnce(new Error('API Down')); + + await service.updatePrices(['XLM']); + + expect((service as any).lastSuccessfulUpdate).toBe(firstUpdate); + expect(logger.error).toHaveBeenCalledWith( + 'Price update cycle failed', + expect.objectContaining({ + error: expect.any(Error), + }) + ); + }); }); diff --git a/oracle/tests/trace-analysis.test.ts b/oracle/tests/trace-analysis.test.ts index f85b12d7..d6d1d60e 100644 --- a/oracle/tests/trace-analysis.test.ts +++ b/oracle/tests/trace-analysis.test.ts @@ -33,7 +33,9 @@ describe('trace-analysis', () => { gasUsed: 8, cpuInstructions: 60, memoryBytes: 24, - stateChanges: [{ key: 'balance:pool', operation: 'update', before: '100', after: '75' }], + stateChanges: [ + { key: 'balance:pool', operation: 'update', before: '100', after: '75' }, + ], }, ], }, @@ -109,4 +111,4 @@ describe('trace-analysis', () => { overheadPercent: 25, }); }); -}); \ No newline at end of file +}); diff --git a/stellar-lend/Cargo.lock b/stellar-lend/Cargo.lock index 96277948..aa2e5240 100644 --- a/stellar-lend/Cargo.lock +++ b/stellar-lend/Cargo.lock @@ -242,12 +242,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.0" @@ -317,6 +311,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "cfg_eval" version = "0.1.2" @@ -357,16 +357,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -706,15 +696,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -899,9 +880,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -915,25 +898,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.13" @@ -945,7 +909,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap 2.13.0", "slab", "tokio", @@ -1040,17 +1004,6 @@ dependencies = [ "digest", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -1061,17 +1014,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1079,7 +1021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -1090,8 +1032,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1107,30 +1049,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -1141,9 +1059,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1156,16 +1074,18 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", + "http", + "hyper", + "hyper-util", "rustls", "tokio", "tokio-rustls", + "tower-service", + "webpki-roots", ] [[package]] @@ -1174,12 +1094,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", + "tower-service", + "tracing", ] [[package]] @@ -1358,6 +1287,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1443,6 +1382,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "macro-string" version = "0.1.4" @@ -1469,12 +1414,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "mio" version = "1.1.1" @@ -1496,10 +1435,10 @@ dependencies = [ "bytes", "colored", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "log", "pin-project-lite", @@ -1702,6 +1641,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -1782,7 +1776,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -1836,43 +1830,40 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-rustls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", + "quinn", "rustls", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-rustls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", ] [[package]] @@ -1899,6 +1890,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1910,32 +1907,36 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ - "log", + "once_cell", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "base64 0.21.7", + "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -1992,16 +1993,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sec1" version = "0.7.3" @@ -2182,16 +2173,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.2" @@ -2305,7 +2286,7 @@ dependencies = [ "serde_with", "soroban-env-common", "soroban-env-host", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2360,7 +2341,7 @@ checksum = "2bc4fef2cad410563bbd56f9fa68731268f89e90a4d7e6c4d62adb45c0b4c571" dependencies = [ "base64 0.22.1", "stellar-xdr", - "thiserror", + "thiserror 1.0.69", "wasmparser", ] @@ -2377,7 +2358,7 @@ dependencies = [ "soroban-spec", "stellar-xdr", "syn 2.0.117", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2515,7 +2496,7 @@ dependencies = [ "serde", "serde_json", "test-case", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-test", "tracing", @@ -2576,9 +2557,12 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2591,27 +2575,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "test-case" version = "3.3.1" @@ -2651,7 +2614,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2665,6 +2637,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -2715,6 +2698,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -2727,7 +2725,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -2745,9 +2743,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2788,6 +2786,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3050,11 +3087,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" -version = "0.25.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "windows-core" @@ -3115,15 +3165,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3151,21 +3192,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -3199,12 +3225,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3217,12 +3237,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3235,12 +3249,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3265,12 +3273,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3283,12 +3285,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3301,12 +3297,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3319,12 +3309,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3337,16 +3321,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wiremock" version = "0.6.5" @@ -3357,9 +3331,9 @@ dependencies = [ "base64 0.22.1", "deadpool", "futures", - "http 1.4.0", + "http", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "log", "once_cell", diff --git a/stellar-lend/client/Cargo.toml b/stellar-lend/client/Cargo.toml index fb6eebc3..9a88b162 100644 --- a/stellar-lend/client/Cargo.toml +++ b/stellar-lend/client/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" [dependencies] # HTTP client for API calls -reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } tokio = { version = "1.35", features = ["full"] } # Serialization