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
2 changes: 2 additions & 0 deletions apps/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DeployModule } from './deploy/deploy.module';
import { LoansModule } from './loans/loans.module';
import { ParticipationTokenModule } from './participation-token/participation-token.module';
import { VaultModule } from './vault/vault.module';
import { TokenFactoryModule } from './token-factory/token-factory.module';

@Module({
imports: [
Expand All @@ -20,6 +21,7 @@ import { VaultModule } from './vault/vault.module';
LoansModule,
ParticipationTokenModule,
VaultModule,
TokenFactoryModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
27 changes: 27 additions & 0 deletions apps/core/src/token-factory/dto/approve.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';

export class ApproveDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
from: string;

@IsString()
@IsNotEmpty()
spender: string;

@IsNumber()
@IsPositive()
amount: number;

@IsNumber()
@IsPositive()
expirationLedger: number;

@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
23 changes: 23 additions & 0 deletions apps/core/src/token-factory/dto/burn-from.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';

export class BurnFromDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
spender: string;

@IsString()
@IsNotEmpty()
from: string;

@IsNumber()
@IsPositive()
amount: number;

@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
19 changes: 19 additions & 0 deletions apps/core/src/token-factory/dto/burn.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';

export class BurnDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
from: string;

@IsNumber()
@IsPositive()
amount: number;

@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
19 changes: 19 additions & 0 deletions apps/core/src/token-factory/dto/mint.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';

export class MintDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
to: string;

@IsNumber()
@IsPositive()
amount: number;

@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
15 changes: 15 additions & 0 deletions apps/core/src/token-factory/dto/set-admin.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsString, IsNotEmpty } from 'class-validator';

export class SetAdminDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
newAdmin: string;

@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
27 changes: 27 additions & 0 deletions apps/core/src/token-factory/dto/transfer-from.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';

export class TransferFromDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
spender: string;

@IsString()
@IsNotEmpty()
from: string;

@IsString()
@IsNotEmpty()
to: string;

@IsNumber()
@IsPositive()
amount: number;
Comment on lines +20 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use a bigint-safe representation for amount.

amount: number accepts fractional values and silently loses precision above 2^53 - 1, which is risky for SEP-41 token amounts. Prefer a positive integer string here, then convert with BigInt(dto.amount) in the service before building the Soroban call.

🔧 Suggested DTO change
-import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';
+import { Transform } from 'class-transformer';
+import { IsString, IsNotEmpty, Matches } from 'class-validator';
@@
-  `@IsNumber`()
-  `@IsPositive`()
-  amount: number;
+  `@Transform`(({ value }) => String(value))
+  `@IsString`()
+  `@Matches`(/^[1-9]\d*$/, { message: 'amount must be a positive integer string' })
+  amount: string;

As per coding guidelines "Use class-validator and class-transformer decorators for DTOs".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/core/src/token-factory/dto/transfer-from.dto.ts` around lines 20 - 22,
The DTO currently uses amount: number which allows fractions and loses
precision; change the property on the TransferFrom DTO (the amount field in
transfer-from.dto.ts) to a string and validate it as a positive integer string
(e.g., replace `@IsNumber`()/@IsPositive() with `@IsString`() plus a regex validator
like `@Matches`(/^[1-9]\d*$/) or an equivalent custom validator) so only integer
strings are accepted; then, in the service code that handles this DTO, convert
to BigInt with BigInt(dto.amount) before building the Soroban call.


@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
23 changes: 23 additions & 0 deletions apps/core/src/token-factory/dto/transfer.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IsString, IsNumber, IsNotEmpty, IsPositive } from 'class-validator';

export class TransferDto {
@IsString()
@IsNotEmpty()
contractId: string;

@IsString()
@IsNotEmpty()
from: string;

@IsString()
@IsNotEmpty()
to: string;

@IsNumber()
@IsPositive()
amount: number;

@IsString()
@IsNotEmpty()
callerPublicKey: string;
}
122 changes: 122 additions & 0 deletions apps/core/src/token-factory/token-factory.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Controller, Post, Get, Body, Query } from '@nestjs/common';
import { TokenFactoryService } from './token-factory.service';
import { MintDto } from './dto/mint.dto';
import { SetAdminDto } from './dto/set-admin.dto';
import { ApproveDto } from './dto/approve.dto';
import { TransferDto } from './dto/transfer.dto';
import { TransferFromDto } from './dto/transfer-from.dto';
import { BurnDto } from './dto/burn.dto';
import { BurnFromDto } from './dto/burn-from.dto';

@Controller('token-factory')
export class TokenFactoryController {
constructor(private readonly tokenFactoryService: TokenFactoryService) {}

// ── POST endpoints (writes) ──

@Post('mint')
async mint(@Body() dto: MintDto) {
const unsignedXdr = await this.tokenFactoryService.mint(dto);
return { unsignedXdr };
}

@Post('set-admin')
async setAdmin(@Body() dto: SetAdminDto) {
const unsignedXdr = await this.tokenFactoryService.setAdmin(dto);
return { unsignedXdr };
}

@Post('approve')
async approve(@Body() dto: ApproveDto) {
const unsignedXdr = await this.tokenFactoryService.approve(dto);
return { unsignedXdr };
}

@Post('transfer')
async transfer(@Body() dto: TransferDto) {
const unsignedXdr = await this.tokenFactoryService.transfer(dto);
return { unsignedXdr };
}

@Post('transfer-from')
async transferFrom(@Body() dto: TransferFromDto) {
const unsignedXdr = await this.tokenFactoryService.transferFrom(dto);
return { unsignedXdr };
}

@Post('burn')
async burn(@Body() dto: BurnDto) {
const unsignedXdr = await this.tokenFactoryService.burn(dto);
return { unsignedXdr };
}

@Post('burn-from')
async burnFrom(@Body() dto: BurnFromDto) {
const unsignedXdr = await this.tokenFactoryService.burnFrom(dto);
return { unsignedXdr };
}

// ── GET endpoints (reads) ──

@Get('balance')
async getBalance(
@Query('contractId') contractId: string,
@Query('address') address: string,
@Query('callerPublicKey') callerPublicKey: string,
) {
const balance = await this.tokenFactoryService.getBalance(contractId, address, callerPublicKey);
return { balance: String(balance) };
}

@Get('allowance')
async getAllowance(
@Query('contractId') contractId: string,
@Query('from') from: string,
@Query('spender') spender: string,
@Query('callerPublicKey') callerPublicKey: string,
) {
const allowance = await this.tokenFactoryService.getAllowance(
contractId,
from,
spender,
callerPublicKey,
);
return { allowance: String(allowance) };
}

@Get('decimals')
async getDecimals(
@Query('contractId') contractId: string,
@Query('callerPublicKey') callerPublicKey: string,
) {
const decimals = await this.tokenFactoryService.getDecimals(contractId, callerPublicKey);
return { decimals };
}

@Get('name')
async getName(
@Query('contractId') contractId: string,
@Query('callerPublicKey') callerPublicKey: string,
) {
const name = await this.tokenFactoryService.getName(contractId, callerPublicKey);
return { name: String(name) };
}

@Get('symbol')
async getSymbol(
@Query('contractId') contractId: string,
@Query('callerPublicKey') callerPublicKey: string,
) {
const symbol = await this.tokenFactoryService.getSymbol(contractId, callerPublicKey);
return { symbol: String(symbol) };
}

@Get('escrow-id')
async getEscrowId(
@Query('contractId') contractId: string,
@Query('callerPublicKey') callerPublicKey: string,
) {
const escrowId = await this.tokenFactoryService.getEscrowId(contractId, callerPublicKey);
return { escrowId: String(escrowId) };
Comment on lines +61 to +120
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate the GET query params before delegating to the service.

These handlers read raw query strings, so the global ValidationPipe never enforces required/non-empty values here. Missing contractId, address, spender, or callerPublicKey will fall through into TokenFactoryService and surface as downstream 500s instead of a clean 400. Please switch these read endpoints to query DTOs or explicit parameter pipes.

As per coding guidelines "Keep business logic in Services only; Controllers should handle HTTP concerns (params, delegation, response)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/core/src/token-factory/token-factory.controller.ts` around lines 61 -
120, These controller GET handlers (getBalance, getAllowance, getDecimals,
getName, getSymbol, getEscrowId) are reading raw query strings so
class-validator + global ValidationPipe isn't applied; create small query DTOs
(e.g., GetBalanceQueryDto, GetAllowanceQueryDto, GetSimpleQueryDto) with
required fields annotated (IsString, IsNotEmpty, IsOptional where applicable)
and replace raw `@Query`(...) params with a single `@Query`() dto parameter for each
handler; use the DTO properties (dto.contractId, dto.address, dto.spender,
dto.callerPublicKey) when calling TokenFactoryService so missing/invalid queries
return 400s instead of downstream 500s.

}
}
9 changes: 9 additions & 0 deletions apps/core/src/token-factory/token-factory.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TokenFactoryController } from './token-factory.controller';
import { TokenFactoryService } from './token-factory.service';

@Module({
controllers: [TokenFactoryController],
providers: [TokenFactoryService],
})
export class TokenFactoryModule {}
Loading
Loading