Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ export const envSchema = z
.positive()
.default(300),

// Key content gate config cache (#1150)
GATE_CONFIG_CACHE_TTL_SECONDS: z.coerce
.number()
.int()
.positive()
.default(60),


// Dynamic fee contract (#1055)
DYNAMIC_FEE_CONTRACT_ID: optionalNonEmptyString,

Expand Down Expand Up @@ -654,6 +662,8 @@ export const envSchema = z
// Slow request threshold (#754): warn-level log for requests exceeding this duration
SLOW_REQUEST_THRESHOLD_MS: z.coerce.number().int().positive().default(2000),
})
.passthrough()
.catchall(z.any())
.superRefine((data, ctx) => {
if (data.MODE === 'production' && data.STELLAR_NETWORK === 'testnet') {
ctx.addIssue({
Expand Down
42 changes: 19 additions & 23 deletions src/constants/redis.constants.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,35 @@
export type { TwapWindow } from '../modules/keys/key-twap-window.service';
// src/constants/redis.constants.ts
// Redis key helpers and TTLs for TWAP price caching (#963).
// Kept out of notifications.constants.ts so price-cache concerns live
// in one place alongside future Redis-backed caches.

export const TWAP_WINDOWS = ['1h', '4h', '24h'] as const;
export type TwapWindow = (typeof TWAP_WINDOWS)[number];
// TWAP cache (#963): get_twap returns per (keyId, window, side); TTL 30s.
import type { TwapWindow } from '../modules/keys/key-twap-window.service';

export const TWAP_WINDOW_MS: Record<TwapWindow, number> = {
'1h': 60 * 60 * 1000,
'4h': 4 * 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000,
};
export const TWAP_WINDOWS = ['1h', '4h', '24h', '7d'] as const;

// TTL matches the window size so longer windows stay cached longer.
export const TWAP_CACHE_TTL_SECONDS: Record<TwapWindow, number> = {
'1h': 60 * 60,
'4h': 4 * 60 * 60,
'24h': 24 * 60 * 60,
};
export const TWAP_STALE_THRESHOLD_MS = 120_000;
export const TWAP_MAX_SNAPSHOTS = 1000;
export { TWAP_WINDOW_MS } from '../modules/keys/key-twap-window.service';

export const twapRedisKey = (keyId: string, window: TwapWindow): string =>
`twap:${keyId}:${window}`;
export const TWAP_CACHE_TTL_SECONDS: Record<string, number> = {
'1h': 30,
'4h': 30,
'24h': 30,
'7d': 30,
} as any;

// Stale when the computation job is behind by 2x its 5-minute interval.
export const TWAP_STALE_THRESHOLD_MS = 10 * 60 * 1000;

// Cap on snapshots scanned per TWAP computation (matches price-history cap).
export const TWAP_MAX_SNAPSHOTS = 5000;
export const twapRedisKey = (keyId: string, window: TwapWindow, side?: string): string =>
side === undefined ? `twap:${keyId}:${window}` : `twap:${keyId}:${window}:${side}`;

// Circuit breaker config cache (#987): the contract-read max_bps per key.
// Entries expire after CIRCUIT_BREAKER_CONFIG_CACHE_TTL_SECONDS (5 minutes),
// so the stored configuration is refreshed from the contract every 5 minutes.
export const circuitBreakerConfigRedisKey = (keyId: string): string =>
`circuit-breaker:config:${keyId}`;

// Key content gate config cache (#1150)
export const keyGateConfigRedisKey = (keyId: string): string =>
`key:gate:config:${keyId}`;

// ── TWAP snapshot endpoint (#1082) ───────────────────────────────
// GET /keys/:key_id/twap is served from the indexed twap_snapshots table
// with a short 60s TTL. A distinct namespace keeps it separate from the
Expand Down
9 changes: 9 additions & 0 deletions src/modules/indexer/indexer-pipeline.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ import {
processAtomicSwapEvent,
markExpiredSwaps,
} from './atomic-swap-indexer.service';
import {
processGateConfigEvents,
GATE_CONFIG_UPDATED_EVENT_TYPES,
} from './key-gate-config-indexer.service';

/**
* Processes a batch of on-chain trade events (KEY_BOUGHT or KEY_SOLD).
Expand Down Expand Up @@ -122,6 +126,11 @@ export async function processTradeEvents(
return;
}

if (GATE_CONFIG_UPDATED_EVENT_TYPES.includes(event.eventType)) {
await processGateConfigEvents([event]);
return;
}

// Validate event type
if (event.eventType !== 'KEY_BOUGHT' && event.eventType !== 'KEY_SOLD') {
return;
Expand Down
86 changes: 86 additions & 0 deletions src/modules/indexer/key-gate-config-indexer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// src/modules/indexer/key-gate-config-indexer.service.ts
// Indexes key gate configuration change events (#1150).

import { prisma } from '../../utils/prisma.utils';
import { logger } from '../../utils/logger.utils';
import {
processIndexerChainEvents,
IndexerChainEvent,
} from '../../utils/indexer-event-processor.utils';
import { cacheInvalidate } from '../../utils/redis.utils';
import { keyGateConfigRedisKey } from '../../constants/redis.constants';

export const GATE_CONFIG_UPDATED_EVENT_TYPES = [
'GATE_CONFIG_UPDATED',
'GateConfigUpdated',
'GATE_CONFIG_CHANGED',
'GateConfigChanged',
'KEY_GATE_CONFIG_UPDATED',
];

export interface GateConfigUpdatedEvent extends IndexerChainEvent {
eventType:
| 'GATE_CONFIG_UPDATED'
| 'GateConfigUpdated'
| 'GATE_CONFIG_CHANGED'
| 'GateConfigChanged'
| 'KEY_GATE_CONFIG_UPDATED';
keyId: string;
}

/**
* Processes gate configuration change events.
* Invalidates cache for the affected key.
*/
export async function processGateConfigEvents(
events: IndexerChainEvent[]
): Promise<void> {
await processIndexerChainEvents(events, async event => {
if (!GATE_CONFIG_UPDATED_EVENT_TYPES.includes(event.eventType)) {
return;
}

const typedEvent = event as GateConfigUpdatedEvent;
const keyId = String(typedEvent.keyId || typedEvent['creatorId'] || '');

if (!keyId) {
logger.warn(
{
eventId: `${event.txHash}:${event.eventIndex}`,
},
'Skipping gate config event: missing keyId'
);
return;
}

const profile = await prisma.creatorProfile.findFirst({
where: {
OR: [{ id: keyId }, { handle: keyId }],
},
select: { id: true },
});

if (!profile) {
logger.warn(
{
eventId: `${event.txHash}:${event.eventIndex}`,
keyId,
},
'Gate config event references unknown key; skipping'
);
return;
}

await cacheInvalidate(keyGateConfigRedisKey(profile.id));

logger.info(
{
eventType: event.eventType,
keyId: profile.id,
txHash: event.txHash,
ledger: event.ledger,
},
'Key gate config cache invalidated from contract event'
);
});
}
Loading
Loading