diff --git a/README.md b/README.md index 9246562be..4b3426168 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # RemitLend +123 + [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) [![Frontend: Next.js](https://img.shields.io/badge/Frontend-Next.js-black?logo=next.js)](https://nextjs.org/) [![Backend: Express](https://img.shields.io/badge/Backend-Express.js-white?logo=express)](https://expressjs.com/) diff --git a/backend/package-lock.json b/backend/package-lock.json index 3c0980142..87648331d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -60,7 +60,7 @@ "typescript": "^5.9.3" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -116,7 +116,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -715,7 +714,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -728,7 +726,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2075,7 +2072,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2097,7 +2093,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2113,7 +2108,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", @@ -2164,7 +2158,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", @@ -2525,7 +2518,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -3075,7 +3067,6 @@ "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -3092,7 +3083,6 @@ "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -3241,7 +3231,6 @@ "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.68.0", "@typescript-eslint/types": "8.68.0", @@ -3808,7 +3797,6 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4398,7 +4386,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", @@ -5508,7 +5495,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5565,7 +5551,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5996,7 +5981,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6040,7 +6024,6 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" @@ -7576,7 +7559,6 @@ "integrity": "sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.5.0", "@jest/types": "30.5.0", @@ -9525,7 +9507,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -9773,7 +9754,6 @@ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11445,7 +11425,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11715,7 +11694,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend/src/__tests__/adminGovernanceController.test.ts b/backend/src/__tests__/adminGovernanceController.test.ts index 2d7b582a4..4dfef9230 100644 --- a/backend/src/__tests__/adminGovernanceController.test.ts +++ b/backend/src/__tests__/adminGovernanceController.test.ts @@ -36,13 +36,13 @@ describe('adminGovernanceController - getPendingGovernance', () => { delete process.env.GOVERNANCE_CURRENT_ADMIN; delete process.env.MULTISIG_GOVERNANCE_CONTRACT_ID; delete process.env.GOVERNANCE_THRESHOLD; + delete process.env.GOVERNANCE_SIGNERS; }); it('filters signers so signers from older pending proposals are not mixed in', async () => { const req = {} as Request; const res = createMockResponse(); - // Database returns rows from multiple proposals (ordered by proposal_id DESC) mockQuery.mockResolvedValueOnce({ rows: [ { @@ -66,7 +66,7 @@ describe('adminGovernanceController - getPendingGovernance', () => { approved: false, }, { - proposal_id: 'prop-1', // Older proposal + proposal_id: 'prop-1', proposed_admin: 'GOLD_ADMIN_1', approval_count: 2, threshold: 2, @@ -83,19 +83,21 @@ describe('adminGovernanceController - getPendingGovernance', () => { await flushAsync(); expect(res.json).toHaveBeenCalledTimes(1); + const responseData = (res.json as jest.Mock).mock.calls[0]?.[0] as Record; expect(responseData.pendingProposal).toBeDefined(); expect(responseData.pendingProposal.id).toBe('prop-2'); expect(responseData.pendingProposal.proposedAdmin).toBe('GNEW_ADMIN_2'); - // Crucial check: only prop-2 signers should be included, NOT prop-1 signer const signers = responseData.pendingProposal.signers; + expect(signers).toHaveLength(2); expect(signers).toEqual([ { address: 'GSIGNER_PROP2_APPROVED', approved: true }, { address: 'GSIGNER_PROP2_PENDING', approved: false }, ]); + expect(signers.some((s: { address: string }) => s.address === 'GSIGNER_PROP1_ONLY')).toBe( false, ); @@ -103,6 +105,7 @@ describe('adminGovernanceController - getPendingGovernance', () => { it('returns fallback signers from env when no pending proposal exists', async () => { process.env.GOVERNANCE_SIGNERS = 'GSIGNER_1, GSIGNER_2'; + const req = {} as Request; const res = createMockResponse(); diff --git a/backend/src/__tests__/remittanceFilters.test.ts b/backend/src/__tests__/remittanceFilters.test.ts index 080c3b124..aadc6172c 100644 --- a/backend/src/__tests__/remittanceFilters.test.ts +++ b/backend/src/__tests__/remittanceFilters.test.ts @@ -37,9 +37,10 @@ jest.unstable_mockModule('../services/notificationService.js', () => ({ })); jest.unstable_mockModule('../utils/stellarEnvelope.js', () => ({ - parseAndValidateSignedEnvelope: jest - .fn() - .mockReturnValue({ source: 'GCWEPACYJLN7S3ZUXSVMXZBFKYXSHRGZ6O326HDDPDKBKZPXD45XNHC3', signatureCount: 1 }), + parseAndValidateSignedEnvelope: jest.fn().mockReturnValue({ + source: 'GCWEPACYJLN7S3ZUXSVMXZBFKYXSHRGZ6O326HDDPDKBKZPXD45XNHC3', + signatureCount: 1, + }), })); const mockQuery = jest.fn(); diff --git a/backend/src/services/__tests__/auditLogService.pagination.test.ts b/backend/src/services/__tests__/auditLogService.pagination.test.ts index d89cc90a1..60645ecbb 100644 --- a/backend/src/services/__tests__/auditLogService.pagination.test.ts +++ b/backend/src/services/__tests__/auditLogService.pagination.test.ts @@ -100,9 +100,7 @@ describe('getAuditLogs keyset pagination and totals (#1808)', () => { it('omits the count query unless withTotal is set', async () => { await getAuditLogs({ limit: 2 }); - const countCalls = mockQuery.mock.calls.filter(([text]) => - String(text).includes('COUNT(*)'), - ); + const countCalls = mockQuery.mock.calls.filter(([text]) => String(text).includes('COUNT(*)')); expect(countCalls).toHaveLength(0); }); @@ -122,9 +120,7 @@ describe('getAuditLogs keyset pagination and totals (#1808)', () => { limit: 2, }); - const countCall = mockQuery.mock.calls.find(([text]) => - String(text).includes('COUNT(*)'), - ); + const countCall = mockQuery.mock.calls.find(([text]) => String(text).includes('COUNT(*)')); const countSql = String(countCall?.[0]); const countValues = (countCall?.[1] as unknown[]) ?? []; diff --git a/backend/src/services/auditLogService.ts b/backend/src/services/auditLogService.ts index 1fcfc8d72..a7dfd33bb 100644 --- a/backend/src/services/auditLogService.ts +++ b/backend/src/services/auditLogService.ts @@ -24,8 +24,7 @@ export function encodeCursor(row: Record | undefined): string | const id = row.id; const createdAt = row.created_at; if (id === undefined || id === null || !createdAt) return null; - const createdAtIso = - createdAt instanceof Date ? createdAt.toISOString() : String(createdAt); + const createdAtIso = createdAt instanceof Date ? createdAt.toISOString() : String(createdAt); return `${createdAtIso}${CURSOR_SEPARATOR}${String(id)}`; } diff --git a/backend/src/tests/idempotency.namespace.test.ts b/backend/src/tests/idempotency.namespace.test.ts index aedb3af73..3b58d4ed1 100644 --- a/backend/src/tests/idempotency.namespace.test.ts +++ b/backend/src/tests/idempotency.namespace.test.ts @@ -1,5 +1,9 @@ import { Request, Response, NextFunction } from 'express'; -import { idempotencyMiddleware, computeFingerprint, namespacedKey } from '../middleware/idempotency.js'; +import { + idempotencyMiddleware, + computeFingerprint, + namespacedKey, +} from '../middleware/idempotency.js'; import { cacheService } from '../services/cacheService.js'; import { jest } from '@jest/globals'; @@ -33,8 +37,7 @@ describe('idempotencyMiddleware key namespacing (#1809)', () => { return request; }; - const cacheKeysRead = () => - asMock(cacheService.get).mock.calls.map(([key]) => String(key)); + const cacheKeysRead = () => asMock(cacheService.get).mock.calls.map(([key]) => String(key)); beforeEach(() => { req = buildRequest(ALICE); @@ -75,11 +78,7 @@ describe('idempotencyMiddleware key namespacing (#1809)', () => { jest.clearAllMocks(); asMock(cacheService.setNotExists).mockResolvedValue(true); - await idempotencyMiddleware( - buildRequest(BOB) as Request, - res as Response, - next, - ); + await idempotencyMiddleware(buildRequest(BOB) as Request, res as Response, next); const bobKey = cacheKeysRead()[0]; expect(aliceKey).not.toBe(bobKey); @@ -173,11 +172,7 @@ describe('idempotencyMiddleware key namespacing (#1809)', () => { jest.clearAllMocks(); asMock(cacheService.setNotExists).mockResolvedValue(true); - await idempotencyMiddleware( - buildRequest(undefined) as Request, - res as Response, - next, - ); + await idempotencyMiddleware(buildRequest(undefined) as Request, res as Response, next); expect(cacheKeysRead()[0]).toBe(first); expect(first).toContain('anon');