From 44bdeaaa1f672821afbda74466cae6157d13bcab Mon Sep 17 00:00:00 2001 From: abduldayantech Date: Wed, 21 Oct 2026 10:12:52 +0000 Subject: [PATCH] potential --- babel.config.json | 5 + package.json | 76 +++++++++++ src/claim/claim.scheduler.ts | 2 - src/config/configuration.ts | 241 +++++++++++++++++++++++++++-------- src/policy/policy.service.ts | 17 ++- 5 files changed, 286 insertions(+), 55 deletions(-) create mode 100644 babel.config.json diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 0000000..0f94361 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "@babel/plugin-transform-modules-commonjs" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index e69de29..e4b3584 100644 --- a/package.json +++ b/package.json @@ -0,0 +1,76 @@ +{ + "name": "refract-protocol-backend", + "version": "0.1.0", + "description": "Refract protocol backend", + "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.1142.0", + "@nestjs/common": "^11.1.28", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.28", + "@nestjs/platform-express": "^11.1.28", + "@nestjs/platform-ws": "^11.1.28", + "@nestjs/schedule": "^6.1.3", + "@nestjs/swagger": "^11.4.7", + "@nestjs/websockets": "^11.1.28", + "@prometheus-io/client": "^0.16.1", + "@stellar/stellar-sdk": "^16.3.0", + "axios": "^1.6.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "ioredis": "^5.3.2", + "nest-winston": "^1.10.2", + "node-pg-migrate": "7.9.1", + "pg": "^8.11.3", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "winston": "^3.11.0", + "ws": "^8.16.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@nestjs/cli": "^11.0.24", + "@nestjs/schematics": "^11.1.0", + "@nestjs/testing": "^11.1.28", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^30.0.0", + "@types/node": "^20.10.0", + "@types/pg": "^8.10.9", + "@types/supertest": "^7.2.0", + "@types/ws": "^8.5.10", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "babel-jest": "^30.5.2", + "eslint": "^8.57.0", + "fast-check": "^4.10.2", + "jest": "^30.4.2", + "nodemon": "^3.0.2", + "supertest": "^7.2.2", + "ts-jest": "^29.4.11", + "ts-node": "^10.9.2", + "typescript": "^5.3.2" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "jest" + }, + "jest": { + "testEnvironment": "node", + "testMatch": [ + "**/src/**/*.spec.ts" + ], + "transform": { + "^.+\\.ts$": "ts-jest", + "^.+\\.js$": "babel-jest" + }, + "transformIgnorePatterns": [ + "/node_modules/(?!(@stellar|@noble|@scure|stellar-base|bignumber.js|crc|tweetnacl|base32.js|uint8array-extras|@adraffy|@noble/curves)/)" + ] + } +} \ No newline at end of file diff --git a/src/claim/claim.scheduler.ts b/src/claim/claim.scheduler.ts index 1cefc74..858012c 100644 --- a/src/claim/claim.scheduler.ts +++ b/src/claim/claim.scheduler.ts @@ -149,6 +149,4 @@ export class ClaimScheduler implements OnModuleInit, OnApplicationShutdown { }); }); } -} - } } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 15da439..27b9a51 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -1,59 +1,196 @@ -import { registerAs } from '@nestjs/config'; - /** - * Resolves the sandbox-mode flag from the environment. - * - * Sandbox mode swaps every real backing service (Postgres repositories, - * Redis cache, Soroban RPC calls) for lightweight in-memory/mocked - * equivalents so a fresh clone can run with `npm install && npm run dev` - * and no other setup. + * Typed application configuration, loaded once by Nest's ConfigModule. * - * Safety: sandbox mode is a contributor-onboarding tool only. It must never - * be reachable from a deployed environment, so we refuse to enable it when - * NODE_ENV=production (see assertSandboxSafety below). + * Kept as a single factory function (rather than scattered `process.env` + * reads) so every consumer gets the same parsed/defaulted values and the + * shape is documented in one place. See `.env.example` for the full list + * of variables this reads. */ -export function isSandboxMode(env: NodeJS.ProcessEnv = process.env): boolean { - const raw = (env.SANDBOX_MODE ?? '').trim().toLowerCase(); - return raw === 'true' || raw === '1' || raw === 'yes' || raw === 'on'; -} -/** - * Boot-time safety check: refuse to start in sandbox mode when running in a - * production-configured deployment, even if SANDBOX_MODE was set by accident. - * Throws so the process fails fast rather than silently serving mocked data. - */ -export function assertSandboxSafety(env: NodeJS.ProcessEnv = process.env): void { - if (!isSandboxMode(env)) { - return; - } +/** Fee-estimation profiles exposed by FeeStrategyService. */ +export type FeeProfileName = "moderate" | "aggressive"; - const nodeEnv = (env.NODE_ENV ?? '').trim().toLowerCase(); - if (nodeEnv === 'production') { - throw new Error( - 'SANDBOX_MODE is enabled but NODE_ENV=production. Sandbox mode is a ' + - 'local contributor-onboarding tool and must never run in a deployed ' + - 'environment. Unset SANDBOX_MODE or set NODE_ENV to a non-production value.', - ); - } -} - -/** - * Prominent startup banner so sandbox mode is never mistaken for a real - * environment. Returns the lines to log; callers decide how to emit them. - */ -export function sandboxStartupBanner(): string[] { - return [ - '============================================================', - ' SANDBOX MODE ENABLED — NOT A REAL ENVIRONMENT', - ' Postgres, Redis, and Soroban RPC are replaced with', - ' in-memory mocks. All data resets on restart.', - ' Do not use this mode outside local development.', - '============================================================', - ]; +export interface AppConfig { + port: number; + frontendUrl: string; + database: { + url: string; + }; + redis: { + url: string; + }; + stellar: { + network: string; + sorobanRpcUrl: string; + networkPassphrase: string; + poolContractId: string; + policyContractId: string; + oracleContractId: string; + relayerSecret: string; + }; + /** + * Shared Soroban confirmation poller defaults (see + * `pollForConfirmation`). Per-call overrides let the HTTP submit path + * use a short deadline while settlement/oracle publish use a longer one. + */ + confirmation: { + initialIntervalMs: number; + backoffMultiplier: number; + maxIntervalMs: number; + deadlineMs: number; + jitterRatio: number; + /** Longer deadline for relayer-signed settlement / oracle publishes. */ + settlementDeadlineMs: number; + /** Shorter deadline so HTTP handlers don't block for a full minute. */ + httpDeadlineMs: number; + }; + oraclePublish: { + /** + * `every_poll` — publish on every scheduler tick (subject to min interval). + * `on_change` — publish only when the scaled fixed-point value changes + * (default; bounds fee spend). + */ + mode: "every_poll" | "on_change"; + /** Floor between publishes for the same coverage type (ms). */ + minIntervalMs: number; + }; + oracles: { + coingeckoBaseUrl: string; + horizonUrl: string; + defiLlamaBaseUrl: string; + defiLlamaProtocolSlug: string; + httpTimeoutMs: number; + }; + /** Soroban inclusion-fee estimation strategy. */ + fees: { + /** Hard ceiling (stroops) above which FeeCeilingExceededError is thrown. */ + ceilingStroops: string; + /** TTL for the cached getFeeStats response (ms). */ + statsTtlMs: number; + /** Per-profile percentile + multiplier used to derive an inclusion fee. */ + profiles: Record; + }; + /** Background Soroban event indexer settings. */ + eventIndexer: { + enabled: boolean; + pollIntervalMs: number; + lagAlertLedgers: number; + pageLimit: number; + }; + /** Ops-facing auth (bearer token + read-only community keys). */ + ops: { + apiToken: string; + communityApiKeys: string[]; + }; + /** Admin-only API key (x-api-key header). */ + adminApiKey: string; + /** Community-scoped API key records accepted on read-only endpoints. */ + communityApiKeys: Array<{ + key: string; + scope: "community"; + contact: string; + rateLimitPerMinute: number; + createdAt: string; + }>; + /** Named throttling tiers consumed by ThrottlerModule. */ + throttle: { + upstream: { ttl: number; limit: number }; + chain: { ttl: number; limit: number }; + default: { ttl: number; limit: number }; + catalog: { ttl: number; limit: number }; + }; + /** Pending off-chain policies that never get submitted expire after this many ms. */ + policyPendingTtlMs: number; } -export default registerAs('app', () => ({ - env: process.env.NODE_ENV ?? 'development', - port: parseInt(process.env.PORT ?? '3000', 10), - sandboxMode: isSandboxMode(), -})); +export default (): AppConfig => ({ + port: parseInt(process.env.PORT || "4001", 10), + frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000", + database: { + url: process.env.DATABASE_URL || "postgres://refract:refract@localhost:5432/refract", + }, + redis: { + url: process.env.REDIS_URL || "redis://localhost:6379", + }, + stellar: { + network: process.env.STELLAR_NETWORK || "testnet", + sorobanRpcUrl: process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org", + networkPassphrase: process.env.STELLAR_NETWORK_PASSPHRASE || "Test SDF Network ; September 2015", + poolContractId: process.env.REFRACT_POOL_CONTRACT_ID || "", + policyContractId: process.env.REFRACT_POLICY_CONTRACT_ID || "", + oracleContractId: process.env.REFRACT_ORACLE_CONTRACT_ID || "", + relayerSecret: process.env.ORACLE_RELAYER_SECRET || "", + }, + confirmation: { + initialIntervalMs: parseInt(process.env.CONFIRMATION_INITIAL_INTERVAL_MS || "400", 10), + backoffMultiplier: parseFloat(process.env.CONFIRMATION_BACKOFF_MULTIPLIER || "1.8"), + maxIntervalMs: parseInt(process.env.CONFIRMATION_MAX_INTERVAL_MS || "8000", 10), + deadlineMs: parseInt(process.env.CONFIRMATION_DEADLINE_MS || "30000", 10), + jitterRatio: parseFloat(process.env.CONFIRMATION_JITTER_RATIO || "0.2"), + settlementDeadlineMs: parseInt(process.env.CONFIRMATION_SETTLEMENT_DEADLINE_MS || "90000", 10), + httpDeadlineMs: parseInt(process.env.CONFIRMATION_HTTP_DEADLINE_MS || "20000", 10), + }, + oraclePublish: { + mode: (process.env.ORACLE_PUBLISH_MODE as "every_poll" | "on_change") || "on_change", + minIntervalMs: parseInt(process.env.ORACLE_PUBLISH_MIN_INTERVAL_MS || "300000", 10), + }, + oracles: { + coingeckoBaseUrl: process.env.COINGECKO_BASE_URL || "https://api.coingecko.com/api/v3", + horizonUrl: process.env.STELLAR_HORIZON_URL || "https://horizon-testnet.stellar.org", + defiLlamaBaseUrl: process.env.DEFILLAMA_BASE_URL || "https://api.llama.fi", + // Placeholder "covered protocol" for the SmartContractRisk TVL-drop + // check until Refract defines a real list of covered Soroban + // protocols. Defaults to a large, consistently-tracked protocol so + // the drop-detection logic has real data to run against. + defiLlamaProtocolSlug: process.env.DEFILLAMA_PROTOCOL_SLUG || "aave", + httpTimeoutMs: parseInt(process.env.ORACLE_HTTP_TIMEOUT_MS || "5000", 10), + }, + fees: { + ceilingStroops: process.env.FEE_CEILING_STROOPS || "1000000", + statsTtlMs: parseInt(process.env.FEE_STATS_TTL_MS || "15000", 10), + profiles: { + moderate: { + percentile: parseInt(process.env.FEE_MODERATE_PERCENTILE || "90", 10), + multiplier: parseFloat(process.env.FEE_MODERATE_MULTIPLIER || "1.2"), + }, + aggressive: { + percentile: parseInt(process.env.FEE_AGGRESSIVE_PERCENTILE || "99", 10), + multiplier: parseFloat(process.env.FEE_AGGRESSIVE_MULTIPLIER || "1.5"), + }, + }, + }, + eventIndexer: { + enabled: process.env.EVENT_INDEXER_ENABLED !== "false", + pollIntervalMs: parseInt(process.env.EVENT_INDEXER_POLL_INTERVAL_MS || "5000", 10), + lagAlertLedgers: parseInt(process.env.EVENT_INDEXER_LAG_ALERT_LEDGERS || "7200", 10), + pageLimit: parseInt(process.env.EVENT_INDEXER_PAGE_LIMIT || "200", 10), + }, + ops: { + apiToken: process.env.OPS_API_TOKEN || "", + communityApiKeys: (process.env.COMMUNITY_API_KEYS || "") + .split(",") + .map((k) => k.trim()) + .filter(Boolean), + }, + adminApiKey: process.env.ADMIN_API_KEY || "", + communityApiKeys: [], + throttle: { + upstream: { + ttl: parseInt(process.env.THROTTLE_UPSTREAM_TTL_MS || "60000", 10), + limit: parseInt(process.env.THROTTLE_UPSTREAM_LIMIT || "60", 10), + }, + chain: { + ttl: parseInt(process.env.THROTTLE_CHAIN_TTL_MS || "60000", 10), + limit: parseInt(process.env.THROTTLE_CHAIN_LIMIT || "20", 10), + }, + default: { + ttl: parseInt(process.env.THROTTLE_DEFAULT_TTL_MS || "60000", 10), + limit: parseInt(process.env.THROTTLE_DEFAULT_LIMIT || "600", 10), + }, + catalog: { + ttl: parseInt(process.env.THROTTLE_CATALOG_TTL_MS || "60000", 10), + limit: parseInt(process.env.THROTTLE_CATALOG_LIMIT || "300", 10), + }, + }, + policyPendingTtlMs: parseInt(process.env.POLICY_PENDING_TTL_MS || "3600000", 10), +}); diff --git a/src/policy/policy.service.ts b/src/policy/policy.service.ts index eacb1a0..c32d641 100644 --- a/src/policy/policy.service.ts +++ b/src/policy/policy.service.ts @@ -276,7 +276,22 @@ export class PolicyService { * calls set_pool_config() — which the catalog alone can't see. */ async onChainCoverageBounds(): Promise<{ minCoverage: bigint; maxCoverage: bigint } | null> { - if (!this.poolCo + if (!this.poolContractId) { + return null; + } + try { + // pool_config() is a stateless view with no caller-specific args, so + // the source account only needs to be well-formed for the tx + // envelope — it never touches the network, unlike getAccount(). + const dummySource = new Account(Keypair.random().publicKey(), "0"); + const contract = new Contract(this.poolContractId); + const tx = new TransactionBuilder(dummySource, { + fee: BASE_FEE, + networkPassphrase: this.networkPassphrase, + }) + .addOperation(contract.call("pool_config")) + .setTimeout(30) + .build(); const sim = await this.metricsService.observeSorobanRpc("simulate_transaction", () => this.server.simulateTransaction(tx)