Skip to content
Merged
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
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ MAIL_USER=your-email@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_FROM_NAME=MindBlock
MAIL_FROM_ADDRESS=noreply@mindblock.com

# Stellar / Soroban configuration (BlockchainModule)
STELLAR_SECRET_KEY=your-stellar-secret-key
STELLAR_CONTRACT_ID=your-soroban-contract-id
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
48 changes: 48 additions & 0 deletions backend/src/blockchain/blockchain.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { BlockchainModule } from './blockchain.module';
import { BlockchainService } from './provider/blockchain.service';
import { ProgressModule } from '../progress/progress.module';
import { UsersModule } from '../users/users.module';
import { QuestsModule } from '../quests/quests.module';

const metadataArrayOf = (key: 'imports' | 'providers', target: object) =>
(Reflect.getMetadata(key, target) as unknown[] | undefined) ?? [];

describe('BlockchainModule wiring', () => {
it('provides and exports BlockchainService', async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [EventEmitterModule.forRoot(), BlockchainModule],
}).compile();

expect(module.get(BlockchainService, { strict: false })).toBeInstanceOf(
BlockchainService,
);
});

it('is imported by ProgressModule', () => {
expect(metadataArrayOf('imports', ProgressModule)).toContain(
BlockchainModule,
);
});

it('is imported by UsersModule', () => {
expect(metadataArrayOf('imports', UsersModule)).toContain(BlockchainModule);
});

it('is imported by QuestsModule', () => {
expect(metadataArrayOf('imports', QuestsModule)).toContain(
BlockchainModule,
);
});

it('does not re-export consumers as service providers', () => {
// BlockchainService must come through the BlockchainModule import,
// never be listed directly in a consumer's own providers array.
for (const consumer of [ProgressModule, UsersModule, QuestsModule]) {
expect(metadataArrayOf('providers', consumer)).not.toContain(
BlockchainService,
);
}
});
});
21 changes: 20 additions & 1 deletion backend/src/blockchain/blockchain.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import stellarConfig from '../config/stellar.config';
import { BlockchainController } from './controller/blockchain.controller';
import { BlockchainService } from './provider/blockchain.service';

/**
* Registers the blockchain providers and exports BlockchainService so any
* importing module (ProgressModule, UsersModule, QuestsModule, ...) can
* inject it to trigger on-chain actions.
*
* STELLAR_SECRET_KEY, STELLAR_CONTRACT_ID, STELLAR_RPC_URL and
* STELLAR_NETWORK_PASSPHRASE are exposed through the namespaced
* `stellar` configuration registered below (see src/config/stellar.config.ts).
*/
@Module({
imports: [ConfigModule.forFeature(stellarConfig)],
controllers: [BlockchainController],
providers: [BlockchainService],
providers: [
// On-chain action providers land with issues 11-14:
// RegisterPlayerProvider, SubmitPuzzleProvider, GetPlayerProvider,
// SyncXpMilestoneProvider. Each must be added to this array (and the
// matching method on BlockchainService) as soon as it is merged so
// they are injectable alongside BlockchainService.
BlockchainService,
],
exports: [BlockchainService],
})
export class BlockchainModule {}
17 changes: 17 additions & 0 deletions backend/src/config/stellar.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { registerAs } from '@nestjs/config';

/**
* Stellar/Soroban on-chain configuration.
*
* Loaded through `ConfigModule.forFeature(stellarConfig)` inside
* BlockchainModule so every blockchain provider can resolve these values
* via `ConfigService.get('stellar')` without reaching for `process.env`.
*/
export default registerAs('stellar', () => ({
secretKey: process.env.STELLAR_SECRET_KEY,
contractId: process.env.STELLAR_CONTRACT_ID,
rpcUrl: process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org',
networkPassphrase:
process.env.STELLAR_NETWORK_PASSPHRASE ||
'Test SDF Network ; September 2015',
}));
4 changes: 4 additions & 0 deletions backend/src/progress/progress.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import { ProgressCalculationProvider } from './providers/progress-calculation.pr
import { Puzzle } from '../puzzles/entities/puzzle.entity';
import { XpLevelService } from '../users/providers/xp-level.service';
import { ScoreService } from '../score/providers/score.service';
import { BlockchainModule } from '../blockchain/blockchain.module';

@Module({
imports: [
TypeOrmModule.forFeature([UserProgress, User, Puzzle, Streak, DailyQuest]),
// BlockchainService is needed by ProgressCalculationProvider for
// submitPuzzleOnChain after puzzle answer verification.
BlockchainModule,
],
controllers: [ProgressController],
providers: [
Expand Down
4 changes: 4 additions & 0 deletions backend/src/quests/quests.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PuzzlesModule } from '../puzzles/puzzles.module';
import { ProgressModule } from '../progress/progress.module';
import { UsersModule } from '../users/users.module';
import { StreakModule } from '../streak/strerak.module';
import { BlockchainModule } from '../blockchain/blockchain.module';

@Module({
imports: [
Expand All @@ -19,6 +20,9 @@ import { StreakModule } from '../streak/strerak.module';
ProgressModule,
UsersModule,
StreakModule,
// BlockchainService is needed by CompleteDailyQuestProvider for
// syncXpMilestone after the daily quest completion bonus XP is awarded.
BlockchainModule,
],
controllers: [DailyQuestController],
providers: [
Expand Down
4 changes: 4 additions & 0 deletions backend/src/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ import { UserProgress } from '../progress/entities/progress.entity';
import { Streak } from '../streak/entities/streak.entity';
import { DailyQuest } from '../quests/entities/daily-quest.entity';
import { XpLevelService } from './providers/xp-level.service';
import { BlockchainModule } from '../blockchain/blockchain.module';

@Module({
imports: [
forwardRef(() => AuthModule),
TypeOrmModule.forFeature([User, UserProgress, Streak, DailyQuest]),
PaginationModule,
// BlockchainService is needed by the wallet-linking onboarding flow
// (registerPlayerOnChain) and for syncXpMilestone after level-up.
BlockchainModule,
],
controllers: [UsersController],
providers: [
Expand Down
Loading