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
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
prefix=/home/l2e/.npm-global
933 changes: 468 additions & 465 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions prisma/schema/creator.prisma
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// prisma/schema/creator.prisma

model CreatorProfile {
id String @id @default(cuid())
userId String @unique
handle String @unique
displayName String
bio String?
avatarUrl String?
perkSummary String?
isVerified Boolean @default(false)
tradingPaused Boolean @default(false)
circulatingSupply Decimal @default(0)
id String @id @default(cuid())
userId String @unique
handle String @unique
displayName String
bio String?
avatarUrl String?
perkSummary String?
isVerified Boolean @default(false)
tradingPaused Boolean @default(false)
circulatingSupply Decimal @default(0)
/// Maximum number of keys that can exist (null = uncapped).
supplyCap Int?
/// Number of keys that have been burned.
Expand All @@ -21,19 +21,19 @@ model CreatorProfile {
creatorRoyaltySellBps Int @default(0)
/// Circuit breaker price movement threshold in basis points (default 3000 = 30%).
circuitBreakerThreshold Int @default(3000) @map("circuit_breaker_threshold")
perks Json?
followersCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
perks Json?
followersCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
priceSnapshot CreatorPriceSnapshot?
priceHistory CreatorPriceHistory[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
priceSnapshot CreatorPriceSnapshot?
priceHistory CreatorPriceHistory[]
pendingPurchases PendingKeyPurchase[]
posts CreatorPost[]
followers Follow[]
posts CreatorPost[]
followers Follow[]
walletFollowers WalletCreatorFollow[]
}

model PendingKeyPurchase {
id String @id @default(cuid())
creatorId String
Expand Down
23 changes: 20 additions & 3 deletions prisma/schema/follow.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,24 @@ model Follow {

creator CreatorProfile @relation(fields: [creatorId], references: [id], onDelete: Cascade)

@@unique([followerAddress, creatorId])
@@index([creatorId])
@@index([followerAddress])
@@unique([followerAddress, creatorId])
@@index([creatorId])
@@index([followerAddress])
}

/// Wallet-to-creator follow relationships (distinct from the creator
/// follower graph tracked by `Follow`). Backed by the `wallet_creator_follows`
/// table created by the add_wallet_creator_follows migration.
model WalletCreatorFollow {
id String @id @default(cuid())
walletAddress String
creatorId String
createdAt DateTime @default(now())

creator CreatorProfile @relation(fields: [creatorId], references: [id], onDelete: Cascade)

@@unique([walletAddress, creatorId])
@@index([creatorId])
@@index([walletAddress])
@@map("wallet_creator_follows")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "CreatorProfile" ADD COLUMN "tradingPaused" BOOLEAN NOT NULL DEFAULT false;
13 changes: 8 additions & 5 deletions prisma/schema/ownership.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ model KeyOwnership {
// The ID or handle of the creator whose keys are owned
creatorId String

// The amount of keys owned
balance Decimal @default(0)
costBasis Decimal? @default(0)
// The amount of keys owned
balance Decimal @default(0)
costBasis Decimal? @default(0)

/// When the current lockup window ends for this holding, if any.
lockupExpiresAt 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
Expand Down
26 changes: 26 additions & 0 deletions src/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const envSchema = z
.min(32, 'JWT_SECRET should be at least 32 characters')
.default('accesslayer_default_development_jwt_secret_key_32_bytes'),
JWT_ISSUER: z.string().default('accesslayer-server'),
JWT_EXPIRES_IN: z.string().default('15m'),
JWT_ACCESS_TOKEN_TTL_SECONDS: z.coerce
.number()
.int()
Expand Down Expand Up @@ -240,6 +241,31 @@ export const envSchema = z
.positive()
.default(5000),
SSE_REPLAY_MAX_EVENTS: z.coerce.number().int().positive().default(100),

// Server-Sent Events (SSE) subscription limits
SSE_MAX_CONNECTIONS_PER_WALLET: z.coerce
.number()
.int()
.positive()
.default(10),
SSE_SUBSCRIPTION_TTL_MS: z.coerce
.number()
.int()
.positive()
.default(300000),
SSE_MAX_SUBSCRIPTIONS_PER_WALLET: z.coerce
.number()
.int()
.positive()
.default(10),
SSE_THROTTLE_DURATION_MS: z.coerce
.number()
.int()
.positive()
.default(1000),

// Stellar auth challenge signing secret
STELLAR_AUTH_SECRET: optionalNonEmptyString,
})
.superRefine((data, ctx) => {
if (data.MODE === 'production' && data.STELLAR_NETWORK === 'testnet') {
Expand Down
1 change: 1 addition & 0 deletions src/constants/creator-detail-include.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const CREATOR_DETAIL_DEFAULT_SELECT = {
avatarUrl: true,
perks: true,
isVerified: true,
tradingPaused: true,
createdAt: true,
updatedAt: true,
priceSnapshot: {
Expand Down
88 changes: 87 additions & 1 deletion src/modules/admin/admin.controllers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { httpReplayIndexerEvents } from './admin.controllers';
import { httpReplayIndexerEvents, httpUpdateCreatorMetadata } from './admin.controllers';
import { emitAuditEvent } from '../../utils/audit.utils';
import { AdminRequest } from '../../middlewares/admin-guard.middleware';
import { Response } from 'express';
Expand Down Expand Up @@ -107,3 +107,89 @@ describe('httpReplayIndexerEvents', () => {
expect(res.status).toHaveBeenCalledWith(200);
});
});

describe('httpUpdateCreatorMetadata — tradingPaused', () => {
const { prisma } = require('../../utils/prisma.utils');
const next = jest.fn();

const createRes = (): Response =>
({
status: jest.fn().mockReturnThis(),
json: jest.fn(),
set: jest.fn().mockReturnThis(),
header: jest.fn().mockReturnThis(),
}) as unknown as Response;

beforeEach(() => {
jest.clearAllMocks();
});

it('persists tradingPaused and emits a pause audit event', async () => {
prisma.creatorProfile.findUnique.mockResolvedValue({
id: 'creator-1',
isVerified: false,
tradingPaused: false,
});
prisma.creatorProfile.update.mockResolvedValue({
id: 'creator-1',
isVerified: false,
tradingPaused: true,
});

const req = {
params: { id: 'creator-1' },
headers: { 'x-admin-id': 'admin-9' },
body: { tradingPaused: true },
} as unknown as AdminRequest;
const res = createRes();

await httpUpdateCreatorMetadata(req, res, next);

expect(prisma.creatorProfile.update).toHaveBeenCalledWith({
where: { id: 'creator-1' },
data: { tradingPaused: true },
});
expect(emitAuditEvent).toHaveBeenCalledWith(
expect.objectContaining({
actor: 'admin-9',
action: 'pause_creator_trading',
targetId: 'creator-1',
metadata: expect.objectContaining({
tradingPaused: expect.objectContaining({ before: false, after: true }),
}),
})
);
});

it('emits a resume audit event when unpausing', async () => {
prisma.creatorProfile.findUnique.mockResolvedValue({
id: 'creator-2',
isVerified: false,
tradingPaused: true,
});
prisma.creatorProfile.update.mockResolvedValue({
id: 'creator-2',
isVerified: false,
tradingPaused: false,
});

const req = {
params: { id: 'creator-2' },
headers: { 'x-admin-id': 'admin-10' },
body: { tradingPaused: false },
} as unknown as AdminRequest;
const res = createRes();

await httpUpdateCreatorMetadata(req, res, next);

expect(emitAuditEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'resume_creator_trading',
targetId: 'creator-2',
metadata: expect.objectContaining({
tradingPaused: expect.objectContaining({ before: true, after: false }),
}),
})
);
});
});
10 changes: 9 additions & 1 deletion src/modules/admin/admin.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { updateProtocolFeeBps } from '../keys/key-fees.service';

const UpdateCreatorMetadataSchema = z.object({
isVerified: z.boolean().optional(),
tradingPaused: z.boolean().optional(),
});

type UpdateCreatorMetadataInput = z.infer<typeof UpdateCreatorMetadataSchema>;
Expand Down Expand Up @@ -76,6 +77,7 @@ export const httpUpdateCreatorMetadata: AsyncController = async (

const previousValues = {
isVerified: creator.isVerified,
tradingPaused: creator.tradingPaused,
};

const updated = await prisma.creatorProfile.update({
Expand All @@ -94,9 +96,15 @@ export const httpUpdateCreatorMetadata: AsyncController = async (
});

if (Object.keys(changes).length > 0) {
const action =
'tradingPaused' in changes
? changes.tradingPaused && (changes.tradingPaused as any).after === true
? 'pause_creator_trading'
: 'resume_creator_trading'
: 'update_creator_metadata';
await emitAuditEvent({
actor: actorId,
action: 'update_creator_metadata',
action,
target: 'CreatorProfile',
targetId: id,
metadata: changes,
Expand Down
1 change: 0 additions & 1 deletion src/modules/admin/audit-log-endpoint.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
1 change: 0 additions & 1 deletion src/modules/admin/audit-log.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
12 changes: 7 additions & 5 deletions src/modules/admin/key-sync.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -27,6 +26,9 @@ describe('Key Sync Integration Tests', () => {
const user = await prisma.user.create({
data: {
email: `test-${Date.now()}@example.com`,
passwordHash: 'test-hash',
firstName: 'Test',
lastName: 'User',
stellarWallet: { create: { address: 'GBTEST0001' } },
},
});
Expand All @@ -46,9 +48,9 @@ describe('Key Sync Integration Tests', () => {
// Create price snapshot
await prisma.creatorPriceSnapshot.create({
data: {
creatorId,
price: 100,
priceUpdatedAt: new Date(),
creatorId: creator.id,
currentPrice: 100,
lastTradeAt: new Date(),
},
});

Expand All @@ -57,7 +59,7 @@ describe('Key Sync Integration Tests', () => {
await prisma.keyOwnership.create({
data: {
ownerAddress: `GHOLDER${String(i).padStart(52, '0')}`,
creatorId,
creatorId: creator.id,
balance: 100,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FIXTURE_PROFILE = {
updatedAt: '2024-01-02T00:00:00.000Z',
perks: [],
links: [],
tradingPaused: false,
currentPrice: null,
price24hAgo: null,
priceChange24h: null,
Expand Down
2 changes: 2 additions & 0 deletions src/modules/creator/creator-profile.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const CreatorProfileReadResponseSchema = z.object({
updatedAt: z.string().datetime().nullable(),
perks: z.array(CreatorPerkSchema).optional(),
links: z.array(z.object({ label: z.string(), url: z.string().url() })),
/** Whether trading is temporarily paused for this key by an admin. */
tradingPaused: z.boolean(),
/** Current key price in stroops as a string. null when no trade has occurred. */
currentPrice: z.string().nullable(),
/** Price 24 h ago in stroops as a string. null when no baseline exists. */
Expand Down
Loading
Loading