From 0d7e9daa8b00eb4046cd3230dcb94a2c45b8cc69 Mon Sep 17 00:00:00 2001 From: success-OG Date: Sun, 26 Apr 2026 12:04:07 +0100 Subject: [PATCH] fix(backend,contract): listCreators chain merge, JwtAuthGuard test wiring, cargo audit exceptions, treasury SorobanError scope --- .../src/creators/creators.controller.spec.ts | 21 ++++++- backend/src/creators/creators.controller.ts | 7 +++ backend/src/creators/creators.service.spec.ts | 1 - backend/src/creators/creators.service.ts | 59 +++++++++++++++++++ .../subscriptions/subscriptions.service.ts | 3 - contract/audit.toml | 26 +++++--- contract/contracts/treasury/src/test.rs | 12 ++-- 7 files changed, 109 insertions(+), 20 deletions(-) diff --git a/backend/src/creators/creators.controller.spec.ts b/backend/src/creators/creators.controller.spec.ts index 08f85a28..918c95db 100644 --- a/backend/src/creators/creators.controller.spec.ts +++ b/backend/src/creators/creators.controller.spec.ts @@ -6,6 +6,7 @@ import { SearchCreatorsDto } from './dto/search-creators.dto'; import { PaginatedResponseDto } from '../common/dto'; import { PublicCreatorDto } from './dto/public-creator.dto'; import { BadRequestException } from '@nestjs/common'; +import { JwtAuthGuard } from '../auth-module/guards/jwt-auth.guard'; describe('CreatorsController', () => { let controller: CreatorsController; @@ -15,6 +16,7 @@ describe('CreatorsController', () => { createPlan: jest.fn(), findAllPlans: jest.fn(), findCreatorPlans: jest.fn(), + listCreators: jest.fn(), }; beforeEach(async () => { @@ -30,7 +32,10 @@ describe('CreatorsController', () => { useValue: { getDashboard: jest.fn() }, }, ], - }).compile(); + }) + .overrideGuard(JwtAuthGuard) + .useValue({ canActivate: () => true }) + .compile(); controller = module.get(CreatorsController); }); @@ -43,6 +48,20 @@ describe('CreatorsController', () => { expect(controller).toBeDefined(); }); + describe('listCreators', () => { + it('should call service.listCreators with mergeChain=false by default', async () => { + mockCreatorsService.listCreators.mockResolvedValue([]); + await controller.listCreators(undefined); + expect(mockCreatorsService.listCreators).toHaveBeenCalledWith(false); + }); + + it('should call service.listCreators with mergeChain=true when chain=true', async () => { + mockCreatorsService.listCreators.mockResolvedValue([]); + await controller.listCreators('true'); + expect(mockCreatorsService.listCreators).toHaveBeenCalledWith(true); + }); + }); + describe('searchCreators', () => { describe('GET /creators endpoint exists and is accessible', () => { diff --git a/backend/src/creators/creators.controller.ts b/backend/src/creators/creators.controller.ts index aa4622f7..122cd3c1 100644 --- a/backend/src/creators/creators.controller.ts +++ b/backend/src/creators/creators.controller.ts @@ -35,6 +35,13 @@ export class CreatorsController { return this.creatorsService.searchCreators(searchDto); } + @Get('list') + @ApiOperation({ summary: 'List all creator plans, optionally merged with on-chain state' }) + @ApiResponse({ status: 200, description: 'Array of plans with optional chain sync status' }) + listCreators(@Query('chain') chain?: string): Promise { + return this.creatorsService.listCreators(chain === 'true'); + } + @Post('plans') @ApiOperation({ summary: 'Create a new subscription plan' }) @ApiResponse({ status: 201, description: 'Plan created successfully' }) diff --git a/backend/src/creators/creators.service.spec.ts b/backend/src/creators/creators.service.spec.ts index 3434505c..e08d69da 100644 --- a/backend/src/creators/creators.service.spec.ts +++ b/backend/src/creators/creators.service.spec.ts @@ -461,7 +461,6 @@ describe('CreatorsService', () => { }); describe('logging and resilience', () => { - it('createPlan logs when EventBus is not injected and still persists plan', async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ CreatorsService, diff --git a/backend/src/creators/creators.service.ts b/backend/src/creators/creators.service.ts index 3382a3f6..a7f569df 100644 --- a/backend/src/creators/creators.service.ts +++ b/backend/src/creators/creators.service.ts @@ -8,6 +8,7 @@ import { User } from '../users/entities/user.entity'; import { PlanDto } from './dto/plan.dto'; import { PublicCreatorDto } from './dto/public-creator.dto'; import { SearchCreatorsDto } from './dto/search-creators.dto'; +import { SubscriptionChainReaderService } from '../subscriptions/subscription-chain-reader.service'; export interface Plan { id: number; @@ -30,6 +31,8 @@ export class CreatorsService { private readonly userRepository: Repository, @Optional() private readonly eventBus?: EventBus, + @Optional() + private readonly chainReader?: SubscriptionChainReaderService, ) {} createPlan( @@ -135,6 +138,62 @@ export class CreatorsService { ); } + /** + * Lists all in-memory plans, optionally merging chain state for each plan. + * Chain reads are best-effort: stale/disconnected results fall back to + * syncStatus='unknown' so callers always receive a valid response. + */ + async listCreators(mergeChain = false): Promise { + const allPlans = Array.from(this.plans.values()).sort((a, b) => a.id - b.id); + + if (!mergeChain) { + return allPlans.map((p) => Object.assign(new PlanDto(), p)); + } + + const contractId = this.chainReader?.getConfiguredContractId(); + if (!contractId || !this.chainReader) { + this.logger.debug('listCreators: chain reader not configured, skipping merge'); + return allPlans.map((p) => + Object.assign(new PlanDto(), { ...p, syncStatus: 'unknown' }), + ); + } + + // Build planMap for O(1) lookup during merge + const planMap = new Map(allPlans.map((p) => [p.id, p])); + + const merged = await Promise.all( + allPlans.map(async (plan) => { + const chainResult = await this.chainReader!.readPlan(contractId, plan.id); + if (!chainResult.ok) { + this.logger.warn( + `listCreators: chain read failed for plan ${plan.id}: ${chainResult.error}`, + ); + const local = planMap.get(plan.id)!; + return Object.assign(new PlanDto(), { + ...local, + syncStatus: 'unknown' as const, + }); + } + + const local = planMap.get(plan.id)!; + const chainPlan = chainResult.plan; + const isSynced = + local.creator === chainPlan.creator && + local.asset === chainPlan.asset && + local.amount === chainPlan.amount && + local.intervalDays === chainPlan.intervalDays; + + return Object.assign(new PlanDto(), { + ...local, + syncStatus: isSynced ? ('synced' as const) : ('stale' as const), + lastSyncedAt: new Date(), + }); + }), + ); + + return merged; + } + async searchCreators( searchDto: SearchCreatorsDto, ): Promise> { diff --git a/backend/src/subscriptions/subscriptions.service.ts b/backend/src/subscriptions/subscriptions.service.ts index 934825c8..9eef4d47 100644 --- a/backend/src/subscriptions/subscriptions.service.ts +++ b/backend/src/subscriptions/subscriptions.service.ts @@ -37,7 +37,6 @@ export enum CheckoutStatus { export const SERVER_NETWORK = process.env.STELLAR_NETWORK ?? 'testnet'; -treasury-deposit-event interface Subscription { id: string; fan: string; @@ -49,8 +48,6 @@ interface Subscription { updatedAt?: Date; } - -main interface Checkout { id: string; fanAddress: string; diff --git a/contract/audit.toml b/contract/audit.toml index 4933cbb3..59593b5c 100644 --- a/contract/audit.toml +++ b/contract/audit.toml @@ -10,13 +10,21 @@ severity_threshold = "high" ## To ignore a specific advisory, add it under `[advisories]` and include ## a justification comment explaining why it is safe in this project. -## -## Example: -## -## [advisories] -## # RUSTSEC-0000-0000: -## # Reason: -## ignore = [ -## "RUSTSEC-0000-0000", -## ] +[advisories] +# RUSTSEC-2024-0436: paste - no longer maintained +# Reason: Transitive dependency via soroban-wasmi → soroban-env-host → soroban-sdk 21.7.7. +# The `paste` crate is used only in macro expansion at compile time and has no runtime +# security surface. No upstream fix is available without upgrading soroban-sdk beyond the +# version pinned by this workspace. Tracked in: upgrade soroban-sdk when 22.x stabilises. +# Severity: warning (unmaintained), not high/critical — below our threshold. +# RUSTSEC-2026-0012: keccak 0.1.5 — unsoundness in opt-in ARMv8 assembly backend +# Reason: Transitive dependency via soroban-env-host → sha3 → keccak. +# The unsoundness only manifests when the `asm` feature is enabled on ARMv8 targets. +# Soroban contracts compile to Wasm (wasm32-unknown-unknown), not ARMv8 native code, +# so the vulnerable code path is never exercised. No upstream fix available without +# upgrading soroban-sdk. Severity: warning (unsound), not high/critical. +ignore = [ + "RUSTSEC-2024-0436", + "RUSTSEC-2026-0012", +] diff --git a/contract/contracts/treasury/src/test.rs b/contract/contracts/treasury/src/test.rs index c5d68d87..cbd0d9d5 100644 --- a/contract/contracts/treasury/src/test.rs +++ b/contract/contracts/treasury/src/test.rs @@ -3,7 +3,7 @@ use soroban_sdk::{ testutils::{Address as _, Events, MockAuth, MockAuthInvoke}, token::{StellarAssetClient, TokenClient}, xdr::SorobanAuthorizationEntry, - Address, Env, Error as SorobanError, IntoVal, Symbol, TryIntoVal, + Address, Env, IntoVal, Symbol, TryIntoVal, }; fn create_token_contract<'a>( @@ -63,7 +63,7 @@ fn test_withdraw_insufficient_balance() { let result = treasury_client.try_withdraw(&user, &500); assert_eq!( result, - Err(Ok(SorobanError::from_contract_error( + Err(Ok(soroban_sdk::Error::from_contract_error( Error::InsufficientBalance as u32, ))) ); @@ -164,7 +164,7 @@ fn test_pause_blocks_deposit() { let result = treasury_client.try_deposit(&user, &100); assert_eq!( result, - Err(Ok(SorobanError::from_contract_error(Error::Paused as u32))) + Err(Ok(soroban_sdk::Error::from_contract_error(Error::Paused as u32))) ); } @@ -190,7 +190,7 @@ fn test_pause_blocks_withdraw() { let result = treasury_client.try_withdraw(&user, &100); assert_eq!( result, - Err(Ok(SorobanError::from_contract_error(Error::Paused as u32))) + Err(Ok(soroban_sdk::Error::from_contract_error(Error::Paused as u32))) ); } @@ -241,7 +241,7 @@ fn test_min_balance_blocks_withdraw() { let result = treasury_client.try_withdraw(&user, &1); // would leave 299 < 300 assert_eq!( result, - Err(Ok(SorobanError::from_contract_error( + Err(Ok(soroban_sdk::Error::from_contract_error( Error::MinBalanceViolation as u32, ))) ); @@ -285,7 +285,7 @@ fn test_set_min_balance_negative_reverts() { let result = treasury_client.try_set_min_balance(&-1); assert_eq!( result, - Err(Ok(SorobanError::from_contract_error( + Err(Ok(soroban_sdk::Error::from_contract_error( Error::NegativeMinBalance as u32, ))) );