From 3f16a69ac29890ff63d4ea68e89aa1b7992c1acf Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 29 Jan 2026 13:43:07 +0100 Subject: [PATCH 1/2] fix: code sharing + address filtering --- .../register/impl/base/citrea.strategy.ts | 167 ++++++++++++++++++ .../register/impl/citrea-testnet.strategy.ts | 153 +--------------- .../register/impl/citrea.strategy.ts | 153 +--------------- 3 files changed, 177 insertions(+), 296 deletions(-) create mode 100644 src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts diff --git a/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts new file mode 100644 index 0000000000..758ff4b4fe --- /dev/null +++ b/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts @@ -0,0 +1,167 @@ +import { CronExpression } from '@nestjs/schedule'; +import { Config } from 'src/config/config'; +import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util'; +import { EvmCoinHistoryEntry, EvmTokenHistoryEntry } from 'src/integration/blockchain/shared/evm/interfaces'; +import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; +import { BlockchainAddress } from 'src/shared/models/blockchain-address'; +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { Process } from 'src/shared/services/process.service'; +import { DfxCron } from 'src/shared/utils/cron'; +import { Util } from 'src/shared/utils/util'; +import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; +import { PayInType } from '../../../../entities/crypto-input.entity'; +import { PayInEntry } from '../../../../interfaces'; +import { RegisterStrategy } from './register.strategy'; + +export interface PayInCitreaServiceInterface { + getHistory(address: string, fromBlock: number): Promise<[EvmCoinHistoryEntry[], EvmTokenHistoryEntry[]]>; +} + +export abstract class CitreaBaseStrategy extends RegisterStrategy { + protected readonly logger = new DfxLogger(CitreaBaseStrategy); + + private readonly paymentDepositAddress: string; + + protected abstract getOwnAddresses(): string[]; + + constructor( + protected readonly payInCitreaService: PayInCitreaServiceInterface, + protected readonly transactionRequestService: TransactionRequestService, + ) { + super(); + this.paymentDepositAddress = EvmUtil.createWallet({ seed: Config.payment.evmSeed, index: 0 }).address; + } + + // --- JOBS --- // + @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.PAY_IN, timeout: 7200 }) + async checkPayInEntries(): Promise { + const activeDepositAddresses = await this.transactionRequestService.getActiveDepositAddresses( + Util.hoursBefore(1), + this.blockchain, + ); + + await this.processNewPayInEntries(activeDepositAddresses.map((a) => BlockchainAddress.create(a, this.blockchain))); + } + + async pollAddress(depositAddress: BlockchainAddress): Promise { + if (depositAddress.blockchain !== this.blockchain) + throw new Error(`Invalid blockchain: ${depositAddress.blockchain}`); + + return this.processNewPayInEntries([depositAddress]); + } + + private async processNewPayInEntries(depositAddresses: BlockchainAddress[]): Promise { + const log = this.createNewLogObject(); + + const newEntries: PayInEntry[] = []; + + for (const depositAddress of depositAddresses) { + const lastCheckedBlockHeight = await this.getLastCheckedBlockHeight(depositAddress); + + newEntries.push(...(await this.getNewEntries(depositAddress, lastCheckedBlockHeight))); + } + + if (newEntries?.length) { + await this.createPayInsAndSave(newEntries, log); + } + + this.printInputLog(log, 'omitted', this.blockchain); + } + + private async getLastCheckedBlockHeight(depositAddress: BlockchainAddress): Promise { + return this.payInRepository + .findOne({ + select: ['id', 'blockHeight'], + where: { address: depositAddress }, + order: { blockHeight: 'DESC' }, + loadEagerRelations: false, + }) + .then((input) => input?.blockHeight ?? 0); + } + + private async getNewEntries( + depositAddress: BlockchainAddress, + lastCheckedBlockHeight: number, + ): Promise { + const fromBlock = lastCheckedBlockHeight + 1; + const [coinTransactions, tokenTransactions] = await this.payInCitreaService.getHistory( + depositAddress.address, + fromBlock, + ); + + const supportedAssets = await this.assetService.getAllBlockchainAssets([this.blockchain]); + + const coinEntries = this.mapCoinTransactionsToEntries(coinTransactions, depositAddress, supportedAssets); + const tokenEntries = this.mapTokenTransactionsToEntries(tokenTransactions, depositAddress, supportedAssets); + + return [...coinEntries, ...tokenEntries]; + } + + private mapCoinTransactionsToEntries( + transactions: EvmCoinHistoryEntry[], + depositAddress: BlockchainAddress, + supportedAssets: Asset[], + ): PayInEntry[] { + const ownAddresses = this.getOwnAddresses(); + const relevantTransactions = transactions.filter( + (t) => + t.to.toLowerCase() === depositAddress.address.toLowerCase() && + !Util.includesIgnoreCase(ownAddresses, t.from), + ); + + const coinAsset = supportedAssets.find((a) => a.type === AssetType.COIN); + + return relevantTransactions.map((tx) => ({ + senderAddresses: tx.from, + receiverAddress: depositAddress, + txId: tx.hash, + txType: this.getTxType(depositAddress.address), + txSequence: 0, + blockHeight: parseInt(tx.blockNumber), + amount: Util.floorByPrecision(EvmUtil.fromWeiAmount(tx.value), 15), + asset: coinAsset, + })); + } + + private mapTokenTransactionsToEntries( + transactions: EvmTokenHistoryEntry[], + depositAddress: BlockchainAddress, + supportedAssets: Asset[], + ): PayInEntry[] { + const ownAddresses = this.getOwnAddresses(); + const relevantTransactions = transactions.filter( + (t) => + t.to.toLowerCase() === depositAddress.address.toLowerCase() && + !Util.includesIgnoreCase(ownAddresses, t.from), + ); + + const entries: PayInEntry[] = []; + const txGroups = Util.groupBy(relevantTransactions, 'hash'); + + for (const txGroup of txGroups.values()) { + for (let i = 0; i < txGroup.length; i++) { + const tx = txGroup[i]; + + const asset = this.assetService.getByChainIdSync(supportedAssets, this.blockchain, tx.contractAddress); + const decimals = tx.tokenDecimal ? parseInt(tx.tokenDecimal) : asset?.decimals; + + entries.push({ + senderAddresses: tx.from, + receiverAddress: depositAddress, + txId: tx.hash, + txType: this.getTxType(depositAddress.address), + txSequence: i, + blockHeight: parseInt(tx.blockNumber), + amount: Util.floorByPrecision(EvmUtil.fromWeiAmount(tx.value, decimals), 15), + asset, + }); + } + } + + return entries; + } + + private getTxType(depositAddress: string): PayInType { + return Util.equalsIgnoreCase(this.paymentDepositAddress, depositAddress) ? PayInType.PAYMENT : PayInType.DEPOSIT; + } +} diff --git a/src/subdomains/supporting/payin/strategies/register/impl/citrea-testnet.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/citrea-testnet.strategy.ts index cba68615af..e6e17e4038 100644 --- a/src/subdomains/supporting/payin/strategies/register/impl/citrea-testnet.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/register/impl/citrea-testnet.strategy.ts @@ -1,166 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { CronExpression } from '@nestjs/schedule'; import { Config } from 'src/config/config'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; -import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util'; -import { EvmCoinHistoryEntry, EvmTokenHistoryEntry } from 'src/integration/blockchain/shared/evm/interfaces'; -import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; -import { BlockchainAddress } from 'src/shared/models/blockchain-address'; -import { DfxLogger } from 'src/shared/services/dfx-logger'; -import { Process } from 'src/shared/services/process.service'; -import { DfxCron } from 'src/shared/utils/cron'; -import { Util } from 'src/shared/utils/util'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; -import { PayInType } from '../../../entities/crypto-input.entity'; -import { PayInEntry } from '../../../interfaces'; import { PayInCitreaTestnetService } from '../../../services/payin-citrea-testnet.service'; -import { RegisterStrategy } from './base/register.strategy'; +import { CitreaBaseStrategy } from './base/citrea.strategy'; @Injectable() -export class CitreaTestnetStrategy extends RegisterStrategy { - protected readonly logger = new DfxLogger(CitreaTestnetStrategy); - - private readonly paymentDepositAddress: string; - +export class CitreaTestnetStrategy extends CitreaBaseStrategy { constructor( - private readonly payInCitreaTestnetService: PayInCitreaTestnetService, - private readonly transactionRequestService: TransactionRequestService, + payInCitreaTestnetService: PayInCitreaTestnetService, + transactionRequestService: TransactionRequestService, ) { - super(); - this.paymentDepositAddress = EvmUtil.createWallet({ seed: Config.payment.evmSeed, index: 0 }).address; + super(payInCitreaTestnetService, transactionRequestService); } get blockchain(): Blockchain { return Blockchain.CITREA_TESTNET; } - // --- JOBS --- // - @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.PAY_IN, timeout: 7200 }) - async checkPayInEntries(): Promise { - const activeDepositAddresses = await this.transactionRequestService.getActiveDepositAddresses( - Util.hoursBefore(1), - this.blockchain, - ); - - await this.processNewPayInEntries(activeDepositAddresses.map((a) => BlockchainAddress.create(a, this.blockchain))); - } - - async pollAddress(depositAddress: BlockchainAddress): Promise { - if (depositAddress.blockchain !== this.blockchain) - throw new Error(`Invalid blockchain: ${depositAddress.blockchain}`); - - return this.processNewPayInEntries([depositAddress]); - } - - private async processNewPayInEntries(depositAddresses: BlockchainAddress[]): Promise { - const log = this.createNewLogObject(); - - const newEntries: PayInEntry[] = []; - - for (const depositAddress of depositAddresses) { - const lastCheckedBlockHeight = await this.getLastCheckedBlockHeight(depositAddress); - - newEntries.push(...(await this.getNewEntries(depositAddress, lastCheckedBlockHeight))); - } - - if (newEntries?.length) { - await this.createPayInsAndSave(newEntries, log); - } - - this.printInputLog(log, 'omitted', this.blockchain); - } - - private async getLastCheckedBlockHeight(depositAddress: BlockchainAddress): Promise { - return this.payInRepository - .findOne({ - select: ['id', 'blockHeight'], - where: { address: depositAddress }, - order: { blockHeight: 'DESC' }, - loadEagerRelations: false, - }) - .then((input) => input?.blockHeight ?? 0); - } - - private async getNewEntries( - depositAddress: BlockchainAddress, - lastCheckedBlockHeight: number, - ): Promise { - const fromBlock = lastCheckedBlockHeight + 1; - const [coinTransactions, tokenTransactions] = await this.payInCitreaTestnetService.getHistory( - depositAddress.address, - fromBlock, - ); - - const supportedAssets = await this.assetService.getAllBlockchainAssets([this.blockchain]); - - const coinEntries = this.mapCoinTransactionsToEntries(coinTransactions, depositAddress, supportedAssets); - const tokenEntries = this.mapTokenTransactionsToEntries(tokenTransactions, depositAddress, supportedAssets); - - return [...coinEntries, ...tokenEntries]; - } - - private mapCoinTransactionsToEntries( - transactions: EvmCoinHistoryEntry[], - depositAddress: BlockchainAddress, - supportedAssets: Asset[], - ): PayInEntry[] { - const relevantTransactions = transactions.filter( - (t) => t.to.toLowerCase() === depositAddress.address.toLowerCase(), - ); - - const coinAsset = supportedAssets.find((a) => a.type === AssetType.COIN); - - return relevantTransactions.map((tx) => ({ - senderAddresses: tx.from, - receiverAddress: depositAddress, - txId: tx.hash, - txType: this.getTxType(depositAddress.address), - txSequence: 0, - blockHeight: parseInt(tx.blockNumber), - amount: Util.floorByPrecision(EvmUtil.fromWeiAmount(tx.value), 15), - asset: coinAsset, - })); - } - - private mapTokenTransactionsToEntries( - transactions: EvmTokenHistoryEntry[], - depositAddress: BlockchainAddress, - supportedAssets: Asset[], - ): PayInEntry[] { - const relevantTransactions = transactions.filter( - (t) => t.to.toLowerCase() === depositAddress.address.toLowerCase(), - ); - - const entries: PayInEntry[] = []; - const txGroups = Util.groupBy(relevantTransactions, 'hash'); - - for (const txGroup of txGroups.values()) { - for (let i = 0; i < txGroup.length; i++) { - const tx = txGroup[i]; - - const asset = this.assetService.getByChainIdSync(supportedAssets, this.blockchain, tx.contractAddress); - const decimals = tx.tokenDecimal ? parseInt(tx.tokenDecimal) : asset?.decimals; - - entries.push({ - senderAddresses: tx.from, - receiverAddress: depositAddress, - txId: tx.hash, - txType: this.getTxType(depositAddress.address), - txSequence: i, - blockHeight: parseInt(tx.blockNumber), - amount: Util.floorByPrecision(EvmUtil.fromWeiAmount(tx.value, decimals), 15), - asset, - }); - } - } - - return entries; - } - - private getTxType(depositAddress: string): PayInType { - return Util.equalsIgnoreCase(this.paymentDepositAddress, depositAddress) ? PayInType.PAYMENT : PayInType.DEPOSIT; - } - protected getOwnAddresses(): string[] { return [Config.blockchain.citreaTestnet.citreaTestnetWalletAddress]; } diff --git a/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts index 05e65e314f..ecd8e925c7 100644 --- a/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts @@ -1,166 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { CronExpression } from '@nestjs/schedule'; import { Config } from 'src/config/config'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; -import { EvmUtil } from 'src/integration/blockchain/shared/evm/evm.util'; -import { EvmCoinHistoryEntry, EvmTokenHistoryEntry } from 'src/integration/blockchain/shared/evm/interfaces'; -import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; -import { BlockchainAddress } from 'src/shared/models/blockchain-address'; -import { DfxLogger } from 'src/shared/services/dfx-logger'; -import { Process } from 'src/shared/services/process.service'; -import { DfxCron } from 'src/shared/utils/cron'; -import { Util } from 'src/shared/utils/util'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; -import { PayInType } from '../../../entities/crypto-input.entity'; -import { PayInEntry } from '../../../interfaces'; import { PayInCitreaService } from '../../../services/payin-citrea.service'; -import { RegisterStrategy } from './base/register.strategy'; +import { CitreaBaseStrategy } from './base/citrea.strategy'; @Injectable() -export class CitreaStrategy extends RegisterStrategy { - protected readonly logger = new DfxLogger(CitreaStrategy); - - private readonly paymentDepositAddress: string; - +export class CitreaStrategy extends CitreaBaseStrategy { constructor( - private readonly payInCitreaService: PayInCitreaService, - private readonly transactionRequestService: TransactionRequestService, + payInCitreaService: PayInCitreaService, + transactionRequestService: TransactionRequestService, ) { - super(); - this.paymentDepositAddress = EvmUtil.createWallet({ seed: Config.payment.evmSeed, index: 0 }).address; + super(payInCitreaService, transactionRequestService); } get blockchain(): Blockchain { return Blockchain.CITREA; } - // --- JOBS --- // - @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.PAY_IN, timeout: 7200 }) - async checkPayInEntries(): Promise { - const activeDepositAddresses = await this.transactionRequestService.getActiveDepositAddresses( - Util.hoursBefore(1), - this.blockchain, - ); - - await this.processNewPayInEntries(activeDepositAddresses.map((a) => BlockchainAddress.create(a, this.blockchain))); - } - - async pollAddress(depositAddress: BlockchainAddress): Promise { - if (depositAddress.blockchain !== this.blockchain) - throw new Error(`Invalid blockchain: ${depositAddress.blockchain}`); - - return this.processNewPayInEntries([depositAddress]); - } - - private async processNewPayInEntries(depositAddresses: BlockchainAddress[]): Promise { - const log = this.createNewLogObject(); - - const newEntries: PayInEntry[] = []; - - for (const depositAddress of depositAddresses) { - const lastCheckedBlockHeight = await this.getLastCheckedBlockHeight(depositAddress); - - newEntries.push(...(await this.getNewEntries(depositAddress, lastCheckedBlockHeight))); - } - - if (newEntries?.length) { - await this.createPayInsAndSave(newEntries, log); - } - - this.printInputLog(log, 'omitted', this.blockchain); - } - - private async getLastCheckedBlockHeight(depositAddress: BlockchainAddress): Promise { - return this.payInRepository - .findOne({ - select: ['id', 'blockHeight'], - where: { address: depositAddress }, - order: { blockHeight: 'DESC' }, - loadEagerRelations: false, - }) - .then((input) => input?.blockHeight ?? 0); - } - - private async getNewEntries( - depositAddress: BlockchainAddress, - lastCheckedBlockHeight: number, - ): Promise { - const fromBlock = lastCheckedBlockHeight + 1; - const [coinTransactions, tokenTransactions] = await this.payInCitreaService.getHistory( - depositAddress.address, - fromBlock, - ); - - const supportedAssets = await this.assetService.getAllBlockchainAssets([this.blockchain]); - - const coinEntries = this.mapCoinTransactionsToEntries(coinTransactions, depositAddress, supportedAssets); - const tokenEntries = this.mapTokenTransactionsToEntries(tokenTransactions, depositAddress, supportedAssets); - - return [...coinEntries, ...tokenEntries]; - } - - private mapCoinTransactionsToEntries( - transactions: EvmCoinHistoryEntry[], - depositAddress: BlockchainAddress, - supportedAssets: Asset[], - ): PayInEntry[] { - const relevantTransactions = transactions.filter( - (t) => t.to.toLowerCase() === depositAddress.address.toLowerCase(), - ); - - const coinAsset = supportedAssets.find((a) => a.type === AssetType.COIN); - - return relevantTransactions.map((tx) => ({ - senderAddresses: tx.from, - receiverAddress: depositAddress, - txId: tx.hash, - txType: this.getTxType(depositAddress.address), - txSequence: 0, - blockHeight: parseInt(tx.blockNumber), - amount: Util.floorByPrecision(EvmUtil.fromWeiAmount(tx.value), 15), - asset: coinAsset, - })); - } - - private mapTokenTransactionsToEntries( - transactions: EvmTokenHistoryEntry[], - depositAddress: BlockchainAddress, - supportedAssets: Asset[], - ): PayInEntry[] { - const relevantTransactions = transactions.filter( - (t) => t.to.toLowerCase() === depositAddress.address.toLowerCase(), - ); - - const entries: PayInEntry[] = []; - const txGroups = Util.groupBy(relevantTransactions, 'hash'); - - for (const txGroup of txGroups.values()) { - for (let i = 0; i < txGroup.length; i++) { - const tx = txGroup[i]; - - const asset = this.assetService.getByChainIdSync(supportedAssets, this.blockchain, tx.contractAddress); - const decimals = tx.tokenDecimal ? parseInt(tx.tokenDecimal) : asset?.decimals; - - entries.push({ - senderAddresses: tx.from, - receiverAddress: depositAddress, - txId: tx.hash, - txType: this.getTxType(depositAddress.address), - txSequence: i, - blockHeight: parseInt(tx.blockNumber), - amount: Util.floorByPrecision(EvmUtil.fromWeiAmount(tx.value, decimals), 15), - asset, - }); - } - } - - return entries; - } - - private getTxType(depositAddress: string): PayInType { - return Util.equalsIgnoreCase(this.paymentDepositAddress, depositAddress) ? PayInType.PAYMENT : PayInType.DEPOSIT; - } - protected getOwnAddresses(): string[] { return [Config.blockchain.citrea.citreaWalletAddress]; } From 0ca0c791d8eb0ea72a5022e418d7fed39f21fa46 Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 29 Jan 2026 14:30:14 +0100 Subject: [PATCH 2/2] fix: format --- .../payin/strategies/register/impl/base/citrea.strategy.ts | 6 ++---- .../payin/strategies/register/impl/citrea.strategy.ts | 5 +---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts index 758ff4b4fe..83f0743c58 100644 --- a/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/register/impl/base/citrea.strategy.ts @@ -105,8 +105,7 @@ export abstract class CitreaBaseStrategy extends RegisterStrategy { const ownAddresses = this.getOwnAddresses(); const relevantTransactions = transactions.filter( (t) => - t.to.toLowerCase() === depositAddress.address.toLowerCase() && - !Util.includesIgnoreCase(ownAddresses, t.from), + t.to.toLowerCase() === depositAddress.address.toLowerCase() && !Util.includesIgnoreCase(ownAddresses, t.from), ); const coinAsset = supportedAssets.find((a) => a.type === AssetType.COIN); @@ -131,8 +130,7 @@ export abstract class CitreaBaseStrategy extends RegisterStrategy { const ownAddresses = this.getOwnAddresses(); const relevantTransactions = transactions.filter( (t) => - t.to.toLowerCase() === depositAddress.address.toLowerCase() && - !Util.includesIgnoreCase(ownAddresses, t.from), + t.to.toLowerCase() === depositAddress.address.toLowerCase() && !Util.includesIgnoreCase(ownAddresses, t.from), ); const entries: PayInEntry[] = []; diff --git a/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts index ecd8e925c7..6a617c0bcd 100644 --- a/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/register/impl/citrea.strategy.ts @@ -7,10 +7,7 @@ import { CitreaBaseStrategy } from './base/citrea.strategy'; @Injectable() export class CitreaStrategy extends CitreaBaseStrategy { - constructor( - payInCitreaService: PayInCitreaService, - transactionRequestService: TransactionRequestService, - ) { + constructor(payInCitreaService: PayInCitreaService, transactionRequestService: TransactionRequestService) { super(payInCitreaService, transactionRequestService); }