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
68 changes: 57 additions & 11 deletions backend/src/api/routes/sep24.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ jest.mock('crypto', () => {
};
});

jest.mock('../../lib/prisma', () => ({
__esModule: true,
default: {
quote: {
findUnique: jest.fn(),
},
},
}));

import sep24Router from './sep24.route';

jest.setTimeout(15000);
Expand All @@ -19,6 +28,7 @@ describe('SEP-24 Routes', () => {
app.use('/', sep24Router);

const baseUrl = 'http://localhost:4100';
const validAccount = 'GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN';

beforeEach(() => {
process.env.INTERACTIVE_URL = baseUrl;
Expand All @@ -32,7 +42,7 @@ describe('SEP-24 Routes', () => {
it('returns 400 when asset_code is missing', async () => {
const res = await request(app)
.post('/transactions/deposit/interactive')
.send({ account: 'GACCOUNT' });
.send({ account: validAccount });

expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('asset_code is required');
Expand All @@ -45,15 +55,15 @@ describe('SEP-24 Routes', () => {

expect(res.statusCode).toBe(400);
expect(res.body.error).toContain('Asset DOGE is not supported');
expect(res.body.error).toContain('Supported assets: USDC, USD, BTC, ETH');
expect(res.body.error).toContain('Supported assets: USDC, USD');
});

it('returns an interactive URL for supported assets (with optional params)', async () => {
const res = await request(app)
.post('/transactions/deposit/interactive')
.send({
asset_code: 'usdc',
account: 'GACCOUNT',
account: validAccount,
amount: '12.50',
lang: 'fr'
});
Expand All @@ -66,11 +76,30 @@ describe('SEP-24 Routes', () => {
expect(parsed.pathname).toBe('/kyc-deposit');
expect(parsed.searchParams.get('transaction_id')).toBe(res.body.id);
expect(parsed.searchParams.get('asset_code')).toBe('USDC');
expect(parsed.searchParams.get('account')).toBe('GACCOUNT');
expect(parsed.searchParams.get('account')).toBe(validAccount);
expect(parsed.searchParams.get('amount')).toBe('12.50');
expect(parsed.searchParams.get('lang')).toBe('fr');
});

it.each([
['plain invalid string', 'INVALID_ADDRESS'],
['secret seed-like value', `S${validAccount.slice(1)}`],
['padded public key', `${validAccount} `],
['checksum mismatch', `${validAccount.slice(0, -1)}A`],
['contract address-like value', `C${validAccount.slice(1)}`],
])('returns 400 when account is %s', async (_caseName, account) => {
const res = await request(app)
.post('/transactions/deposit/interactive')
.send({
asset_code: 'USDC',
account,
});

expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('account must be a valid Stellar public key');
expect(res.body.error).not.toContain(account);
});

it('defaults lang to en when omitted', async () => {
const res = await request(app)
.post('/transactions/deposit/interactive')
Expand All @@ -87,7 +116,7 @@ describe('SEP-24 Routes', () => {
it('returns 400 when asset_code is missing', async () => {
const res = await request(app)
.post('/transactions/withdraw/interactive')
.send({ account: 'GACCOUNT' });
.send({ account: validAccount });

expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('asset_code is required');
Expand All @@ -100,25 +129,42 @@ describe('SEP-24 Routes', () => {

expect(res.statusCode).toBe(400);
expect(res.body.error).toContain('Asset DOGE is not supported');
expect(res.body.error).toContain('Supported assets: USDC, USD, BTC, ETH');
expect(res.body.error).toContain('Supported assets: USDC, USD');
});

it('returns an interactive URL for supported assets', async () => {
const res = await request(app)
.post('/transactions/withdraw/interactive')
.send({
asset_code: 'BTC',
account: 'GACCOUNT',
asset_code: 'USD',
account: validAccount,
amount: '1'
});

expect(res.statusCode).toBe(200);
const parsed = new URL(res.body.url);
expect(parsed.pathname).toBe('/kyc-withdraw');
expect(parsed.searchParams.get('asset_code')).toBe('BTC');
expect(parsed.searchParams.get('account')).toBe('GACCOUNT');
expect(parsed.searchParams.get('asset_code')).toBe('USD');
expect(parsed.searchParams.get('account')).toBe(validAccount);
expect(parsed.searchParams.get('amount')).toBe('1');
});

it.each([
['plain invalid string', 'INVALID_ADDRESS'],
['secret seed-like value', `S${validAccount.slice(1)}`],
['padded public key', ` ${validAccount}`],
['muxed account-like value', `M${validAccount.slice(1)}`],
])('returns 400 when account is %s', async (_caseName, account) => {
const res = await request(app)
.post('/transactions/withdraw/interactive')
.send({
asset_code: 'USD',
account,
});

expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('account must be a valid Stellar public key');
expect(res.body.error).not.toContain(account);
});
});
});

20 changes: 18 additions & 2 deletions backend/src/api/routes/sep24.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SUPPORTED_ASSETS,
} from '../../services/kyc.service';
import prisma from '../../lib/prisma';
import { isValidStellarPublicKey } from '../../utils/stellar-address';

const router = Router();

Expand All @@ -29,8 +30,15 @@ const unsupportedAssetResponse = (assetCode: string) => ({
error: `Asset ${assetCode} is not supported. Supported assets: ${SUPPORTED_ASSETS.join(', ')}`,
});

const invalidAccountResponse = () => ({
error: 'account must be a valid Stellar public key',
});

const getBaseInteractiveUrl = (): string => process.env.INTERACTIVE_URL || 'http://localhost:3000';

const hasInvalidAccount = (account: unknown): boolean =>
account !== undefined && !isValidStellarPublicKey(account);

/**
* @swagger
* /sep24/transactions/deposit/interactive:
Expand All @@ -53,7 +61,7 @@ const getBaseInteractiveUrl = (): string => process.env.INTERACTIVE_URL || 'http
* example: USDC
* account:
* type: string
* description: Stellar account address
* description: Stellar Ed25519 public key (G...)
* amount:
* type: string
* description: Amount to deposit
Expand Down Expand Up @@ -95,6 +103,10 @@ router.post('/transactions/deposit/interactive', async (req: Request, res: Respo
return res.status(400).json(unsupportedAssetResponse(asset_code));
}

if (hasInvalidAccount(account)) {
return res.status(400).json(invalidAccountResponse());
}

if (quote_id) {
const quote = await prisma.quote.findUnique({ where: { id: quote_id } });
if (!quote) {
Expand Down Expand Up @@ -144,7 +156,7 @@ router.post('/transactions/deposit/interactive', async (req: Request, res: Respo
* example: USDC
* account:
* type: string
* description: Destination Stellar account address
* description: Destination Stellar Ed25519 public key (G...)
* amount:
* type: string
* description: Amount to withdraw
Expand Down Expand Up @@ -186,6 +198,10 @@ router.post('/transactions/withdraw/interactive', async (req: Request, res: Resp
return res.status(400).json(unsupportedAssetResponse(asset_code));
}

if (hasInvalidAccount(account)) {
return res.status(400).json(invalidAccountResponse());
}

if (quote_id) {
const quote = await prisma.quote.findUnique({ where: { id: quote_id } });
if (!quote) {
Expand Down
Loading