From 64955d676b95f5a598ba04504de805de3f6e0b3b Mon Sep 17 00:00:00 2001 From: bernd2022 Date: Tue, 18 Nov 2025 10:09:09 +0100 Subject: [PATCH 1/2] [DEV-4476] Citrea monitoring --- .../1763453659323-addCitreaMonitoring.js | 14 +++++ src/config/config.ts | 5 ++ .../blockchain/blockchain.module.ts | 2 + .../blockchain/citrea/citrea-client.ts | 25 +++++++++ .../blockchain/citrea/citrea.module.ts | 12 +++++ .../blockchain/citrea/citrea.service.ts | 26 ++++++++++ .../blockchain/shared/evm/evm-client.ts | 2 +- src/shared/enums/blockchain.enum.ts | 1 + .../entities/monitoring-balance.entity.ts | 26 +++++++--- .../monitoring/services/monitoring.service.ts | 51 ++++++------------- 10 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 migration/1763453659323-addCitreaMonitoring.js create mode 100644 src/integration/blockchain/citrea/citrea-client.ts create mode 100644 src/integration/blockchain/citrea/citrea.module.ts create mode 100644 src/integration/blockchain/citrea/citrea.service.ts diff --git a/migration/1763453659323-addCitreaMonitoring.js b/migration/1763453659323-addCitreaMonitoring.js new file mode 100644 index 00000000..eb9ff70d --- /dev/null +++ b/migration/1763453659323-addCitreaMonitoring.js @@ -0,0 +1,14 @@ +const { MigrationInterface, QueryRunner } = require("typeorm"); + +module.exports = class addCitreaMonitoring1763453659323 { + name = 'addCitreaMonitoring1763453659323' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "monitoring_balance" ADD "citreaBalance" float NOT NULL CONSTRAINT "DF_98f1eabfa79a178eacdc47fe777" DEFAULT 0`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "monitoring_balance" DROP CONSTRAINT "DF_98f1eabfa79a178eacdc47fe777"`); + await queryRunner.query(`ALTER TABLE "monitoring_balance" DROP COLUMN "citreaBalance"`); + } +} diff --git a/src/config/config.ts b/src/config/config.ts index d839ad44..42d957cf 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -154,6 +154,11 @@ export class Configuration { chainId: +(process.env.ROOTSTOCK_CHAIN_ID ?? -1), walletSeed: process.env.ROOTSTOCK_WALLET_SEED ?? '', }, + citrea: { + gatewayUrl: process.env.CITREA_GATEWAY_URL ?? '', + chainId: +(process.env.CITREA_CHAIN_ID ?? -1), + walletAddress: process.env.CITREA_WALLET_ADDRESS ?? '', + }, }; alchemy = { diff --git a/src/integration/blockchain/blockchain.module.ts b/src/integration/blockchain/blockchain.module.ts index 826d4cd6..5f57e745 100644 --- a/src/integration/blockchain/blockchain.module.ts +++ b/src/integration/blockchain/blockchain.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { ArbitrumModule } from './arbitrum/arbitrum.module'; import { BaseModule } from './base/base.module'; import { BitcoinModule } from './bitcoin/bitcoin.module'; +import { CitreaModule } from './citrea/citrea.module'; import { EthereumModule } from './ethereum/ethereum.module'; import { LightningModule } from './lightning/lightning.module'; import { OptimismModule } from './optimism/optimism.module'; @@ -21,6 +22,7 @@ import { UmaModule } from './uma/uma.module'; PolygonModule, BaseModule, RootstockModule, + CitreaModule, ], controllers: [], providers: [CryptoService], diff --git a/src/integration/blockchain/citrea/citrea-client.ts b/src/integration/blockchain/citrea/citrea-client.ts new file mode 100644 index 00000000..a956e7d3 --- /dev/null +++ b/src/integration/blockchain/citrea/citrea-client.ts @@ -0,0 +1,25 @@ +import { Config } from 'src/config/config'; +import { EvmUtil } from 'src/subdomains/evm/evm.util'; +import { AssetTransferEntity } from 'src/subdomains/master-data/asset/entities/asset-transfer.entity'; +import { LightningHelper } from '../lightning/lightning-helper'; +import { EvmTokenBalance } from '../shared/evm/dto/evm-token-balance.dto'; +import { EvmClient, EvmClientParams } from '../shared/evm/evm-client'; + +export class CitreaClient extends EvmClient { + constructor(private readonly params: EvmClientParams) { + super(params); + } + + async getNativeCoinBalance(): Promise { + const balance = await this.provider.getBalance(Config.blockchain.citrea.walletAddress); + return LightningHelper.btcToSat(EvmUtil.fromWeiAmount(balance.toString())); + } + + async getTokenBalance(_asset: AssetTransferEntity): Promise { + throw new Error('Method not implemented.'); + } + + async getTokenBalances(_assets: AssetTransferEntity[]): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/integration/blockchain/citrea/citrea.module.ts b/src/integration/blockchain/citrea/citrea.module.ts new file mode 100644 index 00000000..a5672f25 --- /dev/null +++ b/src/integration/blockchain/citrea/citrea.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { SharedModule } from 'src/shared/shared.module'; +import { AlchemyModule } from 'src/subdomains/alchemy/alchemy.module'; +import { EvmRegistryModule } from '../shared/evm/registry/evm-registry.module'; +import { CitreaService } from './citrea.service'; + +@Module({ + imports: [SharedModule, EvmRegistryModule, AlchemyModule], + providers: [CitreaService], + exports: [], +}) +export class CitreaModule {} diff --git a/src/integration/blockchain/citrea/citrea.service.ts b/src/integration/blockchain/citrea/citrea.service.ts new file mode 100644 index 00000000..debe0c16 --- /dev/null +++ b/src/integration/blockchain/citrea/citrea.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { GetConfig } from 'src/config/config'; +import { Blockchain } from 'src/shared/enums/blockchain.enum'; +import { HttpService } from 'src/shared/services/http.service'; +import { AlchemyService } from 'src/subdomains/alchemy/services/alchemy.service'; +import { EvmService } from '../shared/evm/evm.service'; +import { CitreaClient } from './citrea-client'; + +@Injectable() +export class CitreaService extends EvmService { + constructor(http: HttpService, alchemyService: AlchemyService) { + const { gatewayUrl, chainId } = GetConfig().blockchain.citrea; + + super(CitreaClient, { + http: http, + alchemyService, + gatewayUrl, + apiKey: '', + chainId, + }); + } + + get blockchain(): Blockchain { + return Blockchain.CITREA; + } +} diff --git a/src/integration/blockchain/shared/evm/evm-client.ts b/src/integration/blockchain/shared/evm/evm-client.ts index cd3c9033..222cf6e1 100644 --- a/src/integration/blockchain/shared/evm/evm-client.ts +++ b/src/integration/blockchain/shared/evm/evm-client.ts @@ -21,7 +21,7 @@ export abstract class EvmClient { private readonly alchemyService: AlchemyService; private readonly chainId: number; - private readonly provider: ethers.providers.JsonRpcProvider; + protected readonly provider: ethers.providers.JsonRpcProvider; private readonly tokens = new AsyncCache(); constructor(params: EvmClientParams) { diff --git a/src/shared/enums/blockchain.enum.ts b/src/shared/enums/blockchain.enum.ts index d227756d..32455de9 100644 --- a/src/shared/enums/blockchain.enum.ts +++ b/src/shared/enums/blockchain.enum.ts @@ -7,4 +7,5 @@ export enum Blockchain { POLYGON = 'polygon', BASE = 'base', ROOTSTOCK = 'rootstock', + CITREA = 'citrea', } diff --git a/src/subdomains/monitoring/entities/monitoring-balance.entity.ts b/src/subdomains/monitoring/entities/monitoring-balance.entity.ts index e39d38dc..b8b289cb 100644 --- a/src/subdomains/monitoring/entities/monitoring-balance.entity.ts +++ b/src/subdomains/monitoring/entities/monitoring-balance.entity.ts @@ -5,6 +5,14 @@ import { Price } from 'src/subdomains/support/dto/price.dto'; import { LightningWalletTotalBalanceDto } from 'src/subdomains/user/application/dto/lightning-wallet.dto'; import { Column, Entity, ManyToOne } from 'typeorm'; +export interface MonitoringBlockchainBalance { + onchainBalance: number; + lndOnchainBalance: number; + lightningBalance: number; + rootstockBalance: number; + citreaBalance: number; +} + @Entity('monitoring_balance') export class MonitoringBalanceEntity extends IEntity { @ManyToOne(() => AssetAccountEntity, { eager: true }) @@ -22,6 +30,9 @@ export class MonitoringBalanceEntity extends IEntity { @Column({ type: 'float', default: 0 }) rootstockBalance: number; + @Column({ type: 'float', default: 0 }) + citreaBalance: number; + @Column({ type: 'float', default: 0 }) customerBalance: number; @@ -37,10 +48,7 @@ export class MonitoringBalanceEntity extends IEntity { // --- FACTORY METHODS --- // static createAsBtcEntity( - onchainBalance: number, - lndOnchainBalance: number, - lightningBalance: number, - rootstockBalance: number, + blockchainBalance: MonitoringBlockchainBalance, internalBalance: LightningWalletTotalBalanceDto, customerBalance: LightningWalletTotalBalanceDto, chfPrice: Price, @@ -48,10 +56,11 @@ export class MonitoringBalanceEntity extends IEntity { const entity = new MonitoringBalanceEntity(); entity.asset = { id: customerBalance.assetId } as AssetAccountEntity; - entity.onchainBalance = onchainBalance; - entity.lndOnchainBalance = lndOnchainBalance; - entity.lightningBalance = lightningBalance; - entity.rootstockBalance = rootstockBalance; + entity.onchainBalance = blockchainBalance.onchainBalance; + entity.lndOnchainBalance = blockchainBalance.lndOnchainBalance; + entity.lightningBalance = blockchainBalance.lightningBalance; + entity.rootstockBalance = blockchainBalance.rootstockBalance; + entity.citreaBalance = blockchainBalance.citreaBalance; entity.customerBalance = customerBalance.totalBalance; entity.ldsBalance = @@ -59,6 +68,7 @@ export class MonitoringBalanceEntity extends IEntity { entity.lndOnchainBalance + entity.lightningBalance + entity.rootstockBalance + + entity.citreaBalance + internalBalance.totalBalance - entity.customerBalance; diff --git a/src/subdomains/monitoring/services/monitoring.service.ts b/src/subdomains/monitoring/services/monitoring.service.ts index 0cad920b..7776175b 100644 --- a/src/subdomains/monitoring/services/monitoring.service.ts +++ b/src/subdomains/monitoring/services/monitoring.service.ts @@ -1,6 +1,7 @@ import { Injectable, InternalServerErrorException, OnModuleInit } from '@nestjs/common'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/bitcoin-client'; import { BitcoinService } from 'src/integration/blockchain/bitcoin/bitcoin.service'; +import { CitreaClient } from 'src/integration/blockchain/citrea/citrea-client'; import { LndChannelDto } from 'src/integration/blockchain/lightning/dto/lnd.dto'; import { LightningClient } from 'src/integration/blockchain/lightning/lightning-client'; import { LightningService } from 'src/integration/blockchain/lightning/services/lightning.service'; @@ -12,7 +13,7 @@ import { QueueHandler } from 'src/shared/utils/queue-handler'; import { AssetService } from 'src/subdomains/master-data/asset/services/asset.service'; import { CoinGeckoService } from 'src/subdomains/pricing/services/coingecko.service'; import { LightningWalletTotalBalanceDto } from 'src/subdomains/user/application/dto/lightning-wallet.dto'; -import { MonitoringBalanceEntity } from '../entities/monitoring-balance.entity'; +import { MonitoringBalanceEntity, MonitoringBlockchainBalance } from '../entities/monitoring-balance.entity'; import { MonitoringBalanceRepository } from '../repositories/monitoring-balance.repository'; import { MonitoringRepository } from '../repositories/monitoring.repository'; @@ -23,6 +24,7 @@ export class MonitoringService implements OnModuleInit { private readonly bitcoinClient: BitcoinClient; private readonly lightningClient: LightningClient; private rootstockClient: RootstockClient; + private citreaClient: CitreaClient; private readonly processBalancesQueue: QueueHandler; @@ -43,6 +45,7 @@ export class MonitoringService implements OnModuleInit { onModuleInit() { this.rootstockClient = this.evmRegistryService.getClient(Blockchain.ROOTSTOCK) as RootstockClient; + this.citreaClient = this.evmRegistryService.getClient(Blockchain.CITREA) as CitreaClient; } // --- LIGHTNING --- // @@ -66,10 +69,7 @@ export class MonitoringService implements OnModuleInit { customerBalances: LightningWalletTotalBalanceDto[], ): Promise { try { - const onchainBalance = await this.getOnchainBalance(); - const lndOnchainBalance = await this.getLndOnchainBalance(); - const lightningBalance = await this.getLightningBalance(); - const rootstockBalance = await this.getRootstockBalance(); + const blockchainBalance = await this.getBlockchainBalances(); const btcAccountAsset = await this.assetService.getBtcAccountAssetOrThrow(); const btcAccountAssetId = btcAccountAsset.id; @@ -86,14 +86,7 @@ export class MonitoringService implements OnModuleInit { const customerFiatBalances = customerBalances.filter((b) => b.assetId !== btcAccountAssetId); - await this.processBtcBalance( - onchainBalance, - lndOnchainBalance, - lightningBalance, - rootstockBalance, - internalBtcBalance, - customerBtcBalance, - ); + await this.processBtcBalance(blockchainBalance, internalBtcBalance, customerBtcBalance); await this.processFiatBalances(customerFiatBalances); } catch (e) { this.logger.error('Error while processing balances', e); @@ -119,10 +112,7 @@ export class MonitoringService implements OnModuleInit { } private async processBtcBalance( - onchainBalance: number, - lndOnchainBalance: number, - lightningBalance: number, - rootstockBalance: number, + blockchainBalance: MonitoringBlockchainBalance, internalBtcBalance: LightningWalletTotalBalanceDto, customerBtcBalance: LightningWalletTotalBalanceDto, ) { @@ -130,10 +120,7 @@ export class MonitoringService implements OnModuleInit { if (!chfPrice.isValid) throw new InternalServerErrorException(`Invalid price from BTC to CHF`); const btcMonitoringEntity = MonitoringBalanceEntity.createAsBtcEntity( - onchainBalance, - lndOnchainBalance, - lightningBalance, - rootstockBalance, + blockchainBalance, internalBtcBalance, customerBtcBalance, chfPrice, @@ -169,20 +156,14 @@ export class MonitoringService implements OnModuleInit { return balance; } - private async getOnchainBalance(): Promise { - return this.bitcoinClient.getWalletBalance(); - } - - private async getLndOnchainBalance(): Promise { - return this.lightningClient.getLndConfirmedWalletBalance(); - } - - private async getLightningBalance(): Promise { - return this.lightningClient.getLndLightningBalance(); - } - - private async getRootstockBalance(): Promise { - return this.rootstockClient.getNativeCoinBalance(); + private async getBlockchainBalances(): Promise { + return { + onchainBalance: await this.bitcoinClient.getWalletBalance(), + lndOnchainBalance: await this.lightningClient.getLndConfirmedWalletBalance(), + lightningBalance: await this.lightningClient.getLndLightningBalance(), + rootstockBalance: await this.rootstockClient.getNativeCoinBalance(), + citreaBalance: await this.citreaClient.getNativeCoinBalance(), + }; } private async getChannels(): Promise { From c595a1f74a0f0c0c3ba0aa8abb6ff3a4b14c9ba8 Mon Sep 17 00:00:00 2001 From: bernd2022 Date: Tue, 18 Nov 2025 11:45:45 +0100 Subject: [PATCH 2/2] [DEV-4476] Changed env to EVM_WALLET_SEED --- src/config/config.ts | 6 ++++-- src/integration/blockchain/citrea/citrea-client.ts | 4 +++- src/integration/blockchain/rootstock/rootstock-client.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 42d957cf..a04b0b52 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -152,15 +152,17 @@ export class Configuration { gatewayUrl: process.env.ROOTSTOCK_GATEWAY_URL ?? '', apiKey: process.env.ALCHEMY_API_KEY ?? '', chainId: +(process.env.ROOTSTOCK_CHAIN_ID ?? -1), - walletSeed: process.env.ROOTSTOCK_WALLET_SEED ?? '', }, citrea: { gatewayUrl: process.env.CITREA_GATEWAY_URL ?? '', chainId: +(process.env.CITREA_CHAIN_ID ?? -1), - walletAddress: process.env.CITREA_WALLET_ADDRESS ?? '', }, }; + evm = { + walletSeed: process.env.EVM_WALLET_SEED ?? '', + }; + alchemy = { apiKey: process.env.ALCHEMY_API_KEY, authToken: process.env.ALCHEMY_AUTH_TOKEN, diff --git a/src/integration/blockchain/citrea/citrea-client.ts b/src/integration/blockchain/citrea/citrea-client.ts index a956e7d3..a4025473 100644 --- a/src/integration/blockchain/citrea/citrea-client.ts +++ b/src/integration/blockchain/citrea/citrea-client.ts @@ -11,7 +11,9 @@ export class CitreaClient extends EvmClient { } async getNativeCoinBalance(): Promise { - const balance = await this.provider.getBalance(Config.blockchain.citrea.walletAddress); + const walletAddress = EvmUtil.createWallet({ seed: Config.evm.walletSeed, index: 0 }).address; + + const balance = await this.provider.getBalance(walletAddress); return LightningHelper.btcToSat(EvmUtil.fromWeiAmount(balance.toString())); } diff --git a/src/integration/blockchain/rootstock/rootstock-client.ts b/src/integration/blockchain/rootstock/rootstock-client.ts index 88fbee3d..0a551e00 100644 --- a/src/integration/blockchain/rootstock/rootstock-client.ts +++ b/src/integration/blockchain/rootstock/rootstock-client.ts @@ -16,7 +16,7 @@ export class RootstockClient extends EvmClient { const url = `${this.params.gatewayUrl}/${this.params.apiKey ?? ''}`; - const walletAddress = EvmUtil.createWallet({ seed: Config.blockchain.rootstock.walletSeed, index: 0 }).address; + const walletAddress = EvmUtil.createWallet({ seed: Config.evm.walletSeed, index: 0 }).address; const balanceResult = await http .post<{ result: number }>(url, {