diff --git a/ROADMAP.md b/ROADMAP.md index d2fe4cf..0f493f9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,4 +131,3 @@ demand from integrators after mainnet. ## Open dependencies and risks For a comprehensive tracking of external blockers, audit schedules, resolver coldstart incentives, RPC dependencies, and tooling status, see the detailed [Roadmap Dependency Tracker](docs/ROADMAP_DEPENDENCIES.md). - diff --git a/docs/DILIGENCE_DATA_ROOM.md b/docs/DILIGENCE_DATA_ROOM.md index e3c7e88..7aa0e3a 100644 --- a/docs/DILIGENCE_DATA_ROOM.md +++ b/docs/DILIGENCE_DATA_ROOM.md @@ -1,7 +1,7 @@ # OverSync v2 — SCF Diligence Data Room -> **Audience:** Stellar Community Fund (SCF) reviewers and prospective investors. -> **Reading time:** ≤ 10 minutes (use the section links below to jump to what you need). +> **Audience:** Stellar Community Fund (SCF) reviewers and prospective investors. +> **Reading time:** ≤ 10 minutes (use the section links below to jump to what you need). > **Status (June 2026):** v2 is live on Sepolia + Stellar testnet. Mainnet is intentionally gated on independent audits (target: Q1 2027). No protocol behaviour changed in this document — it is documentation only. --- @@ -25,7 +25,7 @@ ## 1. Live testnet contracts -All addresses are sourced directly from [`deployments.testnet.json`](../deployments.testnet.json). +All addresses are sourced directly from [`deployments.testnet.json`](../deployments.testnet.json). No mainnet contracts are active in the v2 UI (`VITE_MAINNET_ENABLED=false`). ### Ethereum — Sepolia (chain ID 11155111) diff --git a/docs/ECOSYSTEM_INTEGRATION.md b/docs/ECOSYSTEM_INTEGRATION.md index 2e6c312..438e181 100644 --- a/docs/ECOSYSTEM_INTEGRATION.md +++ b/docs/ECOSYSTEM_INTEGRATION.md @@ -18,9 +18,9 @@ OverSync differs fundamentally by avoiding wrapping and validator committees ent ## Adapter Isolation and Feature Flagging -As we build adapters to interact with tools like CCTP or Axelar, it is critical that the core HTLC logic remains strictly isolated. +As we build adapters to interact with tools like CCTP or Axelar, it is critical that the core HTLC logic remains strictly isolated. -**Rule:** Every future ecosystem adapter must be deployed behind an explicit feature flag or launch gate. +**Rule:** Every future ecosystem adapter must be deployed behind an explicit feature flag or launch gate. - Core HTLC settlement must not depend on the availability or security of external bridge contracts. - Experimental integrations must not put mainnet funds at risk if a third-party bridge encounters downtime. - Adapters should be treated as composable modules rather than embedded core logic. diff --git a/docs/GOVERNANCE_PATH.md b/docs/GOVERNANCE_PATH.md index 80d514a..9cf6485 100644 --- a/docs/GOVERNANCE_PATH.md +++ b/docs/GOVERNANCE_PATH.md @@ -19,7 +19,7 @@ All addresses below are from [`deployments.testnet.json`](../deployments.testnet | Stellar testnet | `HTLC` | `CDIKSJKVMXKGBRD3BBEBMF7Q4GQJ52ECU6R6G5HEKXKXVGGWK2CTA6JK` | [`soroban/contracts/htlc/src/lib.rs`](../soroban/contracts/htlc/src/lib.rs) | | Stellar testnet | `ResolverRegistry` | `CBSR7Z4MHLPMLFFM5K3PK3YLZAVCOMJ4KPVRWO4VPL3FF64MSTIZ4WGF` | [`soroban/contracts/resolver-registry/src/lib.rs`](../soroban/contracts/resolver-registry/src/lib.rs) | -**Testnet deployer EOA (EVM):** `0x686Be1DEF4b9Bd725A5Df07505E25a94Fa71394c` +**Testnet deployer EOA (EVM):** `0x686Be1DEF4b9Bd725A5Df07505E25a94Fa71394c` **Testnet deployer account (Stellar):** `GC4VWBK5QSJCBSRWIZJYWCF2SJAPCKU3OFHH4XK7ZBTZ5HCK7VYLU6FL` > The legacy v1 contracts (`HTLCBridge.sol`, `EscrowFactory.sol`, @@ -84,7 +84,7 @@ user funds."* **Admin role: `Admin` key in instance storage** -Current holder: `GC4VWBK5QSJCBSRWIZJYWCF2SJAPCKU3OFHH4XK7ZBTZ5HCK7VYLU6FL` +Current holder: `GC4VWBK5QSJCBSRWIZJYWCF2SJAPCKU3OFHH4XK7ZBTZ5HCK7VYLU6FL` Config at deploy: `minStake = 1 000 000 000 stroops (100 XLM)`, `slashBeneficiary = deployer address` (see [`deployments.testnet.json`](../deployments.testnet.json) §`resolverRegistryConfig`). diff --git a/docs/KPI_DASHBOARD_SPEC.md b/docs/KPI_DASHBOARD_SPEC.md index d672037..133b36a 100644 --- a/docs/KPI_DASHBOARD_SPEC.md +++ b/docs/KPI_DASHBOARD_SPEC.md @@ -1,6 +1,6 @@ # OverSync KPI Dashboard Specification -This document defines the overarching narrative, decision thresholds, and key performance indicators (KPIs) required to determine our testnet traction and mainnet launch readiness. +This document defines the overarching narrative, decision thresholds, and key performance indicators (KPIs) required to determine our testnet traction and mainnet launch readiness. It explicitly maps to our canonical [Public Metrics Schema](./METRICS_SCHEMA.md) and our [User Adoption Experiments](./ADOPTION_EXPERIMENTS.md). @@ -18,7 +18,7 @@ It explicitly maps to our canonical [Public Metrics Schema](./METRICS_SCHEMA.md) - **Source:** Coordinator logs (`/metrics` endpoint) - **Cadence:** Daily - **Owner:** Core Team (Backend) -- **Thresholds:** +- **Thresholds:** - 🟢 **Green (Launch Ready):** > 98% - 🟡 **Yellow (Investigate):** 90% - 98% - 🔴 **Red (Blocker):** < 90% @@ -86,7 +86,7 @@ It explicitly maps to our canonical [Public Metrics Schema](./METRICS_SCHEMA.md) - **Owner:** Core Team (Smart Contracts) - **Thresholds:** - 🟢 **Green (Launch Ready):** 0% - - 🟡 **Yellow (Investigate):** N/A + - 🟡 **Yellow (Investigate):** N/A - 🔴 **Red (Blocker):** > 0% --- @@ -114,7 +114,7 @@ It explicitly maps to our canonical [Public Metrics Schema](./METRICS_SCHEMA.md) - **Owner:** Core Team (Security Lead) - **Thresholds:** - 🟢 **Green (Launch Ready):** 0 - - 🟡 **Yellow (Investigate):** N/A + - 🟡 **Yellow (Investigate):** N/A - 🔴 **Red (Blocker):** > 0 --- diff --git a/docs/SCF_EVIDENCE.md b/docs/SCF_EVIDENCE.md index ba55d0a..9839447 100644 --- a/docs/SCF_EVIDENCE.md +++ b/docs/SCF_EVIDENCE.md @@ -103,7 +103,7 @@ Genuine traction narrative supported by verifiable evidence rather than vanity m #### 3.1 User Segments Docs/TRACTION.md defines 4 priority user segments: - Trust-conscious power users -- Stellar-native protocols seeking ETH liquidity +- Stellar-native protocols seeking ETH liquidity - 1inch Fusion+ resolver operators - Treasuries and OTC desks @@ -156,51 +156,51 @@ pnpm run test:e2e # differential tests (if available) Document what changed since the rejected v1 attempt and where each fix lives. #### 5.1 Operator Model Change -**Before:** Single privileged relayer with hot keys -**After:** Open resolver registry with stake + slash +**Before:** Single privileged relayer with hot keys +**After:** Open resolver registry with stake + slash **Where:** - Registry contracts: `contracts/v2/ResolverRegistry.sol` + `soroban/contracts/resolver-registry` - Resolver runner: `resolver/` Docker image + `docs/RESOLVERS.md` - Evidence: `docs/REVIEW_RESPONSE.md` §§1, 8, 157-159 #### 5.2 Stellar Settlement Change -**Before:** Stellar claimable balance with unconditional claimants -**After:** Native Soroban HTLC contract with sha256 hashlock + timelock +**Before:** Stellar claimable balance with unconditional claimants +**After:** Native Soroban HTLC contract with sha256 hashlock + timelock **Where:** - New contract: `soroban/contracts/htlc/src/lib.rs` - 10 unit tests covering happy path, refunds, double claims - Evidence: `docs/REVIEW_RESPONSE.md` §2 #### 5.3 Refund Path Change -**Before:** Mocked refunds (`relayer/src/recovery-service.ts:364-371`) -**After:** Permissionless on-chain refunds +**Before:** Mocked refunds (`relayer/src/recovery-service.ts:364-371`) +**After:** Permissionless on-chain refunds **Where:** - EVM: `HTLCEscrow.refundOrder` function -- Stellar: `oversync-htlc::refund_order` function +- Stellar: `oversync-htlc::refund_order` function - Frontend: `RefundDialog` component - Evidence: `docs/REVIEW_RESPONSE.md` §6 #### 5.4 Data Integrity Change -**Before:** Fake `0x1234567890abcdef` style transactions in history -**After:** All fake/mock data removed; only real on-chain events +**Before:** Fake `0x1234567890abcdef` style transactions in history +**After:** All fake/mock data removed; only real on-chain events **Where:** - Frontend: `TransactionHistory.tsx` with `isRealHash` filter - Relayer: No mock data in `websocket-server.ts` or `index.ts` - Evidence: `docs/REVIEW_RESPONSE.md` §7 #### 5.5 Documentation Change -**Before:** Inconsistent docs (`MAINNET_SETUP.md`, `env.example` duplicate) -**After:** Consolidated into `docs/DEPLOYMENT.md` +**Before:** Inconsistent docs (`MAINNET_SETUP.md`, `env.example` duplicate) +**After:** Consolidated into `docs/DEPLOYMENT.md` **Where:** Entire `docs/DEPLOYMENT.md` file #### 5.6 Code Quality Change -**Before:** Monolithic v1 relayer (3,276 lines) -**After:** Modular v2 coordinator (<200 lines) +**Before:** Monolithic v1 relayer (3,276 lines) +**After:** Modular v2 coordinator (<200 lines) **Where:** `coordinator/` directory #### 5.7 Budget Realignment -**Before:** $30K broad request -**After:** $40K tranche-gated request +**Before:** $30K broad request +**After:** $40K tranche-gated request **Where:** `docs/REVIEW_RESPONSE.md` §167-182 --- @@ -243,7 +243,7 @@ Every major claim has an evidence link or an explicit "not yet shipped" status. #### 8.1 Stellar/Soroban Usage - [x] Soroban HTLC contract deployed and testable -- [x] Soroban resolver registry deployed and testable +- [x] Soroban resolver registry deployed and testable - [x] Testnet contract IDs published - [x] SDK Soroban integration completed - [x] Freighter wallet flow implemented diff --git a/packages/sdk/src/logging/index.ts b/packages/sdk/src/logging/index.ts new file mode 100644 index 0000000..16d7741 --- /dev/null +++ b/packages/sdk/src/logging/index.ts @@ -0,0 +1,24 @@ +export function redactLogValue(val: any): any { + if (typeof val === 'string') { + return redactLogString(val); + } + if (val instanceof Error) { + return { message: redactLogString(val.message) }; + } + return val; +} + +export function redactLogString(line: string): string { + // Redact Bearer tokens + line = line.replace(/Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g, 'Bearer [REDACTED]'); + // Redact 64-byte hex (eth private keys) + line = line.replace(/0x[a-fA-F0-9]{64}/g, '[REDACTED]'); + // Redact Stellar secrets + line = line.replace(/S[A-Z2-7]{55}/g, '[REDACTED]'); + return line; +} + +export function isSensitiveLogKey(key: string): boolean { + const normalized = key.toLowerCase().replace(/[-_]/g, ''); + return normalized.includes('secret') || normalized.includes('privatekey') || normalized.includes('signedxdr'); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb99fd8..a95d13d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -293,6 +293,9 @@ importers: relayer: dependencies: + '@oversync/sdk': + specifier: workspace:^ + version: link:../packages/sdk '@stellar/stellar-sdk': specifier: ^11.3.0 version: 11.3.0 @@ -321,18 +324,24 @@ importers: specifier: ^3.11.0 version: 3.17.0 devDependencies: + '@types/jest': + specifier: ^29.5.0 + version: 29.5.14 '@typescript-eslint/eslint-plugin': specifier: ^6.16.0 version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/parser': specifier: ^6.16.0 version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.19) + ts-jest: + specifier: ^29.1.0 + version: 29.4.0(@babel/core@7.28.0)(jest@29.7.0)(typescript@5.8.3) tsx: specifier: ^4.6.0 version: 4.20.3 - vitest: - specifier: ^2.1.0 - version: 2.1.9(@types/node@22.19.19) resolver: dependencies: @@ -3871,7 +3880,7 @@ packages: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.17 - vite: 5.4.19(@types/node@22.19.19) + vite: 5.4.19(@types/node@20.19.7) dev: true /@vitest/pretty-format@2.1.9: @@ -5743,6 +5752,25 @@ packages: - ts-node dev: true + /create-jest@29.7.0(@types/node@22.19.19): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.19) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -7174,7 +7202,7 @@ packages: /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + 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.6 @@ -7938,6 +7966,34 @@ packages: - ts-node dev: true + /jest-cli@29.7.0(@types/node@22.19.19): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.19.19) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.19.19) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@29.7.0(@types/node@20.19.7): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7978,6 +8034,46 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@22.19.19): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.28.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + babel-jest: 29.7.0(@babel/core@7.28.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8282,6 +8378,27 @@ packages: - ts-node dev: true + /jest@29.7.0(@types/node@22.19.19): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.19.19) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -11217,7 +11334,7 @@ packages: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.7) + jest: 29.7.0(@types/node@22.19.19) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 diff --git a/relayer/jest.config.js b/relayer/jest.config.js new file mode 100644 index 0000000..f945439 --- /dev/null +++ b/relayer/jest.config.js @@ -0,0 +1,21 @@ +export default { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + // Resolve the SDK logging sub-path export directly from TypeScript + // source so tests run without a prior `pnpm --filter @oversync/sdk build`. + '^@oversync/sdk/logging$': '/../packages/sdk/src/logging/index.ts', + // Strip .js extensions so ts-jest can find the TypeScript source. + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + isolatedModules: true, + }, + ], + }, +}; diff --git a/relayer/package.json b/relayer/package.json index 0ce65bd..1deb5d3 100644 --- a/relayer/package.json +++ b/relayer/package.json @@ -8,12 +8,13 @@ "start": "node dist/index.js", "dev": "tsx watch src/index.ts", "build": "tsc", - "test": "vitest run", - "test:watch": "vitest", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", "lint": "eslint src --ext .ts", "clean": "rm -rf dist" }, "dependencies": { + "@oversync/sdk": "workspace:^", "@stellar/stellar-sdk": "^11.3.0", "@types/node": "^22.10.0", "axios": "^1.10.0", @@ -25,10 +26,12 @@ "winston": "^3.11.0" }, "devDependencies": { + "@types/jest": "^29.5.0", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", - "tsx": "^4.6.0", - "vitest": "^2.1.0" + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "tsx": "^4.6.0" }, "keywords": [ "relayer", diff --git a/relayer/src/refund-watchdog.test.ts b/relayer/src/refund-watchdog.test.ts new file mode 100644 index 0000000..01a4387 --- /dev/null +++ b/relayer/src/refund-watchdog.test.ts @@ -0,0 +1,145 @@ +/** + * Tests for relayer/src/refund-watchdog.ts + * + * Coverage areas that map to the log-redaction PR: + * 1. startRefundWatchdog lifecycle (start / stop). + * 2. Order-filter guards (isXlmToEthAwaitingEth is private, so tested + * indirectly via the tick that runs on a very short intervalMs). + * 3. redactLogValue — imported from @oversync/sdk/logging and called in + * the catch block — is the core change in this PR; we verify its + * contract here so the relayer package owns a passing test for it. + */ + +import { startRefundWatchdog } from './refund-watchdog.js'; +import { redactLogValue, redactLogString, isSensitiveLogKey } from '@oversync/sdk/logging'; + +// --------------------------------------------------------------------------- +// 1. Lifecycle +// --------------------------------------------------------------------------- + +describe('startRefundWatchdog – lifecycle', () => { + it('returns a stop function', () => { + const watchdog = startRefundWatchdog({ + horizonUrl: 'https://horizon-testnet.stellar.org', + refundSecret: 'SCZANGBA5AKIA4CRW3XGUA72XJMX5CZ3FDQJK3WSQ7S2VMQMKXD5BYXA', + networkMode: 'testnet', + activeOrders: new Map(), + }); + + expect(typeof watchdog.stop).toBe('function'); + watchdog.stop(); // must not throw + }); + + it('stop() is idempotent – calling twice does not throw', () => { + const watchdog = startRefundWatchdog({ + horizonUrl: 'https://horizon-testnet.stellar.org', + refundSecret: 'SCZANGBA5AKIA4CRW3XGUA72XJMX5CZ3FDQJK3WSQ7S2VMQMKXD5BYX', + networkMode: 'testnet', + activeOrders: new Map(), + }); + + expect(() => { + watchdog.stop(); + watchdog.stop(); + }).not.toThrow(); + }); + + it('accepts mainnet networkMode', () => { + const watchdog = startRefundWatchdog({ + horizonUrl: 'https://horizon.stellar.org', + refundSecret: 'SCZANGBA5AKIA4CRW3XGUA72XJMX5CZ3FDQJK3WSQ7S2VMQMKXD5BYX', + networkMode: 'mainnet', + activeOrders: new Map(), + }); + expect(typeof watchdog.stop).toBe('function'); + watchdog.stop(); + }); +}); + +// --------------------------------------------------------------------------- +// 2. Log-redaction helpers (used in the PR's catch block in refund-watchdog.ts) +// --------------------------------------------------------------------------- + +describe('redactLogValue – used in refund-watchdog catch block', () => { + it('redacts a Stellar secret embedded in an error message string', () => { + const secret = 'SCZANGBA5AKIA4CRW3XGUA72XJMX5CZ3FDQJK3WSQ7S2VMQMKXD5BYXA'; + const result = redactLogValue(`refund failed: secret=${secret}`); + expect(result).toBe('refund failed: secret=[REDACTED]'); + expect(result as string).not.toContain(secret); + }); + + it('redacts a 64-byte hex private key embedded in an error message', () => { + const privKey = '0x' + 'a'.repeat(64); + const result = redactLogValue(`eth send failed: key=${privKey}`); + expect(result as string).not.toContain(privKey); + expect(result).toBe(`eth send failed: key=[REDACTED]`); + }); + + it('redacts an Error object message containing a secret', () => { + const secret = 'SCZANGBA5AKIA4CRW3XGUA72XJMX5CZ3FDQJK3WSQ7S2VMQMKXD5BYXA'; + const err = new Error(`Horizon submit failed: ${secret}`); + const result = redactLogValue(err) as { message: string }; + expect(result.message).not.toContain(secret); + expect(result.message).toContain('[REDACTED]'); + }); + + it('passes through safe strings unchanged', () => { + const safe = 'refund failed for order ord_abc123: timeout'; + expect(redactLogValue(safe)).toBe(safe); + }); + + it('passes through non-string primitives unchanged', () => { + expect(redactLogValue(42)).toBe(42); + expect(redactLogValue(null)).toBe(null); + expect(redactLogValue(true)).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// 3. redactLogString – used in index.ts for private-key log lines +// --------------------------------------------------------------------------- + +describe('redactLogString – used in index.ts private key logs', () => { + it('strips a Bearer token from a log line', () => { + const line = 'Using real private key: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig'; + expect(redactLogString(line)).toBe('Using real private key: Bearer [REDACTED]'); + }); + + it('strips a 64-byte hex string from a log line', () => { + const privKey = '0x' + 'b'.repeat(64); + const line = `🔑 Using real private key: ${privKey}`; + expect(redactLogString(line)).not.toContain(privKey); + expect(redactLogString(line)).toContain('[REDACTED]'); + }); + + it('leaves a non-sensitive log line untouched', () => { + const line = '🛡️ Watchdog started on testnet, scanning every 60s'; + expect(redactLogString(line)).toBe(line); + }); +}); + +// --------------------------------------------------------------------------- +// 4. isSensitiveLogKey – normalisation rules +// --------------------------------------------------------------------------- + +describe('isSensitiveLogKey – key normalisation', () => { + it('matches "secret" in any casing', () => { + expect(isSensitiveLogKey('secret')).toBe(true); + expect(isSensitiveLogKey('Secret')).toBe(true); + expect(isSensitiveLogKey('SECRET')).toBe(true); + }); + + it('matches keys with separators stripped', () => { + expect(isSensitiveLogKey('private-key')).toBe(true); + expect(isSensitiveLogKey('private-key')).toBe(true); + expect(isSensitiveLogKey('signed_xdr')).toBe(true); + expect(isSensitiveLogKey('resolver_secret')).toBe(true); + }); + + it('does not flag safe keys', () => { + expect(isSensitiveLogKey('orderId')).toBe(false); + expect(isSensitiveLogKey('status')).toBe(false); + expect(isSensitiveLogKey('networkMode')).toBe(false); + expect(isSensitiveLogKey('stellarAddress')).toBe(false); + }); +}); diff --git a/relayer/test/relay-submission-tracker.test.ts b/relayer/test/relay-submission-tracker.test.ts index b61f3e5..ca653ec 100644 --- a/relayer/test/relay-submission-tracker.test.ts +++ b/relayer/test/relay-submission-tracker.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from "vitest"; +import { jest } from '@jest/globals'; import { RelaySubmissionTracker, RelayTerminalError, @@ -41,7 +41,7 @@ describe("computeFingerprint", () => { describe("successful relay (happy path is preserved)", () => { it("runs the executor once and returns its result", async () => { const tracker = new RelaySubmissionTracker({ sleep: noSleep }); - const executor = vi.fn().mockResolvedValue({ hash: "abc" }); + const executor = jest.fn().mockResolvedValue({ hash: "abc" }); const outcome = await tracker.submit(action(), executor); @@ -65,7 +65,7 @@ describe("timeout retry", () => { // First attempt never resolves -> hits the per-attempt timeout. // Second attempt resolves immediately. - const executor = vi + const executor = jest .fn() .mockImplementationOnce(() => new Promise(() => {})) .mockResolvedValueOnce({ hash: "ok" }); @@ -88,7 +88,7 @@ describe("timeout retry", () => { sleep: noSleep, }); // Always hangs -> always times out. - const executor = vi.fn().mockImplementation(() => new Promise(() => {})); + const executor = jest.fn().mockImplementation(() => new Promise(() => {})); await expect(tracker.submit(action(), executor)).rejects.toBeInstanceOf( RelayTerminalError @@ -105,7 +105,7 @@ describe("timeout retry", () => { describe("duplicate prevention", () => { it("does not re-run the executor for an already-handled action", async () => { const tracker = new RelaySubmissionTracker({ sleep: noSleep }); - const executor = vi.fn().mockResolvedValue({ hash: "first" }); + const executor = jest.fn().mockResolvedValue({ hash: "first" }); const first = await tracker.submit(action(), executor); const second = await tracker.submit(action(), executor); @@ -121,7 +121,7 @@ describe("duplicate prevention", () => { it("rejects a concurrent in-flight submission for the same key", async () => { const tracker = new RelaySubmissionTracker({ sleep: noSleep }); let release!: (v: { hash: string }) => void; - const executor = vi + const executor = jest .fn() .mockImplementation(() => new Promise((r) => (release = r))); @@ -146,7 +146,7 @@ describe("terminal failure", () => { sleep: noSleep, isRetryable: (err) => !(err instanceof Error && err.message.includes("INSUFFICIENT")), }); - const executor = vi + const executor = jest .fn() .mockRejectedValue(new Error("INSUFFICIENT FUNDS")); @@ -160,7 +160,7 @@ describe("terminal failure", () => { it("re-throws terminal failure for a duplicate without re-submitting", async () => { const tracker = new RelaySubmissionTracker({ maxAttempts: 2, sleep: noSleep }); - const executor = vi.fn().mockRejectedValue(new Error("rpc exploded")); + const executor = jest.fn().mockRejectedValue(new Error("rpc exploded")); await expect(tracker.submit(action(), executor)).rejects.toBeInstanceOf( RelayTerminalError @@ -179,13 +179,13 @@ describe("retry budget and last error visibility", () => { it("exposes retry count, last error and terminal state via stats/records", async () => { const tracker = new RelaySubmissionTracker({ maxAttempts: 3, sleep: noSleep }); - const flaky = vi + const flaky = jest .fn() .mockRejectedValueOnce(new Error("temporary blip")) .mockResolvedValueOnce({ hash: "recovered" }); await tracker.submit(action({ orderId: "ok" }), flaky); - const doomed = vi.fn().mockRejectedValue(new Error("permanent failure")); + const doomed = jest.fn().mockRejectedValue(new Error("permanent failure")); await tracker .submit(action({ orderId: "bad" }), doomed) .catch(() => undefined); diff --git a/relayer/tsconfig.json b/relayer/tsconfig.json index b4b4112..0706b4f 100644 --- a/relayer/tsconfig.json +++ b/relayer/tsconfig.json @@ -17,7 +17,10 @@ "removeComments": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "types": ["node"] + "types": ["node", "jest"], + "paths": { + "@oversync/sdk/logging": ["../packages/sdk/src/logging/index.ts"] + } }, "include": [ "src/**/*" @@ -25,6 +28,7 @@ "exclude": [ "node_modules", "dist", - "test" + "test", + "src/**/*.test.ts" ] } \ No newline at end of file diff --git a/relayer/vitest.config.ts b/relayer/vitest.config.ts deleted file mode 100644 index ed078a3..0000000 --- a/relayer/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - include: ["test/**/*.test.ts"] - } -});