From 915949426da483fc121e20efa3cc95b1c5a256f7 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Thu, 27 Aug 2026 15:42:53 +0100 Subject: [PATCH 01/11] feat: distinguish no-results search state from empty list Add a field ( | | ) and an optional to the creator list response envelope so clients can render a tailored no-results message referencing the search term instead of the generic empty state. Includes an integration test covering the zzznomatch zero-result search scenario. --- ...list-no-results-search.integration.test.ts | 109 ++++++++++++++++++ src/modules/creators/creators.controllers.ts | 3 +- src/modules/creators/creators.serializers.ts | 59 ++++++++-- 3 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 src/modules/creators/creator-list-no-results-search.integration.test.ts diff --git a/src/modules/creators/creator-list-no-results-search.integration.test.ts b/src/modules/creators/creator-list-no-results-search.integration.test.ts new file mode 100644 index 00000000..21749886 --- /dev/null +++ b/src/modules/creators/creator-list-no-results-search.integration.test.ts @@ -0,0 +1,109 @@ +// Integration test: creator list no-results state for an unmatched search term +// +// Verifies that when a search query returns zero creators the list response +// exposes a distinct `noResults` state with a message that references the +// search term, and that this state is separate from the unfiltered empty +// state (which uses `state: 'empty'` and no message). Uses Jest mocks so no +// database is required. + +import { httpListCreators } from './creators.controllers'; +import * as creatorsUtils from './creators.utils'; + +// ── Lightweight request/response mocks ──────────────────────────────────────── + +const SEARCH_TERM = 'zzznomatch'; + +function makeReq(query: Record = {}): any { + return { query }; +} + +function makeRes(): any { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.setHeader = jest.fn().mockReturnValue(res); + res.set = jest.fn().mockReturnValue(res); + return res; +} + +function makeNext(): jest.Mock { + return jest.fn(); +} + +function getBody(res: any) { + return res.json.mock.calls[0][0]; +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe('GET /api/v1/creators — no-results state for unmatched search', () => { + beforeEach(() => { + // Mock the creator search to return zero results for every query. + jest + .spyOn(creatorsUtils, 'fetchCreatorList') + .mockResolvedValue([[], 0]); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns a no-results message that references the search term', async () => { + const req = makeReq({ search: SEARCH_TERM }); + const res = makeRes(); + await httpListCreators(req, res, makeNext()); + + expect(res.status).toHaveBeenCalledWith(200); + const body = getBody(res); + expect(body.data.state).toBe('noResults'); + expect(body.data.message).toEqual( + expect.stringContaining(SEARCH_TERM) + ); + }); + + it('keeps the no-results state distinct from the unfiltered empty state', async () => { + const searchRes = makeRes(); + await httpListCreators(makeReq({ search: SEARCH_TERM }), searchRes, makeNext()); + const noResultsBody = getBody(searchRes); + + const emptyRes = makeRes(); + await httpListCreators(makeReq(), emptyRes, makeNext()); + const emptyBody = getBody(emptyRes); + + // Search with zero matches → noResults + message + expect(noResultsBody.data.state).toBe('noResults'); + expect(noResultsBody.data.message).toBeDefined(); + + // Unfiltered empty list → empty, no message + expect(emptyBody.data.state).toBe('empty'); + expect(emptyBody.data.message).toBeUndefined(); + + // The two states must differ + expect(noResultsBody.data.state).not.toBe(emptyBody.data.state); + }); + + it('removes the no-results state when the search input is cleared', async () => { + const searchRes = makeRes(); + await httpListCreators(makeReq({ search: SEARCH_TERM }), searchRes, makeNext()); + expect(getBody(searchRes).data.state).toBe('noResults'); + + // Clearing the search returns the unfiltered empty state + const clearedRes = makeRes(); + await httpListCreators(makeReq(), clearedRes, makeNext()); + const clearedBody = getBody(clearedRes); + + expect(clearedBody.data.state).toBe('empty'); + expect(clearedBody.data.message).toBeUndefined(); + }); + + it('still reports zero total creators for the no-results search', async () => { + const req = makeReq({ search: SEARCH_TERM }); + const res = makeRes(); + await httpListCreators(req, res, makeNext()); + + const body = getBody(res); + expect(body.data.items).toEqual([]); + expect(body.data.meta.total).toBe(0); + expect(body.data.meta.hasMore).toBe(false); + }); +}); diff --git a/src/modules/creators/creators.controllers.ts b/src/modules/creators/creators.controllers.ts index 31a40f7a..ab056536 100644 --- a/src/modules/creators/creators.controllers.ts +++ b/src/modules/creators/creators.controllers.ts @@ -76,7 +76,8 @@ export const httpListCreators: AsyncController = async (req, res, next) => { limit: validatedQuery.limit, offset: validatedQuery.offset, total, - }) + }), + { search: validatedQuery.search } ); attachTimestampHeader(res); diff --git a/src/modules/creators/creators.serializers.ts b/src/modules/creators/creators.serializers.ts index e24419e3..8e804689 100644 --- a/src/modules/creators/creators.serializers.ts +++ b/src/modules/creators/creators.serializers.ts @@ -164,13 +164,31 @@ export function serializeCreatorListOffsetMeta( }; } +/** + * Distinguishes an empty list from a "no matches" search result so that + * clients can render a tailored no-results message instead of the generic + * empty state. + * + * - `results` — at least one creator was returned. + * - `empty` — no creators exist and no search/filter narrowed the list. + * - `noResults`— a search term was supplied but matched zero creators. + */ +export type CreatorListState = 'results' | 'empty' | 'noResults'; + /** * Paginated creator list response body (offset pagination metadata). + * + * Adds `state` and an optional `message` so clients can differentiate the + * unfiltered empty list from a zero-result search and surface a message that + * references the search term. */ export type CreatorListResponse = PublicCreatorListEnvelope< CreatorListItem, OffsetPaginationMeta ->; +> & { + state: CreatorListState; + message?: string; +}; /** * Cursor-aware creator list response body. @@ -184,16 +202,43 @@ export type CreatorCursorListResponse = PublicCreatorListEnvelope< * Serializes a standard offset-paginated creator list response. * * This centralizes the wrapping of creators and metadata to ensure - * a consistent public response shape (envelope). + * a consistent public response shape (envelope). When the result set is + * empty, `state` distinguishes an unfiltered empty list (`empty`) from a + * zero-result search (`noResults`); the latter includes a `message` that + * references the supplied search term so clients can render a tailored + * no-results state. + * + * @param profiles - Creator profiles (null/undefined treated as empty) + * @param meta - Offset pagination metadata + * @param options - Serialization context (e.g. the active search term) */ export function serializeCreatorListResponse( profiles: CreatorProfile[], - meta: OffsetPaginationMeta + meta: OffsetPaginationMeta, + options: { search?: string } = {} ): CreatorListResponse { - return wrapPublicCreatorListResponse( - serializeCreatorList(profiles), - serializeCreatorListOffsetMeta(meta) - ); + const items = serializeCreatorList(profiles); + + let state: CreatorListState; + let message: string | undefined; + + if (meta.total > 0) { + state = 'results'; + } else if (options.search) { + state = 'noResults'; + message = `No creators match "${options.search}". Try a different search term.`; + } else { + state = 'empty'; + } + + return { + ...wrapPublicCreatorListResponse( + items, + serializeCreatorListOffsetMeta(meta) + ), + state, + ...(message ? { message } : {}), + }; } /** From 46b170bbf8ea2270b4469be0d238559892559ab4 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Thu, 27 Aug 2026 20:10:15 +0100 Subject: [PATCH 02/11] Fix leftover merge conflict artifacts in creators controllers/serializers --- src/modules/creators/creators.controllers.ts | 9 +++------ src/modules/creators/creators.serializers.ts | 9 --------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/modules/creators/creators.controllers.ts b/src/modules/creators/creators.controllers.ts index 2895238e..c7aad47c 100644 --- a/src/modules/creators/creators.controllers.ts +++ b/src/modules/creators/creators.controllers.ts @@ -76,15 +76,13 @@ export const httpListCreators: AsyncController = async (req, res, next) => { limit: validatedQuery.limit, offset: validatedQuery.offset, total, -no-results-state }), - { search: validatedQuery.search } - + { + search: validatedQuery.search, ...(validatedQuery.search !== undefined && total === 0 ? { searchTerm: validatedQuery.search } : {}), - }) -main + } ); attachTimestampHeader(res); @@ -93,7 +91,6 @@ main next(error); } }; - /** * Categorize a parse error based on the validation details. * diff --git a/src/modules/creators/creators.serializers.ts b/src/modules/creators/creators.serializers.ts index 556e9efe..4787b10d 100644 --- a/src/modules/creators/creators.serializers.ts +++ b/src/modules/creators/creators.serializers.ts @@ -216,7 +216,6 @@ export type CreatorCursorListResponse = PublicCreatorListEnvelope< */ export async function serializeCreatorListResponse( profiles: CreatorProfile[], -no-results-state meta: OffsetPaginationMeta, options: { search?: string } = {} ): CreatorListResponse { @@ -242,14 +241,6 @@ no-results-state state, ...(message ? { message } : {}), }; - - meta: OffsetPaginationMeta -): Promise { - return wrapPublicCreatorListResponse( - await serializeCreatorList(profiles), - serializeCreatorListOffsetMeta(meta) - ); - main } /** From a5f7e9f111e2fb8d110e04a7a9489ffa74f49d02 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Fri, 28 Aug 2026 13:18:13 +0100 Subject: [PATCH 03/11] fix: resolve Vercel build TypeScript errors across schema, config, and modules --- package.json | 1 + prisma/schema/follow.prisma | 11 +++++ prisma/schema/ownership.prisma | 5 +- src/config.schema.ts | 25 ++++++++++ .../audit-log-endpoint.integration.test.ts | 1 - .../admin/audit-log.integration.test.ts | 1 - .../admin/key-sync.integration.test.ts | 12 +++-- src/modules/creators/creators.serializers.ts | 4 +- .../dividend-endpoint.integration.test.ts | 4 ++ src/modules/investor/dividend.service.ts | 2 +- src/modules/keys/keys.routes.ts | 2 + src/modules/keys/price-moved.redis.ts | 6 ++- .../notifications/notification.service.ts | 8 +++- .../subscriptions/subscription.service.ts | 48 ++++++++++++------- src/modules/wallets/wallets.routes.ts | 2 + .../whitelist/whitelist.integration.test.ts | 8 ++-- src/utils/redis.utils.ts | 9 ++++ src/utils/sequencer-lock.utils.ts | 5 ++ src/utils/server.utils.ts | 12 +++++ src/utils/supply-drift-guard.utils.ts | 3 ++ 20 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 src/utils/server.utils.ts diff --git a/package.json b/package.json index fcb38906..d920c9b7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "preinstall": "node -e \"const ua = process.env.npm_config_user_agent || ''; if (!ua.includes('pnpm')) { console.error('Use pnpm for this repository. Run: pnpm install'); process.exit(1); }\"", "dev": "nodemon", "start": "node dist/server.js", + "prebuild": "prisma generate", "build": "tsc", "test": "jest", "format": "prettier --write .", diff --git a/prisma/schema/follow.prisma b/prisma/schema/follow.prisma index f05ad9fe..079aabf9 100644 --- a/prisma/schema/follow.prisma +++ b/prisma/schema/follow.prisma @@ -12,3 +12,14 @@ model Follow { @@index([creatorId]) @@index([followerAddress]) } + +model WalletCreatorFollow { + id String @id @default(cuid()) + walletAddress String + creatorId String + createdAt DateTime @default(now()) + + @@unique([walletAddress, creatorId]) + @@index([walletAddress]) + @@index([creatorId]) +} diff --git a/prisma/schema/ownership.prisma b/prisma/schema/ownership.prisma index 705d50a5..a5a24a31 100644 --- a/prisma/schema/ownership.prisma +++ b/prisma/schema/ownership.prisma @@ -15,7 +15,10 @@ model KeyOwnership { /// When the current lockup window ends for this holding, if any. lockupExpiresAt DateTime? - + + /// ISO timestamp of the holder's most recent buy, null when never bought. + lastBuyAt DateTime? + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/config.schema.ts b/src/config.schema.ts index 433f0226..b2b51158 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -106,6 +106,7 @@ export const envSchema = z .int() .positive() .default(900), + JWT_EXPIRES_IN: z.string().default('1h'), // Redis cache REDIS_URL: z.string().default('redis://localhost:6379'), @@ -172,6 +173,10 @@ export const envSchema = z 'STELLAR_SOROBAN_RPC_URL must be a valid URL (e.g. https://soroban-testnet.stellar.org)' ) .default('https://soroban-testnet.stellar.org'), + STELLAR_AUTH_SECRET: z + .string() + .min(32, 'STELLAR_AUTH_SECRET should be at least 32 characters') + .default('accesslayer_default_development_stellar_auth_secret_32b'), // Ownership snapshot cleanup job OWNERSHIP_SNAPSHOT_TABLE_NAME: z @@ -240,6 +245,26 @@ export const envSchema = z .positive() .default(5000), SSE_REPLAY_MAX_EVENTS: z.coerce.number().int().positive().default(100), + SSE_MAX_CONNECTIONS_PER_WALLET: z.coerce + .number() + .int() + .positive() + .default(5), + SSE_MAX_SUBSCRIPTIONS_PER_WALLET: z.coerce + .number() + .int() + .positive() + .default(10), + SSE_SUBSCRIPTION_TTL_MS: z.coerce + .number() + .int() + .positive() + .default(300000), + SSE_THROTTLE_DURATION_MS: z.coerce + .number() + .int() + .positive() + .default(1000), }) .superRefine((data, ctx) => { if (data.MODE === 'production' && data.STELLAR_NETWORK === 'testnet') { diff --git a/src/modules/admin/audit-log-endpoint.integration.test.ts b/src/modules/admin/audit-log-endpoint.integration.test.ts index b6c3a3f4..17c325ab 100644 --- a/src/modules/admin/audit-log-endpoint.integration.test.ts +++ b/src/modules/admin/audit-log-endpoint.integration.test.ts @@ -1,4 +1,3 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import { createServer } from '../../utils/server.utils'; import { prisma } from '../../utils/prisma.utils'; diff --git a/src/modules/admin/audit-log.integration.test.ts b/src/modules/admin/audit-log.integration.test.ts index 9a3b7f07..f60aeb51 100644 --- a/src/modules/admin/audit-log.integration.test.ts +++ b/src/modules/admin/audit-log.integration.test.ts @@ -1,4 +1,3 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { prisma } from '../../utils/prisma.utils'; import { createAuditEntry, getAuditLogs } from './audit-log.service'; diff --git a/src/modules/admin/key-sync.integration.test.ts b/src/modules/admin/key-sync.integration.test.ts index 2c32caa5..f75e3a93 100644 --- a/src/modules/admin/key-sync.integration.test.ts +++ b/src/modules/admin/key-sync.integration.test.ts @@ -1,4 +1,3 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import { createServer } from '../../utils/server.utils'; import { prisma } from '../../utils/prisma.utils'; @@ -27,6 +26,9 @@ describe('Key Sync Integration Tests', () => { const user = await prisma.user.create({ data: { email: `test-${Date.now()}@example.com`, + passwordHash: 'hash', + firstName: 'Test', + lastName: 'User', stellarWallet: { create: { address: 'GBTEST0001' } }, }, }); @@ -46,9 +48,9 @@ describe('Key Sync Integration Tests', () => { // Create price snapshot await prisma.creatorPriceSnapshot.create({ data: { - creatorId, - price: 100, - priceUpdatedAt: new Date(), + creatorId: testCreatorId, + currentPrice: 100n, + lastTradeAt: new Date(), }, }); @@ -57,7 +59,7 @@ describe('Key Sync Integration Tests', () => { await prisma.keyOwnership.create({ data: { ownerAddress: `GHOLDER${String(i).padStart(52, '0')}`, - creatorId, + creatorId: testCreatorId, balance: 100, }, }); diff --git a/src/modules/creators/creators.serializers.ts b/src/modules/creators/creators.serializers.ts index 4787b10d..0ce5f188 100644 --- a/src/modules/creators/creators.serializers.ts +++ b/src/modules/creators/creators.serializers.ts @@ -218,8 +218,8 @@ export async function serializeCreatorListResponse( profiles: CreatorProfile[], meta: OffsetPaginationMeta, options: { search?: string } = {} -): CreatorListResponse { - const items = serializeCreatorList(profiles); +): Promise { + const items = await serializeCreatorList(profiles); let state: CreatorListState; let message: string | undefined; diff --git a/src/modules/dividends/dividend-endpoint.integration.test.ts b/src/modules/dividends/dividend-endpoint.integration.test.ts index cdaabb07..edb3e937 100644 --- a/src/modules/dividends/dividend-endpoint.integration.test.ts +++ b/src/modules/dividends/dividend-endpoint.integration.test.ts @@ -1,4 +1,5 @@ import request from 'supertest'; +import { createServer } from '../../utils/server.utils'; import { prisma } from '../../utils/prisma.utils'; import { processDividendEvents } from '../indexer/dividend-indexer.service'; import { IndexerChainEvent } from '../../utils/indexer-event-processor.utils'; @@ -234,6 +235,9 @@ describe('Dividend Endpoints Integration Tests', () => { const user = await prisma.user.create({ data: { email: `test2-${Date.now()}@example.com`, + passwordHash: 'hash123', + firstName: 'Test', + lastName: 'User', stellarWallet: { create: { address: 'GBTEST0002' } }, }, }); diff --git a/src/modules/investor/dividend.service.ts b/src/modules/investor/dividend.service.ts index c1936177..6c8abe6d 100644 --- a/src/modules/investor/dividend.service.ts +++ b/src/modules/investor/dividend.service.ts @@ -14,7 +14,7 @@ export async function getInvestorDividends( } const items = await prisma.dividendDistribution.findMany({ where, - orderBy: { distributedAt: 'desc' }, + orderBy: { createdAt: 'desc' }, take: limit + 1, }); const hasMore = items.length > limit; diff --git a/src/modules/keys/keys.routes.ts b/src/modules/keys/keys.routes.ts index b17b7b33..4c4caf83 100644 --- a/src/modules/keys/keys.routes.ts +++ b/src/modules/keys/keys.routes.ts @@ -14,6 +14,8 @@ import { PRICE_HISTORY_INTERVALS, } from './key-price-history.service'; import { getKeyFees, KeyNotFoundError } from './key-fees.service'; +import { getKeyProposals, KeyNotFoundError as ProposalKeyNotFoundError } from './key-proposals.service'; +import { getKeySupply, KeyNotFoundError as SupplyKeyNotFoundError } from './key-supply.service'; import { KeySearchQueryTooShortError, searchKeys } from './key-search.service'; import { KEY_SEARCH_MIN_QUERY_LENGTH } from '../../constants/notifications.constants'; import dividendRouter from '../dividends/dividend.routes'; diff --git a/src/modules/keys/price-moved.redis.ts b/src/modules/keys/price-moved.redis.ts index ad53ea84..cc0666ce 100644 --- a/src/modules/keys/price-moved.redis.ts +++ b/src/modules/keys/price-moved.redis.ts @@ -8,6 +8,7 @@ import { export async function writePriceMovedKeys(keyIds: string[]): Promise { const redis = getRedis(); + if (!redis) return; const pipeline = redis.pipeline(); pipeline.del(REDIS_KEYS.priceMovedSet); if (keyIds.length > 0) { @@ -18,7 +19,9 @@ export async function writePriceMovedKeys(keyIds: string[]): Promise { } export async function getPriceMovedKeyIds(): Promise { - return getRedis().smembers(REDIS_KEYS.priceMovedSet); + const redis = getRedis(); + if (!redis) return []; + return redis.smembers(REDIS_KEYS.priceMovedSet); } export async function markPriceMovedDelivered( @@ -26,6 +29,7 @@ export async function markPriceMovedDelivered( walletAddress: string ): Promise { const redis = getRedis(); + if (!redis) return; const deliveredKey = REDIS_KEYS.priceMovedDelivered(keyId); await redis.sadd(deliveredKey, walletAddress); await redis.expire(deliveredKey, PRICE_MOVED_SET_TTL_SECONDS); diff --git a/src/modules/notifications/notification.service.ts b/src/modules/notifications/notification.service.ts index 11cbda06..5ad38dde 100644 --- a/src/modules/notifications/notification.service.ts +++ b/src/modules/notifications/notification.service.ts @@ -13,7 +13,9 @@ import { import { NotificationItem } from './notification.types'; async function getLastReadAt(walletAddress: string): Promise { - const raw = await getRedis().get( + const redis = getRedis(); + if (!redis) return null; + const raw = await redis.get( REDIS_KEYS.notificationsReadAt(walletAddress) ); if (!raw) { @@ -182,7 +184,9 @@ export async function markAllNotificationsRead( walletAddress: string, now: Date = new Date() ): Promise { - await getRedis().set( + const redis = getRedis(); + if (!redis) return; + await redis.set( REDIS_KEYS.notificationsReadAt(walletAddress), now.toISOString() ); diff --git a/src/modules/subscriptions/subscription.service.ts b/src/modules/subscriptions/subscription.service.ts index 2049d691..5bf89a44 100644 --- a/src/modules/subscriptions/subscription.service.ts +++ b/src/modules/subscriptions/subscription.service.ts @@ -1,4 +1,5 @@ import { randomUUID } from 'crypto'; +import { Redis } from 'ioredis'; import { getRedis } from '../../utils/redis.utils'; import { envConfig } from '../../config'; import { @@ -36,14 +37,27 @@ function walletSubsKey(walletAddress: string): string { } function generateSubscriptionId(): string { - return `sub_${randomUUID().replace(/-/g, '').slice(0, 24)}`; + return `sub_${randomUUID().replace(/-/g, '').slice(0, 24)}`; +} + +/** + * Resolve the shared Redis client, throwing if it is unavailable. The + * subscription/SSE layer is fundamentally Redis-backed, so operating without + * it is an error rather than a degradable cache miss. + */ +function assertRedis(): Redis { + const client = getRedis(); + if (!client) { + throw new Error('Redis is not available; subscriptions require Redis'); + } + return client; } export async function createSubscription( walletAddress: string, topics: SubscriptionTopic[] ): Promise { - const redis = getRedis(); + const redis = assertRedis(); const walletKey = walletSubsKey(walletAddress); @@ -86,7 +100,7 @@ export async function createSubscription( export async function getSubscription( subscriptionId: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); const data = await redis.hgetall(subKey(subscriptionId)); if (!data || !data.walletAddress) return null; @@ -99,7 +113,7 @@ export async function getSubscription( } export async function deleteSubscription(subscriptionId: string): Promise { - const redis = getRedis(); + const redis = assertRedis(); const sub = await getSubscription(subscriptionId); if (!sub) return; @@ -111,14 +125,14 @@ export async function deleteSubscription(subscriptionId: string): Promise } export async function touchSubscription(subscriptionId: string): Promise { - const redis = getRedis(); + const redis = assertRedis(); await redis.expire(subKey(subscriptionId), SUBSCRIPTION_TTL_S); } export async function getLastCursor( subscriptionId: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); return redis.get(cursorKey(subscriptionId)); } @@ -126,25 +140,25 @@ export async function saveCursor( subscriptionId: string, cursor: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); await redis.set(cursorKey(subscriptionId), cursor); } export async function isThrottled(walletAddress: string): Promise { - const redis = getRedis(); + const redis = assertRedis(); const exists = await redis.exists(throttledKey(walletAddress)); return exists === 1; } export async function setThrottled(walletAddress: string): Promise { - const redis = getRedis(); + const redis = assertRedis(); await redis.setex(throttledKey(walletAddress), THROTTLE_DURATION_S, '1'); } export async function incrementConnectionCount( walletAddress: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); const count = await redis.incr(connectionCountKey(walletAddress)); await redis.expire(connectionCountKey(walletAddress), 60); return count; @@ -153,18 +167,18 @@ export async function incrementConnectionCount( export async function decrementConnectionCount( walletAddress: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); await redis.decr(connectionCountKey(walletAddress)); } export async function getWalletSubscriptions( walletAddress: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); const ids = await redis.zrange( walletSubsKey(walletAddress), - 0, - -1 + '0', + '-1' ); const subs: Subscription[] = []; @@ -178,7 +192,7 @@ export async function getWalletSubscriptions( export async function getSubscriptionsByTopic( topic: string ): Promise { - const redis = getRedis(); + const redis = assertRedis(); const ids = await redis.keys(`${SUBSCRIPTION_KEY_PREFIX}*`); const subs: Subscription[] = []; @@ -201,12 +215,12 @@ export async function getSubscriptionsByTopic( } export async function pruneExpiredSubscriptions(): Promise { - const redis = getRedis(); + const redis = assertRedis(); const walletKeys = await redis.keys(`${WALLET_SUBSCRIPTIONS_KEY_PREFIX}*`); let pruned = 0; for (const wk of walletKeys) { - const ids = await redis.zrange(wk, 0, -1); + const ids = await redis.zrange(wk, '0', '-1'); for (const id of ids) { const exists = await redis.exists(subKey(id)); if (exists === 0) { diff --git a/src/modules/wallets/wallets.routes.ts b/src/modules/wallets/wallets.routes.ts index b9d42318..fde9c020 100644 --- a/src/modules/wallets/wallets.routes.ts +++ b/src/modules/wallets/wallets.routes.ts @@ -1,9 +1,11 @@ import { Router } from "express"; import { httpGetWalletActivity } from "./wallet-activity.controllers"; import { httpGetWalletHoldings } from "./wallet-holdings.controllers"; +import { httpGetWalletFollowing } from "./wallet-following.controllers"; import { cacheControl } from "../../middlewares/cache-control.middleware"; import { ACTIVITY_FEED_CACHE_PRESET } from "../../constants/activity-feed-cache.constants"; import { requireWalletParamMatch } from "../../middlewares/jwt-auth.middleware"; +import { jwtAuth } from "../../middlewares/jwt.middleware"; const walletsRouter = Router(); diff --git a/src/modules/whitelist/whitelist.integration.test.ts b/src/modules/whitelist/whitelist.integration.test.ts index f8afcc10..bfa3737d 100644 --- a/src/modules/whitelist/whitelist.integration.test.ts +++ b/src/modules/whitelist/whitelist.integration.test.ts @@ -1,4 +1,3 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import request from 'supertest'; import { createServer } from '../../utils/server.utils'; import { prisma } from '../../utils/prisma.utils'; @@ -21,6 +20,9 @@ describe('Whitelist Endpoint Integration Tests', () => { const user = await prisma.user.create({ data: { email: `test-${Date.now()}@example.com`, + passwordHash: 'hash', + firstName: 'Test', + lastName: 'User', stellarWallet: { create: { address: 'GBTEST0001' } }, }, }); @@ -239,8 +241,8 @@ describe('Whitelist Endpoint Integration Tests', () => { // Note: This test requires Redis to be available // We spy on the caching functions to verify they're called - const cacheGetSpy = vi.spyOn(cacheUtils, 'cacheGetJson'); - const cacheSetSpy = vi.spyOn(cacheUtils, 'cacheSetJson'); + const cacheGetSpy = jest.spyOn(cacheUtils, 'cacheGetJson'); + const cacheSetSpy = jest.spyOn(cacheUtils, 'cacheSetJson'); // First request should miss cache and populate it const response1 = await request(app) diff --git a/src/utils/redis.utils.ts b/src/utils/redis.utils.ts index 644eac40..92030bdc 100644 --- a/src/utils/redis.utils.ts +++ b/src/utils/redis.utils.ts @@ -214,5 +214,14 @@ export async function disconnectRedis(): Promise { } } +/** + * Ensure the shared Redis client is initialised. The client connects eagerly + * on creation (lazyConnect is disabled), so simply touching the singleton is + * enough to "connect". No-op when caching is disabled (returns null). + */ +export async function connectRedis(): Promise { + getRedisClient(); +} + export const getRedis = getRedisClient; export const redis = getRedisClient; diff --git a/src/utils/sequencer-lock.utils.ts b/src/utils/sequencer-lock.utils.ts index 04e9652f..f5651094 100644 --- a/src/utils/sequencer-lock.utils.ts +++ b/src/utils/sequencer-lock.utils.ts @@ -23,6 +23,11 @@ export async function acquireSequencerLock( creatorWallet: string ): Promise<{ release: () => Promise }> { const redis = getRedis(); + if (!redis) { + throw new SequencerContentionError( + `Cannot acquire sequencer lock for ${creatorWallet}: Redis is not available` + ); + } const key = lockKey(creatorWallet); const lockValue = `${process.pid}:${Date.now()}`; const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS; diff --git a/src/utils/server.utils.ts b/src/utils/server.utils.ts new file mode 100644 index 00000000..737ddbc7 --- /dev/null +++ b/src/utils/server.utils.ts @@ -0,0 +1,12 @@ +// src/utils/server.utils.ts +// Builds the Express app for use in integration tests without binding a port. +import app from '../app'; + +/** + * Returns the configured Express application instance. Used by integration + * tests so they can drive the full HTTP stack via supertest without starting + * a listening server. + */ +export async function createServer() { + return app; +} diff --git a/src/utils/supply-drift-guard.utils.ts b/src/utils/supply-drift-guard.utils.ts index 90604e2f..c38dd81c 100644 --- a/src/utils/supply-drift-guard.utils.ts +++ b/src/utils/supply-drift-guard.utils.ts @@ -16,6 +16,7 @@ function driftKey(creatorWallet: string): string { export async function isDriftHalted(creatorWallet: string): Promise { const redis = getRedis(); + if (!redis) return false; const exists = await redis.exists(driftKey(creatorWallet)); return exists === 1; } @@ -46,6 +47,7 @@ export async function verifySupplyAndGuard( ); const redis = getRedis(); + if (!redis) return false; await redis.set(driftKey(creatorWallet), '1'); return false; } @@ -55,6 +57,7 @@ export async function verifySupplyAndGuard( export async function clearDrift(creatorWallet: string): Promise { const redis = getRedis(); + if (!redis) return; await redis.del(driftKey(creatorWallet)); logger.info( { creator_wallet: creatorWallet }, From 31dd3538fb5159231c91dc4ff6e32fe5d6309ca6 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 10:18:11 +0100 Subject: [PATCH 04/11] Fix duplicate Prisma model/field definitions --- prisma/schema/follow.prisma | 13 +------------ prisma/schema/ownership.prisma | 16 ++++------------ 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/prisma/schema/follow.prisma b/prisma/schema/follow.prisma index b658b265..8e1f77e7 100644 --- a/prisma/schema/follow.prisma +++ b/prisma/schema/follow.prisma @@ -28,15 +28,4 @@ model WalletCreatorFollow { @@index([creatorId]) @@index([walletAddress]) @@map("wallet_creator_follows") -} - -model WalletCreatorFollow { - id String @id @default(cuid()) - walletAddress String - creatorId String - createdAt DateTime @default(now()) - - @@unique([walletAddress, creatorId]) - @@index([walletAddress]) - @@index([creatorId]) -} +} \ No newline at end of file diff --git a/prisma/schema/ownership.prisma b/prisma/schema/ownership.prisma index 27c5d74b..bae2716f 100644 --- a/prisma/schema/ownership.prisma +++ b/prisma/schema/ownership.prisma @@ -13,24 +13,16 @@ model KeyOwnership { balance Decimal @default(0) costBasis Decimal? @default(0) - /// When the current lockup window ends for this holding, if any. lockupExpiresAt DateTime? /// ISO timestamp of the holder's most recent buy, null when never bought. lastBuyAt DateTime? - - /// Timestamp of the owner's most recent buy of this key, if any. - lastBuyAt DateTime? - - /// When the current lockup window ends for this holding, if any. - lockupExpiresAt DateTime? - createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - @@unique([ownerAddress, creatorId]) - @@index([ownerAddress]) - @@index([creatorId]) -} + @@unique([ownerAddress, creatorId]) + @@index([ownerAddress]) + @@index([creatorId]) +} \ No newline at end of file From 43a5ca5654061e2de5ae449a8189a552ca3d562f Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 10:34:32 +0100 Subject: [PATCH 05/11] Fix 4 lint errors: unused imports, duplicate function, merge conflict --- src/modules/keys/keys.routes.ts | 3 --- src/utils/redis.utils.ts | 7 ------- src/utils/sequencer-lock.utils.ts | 7 ------- 3 files changed, 17 deletions(-) diff --git a/src/modules/keys/keys.routes.ts b/src/modules/keys/keys.routes.ts index c7d1a994..e8c7706b 100644 --- a/src/modules/keys/keys.routes.ts +++ b/src/modules/keys/keys.routes.ts @@ -15,9 +15,6 @@ import { } from './key-price-history.service'; import { getKeyFees, KeyNotFoundError } from './key-fees.service'; -import { getKeyProposals, KeyNotFoundError as ProposalKeyNotFoundError } from './key-proposals.service'; -import { getKeySupply, KeyNotFoundError as SupplyKeyNotFoundError } from './key-supply.service'; - import { getKeyProposals } from './key-proposals.service'; import { getKeySupply } from './key-supply.service'; diff --git a/src/utils/redis.utils.ts b/src/utils/redis.utils.ts index 0d430508..ecc10e88 100644 --- a/src/utils/redis.utils.ts +++ b/src/utils/redis.utils.ts @@ -220,12 +220,6 @@ export async function disconnectRedis(): Promise { * on creation (lazyConnect is disabled), so simply touching the singleton is * enough to "connect". No-op when caching is disabled (returns null). */ -export async function connectRedis(): Promise { - getRedisClient(); - - * Ensure the shared Redis client is initialised and connected. No-op (resolves - * immediately) when caching is disabled via ENABLE_REDIS_CACHE. - */ export async function connectRedis(): Promise { const client = getRedisClient(); if (client && client.status !== 'ready') { @@ -233,7 +227,6 @@ export async function connectRedis(): Promise { // Connection failures are non-fatal; caching degrades to cache-miss. }); } - } export const getRedis = getRedisClient; diff --git a/src/utils/sequencer-lock.utils.ts b/src/utils/sequencer-lock.utils.ts index 8c2750c3..bcab52c7 100644 --- a/src/utils/sequencer-lock.utils.ts +++ b/src/utils/sequencer-lock.utils.ts @@ -24,13 +24,6 @@ export async function acquireSequencerLock( ): Promise<{ release: () => Promise }> { const redis = getRedis(); - if (!redis) { - throw new SequencerContentionError( - `Cannot acquire sequencer lock for ${creatorWallet}: Redis is not available` - ); - } -======= - if (!redis) { // Redis caching is disabled: degrade to a no-op lock so single-instance // operation continues without distributed coordination. From 8f2baca03e9015d9db2aea82e6ee8fc6a9b98290 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 10:54:10 +0100 Subject: [PATCH 06/11] Fix TypeScript duplicate errors: server.utils, subscription.service, config.schema, error.constants, creator.routes, test files --- src/config.schema.ts | 1 - src/constants/error.constants.ts | 1 + .../admin/key-sync.integration.test.ts | 6 +-- src/modules/creator/creator.routes.ts | 6 +-- .../dividend-endpoint.integration.test.ts | 4 +- .../subscriptions/subscription.service.ts | 50 +------------------ src/utils/server.utils.ts | 9 +--- 7 files changed, 8 insertions(+), 69 deletions(-) diff --git a/src/config.schema.ts b/src/config.schema.ts index 32f511a6..7ded9285 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -108,7 +108,6 @@ export const envSchema = z .int() .positive() .default(900), - JWT_EXPIRES_IN: z.string().default('1h'), // Redis cache REDIS_URL: z.string().default('redis://localhost:6379'), diff --git a/src/constants/error.constants.ts b/src/constants/error.constants.ts index c207198a..3d40c3ab 100644 --- a/src/constants/error.constants.ts +++ b/src/constants/error.constants.ts @@ -15,6 +15,7 @@ export const ErrorCode = { JWT_ERROR: 'TOKEN_ERROR', INSUFFICIENT_BALANCE: 'insufficient_balance', NOT_A_CREATOR: 'not_a_creator', + UNPROCESSABLE_ENTITY: 'UNPROCESSABLE_ENTITY', } as const; export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]; diff --git a/src/modules/admin/key-sync.integration.test.ts b/src/modules/admin/key-sync.integration.test.ts index 134ee66d..928f4c3d 100644 --- a/src/modules/admin/key-sync.integration.test.ts +++ b/src/modules/admin/key-sync.integration.test.ts @@ -29,8 +29,6 @@ describe('Key Sync Integration Tests', () => { passwordHash: 'hash', - passwordHash: 'test-hash', - firstName: 'Test', lastName: 'User', stellarWallet: { create: { address: 'GBTEST0001' } }, @@ -69,9 +67,7 @@ describe('Key Sync Integration Tests', () => { data: { ownerAddress: `GHOLDER${String(i).padStart(52, '0')}`, - creatorId: testCreatorId, - - creatorId: creator.id, +creatorId: testCreatorId, balance: 100, }, diff --git a/src/modules/creator/creator.routes.ts b/src/modules/creator/creator.routes.ts index dc290fa3..9f81452a 100644 --- a/src/modules/creator/creator.routes.ts +++ b/src/modules/creator/creator.routes.ts @@ -76,10 +76,10 @@ creatorsRouter.post( return; } - const keyId = req.params.keyId; +const keyId = Array.isArray(req.params.keyId) ? req.params.keyId[0] : req.params.keyId; try { - const creatorProfile = await prisma.creatorProfile.findFirst({ - where: { OR: [{ id: keyId }, { handle: keyId }] }, + const creatorProfile = await prisma.creatorProfile.findFirst({ + where: { OR: [{ id: keyId }, { handle: keyId }] }, }); if (!creatorProfile) { sendError(res, 404, ErrorCode.NOT_FOUND, 'Key not found'); diff --git a/src/modules/dividends/dividend-endpoint.integration.test.ts b/src/modules/dividends/dividend-endpoint.integration.test.ts index 8c8dc6c5..a906cb9b 100644 --- a/src/modules/dividends/dividend-endpoint.integration.test.ts +++ b/src/modules/dividends/dividend-endpoint.integration.test.ts @@ -236,9 +236,7 @@ describe('Dividend Endpoints Integration Tests', () => { data: { email: `test2-${Date.now()}@example.com`, - passwordHash: 'hash123', - - passwordHash: 'test-hash', +passwordHash: 'hash123', firstName: 'Test', lastName: 'User', diff --git a/src/modules/subscriptions/subscription.service.ts b/src/modules/subscriptions/subscription.service.ts index cb435a80..10a2502a 100644 --- a/src/modules/subscriptions/subscription.service.ts +++ b/src/modules/subscriptions/subscription.service.ts @@ -60,12 +60,6 @@ export async function createSubscription( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) { - throw new Error('Redis is unavailable; subscriptions require a Redis connection'); - } - - const walletKey = walletSubsKey(walletAddress); const currentCount = await redis.zcard(walletKey); @@ -110,9 +104,6 @@ export async function getSubscription( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return null; - const data = await redis.hgetall(subKey(subscriptionId)); if (!data || !data.walletAddress) return null; @@ -128,9 +119,6 @@ export async function deleteSubscription(subscriptionId: string): Promise const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return; -n const sub = await getSubscription(subscriptionId); if (!sub) return; @@ -145,9 +133,6 @@ export async function touchSubscription(subscriptionId: string): Promise { const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return; - await redis.expire(subKey(subscriptionId), SUBSCRIPTION_TTL_S); } @@ -157,9 +142,6 @@ export async function getLastCursor( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return null; - return redis.get(cursorKey(subscriptionId)); } @@ -170,9 +152,6 @@ export async function saveCursor( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return; - await redis.set(cursorKey(subscriptionId), cursor); } @@ -180,9 +159,6 @@ export async function isThrottled(walletAddress: string): Promise { const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return false; - const exists = await redis.exists(throttledKey(walletAddress)); return exists === 1; } @@ -191,9 +167,6 @@ export async function setThrottled(walletAddress: string): Promise { const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return; - await redis.setex(throttledKey(walletAddress), THROTTLE_DURATION_S, '1'); } @@ -203,9 +176,6 @@ export async function incrementConnectionCount( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return 0; - const count = await redis.incr(connectionCountKey(walletAddress)); await redis.expire(connectionCountKey(walletAddress), 60); return count; @@ -217,9 +187,6 @@ export async function decrementConnectionCount( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return; - await redis.decr(connectionCountKey(walletAddress)); } @@ -234,15 +201,6 @@ export async function getWalletSubscriptions( '-1' ); - const redis = getRedis(); - if (!redis) return []; - const ids = await redis.zrange( - walletSubsKey(walletAddress), - '0', - '-1' - ); - - const subs: Subscription[] = []; for (const id of ids) { const sub = await getSubscription(id); @@ -257,9 +215,6 @@ export async function getSubscriptionsByTopic( const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return []; - const ids = await redis.keys(`${SUBSCRIPTION_KEY_PREFIX}*`); const subs: Subscription[] = []; @@ -285,9 +240,6 @@ export async function pruneExpiredSubscriptions(): Promise { const redis = assertRedis(); - const redis = getRedis(); - if (!redis) return 0; - const walletKeys = await redis.keys(`${WALLET_SUBSCRIPTIONS_KEY_PREFIX}*`); let pruned = 0; @@ -304,4 +256,4 @@ export async function pruneExpiredSubscriptions(): Promise { } return pruned; -} +} \ No newline at end of file diff --git a/src/utils/server.utils.ts b/src/utils/server.utils.ts index 1ff730af..664fbbd8 100644 --- a/src/utils/server.utils.ts +++ b/src/utils/server.utils.ts @@ -17,13 +17,6 @@ export async function createServer() { // `src/server.ts` boots. Redis helpers are re-exported here so callers that // previously imported them from this module keep working. -import app from '../app'; import { connectRedis, disconnectRedis } from './redis.utils'; -/** Build and return the configured Express app (without binding a port). */ -export async function createServer() { - return app; -} - -export { connectRedis, disconnectRedis }; - +export { connectRedis, disconnectRedis }; \ No newline at end of file From 4171fedca402106b2f85252979bf361fc8ea8a2a Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 11:08:16 +0100 Subject: [PATCH 07/11] Fix 3 remaining TS1117 duplicate-key errors --- src/config.schema.ts | 6 ------ src/modules/investor/dividend.service.ts | 4 +--- src/modules/whitelist/whitelist.integration.test.ts | 2 -- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/config.schema.ts b/src/config.schema.ts index 7ded9285..87086dbe 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -272,12 +272,6 @@ export const envSchema = z .positive() .default(300000), - SSE_MAX_SUBSCRIPTIONS_PER_WALLET: z.coerce - .number() - .int() - .positive() - .default(10), - SSE_THROTTLE_DURATION_MS: z.coerce .number() .int() diff --git a/src/modules/investor/dividend.service.ts b/src/modules/investor/dividend.service.ts index beb20efd..8d57a174 100644 --- a/src/modules/investor/dividend.service.ts +++ b/src/modules/investor/dividend.service.ts @@ -15,9 +15,7 @@ export async function getInvestorDividends( const items = await prisma.dividendDistribution.findMany({ where, - orderBy: { createdAt: 'desc' }, - - orderBy: { distributionDate: 'desc' }, + orderBy: { createdAt: 'desc' } as const, take: limit + 1, }); diff --git a/src/modules/whitelist/whitelist.integration.test.ts b/src/modules/whitelist/whitelist.integration.test.ts index cb659dce..8be862d0 100644 --- a/src/modules/whitelist/whitelist.integration.test.ts +++ b/src/modules/whitelist/whitelist.integration.test.ts @@ -23,8 +23,6 @@ describe('Whitelist Endpoint Integration Tests', () => { passwordHash: 'hash', - passwordHash: 'test-hash', - firstName: 'Test', lastName: 'User', stellarWallet: { create: { address: 'GBTEST0001' } }, From 0381ba69280430bb2e0fb6b0e4367a2ab5a82280 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 12:02:36 +0100 Subject: [PATCH 08/11] Fix 2 remaining TS1117 duplicate-key errors --- src/config.schema.ts | 3 --- src/modules/admin/key-sync.integration.test.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/config.schema.ts b/src/config.schema.ts index 87086dbe..ae3553e0 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -157,7 +157,6 @@ export const envSchema = z // Stellar auth — optional server keypair secret used for SEP-10 challenge // signing. When absent the server falls back to an ephemeral random keypair. - STELLAR_AUTH_SECRET: optionalNonEmptyString, // Stellar network STELLAR_NETWORK: z @@ -178,7 +177,6 @@ export const envSchema = z 'STELLAR_SOROBAN_RPC_URL must be a valid URL (e.g. https://soroban-testnet.stellar.org)' ) .default('https://soroban-testnet.stellar.org'), - STELLAR_AUTH_SECRET: z .string() .min(32, 'STELLAR_AUTH_SECRET should be at least 32 characters') .default('accesslayer_default_development_stellar_auth_secret_32b'), @@ -278,7 +276,6 @@ export const envSchema = z .positive() .default(1000), // Stellar auth challenge signing secret - STELLAR_AUTH_SECRET: optionalNonEmptyString, }) .superRefine((data, ctx) => { diff --git a/src/modules/admin/key-sync.integration.test.ts b/src/modules/admin/key-sync.integration.test.ts index 928f4c3d..d702c08a 100644 --- a/src/modules/admin/key-sync.integration.test.ts +++ b/src/modules/admin/key-sync.integration.test.ts @@ -54,7 +54,6 @@ describe('Key Sync Integration Tests', () => { creatorId: testCreatorId, currentPrice: 100n, - creatorId: creator.id, currentPrice: 100, lastTradeAt: new Date(), From 440b51cc5fa03b752053924af58c4ad69f27d123 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 12:44:16 +0100 Subject: [PATCH 09/11] Fix TS1117 duplicate-key in config.schema.ts: remove orphaned .string, move STELLAR_AUTH_SECRET to proper property --- src/config.schema.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/config.schema.ts b/src/config.schema.ts index ae3553e0..c5bd97b6 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -176,12 +176,11 @@ export const envSchema = z .url( 'STELLAR_SOROBAN_RPC_URL must be a valid URL (e.g. https://soroban-testnet.stellar.org)' ) - .default('https://soroban-testnet.stellar.org'), - .string() - .min(32, 'STELLAR_AUTH_SECRET should be at least 32 characters') - .default('accesslayer_default_development_stellar_auth_secret_32b'), +.default('https://soroban-testnet.stellar.org'), + + STELLAR_AUTH_SECRET: z.string().min(32).default('accesslayer_default_development_stellar_auth_secret_32b'), - // Ownership snapshot cleanup job + // Ownership snapshot cleanup job OWNERSHIP_SNAPSHOT_TABLE_NAME: z .string() .min(1) From b3b3a2e10d3dafed06529a5c30318ed07771e634 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 12:53:35 +0100 Subject: [PATCH 10/11] Fix last TS1117 duplicate-key: remove duplicate currentPrice at line 57 --- src/modules/admin/key-sync.integration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/admin/key-sync.integration.test.ts b/src/modules/admin/key-sync.integration.test.ts index d702c08a..ef5d2cd5 100644 --- a/src/modules/admin/key-sync.integration.test.ts +++ b/src/modules/admin/key-sync.integration.test.ts @@ -54,7 +54,6 @@ describe('Key Sync Integration Tests', () => { creatorId: testCreatorId, currentPrice: 100n, - currentPrice: 100, lastTradeAt: new Date(), }, From af3e38adc313761c385012d8a650482ad2c3dc47 Mon Sep 17 00:00:00 2001 From: Damilorlar Date: Sun, 30 Aug 2026 20:19:11 +0100 Subject: [PATCH 11/11] Fix TypeScript compile errors: duplicate UNPROCESSABLE_ENTITY in ErrorCode, duplicate keyId declaration in creator routes --- src/constants/error.constants.ts | 1 - src/modules/creator/creator.routes.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/constants/error.constants.ts b/src/constants/error.constants.ts index 0064e09b..3d40c3ab 100644 --- a/src/constants/error.constants.ts +++ b/src/constants/error.constants.ts @@ -9,7 +9,6 @@ export const ErrorCode = { FORBIDDEN: 'FORBIDDEN', CONFLICT: 'CONFLICT', BAD_REQUEST: 'BAD_REQUEST', - UNPROCESSABLE_ENTITY: 'UNPROCESSABLE_ENTITY', INTERNAL_ERROR: 'INTERNAL_ERROR', RATE_LIMIT: 'RATE_LIMIT', PRISMA_ERROR: 'DATABASE_ERROR', diff --git a/src/modules/creator/creator.routes.ts b/src/modules/creator/creator.routes.ts index 2faa3140..136a1c92 100644 --- a/src/modules/creator/creator.routes.ts +++ b/src/modules/creator/creator.routes.ts @@ -78,10 +78,6 @@ creatorsRouter.post( const keyId = Array.isArray(req.params.keyId) ? req.params.keyId[0] : req.params.keyId; - const keyId = Array.isArray(req.params.keyId) - ? req.params.keyId[0] - : req.params.keyId; - try { const creatorProfile = await prisma.creatorProfile.findFirst({ where: { OR: [{ id: keyId }, { handle: keyId }] },