Skip to content
Closed
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
14 changes: 13 additions & 1 deletion app/api/remittance/build/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { NextRequest } from 'next/server';
import { requireAuth } from '@/lib/session';
import { jsonSuccess, jsonError } from '@/lib/api/types';
import { validateMessage } from '@/utils/validation';
import {
TransactionBuilder,
Operation,
Expand All @@ -40,6 +41,7 @@ interface BuildRemittanceRequest {
currency?: string;
recipientAddress: string;
memo?: string;
message?: string;
}

interface BuildRemittanceResponse {
Expand All @@ -53,7 +55,7 @@ interface BuildRemittanceResponse {

// ── Validation ────────────────────────────────────────────────────────────────

function validateBuildRequest(body: unknown): BuildRemittanceRequest {
export function validateBuildRequest(body: unknown): BuildRemittanceRequest {
if (!body || typeof body !== 'object') {
throw new Error('Request body must be a JSON object');
}
Expand Down Expand Up @@ -90,11 +92,21 @@ function validateBuildRequest(body: unknown): BuildRemittanceRequest {
}
}

// Validate message (optional) — cap at 2000 characters
const message = typeof o.message === 'string' ? o.message.trim() : undefined;
if (message) {
const msgError = validateMessage(message);
if (msgError) {
throw new Error(msgError.message);
}
}

return {
amount: o.amount,
currency,
recipientAddress,
memo,
message,
};
}

Expand Down
14 changes: 13 additions & 1 deletion app/api/v1/remittance/emergency/build/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StellarTransactionBuilder } from '../../../../../../services/transactio
import { getSession } from '../../../../../../lib/session';
import { PolicyService } from '../../../../../../services/policy-service';
import { EventStorageService } from '../../../../../../services/event-storage-service';
import { validateMessage } from '../../../../../../utils/validation';

export const dynamic = 'force-dynamic';

Expand All @@ -17,7 +18,7 @@ export async function POST(req: NextRequest) {
}

const body = await req.json();
const { destinationAccount, amount, assetCode, assetIssuer, memo } = body;
const { destinationAccount, amount, assetCode, assetIssuer, memo, message } = body;

if (!destinationAccount || !amount) {
return NextResponse.json(
Expand All @@ -42,6 +43,17 @@ export async function POST(req: NextRequest) {
);
}

// Validate message (optional) — cap at 2000 characters
if (message) {
const msgError = validateMessage(message);
if (msgError) {
return NextResponse.json(
{ error: 'Bad Request', message: msgError.message },
{ status: 400 }
);
}
}

const builder = new StellarTransactionBuilder();
const xdr = await builder.buildEmergencyTransfer({
sourceAccount: session.address,
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/remittance/build-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import { describe, it, expect, vi } from 'vitest';
import { validateMessage } from '@/utils/validation';

vi.mock('next/server', () => ({
NextRequest: class NextRequest {
Expand Down Expand Up @@ -118,3 +119,42 @@ describe('validateBuildRequest – memo byte-length validation', () => {
expect(result.memo).toBe(memo);
});
});

describe('validateMessage', () => {
it('returns null for undefined message', () => {
expect(validateMessage(undefined)).toBeNull();
});

it('returns null for empty string message', () => {
expect(validateMessage('')).toBeNull();
});

it('accepts message within 2000 characters', () => {
const msg = 'A'.repeat(2000);
expect(validateMessage(msg)).toBeNull();
});

it('rejects message exceeding 2000 characters', () => {
const msg = 'A'.repeat(2001);
const error = validateMessage(msg);
expect(error).not.toBeNull();
expect(error!.code).toBe('INVALID_MESSAGE');
expect(error!.message).toBe('Message must not exceed 2000 characters');
expect(error!.field).toBe('message');
});

it('accepts message exactly 2000 characters', () => {
const msg = 'B'.repeat(2000);
expect(validateMessage(msg)).toBeNull();
});

it('handles multi-byte characters within 2000 chars', () => {
const msg = 'é'.repeat(1000);
expect(validateMessage(msg)).toBeNull();
});

it('handles emoji within 2000 chars', () => {
const msg = '😀'.repeat(500);
expect(validateMessage(msg)).toBeNull();
});
});
2 changes: 2 additions & 0 deletions types/emergency-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface EmergencyTransferRequest {
amount: string; // Amount in base units (e.g., stroops for XLM)
recipientAddress: string; // Stellar public key (G...)
memo?: string; // Optional custom memo (max 28 bytes)
message?: string; // Optional user-facing message (max 2000 chars)
assetCode?: string; // Optional asset code (default: XLM)
assetIssuer?: string; // Required if assetCode is not XLM
}
Expand Down Expand Up @@ -95,6 +96,7 @@ export enum EmergencyTransferErrorCode {
INVALID_RECIPIENT = 'INVALID_RECIPIENT',
INVALID_ASSET = 'INVALID_ASSET',
INVALID_MEMO = 'INVALID_MEMO',
INVALID_MESSAGE = 'INVALID_MESSAGE',

// Limit errors
AMOUNT_EXCEEDS_LIMIT = 'AMOUNT_EXCEEDS_LIMIT',
Expand Down
24 changes: 24 additions & 0 deletions utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,26 @@ export function validateAssetIssuer(
return validateRecipientAddress(assetIssuer);
}

/**
* Validates a user-facing message is within 2000 characters
*/
export function validateMessage(message?: string): ValidationError | null {
// Message is optional
if (!message) {
return null;
}

if (message.length > 2000) {
return {
code: EmergencyTransferErrorCode.INVALID_MESSAGE,
message: 'Message must not exceed 2000 characters',
field: 'message',
};
}

return null;
}

/**
* Validates entire emergency transfer request
*/
Expand Down Expand Up @@ -219,5 +239,9 @@ export function validateEmergencyTransferRequest(request: {
const memoError = validateMemo(request.memo);
if (memoError) errors.push(memoError);

// Validate message
const messageError = validateMessage(request.message);
if (messageError) errors.push(messageError);

return errors;
}
Loading