diff --git a/src/main.ts b/src/main.ts index 2841a21..2cd6c72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import * as bodyParser from 'body-parser'; import * as fs from 'fs'; import * as path from 'path'; import { AppModule } from './app.module'; +import { RoyaltyConfigurationService } from './nft/royalty-configuration.service'; import { PayoutsService } from './payouts/payouts.service'; import { StellarWebhookService } from './subscriptions/stellar-webhook.service'; import { MetricsInterceptor } from './metrics/metrics.interceptor'; @@ -52,6 +53,20 @@ async function bootstrap() { process.exit(1); } + // Validate royalty configuration on startup + const royaltyConfigService = app.get(RoyaltyConfigurationService); + try { + royaltyConfigService.validateRoyaltyConfiguration(); + logger.log( + `Royalty configuration validated: ` + + `creatorRoyaltyBps=${royaltyConfigService.getCreatorRoyaltyBps()}, ` + + `platformRoyaltyBps=${royaltyConfigService.getPlatformRoyaltyBps()}`, + ); + } catch (error) { + logger.error(`Invalid royalty configuration: ${error.message}`); + process.exit(1); + } + // Swagger setup - only available in non-production environments const swaggerConfig = new DocumentBuilder() .setTitle('ClipCash API') diff --git a/src/nft/royalty-configuration.service.ts b/src/nft/royalty-configuration.service.ts index 8d500b2..faaa8f6 100644 --- a/src/nft/royalty-configuration.service.ts +++ b/src/nft/royalty-configuration.service.ts @@ -1,7 +1,12 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import StellarSdk from '@stellar/stellar-sdk'; import { ConfigService } from '../config/config.service'; -import { CLIP_ROYALTY_BPS_MAX } from '../common/validators/is-valid-royalty-bps.validator'; +import { + CLIP_ROYALTY_BPS_MAX, + DEFAULT_ROYALTY_BPS_MAX, +} from '../common/validators/is-valid-royalty-bps.validator'; + +export const ROYALTY_PROTOCOL_MAX_BPS = DEFAULT_ROYALTY_BPS_MAX; export interface RoyaltyMapEntry { key: unknown; @@ -42,12 +47,70 @@ export class RoyaltyConfigurationService { } } + validatePlatformRoyaltyBps(bps: number): void { + if (!Number.isInteger(bps) || isNaN(bps) || bps < 0) { + throw new BadRequestException( + `Invalid platformRoyaltyBps: ${bps}. Must be a non-negative integer.`, + ); + } + } + + validateCombinedRoyaltyBps(creatorBps: number, platformBps: number): void { + const total = creatorBps + platformBps; + if (total > ROYALTY_PROTOCOL_MAX_BPS) { + throw new BadRequestException( + `Combined royalty (${total} bps = ${creatorBps} creator + ${platformBps} platform) exceeds protocol maximum of ${ROYALTY_PROTOCOL_MAX_BPS} bps.`, + ); + } + } + + /** + * Full startup validation of royalty environment configuration. + * Throws Error (not BadRequestException) so bootstrap can catch it and exit. + */ + validateRoyaltyConfiguration(): void { + const platformWallet = this.config.platformWallet; + if (!platformWallet) { + throw new Error('PLATFORM_WALLET_ADDRESS must be configured'); + } + if (!StellarSdk.StrKey.isValidEd25519PublicKey(platformWallet)) { + throw new Error( + `PLATFORM_WALLET_ADDRESS "${platformWallet}" is not a valid Stellar Ed25519 public key`, + ); + } + + const platformBps = this.config.platformRoyaltyBps; + if (!Number.isInteger(platformBps) || isNaN(platformBps) || platformBps < 0) { + throw new Error( + `PLATFORM_ROYALTY_BPS must be a non-negative integer, got: ${platformBps}`, + ); + } + + const creatorBps = this.config.creatorRoyaltyBps; + if (!Number.isInteger(creatorBps) || isNaN(creatorBps) || creatorBps < 0) { + throw new Error( + `CREATOR_ROYALTY_BPS must be a non-negative integer, got: ${creatorBps}`, + ); + } + + const total = creatorBps + platformBps; + if (total > ROYALTY_PROTOCOL_MAX_BPS) { + throw new Error( + `Combined royalty (${total} bps = ${creatorBps} creator + ${platformBps} platform) exceeds protocol maximum of ${ROYALTY_PROTOCOL_MAX_BPS} bps`, + ); + } + } + buildRoyaltyMap( creatorWallet: string, clipRoyaltyBps?: number | null, ): RoyaltyMapEntry[] { const creatorRoyaltyBps = this.getCreatorRoyaltyBps(clipRoyaltyBps); const platformWallet = this.getPlatformWallet(); + const platformBps = this.getPlatformRoyaltyBps(); + + this.validatePlatformRoyaltyBps(platformBps); + this.validateCombinedRoyaltyBps(creatorRoyaltyBps, platformBps); return [ { @@ -56,9 +119,7 @@ export class RoyaltyConfigurationService { }, { key: StellarSdk.Address.fromString(platformWallet).toScVal(), - value: StellarSdk.nativeToScVal(this.getPlatformRoyaltyBps(), { - type: 'u32', - }), + value: StellarSdk.nativeToScVal(platformBps, { type: 'u32' }), }, ]; }