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: 1 addition & 1 deletion src/shared/models/ip-log/ip-log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class IpLogService {
return this.ipLogRepo.save(ipLog);
}

async getByUserDataId(userDataId: number, limit = 50): Promise<IpLog[]> {
async getByUserDataId(userDataId: number, limit = 100): Promise<IpLog[]> {
return this.ipLogRepo.find({
where: { userData: { id: userDataId } },
order: { created: 'DESC' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class SwapService {
type = 'signed transaction';
payIn = await this.transactionUtilService.handleSignedTxInput(route, request, dto.signedTxHex);
} else if (dto.txHash) {
type = 'EIP-5792 sponsored transfer';
type = 'txHash';
payIn = await this.transactionUtilService.handleTxHashInput(route, request, dto.txHash);
} else {
throw new BadRequestException('Either permit, signedTxHex, txHash, or authorization must be provided');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { isFiatDto } from 'src/shared/models/active';
import { AssetDtoMapper } from 'src/shared/models/asset/dto/asset-dto.mapper';
import { FiatService } from 'src/shared/models/fiat/fiat.service';
import { DfxCron } from 'src/shared/utils/cron';
import { Util } from 'src/shared/utils/util';
import { AmountType, Util } from 'src/shared/utils/util';
import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service';
import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity';
import { UserDataStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum';
Expand Down Expand Up @@ -573,7 +573,10 @@ export class TransactionController {
return this.bankTxReturnService.refundBankTx(targetEntity, {
refundIban: dto.refundTarget ?? refundData.refundTarget,
creditorData: dto.creditorData,
chargebackReferenceAmount: refundData.refundPrice.invert().convert(refundData.refundAmount),
chargebackReferenceAmount: Util.roundReadable(
refundData.refundPrice.invert().convert(refundData.refundAmount),
AmountType.FIAT,
),
...refundDto,
});
}
Expand Down Expand Up @@ -601,7 +604,10 @@ export class TransactionController {
return this.buyCryptoService.refundBankTx(targetEntity, {
refundIban: refundData.refundTarget ?? dto.refundTarget,
creditorData: dto.creditorData,
chargebackReferenceAmount: refundData.refundPrice.invert().convert(refundData.refundAmount),
chargebackReferenceAmount: Util.roundReadable(
refundData.refundPrice.invert().convert(refundData.refundAmount),
AmountType.FIAT,
),
...refundDto,
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/subdomains/core/sell-crypto/route/dto/confirm.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class ConfirmDto {
@IsString()
signedTxHex?: string;

@ApiPropertyOptional({ description: 'Transaction hash from wallet_sendCalls (EIP-5792 gasless transfer)' })
@ApiPropertyOptional({ description: 'Transaction hash of a user-sent transaction (plain transfer or EIP-5792)' })
@IsOptional()
@IsString()
txHash?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/subdomains/core/sell-crypto/route/sell.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export class SellService {
type = 'signed transaction';
payIn = await this.transactionUtilService.handleSignedTxInput(route, request, dto.signedTxHex);
} else if (dto.txHash) {
type = 'EIP-5792 sponsored transfer';
type = 'txHash';
payIn = await this.transactionUtilService.handleTxHashInput(route, request, dto.txHash);
} else {
throw new BadRequestException('Either permit, signedTxHex, txHash, or authorization must be provided');
Expand Down
8 changes: 4 additions & 4 deletions src/subdomains/core/transaction/transaction-util.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ export class TransactionUtilService {
}

/**
* Handle transaction hash from EIP-5792 wallet_sendCalls (gasless/sponsored transfer)
* The frontend sends the transaction via wallet_sendCalls and provides the txHash
* Handle transaction hash provided by the client
* The client sends the transaction themselves and provides the txHash for tracking
*/
async handleTxHashInput(route: Swap | Sell, request: TransactionRequest, txHash: string): Promise<CryptoInput> {
const asset = await this.assetService.getAssetById(request.sourceId);
Expand All @@ -225,14 +225,14 @@ export class TransactionUtilService {
const client = this.blockchainRegistry.getEvmClient(asset.blockchain);
const blockHeight = await client.getCurrentBlock();

// The transaction was already sent by the frontend via wallet_sendCalls
// The transaction was already sent by the client
// We just need to create a PayIn record to track it
return this.payInService.createPayIn(
request.user.address,
route.deposit.address,
asset,
txHash,
PayInType.SPONSORED_TRANSFER,
PayInType.CONFIRMED_DEPOSIT,
blockHeight,
request.amount,
);
Expand Down
4 changes: 4 additions & 0 deletions src/subdomains/generic/support/dto/user-data-support.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class BankTxSupportInfo {
export class UserSupportInfo {
id: number;
address: string;
ref?: string;
role: string;
status: string;
created: Date;
Expand All @@ -44,7 +45,10 @@ export class TransactionSupportInfo {
uid: string;
type?: string;
sourceType: string;
inputAmount?: number;
inputAsset?: string;
amountInChf?: number;
amountInEur?: number;
amlCheck?: string;
chargebackDate?: Date;
amlReason?: string;
Expand Down
9 changes: 9 additions & 0 deletions src/subdomains/generic/support/support.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ export class SupportController {
return this.supportService.getRecommendationGraph(+id);
}

@Get(':id/ip-log-pdf')
@ApiBearerAuth()
@ApiExcludeEndpoint()
@UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard())
async getIpLogPdf(@Param('id') id: string): Promise<{ pdfData: string }> {
const pdfData = await this.supportService.generateIpLogPdf(+id);
return { pdfData };
}

@Get(':id')
@ApiBearerAuth()
@ApiExcludeEndpoint()
Expand Down
116 changes: 116 additions & 0 deletions src/subdomains/generic/support/support.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { BadRequestException, Inject, Injectable, NotFoundException, forwardRef } from '@nestjs/common';
import PDFDocument from 'pdfkit';
import { PdfUtil } from 'src/shared/utils/pdf.util';
import { isIP } from 'class-validator';
import * as IbanTools from 'ibantools';
import { Config } from 'src/config/config';
Expand Down Expand Up @@ -109,6 +111,116 @@ export class SupportService {
private readonly supportIssueService: SupportIssueService,
) {}

async generateIpLogPdf(userDataId: number): Promise<string> {
const ipLogs = await this.ipLogService.getByUserDataId(userDataId);

return new Promise<string>((resolve, reject) => {
try {
const pdf = new PDFDocument({ size: 'A4', margin: 50 });
const chunks: Buffer[] = [];

pdf.on('data', (chunk) => chunks.push(chunk));
pdf.on('end', () => resolve(Buffer.concat(chunks).toString('base64')));

PdfUtil.drawLogo(pdf);

// Header
const marginX = 50;
pdf.moveDown(2);
pdf.fontSize(18).font('Helvetica-Bold').fillColor('#072440');
pdf.text('IP Log Report', marginX);
pdf.moveDown(0.5);
pdf.fontSize(10).font('Helvetica').fillColor('#333333');
pdf.text(`User Data ID: ${userDataId}`, marginX);
pdf.text(`Date: ${new Date().toISOString().split('T')[0]}`, marginX);
pdf.text(`Total Entries: ${ipLogs.length}`, marginX);
pdf.moveDown(1);

// Table
this.drawIpLogTable(pdf, ipLogs);

// Footer
pdf.moveDown(2);
pdf.fontSize(8).font('Helvetica').fillColor('#999999');
pdf.text(`Generated by DFX - ${new Date().toISOString()}`, marginX);

pdf.end();
} catch (e) {
reject(e);
}
});
}

private drawIpLogTable(pdf: InstanceType<typeof PDFDocument>, ipLogs: IpLog[]): void {
const marginX = 50;
const { width } = pdf.page;
const tableWidth = width - marginX * 2;

const cols = [
{ header: 'Date', width: tableWidth * 0.2 },
{ header: 'IP', width: tableWidth * 0.2 },
{ header: 'Country', width: tableWidth * 0.12 },
{ header: 'Endpoint', width: tableWidth * 0.36 },
{ header: 'Status', width: tableWidth * 0.12 },
];

let y = pdf.y;

// Headers
pdf.fontSize(10).font('Helvetica-Bold').fillColor('#072440');
let x = marginX;
for (const col of cols) {
pdf.text(col.header, x, y, { width: col.width - 5 });
x += col.width;
}

y += 18;
pdf
.moveTo(marginX, y)
.lineTo(width - marginX, y)
.stroke('#CCCCCC');
y += 8;

// Rows
pdf.fontSize(9).font('Helvetica').fillColor('#333333');

if (ipLogs.length === 0) {
pdf.text('No IP logs found', marginX, y);
} else {
for (const log of ipLogs) {
if (y > pdf.page.height - 80) {
pdf.addPage();
y = 50;
}

x = marginX;
const date = log.created ? new Date(log.created).toISOString().replace('T', ' ').substring(0, 19) : '-';
const endpoint = log.url?.replace('/v1/', '') ?? '-';

pdf.fillColor('#333333');
pdf.text(date, x, y, { width: cols[0].width - 5 });
x += cols[0].width;
pdf.text(log.ip ?? '-', x, y, { width: cols[1].width - 5 });
x += cols[1].width;
pdf.text(log.country ?? '-', x, y, { width: cols[2].width - 5 });
x += cols[2].width;
pdf.text(endpoint, x, y, { width: cols[3].width - 5 });
x += cols[3].width;

pdf.fillColor(log.result ? '#28a745' : '#dc3545');
pdf.text(log.result ? 'Pass' : 'Fail', x, y, { width: cols[4].width - 5 });

y += 20;
}
}

pdf
.moveTo(marginX, y)
.lineTo(width - marginX, y)
.stroke('#CCCCCC');
pdf.y = y + 10;
}

async getUserDataDetails(id: number): Promise<UserDataSupportInfoDetails> {
const userData = await this.userDataService.getUserData(id, { wallet: true, bankDatas: true });
if (!userData) throw new NotFoundException(`User not found`);
Expand Down Expand Up @@ -328,7 +440,10 @@ export class SupportService {
uid: tx.uid,
type: tx.type,
sourceType: tx.sourceType,
inputAmount: tx.buyCrypto?.inputAmount ?? tx.buyFiat?.inputAmount,
inputAsset: tx.buyCrypto?.inputAsset ?? tx.buyFiat?.inputAsset,
amountInChf: tx.amountInChf,
amountInEur: tx.buyCrypto?.amountInEur ?? tx.buyFiat?.amountInEur,
amlCheck: tx.amlCheck,
chargebackDate:
tx.buyCrypto?.chargebackDate ??
Expand All @@ -344,6 +459,7 @@ export class SupportService {
return {
id: user.id,
address: user.address,
ref: user.ref,
role: user.role,
status: user.status,
created: user.created,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface {
)
.map((b) => b.value);

this.currentBIC = await this.bankAccountService.getOrCreateIbanBankAccountInternal(args.value).then((b) => b.bic);
this.currentBIC = args.value
? await this.bankAccountService.getOrCreateIbanBankAccountInternal(args.value).then((b) => b.bic)
: undefined;

return this.defaultMessage(args) == null;
}
Expand Down
73 changes: 39 additions & 34 deletions src/subdomains/supporting/log/log-job.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AssetService } from 'src/shared/models/asset/asset.service';
import { SettingService } from 'src/shared/models/setting/setting.service';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import { Process, ProcessService } from 'src/shared/services/process.service';
import { AsyncCache, CacheItemResetPeriod } from 'src/shared/utils/async-cache';
import { DfxCron } from 'src/shared/utils/cron';
import { AmountType, Util } from 'src/shared/utils/util';
import { BuyCrypto } from 'src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity';
Expand Down Expand Up @@ -62,11 +63,12 @@ export class LogJobService {

private readonly unavailableClientWarningsLogged = new Set<Blockchain>();

private paymentBalanceCache: Map<number, BlockchainTokenBalance> = new Map();
private paymentBalanceCacheTime: Date = new Date(0);

private customBalanceCache: { blockchain: Blockchain; balances: BlockchainTokenBalance[] }[] = [];
private customBalanceCacheTime: Date = new Date(0);
private readonly paymentBalanceCache = new AsyncCache<Map<number, BlockchainTokenBalance>>(
CacheItemResetPeriod.EVERY_HOUR,
);
private readonly customBalanceCache = new AsyncCache<
{ blockchain: Blockchain; balances: BlockchainTokenBalance[] }[]
>(CacheItemResetPeriod.EVERY_HOUR);

constructor(
private readonly tradingRuleService: TradingRuleService,
Expand Down Expand Up @@ -223,40 +225,43 @@ export class LogJobService {
}),
);

if (Util.minutesDiff(this.customBalanceCacheTime) >= 60) {
this.customBalanceCache = await Promise.all(
Array.from(customAssetMap.entries()).map(async ([b, a]) => {
try {
const client = this.blockchainRegistryService.getClient(b);
if (!client) {
if (!this.unavailableClientWarningsLogged.has(b)) {
this.logger.warn(`Blockchain client not configured for ${b} - skipping custom balances`);
this.unavailableClientWarningsLogged.add(b);
const customBalances = await this.customBalanceCache.get(
'all',
() =>
Promise.all(
Array.from(customAssetMap.entries()).map(async ([b, a]) => {
try {
const client = this.blockchainRegistryService.getClient(b);
if (!client) {
if (!this.unavailableClientWarningsLogged.has(b)) {
this.logger.warn(`Blockchain client not configured for ${b} - skipping custom balances`);
this.unavailableClientWarningsLogged.add(b);
}
return { blockchain: b, balances: [] };
}

const balances = await Util.timeout(
this.getCustomBalances(client, a, customBalanceSettings.addresses).then((b) => b.flat()),
30000,
);
return { blockchain: b, balances };
} catch (e) {
this.logger.error(`Error in FinanceLog customBalances for blockchain ${b}:`, e);
return { blockchain: b, balances: [] };
}

const balances = await Util.timeout(
this.getCustomBalances(client, a, customBalanceSettings.addresses).then((b) => b.flat()),
30000,
);
return { blockchain: b, balances };
} catch (e) {
this.logger.error(`Error in FinanceLog customBalances for blockchain ${b}:`, e);
return { blockchain: b, balances: [] };
}
}),
);
this.customBalanceCacheTime = new Date();
}
const customBalances = this.customBalanceCache;
}),
),
undefined,
true,
);

// payment deposit address balance (Monero/Lightning have no separated balance)
if (Util.minutesDiff(this.paymentBalanceCacheTime) >= 60) {
this.paymentBalanceCache = await this.paymentBalanceService.getPaymentBalances(assets, true);
this.paymentBalanceCacheTime = new Date();
}
const paymentDepositBalances = this.paymentBalanceCache;
const paymentDepositBalances = await this.paymentBalanceCache.get(
'all',
() => this.paymentBalanceService.getPaymentBalances(assets, true),
undefined,
true,
);

// banks
const olkyBank = await this.bankService.getBankInternal(IbanBankName.OLKY, 'EUR');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export enum PayInStatus {
export enum PayInType {
PERMIT_TRANSFER = 'PermitTransfer',
SIGNED_TRANSFER = 'SignedTransfer',
SPONSORED_TRANSFER = 'SponsoredTransfer', // EIP-5792 wallet_sendCalls with paymaster
CONFIRMED_DEPOSIT = 'ConfirmedDeposit',
DEPOSIT = 'Deposit',
PAYMENT = 'Payment',
}
Expand Down
Loading
Loading