diff --git a/src/config/config.ts b/src/config/config.ts index 86ba1c081a..eb2b248aed 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1010,6 +1010,7 @@ export class Configuration { }, appInsights: { appId: process.env.APPINSIGHTS_APP_ID, + apiKey: process.env.APPINSIGHTS_API_KEY, }, }; diff --git a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts index a402e5cc57..7f31a89a29 100644 --- a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts +++ b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts @@ -347,7 +347,7 @@ export class PimlicoBundlerService { preVerificationGas: toHex(100000n), maxFeePerGas: toHex(gasPrice.maxFeePerGas), maxPriorityFeePerGas: toHex(gasPrice.maxPriorityFeePerGas), - paymaster: '0x' as Address, + paymaster: '0x0000000000000000000000000000000000000000' as Address, paymasterVerificationGasLimit: toHex(0n), paymasterPostOpGasLimit: toHex(0n), paymasterData: '0x' as Hex, diff --git a/src/integration/infrastructure/app-insights-query.service.ts b/src/integration/infrastructure/app-insights-query.service.ts index ff29ca7656..b44ccaaee1 100644 --- a/src/integration/infrastructure/app-insights-query.service.ts +++ b/src/integration/infrastructure/app-insights-query.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { Config } from 'src/config/config'; -import { DfxLogger } from 'src/shared/services/dfx-logger'; import { HttpService } from 'src/shared/services/http.service'; interface AppInsightsQueryResponse { @@ -13,69 +12,28 @@ interface AppInsightsQueryResponse { @Injectable() export class AppInsightsQueryService { - private readonly logger = new DfxLogger(AppInsightsQueryService); - private readonly baseUrl = 'https://api.applicationinsights.io/v1'; - private readonly TOKEN_REFRESH_BUFFER_MS = 60000; - - private accessToken: string | null = null; - private tokenExpiresAt = 0; constructor(private readonly http: HttpService) {} async query(kql: string, timespan?: string): Promise { - const appId = Config.azure.appInsights?.appId; - if (!appId) { - throw new Error('App Insights App ID not configured'); + const { appId, apiKey } = Config.azure.appInsights; + + if (!appId || !apiKey) { + throw new Error('App insights config missing'); } const body: { query: string; timespan?: string } = { query: kql }; if (timespan) body.timespan = timespan; - return this.request(`apps/${appId}/query`, body); - } - - private async request(url: string, body: object, nthTry = 3): Promise { - try { - if (!this.accessToken || Date.now() >= this.tokenExpiresAt - this.TOKEN_REFRESH_BUFFER_MS) { - await this.refreshAccessToken(); - } - - return await this.http.request({ - url: `${this.baseUrl}/${url}`, - method: 'POST', - data: body, - headers: { - Authorization: `Bearer ${this.accessToken}`, - 'Content-Type': 'application/json', - }, - }); - } catch (e) { - if (nthTry > 1 && e.response?.status === 401) { - await this.refreshAccessToken(); - return this.request(url, body, nthTry - 1); - } - throw e; - } - } - - private async refreshAccessToken(): Promise { - try { - const { access_token, expires_in } = await this.http.post<{ access_token: string; expires_in: number }>( - `https://login.microsoftonline.com/${Config.azure.tenantId}/oauth2/token`, - new URLSearchParams({ - grant_type: 'client_credentials', - client_id: Config.azure.clientId, - client_secret: Config.azure.clientSecret, - resource: 'https://api.applicationinsights.io', - }), - ); - - this.accessToken = access_token; - this.tokenExpiresAt = Date.now() + expires_in * 1000; - } catch (e) { - this.logger.error('Failed to refresh App Insights access token:', e); - throw new Error('Failed to authenticate with App Insights'); - } + return this.http.request({ + url: `${this.baseUrl}/apps/${appId}/query`, + method: 'POST', + data: body, + headers: { + 'x-api-key': apiKey, + 'Content-Type': 'application/json', + }, + }); } } diff --git a/src/subdomains/core/buy-crypto/process/entities/__tests__/buy-crypto-batch.entity.spec.ts b/src/subdomains/core/buy-crypto/process/entities/__tests__/buy-crypto-batch.entity.spec.ts index 666b84532d..9d64e3dd78 100644 --- a/src/subdomains/core/buy-crypto/process/entities/__tests__/buy-crypto-batch.entity.spec.ts +++ b/src/subdomains/core/buy-crypto/process/entities/__tests__/buy-crypto-batch.entity.spec.ts @@ -234,7 +234,7 @@ describe('BuyCryptoBatch', () => { expect(batch.outputReferenceAmount).toBe(1.9); }); - it('stops adding transactions when next sorted transaction exceeds limit', () => { + it('continues checking remaining transactions when higher priority transaction exceeds limit', () => { const batch = createBatchWithPipelinePriority(); // Batch has: TX1 (1.0, pipeline 5), TX2 (0.4, no pipeline), TX3 (0.5, pipeline 3) // Total: 1.9, Available: 0.95 @@ -243,11 +243,28 @@ describe('BuyCryptoBatch', () => { // Sorted order: TX3 (0.5, pipeline 3), TX1 (1.0, pipeline 5), TX2 (0.4, no pipeline) // TX3 (0.5) fits, running total = 0.5 - // TX1 (1.0) would make total 1.5 > 0.95, so algorithm stops (greedy approach) - // TX2 is not considered because algorithm breaks when a TX doesn't fit + // TX1 (1.0) would make total 1.5 > 0.95, skip it but continue + // TX2 (0.4) fits! Total 0.9 <= 0.95, so it gets added + expect(batch.transactions.length).toBe(2); + expect(batch.transactions[0].id).toBe(3); // pipeline TX + expect(batch.transactions[1].id).toBe(2); // no pipeline but fits + expect(batch.outputReferenceAmount).toBe(0.9); + }); + + it('processes non-pipeline transactions when all pipeline transactions exceed limit', () => { + const batch = createBatchWithPipelinePriority(); + // Batch has: TX1 (1.0, pipeline 5), TX2 (0.4, no pipeline), TX3 (0.5, pipeline 3) + // Total: 1.9, Available: 0.45 + + batch.optimizeByLiquidity(0.45, 0); + + // Sorted order: TX3 (0.5, pipeline 3), TX1 (1.0, pipeline 5), TX2 (0.4, no pipeline) + // TX3 (0.5) exceeds 0.45, skip but continue + // TX1 (1.0) exceeds 0.45, skip but continue + // TX2 (0.4) fits! This is the critical case that would have failed with old logic expect(batch.transactions.length).toBe(1); - expect(batch.transactions[0].id).toBe(3); - expect(batch.outputReferenceAmount).toBe(0.5); + expect(batch.transactions[0].id).toBe(2); // non-pipeline TX is processed + expect(batch.outputReferenceAmount).toBe(0.4); }); }); diff --git a/src/subdomains/core/buy-crypto/process/entities/buy-crypto-batch.entity.ts b/src/subdomains/core/buy-crypto/process/entities/buy-crypto-batch.entity.ts index a8caeac367..b66e19c3ba 100644 --- a/src/subdomains/core/buy-crypto/process/entities/buy-crypto-batch.entity.ts +++ b/src/subdomains/core/buy-crypto/process/entities/buy-crypto-batch.entity.ts @@ -189,15 +189,13 @@ export class BuyCryptoBatch extends IEntity { let requiredLiquidity = 0; for (const tx of currentTransactions) { - requiredLiquidity += tx.outputReferenceAmount; + const newRequiredLiquidity = requiredLiquidity + tx.outputReferenceAmount; // configurable reserve cap, because purchasable amounts are indicative and may be different on actual purchase - if (requiredLiquidity <= liquidityLimit * (1 - bufferCap)) { + if (newRequiredLiquidity <= liquidityLimit * (1 - bufferCap)) { reBatchTransactions.push(tx); - continue; + requiredLiquidity = newRequiredLiquidity; } - - break; } if (reBatchTransactions.length === 0) { diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat-job.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat-job.service.ts index 109dd9340c..f788e47664 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat-job.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat-job.service.ts @@ -26,7 +26,7 @@ export class BuyFiatJobService { await this.buyFiatPreparationService.chargebackTx(); } - @DfxCron(CronExpression.EVERY_10_MINUTES, { process: Process.BUY_FIAT, timeout: 7200 }) + @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.BUY_FIAT, timeout: 7200 }) async addFiatOutputs(): Promise { await this.buyFiatPreparationService.addFiatOutputs(); } diff --git a/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts b/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts index 8ed6bfd8b1..b47c598542 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts @@ -7,7 +7,9 @@ import { DfxCron } from 'src/shared/utils/cron'; import { Util } from 'src/shared/utils/util'; import { BankTxRefund, RefundInternalDto } from 'src/subdomains/core/history/dto/refund-internal.dto'; import { TransactionUtilService } from 'src/subdomains/core/transaction/transaction-util.service'; -import { IsNull, Not } from 'typeorm'; +import { KycStatus, RiskStatus, UserDataStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; +import { UserStatus } from 'src/subdomains/generic/user/models/user/user.enum'; +import { In, IsNull, Not } from 'typeorm'; import { FiatOutputType } from '../../fiat-output/fiat-output.entity'; import { FiatOutputService } from '../../fiat-output/fiat-output.service'; import { TransactionTypeInternal } from '../../payment/entities/transaction.entity'; @@ -36,9 +38,42 @@ export class BankTxReturnService { @DfxCron(CronExpression.EVERY_5_MINUTES, { process: Process.BANK_TX_RETURN, timeout: 1800 }) async fillBankTxReturn() { + await this.chargebackTx(); await this.setFiatAmounts(); } + async chargebackTx(): Promise { + const entities = await this.bankTxReturnRepo.find({ + where: { + chargebackAllowedDate: IsNull(), + chargebackAllowedDateUser: Not(IsNull()), + chargebackAmount: Not(IsNull()), + chargebackIban: Not(IsNull()), + chargebackOutput: IsNull(), + transaction: { + user: { status: In([UserStatus.NA, UserStatus.ACTIVE]) }, + }, + userData: { + kycStatus: In([KycStatus.NA, KycStatus.COMPLETED]), + status: Not(UserDataStatus.BLOCKED), + riskStatus: In([RiskStatus.NA, RiskStatus.RELEASED]), + }, + }, + relations: { bankTx: true, userData: true, transaction: { user: true } }, + }); + + for (const entity of entities) { + try { + await this.refundBankTx(entity, { + chargebackAllowedDate: new Date(), + chargebackAllowedBy: 'API', + }); + } catch (e) { + this.logger.error(`Failed to chargeback bank-tx-return ${entity.id}:`, e); + } + } + } + async setFiatAmounts(): Promise { const entities = await this.bankTxReturnRepo.find({ where: { diff --git a/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts b/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts index 42bc973678..826fb49f30 100644 --- a/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts +++ b/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts @@ -358,9 +358,19 @@ export class FiatOutputJobService { endToEndId, isTransmittedDate: new Date(), isApprovedDate: new Date(), + ...(entity.info?.startsWith('YAPEAL error') && { info: null }), }); } catch (e) { this.logger.error(`Failed to transmit YAPEAL payment for fiat output ${entity.id}:`, e); + + if (!entity.info) { + try { + const errorMsg = e?.response?.data ? JSON.stringify(e.response.data) : e?.message || String(e); + await this.fiatOutputRepo.update(entity.id, { info: `YAPEAL error: ${errorMsg}`.substring(0, 256) }); + } catch (updateError) { + this.logger.error(`Failed to persist YAPEAL error for fiat output ${entity.id}:`, updateError); + } + } } } }