diff --git a/apps/core/src/app.module.ts b/apps/core/src/app.module.ts index b094236..f4d57c9 100644 --- a/apps/core/src/app.module.ts +++ b/apps/core/src/app.module.ts @@ -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: [ @@ -20,6 +21,7 @@ import { VaultModule } from './vault/vault.module'; LoansModule, ParticipationTokenModule, VaultModule, + TokenFactoryModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/core/src/token-factory/dto/approve.dto.ts b/apps/core/src/token-factory/dto/approve.dto.ts new file mode 100644 index 0000000..471faae --- /dev/null +++ b/apps/core/src/token-factory/dto/approve.dto.ts @@ -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; +} diff --git a/apps/core/src/token-factory/dto/burn-from.dto.ts b/apps/core/src/token-factory/dto/burn-from.dto.ts new file mode 100644 index 0000000..0b7f80b --- /dev/null +++ b/apps/core/src/token-factory/dto/burn-from.dto.ts @@ -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; +} diff --git a/apps/core/src/token-factory/dto/burn.dto.ts b/apps/core/src/token-factory/dto/burn.dto.ts new file mode 100644 index 0000000..b4ad815 --- /dev/null +++ b/apps/core/src/token-factory/dto/burn.dto.ts @@ -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; +} diff --git a/apps/core/src/token-factory/dto/mint.dto.ts b/apps/core/src/token-factory/dto/mint.dto.ts new file mode 100644 index 0000000..a7d0a4c --- /dev/null +++ b/apps/core/src/token-factory/dto/mint.dto.ts @@ -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; +} diff --git a/apps/core/src/token-factory/dto/set-admin.dto.ts b/apps/core/src/token-factory/dto/set-admin.dto.ts new file mode 100644 index 0000000..2a74695 --- /dev/null +++ b/apps/core/src/token-factory/dto/set-admin.dto.ts @@ -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; +} diff --git a/apps/core/src/token-factory/dto/transfer-from.dto.ts b/apps/core/src/token-factory/dto/transfer-from.dto.ts new file mode 100644 index 0000000..4c50ee2 --- /dev/null +++ b/apps/core/src/token-factory/dto/transfer-from.dto.ts @@ -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; + + @IsString() + @IsNotEmpty() + callerPublicKey: string; +} diff --git a/apps/core/src/token-factory/dto/transfer.dto.ts b/apps/core/src/token-factory/dto/transfer.dto.ts new file mode 100644 index 0000000..1eea777 --- /dev/null +++ b/apps/core/src/token-factory/dto/transfer.dto.ts @@ -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; +} diff --git a/apps/core/src/token-factory/token-factory.controller.ts b/apps/core/src/token-factory/token-factory.controller.ts new file mode 100644 index 0000000..c0f714d --- /dev/null +++ b/apps/core/src/token-factory/token-factory.controller.ts @@ -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) }; + } +} diff --git a/apps/core/src/token-factory/token-factory.module.ts b/apps/core/src/token-factory/token-factory.module.ts new file mode 100644 index 0000000..b5912c6 --- /dev/null +++ b/apps/core/src/token-factory/token-factory.module.ts @@ -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 {} diff --git a/apps/core/src/token-factory/token-factory.service.ts b/apps/core/src/token-factory/token-factory.service.ts new file mode 100644 index 0000000..33c8e7b --- /dev/null +++ b/apps/core/src/token-factory/token-factory.service.ts @@ -0,0 +1,129 @@ +import { Injectable } from '@nestjs/common'; +import { SorobanService } from '../soroban/soroban.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'; + +@Injectable() +export class TokenFactoryService { + constructor(private readonly soroban: SorobanService) {} + + // ── Writes ── + + mint(dto: MintDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'mint', + { to: dto.to, amount: dto.amount }, + dto.callerPublicKey, + ); + } + + setAdmin(dto: SetAdminDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'set_admin', + { new_admin: dto.newAdmin }, + dto.callerPublicKey, + ); + } + + approve(dto: ApproveDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'approve', + { + from: dto.from, + spender: dto.spender, + amount: dto.amount, + expiration_ledger: dto.expirationLedger, + }, + dto.callerPublicKey, + ); + } + + transfer(dto: TransferDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'transfer', + { from: dto.from, to_muxed: dto.to, amount: dto.amount }, + dto.callerPublicKey, + ); + } + + transferFrom(dto: TransferFromDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'transfer_from', + { + spender: dto.spender, + from: dto.from, + to: dto.to, + amount: dto.amount, + }, + dto.callerPublicKey, + ); + } + + burn(dto: BurnDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'burn', + { from: dto.from, amount: dto.amount }, + dto.callerPublicKey, + ); + } + + burnFrom(dto: BurnFromDto): Promise { + return this.soroban.buildContractCallTransaction( + dto.contractId, + 'burn_from', + { + spender: dto.spender, + from: dto.from, + amount: dto.amount, + }, + dto.callerPublicKey, + ); + } + + // ── Reads ── + + getBalance(contractId: string, address: string, callerPublicKey: string): Promise { + return this.soroban.readContractState(contractId, 'balance', { id: address }, callerPublicKey); + } + + getAllowance( + contractId: string, + from: string, + spender: string, + callerPublicKey: string, + ): Promise { + return this.soroban.readContractState( + contractId, + 'allowance', + { from, spender }, + callerPublicKey, + ); + } + + getDecimals(contractId: string, callerPublicKey: string): Promise { + return this.soroban.readContractState(contractId, 'decimals', {}, callerPublicKey); + } + + getName(contractId: string, callerPublicKey: string): Promise { + return this.soroban.readContractState(contractId, 'name', {}, callerPublicKey); + } + + getSymbol(contractId: string, callerPublicKey: string): Promise { + return this.soroban.readContractState(contractId, 'symbol', {}, callerPublicKey); + } + + getEscrowId(contractId: string, callerPublicKey: string): Promise { + return this.soroban.readContractState(contractId, 'escrow_id', {}, callerPublicKey); + } +}