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 .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(mkdir -p /home/feyishola/Desktop/mydev/opensource/starkpulse-frontend/apps/backend/src/contributor-registry/dto)"
],
"additionalDirectories": [
"/home/feyishola/Desktop/mydev/opensource/starkpulse-frontend/apps/backend/src/contributor-registry"
]
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { SignalsModule } from './signals/signals.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { AppConfigModule } from './config/config.module';
import { CrowdfundModule } from './crowdfund/crowdfund.module';
import { ContributorRegistryModule } from './contributor-registry/contributor-registry.module';
import { AuditModule } from './audit/audit.module';
import { AuditLogInterceptor } from './audit/interceptors/audit-log.interceptor';
import { SorobanEventsModule } from './soroban-events/soroban-events.module';
Expand Down Expand Up @@ -123,6 +124,7 @@ import { VestingWalletModule } from './vesting-wallet/vesting-wallet.module';
SearchModule,
FeatureFlagsModule,
CrowdfundModule,
ContributorRegistryModule,
AppConfigModule,
AuditModule,
SorobanEventsModule,
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/common/rate-limit/rate-limit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,15 @@ export function getAnalyticsReadThrottleOverride() {
default: getRateLimitSettings().analyticsRead,
};
}

export function getRegistryReadThrottleOverride() {
return {
default: getRateLimitSettings().crowdfundRead,
};
}

export function getRegistryWriteThrottleOverride() {
return {
default: getRateLimitSettings().portfolioWrite,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import {
getRegistryReadThrottleOverride,
getRegistryWriteThrottleOverride,
} from '../common/rate-limit/rate-limit.config';
import { ContributorRegistryService } from './contributor-registry.service';
import {
ContributorResponseDto,
NonceResponseDto,
RegisterContributorDto,
RegisterWithSigDto,
RegistrationXdrResponseDto,
ReputationResponseDto,
SubmitResponseDto,
} from './dto/contributor-registry.dto';

@ApiTags('contributor-registry')
@Controller('contributor-registry')
export class ContributorRegistryController {
constructor(private readonly svc: ContributorRegistryService) {}

// ── Registration ──────────────────────────────────────────────────────────────

@Post('register')
@Throttle(getRegistryWriteThrottleOverride())
@ApiOperation({
summary: 'Register a contributor (direct)',
description:
'In mock mode: immediately stores the contributor and returns a placeholder XDR. ' +
'In real mode: builds an unsigned Soroban transaction XDR that the contributor ' +
'must sign with their Stellar wallet and submit independently.',
})
@ApiResponse({
status: 201,
description: 'Registration XDR built (or contributor stored in mock mode)',
type: RegistrationXdrResponseDto,
})
@ApiResponse({ status: 400, description: 'Contract not configured or invalid input' })
@ApiResponse({ status: 409, description: 'Contributor already registered or handle taken' })
register(@Body() dto: RegisterContributorDto): Promise<RegistrationXdrResponseDto> {
return this.svc.buildRegistrationXdr(dto);
}

@Post('register-with-sig')
@Throttle(getRegistryWriteThrottleOverride())
@ApiOperation({
summary: 'Gasless contributor registration (testnet signing + submission)',
description:
'The contributor signs a SorobanAuthorizationEntry off-chain (no XLM required). ' +
'The server (relayer) builds the transaction, attaches the signed entry, and ' +
'submits it — paying the network fees. ' +
'Obtain the current nonce first via GET /contributor-registry/nonce/:address.',
})
@ApiResponse({
status: 201,
description: 'Transaction submitted successfully',
type: SubmitResponseDto,
})
@ApiResponse({ status: 400, description: 'Invalid signed auth entry or contract not configured' })
@ApiResponse({ status: 409, description: 'Contributor already registered or handle taken' })
registerWithSig(@Body() dto: RegisterWithSigDto): Promise<SubmitResponseDto> {
return this.svc.registerWithSignature(dto);
}

// ── Lookups ───────────────────────────────────────────────────────────────────

@Get('wallet/:address')
@Throttle(getRegistryReadThrottleOverride())
@ApiOperation({
summary: 'Look up contributor by Stellar wallet address',
description: 'Returns contributor profile. Result is cached for 60 seconds.',
})
@ApiParam({ name: 'address', example: 'GABC1234...', description: 'Stellar public key (G...)' })
@ApiResponse({
status: 200,
description: 'Contributor profile',
type: ContributorResponseDto,
})
@ApiResponse({ status: 404, description: 'Contributor not found' })
getByAddress(@Param('address') address: string): Promise<ContributorResponseDto> {
return this.svc.getContributorByAddress(address);
}

@Get('github/:handle')
@Throttle(getRegistryReadThrottleOverride())
@ApiOperation({
summary: 'Look up contributor by GitHub handle',
description: 'Returns contributor profile. Result is cached for 60 seconds.',
})
@ApiParam({ name: 'handle', example: 'octocat', description: 'GitHub username' })
@ApiResponse({
status: 200,
description: 'Contributor profile',
type: ContributorResponseDto,
})
@ApiResponse({ status: 404, description: 'Contributor not found' })
getByGithub(@Param('handle') handle: string): Promise<ContributorResponseDto> {
return this.svc.getContributorByGithub(handle);
}

// ── Reputation ────────────────────────────────────────────────────────────────

@Get('reputation/:address')
@Throttle(getRegistryReadThrottleOverride())
@ApiOperation({
summary: 'Read contributor reputation score and tier',
description:
'Returns the on-chain reputation score and derived tier. ' +
'Result is cached for 60 seconds to reduce RPC load.',
})
@ApiParam({ name: 'address', example: 'GABC1234...', description: 'Stellar public key (G...)' })
@ApiResponse({
status: 200,
description: 'Reputation data',
type: ReputationResponseDto,
})
@ApiResponse({ status: 404, description: 'Contributor not found' })
getReputation(@Param('address') address: string): Promise<ReputationResponseDto> {
return this.svc.getReputation(address);
}

// ── Nonce ─────────────────────────────────────────────────────────────────────

@Get('nonce/:address')
@Throttle(getRegistryReadThrottleOverride())
@ApiOperation({
summary: 'Get registration nonce for off-chain signing',
description:
'Returns the current per-address nonce required when building the ' +
'SorobanAuthorizationEntry for register_contributor_with_sig. ' +
'Cached for 5 seconds only — always fetch immediately before signing.',
})
@ApiParam({ name: 'address', example: 'GABC1234...', description: 'Stellar public key (G...)' })
@ApiResponse({
status: 200,
description: 'Current nonce',
type: NonceResponseDto,
})
getNonce(@Param('address') address: string): Promise<NonceResponseDto> {
return this.svc.getNonce(address);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { AppCacheModule } from '../cache/cache.module';
import { StellarModule } from '../stellar/stellar.module';
import { ContributorRegistryController } from './contributor-registry.controller';
import { ContributorRegistryService } from './contributor-registry.service';

@Module({
imports: [AppCacheModule, StellarModule],
controllers: [ContributorRegistryController],
providers: [ContributorRegistryService],
exports: [ContributorRegistryService],
})
export class ContributorRegistryModule {}
Loading
Loading