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
977 changes: 637 additions & 340 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"postinstall": "prisma generate",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
Expand Down Expand Up @@ -35,11 +36,11 @@
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.4.22",
"@nestjs/platform-express": "^10.4.22",
"@nestjs/platform-socket.io": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.22",
"@nestjs/swagger": "^7.1.1",
"@nestjs/throttler": "^6.5.0",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.0.0",
"@nestjs/websockets": "^10.4.22",
"@prisma/client": "^5.19.1",
"@stellar/stellar-sdk": "^11.0.0",
"@types/jsonwebtoken": "^9.0.10",
Expand Down
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This is your Prisma schema file,
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ConfigModule } from '@nestjs/config';
import { MaintenanceMiddleware } from './common/middleware/maintenance.middleware';
import { RequestIdMiddleware } from './common/middleware/request-id.middleware';
import { RedisModule } from './common/redis/redis.module';
import { TransactionsModule } from './transactions/transactions.module';

@Module({
imports: [
Expand All @@ -32,6 +33,7 @@ import { RedisModule } from './common/redis/redis.module';
TokensModule,
OgModule,
TradeModule,
TransactionsModule,
OrdersModule,
GasModule,
],
Expand Down
4 changes: 2 additions & 2 deletions src/common/logger/custom.logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
import { ConsoleLogger, Injectable, LogLevel, Scope } from '@nestjs/common';
import { requestContextStorage } from '../storage/request-context.storage';

/**
Expand All @@ -19,7 +19,7 @@ export class CustomLogger extends ConsoleLogger {
* @returns The fully formatted log string.
*/
formatMessage(
logLevel: string,
logLevel: LogLevel,
message: unknown,
pidMessage: string,
formattedLogLevel: string,
Expand Down
4 changes: 2 additions & 2 deletions src/dynamic/dynamic.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { InvoicesController } from './invoices.controller';
import { PdfService } from './pdf.service';
import { InvoicesController } from '../invoices/invoices.controller';
import { PdfService } from '../invoices/pdf.service';

@Module({
controllers: [InvoicesController],
Expand Down
8 changes: 4 additions & 4 deletions src/gas/gas.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { RedisService } from '../common/redis/redis.service';
import { Server } from '@stellar/stellar-sdk/rpc';
import { SorobanRpc } from '@stellar/stellar-sdk';

export interface FeeTiers {
low: string;
Expand Down Expand Up @@ -33,9 +33,9 @@ export class GasService {
private async fetchAndCache() {
try {
const rpcUrl = process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org';
const server = new Server(rpcUrl);
const ledger = await server.getLatestLedger();
const baseFee = parseInt(ledger.baseFeeInStroops || '100', 10);
const server = new SorobanRpc.Server(rpcUrl);
await server.getLatestLedger();
const baseFee = parseInt(process.env.STELLAR_BASE_FEE_STROOPS || '100', 10);

const tiers: FeeTiers = {
low: Math.ceil(baseFee * 1.0).toString(),
Expand Down
25 changes: 20 additions & 5 deletions src/pools/pools.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApyHistoryPoint, generateMockApyHistory } from './apy-history.helper';
import { PoolIdParamDto } from './dto/pool-id-param.dto';

interface ApyHistoryResponse {
status: 'success';
data: ApyHistoryPoint[];
}

/**
* Controller for liquidity pool information and history.
* Provides simulated APY history and recent trade data for pools.
Expand All @@ -23,11 +28,22 @@ export class PoolsController {
@ApiResponse({
status: 200,
description: 'APY history retrieved successfully',
type: ApyHistoryPoint,
isArray: true,
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'success' },
data: {
type: 'array',
items: { $ref: '#/components/schemas/ApyHistoryPoint' },
},
},
},
})
getApyHistory(@Param() params: PoolIdParamDto): ApyHistoryPoint[] {
return generateMockApyHistory(params.poolId);
getApyHistory(@Param() params: PoolIdParamDto): ApyHistoryResponse {
return {
status: 'success',
data: generateMockApyHistory(params.poolId),
};
}

/**
Expand Down Expand Up @@ -97,4 +113,3 @@ export class PoolsController {
};
}
}

4 changes: 2 additions & 2 deletions src/trade/trade.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { WebSocketGateway, WebSocketServer, OnModuleInit } from '@nestjs/websockets';
import { OnModuleInit, Logger } from '@nestjs/common';
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';
import { RedisService } from '../common/redis/redis.service';
import { Logger } from '@nestjs/common';

/**
* WebSocket Gateway for real-time trade updates.
Expand Down
68 changes: 68 additions & 0 deletions src/transactions/transactions.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Keypair, Networks } from '@stellar/stellar-sdk';
import * as request from 'supertest';
import { TransactionsModule } from './transactions.module';
import { TransactionsService } from './transactions.service';

describe('TransactionsController', () => {
let app: INestApplication;
const source = Keypair.random().publicKey();
const destination = Keypair.random().publicKey();

const transactionsService = {
buildTransaction: jest.fn(),
};

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TransactionsModule],
})
.overrideProvider(TransactionsService)
.useValue(transactionsService)
.compile();

app = moduleFixture.createNestApplication();
await app.init();
});

beforeEach(() => {
transactionsService.buildTransaction.mockResolvedValue({
xdr: 'AAAA',
source,
operationCount: 1,
networkPassphrase: Networks.TESTNET,
});
});

afterAll(async () => {
await app.close();
});

it('POST /api/v1/transactions/build delegates to the transaction builder service', async () => {
const payload = {
source,
operations: [
{
type: 'payment',
destination,
asset: { type: 'native' },
amount: '1.0000000',
},
],
};

const response = await request(app.getHttpServer())
.post('/api/v1/transactions/build')
.send(payload)
.expect(200);

expect(transactionsService.buildTransaction).toHaveBeenCalledWith(payload);
expect(response.body).toEqual({
xdr: 'AAAA',
source,
operationCount: 1,
networkPassphrase: Networks.TESTNET,
});
});
});
35 changes: 35 additions & 0 deletions src/transactions/transactions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { BuildTransactionDto, TransactionsService } from './transactions.service';

@ApiTags('transactions')
@Controller('api/v1/transactions')
export class TransactionsController {
constructor(private readonly transactionsService: TransactionsService) {}

@Post('build')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Build an unsigned Stellar transaction envelope' })
@ApiBody({
schema: {
type: 'object',
required: ['source', 'operations'],
properties: {
source: { type: 'string', description: 'Source Stellar public key' },
fee: { type: 'string', description: 'Optional per-operation fee in stroops' },
networkPassphrase: { type: 'string', description: 'Optional Stellar network passphrase' },
timeoutSeconds: { type: 'number', description: 'Optional transaction timeout in seconds' },
operations: {
type: 'array',
description: 'Sequential payment or pathPaymentStrictSend intents',
items: { type: 'object' },
},
},
},
})
@ApiResponse({ status: 200, description: 'Unsigned base64 XDR envelope built successfully' })
@ApiResponse({ status: 400, description: 'Invalid transaction build request' })
async buildTransaction(@Body() body: BuildTransactionDto) {
return this.transactionsService.buildTransaction(body);
}
}
9 changes: 9 additions & 0 deletions src/transactions/transactions.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TransactionsController } from './transactions.controller';
import { TransactionsService } from './transactions.service';

@Module({
controllers: [TransactionsController],
providers: [TransactionsService],
})
export class TransactionsModule {}
66 changes: 66 additions & 0 deletions src/transactions/transactions.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Account, Keypair, Networks, TransactionBuilder } from '@stellar/stellar-sdk';
import { BadRequestException } from '@nestjs/common';
import { TransactionsService } from './transactions.service';

describe('TransactionsService', () => {
const source = Keypair.random().publicKey();
const firstDestination = Keypair.random().publicKey();
const secondDestination = Keypair.random().publicKey();
const issuer = Keypair.random().publicKey();

const accountLoader = {
loadAccount: jest.fn(),
};

beforeEach(() => {
accountLoader.loadAccount.mockResolvedValue(new Account(source, '123'));
});

it('builds an unsigned XDR envelope from sequential swap intents', async () => {
const service = new TransactionsService(accountLoader);

const result = await service.buildTransaction({
source,
fee: '200',
networkPassphrase: Networks.TESTNET,
operations: [
{
type: 'pathPaymentStrictSend',
sendAsset: { type: 'native' },
sendAmount: '10.0000000',
destination: firstDestination,
destAsset: { type: 'credit_alphanum4', code: 'USDC', issuer },
destMin: '9.5000000',
path: [{ type: 'credit_alphanum4', code: 'EURC', issuer }],
},
{
type: 'payment',
destination: secondDestination,
asset: { type: 'native' },
amount: '1.0000000',
},
],
});

expect(accountLoader.loadAccount).toHaveBeenCalledWith(source);
expect(result.source).toBe(source);
expect(result.operationCount).toBe(2);
expect(typeof result.xdr).toBe('string');

const transaction = TransactionBuilder.fromXDR(result.xdr, Networks.TESTNET) as any;
expect(transaction.operations).toHaveLength(2);
expect(transaction.operations[0].type).toBe('pathPaymentStrictSend');
expect(transaction.operations[1].type).toBe('payment');
});

it('rejects empty operation arrays', async () => {
const service = new TransactionsService(accountLoader);

await expect(
service.buildTransaction({
source,
operations: [],
}),
).rejects.toBeInstanceOf(BadRequestException);
});
});
Loading