diff --git a/infrastructure/config/docker/docker-compose-firo.yml b/infrastructure/config/docker/docker-compose-firo.yml index 2826fb2259..3eecb5c835 100644 --- a/infrastructure/config/docker/docker-compose-firo.yml +++ b/infrastructure/config/docker/docker-compose-firo.yml @@ -4,16 +4,22 @@ services: firod: image: firoorg/firod:0.14.15.2 restart: unless-stopped + deploy: + resources: + limits: + memory: 2048M + reservations: + memory: 1024M volumes: - ./volumes/firo:/home/firod/.firo ports: - '8168:8168' - '8888:8888' healthcheck: - test: firo-cli -conf=/home/firod/.firo/firo.conf getblockchaininfo || exit 1 + test: firo-cli -conf=/home/firod/.firo/firo.conf getblockcount || exit 1 start_period: 120s - interval: 30s - timeout: 60s - retries: 10 + interval: 120s + timeout: 10s + retries: 3 command: > -conf=/home/firod/.firo/firo.conf diff --git a/infrastructure/config/firo/firo.conf b/infrastructure/config/firo/firo.conf index f5f85522f3..3f141c0085 100644 --- a/infrastructure/config/firo/firo.conf +++ b/infrastructure/config/firo/firo.conf @@ -22,9 +22,18 @@ spentindex=0 # Network onlynet=ipv4 - -# Performance -dbcache=512 maxconnections=40 + +# Mempool (defaults: maxmempool=300MB, mempoolexpiry=72h, maxorphantx=100) +maxmempool=100 +mempoolexpiry=24 +maxorphantx=10 + +# Performance (defaults: dbcache=450MB, rpcthreads=4, rpcworkqueue=16) +dbcache=256 rpcthreads=8 -rpcworkqueue=32 +rpcworkqueue=64 + +# Per-connection buffer limits (value * 1000 bytes; defaults: send=1000, receive=5000) +maxsendbuffer=500 +maxreceivebuffer=2000 diff --git a/src/integration/blockchain/spark/spark-client.ts b/src/integration/blockchain/spark/spark-client.ts index 2234ffc49e..52c34e823b 100644 --- a/src/integration/blockchain/spark/spark-client.ts +++ b/src/integration/blockchain/spark/spark-client.ts @@ -16,6 +16,22 @@ export interface SparkTransaction { fee?: number; } +export enum SparkTransferDirection { + INCOMING = 'INCOMING', + OUTGOING = 'OUTGOING', +} + +export interface SparkTransfer { + id: string; + amountSats: number; + status: string; + direction: SparkTransferDirection; + senderSparkAddress?: string; + receiverSparkAddress?: string; + createdTime?: Date; + updatedTime?: Date; +} + export interface SparkNodeInfo { version: string; testnet: boolean; @@ -87,6 +103,31 @@ export class SparkClient extends BlockchainClient { }; } + async getTransfers(limit = 100, offset = 0): Promise { + const wallet = await this.wallet; + const result = await wallet.getTransfers(limit, offset); + + return result.transfers.map((t) => ({ + id: t.id, + amountSats: t.totalValue, + status: t.status, + direction: t.transferDirection as SparkTransferDirection, + senderSparkAddress: t.senderIdentityPublicKey, + receiverSparkAddress: t.receiverIdentityPublicKey, + createdTime: t.createdTime, + updatedTime: t.updatedTime, + })); + } + + async getIncomingTransfers(limit = 100, offset = 0): Promise { + const transfers = await this.getTransfers(limit, offset); + + // Filter only completed incoming transfers + return transfers.filter( + (t) => t.status === 'TRANSFER_STATUS_COMPLETED' && t.direction === SparkTransferDirection.INCOMING, + ); + } + // --- FEE METHODS (always 0 for Spark L2) --- // async getNativeFee(): Promise { diff --git a/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts b/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts index 0933767d05..4242b4c717 100644 --- a/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts @@ -87,6 +87,7 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { break; case Blockchain.LIGHTNING: + case Blockchain.SPARK: case Blockchain.FIRO: case Blockchain.MONERO: await this.updateCoinOnlyBalance(assets); diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index 9ee2411337..b5024508bb 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -1192,7 +1192,12 @@ export class KycService { if (user.kycLevel >= KycLevel.LEVEL_50) { kycStep.complete(); - } else if (missingCompletedSteps.length === 1) { + } else if ( + !missingCompletedSteps.length || + (missingCompletedSteps.length === 1 && + missingCompletedSteps[0] === KycStepName.DFX_APPROVAL && + kycStep.name !== KycStepName.DFX_APPROVAL) + ) { kycStep.manualReview(); } else { kycStep.onHold(); diff --git a/src/subdomains/supporting/dex/dex.module.ts b/src/subdomains/supporting/dex/dex.module.ts index 19fe7e07b8..6e5f8f6cdc 100644 --- a/src/subdomains/supporting/dex/dex.module.ts +++ b/src/subdomains/supporting/dex/dex.module.ts @@ -25,6 +25,7 @@ import { DexOptimismService } from './services/dex-optimism.service'; import { DexPolygonService } from './services/dex-polygon.service'; import { DexSepoliaService } from './services/dex-sepolia.service'; import { DexSolanaService } from './services/dex-solana.service'; +import { DexSparkService } from './services/dex-spark.service'; import { DexTronService } from './services/dex-tron.service'; import { DexZanoService } from './services/dex-zano.service'; import { DexService } from './services/dex.service'; @@ -60,6 +61,7 @@ import { SepoliaCoinStrategy as SepoliaCoinStrategyCL } from './strategies/check import { SepoliaTokenStrategy as SepoliaTokenStrategyCL } from './strategies/check-liquidity/impl/sepolia-token.strategy'; import { SolanaCoinStrategy as SolanaCoinStrategyCL } from './strategies/check-liquidity/impl/solana-coin.strategy'; import { SolanaTokenStrategy as SolanaTokenStrategyCL } from './strategies/check-liquidity/impl/solana-token.strategy'; +import { SparkStrategy as SparkStrategyCL } from './strategies/check-liquidity/impl/spark.strategy'; import { TronCoinStrategy as TronCoinStrategyCL } from './strategies/check-liquidity/impl/tron-coin.strategy'; import { TronTokenStrategy as TronTokenStrategyCL } from './strategies/check-liquidity/impl/tron-token.strategy'; import { ZanoCoinStrategy as ZanoCoinStrategyCL } from './strategies/check-liquidity/impl/zano-coin.strategy'; @@ -95,6 +97,7 @@ import { SepoliaCoinStrategy as SepoliaCoinStrategyPL } from './strategies/purch import { SepoliaTokenStrategy as SepoliaTokenStrategyPL } from './strategies/purchase-liquidity/impl/sepolia-token.strategy'; import { SolanaCoinStrategy as SolanaCoinStrategyPL } from './strategies/purchase-liquidity/impl/solana-coin.strategy'; import { SolanaTokenStrategy as SolanaTokenStrategyPL } from './strategies/purchase-liquidity/impl/solana-token.strategy'; +import { SparkStrategy as SparkStrategyPL } from './strategies/purchase-liquidity/impl/spark.strategy'; import { TronCoinStrategy as TronCoinStrategyPL } from './strategies/purchase-liquidity/impl/tron-coin.strategy'; import { TronTokenStrategy as TronTokenStrategyPL } from './strategies/purchase-liquidity/impl/tron-token.strategy'; import { ZanoCoinStrategy as ZanoCoinStrategyPL } from './strategies/purchase-liquidity/impl/zano-coin.strategy'; @@ -175,6 +178,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z DexCitreaService, DexCitreaTestnetService, DexLightningService, + DexSparkService, DexFiroService, DexMoneroService, DexZanoService, @@ -193,6 +197,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z BitcoinStrategyCL, BitcoinTestnet4StrategyCL, LightningStrategyCL, + SparkStrategyCL, FiroCoinStrategyCL, MoneroStrategyCL, ZanoCoinStrategyCL, @@ -227,6 +232,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z BitcoinTestnet4StrategyPL, FiroStrategyPL, MoneroStrategyPL, + SparkStrategyPL, ZanoCoinStrategyPL, ZanoTokenStrategyPL, ArbitrumCoinStrategyPL, diff --git a/src/subdomains/supporting/dex/services/dex-spark.service.ts b/src/subdomains/supporting/dex/services/dex-spark.service.ts new file mode 100644 index 0000000000..c1d4199370 --- /dev/null +++ b/src/subdomains/supporting/dex/services/dex-spark.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { SparkClient } from 'src/integration/blockchain/spark/spark-client'; +import { SparkService } from 'src/integration/blockchain/spark/spark.service'; +import { Util } from 'src/shared/utils/util'; +import { LiquidityOrder } from '../entities/liquidity-order.entity'; +import { LiquidityOrderRepository } from '../repositories/liquidity-order.repository'; + +@Injectable() +export class DexSparkService { + private readonly sparkClient: SparkClient; + + constructor( + private readonly liquidityOrderRepo: LiquidityOrderRepository, + sparkService: SparkService, + ) { + this.sparkClient = sparkService.getDefaultClient(); + } + + async checkAvailableTargetLiquidity(inputAmount: number): Promise<[number, number]> { + const pendingAmount = await this.getPendingAmount(); + const availableAmount = await this.sparkClient.getNativeCoinBalance(); + + return [inputAmount, availableAmount - pendingAmount]; + } + + private async getPendingAmount(): Promise { + const pendingOrders = await this.liquidityOrderRepo.findBy({ + isComplete: false, + targetAsset: { dexName: 'BTC', blockchain: Blockchain.SPARK }, + }); + + return Util.sumObjValue(pendingOrders, 'estimatedTargetAmount'); + } +} diff --git a/src/subdomains/supporting/dex/strategies/check-liquidity/impl/spark.strategy.ts b/src/subdomains/supporting/dex/strategies/check-liquidity/impl/spark.strategy.ts new file mode 100644 index 0000000000..4abed0004f --- /dev/null +++ b/src/subdomains/supporting/dex/strategies/check-liquidity/impl/spark.strategy.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetCategory, AssetType } from 'src/shared/models/asset/asset.entity'; +import { AssetService } from 'src/shared/models/asset/asset.service'; +import { CheckLiquidityRequest, CheckLiquidityResult } from '../../../interfaces'; +import { DexSparkService } from '../../../services/dex-spark.service'; +import { CheckLiquidityUtil } from '../utils/check-liquidity.util'; +import { CheckLiquidityStrategy } from './base/check-liquidity.strategy'; + +@Injectable() +export class SparkStrategy extends CheckLiquidityStrategy { + constructor( + private readonly assetService: AssetService, + private readonly dexSparkService: DexSparkService, + ) { + super(); + } + + get blockchain(): Blockchain { + return Blockchain.SPARK; + } + + get assetType(): AssetType { + return undefined; + } + + get assetCategory(): AssetCategory { + return undefined; + } + + async checkLiquidity(request: CheckLiquidityRequest): Promise { + const { context, correlationId, referenceAsset, referenceAmount: bitcoinAmount } = request; + + if (referenceAsset.dexName === 'BTC') { + const [targetAmount, availableAmount] = await this.dexSparkService.checkAvailableTargetLiquidity(bitcoinAmount); + + return CheckLiquidityUtil.createNonPurchasableCheckLiquidityResult( + request, + targetAmount, + availableAmount, + await this.feeAsset(), + ); + } + + throw new Error( + `Only native coin reference is supported by Spark CheckLiquidity strategy. Provided reference asset: ${referenceAsset.dexName} Context: ${context}. CorrelationID: ${correlationId}`, + ); + } + + protected getFeeAsset(): Promise { + return this.assetService.getSparkCoin(); + } +} diff --git a/src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/spark.strategy.ts b/src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/spark.strategy.ts new file mode 100644 index 0000000000..a260b2fef5 --- /dev/null +++ b/src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/spark.strategy.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetCategory, AssetType } from 'src/shared/models/asset/asset.entity'; +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { NoPurchaseStrategy } from './base/no-purchase.strategy'; + +@Injectable() +export class SparkStrategy extends NoPurchaseStrategy { + protected readonly logger = new DfxLogger(SparkStrategy); + + get blockchain(): Blockchain { + return Blockchain.SPARK; + } + + get assetType(): AssetType { + return undefined; + } + + get assetCategory(): AssetCategory { + return undefined; + } + + get dexName(): string { + return undefined; + } + + protected getFeeAsset(): Promise { + return this.assetService.getSparkCoin(); + } +} diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index ec2a043176..989b256596 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -261,6 +261,10 @@ export class LogJobService { const pendingPayIns = await this.payInService.getPendingPayIns(); const pendingBuyFiat = await this.buyFiatService.getPendingTransactions(); const pendingBuyCrypto = await this.buyCryptoService.getPendingTransactions(); + const payoutSentBuyCryptoIds = await this.payoutService.getRecentPayoutSentCorrelationIds( + PayoutOrderContext.BUY_CRYPTO, + ); + const filteredPendingBuyCrypto = pendingBuyCrypto.filter((tx) => !payoutSentBuyCryptoIds.has(tx.id.toString())); const pendingBankTx = await this.bankTxService.getPendingTx(); const pendingBankTxRepeat = await this.bankTxRepeatService.getPendingTx(); const pendingBankTxReturn = await this.bankTxReturnService.getPendingTx(); @@ -822,7 +826,7 @@ export class LogJobService { const manualDebtPosition = manualDebtPositions.find((p) => p.assetId === curr.id)?.value ?? 0; const { input: buyFiat, output: buyFiatPass } = this.getPendingAmounts([curr], pendingBuyFiat); - const { input: buyCrypto, output: buyCryptoPass } = this.getPendingAmounts([curr], pendingBuyCrypto); + const { input: buyCrypto, output: buyCryptoPass } = this.getPendingAmounts([curr], filteredPendingBuyCrypto); const bankTxNull = this.getPendingAmounts( [curr], diff --git a/src/subdomains/supporting/payout/services/payout.service.ts b/src/subdomains/supporting/payout/services/payout.service.ts index d96496be56..da3b82e7c1 100644 --- a/src/subdomains/supporting/payout/services/payout.service.ts +++ b/src/subdomains/supporting/payout/services/payout.service.ts @@ -7,7 +7,7 @@ import { DfxCron } from 'src/shared/utils/cron'; import { Util } from 'src/shared/utils/util'; import { MailContext, MailType } from 'src/subdomains/supporting/notification/enums'; import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; -import { FindOptionsRelations, IsNull, MoreThan, Not } from 'typeorm'; +import { FindOptionsRelations, In, IsNull, MoreThan, Not } from 'typeorm'; import { MailRequest } from '../../notification/interfaces'; import { PayoutOrder, PayoutOrderContext, PayoutOrderStatus } from '../entities/payout-order.entity'; import { PayoutOrderFactory } from '../factories/payout-order.factory'; @@ -79,6 +79,16 @@ export class PayoutService { }; } + async getRecentPayoutSentCorrelationIds(context: PayoutOrderContext): Promise> { + const since = new Date(Date.now() - 3600_000); // 1 hour + const orders = await this.payoutOrderRepo.findBy({ + context, + status: In([PayoutOrderStatus.PAYOUT_PENDING, PayoutOrderStatus.COMPLETE]), + updated: MoreThan(since), + }); + return new Set(orders.map((o) => o.correlationId)); + } + async estimateFee(targetAsset: Asset, address: string, amount: number, asset: Asset): Promise { const prepareStrategy = this.prepareStrategyRegistry.getPrepareStrategy(targetAsset); const payoutStrategy = this.payoutStrategyRegistry.getPayoutStrategy(targetAsset);