From cb82a9245a1a07ed373a80f81915589d4cae56cb Mon Sep 17 00:00:00 2001 From: Bernd Date: Sun, 15 Feb 2026 08:40:15 +0100 Subject: [PATCH 1/4] feat: add Firo blockchain integration Refactor Bitcoin services into shared bitcoin-based abstractions (BitcoinBasedFeeService, BitcoinBasedClient) for UTXO blockchain reuse. Add full Firo support: node client, PayIn (register/send), PayOut (prepare/payout), DEX strategies, deposit addresses, asset seed data, and infrastructure config. --- .vscode/settings.json | 1 + .../config/docker/docker-compose-firo.yml | 19 ++ infrastructure/config/firo/firo.conf | 26 +++ migration/seed/asset.csv | 1 + src/config/config.ts | 49 +++-- .../blockchain/bitcoin/bitcoin.module.ts | 2 +- .../__tests__/bitcoin-rpc.integration.spec.ts | 8 +- .../bitcoin/node/bitcoin-based-client.ts | 3 +- .../bitcoin/node/node.controller.ts | 4 +- .../__tests__/bitcoin-fee.service.spec.ts | 4 +- .../services/__tests__/crypto.service.spec.ts | 10 +- .../services/bitcoin-based-fee.service.ts | 77 ++++++++ .../bitcoin/services/bitcoin-fee.service.ts | 85 +------- .../{node => services}/bitcoin.service.ts | 4 +- .../blockchain/blockchain.module.ts | 31 +-- .../blockchain/firo/firo-client.ts | 181 ++++++++++++++++++ .../blockchain/firo/firo.controller.ts | 22 +++ .../blockchain/firo/firo.module.ts | 13 ++ .../blockchain/firo/rpc/firo-rpc-types.ts | 18 ++ src/integration/blockchain/firo/rpc/index.ts | 1 + .../firo/services/firo-fee.service.ts | 10 + .../blockchain/firo/services/firo.service.ts | 26 +++ .../blockchain/monero/monero-client.ts | 4 +- .../shared/enums/blockchain.enum.ts | 1 + .../services/blockchain-registry.service.ts | 55 +++++- .../shared/services/crypto.service.ts | 19 +- .../shared/util/blockchain-client.ts | 4 + .../blockchain/shared/util/blockchain.util.ts | 5 + .../services/__tests__/exchange.test.ts | 1 + .../exchange/services/binance.service.ts | 1 + .../exchange/services/bitstamp.service.ts | 1 + .../exchange/services/kraken.service.ts | 1 + .../exchange/services/kucoin.service.ts | 1 + .../exchange/services/mexc.service.ts | 1 + .../exchange/services/xt.service.ts | 1 + src/integration/lightning/lightning-client.ts | 7 +- src/shared/models/asset/asset.service.ts | 24 ++- .../actions/clementine-bridge.adapter.ts | 2 +- .../adapters/balances/blockchain.adapter.ts | 40 +--- .../observers/node-balance.observer.ts | 2 +- .../observers/node-health.observer.ts | 2 +- .../dto/payment-request.mapper.ts | 3 +- .../services/payment-activation.service.ts | 5 +- .../services/payment-balance.service.ts | 11 +- .../services/payment-link-fee.service.ts | 9 +- .../services/payment-quote.service.ts | 20 +- .../reward/services/ref-reward.service.ts | 1 + .../generic/user/models/user/user.enum.ts | 1 + .../address-pool/deposit/deposit.service.ts | 20 +- src/subdomains/supporting/dex/dex.module.ts | 30 ++- .../dex/services/dex-bitcoin.service.ts | 2 +- .../dex/services/dex-firo.service.ts | 58 ++++++ .../check-liquidity.registry.spec.ts | 8 +- .../impl/firo-coin.strategy.ts | 54 ++++++ .../purchase-liquidity/impl/firo.strategy.ts | 30 +++ .../sell-liquidity/impl/firo.strategy.ts | 35 ++++ .../supplementary/impl/firo.strategy.ts | 51 +++++ .../supporting/payin/payin.module.ts | 14 +- .../base/payin-bitcoin-based.service.ts | 5 + .../payin/services/payin-bitcoin.service.ts | 13 +- .../payin/services/payin-firo.service.ts | 154 +++++++++++++++ .../payin/services/payin.service.ts | 46 +++-- .../strategies/register/impl/firo.strategy.ts | 99 ++++++++++ .../strategies/send/impl/firo.strategy.ts | 41 ++++ .../supporting/payout/payout.module.ts | 31 +-- .../payout/services/payout-bitcoin.service.ts | 2 +- .../payout/services/payout-firo.service.ts | 52 +++++ .../strategies/payout/impl/firo.strategy.ts | 80 ++++++++ .../strategies/prepare/impl/firo.strategy.ts | 24 +++ 69 files changed, 1406 insertions(+), 260 deletions(-) create mode 100644 infrastructure/config/docker/docker-compose-firo.yml create mode 100644 infrastructure/config/firo/firo.conf create mode 100644 src/integration/blockchain/bitcoin/services/bitcoin-based-fee.service.ts rename src/integration/blockchain/bitcoin/{node => services}/bitcoin.service.ts (96%) create mode 100644 src/integration/blockchain/firo/firo-client.ts create mode 100644 src/integration/blockchain/firo/firo.controller.ts create mode 100644 src/integration/blockchain/firo/firo.module.ts create mode 100644 src/integration/blockchain/firo/rpc/firo-rpc-types.ts create mode 100644 src/integration/blockchain/firo/rpc/index.ts create mode 100644 src/integration/blockchain/firo/services/firo-fee.service.ts create mode 100644 src/integration/blockchain/firo/services/firo.service.ts create mode 100644 src/subdomains/supporting/dex/services/dex-firo.service.ts create mode 100644 src/subdomains/supporting/dex/strategies/check-liquidity/impl/firo-coin.strategy.ts create mode 100644 src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/firo.strategy.ts create mode 100644 src/subdomains/supporting/dex/strategies/sell-liquidity/impl/firo.strategy.ts create mode 100644 src/subdomains/supporting/dex/strategies/supplementary/impl/firo.strategy.ts create mode 100644 src/subdomains/supporting/payin/services/payin-firo.service.ts create mode 100644 src/subdomains/supporting/payin/strategies/register/impl/firo.strategy.ts create mode 100644 src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts create mode 100644 src/subdomains/supporting/payout/services/payout-firo.service.ts create mode 100644 src/subdomains/supporting/payout/strategies/payout/impl/firo.strategy.ts create mode 100644 src/subdomains/supporting/payout/strategies/prepare/impl/firo.strategy.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 26fcc2d0cf..f320a0d975 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,7 @@ "dfip", "dilisense", "ebics", + "firo", "firstname", "forex", "frankencoin", diff --git a/infrastructure/config/docker/docker-compose-firo.yml b/infrastructure/config/docker/docker-compose-firo.yml new file mode 100644 index 0000000000..2826fb2259 --- /dev/null +++ b/infrastructure/config/docker/docker-compose-firo.yml @@ -0,0 +1,19 @@ +name: 'firo' + +services: + firod: + image: firoorg/firod:0.14.15.2 + restart: unless-stopped + volumes: + - ./volumes/firo:/home/firod/.firo + ports: + - '8168:8168' + - '8888:8888' + healthcheck: + test: firo-cli -conf=/home/firod/.firo/firo.conf getblockchaininfo || exit 1 + start_period: 120s + interval: 30s + timeout: 60s + retries: 10 + command: > + -conf=/home/firod/.firo/firo.conf diff --git a/infrastructure/config/firo/firo.conf b/infrastructure/config/firo/firo.conf new file mode 100644 index 0000000000..5f3e0c79a9 --- /dev/null +++ b/infrastructure/config/firo/firo.conf @@ -0,0 +1,26 @@ +# Firo Full Node Configuration (DFX Integration) +# Based on firoorg/firo v0.14.15.2 source code + +# Server +server=1 +listen=1 +daemon=0 +logtimestamps=1 + +# RPC +rpcuser=[RPC_USER] +rpcpassword=[RPC_PASSWORD] +rpcport=8888 +rpcbind=0.0.0.0 +rpcallowip=0.0.0.0/0 + +# Indexes +txindex=1 +addressindex=0 +timestampindex=0 +spentindex=0 + +# Performance +dbcache=1024 +maxconnections=125 +rpcthreads=8 diff --git a/migration/seed/asset.csv b/migration/seed/asset.csv index 65eb046912..f7b2f3168e 100644 --- a/migration/seed/asset.csv +++ b/migration/seed/asset.csv @@ -225,3 +225,4 @@ id,name,type,buyable,sellable,chainId,sellCommand,dexName,category,blockchain,un 113,BTC,Coin,TRUE,TRUE,,,BTC,Public,Bitcoin,Bitcoin/BTC,Bitcoin,FALSE,,87278.97353,FALSE,11,68819.65463,FALSE,FALSE,FALSE,FALSE,BTC,,TRUE,0,0,74099.1069,TRUE 112,BNB,Coin,TRUE,TRUE,0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c,,BNB,Public,BinanceSmartChain,BinanceSmartChain/BNB,Binance Coin,FALSE,601,833.1583387,FALSE,36,656.9471065,FALSE,FALSE,FALSE,FALSE,Other,18,FALSE,0,0,707.34435,TRUE 111,ETH,Coin,TRUE,TRUE,0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2,,ETH,Public,Ethereum,Ethereum/ETH,Ether,FALSE,1,2922.079246,FALSE,6,2304.065646,FALSE,FALSE,FALSE,FALSE,Other,18,TRUE,0,0,2480.82045,TRUE +409,FIRO,Coin,TRUE,TRUE,,,FIRO,Public,Firo,Firo/FIRO,,FALSE,,1.5,FALSE,,1.35,FALSE,FALSE,FALSE,FALSE,Other,8,FALSE,0,0,1.4,TRUE diff --git a/src/config/config.ts b/src/config/config.ts index dc11a46087..b445497eec 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -149,6 +149,7 @@ export class Configuration { bitcoinAddressFormat = '([13]|bc1)[a-zA-HJ-NP-Z0-9]{25,62}'; lightningAddressFormat = '(LNURL|LNDHUB)[A-Z0-9]{25,250}|LNNID[A-Z0-9]{66}'; sparkAddressFormat = 'sp1[a-z0-9]{6,87}'; + firoAddressFormat = 'a[a-zA-HJ-NP-Z0-9]{33}'; moneroAddressFormat = '[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93}'; ethereumAddressFormat = '0x\\w{40}'; liquidAddressFormat = '(VTp|VJL)[a-zA-HJ-NP-Z0-9]{77}'; @@ -161,13 +162,14 @@ export class Configuration { tronAddressFormat = 'T[1-9A-HJ-NP-Za-km-z]{32,34}'; zanoAddressFormat = 'Z[a-zA-Z0-9]{96}|iZ[a-zA-Z0-9]{106}'; - allAddressFormat = `${this.bitcoinAddressFormat}|${this.lightningAddressFormat}|${this.sparkAddressFormat}|${this.moneroAddressFormat}|${this.ethereumAddressFormat}|${this.liquidAddressFormat}|${this.arweaveAddressFormat}|${this.cardanoAddressFormat}|${this.defichainAddressFormat}|${this.railgunAddressFormat}|${this.solanaAddressFormat}|${this.tronAddressFormat}|${this.zanoAddressFormat}`; + allAddressFormat = `${this.bitcoinAddressFormat}|${this.lightningAddressFormat}|${this.sparkAddressFormat}|${this.firoAddressFormat}|${this.moneroAddressFormat}|${this.ethereumAddressFormat}|${this.liquidAddressFormat}|${this.arweaveAddressFormat}|${this.cardanoAddressFormat}|${this.defichainAddressFormat}|${this.railgunAddressFormat}|${this.solanaAddressFormat}|${this.tronAddressFormat}|${this.zanoAddressFormat}`; masterKeySignatureFormat = '[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}'; hashSignatureFormat = '[A-Fa-f0-9]{64}'; bitcoinSignatureFormat = '(.{87}=|[A-Za-z0-9+/]+={0,2})'; lightningSignatureFormat = '[a-z0-9]{104}'; lightningCustodialSignatureFormat = '[a-z0-9]{140,146}'; + firoSignatureFormat = '(.{87}=|[A-Za-z0-9+/]+={0,2})'; moneroSignatureFormat = 'SigV\\d[0-9a-zA-Z]{88}'; ethereumSignatureFormat = '(0x)?[a-f0-9]{130}'; arweaveSignatureFormat = '[\\w\\-]{683}'; @@ -177,7 +179,7 @@ export class Configuration { tronSignatureFormat = '(0x)?[a-f0-9]{130}'; zanoSignatureFormat = '[a-f0-9]{128}'; - allSignatureFormat = `${this.masterKeySignatureFormat}|${this.hashSignatureFormat}|${this.bitcoinSignatureFormat}|${this.lightningSignatureFormat}|${this.lightningCustodialSignatureFormat}|${this.moneroSignatureFormat}|${this.ethereumSignatureFormat}|${this.arweaveSignatureFormat}|${this.cardanoSignatureFormat}|${this.railgunSignatureFormat}|${this.solanaSignatureFormat}|${this.tronSignatureFormat}|${this.zanoSignatureFormat}`; + allSignatureFormat = `${this.masterKeySignatureFormat}|${this.hashSignatureFormat}|${this.bitcoinSignatureFormat}|${this.lightningSignatureFormat}|${this.lightningCustodialSignatureFormat}|${this.firoSignatureFormat}|${this.moneroSignatureFormat}|${this.ethereumSignatureFormat}|${this.arweaveSignatureFormat}|${this.cardanoSignatureFormat}|${this.railgunSignatureFormat}|${this.solanaSignatureFormat}|${this.tronSignatureFormat}|${this.zanoSignatureFormat}`; arweaveKeyFormat = '[\\w\\-]{683}'; cardanoKeyFormat = '.*'; @@ -628,10 +630,11 @@ export class Configuration { tronSeed: process.env.PAYMENT_TRON_SEED, cardanoSeed: process.env.PAYMENT_CARDANO_SEED, bitcoinAddress: process.env.PAYMENT_BITCOIN_ADDRESS, + firoAddress: process.env.PAYMENT_FIRO_ADDRESS, moneroAddress: process.env.PAYMENT_MONERO_ADDRESS, zanoAddress: process.env.PAYMENT_ZANO_ADDRESS, minConfirmations: (blockchain: Blockchain) => - [Blockchain.ETHEREUM, Blockchain.BITCOIN, Blockchain.MONERO, Blockchain.ZANO].includes(blockchain) ? 6 : 100, + [Blockchain.ETHEREUM, Blockchain.BITCOIN, Blockchain.FIRO, Blockchain.MONERO, Blockchain.ZANO].includes(blockchain) ? 6 : 100, minVolume: 0.01, // CHF maxDepositBalance: 10000, // CHF cryptoPayoutMinAmount: +(process.env.PAYMENT_CRYPTO_PAYOUT_MIN ?? 1000), // CHF @@ -882,6 +885,21 @@ export class Configuration { }, certificate: process.env.LIGHTNING_API_CERTIFICATE?.split('
').join('\n'), }, + spark: { + sparkWalletSeed: process.env.SPARK_WALLET_SEED, + }, + firo: { + node: { + url: process.env.FIRO_NODE_URL, + }, + user: process.env.FIRO_NODE_USER, + password: process.env.FIRO_NODE_PASSWORD, + walletPassword: process.env.FIRO_NODE_WALLET_PASSWORD, + walletAddress: process.env.FIRO_WALLET_ADDRESS, + allowUnconfirmedUtxos: process.env.FIRO_ALLOW_UNCONFIRMED_UTXOS === 'true', + cpfpFeeMultiplier: +(process.env.FIRO_CPFP_FEE_MULTIPLIER ?? '2.0'), + defaultFeeMultiplier: +(process.env.FIRO_DEFAULT_FEE_MULTIPLIER ?? '1.5'), + }, monero: { node: { url: process.env.MONERO_NODE_URL, @@ -892,6 +910,17 @@ export class Configuration { walletAddress: process.env.MONERO_WALLET_ADDRESS, certificate: process.env.MONERO_RPC_CERTIFICATE?.split('
').join('\n'), }, + zano: { + node: { + url: process.env.ZANO_NODE_URL, + }, + wallet: { + url: process.env.ZANO_WALLET_URL, + address: process.env.ZANO_WALLET_ADDRESS, + }, + coinId: 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a', + fee: 0.01, + }, solana: { solanaWalletSeed: process.env.SOLANA_WALLET_SEED, solanaGatewayUrl: process.env.SOLANA_GATEWAY_URL, @@ -938,20 +967,6 @@ export class Configuration { index: accountIndex, }), }, - zano: { - node: { - url: process.env.ZANO_NODE_URL, - }, - wallet: { - url: process.env.ZANO_WALLET_URL, - address: process.env.ZANO_WALLET_ADDRESS, - }, - coinId: 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a', - fee: 0.01, - }, - spark: { - sparkWalletSeed: process.env.SPARK_WALLET_SEED, - }, frankencoin: { zchfGraphUrl: process.env.ZCHF_GRAPH_URL, contractAddress: { diff --git a/src/integration/blockchain/bitcoin/bitcoin.module.ts b/src/integration/blockchain/bitcoin/bitcoin.module.ts index 0dcd1d0498..f60bcd4989 100644 --- a/src/integration/blockchain/bitcoin/bitcoin.module.ts +++ b/src/integration/blockchain/bitcoin/bitcoin.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { SharedModule } from 'src/shared/shared.module'; -import { BitcoinService } from './node/bitcoin.service'; import { NodeController } from './node/node.controller'; import { BitcoinFeeService } from './services/bitcoin-fee.service'; +import { BitcoinService } from './services/bitcoin.service'; @Module({ imports: [SharedModule], diff --git a/src/integration/blockchain/bitcoin/node/__tests__/bitcoin-rpc.integration.spec.ts b/src/integration/blockchain/bitcoin/node/__tests__/bitcoin-rpc.integration.spec.ts index e868c434f6..e932edc2de 100644 --- a/src/integration/blockchain/bitcoin/node/__tests__/bitcoin-rpc.integration.spec.ts +++ b/src/integration/blockchain/bitcoin/node/__tests__/bitcoin-rpc.integration.spec.ts @@ -14,12 +14,12 @@ * npm run test -- --testPathPattern=bitcoin-rpc.integration */ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigModule } from '@nestjs/config'; -import { BitcoinService, BitcoinNodeType } from '../bitcoin.service'; -import { BitcoinClient } from '../bitcoin-client'; import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; import { HttpService } from 'src/shared/services/http.service'; +import { BitcoinNodeType, BitcoinService } from '../../services/bitcoin.service'; +import { BitcoinClient } from '../bitcoin-client'; // Skip tests if no Bitcoin node is configured const SKIP_INTEGRATION_TESTS = !process.env.NODE_BTC_INP_URL_ACTIVE; diff --git a/src/integration/blockchain/bitcoin/node/bitcoin-based-client.ts b/src/integration/blockchain/bitcoin/node/bitcoin-based-client.ts index 83644b00c7..1cc77a0070 100644 --- a/src/integration/blockchain/bitcoin/node/bitcoin-based-client.ts +++ b/src/integration/blockchain/bitcoin/node/bitcoin-based-client.ts @@ -3,6 +3,7 @@ import { Asset } from 'src/shared/models/asset/asset.entity'; import { HttpService } from 'src/shared/services/http.service'; import { BlockchainTokenBalance } from '../../shared/dto/blockchain-token-balance.dto'; import { BlockchainSignedTransactionResponse } from '../../shared/dto/signed-transaction-reponse.dto'; +import { CoinOnly } from '../../shared/util/blockchain-client'; import { NodeClient, NodeClientConfig } from './node-client'; export interface TransactionHistory { @@ -24,7 +25,7 @@ export interface TestMempoolResult { 'reject-reason': string; } -export abstract class BitcoinBasedClient extends NodeClient { +export abstract class BitcoinBasedClient extends NodeClient implements CoinOnly { constructor(http: HttpService, url: string, config: NodeClientConfig) { super(http, url, config); } diff --git a/src/integration/blockchain/bitcoin/node/node.controller.ts b/src/integration/blockchain/bitcoin/node/node.controller.ts index 188c9e5ce8..a2b29db33d 100644 --- a/src/integration/blockchain/bitcoin/node/node.controller.ts +++ b/src/integration/blockchain/bitcoin/node/node.controller.ts @@ -1,13 +1,13 @@ import { BadRequestException, Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; -import { InWalletTransaction } from './node-client'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiExcludeEndpoint } from '@nestjs/swagger'; import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { HttpError } from 'src/shared/services/http.service'; -import { BitcoinNodeType, BitcoinService } from './bitcoin.service'; +import { BitcoinNodeType, BitcoinService } from '../services/bitcoin.service'; import { CommandDto } from './dto/command.dto'; +import { InWalletTransaction } from './node-client'; @Controller('node') export class NodeController { diff --git a/src/integration/blockchain/bitcoin/services/__tests__/bitcoin-fee.service.spec.ts b/src/integration/blockchain/bitcoin/services/__tests__/bitcoin-fee.service.spec.ts index 9b6cd70728..8fab3f6904 100644 --- a/src/integration/blockchain/bitcoin/services/__tests__/bitcoin-fee.service.spec.ts +++ b/src/integration/blockchain/bitcoin/services/__tests__/bitcoin-fee.service.spec.ts @@ -5,9 +5,9 @@ * including fee rate caching, TX fee rate lookup, and batch operations. */ -import { BitcoinFeeService } from '../bitcoin-fee.service'; -import { BitcoinService, BitcoinNodeType } from '../../node/bitcoin.service'; import { BitcoinClient } from '../../node/bitcoin-client'; +import { BitcoinFeeService } from '../bitcoin-fee.service'; +import { BitcoinNodeType, BitcoinService } from '../bitcoin.service'; describe('BitcoinFeeService', () => { let service: BitcoinFeeService; diff --git a/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts b/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts index 65813a1cde..0398e9fd3e 100644 --- a/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts +++ b/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts @@ -2,6 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test } from '@nestjs/testing'; import { ArweaveService } from 'src/integration/blockchain/arweave/services/arweave.service'; import { CardanoService } from 'src/integration/blockchain/cardano/services/cardano.service'; +import { FiroService } from 'src/integration/blockchain/firo/services/firo.service'; import { MoneroService } from 'src/integration/blockchain/monero/services/monero.service'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { BlockchainRegistryService } from 'src/integration/blockchain/shared/services/blockchain-registry.service'; @@ -14,7 +15,7 @@ import { LightningService } from 'src/integration/lightning/services/lightning.s import { RailgunService } from 'src/integration/railgun/railgun.service'; import { TestUtil } from 'src/shared/utils/test.util'; import { UserAddressType } from 'src/subdomains/generic/user/models/user/user.enum'; -import { BitcoinService } from '../../node/bitcoin.service'; +import { BitcoinService } from '../bitcoin.service'; describe('CryptoService', () => { beforeEach(async () => { @@ -24,6 +25,7 @@ describe('CryptoService', () => { { provide: BitcoinService, useValue: createMock() }, { provide: LightningService, useValue: createMock() }, { provide: SparkService, useValue: createMock() }, + { provide: FiroService, useValue: createMock() }, { provide: MoneroService, useValue: createMock() }, { provide: ZanoService, useValue: createMock() }, { provide: SolanaService, useValue: createMock() }, @@ -121,6 +123,12 @@ describe('CryptoService', () => { ).toEqual(UserAddressType.LN_NID); }); + it('should return Blockchain.FIRO for address a8MuyHBKL3nYZKAa82x13FxqtExP2sQCqu', () => { + expect( + CryptoService.getBlockchainsBasedOn('a8MuyHBKL3nYZKAa82x13FxqtExP2sQCqu'), + ).toEqual([Blockchain.FIRO]); + }); + it('should return Blockchain.ETHEREUM and Blockchain.BINANCE_SMART_CHAIN for address 0x2d84553B3A4753009A314106d58F0CC21f441234', () => { expect(CryptoService.getBlockchainsBasedOn('0x2d84553B3A4753009A314106d58F0CC21f441234')).toEqual([ Blockchain.ETHEREUM, diff --git a/src/integration/blockchain/bitcoin/services/bitcoin-based-fee.service.ts b/src/integration/blockchain/bitcoin/services/bitcoin-based-fee.service.ts new file mode 100644 index 0000000000..a4a9775b7d --- /dev/null +++ b/src/integration/blockchain/bitcoin/services/bitcoin-based-fee.service.ts @@ -0,0 +1,77 @@ +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { AsyncCache, CacheItemResetPeriod } from 'src/shared/utils/async-cache'; +import { NodeClient } from '../node/node-client'; + +export type TxFeeRateStatus = 'unconfirmed' | 'confirmed' | 'not_found' | 'error'; + +export interface TxFeeRateResult { + status: TxFeeRateStatus; + feeRate?: number; +} + +export abstract class BitcoinBasedFeeService { + private readonly logger = new DfxLogger(BitcoinBasedFeeService); + + private readonly feeRateCache = new AsyncCache(CacheItemResetPeriod.EVERY_30_SECONDS); + private readonly txFeeRateCache = new AsyncCache(CacheItemResetPeriod.EVERY_30_SECONDS); + + constructor(protected readonly client: NodeClient) {} + + async getRecommendedFeeRate(): Promise { + return this.feeRateCache.get( + 'fastestFee', + async () => { + const feeRate = await this.client.estimateSmartFee(1); + if (feeRate === null) { + throw new Error('Failed to estimate fee rate from node'); + } + return feeRate; + }, + undefined, + true, + ); + } + + async getTxFeeRate(txid: string): Promise { + return this.txFeeRateCache.get( + txid, + async () => { + try { + const entry = await this.client.getMempoolEntry(txid); + + if (entry === null) { + const tx = await this.client.getTx(txid); + if (tx && tx.confirmations > 0) { + return { status: 'confirmed' as const }; + } + return { status: 'not_found' as const }; + } + + return { status: 'unconfirmed' as const, feeRate: entry.feeRate }; + } catch (e) { + this.logger.error(`Failed to get TX fee rate for ${txid}:`, e); + return { status: 'error' as const }; + } + }, + undefined, + true, + ); + } + + async getTxFeeRates(txids: string[]): Promise> { + const results = new Map(); + + const promises = txids.map(async (txid) => { + const result = await this.getTxFeeRate(txid); + return { txid, result }; + }); + + const responses = await Promise.all(promises); + + for (const { txid, result } of responses) { + results.set(txid, result); + } + + return results; + } +} diff --git a/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts b/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts index ee5dcfb902..ac6eeb95d4 100644 --- a/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts +++ b/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts @@ -1,87 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { DfxLogger } from 'src/shared/services/dfx-logger'; -import { AsyncCache, CacheItemResetPeriod } from 'src/shared/utils/async-cache'; -import { BitcoinClient } from '../node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from '../node/bitcoin.service'; +import { BitcoinBasedFeeService } from './bitcoin-based-fee.service'; +import { BitcoinNodeType, BitcoinService } from './bitcoin.service'; -export type TxFeeRateStatus = 'unconfirmed' | 'confirmed' | 'not_found' | 'error'; - -export interface TxFeeRateResult { - status: TxFeeRateStatus; - feeRate?: number; -} +export { TxFeeRateResult, TxFeeRateStatus } from './bitcoin-based-fee.service'; @Injectable() -export class BitcoinFeeService { - private readonly logger = new DfxLogger(BitcoinFeeService); - private readonly client: BitcoinClient; - - // Thread-safe cache with fallback support - private readonly feeRateCache = new AsyncCache(CacheItemResetPeriod.EVERY_30_SECONDS); - private readonly txFeeRateCache = new AsyncCache(CacheItemResetPeriod.EVERY_30_SECONDS); - +export class BitcoinFeeService extends BitcoinBasedFeeService { constructor(bitcoinService: BitcoinService) { - this.client = bitcoinService.getDefaultClient(BitcoinNodeType.BTC_INPUT); - } - - async getRecommendedFeeRate(): Promise { - return this.feeRateCache.get( - 'fastestFee', - async () => { - const feeRate = await this.client.estimateSmartFee(1); - if (feeRate === null) { - throw new Error('Failed to estimate fee rate from Bitcoin node'); - } - return feeRate; - }, - undefined, - true, // fallbackToCache on error - ); - } - - async getTxFeeRate(txid: string): Promise { - return this.txFeeRateCache.get( - txid, - async () => { - try { - const entry = await this.client.getMempoolEntry(txid); - - if (entry === null) { - // TX not in mempool - either confirmed or doesn't exist - // Check if TX exists in wallet - const tx = await this.client.getTx(txid); - if (tx && tx.confirmations > 0) { - return { status: 'confirmed' as const }; - } - return { status: 'not_found' as const }; - } - - return { status: 'unconfirmed' as const, feeRate: entry.feeRate }; - } catch (e) { - this.logger.error(`Failed to get TX fee rate for ${txid}:`, e); - return { status: 'error' as const }; - } - }, - undefined, - true, // fallbackToCache on error - ); - } - - async getTxFeeRates(txids: string[]): Promise> { - const results = new Map(); - - // Parallel fetch - errors are handled in getTxFeeRate - const promises = txids.map(async (txid) => { - const result = await this.getTxFeeRate(txid); - return { txid, result }; - }); - - const responses = await Promise.all(promises); - - for (const { txid, result } of responses) { - results.set(txid, result); - } - - return results; + super(bitcoinService.getDefaultClient(BitcoinNodeType.BTC_INPUT)); } } diff --git a/src/integration/blockchain/bitcoin/node/bitcoin.service.ts b/src/integration/blockchain/bitcoin/services/bitcoin.service.ts similarity index 96% rename from src/integration/blockchain/bitcoin/node/bitcoin.service.ts rename to src/integration/blockchain/bitcoin/services/bitcoin.service.ts index ac84744b8e..bc410d6f75 100644 --- a/src/integration/blockchain/bitcoin/node/bitcoin.service.ts +++ b/src/integration/blockchain/bitcoin/services/bitcoin.service.ts @@ -3,8 +3,8 @@ import { Config } from 'src/config/config'; import { HttpService } from 'src/shared/services/http.service'; import { Util } from 'src/shared/utils/util'; import { BlockchainService } from '../../shared/util/blockchain.service'; -import { BitcoinClient } from './bitcoin-client'; -import { BlockchainInfo } from './rpc'; +import { BitcoinClient } from '../node/bitcoin-client'; +import { BlockchainInfo } from '../node/rpc'; export enum BitcoinNodeType { BTC_INPUT = 'btc-inp', diff --git a/src/integration/blockchain/blockchain.module.ts b/src/integration/blockchain/blockchain.module.ts index 084994d5b9..34e0ca105a 100644 --- a/src/integration/blockchain/blockchain.module.ts +++ b/src/integration/blockchain/blockchain.module.ts @@ -1,38 +1,39 @@ import { Module } from '@nestjs/common'; -import { BitcoinModule } from 'src/integration/blockchain/bitcoin/bitcoin.module'; import { BitcoinTestnet4Module } from 'src/integration/blockchain/bitcoin-testnet4/bitcoin-testnet4.module'; +import { BitcoinModule } from 'src/integration/blockchain/bitcoin/bitcoin.module'; import { SharedModule } from 'src/shared/shared.module'; import { LightningModule } from '../lightning/lightning.module'; import { RailgunModule } from '../railgun/railgun.module'; +import { BlockchainApiModule } from './api/blockchain-api.module'; import { ArbitrumModule } from './arbitrum/arbitrum.module'; import { ArweaveModule } from './arweave/arweave.module'; -import { BlockchainApiModule } from './api/blockchain-api.module'; import { BaseModule } from './base/base.module'; import { BscModule } from './bsc/bsc.module'; -import { CitreaModule } from './citrea/citrea.module'; +import { CardanoModule } from './cardano/cardano.module'; import { CitreaTestnetModule } from './citrea-testnet/citrea-testnet.module'; +import { CitreaModule } from './citrea/citrea.module'; import { ClementineModule } from './clementine/clementine.module'; import { DEuroModule } from './deuro/deuro.module'; import { Ebel2xModule } from './ebel2x/ebel2x.module'; -import { JuiceModule } from './juice/juice.module'; import { EthereumModule } from './ethereum/ethereum.module'; +import { FiroModule } from './firo/firo.module'; import { FrankencoinModule } from './frankencoin/frankencoin.module'; import { GnosisModule } from './gnosis/gnosis.module'; +import { JuiceModule } from './juice/juice.module'; import { MoneroModule } from './monero/monero.module'; import { OptimismModule } from './optimism/optimism.module'; import { PolygonModule } from './polygon/polygon.module'; import { RealUnitBlockchainModule } from './realunit/realunit-blockchain.module'; import { SepoliaModule } from './sepolia/sepolia.module'; import { Eip7702DelegationModule } from './shared/evm/delegation/eip7702-delegation.module'; -import { PimlicoPaymasterModule } from './shared/evm/paymaster/pimlico-paymaster.module'; import { EvmDecimalsService } from './shared/evm/evm-decimals.service'; +import { PimlicoPaymasterModule } from './shared/evm/paymaster/pimlico-paymaster.module'; import { BlockchainRegistryService } from './shared/services/blockchain-registry.service'; import { CryptoService } from './shared/services/crypto.service'; import { TxValidationService } from './shared/services/tx-validation.service'; import { SolanaModule } from './solana/solana.module'; import { SparkModule } from './spark/spark.module'; import { TronModule } from './tron/tron.module'; -import { CardanoModule } from './cardano/cardano.module'; import { ZanoModule } from './zano/zano.module'; @Module({ @@ -41,6 +42,11 @@ import { ZanoModule } from './zano/zano.module'; SharedModule, BitcoinModule, BitcoinTestnet4Module, + LightningModule, + SparkModule, + FiroModule, + MoneroModule, + ZanoModule, BscModule, EthereumModule, SepoliaModule, @@ -49,10 +55,6 @@ import { ZanoModule } from './zano/zano.module'; PolygonModule, BaseModule, GnosisModule, - LightningModule, - SparkModule, - MoneroModule, - ZanoModule, FrankencoinModule, DEuroModule, JuiceModule, @@ -73,6 +75,11 @@ import { ZanoModule } from './zano/zano.module'; exports: [ BitcoinModule, BitcoinTestnet4Module, + LightningModule, + SparkModule, + FiroModule, + MoneroModule, + ZanoModule, BscModule, EthereumModule, SepoliaModule, @@ -81,10 +88,6 @@ import { ZanoModule } from './zano/zano.module'; PolygonModule, BaseModule, GnosisModule, - LightningModule, - SparkModule, - MoneroModule, - ZanoModule, FrankencoinModule, DEuroModule, JuiceModule, diff --git a/src/integration/blockchain/firo/firo-client.ts b/src/integration/blockchain/firo/firo-client.ts new file mode 100644 index 0000000000..f5194a0c81 --- /dev/null +++ b/src/integration/blockchain/firo/firo-client.ts @@ -0,0 +1,181 @@ +import { Config, GetConfig } from 'src/config/config'; +import { HttpService } from 'src/shared/services/http.service'; +import { BitcoinBasedClient, TestMempoolResult } from '../bitcoin/node/bitcoin-based-client'; +import { Block, NodeClientConfig } from '../bitcoin/node/node-client'; +import { FiroRawTransaction } from './rpc'; + +/** + * Firo RPC client - overrides Bitcoin Core methods that are incompatible with Firo. + * + * Firo (v0.14.15.2) is based on an older Bitcoin Core fork and differs in several RPC methods: + * - send: does not exist (Bitcoin Core 0.21+), use settxfee + sendmany instead + * - getnewaddress: only accepts (account), no address_type parameter + * - getbalances: does not exist, use getbalance instead + * - estimatesmartfee: only accepts (nblocks), no estimate_mode parameter + * - testmempoolaccept: does not exist + * - listwallets: does not exist (no multi-wallet support) + * - sendmany: max 5 params (no replaceable/conf_target/estimate_mode) + * - getblock: verbose is boolean, not int verbosity (0/1/2) + * - getrawtransaction: boolean verbose, no multi-level verbosity, no prevout in result + */ +export class FiroClient extends BitcoinBasedClient { + constructor(http: HttpService, url: string) { + const firoConfig = GetConfig().blockchain.firo; + + const config: NodeClientConfig = { + user: firoConfig.user, + password: firoConfig.password, + walletPassword: firoConfig.walletPassword, + allowUnconfirmedUtxos: firoConfig.allowUnconfirmedUtxos, + }; + + super(http, url, config); + } + + get walletAddress(): string { + return Config.blockchain.firo.walletAddress; + } + + // --- RPC Overrides for Firo compatibility --- // + + // Firo's getnewaddress only accepts an optional account parameter, no address type + async createAddress(label: string, _type?: string): Promise { + return this.callNode(() => this.rpc.call('getnewaddress', [label]), true); + } + + // Firo does not support getbalances (Bitcoin Core 0.19+), use getbalance instead + // Firo's getbalance: (account, minconf, include_watchonly, addlocked) + async getBalance(): Promise { + const minconf = this.nodeConfig.allowUnconfirmedUtxos ? 0 : 1; + return this.callNode(() => this.rpc.call('getbalance', ['', minconf]), true); + } + + // Firo's getblock uses boolean verbose, not int verbosity (0/1/2) + async getBlock(hash: string): Promise { + return this.callNode(() => this.rpc.call('getblock', [hash, true])); + } + + // Firo's getrawtransaction uses boolean verbose (true/false), not numeric verbosity (0/1/2). + // Returns FiroRawTransaction with vin[].address directly (no prevout nesting like Bitcoin Core). + async getRawTx(txId: string): Promise { + try { + return await this.callNode(() => this.rpc.call('getrawtransaction', [txId, true])); + } catch { + return null; + } + } + + // Firo's estimatesmartfee only accepts nblocks, no estimate_mode parameter + async estimateSmartFee(confTarget = 1): Promise { + const result = await this.callNode(() => + this.rpc.call<{ feerate?: number; blocks: number }>('estimatesmartfee', [confTarget]), + ); + + if (result?.feerate && result.feerate > 0) { + return result.feerate * 100000; // BTC/kvB → sat/vB + } + return null; + } + + // Firo does not have the 'send' RPC (Bitcoin Core 0.21+). + // Use settxfee + sendmany instead for single sends with specific UTXOs. + async send( + addressTo: string, + _txId: string, + amount: number, + _vout: number, + feeRate: number, + ): Promise<{ outTxId: string; feeAmount: number }> { + const feeAmount = (feeRate * 225) / Math.pow(10, 8); + const sendAmount = this.roundAmount(amount - feeAmount); + + // Set fee rate via settxfee (FIRO/kB), feeRate is in sat/vB + const feePerKb = (feeRate * 1000) / Math.pow(10, 8); + await this.callNode(() => this.rpc.call('settxfee', [feePerKb]), true); + + const txid = await this.callNode( + () => this.rpc.call('sendmany', ['', { [addressTo]: sendAmount }]), + true, + ); + + return { outTxId: txid ?? '', feeAmount }; + } + + // Firo does not have the 'send' RPC. Use settxfee + sendmany instead. + async sendMany(payload: { addressTo: string; amount: number }[], feeRate: number): Promise { + const amounts = payload.reduce((acc, p) => ({ ...acc, [p.addressTo]: p.amount }), {} as Record); + + // Set fee rate via settxfee (FIRO/kB), feeRate is in sat/vB + const feePerKb = (feeRate * 1000) / Math.pow(10, 8); + await this.callNode(() => this.rpc.call('settxfee', [feePerKb]), true); + + return this.callNode(() => this.rpc.call('sendmany', ['', amounts]), true); + } + + // Firo's sendmany only accepts 5 params (no replaceable/conf_target/estimate_mode). + // Delegates to sendMany to ensure settxfee is called with a current fee rate estimate. + async sendUtxoToMany(payload: { addressTo: string; amount: number }[]): Promise { + if (payload.length > 100) { + throw new Error('Too many addresses in one transaction batch, allowed max 100 for UTXO'); + } + + const feeRate = (await this.estimateSmartFee(1)) ?? 10; + return this.sendMany(payload, feeRate); + } + + // Firo's getmempoolentry returns { fee, size } instead of { fees: { base }, vsize } (pre-0.17 format) + async getMempoolEntry(txid: string): Promise<{ feeRate: number; vsize: number } | null> { + try { + const result = await this.callNode(() => + this.rpc.call<{ fee?: number; size?: number }>('getmempoolentry', [txid]), + ); + + if (result?.fee && result?.size) { + const feeRate = (result.fee * 100000000) / result.size; + return { feeRate, vsize: result.size }; + } + return null; + } catch { + return null; + } + } + + // Firo does not support testmempoolaccept RPC. + // Emulate it using decoderawtransaction + input lookup to calculate fee and size. + // Firo has no SegWit, so size == vsize. + async testMempoolAccept(hex: string): Promise { + try { + const decoded = await this.callNode(() => + this.rpc.call('decoderawtransaction', [hex]), + ); + + const outputTotal = decoded.vout.reduce((sum, out) => sum + out.value, 0); + + // Firo's decoderawtransaction includes vin.value directly + let inputTotal = 0; + for (const vin of decoded.vin) { + if (vin.value === undefined) { + const prevTx = await this.getRawTx(vin.txid); + if (!prevTx) { + return [{ txid: decoded.txid, allowed: false, vsize: 0, fees: { base: 0 }, 'reject-reason': 'missing-inputs' }]; + } + inputTotal += prevTx.vout[vin.vout].value; + } else { + inputTotal += vin.value; + } + } + + const fee = this.roundAmount(inputTotal - outputTotal); + + return [{ + txid: decoded.txid, + allowed: fee > 0, + vsize: decoded.size, + fees: { base: fee }, + 'reject-reason': fee <= 0 ? 'insufficient-fee' : '', + }]; + } catch (e) { + return [{ txid: '', allowed: false, vsize: 0, fees: { base: 0 }, 'reject-reason': e.message ?? 'decode-failed' }]; + } + } +} diff --git a/src/integration/blockchain/firo/firo.controller.ts b/src/integration/blockchain/firo/firo.controller.ts new file mode 100644 index 0000000000..59a7395c98 --- /dev/null +++ b/src/integration/blockchain/firo/firo.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { FiroService } from './services/firo.service'; + +@ApiTags('Firo') +@Controller('firo') +export class FiroController { + constructor(private readonly firoService: FiroService) {} + + @Get('info') + async getInfo(): Promise<{ blockHeight: number; headers: number; blocks: number; chain: string }> { + const client = this.firoService.getDefaultClient(); + const info = await client.getInfo(); + + return { + blockHeight: await client.getBlockCount(), + headers: info.headers, + blocks: info.blocks, + chain: info.chain, + }; + } +} diff --git a/src/integration/blockchain/firo/firo.module.ts b/src/integration/blockchain/firo/firo.module.ts new file mode 100644 index 0000000000..a54bc7259b --- /dev/null +++ b/src/integration/blockchain/firo/firo.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { SharedModule } from 'src/shared/shared.module'; +import { FiroController } from './firo.controller'; +import { FiroFeeService } from './services/firo-fee.service'; +import { FiroService } from './services/firo.service'; + +@Module({ + imports: [SharedModule], + controllers: [FiroController], + providers: [FiroService, FiroFeeService], + exports: [FiroService, FiroFeeService], +}) +export class FiroModule {} diff --git a/src/integration/blockchain/firo/rpc/firo-rpc-types.ts b/src/integration/blockchain/firo/rpc/firo-rpc-types.ts new file mode 100644 index 0000000000..d43a96a6f5 --- /dev/null +++ b/src/integration/blockchain/firo/rpc/firo-rpc-types.ts @@ -0,0 +1,18 @@ +/** + * Firo-specific RPC type extensions. + * + * Firo's getrawtransaction (verbose=true) returns address and value directly + * on the vin level, instead of nesting them in a prevout object like Bitcoin Core (verbosity=2). + */ + +import { RawTransaction, RawTransactionVin } from '../../bitcoin/node/rpc'; + +export interface FiroRawTransactionVin extends RawTransactionVin { + address?: string; + value?: number; + valueSat?: number; +} + +export interface FiroRawTransaction extends Omit { + vin: FiroRawTransactionVin[]; +} diff --git a/src/integration/blockchain/firo/rpc/index.ts b/src/integration/blockchain/firo/rpc/index.ts new file mode 100644 index 0000000000..9d1e6c2b0a --- /dev/null +++ b/src/integration/blockchain/firo/rpc/index.ts @@ -0,0 +1 @@ +export * from './firo-rpc-types'; diff --git a/src/integration/blockchain/firo/services/firo-fee.service.ts b/src/integration/blockchain/firo/services/firo-fee.service.ts new file mode 100644 index 0000000000..dec213c517 --- /dev/null +++ b/src/integration/blockchain/firo/services/firo-fee.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { BitcoinBasedFeeService } from '../../bitcoin/services/bitcoin-based-fee.service'; +import { FiroService } from './firo.service'; + +@Injectable() +export class FiroFeeService extends BitcoinBasedFeeService { + constructor(firoService: FiroService) { + super(firoService.getDefaultClient()); + } +} diff --git a/src/integration/blockchain/firo/services/firo.service.ts b/src/integration/blockchain/firo/services/firo.service.ts new file mode 100644 index 0000000000..d20ede4e9f --- /dev/null +++ b/src/integration/blockchain/firo/services/firo.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { Config } from 'src/config/config'; +import { HttpService } from 'src/shared/services/http.service'; +import { Util } from 'src/shared/utils/util'; +import { BlockchainService } from '../../shared/util/blockchain.service'; +import { FiroClient } from '../firo-client'; + +@Injectable() +export class FiroService extends BlockchainService { + private readonly client: FiroClient; + + constructor(private readonly http: HttpService) { + super(); + + const url = Config.blockchain.firo.node.url; + this.client = url ? new FiroClient(this.http, url) : undefined; + } + + getDefaultClient(): FiroClient { + return this.client; + } + + getPaymentRequest(address: string, amount: number): string { + return `firo:${address}?amount=${Util.numberToFixedString(amount)}`; + } +} diff --git a/src/integration/blockchain/monero/monero-client.ts b/src/integration/blockchain/monero/monero-client.ts index bcc584b66f..9d3f0cfe0a 100644 --- a/src/integration/blockchain/monero/monero-client.ts +++ b/src/integration/blockchain/monero/monero-client.ts @@ -7,7 +7,7 @@ import { Util } from 'src/shared/utils/util'; import { PayoutGroup } from 'src/subdomains/supporting/payout/services/base/payout-bitcoin-based.service'; import { BlockchainTokenBalance } from '../shared/dto/blockchain-token-balance.dto'; import { SignedTransactionResponse } from '../shared/dto/signed-transaction-reponse.dto'; -import { BlockchainClient } from '../shared/util/blockchain-client'; +import { BlockchainClient, CoinOnly } from '../shared/util/blockchain-client'; import { AddressResultDto, GetAddressResultDto, @@ -24,7 +24,7 @@ import { } from './dto/monero.dto'; import { MoneroHelper } from './monero-helper'; -export class MoneroClient extends BlockchainClient { +export class MoneroClient extends BlockchainClient implements CoinOnly { constructor(private readonly http: HttpService) { super(); } diff --git a/src/integration/blockchain/shared/enums/blockchain.enum.ts b/src/integration/blockchain/shared/enums/blockchain.enum.ts index ebdb2209ff..a164357234 100644 --- a/src/integration/blockchain/shared/enums/blockchain.enum.ts +++ b/src/integration/blockchain/shared/enums/blockchain.enum.ts @@ -2,6 +2,7 @@ export enum Blockchain { BITCOIN = 'Bitcoin', LIGHTNING = 'Lightning', SPARK = 'Spark', + FIRO = 'Firo', MONERO = 'Monero', ZANO = 'Zano', ETHEREUM = 'Ethereum', diff --git a/src/integration/blockchain/shared/services/blockchain-registry.service.ts b/src/integration/blockchain/shared/services/blockchain-registry.service.ts index 30b8cf8c22..0ad1d8fba5 100644 --- a/src/integration/blockchain/shared/services/blockchain-registry.service.ts +++ b/src/integration/blockchain/shared/services/blockchain-registry.service.ts @@ -1,16 +1,19 @@ import { Injectable } from '@nestjs/common'; +import { LightningService } from '../../../lightning/services/lightning.service'; import { ArbitrumService } from '../../arbitrum/arbitrum.service'; import { BaseService } from '../../base/base.service'; -import { BitcoinClient } from '../../bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from '../../bitcoin/node/bitcoin.service'; import { BitcoinTestnet4Client } from '../../bitcoin-testnet4/bitcoin-testnet4-client'; import { BitcoinTestnet4NodeType, BitcoinTestnet4Service } from '../../bitcoin-testnet4/bitcoin-testnet4.service'; +import { BitcoinClient } from '../../bitcoin/node/bitcoin-client'; +import { BitcoinNodeType, BitcoinService } from '../../bitcoin/services/bitcoin.service'; import { BscService } from '../../bsc/bsc.service'; import { CardanoClient } from '../../cardano/cardano-client'; import { CardanoService } from '../../cardano/services/cardano.service'; -import { CitreaService } from '../../citrea/citrea.service'; import { CitreaTestnetService } from '../../citrea-testnet/citrea-testnet.service'; +import { CitreaService } from '../../citrea/citrea.service'; import { EthereumService } from '../../ethereum/ethereum.service'; +import { FiroClient } from '../../firo/firo-client'; +import { FiroService } from '../../firo/services/firo.service'; import { GnosisService } from '../../gnosis/gnosis.service'; import { MoneroClient } from '../../monero/monero-client'; import { MoneroService } from '../../monero/services/monero.service'; @@ -29,28 +32,43 @@ import { Blockchain } from '../enums/blockchain.enum'; import { EvmClient } from '../evm/evm-client'; import { EvmService } from '../evm/evm.service'; import { L2BridgeEvmClient } from '../evm/interfaces'; +import { CoinOnly } from '../util/blockchain-client'; type BlockchainClientType = | EvmClient | BitcoinClient | BitcoinTestnet4Client - | MoneroClient | SparkClient + | FiroClient + | MoneroClient | ZanoClient | SolanaClient | TronClient | CardanoClient; + type BlockchainServiceType = | EvmService | BitcoinService | BitcoinTestnet4Service - | MoneroService | SparkService + | FiroService + | MoneroService | ZanoService | SolanaService | TronService | CardanoService; +type CoinOnlyServiceType = BlockchainServiceType | LightningService; + +const COIN_ONLY_BLOCKCHAINS = new Set([ + Blockchain.BITCOIN, + Blockchain.BITCOIN_TESTNET4, + Blockchain.LIGHTNING, + Blockchain.SPARK, + Blockchain.FIRO, + Blockchain.MONERO, +]); + @Injectable() export class BlockchainRegistryService { constructor( @@ -63,8 +81,10 @@ export class BlockchainRegistryService { private readonly baseService: BaseService, private readonly gnosisService: GnosisService, private readonly bitcoinService: BitcoinService, - private readonly moneroService: MoneroService, + private readonly lightningService: LightningService, private readonly sparkService: SparkService, + private readonly firoService: FiroService, + private readonly moneroService: MoneroService, private readonly zanoService: ZanoService, private readonly solanaService: SolanaService, private readonly tronService: TronService, @@ -98,6 +118,14 @@ export class BlockchainRegistryService { return blockchainService.getDefaultClient(type); } + getCoinOnlyClient(blockchain: Blockchain, bitcoinNodeType?: BitcoinNodeType): CoinOnly { + if (!COIN_ONLY_BLOCKCHAINS.has(blockchain)) + throw new Error(`No coin only client found for blockchain ${blockchain}`); + const coinOnlyService = this.getCoinOnlyService(blockchain); + if (coinOnlyService instanceof BitcoinService) return coinOnlyService.getDefaultClient(bitcoinNodeType); + return coinOnlyService.getDefaultClient(); + } + getService(blockchain: Blockchain): BlockchainServiceType { switch (blockchain) { case Blockchain.ETHEREUM: @@ -118,10 +146,14 @@ export class BlockchainRegistryService { return this.gnosisService; case Blockchain.BITCOIN: return this.bitcoinService; - case Blockchain.MONERO: - return this.moneroService; + case Blockchain.BITCOIN_TESTNET4: + return this.bitcoinTestnet4Service; case Blockchain.SPARK: return this.sparkService; + case Blockchain.FIRO: + return this.firoService; + case Blockchain.MONERO: + return this.moneroService; case Blockchain.ZANO: return this.zanoService; case Blockchain.SOLANA: @@ -134,14 +166,17 @@ export class BlockchainRegistryService { return this.citreaService; case Blockchain.CITREA_TESTNET: return this.citreaTestnetService; - case Blockchain.BITCOIN_TESTNET4: - return this.bitcoinTestnet4Service; default: throw new Error(`No service found for blockchain ${blockchain}`); } } + private getCoinOnlyService(blockchain: Blockchain): CoinOnlyServiceType { + if (blockchain === Blockchain.LIGHTNING) return this.lightningService; + return this.getService(blockchain); + } + getL2Client(blockchain: Blockchain): EvmClient & L2BridgeEvmClient { switch (blockchain) { case Blockchain.ARBITRUM: diff --git a/src/integration/blockchain/shared/services/crypto.service.ts b/src/integration/blockchain/shared/services/crypto.service.ts index ef050fe1e1..42c8392833 100644 --- a/src/integration/blockchain/shared/services/crypto.service.ts +++ b/src/integration/blockchain/shared/services/crypto.service.ts @@ -10,8 +10,9 @@ import { RailgunService } from 'src/integration/railgun/railgun.service'; import { Asset } from 'src/shared/models/asset/asset.entity'; import { UserAddressType } from 'src/subdomains/generic/user/models/user/user.enum'; import { ArweaveService } from '../../arweave/services/arweave.service'; -import { BitcoinService } from '../../bitcoin/node/bitcoin.service'; +import { BitcoinService } from '../../bitcoin/services/bitcoin.service'; import { CardanoService } from '../../cardano/services/cardano.service'; +import { FiroService } from '../../firo/services/firo.service'; import { LiquidHelper } from '../../liquid/liquid-helper'; import { MoneroService } from '../../monero/services/monero.service'; import { SolanaService } from '../../solana/services/solana.service'; @@ -28,10 +29,13 @@ import { BlockchainRegistryService } from './blockchain-registry.service'; export class CryptoService { private static readonly defaultEthereumChain = Blockchain.ETHEREUM; + private static readonly firoMessagePrefix = '\u0016Zcoin Signed Message:\n'; + constructor( private readonly bitcoinService: BitcoinService, private readonly lightningService: LightningService, private readonly sparkService: SparkService, + private readonly firoService: FiroService, private readonly moneroService: MoneroService, private readonly zanoService: ZanoService, private readonly solanaService: SolanaService, @@ -62,6 +66,9 @@ export class CryptoService { case Blockchain.SPARK: return this.sparkService.getPaymentRequest(address, amount); + case Blockchain.FIRO: + return this.firoService.getPaymentRequest(address, amount); + case Blockchain.MONERO: return this.moneroService.getPaymentRequest(address, amount); @@ -111,6 +118,9 @@ export class CryptoService { case Blockchain.SPARK: return UserAddressType.SPARK; + case Blockchain.FIRO: + return UserAddressType.FIRO; + case Blockchain.MONERO: return UserAddressType.MONERO; @@ -162,6 +172,7 @@ export class CryptoService { if (CryptoService.isBitcoinAddress(address)) return [Blockchain.BITCOIN]; if (CryptoService.isLightningAddress(address)) return [Blockchain.LIGHTNING]; if (CryptoService.isSparkAddress(address)) return [Blockchain.SPARK]; + if (CryptoService.isFiroAddress(address)) return [Blockchain.FIRO]; if (CryptoService.isMoneroAddress(address)) return [Blockchain.MONERO]; if (CryptoService.isZanoAddress(address)) return [Blockchain.ZANO]; if (CryptoService.isSolanaAddress(address)) return [Blockchain.SOLANA]; @@ -192,6 +203,10 @@ export class CryptoService { return RegExp(`^(${Config.sparkAddressFormat})$`).test(address); } + public static isFiroAddress(address: string): boolean { + return new RegExp(`^(${Config.firoAddressFormat})$`).test(address); + } + private static isMoneroAddress(address: string): boolean { return RegExp(`^(${Config.moneroAddressFormat})$`).test(address); } @@ -251,6 +266,8 @@ export class CryptoService { if (detectedBlockchain === Blockchain.BITCOIN) return this.verifyBitcoinBased(message, address, signature, null); if (detectedBlockchain === Blockchain.LIGHTNING) return await this.verifyLightning(address, message, signature); if (detectedBlockchain === Blockchain.SPARK) return await this.verifySpark(message, address, signature); + if (detectedBlockchain === Blockchain.FIRO) + return this.verifyBitcoinBased(message, address, signature, CryptoService.firoMessagePrefix); if (detectedBlockchain === Blockchain.MONERO) return await this.verifyMonero(message, address, signature); if (detectedBlockchain === Blockchain.ZANO) return await this.verifyZano(message, address, signature); if (detectedBlockchain === Blockchain.SOLANA) return await this.verifySolana(message, address, signature); diff --git a/src/integration/blockchain/shared/util/blockchain-client.ts b/src/integration/blockchain/shared/util/blockchain-client.ts index f355f1944c..b0809a4834 100644 --- a/src/integration/blockchain/shared/util/blockchain-client.ts +++ b/src/integration/blockchain/shared/util/blockchain-client.ts @@ -15,6 +15,10 @@ export class BlockchainToken { export type BlockchainCurrency = Currency | BlockchainToken; +export interface CoinOnly { + getNativeCoinBalance(): Promise; +} + export abstract class BlockchainClient { abstract get walletAddress(): string; abstract getNativeCoinBalance(): Promise; diff --git a/src/integration/blockchain/shared/util/blockchain.util.ts b/src/integration/blockchain/shared/util/blockchain.util.ts index e3d01c3a7d..e5ab5482e3 100644 --- a/src/integration/blockchain/shared/util/blockchain.util.ts +++ b/src/integration/blockchain/shared/util/blockchain.util.ts @@ -35,6 +35,7 @@ export const PaymentLinkBlockchains = [ Blockchain.GNOSIS, Blockchain.BINANCE_SMART_CHAIN, Blockchain.BITCOIN, + Blockchain.FIRO, Blockchain.ZANO, Blockchain.BINANCE_PAY, Blockchain.KUCOIN_PAY, @@ -68,6 +69,7 @@ const BlockchainExplorerUrls: { [b in Blockchain]: string } = { [Blockchain.BITCOIN]: 'https://mempool.space', [Blockchain.LIGHTNING]: undefined, [Blockchain.SPARK]: 'https://sparkscan.io', + [Blockchain.FIRO]: 'https://explorer.firo.org', [Blockchain.MONERO]: 'https://xmrscan.org', [Blockchain.ZANO]: 'https://explorer.zano.org', [Blockchain.ETHEREUM]: 'https://etherscan.io', @@ -106,6 +108,7 @@ const TxPaths: { [b in Blockchain]: string } = { [Blockchain.BITCOIN]: 'tx', [Blockchain.LIGHTNING]: undefined, [Blockchain.SPARK]: 'tx', + [Blockchain.FIRO]: 'tx', [Blockchain.MONERO]: 'tx', [Blockchain.ZANO]: 'transaction', [Blockchain.ETHEREUM]: 'tx', @@ -147,6 +150,7 @@ function assetPaths(asset: Asset): string | undefined { case Blockchain.BITCOIN: case Blockchain.BITCOIN_TESTNET4: case Blockchain.LIGHTNING: + case Blockchain.FIRO: case Blockchain.MONERO: return undefined; @@ -182,6 +186,7 @@ function addressPaths(blockchain: Blockchain): string | undefined { case Blockchain.DEFICHAIN: case Blockchain.BITCOIN: case Blockchain.BITCOIN_TESTNET4: + case Blockchain.FIRO: case Blockchain.ETHEREUM: case Blockchain.BINANCE_SMART_CHAIN: case Blockchain.OPTIMISM: diff --git a/src/integration/exchange/services/__tests__/exchange.test.ts b/src/integration/exchange/services/__tests__/exchange.test.ts index d59f9bd670..4be13c59aa 100644 --- a/src/integration/exchange/services/__tests__/exchange.test.ts +++ b/src/integration/exchange/services/__tests__/exchange.test.ts @@ -18,6 +18,7 @@ export class TestExchangeService extends ExchangeService { Bitcoin: undefined, Lightning: undefined, Spark: undefined, + Firo: undefined, Monero: undefined, Zano: undefined, Cardano: undefined, diff --git a/src/integration/exchange/services/binance.service.ts b/src/integration/exchange/services/binance.service.ts index 673fd1fc03..b49d6de205 100644 --- a/src/integration/exchange/services/binance.service.ts +++ b/src/integration/exchange/services/binance.service.ts @@ -15,6 +15,7 @@ export class BinanceService extends ExchangeService { Bitcoin: 'BTC', Lightning: 'LIGHTNING', Spark: undefined, + Firo: undefined, Monero: 'XMR', Zano: undefined, Cardano: 'ADA', diff --git a/src/integration/exchange/services/bitstamp.service.ts b/src/integration/exchange/services/bitstamp.service.ts index a9517a956e..f5d393a772 100644 --- a/src/integration/exchange/services/bitstamp.service.ts +++ b/src/integration/exchange/services/bitstamp.service.ts @@ -15,6 +15,7 @@ export class BitstampService extends ExchangeService { Bitcoin: undefined, Lightning: undefined, Spark: undefined, + Firo: undefined, Monero: undefined, Zano: undefined, Cardano: undefined, diff --git a/src/integration/exchange/services/kraken.service.ts b/src/integration/exchange/services/kraken.service.ts index e57b76044b..d8c57139f0 100644 --- a/src/integration/exchange/services/kraken.service.ts +++ b/src/integration/exchange/services/kraken.service.ts @@ -22,6 +22,7 @@ export class KrakenService extends ExchangeService { Bitcoin: false, Lightning: undefined, Spark: undefined, + Firo: undefined, Monero: false, Zano: undefined, Cardano: false, diff --git a/src/integration/exchange/services/kucoin.service.ts b/src/integration/exchange/services/kucoin.service.ts index 81641ac49b..1191f72943 100644 --- a/src/integration/exchange/services/kucoin.service.ts +++ b/src/integration/exchange/services/kucoin.service.ts @@ -15,6 +15,7 @@ export class KucoinService extends ExchangeService { Bitcoin: undefined, Lightning: undefined, Spark: undefined, + Firo: undefined, Monero: undefined, Zano: undefined, Cardano: undefined, diff --git a/src/integration/exchange/services/mexc.service.ts b/src/integration/exchange/services/mexc.service.ts index b344ba205f..a212c2c012 100644 --- a/src/integration/exchange/services/mexc.service.ts +++ b/src/integration/exchange/services/mexc.service.ts @@ -31,6 +31,7 @@ export class MexcService extends ExchangeService { Bitcoin: 'BTC', Lightning: undefined, Spark: undefined, + Firo: undefined, Monero: 'XMR', Zano: 'ZANO', Cardano: undefined, diff --git a/src/integration/exchange/services/xt.service.ts b/src/integration/exchange/services/xt.service.ts index 04897946de..3a83c805a7 100644 --- a/src/integration/exchange/services/xt.service.ts +++ b/src/integration/exchange/services/xt.service.ts @@ -15,6 +15,7 @@ export class XtService extends ExchangeService { Bitcoin: undefined, Lightning: undefined, Spark: undefined, + Firo: undefined, Monero: undefined, Zano: undefined, Cardano: undefined, diff --git a/src/integration/lightning/lightning-client.ts b/src/integration/lightning/lightning-client.ts index 0649f33d91..647b20a9ce 100644 --- a/src/integration/lightning/lightning-client.ts +++ b/src/integration/lightning/lightning-client.ts @@ -22,9 +22,10 @@ import { LnurlpLinkUpdateDto, } from './dto/lnurlp.dto'; import { LnurlWithdrawRequestDto, LnurlwInvoiceDto, LnurlwLinkDto, LnurlwLinkRemoveDto } from './dto/lnurlw.dto'; +import { CoinOnly } from 'src/integration/blockchain/shared/util/blockchain-client'; import { LightningHelper } from './lightning-helper'; -export class LightningClient { +export class LightningClient implements CoinOnly { constructor(private readonly http: HttpService) {} // --- LND --- // @@ -69,6 +70,10 @@ export class LightningClient { ); } + async getNativeCoinBalance(): Promise { + return this.getBalance(); + } + async getBalance(): Promise { const channels = await this.getChannels(); diff --git a/src/shared/models/asset/asset.service.ts b/src/shared/models/asset/asset.service.ts index f6f11772ac..9d14259ce0 100644 --- a/src/shared/models/asset/asset.service.ts +++ b/src/shared/models/asset/asset.service.ts @@ -221,6 +221,14 @@ export class AssetService { }); } + async getFiroCoin(): Promise { + return this.getAssetByQuery({ + name: 'FIRO', + blockchain: Blockchain.FIRO, + type: AssetType.COIN, + }); + } + async getMoneroCoin(): Promise { return this.getAssetByQuery({ name: 'XMR', @@ -229,26 +237,26 @@ export class AssetService { }); } - async getCitreaCoin(): Promise { + async getZanoCoin(): Promise { return this.getAssetByQuery({ - name: 'cBTC', - blockchain: Blockchain.CITREA, + name: 'ZANO', + blockchain: Blockchain.ZANO, type: AssetType.COIN, }); } - async getCitreaTestnetCoin(): Promise { + async getCitreaCoin(): Promise { return this.getAssetByQuery({ name: 'cBTC', - blockchain: Blockchain.CITREA_TESTNET, + blockchain: Blockchain.CITREA, type: AssetType.COIN, }); } - async getZanoCoin(): Promise { + async getCitreaTestnetCoin(): Promise { return this.getAssetByQuery({ - name: 'ZANO', - blockchain: Blockchain.ZANO, + name: 'cBTC', + blockchain: Blockchain.CITREA_TESTNET, type: AssetType.COIN, }); } diff --git a/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts index 249e7198ce..0494af8883 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/clementine-bridge.adapter.ts @@ -3,8 +3,8 @@ import { GetConfig } from 'src/config/config'; import { BitcoinTestnet4Service } from 'src/integration/blockchain/bitcoin-testnet4/bitcoin-testnet4.service'; import { BitcoinTestnet4FeeService } from 'src/integration/blockchain/bitcoin-testnet4/services/bitcoin-testnet4-fee.service'; import { BitcoinBasedClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { CitreaTestnetService } from 'src/integration/blockchain/citrea-testnet/citrea-testnet.service'; import { CitreaClient } from 'src/integration/blockchain/citrea/citrea-client'; import { CitreaService } from 'src/integration/blockchain/citrea/citrea.service'; 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 d7106efc6d..dc6a9d8019 100644 --- a/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/balances/blockchain.adapter.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; +import { BitcoinNodeType } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { CardanoClient } from 'src/integration/blockchain/cardano/cardano-client'; import { BlockchainTokenBalance } from 'src/integration/blockchain/shared/dto/blockchain-token-balance.dto'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; @@ -9,8 +8,6 @@ import { BlockchainRegistryService } from 'src/integration/blockchain/shared/ser import { SolanaClient } from 'src/integration/blockchain/solana/solana-client'; import { TronClient } from 'src/integration/blockchain/tron/tron-client'; import { ZanoClient } from 'src/integration/blockchain/zano/zano-client'; -import { LightningClient } from 'src/integration/lightning/lightning-client'; -import { LightningService } from 'src/integration/lightning/services/lightning.service'; import { isAsset } from 'src/shared/models/active'; import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; import { DfxLogger } from 'src/shared/services/dfx-logger'; @@ -32,18 +29,10 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { private readonly updateCalls = new Map>(); private readonly updateTimestamps = new Map(); - private readonly bitcoinClient: BitcoinClient; - private readonly lightningClient: LightningClient; - constructor( private readonly dexService: DexService, private readonly blockchainRegistryService: BlockchainRegistryService, - bitcoinService: BitcoinService, - lightningService: LightningService, - ) { - this.bitcoinClient = bitcoinService.getDefaultClient(BitcoinNodeType.BTC_OUTPUT); - this.lightningClient = lightningService.getDefaultClient(); - } + ) {} async getBalances(assets: (Asset & { context: LiquidityManagementContext })[]): Promise { if (!assets.every(isAsset)) { @@ -93,10 +82,11 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { try { switch (blockchain) { case Blockchain.BITCOIN: - case Blockchain.LIGHTNING: - await this.updateBitcoinBalance(assets); + await this.updateCoinOnlyBalance(assets, BitcoinNodeType.BTC_OUTPUT); break; + case Blockchain.LIGHTNING: + case Blockchain.FIRO: case Blockchain.MONERO: await this.updateCoinOnlyBalance(assets); break; @@ -144,28 +134,12 @@ export class BlockchainAdapter implements LiquidityBalanceIntegration { // --- BLOCKCHAIN INTEGRATIONS --- // - private async updateBitcoinBalance(assets: Asset[]): Promise { - for (const asset of assets) { - try { - if (asset.type !== AssetType.COIN) throw new Error(`Only coins are available on ${asset.blockchain}`); - - const client = asset.blockchain === Blockchain.BITCOIN ? this.bitcoinClient : this.lightningClient; - - const balance = await client.getBalance(); - this.balanceCache.set(asset.id, +balance); - } catch (e) { - this.logger.error(`Failed to update liquidity management balance for ${asset.uniqueName}:`, e); - this.invalidateCacheFor([asset]); - } - } - } - - private async updateCoinOnlyBalance(assets: Asset[]): Promise { + private async updateCoinOnlyBalance(assets: Asset[], bitcoinNodeType?: BitcoinNodeType.BTC_OUTPUT): Promise { for (const asset of assets) { try { if (asset.type !== AssetType.COIN) throw new Error(`Only coins are available on ${asset.blockchain}`); - const client = this.blockchainRegistryService.getClient(asset.blockchain); + const client = this.blockchainRegistryService.getCoinOnlyClient(asset.blockchain, bitcoinNodeType); const balance = await client.getNativeCoinBalance(); this.balanceCache.set(asset.id, balance); } catch (e) { diff --git a/src/subdomains/core/monitoring/observers/node-balance.observer.ts b/src/subdomains/core/monitoring/observers/node-balance.observer.ts index d48cced246..906f98fc67 100644 --- a/src/subdomains/core/monitoring/observers/node-balance.observer.ts +++ b/src/subdomains/core/monitoring/observers/node-balance.observer.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { CronExpression } from '@nestjs/schedule'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Process } from 'src/shared/services/process.service'; import { DfxCron } from 'src/shared/utils/cron'; diff --git a/src/subdomains/core/monitoring/observers/node-health.observer.ts b/src/subdomains/core/monitoring/observers/node-health.observer.ts index b1c26424d8..78b5f37adf 100644 --- a/src/subdomains/core/monitoring/observers/node-health.observer.ts +++ b/src/subdomains/core/monitoring/observers/node-health.observer.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { CronExpression } from '@nestjs/schedule'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Process } from 'src/shared/services/process.service'; import { DfxCron } from 'src/shared/utils/cron'; diff --git a/src/subdomains/core/payment-link/dto/payment-request.mapper.ts b/src/subdomains/core/payment-link/dto/payment-request.mapper.ts index 7083666bfb..22d7f0b895 100644 --- a/src/subdomains/core/payment-link/dto/payment-request.mapper.ts +++ b/src/subdomains/core/payment-link/dto/payment-request.mapper.ts @@ -18,8 +18,9 @@ export class PaymentRequestMapper { case Blockchain.GNOSIS: case Blockchain.POLYGON: case Blockchain.BINANCE_SMART_CHAIN: - case Blockchain.MONERO: case Blockchain.BITCOIN: + case Blockchain.FIRO: + case Blockchain.MONERO: case Blockchain.ZANO: case Blockchain.SOLANA: case Blockchain.TRON: diff --git a/src/subdomains/core/payment-link/services/payment-activation.service.ts b/src/subdomains/core/payment-link/services/payment-activation.service.ts index 800685e74b..d81e5d2d35 100644 --- a/src/subdomains/core/payment-link/services/payment-activation.service.ts +++ b/src/subdomains/core/payment-link/services/payment-activation.service.ts @@ -174,6 +174,9 @@ export class PaymentActivationService { return this.createLightningRequest(payment, transferInfo, expirySec); case Blockchain.BITCOIN: + case Blockchain.FIRO: + case Blockchain.MONERO: + case Blockchain.ZANO: case Blockchain.ETHEREUM: case Blockchain.ARBITRUM: case Blockchain.OPTIMISM: @@ -181,8 +184,6 @@ export class PaymentActivationService { case Blockchain.GNOSIS: case Blockchain.POLYGON: case Blockchain.BINANCE_SMART_CHAIN: - case Blockchain.MONERO: - case Blockchain.ZANO: case Blockchain.SOLANA: case Blockchain.TRON: case Blockchain.CARDANO: { diff --git a/src/subdomains/core/payment-link/services/payment-balance.service.ts b/src/subdomains/core/payment-link/services/payment-balance.service.ts index bf05319207..31cfebdd75 100644 --- a/src/subdomains/core/payment-link/services/payment-balance.service.ts +++ b/src/subdomains/core/payment-link/services/payment-balance.service.ts @@ -35,8 +35,9 @@ export class PaymentBalanceService implements OnModuleInit { private solanaDepositAddress: string; private tronDepositAddress: string; private cardanoDepositAddress: string; - private moneroDepositAddress: string; private bitcoinDepositAddress: string; + private firoDepositAddress: string; + private moneroDepositAddress: string; private zanoDepositAddress: string; constructor( @@ -50,8 +51,9 @@ export class PaymentBalanceService implements OnModuleInit { this.tronDepositAddress = TronUtil.createWallet({ seed: Config.payment.tronSeed, index: 0 }).address; this.cardanoDepositAddress = CardanoUtil.createWallet({ seed: Config.payment.cardanoSeed, index: 0 })?.address; - this.moneroDepositAddress = Config.payment.moneroAddress; this.bitcoinDepositAddress = Config.payment.bitcoinAddress; + this.firoDepositAddress = Config.payment.firoAddress; + this.moneroDepositAddress = Config.payment.moneroAddress; this.zanoDepositAddress = Config.payment.zanoAddress; } @@ -128,6 +130,9 @@ export class PaymentBalanceService implements OnModuleInit { case Blockchain.BITCOIN: return this.bitcoinDepositAddress; + case Blockchain.FIRO: + return this.firoDepositAddress; + case Blockchain.MONERO: return this.moneroDepositAddress; @@ -146,7 +151,7 @@ export class PaymentBalanceService implements OnModuleInit { } async forwardDeposits() { - const chainsWithoutForwarding = [Blockchain.BITCOIN, ...this.chainsWithoutPaymentBalance]; + const chainsWithoutForwarding = [Blockchain.BITCOIN, Blockchain.FIRO, ...this.chainsWithoutPaymentBalance]; const paymentAssets = await this.assetService .getPaymentAssets() diff --git a/src/subdomains/core/payment-link/services/payment-link-fee.service.ts b/src/subdomains/core/payment-link/services/payment-link-fee.service.ts index cafd96211d..acc4a79a4b 100644 --- a/src/subdomains/core/payment-link/services/payment-link-fee.service.ts +++ b/src/subdomains/core/payment-link/services/payment-link-fee.service.ts @@ -8,6 +8,7 @@ import { Process } from 'src/shared/services/process.service'; import { DfxCron } from 'src/shared/utils/cron'; import { Util } from 'src/shared/utils/util'; import { PayoutBitcoinService } from 'src/subdomains/supporting/payout/services/payout-bitcoin.service'; +import { PayoutFiroService } from 'src/subdomains/supporting/payout/services/payout-firo.service'; import { BlockchainRegistryService } from '../../../../integration/blockchain/shared/services/blockchain-registry.service'; interface FeeCacheData { @@ -26,6 +27,7 @@ export class PaymentLinkFeeService implements OnModuleInit { constructor( private readonly blockchainRegistryService: BlockchainRegistryService, private readonly payoutBitcoinService: PayoutBitcoinService, + private readonly payoutFiroService: PayoutFiroService, ) { this.feeCache = new Map(); } @@ -37,11 +39,9 @@ export class PaymentLinkFeeService implements OnModuleInit { // --- JOBS --- // @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.UPDATE_BLOCKCHAIN_FEE }) async updateFees(): Promise { - if (GetConfig().environment === Environment.LOC) return; - for (const blockchain of PaymentLinkBlockchains) { try { - const fee = await this.calculateFee(blockchain); + const fee = GetConfig().environment === Environment.LOC ? 0 : await this.calculateFee(blockchain); this.feeCache.set(blockchain, { timestamp: new Date(), fee, @@ -78,6 +78,9 @@ export class PaymentLinkFeeService implements OnModuleInit { case Blockchain.BITCOIN: return this.payoutBitcoinService.getCurrentFeeRate(); + + case Blockchain.FIRO: + return this.payoutFiroService.getCurrentFeeRate(); } } diff --git a/src/subdomains/core/payment-link/services/payment-quote.service.ts b/src/subdomains/core/payment-link/services/payment-quote.service.ts index 5ee9ce6473..0d113fe15c 100644 --- a/src/subdomains/core/payment-link/services/payment-quote.service.ts +++ b/src/subdomains/core/payment-link/services/payment-quote.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Config } from 'src/config/config'; -import { BitcoinNodeType } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; +import { BitcoinBasedClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client'; +import { BitcoinNodeType } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { BlockchainRegistryService } from 'src/integration/blockchain/shared/services/blockchain-registry.service'; import { LightningHelper } from 'src/integration/lightning/lightning-helper'; @@ -38,8 +39,9 @@ export class PaymentQuoteService { Blockchain.GNOSIS, Blockchain.ETHEREUM, Blockchain.BINANCE_SMART_CHAIN, - Blockchain.MONERO, Blockchain.BITCOIN, + Blockchain.FIRO, + Blockchain.MONERO, Blockchain.ZANO, Blockchain.SOLANA, Blockchain.TRON, @@ -379,7 +381,8 @@ export class PaymentQuoteService { break; case Blockchain.BITCOIN: - await this.doBitcoinHexPayment(transferInfo.method, transferInfo, quote); + case Blockchain.FIRO: + await this.doBitcoinBasedHexPayment(transferInfo.method, transferInfo, quote); break; case Blockchain.MONERO: @@ -449,19 +452,22 @@ export class PaymentQuoteService { } } - private async doBitcoinHexPayment( + private async doBitcoinBasedHexPayment( method: Blockchain, transferInfo: TransferInfo, quote: PaymentQuote, ): Promise { try { - const transferAmount = quote.getTransferAmount(Blockchain.BITCOIN); + const transferAmount = quote.getTransferAmount(method); if (!transferAmount) { - quote.txFailed(`Quote ${quote.uniqueId}: No transfer amount for Bitcoin hex payment`); + quote.txFailed(`Quote ${quote.uniqueId}: No transfer amount for ${method} hex payment`); return; } - const client = this.blockchainRegistryService.getBitcoinClient(method, BitcoinNodeType.BTC_OUTPUT); + const client: BitcoinBasedClient = + method === Blockchain.BITCOIN + ? this.blockchainRegistryService.getBitcoinClient(method, BitcoinNodeType.BTC_OUTPUT) + : (this.blockchainRegistryService.getClient(method) as BitcoinBasedClient); const testMempoolResults = await client.testMempoolAccept(transferInfo.hex); diff --git a/src/subdomains/core/referral/reward/services/ref-reward.service.ts b/src/subdomains/core/referral/reward/services/ref-reward.service.ts index 98357b957e..9a9be6fcad 100644 --- a/src/subdomains/core/referral/reward/services/ref-reward.service.ts +++ b/src/subdomains/core/referral/reward/services/ref-reward.service.ts @@ -27,6 +27,7 @@ const PayoutLimits: { [k in Blockchain]: number } = { [Blockchain.BITCOIN]: 100, [Blockchain.LIGHTNING]: 1, [Blockchain.SPARK]: 1, + [Blockchain.FIRO]: undefined, [Blockchain.MONERO]: 1, [Blockchain.ZANO]: undefined, [Blockchain.CARDANO]: undefined, diff --git a/src/subdomains/generic/user/models/user/user.enum.ts b/src/subdomains/generic/user/models/user/user.enum.ts index 63ded2b959..5fe5bce9e2 100644 --- a/src/subdomains/generic/user/models/user/user.enum.ts +++ b/src/subdomains/generic/user/models/user/user.enum.ts @@ -14,6 +14,7 @@ export enum UserAddressType { LND_HUB = 'LNDHUB', UMA = 'UMA', SPARK = 'Spark', + FIRO = 'Firo', MONERO = 'Monero', LIQUID = 'Liquid', ARWEAVE = 'Arweave', diff --git a/src/subdomains/supporting/address-pool/deposit/deposit.service.ts b/src/subdomains/supporting/address-pool/deposit/deposit.service.ts index d485bcc0ed..53b0b38360 100644 --- a/src/subdomains/supporting/address-pool/deposit/deposit.service.ts +++ b/src/subdomains/supporting/address-pool/deposit/deposit.service.ts @@ -3,8 +3,10 @@ import { Config } from 'src/config/config'; import { AlchemyNetworkMapper } from 'src/integration/alchemy/alchemy-network-mapper'; import { AlchemyWebhookService } from 'src/integration/alchemy/services/alchemy-webhook.service'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { CardanoUtil } from 'src/integration/blockchain/cardano/cardano.util'; +import { FiroClient } from 'src/integration/blockchain/firo/firo-client'; +import { FiroService } from 'src/integration/blockchain/firo/services/firo.service'; import { MoneroClient } from 'src/integration/blockchain/monero/monero-client'; import { MoneroService } from 'src/integration/blockchain/monero/services/monero.service'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; @@ -29,6 +31,7 @@ import { CreateDepositDto } from './dto/create-deposit.dto'; export class DepositService { private readonly bitcoinClient: BitcoinClient; private readonly lightningClient: LightningClient; + private readonly firoClient: FiroClient; private readonly moneroClient: MoneroClient; constructor( @@ -37,10 +40,12 @@ export class DepositService { private readonly tatumWebhookService: TatumWebhookService, bitcoinService: BitcoinService, lightningService: LightningService, + firoService: FiroService, moneroService: MoneroService, ) { this.bitcoinClient = bitcoinService.getDefaultClient(BitcoinNodeType.BTC_INPUT); this.lightningClient = lightningService.getDefaultClient(); + this.firoClient = firoService.getDefaultClient(); this.moneroClient = moneroService.getDefaultClient(); } @@ -87,6 +92,8 @@ export class DepositService { return this.createEvmDeposits(blockchain, count); } else if (blockchain === Blockchain.LIGHTNING) { return this.createLightningDeposits(blockchain, count); + } else if (blockchain === Blockchain.FIRO) { + return this.createFiroDeposits(blockchain, count); } else if (blockchain === Blockchain.MONERO) { return this.createMoneroDeposits(blockchain, count); } else if (blockchain === Blockchain.ZANO) { @@ -197,6 +204,17 @@ export class DepositService { } } + private async createFiroDeposits(blockchain: Blockchain, count: number): Promise { + const client = this.firoClient; + const label = Util.isoDate(new Date()); + + for (let i = 0; i < count; i++) { + const address = await client.createAddress(label, 'legacy'); + const deposit = Deposit.create(address, [blockchain]); + await this.depositRepo.save(deposit); + } + } + private async createMoneroDeposits(blockchain: Blockchain, count: number): Promise { for (let i = 0; i < count; i++) { const moneroAddress = await this.moneroClient.createAddress(); diff --git a/src/subdomains/supporting/dex/dex.module.ts b/src/subdomains/supporting/dex/dex.module.ts index 7d3b9c6751..6725d2d342 100644 --- a/src/subdomains/supporting/dex/dex.module.ts +++ b/src/subdomains/supporting/dex/dex.module.ts @@ -9,13 +9,14 @@ import { LiquidityOrderFactory } from './factories/liquidity-order.factory'; import { LiquidityOrderRepository } from './repositories/liquidity-order.repository'; import { DexArbitrumService } from './services/dex-arbitrum.service'; import { DexBaseService } from './services/dex-base.service'; -import { DexBitcoinService } from './services/dex-bitcoin.service'; import { DexBitcoinTestnet4Service } from './services/dex-bitcoin-testnet4.service'; +import { DexBitcoinService } from './services/dex-bitcoin.service'; import { DexBscService } from './services/dex-bsc.service'; import { DexCardanoService } from './services/dex-cardano.service'; -import { DexCitreaService } from './services/dex-citrea.service'; import { DexCitreaTestnetService } from './services/dex-citrea-testnet.service'; +import { DexCitreaService } from './services/dex-citrea.service'; import { DexEthereumService } from './services/dex-ethereum.service'; +import { DexFiroService } from './services/dex-firo.service'; import { DexGnosisService } from './services/dex-gnosis.service'; import { DexLightningService } from './services/dex-lightning.service'; import { DexMoneroService } from './services/dex-monero.service'; @@ -31,18 +32,19 @@ import { ArbitrumTokenStrategy as ArbitrumTokenStrategyCL } from './strategies/c import { BaseCoinStrategy as BaseCoinStrategyCL } from './strategies/check-liquidity/impl/base-coin.strategy'; import { BaseTokenStrategy as BaseTokenStrategyCL } from './strategies/check-liquidity/impl/base-token.strategy'; import { CheckLiquidityStrategyRegistry } from './strategies/check-liquidity/impl/base/check-liquidity.strategy-registry'; -import { BitcoinStrategy as BitcoinStrategyCL } from './strategies/check-liquidity/impl/bitcoin.strategy'; import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyCL } from './strategies/check-liquidity/impl/bitcoin-testnet4.strategy'; +import { BitcoinStrategy as BitcoinStrategyCL } from './strategies/check-liquidity/impl/bitcoin.strategy'; import { BscCoinStrategy as BscCoinStrategyCL } from './strategies/check-liquidity/impl/bsc-coin.strategy'; import { BscTokenStrategy as BscTokenStrategyCL } from './strategies/check-liquidity/impl/bsc-token.strategy'; import { CardanoCoinStrategy as CardanoCoinStrategyCL } from './strategies/check-liquidity/impl/cardano-coin.strategy'; import { CardanoTokenStrategy as CardanoTokenStrategyCL } from './strategies/check-liquidity/impl/cardano-token.strategy'; import { CitreaCoinStrategy as CitreaCoinStrategyCL } from './strategies/check-liquidity/impl/citrea-coin.strategy'; -import { CitreaTokenStrategy as CitreaTokenStrategyCL } from './strategies/check-liquidity/impl/citrea-token.strategy'; import { CitreaTestnetCoinStrategy as CitreaTestnetCoinStrategyCL } from './strategies/check-liquidity/impl/citrea-testnet-coin.strategy'; import { CitreaTestnetTokenStrategy as CitreaTestnetTokenStrategyCL } from './strategies/check-liquidity/impl/citrea-testnet-token.strategy'; +import { CitreaTokenStrategy as CitreaTokenStrategyCL } from './strategies/check-liquidity/impl/citrea-token.strategy'; import { EthereumCoinStrategy as EthereumCoinStrategyCL } from './strategies/check-liquidity/impl/ethereum-coin.strategy'; import { EthereumTokenStrategy as EthereumTokenStrategyCL } from './strategies/check-liquidity/impl/ethereum-token.strategy'; +import { FiroCoinStrategy as FiroCoinStrategyCL } from './strategies/check-liquidity/impl/firo-coin.strategy'; import { GnosisCoinStrategy as GnosisCoinStrategyCL } from './strategies/check-liquidity/impl/gnosis-coin.strategy'; import { GnosisTokenStrategy as GnosisTokenStrategyCL } from './strategies/check-liquidity/impl/gnosis-token.strategy'; import { LightningStrategy as LightningStrategyCL } from './strategies/check-liquidity/impl/lightning.strategy'; @@ -64,18 +66,19 @@ import { ArbitrumTokenStrategy as ArbitrumTokenStrategyPL } from './strategies/p import { BaseCoinStrategy as BaseCoinStrategyPL } from './strategies/purchase-liquidity/impl/base-coin.strategy'; import { BaseTokenStrategy as BaseTokenStrategyPL } from './strategies/purchase-liquidity/impl/base-token.strategy'; import { PurchaseLiquidityStrategyRegistry } from './strategies/purchase-liquidity/impl/base/purchase-liquidity.strategy-registry'; -import { BitcoinStrategy as BitcoinStrategyPL } from './strategies/purchase-liquidity/impl/bitcoin.strategy'; import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPL } from './strategies/purchase-liquidity/impl/bitcoin-testnet4.strategy'; +import { BitcoinStrategy as BitcoinStrategyPL } from './strategies/purchase-liquidity/impl/bitcoin.strategy'; import { BscCoinStrategy as BscCoinStrategyPL } from './strategies/purchase-liquidity/impl/bsc-coin.strategy'; import { BscTokenStrategy as BscTokenStrategyPL } from './strategies/purchase-liquidity/impl/bsc-token.strategy'; import { CardanoCoinStrategy as CardanoCoinStrategyPL } from './strategies/purchase-liquidity/impl/cardano-coin.strategy'; import { CardanoTokenStrategy as CardanoTokenStrategyPL } from './strategies/purchase-liquidity/impl/cardano-token.strategy'; import { CitreaCoinStrategy as CitreaCoinStrategyPL } from './strategies/purchase-liquidity/impl/citrea-coin.strategy'; -import { CitreaTokenStrategy as CitreaTokenStrategyPL } from './strategies/purchase-liquidity/impl/citrea-token.strategy'; import { CitreaTestnetCoinStrategy as CitreaTestnetCoinStrategyPL } from './strategies/purchase-liquidity/impl/citrea-testnet-coin.strategy'; import { CitreaTestnetTokenStrategy as CitreaTestnetTokenStrategyPL } from './strategies/purchase-liquidity/impl/citrea-testnet-token.strategy'; +import { CitreaTokenStrategy as CitreaTokenStrategyPL } from './strategies/purchase-liquidity/impl/citrea-token.strategy'; import { EthereumCoinStrategy as EthereumCoinStrategyPL } from './strategies/purchase-liquidity/impl/ethereum-coin.strategy'; import { EthereumTokenStrategy as EthereumTokenStrategyPL } from './strategies/purchase-liquidity/impl/ethereum-token.strategy'; +import { FiroStrategy as FiroStrategyPL } from './strategies/purchase-liquidity/impl/firo.strategy'; import { GnosisCoinStrategy as GnosisCoinStrategyPL } from './strategies/purchase-liquidity/impl/gnosis-coin.strategy'; import { GnosisTokenStrategy as GnosisTokenStrategyPL } from './strategies/purchase-liquidity/impl/gnosis-token.strategy'; import { MoneroStrategy as MoneroStrategyPL } from './strategies/purchase-liquidity/impl/monero.strategy'; @@ -96,18 +99,19 @@ import { ArbitrumTokenStrategy as ArbitrumTokenStrategySL } from './strategies/s import { BaseCoinStrategy as BaseCoinStrategySL } from './strategies/sell-liquidity/impl/base-coin.strategy'; import { BaseTokenStrategy as BaseTokenStrategySL } from './strategies/sell-liquidity/impl/base-token.strategy'; import { SellLiquidityStrategyRegistry } from './strategies/sell-liquidity/impl/base/sell-liquidity.strategy-registry'; -import { BitcoinStrategy as BitcoinStrategySL } from './strategies/sell-liquidity/impl/bitcoin.strategy'; import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategySL } from './strategies/sell-liquidity/impl/bitcoin-testnet4.strategy'; +import { BitcoinStrategy as BitcoinStrategySL } from './strategies/sell-liquidity/impl/bitcoin.strategy'; import { BscCoinStrategy as BscCoinStrategySL } from './strategies/sell-liquidity/impl/bsc-coin.strategy'; import { BscTokenStrategy as BscTokenStrategySL } from './strategies/sell-liquidity/impl/bsc-token.strategy'; import { CardanoCoinStrategy as CardanoCoinStrategySL } from './strategies/sell-liquidity/impl/cardano-coin.strategy'; import { CardanoTokenStrategy as CardanoTokenStrategySL } from './strategies/sell-liquidity/impl/cardano-token.strategy'; import { CitreaCoinStrategy as CitreaCoinStrategySL } from './strategies/sell-liquidity/impl/citrea-coin.strategy'; -import { CitreaTokenStrategy as CitreaTokenStrategySL } from './strategies/sell-liquidity/impl/citrea-token.strategy'; import { CitreaTestnetCoinStrategy as CitreaTestnetCoinStrategySL } from './strategies/sell-liquidity/impl/citrea-testnet-coin.strategy'; import { CitreaTestnetTokenStrategy as CitreaTestnetTokenStrategySL } from './strategies/sell-liquidity/impl/citrea-testnet-token.strategy'; +import { CitreaTokenStrategy as CitreaTokenStrategySL } from './strategies/sell-liquidity/impl/citrea-token.strategy'; import { EthereumCoinStrategy as EthereumCoinStrategySL } from './strategies/sell-liquidity/impl/ethereum-coin.strategy'; import { EthereumTokenStrategy as EthereumTokenStrategySL } from './strategies/sell-liquidity/impl/ethereum-token.strategy'; +import { FiroStrategy as FiroStrategySL } from './strategies/sell-liquidity/impl/firo.strategy'; import { GnosisCoinStrategy as GnosisCoinStrategySL } from './strategies/sell-liquidity/impl/gnosis-coin.strategy'; import { GnosisTokenStrategy as GnosisTokenStrategySL } from './strategies/sell-liquidity/impl/gnosis-token.strategy'; import { MoneroStrategy as MoneroStrategySL } from './strategies/sell-liquidity/impl/monero.strategy'; @@ -126,13 +130,14 @@ import { ZanoTokenStrategy as ZanoTokenStrategySL } from './strategies/sell-liqu import { ArbitrumStrategy as ArbitrumStrategyS } from './strategies/supplementary/impl/arbitrum.strategy'; import { BaseStrategy as BaseStrategyS } from './strategies/supplementary/impl/base.strategy'; import { SupplementaryStrategyRegistry } from './strategies/supplementary/impl/base/supplementary.strategy-registry'; -import { BitcoinStrategy as BitcoinStrategyS } from './strategies/supplementary/impl/bitcoin.strategy'; import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyS } from './strategies/supplementary/impl/bitcoin-testnet4.strategy'; +import { BitcoinStrategy as BitcoinStrategyS } from './strategies/supplementary/impl/bitcoin.strategy'; import { BscStrategy as BscStrategyS } from './strategies/supplementary/impl/bsc.strategy'; import { CardanoStrategy as CardanoStrategyS } from './strategies/supplementary/impl/cardano.strategy'; -import { CitreaStrategy as CitreaStrategyS } from './strategies/supplementary/impl/citrea.strategy'; import { CitreaTestnetStrategy as CitreaTestnetStrategyS } from './strategies/supplementary/impl/citrea-testnet.strategy'; +import { CitreaStrategy as CitreaStrategyS } from './strategies/supplementary/impl/citrea.strategy'; import { EthereumStrategy as EthereumStrategyS } from './strategies/supplementary/impl/ethereum.strategy'; +import { FiroStrategy as FiroStrategyS } from './strategies/supplementary/impl/firo.strategy'; import { GnosisStrategy as GnosisStrategyS } from './strategies/supplementary/impl/gnosis.strategy'; import { MoneroStrategy as MoneroStrategyS } from './strategies/supplementary/impl/monero.strategy'; import { OptimismStrategy as OptimismStrategyS } from './strategies/supplementary/impl/optimism.strategy'; @@ -162,6 +167,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z DexCitreaService, DexCitreaTestnetService, DexLightningService, + DexFiroService, DexMoneroService, DexZanoService, DexSolanaService, @@ -178,6 +184,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z BitcoinStrategyCL, BitcoinTestnet4StrategyCL, LightningStrategyCL, + FiroCoinStrategyCL, MoneroStrategyCL, ZanoCoinStrategyCL, ZanoTokenStrategyCL, @@ -207,6 +214,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z BscCoinStrategyPL, BitcoinStrategyPL, BitcoinTestnet4StrategyPL, + FiroStrategyPL, MoneroStrategyPL, ZanoCoinStrategyPL, ZanoTokenStrategyPL, @@ -236,6 +244,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z CardanoTokenStrategyPL, BitcoinStrategySL, BitcoinTestnet4StrategySL, + FiroStrategySL, MoneroStrategySL, ZanoCoinStrategySL, ZanoTokenStrategySL, @@ -268,6 +277,7 @@ import { ZanoStrategy as ZanoStrategyS } from './strategies/supplementary/impl/z ArbitrumStrategyS, BitcoinStrategyS, BitcoinTestnet4StrategyS, + FiroStrategyS, MoneroStrategyS, ZanoStrategyS, BscStrategyS, diff --git a/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts b/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts index 3627dc640f..11c9dbfd2a 100644 --- a/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts +++ b/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { TransactionHistory } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { Util } from 'src/shared/utils/util'; import { LiquidityOrder } from '../entities/liquidity-order.entity'; diff --git a/src/subdomains/supporting/dex/services/dex-firo.service.ts b/src/subdomains/supporting/dex/services/dex-firo.service.ts new file mode 100644 index 0000000000..af2986774e --- /dev/null +++ b/src/subdomains/supporting/dex/services/dex-firo.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { TransactionHistory } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client'; +import { FiroClient } from 'src/integration/blockchain/firo/firo-client'; +import { FiroService } from 'src/integration/blockchain/firo/services/firo.service'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Util } from 'src/shared/utils/util'; +import { LiquidityOrder } from '../entities/liquidity-order.entity'; +import { LiquidityOrderRepository } from '../repositories/liquidity-order.repository'; + +@Injectable() +export class DexFiroService { + private readonly client: FiroClient; + + constructor( + private readonly liquidityOrderRepo: LiquidityOrderRepository, + readonly firoService: FiroService, + ) { + this.client = firoService.getDefaultClient(); + } + + async sendUtxoToMany(payout: { addressTo: string; amount: number }[]): Promise { + const feeRate = await this.getFeeRate(); + return this.client.sendMany(payout, feeRate); + } + + async checkAvailableTargetLiquidity(inputAmount: number): Promise<[number, number]> { + const pendingAmount = await this.getPendingAmount(); + const availableAmount = await this.client.getBalance(); + + return [inputAmount, availableAmount - pendingAmount]; + } + + async checkTransferCompletion(transferTxId: string): Promise { + const transaction = await this.client.getTx(transferTxId); + + return transaction != null; + } + + async getRecentHistory(txCount: number): Promise { + return this.client.getRecentHistory(txCount); + } + + //*** HELPER METHODS ***// + + private async getFeeRate(): Promise { + const feeRate = await this.client.estimateSmartFee(1); + return (feeRate ?? 10) * 1.5; + } + + private async getPendingAmount(): Promise { + const pendingOrders = await this.liquidityOrderRepo.findBy({ + isComplete: false, + targetAsset: { dexName: 'FIRO', blockchain: Blockchain.FIRO }, + }); + + return Util.sumObjValue(pendingOrders, 'estimatedTargetAmount'); + } +} diff --git a/src/subdomains/supporting/dex/strategies/check-liquidity/__tests__/check-liquidity.registry.spec.ts b/src/subdomains/supporting/dex/strategies/check-liquidity/__tests__/check-liquidity.registry.spec.ts index cbea6b61c8..09e743e311 100644 --- a/src/subdomains/supporting/dex/strategies/check-liquidity/__tests__/check-liquidity.registry.spec.ts +++ b/src/subdomains/supporting/dex/strategies/check-liquidity/__tests__/check-liquidity.registry.spec.ts @@ -1,6 +1,6 @@ import { mock } from 'jest-mock-extended'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; +import { BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; import { AssetType } from 'src/shared/models/asset/asset.entity'; @@ -9,6 +9,7 @@ import { DexArbitrumService } from '../../../services/dex-arbitrum.service'; import { DexBaseService } from '../../../services/dex-base.service'; import { DexBitcoinService } from '../../../services/dex-bitcoin.service'; import { DexBscService } from '../../../services/dex-bsc.service'; +import { DexCardanoService } from '../../../services/dex-cardano.service'; import { DexEthereumService } from '../../../services/dex-ethereum.service'; import { DexGnosisService } from '../../../services/dex-gnosis.service'; import { DexLightningService } from '../../../services/dex-lightning.service'; @@ -18,7 +19,6 @@ import { DexPolygonService } from '../../../services/dex-polygon.service'; import { DexSolanaService } from '../../../services/dex-solana.service'; import { DexTronService } from '../../../services/dex-tron.service'; import { DexZanoService } from '../../../services/dex-zano.service'; -import { DexCardanoService } from '../../../services/dex-cardano.service'; import { ArbitrumCoinStrategy } from '../impl/arbitrum-coin.strategy'; import { ArbitrumTokenStrategy } from '../impl/arbitrum-token.strategy'; import { BaseCoinStrategy } from '../impl/base-coin.strategy'; @@ -27,6 +27,8 @@ import { CheckLiquidityStrategyRegistry } from '../impl/base/check-liquidity.str import { BitcoinStrategy } from '../impl/bitcoin.strategy'; import { BscCoinStrategy } from '../impl/bsc-coin.strategy'; import { BscTokenStrategy } from '../impl/bsc-token.strategy'; +import { CardanoCoinStrategy } from '../impl/cardano-coin.strategy'; +import { CardanoTokenStrategy } from '../impl/cardano-token.strategy'; import { EthereumCoinStrategy } from '../impl/ethereum-coin.strategy'; import { EthereumTokenStrategy } from '../impl/ethereum-token.strategy'; import { GnosisCoinStrategy } from '../impl/gnosis-coin.strategy'; @@ -43,8 +45,6 @@ import { TronCoinStrategy } from '../impl/tron-coin.strategy'; import { TronTokenStrategy } from '../impl/tron-token.strategy'; import { ZanoCoinStrategy } from '../impl/zano-coin.strategy'; import { ZanoTokenStrategy } from '../impl/zano-token.strategy'; -import { CardanoCoinStrategy } from '../impl/cardano-coin.strategy'; -import { CardanoTokenStrategy } from '../impl/cardano-token.strategy'; describe('CheckLiquidityStrategies', () => { let bitcoinService: BitcoinService; diff --git a/src/subdomains/supporting/dex/strategies/check-liquidity/impl/firo-coin.strategy.ts b/src/subdomains/supporting/dex/strategies/check-liquidity/impl/firo-coin.strategy.ts new file mode 100644 index 0000000000..b2bf8b62c0 --- /dev/null +++ b/src/subdomains/supporting/dex/strategies/check-liquidity/impl/firo-coin.strategy.ts @@ -0,0 +1,54 @@ +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 { DexFiroService } from '../../../services/dex-firo.service'; +import { CheckLiquidityUtil } from '../utils/check-liquidity.util'; +import { CheckLiquidityStrategy } from './base/check-liquidity.strategy'; + +@Injectable() +export class FiroCoinStrategy extends CheckLiquidityStrategy { + constructor( + protected readonly assetService: AssetService, + private readonly dexFiroService: DexFiroService, + ) { + super(); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + get assetType(): AssetType { + return undefined; + } + + get assetCategory(): AssetCategory { + return undefined; + } + + async checkLiquidity(request: CheckLiquidityRequest): Promise { + const { context, correlationId, referenceAsset, referenceAmount: firoAmount } = request; + + if (referenceAsset.dexName === 'FIRO') { + const [targetAmount, availableAmount] = await this.dexFiroService.checkAvailableTargetLiquidity(firoAmount); + + return CheckLiquidityUtil.createNonPurchasableCheckLiquidityResult( + request, + targetAmount, + availableAmount, + await this.feeAsset(), + ); + } + + // only native coin is enabled as a referenceAsset + throw new Error( + `Only native coin reference is supported by Firo CheckLiquidity strategy. Provided reference asset: ${referenceAsset.dexName} Context: ${context}. CorrelationID: ${correlationId}`, + ); + } + + protected getFeeAsset(): Promise { + return this.assetService.getFiroCoin(); + } +} diff --git a/src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/firo.strategy.ts b/src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/firo.strategy.ts new file mode 100644 index 0000000000..e4c20221c2 --- /dev/null +++ b/src/subdomains/supporting/dex/strategies/purchase-liquidity/impl/firo.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 FiroStrategy extends NoPurchaseStrategy { + protected readonly logger = new DfxLogger(FiroStrategy); + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + get assetType(): AssetType { + return undefined; + } + + get assetCategory(): AssetCategory { + return undefined; + } + + get dexName(): string { + return undefined; + } + + protected getFeeAsset(): Promise { + return this.assetService.getFiroCoin(); + } +} diff --git a/src/subdomains/supporting/dex/strategies/sell-liquidity/impl/firo.strategy.ts b/src/subdomains/supporting/dex/strategies/sell-liquidity/impl/firo.strategy.ts new file mode 100644 index 0000000000..a0d696f742 --- /dev/null +++ b/src/subdomains/supporting/dex/strategies/sell-liquidity/impl/firo.strategy.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; +import { AssetService } from 'src/shared/models/asset/asset.service'; +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { SellLiquidityStrategy } from './base/sell-liquidity.strategy'; + +@Injectable() +export class FiroStrategy extends SellLiquidityStrategy { + protected readonly logger = new DfxLogger(FiroStrategy); + + constructor(protected readonly assetService: AssetService) { + super(); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + get assetType(): AssetType { + return undefined; + } + + sellLiquidity(): Promise { + throw new Error('Selling liquidity on DEX is not supported for firo'); + } + + addSellData(): Promise { + throw new Error('Selling liquidity on DEX is not supported for firo'); + } + + protected getFeeAsset(): Promise { + return this.assetService.getFiroCoin(); + } +} diff --git a/src/subdomains/supporting/dex/strategies/supplementary/impl/firo.strategy.ts b/src/subdomains/supporting/dex/strategies/supplementary/impl/firo.strategy.ts new file mode 100644 index 0000000000..6e10a30ec5 --- /dev/null +++ b/src/subdomains/supporting/dex/strategies/supplementary/impl/firo.strategy.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import { TransactionHistory } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset } from 'src/shared/models/asset/asset.entity'; +import { Util } from 'src/shared/utils/util'; +import { TransactionQuery, TransactionResult, TransferRequest } from '../../../interfaces'; +import { DexFiroService } from '../../../services/dex-firo.service'; +import { SupplementaryStrategy } from './base/supplementary.strategy'; + +@Injectable() +export class FiroStrategy extends SupplementaryStrategy { + constructor(protected readonly dexFiroService: DexFiroService) { + super(); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + async transferLiquidity(request: TransferRequest): Promise { + const { destinationAddress, amount } = request; + + return this.dexFiroService.sendUtxoToMany([{ addressTo: destinationAddress, amount }]); + } + + async checkTransferCompletion(transferTxId: string): Promise { + return this.dexFiroService.checkTransferCompletion(transferTxId); + } + + async findTransaction(query: TransactionQuery): Promise { + const { amount, since } = query; + + const allHistory = await this.dexFiroService.getRecentHistory(100); + const relevantHistory = this.filterRelevantHistory(allHistory, since); + const targetEntry = relevantHistory.find((e) => e.amount === amount); + + if (!targetEntry) return { isComplete: false }; + + return { isComplete: true, txId: targetEntry.txid }; + } + + async getTargetAmount(_a: number, _f: Asset, _t: Asset): Promise { + throw new Error(`Swapping is not implemented on ${this.blockchain}`); + } + + //*** HELPER METHODS ***// + + private filterRelevantHistory(allHistory: TransactionHistory[], since: Date): TransactionHistory[] { + return allHistory.filter((h) => Util.round(h.blocktime * 1000, 0) > since.getTime()); + } +} diff --git a/src/subdomains/supporting/payin/payin.module.ts b/src/subdomains/supporting/payin/payin.module.ts index bde0382cae..cd61fcbf1f 100644 --- a/src/subdomains/supporting/payin/payin.module.ts +++ b/src/subdomains/supporting/payin/payin.module.ts @@ -21,9 +21,11 @@ import { PayInArbitrumService } from './services/payin-arbitrum.service'; import { PayInBaseService } from './services/payin-base.service'; import { PayInBitcoinService } from './services/payin-bitcoin.service'; import { PayInBscService } from './services/payin-bsc.service'; -import { PayInCitreaService } from './services/payin-citrea.service'; +import { PayInCardanoService } from './services/payin-cardano.service'; import { PayInCitreaTestnetService } from './services/payin-citrea-testnet.service'; +import { PayInCitreaService } from './services/payin-citrea.service'; import { PayInEthereumService } from './services/payin-ethereum.service'; +import { PayInFiroService } from './services/payin-firo.service'; import { PayInGnosisService } from './services/payin-gnosis.service'; import { PayInLightningService } from './services/payin-lightning.service'; import { PayInMoneroService } from './services/payin-monero.service'; @@ -32,7 +34,6 @@ import { PayInOptimismService } from './services/payin-optimism.service'; import { PayInPolygonService } from './services/payin-polygon.service'; import { PayInSepoliaService } from './services/payin-sepolia.service'; import { PayInSolanaService } from './services/payin-solana.service'; -import { PayInCardanoService } from './services/payin-cardano.service'; import { PayInTronService } from './services/payin-tron.service'; import { PayInZanoService } from './services/payin-zano.service'; import { PayInService } from './services/payin.service'; @@ -43,9 +44,10 @@ import { BinancePayStrategy as BinancePayStrategyR } from './strategies/register import { BitcoinStrategy as BitcoinStrategyR } from './strategies/register/impl/bitcoin.strategy'; import { BscStrategy as BscStrategyR } from './strategies/register/impl/bsc.strategy'; import { CardanoStrategy as CardanoStrategyR } from './strategies/register/impl/cardano.strategy'; -import { CitreaStrategy as CitreaStrategyR } from './strategies/register/impl/citrea.strategy'; import { CitreaTestnetStrategy as CitreaTestnetStrategyR } from './strategies/register/impl/citrea-testnet.strategy'; +import { CitreaStrategy as CitreaStrategyR } from './strategies/register/impl/citrea.strategy'; import { EthereumStrategy as EthereumStrategyR } from './strategies/register/impl/ethereum.strategy'; +import { FiroStrategy as FiroStrategyR } from './strategies/register/impl/firo.strategy'; import { GnosisStrategy as GnosisStrategyR } from './strategies/register/impl/gnosis.strategy'; import { KucoinPayStrategy as KucoinPayStrategyR } from './strategies/register/impl/kucoin-pay.strategy'; import { LightningStrategy as LightningStrategyR } from './strategies/register/impl/lightning.strategy'; @@ -68,11 +70,12 @@ import { BscTokenStrategy as BscTokenStrategyS } from './strategies/send/impl/bs import { CardanoCoinStrategy as CardanoCoinStrategyS } from './strategies/send/impl/cardano-coin.strategy'; import { CardanoTokenStrategy as CardanoTokenStrategyS } from './strategies/send/impl/cardano-token.strategy'; import { CitreaCoinStrategy as CitreaCoinStrategyS } from './strategies/send/impl/citrea-coin.strategy'; -import { CitreaTokenStrategy as CitreaTokenStrategyS } from './strategies/send/impl/citrea-token.strategy'; import { CitreaTestnetCoinStrategy as CitreaTestnetCoinStrategyS } from './strategies/send/impl/citrea-testnet-coin.strategy'; import { CitreaTestnetTokenStrategy as CitreaTestnetTokenStrategyS } from './strategies/send/impl/citrea-testnet-token.strategy'; +import { CitreaTokenStrategy as CitreaTokenStrategyS } from './strategies/send/impl/citrea-token.strategy'; import { EthereumCoinStrategy as EthereumCoinStrategyS } from './strategies/send/impl/ethereum-coin.strategy'; import { EthereumTokenStrategy as EthereumTokenStrategyS } from './strategies/send/impl/ethereum-token.strategy'; +import { FiroStrategy as FiroStrategyS } from './strategies/send/impl/firo.strategy'; import { GnosisCoinStrategy as GnosisCoinStrategyS } from './strategies/send/impl/gnosis-coin.strategy'; import { GnosisTokenStrategy as GnosisTokenStrategyS } from './strategies/send/impl/gnosis-token.strategy'; import { KucoinPayStrategy as KucoinPayStrategyS } from './strategies/send/impl/kucoin-pay.strategy'; @@ -116,6 +119,7 @@ import { ZanoTokenStrategy as ZanoTokenStrategyS } from './strategies/send/impl/ PayInNotificationService, PayInBitcoinService, PayInLightningService, + PayInFiroService, PayInMoneroService, PayInZanoService, PayInEthereumService, @@ -137,6 +141,8 @@ import { ZanoTokenStrategy as ZanoTokenStrategyS } from './strategies/send/impl/ BitcoinStrategyS, LightningStrategyR, LightningStrategyS, + FiroStrategyR, + FiroStrategyS, MoneroStrategyR, MoneroStrategyS, ZanoStrategyR, diff --git a/src/subdomains/supporting/payin/services/base/payin-bitcoin-based.service.ts b/src/subdomains/supporting/payin/services/base/payin-bitcoin-based.service.ts index 5da8ce5e25..643a911a12 100644 --- a/src/subdomains/supporting/payin/services/base/payin-bitcoin-based.service.ts +++ b/src/subdomains/supporting/payin/services/base/payin-bitcoin-based.service.ts @@ -1,5 +1,10 @@ import { CryptoInput } from '../../entities/crypto-input.entity'; +export interface UnconfirmedPayInFilterResult { + nextBlockCandidates: CryptoInput[]; + failedPayIns: CryptoInput[]; +} + export abstract class PayInBitcoinBasedService { abstract checkHealthOrThrow(); abstract getBlockHeight(): Promise; diff --git a/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts b/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts index 26644517d2..7a393d8a40 100644 --- a/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts +++ b/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts @@ -1,18 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { InWalletTransaction } from 'src/integration/blockchain/bitcoin/node/node-client'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; import { BitcoinTransaction, BitcoinUTXO } from 'src/integration/blockchain/bitcoin/node/dto/bitcoin-transaction.dto'; +import { InWalletTransaction } from 'src/integration/blockchain/bitcoin/node/node-client'; import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { QueueHandler } from 'src/shared/utils/queue-handler'; import { CryptoInput, PayInStatus } from '../entities/crypto-input.entity'; -import { PayInBitcoinBasedService } from './base/payin-bitcoin-based.service'; - -export interface UnconfirmedPayInFilterResult { - nextBlockCandidates: CryptoInput[]; - failedPayIns: CryptoInput[]; -} +import { PayInBitcoinBasedService, UnconfirmedPayInFilterResult } from './base/payin-bitcoin-based.service'; @Injectable() export class PayInBitcoinService extends PayInBitcoinBasedService { @@ -24,7 +19,7 @@ export class PayInBitcoinService extends PayInBitcoinBasedService { private readonly nodeCallQueue = QueueHandler.createParallelQueueHandler(5); constructor( - readonly bitcoinService: BitcoinService, + bitcoinService: BitcoinService, private readonly feeService: BitcoinFeeService, ) { super(); diff --git a/src/subdomains/supporting/payin/services/payin-firo.service.ts b/src/subdomains/supporting/payin/services/payin-firo.service.ts new file mode 100644 index 0000000000..76f376cbe6 --- /dev/null +++ b/src/subdomains/supporting/payin/services/payin-firo.service.ts @@ -0,0 +1,154 @@ +import { Injectable } from '@nestjs/common'; +import { BitcoinUTXO } from 'src/integration/blockchain/bitcoin/node/dto/bitcoin-transaction.dto'; +import { InWalletTransaction } from 'src/integration/blockchain/bitcoin/node/node-client'; +import { FiroClient } from 'src/integration/blockchain/firo/firo-client'; +import { FiroFeeService } from 'src/integration/blockchain/firo/services/firo-fee.service'; +import { FiroService } from 'src/integration/blockchain/firo/services/firo.service'; +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { QueueHandler } from 'src/shared/utils/queue-handler'; +import { CryptoInput, PayInStatus } from '../entities/crypto-input.entity'; +import { PayInBitcoinBasedService, UnconfirmedPayInFilterResult } from './base/payin-bitcoin-based.service'; + +@Injectable() +export class PayInFiroService extends PayInBitcoinBasedService { + private readonly logger = new DfxLogger(PayInFiroService); + + private readonly client: FiroClient; + + private readonly nodeCallQueue = QueueHandler.createParallelQueueHandler(5); + + constructor( + firoService: FiroService, + private readonly feeService: FiroFeeService, + ) { + super(); + + this.client = firoService.getDefaultClient(); + } + + isAvailable(): boolean { + return this.client != null; + } + + async checkHealthOrThrow(): Promise { + await this.client.checkSync(); + } + + async getBlockHeight(): Promise { + return this.client.getBlockCount(); + } + + async getUtxo(includeUnconfirmed = false): Promise { + const utxos = await this.client.getUtxo(includeUnconfirmed); + + // Firo's getrawtransaction includes address/value directly in vin (not nested in prevout like Bitcoin Core verbosity=2). + // Read vin.address directly - no need to look up each input's previous TX. + await Promise.all( + utxos.map((utxo) => + this.nodeCallQueue.handle(async () => { + const transaction = await this.client.getRawTx(utxo.txid); + if (!transaction) return; + + const senderAddresses = transaction.vin + .filter((vin) => vin.address) + .map((vin) => vin.address); + utxo.prevoutAddresses = [...new Set(senderAddresses)]; + utxo.isUnconfirmed = utxo.confirmations === 0; + }), + ), + ); + + if (includeUnconfirmed) { + const unconfirmedUtxos = utxos.filter((u) => u.isUnconfirmed); + + if (unconfirmedUtxos.length > 0) { + const fastestFee = await this.feeService.getRecommendedFeeRate(); + const txids = unconfirmedUtxos.map((u) => u.txid); + const feeRates = await this.feeService.getTxFeeRates(txids); + + for (const utxo of unconfirmedUtxos) { + const result = feeRates.get(utxo.txid); + if (result?.status === 'unconfirmed' && result.feeRate !== undefined) { + utxo.feeRate = result.feeRate; + utxo.isNextBlockCandidate = result.feeRate >= fastestFee; + } else { + utxo.isNextBlockCandidate = false; + } + } + } + + return utxos.filter((u) => !u.isUnconfirmed || u.isNextBlockCandidate); + } + + return utxos; + } + + async checkTransactionCompletion(txId: string, minConfirmations: number): Promise { + return this.client.isTxComplete(txId, minConfirmations); + } + + async getTx(outTxId: string): Promise { + return this.client.getTx(outTxId); + } + + async sendTransfer(input: CryptoInput): Promise<{ outTxId: string; feeAmount: number }> { + const feeRate = await this.feeService.getRecommendedFeeRate(); + return this.client.send( + input.destinationAddress.address, + input.inTxId, + input.sendingAmount, + input.txSequence, + feeRate, + ); + } + + async filterUnconfirmedPayInsForForward(payIns: CryptoInput[]): Promise { + if (payIns.length === 0) { + return { nextBlockCandidates: [], failedPayIns: [] }; + } + + const fastestFee = await this.feeService.getRecommendedFeeRate(); + const txids = payIns.map((p) => p.inTxId); + const feeRates = await this.feeService.getTxFeeRates(txids); + + const nextBlockCandidates: CryptoInput[] = []; + const failedPayIns: CryptoInput[] = []; + + for (const payIn of payIns) { + const result = feeRates.get(payIn.inTxId); + + if (!result) { + this.logger.warn(`PayIn ${payIn.id}: No fee rate result for TX ${payIn.inTxId} - skipping`); + continue; + } + + switch (result.status) { + case 'not_found': + this.logger.warn(`PayIn ${payIn.id}: TX ${payIn.inTxId} not in mempool - marking as FAILED`); + payIn.status = PayInStatus.FAILED; + failedPayIns.push(payIn); + break; + + case 'confirmed': + this.logger.verbose(`PayIn ${payIn.id}: TX ${payIn.inTxId} already confirmed - skipping unconfirmed forward`); + break; + + case 'error': + this.logger.warn(`PayIn ${payIn.id}: API error checking TX ${payIn.inTxId} - skipping`); + break; + + case 'unconfirmed': + if (result.feeRate !== undefined && result.feeRate >= fastestFee) { + nextBlockCandidates.push(payIn); + } else { + this.logger.verbose( + `PayIn ${payIn.id}: Fee rate ${result.feeRate} < ${fastestFee} - waiting for confirmation`, + ); + } + break; + } + } + + return { nextBlockCandidates, failedPayIns }; + } +} diff --git a/src/subdomains/supporting/payin/services/payin.service.ts b/src/subdomains/supporting/payin/services/payin.service.ts index 1dc932bcb7..247e99f59a 100644 --- a/src/subdomains/supporting/payin/services/payin.service.ts +++ b/src/subdomains/supporting/payin/services/payin.service.ts @@ -31,6 +31,7 @@ import { RegisterStrategyRegistry } from '../strategies/register/impl/base/regis import { SendType } from '../strategies/send/impl/base/send.strategy'; import { SendStrategyRegistry } from '../strategies/send/impl/base/send.strategy-registry'; import { PayInBitcoinService } from './payin-bitcoin.service'; +import { PayInFiroService } from './payin-firo.service'; @Injectable() export class PayInService { @@ -43,6 +44,7 @@ export class PayInService { private readonly transactionService: TransactionService, private readonly paymentLinkPaymentService: PaymentLinkPaymentService, private readonly payInBitcoinService: PayInBitcoinService, + private readonly payInFiroService: PayInFiroService, ) {} // --- PUBLIC API --- // @@ -313,13 +315,13 @@ export class PayInService { } private async getUnconfirmedNextBlockPayIns(): Promise { - if (!Config.blockchain.default.allowUnconfirmedUtxos) return []; - if (!this.payInBitcoinService.isAvailable()) { - this.logger.warn('Bitcoin service not available - skipping unconfirmed UTXO processing'); - return []; - } + const chains = [ + { blockchain: Blockchain.BITCOIN, enabled: Config.blockchain.default.allowUnconfirmedUtxos, service: this.payInBitcoinService }, + { blockchain: Blockchain.FIRO, enabled: Config.blockchain.firo.allowUnconfirmedUtxos, service: this.payInFiroService }, + ].filter((c) => c.enabled && c.service.isAvailable()); + + if (chains.length === 0) return []; - // Only Bitcoin supports unconfirmed UTXO forwarding const candidates = await this.payInRepository.find({ where: { status: In([PayInStatus.ACKNOWLEDGED, PayInStatus.PREPARING, PayInStatus.PREPARED]), @@ -331,26 +333,30 @@ export class PayInService { relations: { buyCrypto: true, buyFiat: true, asset: true }, }); - // Filter to Bitcoin only - const bitcoinCandidates = candidates.filter((p) => p.asset?.blockchain === Blockchain.BITCOIN); + if (candidates.length === 0) return []; - if (bitcoinCandidates.length === 0) return []; + const allNextBlockCandidates: CryptoInput[] = []; + const allFailedPayIns: CryptoInput[] = []; - // Delegate to Bitcoin service for fee-rate filtering - try { - const { nextBlockCandidates, failedPayIns } = - await this.payInBitcoinService.filterUnconfirmedPayInsForForward(bitcoinCandidates); + for (const { blockchain, service } of chains) { + const chainCandidates = candidates.filter((p) => p.asset?.blockchain === blockchain); + if (chainCandidates.length === 0) continue; - // Persist failed PayIns - if (failedPayIns.length > 0) { - await this.payInRepository.save(failedPayIns); + try { + const { nextBlockCandidates, failedPayIns } = + await service.filterUnconfirmedPayInsForForward(chainCandidates); + allNextBlockCandidates.push(...nextBlockCandidates); + allFailedPayIns.push(...failedPayIns); + } catch (e) { + this.logger.error(`Failed to filter unconfirmed ${blockchain} PayIns:`, e); } + } - return nextBlockCandidates; - } catch (e) { - this.logger.error('Failed to filter unconfirmed PayIns:', e); - return []; + if (allFailedPayIns.length > 0) { + await this.payInRepository.save(allFailedPayIns); } + + return allNextBlockCandidates; } private async checkOutputConfirmations(): Promise { diff --git a/src/subdomains/supporting/payin/strategies/register/impl/firo.strategy.ts b/src/subdomains/supporting/payin/strategies/register/impl/firo.strategy.ts new file mode 100644 index 0000000000..cfe6551888 --- /dev/null +++ b/src/subdomains/supporting/payin/strategies/register/impl/firo.strategy.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { CronExpression } from '@nestjs/schedule'; +import { Config } from 'src/config/config'; +import { BitcoinUTXO } from 'src/integration/blockchain/bitcoin/node/dto/bitcoin-transaction.dto'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +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 { DepositService } from 'src/subdomains/supporting/address-pool/deposit/deposit.service'; +import { PayInType } from '../../../entities/crypto-input.entity'; +import { PayInEntry } from '../../../interfaces'; +import { PayInFiroService } from '../../../services/payin-firo.service'; +import { PollingStrategy } from './base/polling.strategy'; + +@Injectable() +export class FiroStrategy extends PollingStrategy { + protected readonly logger = new DfxLogger(FiroStrategy); + + @Inject() private readonly depositService: DepositService; + + private unavailableWarningLogged = false; + + constructor(private readonly payInFiroService: PayInFiroService) { + super(); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + //*** JOBS ***// + @DfxCron(CronExpression.EVERY_SECOND, { process: Process.PAY_IN, timeout: 7200 }) + async checkPayInEntries(): Promise { + if (!this.payInFiroService.isAvailable()) { + if (!this.unavailableWarningLogged) { + this.logger.warn('Firo node not configured - skipping checkPayInEntries'); + this.unavailableWarningLogged = true; + } + return; + } + + return super.checkPayInEntries(); + } + + //*** HELPER METHODS ***// + protected async getBlockHeight(): Promise { + return this.payInFiroService.getBlockHeight(); + } + + protected async getPayInAddresses(): Promise { + const deposits = await this.depositService.getUsedDepositsByBlockchain(this.blockchain); + + const addresses = deposits.map((dr) => dr.address); + addresses.push(Config.payment.firoAddress); + + return addresses; + } + + protected async processNewPayInEntries(): Promise { + const log = this.createNewLogObject(); + const newEntries = await this.getNewEntries(); + + await this.createPayInsAndSave(newEntries, log); + + this.printInputLog(log, 'omitted', Blockchain.FIRO); + } + + private async getNewEntries(): Promise { + await this.payInFiroService.checkHealthOrThrow(); + + const utxos = await this.payInFiroService.getUtxo(Config.blockchain.firo.allowUnconfirmedUtxos); + + return this.mapUtxosToEntries(utxos); + } + + private async mapUtxosToEntries(utxos: BitcoinUTXO[]): Promise { + const asset = await this.assetService.getFiroCoin(); + const toAddresses = await this.getPayInAddresses(); + + return utxos + .filter((u) => toAddresses.includes(u.address)) + .map((u) => ({ + senderAddresses: u.prevoutAddresses.toString(), + receiverAddress: BlockchainAddress.create(u.address, Blockchain.FIRO), + txId: u.txid, + txType: this.getTxType(u.address), + txSequence: u.vout, + blockHeight: null, + amount: u.amount, + asset, + })); + } + + private getTxType(address: string): PayInType | undefined { + return Util.equalsIgnoreCase(Config.payment.firoAddress, address) ? PayInType.PAYMENT : PayInType.DEPOSIT; + } +} diff --git a/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts b/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts new file mode 100644 index 0000000000..ceed104a02 --- /dev/null +++ b/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { Config } from 'src/config/config'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { 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 { PayInRepository } from '../../../repositories/payin.repository'; +import { PayInFiroService } from '../../../services/payin-firo.service'; +import { BitcoinBasedStrategy } from './base/bitcoin-based.strategy'; + +@Injectable() +export class FiroStrategy extends BitcoinBasedStrategy { + protected readonly logger = new DfxLogger(FiroStrategy); + + constructor( + protected readonly firoService: PayInFiroService, + protected payInRepo: PayInRepository, + ) { + super(firoService, payInRepo); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + get assetType(): AssetType { + return undefined; + } + + get forwardRequired(): boolean { + return false; + } + + protected getForwardAddress(): BlockchainAddress { + return BlockchainAddress.create(Config.blockchain.firo.walletAddress, Blockchain.FIRO); + } + + async checkTransactionCompletion(txId: string, minConfirmations: number): Promise { + return this.firoService.checkTransactionCompletion(txId, minConfirmations); + } +} diff --git a/src/subdomains/supporting/payout/payout.module.ts b/src/subdomains/supporting/payout/payout.module.ts index 4c7e00168e..50f40728a4 100644 --- a/src/subdomains/supporting/payout/payout.module.ts +++ b/src/subdomains/supporting/payout/payout.module.ts @@ -11,12 +11,14 @@ import { PayoutController } from './payout.controller'; import { PayoutOrderRepository } from './repositories/payout-order.repository'; import { PayoutArbitrumService } from './services/payout-arbitrum.service'; import { PayoutBaseService } from './services/payout-base.service'; +import { PayoutBitcoinTestnet4Service } from './services/payout-bitcoin-testnet4.service'; import { PayoutBitcoinService } from './services/payout-bitcoin.service'; import { PayoutBscService } from './services/payout-bsc.service'; import { PayoutCardanoService } from './services/payout-cardano.service'; -import { PayoutCitreaService } from './services/payout-citrea.service'; import { PayoutCitreaTestnetService } from './services/payout-citrea-testnet.service'; +import { PayoutCitreaService } from './services/payout-citrea.service'; import { PayoutEthereumService } from './services/payout-ethereum.service'; +import { PayoutFiroService } from './services/payout-firo.service'; import { PayoutGnosisService } from './services/payout-gnosis.service'; import { PayoutLightningService } from './services/payout-lightning.service'; import { PayoutLogService } from './services/payout-log.service'; @@ -25,27 +27,28 @@ import { PayoutOptimismService } from './services/payout-optimism.service'; import { PayoutPolygonService } from './services/payout-polygon.service'; import { PayoutSepoliaService } from './services/payout-sepolia.service'; import { PayoutSolanaService } from './services/payout-solana.service'; +import { PayoutSparkService } from './services/payout-spark.service'; import { PayoutTronService } from './services/payout-tron.service'; import { PayoutZanoService } from './services/payout-zano.service'; -import { PayoutSparkService } from './services/payout-spark.service'; -import { PayoutBitcoinTestnet4Service } from './services/payout-bitcoin-testnet4.service'; import { PayoutService } from './services/payout.service'; import { ArbitrumCoinStrategy as ArbitrumCoinStrategyPO } from './strategies/payout/impl/arbitrum-coin.strategy'; import { ArbitrumTokenStrategy as ArbitrumTokenStrategyPO } from './strategies/payout/impl/arbitrum-token.strategy'; import { BaseCoinStrategy as BaseCoinStrategyPO } from './strategies/payout/impl/base-coin.strategy'; import { BaseTokenStrategy as BaseTokenStrategyPO } from './strategies/payout/impl/base-token.strategy'; import { PayoutStrategyRegistry } from './strategies/payout/impl/base/payout.strategy-registry'; +import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPO } from './strategies/payout/impl/bitcoin-testnet4.strategy'; import { BitcoinStrategy as BitcoinStrategyPO } from './strategies/payout/impl/bitcoin.strategy'; import { BscCoinStrategy as BscCoinStrategyPO } from './strategies/payout/impl/bsc-coin.strategy'; import { BscTokenStrategy as BscTokenStrategyPO } from './strategies/payout/impl/bsc-token.strategy'; import { CardanoCoinStrategy as CardanoCoinStrategyPO } from './strategies/payout/impl/cardano-coin.strategy'; import { CardanoTokenStrategy as CardanoTokenStrategyPO } from './strategies/payout/impl/cardano-token.strategy'; import { CitreaCoinStrategy as CitreaCoinStrategyPO } from './strategies/payout/impl/citrea-coin.strategy'; -import { CitreaTokenStrategy as CitreaTokenStrategyPO } from './strategies/payout/impl/citrea-token.strategy'; import { CitreaTestnetCoinStrategy as CitreaTestnetCoinStrategyPO } from './strategies/payout/impl/citrea-testnet-coin.strategy'; import { CitreaTestnetTokenStrategy as CitreaTestnetTokenStrategyPO } from './strategies/payout/impl/citrea-testnet-token.strategy'; +import { CitreaTokenStrategy as CitreaTokenStrategyPO } from './strategies/payout/impl/citrea-token.strategy'; import { EthereumCoinStrategy as EthereumCoinStrategyPO } from './strategies/payout/impl/ethereum-coin.strategy'; import { EthereumTokenStrategy as EthereumTokenStrategyPO } from './strategies/payout/impl/ethereum-token.strategy'; +import { FiroStrategy as FiroStrategyPO } from './strategies/payout/impl/firo.strategy'; import { GnosisCoinStrategy as GnosisCoinStrategyPO } from './strategies/payout/impl/gnosis-coin.strategy'; import { GnosisTokenStrategy as GnosisTokenStrategyPO } from './strategies/payout/impl/gnosis-token.strategy'; import { LightningStrategy as LightningStrategyPO } from './strategies/payout/impl/lightning.strategy'; @@ -58,21 +61,22 @@ import { SepoliaCoinStrategy as SepoliaCoinStrategyPO } from './strategies/payou import { SepoliaTokenStrategy as SepoliaTokenStrategyPO } from './strategies/payout/impl/sepolia-token.strategy'; import { SolanaCoinStrategy as SolanaCoinStrategyPO } from './strategies/payout/impl/solana-coin.strategy'; import { SolanaTokenStrategy as SolanaTokenStrategyPO } from './strategies/payout/impl/solana-token.strategy'; +import { SparkStrategy as SparkStrategyPO } from './strategies/payout/impl/spark.strategy'; import { TronCoinStrategy as TronCoinStrategyPO } from './strategies/payout/impl/tron-coin.strategy'; import { TronTokenStrategy as TronTokenStrategyPO } from './strategies/payout/impl/tron-token.strategy'; import { ZanoCoinStrategy as ZanoCoinStrategyPO } from './strategies/payout/impl/zano-coin.strategy'; import { ZanoTokenStrategy as ZanoTokenStrategyPO } from './strategies/payout/impl/zano-token.strategy'; -import { SparkStrategy as SparkStrategyPO } from './strategies/payout/impl/spark.strategy'; -import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPO } from './strategies/payout/impl/bitcoin-testnet4.strategy'; import { ArbitrumStrategy as ArbitrumStrategyPR } from './strategies/prepare/impl/arbitrum.strategy'; import { BaseStrategy as BaseStrategyPR } from './strategies/prepare/impl/base.strategy'; import { PrepareStrategyRegistry } from './strategies/prepare/impl/base/prepare.strategy-registry'; +import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPR } from './strategies/prepare/impl/bitcoin-testnet4.strategy'; import { BitcoinStrategy as BitcoinStrategyPR } from './strategies/prepare/impl/bitcoin.strategy'; import { BscStrategy as BscStrategyPR } from './strategies/prepare/impl/bsc.strategy'; import { CardanoStrategy as CardanoStrategyPR } from './strategies/prepare/impl/cardano.strategy'; -import { CitreaStrategy as CitreaStrategyPR } from './strategies/prepare/impl/citrea.strategy'; import { CitreaTestnetStrategy as CitreaTestnetStrategyPR } from './strategies/prepare/impl/citrea-testnet.strategy'; +import { CitreaStrategy as CitreaStrategyPR } from './strategies/prepare/impl/citrea.strategy'; import { EthereumStrategy as EthereumStrategyPR } from './strategies/prepare/impl/ethereum.strategy'; +import { FiroStrategy as FiroStrategyPR } from './strategies/prepare/impl/firo.strategy'; import { GnosisStrategy as GnosisStrategyPR } from './strategies/prepare/impl/gnosis.strategy'; import { LightningStrategy as LightningStrategyPR } from './strategies/prepare/impl/lightning.strategy'; import { MoneroStrategy as MoneroStrategyPR } from './strategies/prepare/impl/monero.strategy'; @@ -80,10 +84,9 @@ import { OptimismStrategy as OptimismStrategyPR } from './strategies/prepare/imp import { PolygonStrategy as PolygonStrategyPR } from './strategies/prepare/impl/polygon.strategy'; import { SepoliaStrategy as SepoliaStrategyPR } from './strategies/prepare/impl/sepolia.strategy'; import { SolanaStrategy as SolanaStrategyPR } from './strategies/prepare/impl/solana.strategy'; +import { SparkStrategy as SparkStrategyPR } from './strategies/prepare/impl/spark.strategy'; import { TronStrategy as TronStrategyPR } from './strategies/prepare/impl/tron.strategy'; import { ZanoStrategy as ZanoStrategyPR } from './strategies/prepare/impl/zano.strategy'; -import { SparkStrategy as SparkStrategyPR } from './strategies/prepare/impl/spark.strategy'; -import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPR } from './strategies/prepare/impl/bitcoin-testnet4.strategy'; @Module({ imports: [ @@ -102,9 +105,10 @@ import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPR } from './strategi PayoutService, PayoutBitcoinService, PayoutLightningService, + PayoutSparkService, + PayoutFiroService, PayoutMoneroService, PayoutZanoService, - PayoutSparkService, PayoutArbitrumService, PayoutOptimismService, PayoutPolygonService, @@ -125,12 +129,15 @@ import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPR } from './strategi BitcoinStrategyPO, LightningStrategyPR, LightningStrategyPO, + SparkStrategyPR, + SparkStrategyPO, + FiroStrategyPR, + FiroStrategyPO, MoneroStrategyPR, MoneroStrategyPO, ZanoStrategyPR, ZanoCoinStrategyPO, ZanoTokenStrategyPO, - SparkStrategyPO, EthereumStrategyPR, EthereumCoinStrategyPO, EthereumTokenStrategyPO, @@ -172,11 +179,11 @@ import { BitcoinTestnet4Strategy as BitcoinTestnet4StrategyPR } from './strategi CitreaTestnetTokenStrategyPO, BitcoinTestnet4StrategyPR, BitcoinTestnet4StrategyPO, - SparkStrategyPR, ], exports: [ PayoutService, PayoutBitcoinService, + PayoutFiroService, PayoutMoneroService, PayoutZanoService, PayoutSparkService, diff --git a/src/subdomains/supporting/payout/services/payout-bitcoin.service.ts b/src/subdomains/supporting/payout/services/payout-bitcoin.service.ts index a41000ead3..cb946dc201 100644 --- a/src/subdomains/supporting/payout/services/payout-bitcoin.service.ts +++ b/src/subdomains/supporting/payout/services/payout-bitcoin.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { Config } from 'src/config/config'; import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client'; -import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/node/bitcoin.service'; import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service'; +import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service'; import { PayoutOrderContext } from '../entities/payout-order.entity'; import { PayoutBitcoinBasedService, PayoutGroup } from './base/payout-bitcoin-based.service'; diff --git a/src/subdomains/supporting/payout/services/payout-firo.service.ts b/src/subdomains/supporting/payout/services/payout-firo.service.ts new file mode 100644 index 0000000000..a7365d55b6 --- /dev/null +++ b/src/subdomains/supporting/payout/services/payout-firo.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { Config } from 'src/config/config'; +import { FiroClient } from 'src/integration/blockchain/firo/firo-client'; +import { FiroFeeService } from 'src/integration/blockchain/firo/services/firo-fee.service'; +import { FiroService } from 'src/integration/blockchain/firo/services/firo.service'; +import { PayoutOrderContext } from '../entities/payout-order.entity'; +import { PayoutBitcoinBasedService, PayoutGroup } from './base/payout-bitcoin-based.service'; + +@Injectable() +export class PayoutFiroService extends PayoutBitcoinBasedService { + private readonly client: FiroClient; + + constructor( + private readonly firoService: FiroService, + private readonly feeService: FiroFeeService, + ) { + super(); + + this.client = firoService.getDefaultClient(); + } + + async isHealthy(): Promise { + try { + return !!(await this.client.getInfo()); + } catch { + return false; + } + } + + async sendUtxoToMany(_context: PayoutOrderContext, payout: PayoutGroup): Promise { + const feeRate = await this.getCurrentFeeRate(); + return this.client.sendMany(payout, feeRate); + } + + async getPayoutCompletionData(_context: PayoutOrderContext, payoutTxId: string): Promise<[boolean, number]> { + const transaction = await this.client.getTx(payoutTxId); + + const isComplete = transaction && transaction.blockhash && transaction.confirmations > 0; + const payoutFee = isComplete ? -(transaction.fee ?? 0) : 0; + + return [isComplete, payoutFee]; + } + + async getCurrentFeeRate(): Promise { + const baseRate = await this.feeService.getRecommendedFeeRate(); + + const { allowUnconfirmedUtxos, cpfpFeeMultiplier, defaultFeeMultiplier } = Config.blockchain.firo; + const multiplier = allowUnconfirmedUtxos ? cpfpFeeMultiplier : defaultFeeMultiplier; + + return baseRate * multiplier; + } +} diff --git a/src/subdomains/supporting/payout/strategies/payout/impl/firo.strategy.ts b/src/subdomains/supporting/payout/strategies/payout/impl/firo.strategy.ts new file mode 100644 index 0000000000..83e51d69cf --- /dev/null +++ b/src/subdomains/supporting/payout/strategies/payout/impl/firo.strategy.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; +import { AssetService } from 'src/shared/models/asset/asset.service'; +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { Util } from 'src/shared/utils/util'; +import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; +import { PayoutOrder, PayoutOrderContext } from '../../../entities/payout-order.entity'; +import { FeeResult } from '../../../interfaces'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutGroup } from '../../../services/base/payout-bitcoin-based.service'; +import { PayoutFiroService } from '../../../services/payout-firo.service'; +import { BitcoinBasedStrategy } from './base/bitcoin-based.strategy'; + +@Injectable() +export class FiroStrategy extends BitcoinBasedStrategy { + protected readonly logger = new DfxLogger(FiroStrategy); + + private readonly averageTransactionSize = 225; // bytes (Firo Legacy P2PKH, no SegWit) + + constructor( + notificationService: NotificationService, + protected readonly firoService: PayoutFiroService, + protected readonly payoutOrderRepo: PayoutOrderRepository, + protected readonly assetService: AssetService, + ) { + super(notificationService, payoutOrderRepo, firoService); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + get assetType(): AssetType { + return undefined; + } + + async estimateFee(): Promise { + const feeRate = await this.firoService.getCurrentFeeRate(); + const satoshiFeeAmount = this.averageTransactionSize * feeRate; + const firoFeeAmount = Util.round(satoshiFeeAmount / 100000000, 8); + + return { asset: await this.feeAsset(), amount: firoFeeAmount }; + } + + protected async doPayoutForContext(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { + const payoutGroups = this.createPayoutGroups(orders, 100); + + for (const group of payoutGroups) { + try { + if (group.length === 0) { + continue; + } + + this.logger.verbose(`Paying out ${group.length} FIRO orders(s). Order ID(s): ${group.map((o) => o.id)}`); + + await this.sendFIRO(context, group); + } catch (e) { + this.logger.error( + `Error in paying out a group of ${group.length} FIRO orders(s). Order ID(s): ${group.map((o) => o.id)}`, + e, + ); + // continue with next group in case payout failed + continue; + } + } + } + + protected dispatchPayout(context: PayoutOrderContext, payout: PayoutGroup): Promise { + return this.firoService.sendUtxoToMany(context, payout); + } + + protected getFeeAsset(): Promise { + return this.assetService.getFiroCoin(); + } + + private async sendFIRO(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { + await this.send(context, orders); + } +} diff --git a/src/subdomains/supporting/payout/strategies/prepare/impl/firo.strategy.ts b/src/subdomains/supporting/payout/strategies/prepare/impl/firo.strategy.ts new file mode 100644 index 0000000000..827b61706c --- /dev/null +++ b/src/subdomains/supporting/payout/strategies/prepare/impl/firo.strategy.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; +import { Asset } from 'src/shared/models/asset/asset.entity'; +import { AssetService } from 'src/shared/models/asset/asset.service'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { AutoConfirmStrategy } from './base/auto-confirm.strategy'; + +@Injectable() +export class FiroStrategy extends AutoConfirmStrategy { + constructor( + private readonly assetService: AssetService, + payoutOrderRepo: PayoutOrderRepository, + ) { + super(payoutOrderRepo); + } + + get blockchain(): Blockchain { + return Blockchain.FIRO; + } + + protected getFeeAsset(): Promise { + return this.assetService.getFiroCoin(); + } +} From d3b1faa9836f25ac6f92a7d26f19f307a0e04c52 Mon Sep 17 00:00:00 2001 From: Bernd Date: Sun, 15 Feb 2026 08:47:03 +0100 Subject: [PATCH 2/4] style: fix prettier formatting --- src/config/config.ts | 6 +++- .../services/__tests__/crypto.service.spec.ts | 4 +-- .../blockchain/firo/firo-client.ts | 29 +++++++++---------- .../services/payment-link-fee.service.ts | 2 +- .../payin/services/payin-firo.service.ts | 4 +-- .../payin/services/payin.service.ts | 15 +++++++--- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index b445497eec..12a19d9fc6 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -634,7 +634,11 @@ export class Configuration { moneroAddress: process.env.PAYMENT_MONERO_ADDRESS, zanoAddress: process.env.PAYMENT_ZANO_ADDRESS, minConfirmations: (blockchain: Blockchain) => - [Blockchain.ETHEREUM, Blockchain.BITCOIN, Blockchain.FIRO, Blockchain.MONERO, Blockchain.ZANO].includes(blockchain) ? 6 : 100, + [Blockchain.ETHEREUM, Blockchain.BITCOIN, Blockchain.FIRO, Blockchain.MONERO, Blockchain.ZANO].includes( + blockchain, + ) + ? 6 + : 100, minVolume: 0.01, // CHF maxDepositBalance: 10000, // CHF cryptoPayoutMinAmount: +(process.env.PAYMENT_CRYPTO_PAYOUT_MIN ?? 1000), // CHF diff --git a/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts b/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts index 0398e9fd3e..69cc0f5b9d 100644 --- a/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts +++ b/src/integration/blockchain/bitcoin/services/__tests__/crypto.service.spec.ts @@ -124,9 +124,7 @@ describe('CryptoService', () => { }); it('should return Blockchain.FIRO for address a8MuyHBKL3nYZKAa82x13FxqtExP2sQCqu', () => { - expect( - CryptoService.getBlockchainsBasedOn('a8MuyHBKL3nYZKAa82x13FxqtExP2sQCqu'), - ).toEqual([Blockchain.FIRO]); + expect(CryptoService.getBlockchainsBasedOn('a8MuyHBKL3nYZKAa82x13FxqtExP2sQCqu')).toEqual([Blockchain.FIRO]); }); it('should return Blockchain.ETHEREUM and Blockchain.BINANCE_SMART_CHAIN for address 0x2d84553B3A4753009A314106d58F0CC21f441234', () => { diff --git a/src/integration/blockchain/firo/firo-client.ts b/src/integration/blockchain/firo/firo-client.ts index f5194a0c81..b6d36a9566 100644 --- a/src/integration/blockchain/firo/firo-client.ts +++ b/src/integration/blockchain/firo/firo-client.ts @@ -93,10 +93,7 @@ export class FiroClient extends BitcoinBasedClient { const feePerKb = (feeRate * 1000) / Math.pow(10, 8); await this.callNode(() => this.rpc.call('settxfee', [feePerKb]), true); - const txid = await this.callNode( - () => this.rpc.call('sendmany', ['', { [addressTo]: sendAmount }]), - true, - ); + const txid = await this.callNode(() => this.rpc.call('sendmany', ['', { [addressTo]: sendAmount }]), true); return { outTxId: txid ?? '', feeAmount }; } @@ -145,9 +142,7 @@ export class FiroClient extends BitcoinBasedClient { // Firo has no SegWit, so size == vsize. async testMempoolAccept(hex: string): Promise { try { - const decoded = await this.callNode(() => - this.rpc.call('decoderawtransaction', [hex]), - ); + const decoded = await this.callNode(() => this.rpc.call('decoderawtransaction', [hex])); const outputTotal = decoded.vout.reduce((sum, out) => sum + out.value, 0); @@ -157,7 +152,9 @@ export class FiroClient extends BitcoinBasedClient { if (vin.value === undefined) { const prevTx = await this.getRawTx(vin.txid); if (!prevTx) { - return [{ txid: decoded.txid, allowed: false, vsize: 0, fees: { base: 0 }, 'reject-reason': 'missing-inputs' }]; + return [ + { txid: decoded.txid, allowed: false, vsize: 0, fees: { base: 0 }, 'reject-reason': 'missing-inputs' }, + ]; } inputTotal += prevTx.vout[vin.vout].value; } else { @@ -167,13 +164,15 @@ export class FiroClient extends BitcoinBasedClient { const fee = this.roundAmount(inputTotal - outputTotal); - return [{ - txid: decoded.txid, - allowed: fee > 0, - vsize: decoded.size, - fees: { base: fee }, - 'reject-reason': fee <= 0 ? 'insufficient-fee' : '', - }]; + return [ + { + txid: decoded.txid, + allowed: fee > 0, + vsize: decoded.size, + fees: { base: fee }, + 'reject-reason': fee <= 0 ? 'insufficient-fee' : '', + }, + ]; } catch (e) { return [{ txid: '', allowed: false, vsize: 0, fees: { base: 0 }, 'reject-reason': e.message ?? 'decode-failed' }]; } diff --git a/src/subdomains/core/payment-link/services/payment-link-fee.service.ts b/src/subdomains/core/payment-link/services/payment-link-fee.service.ts index acc4a79a4b..55bbd00145 100644 --- a/src/subdomains/core/payment-link/services/payment-link-fee.service.ts +++ b/src/subdomains/core/payment-link/services/payment-link-fee.service.ts @@ -78,7 +78,7 @@ export class PaymentLinkFeeService implements OnModuleInit { case Blockchain.BITCOIN: return this.payoutBitcoinService.getCurrentFeeRate(); - + case Blockchain.FIRO: return this.payoutFiroService.getCurrentFeeRate(); } diff --git a/src/subdomains/supporting/payin/services/payin-firo.service.ts b/src/subdomains/supporting/payin/services/payin-firo.service.ts index 76f376cbe6..55ba2f270e 100644 --- a/src/subdomains/supporting/payin/services/payin-firo.service.ts +++ b/src/subdomains/supporting/payin/services/payin-firo.service.ts @@ -49,9 +49,7 @@ export class PayInFiroService extends PayInBitcoinBasedService { const transaction = await this.client.getRawTx(utxo.txid); if (!transaction) return; - const senderAddresses = transaction.vin - .filter((vin) => vin.address) - .map((vin) => vin.address); + const senderAddresses = transaction.vin.filter((vin) => vin.address).map((vin) => vin.address); utxo.prevoutAddresses = [...new Set(senderAddresses)]; utxo.isUnconfirmed = utxo.confirmations === 0; }), diff --git a/src/subdomains/supporting/payin/services/payin.service.ts b/src/subdomains/supporting/payin/services/payin.service.ts index 247e99f59a..fe4d53a70a 100644 --- a/src/subdomains/supporting/payin/services/payin.service.ts +++ b/src/subdomains/supporting/payin/services/payin.service.ts @@ -316,8 +316,16 @@ export class PayInService { private async getUnconfirmedNextBlockPayIns(): Promise { const chains = [ - { blockchain: Blockchain.BITCOIN, enabled: Config.blockchain.default.allowUnconfirmedUtxos, service: this.payInBitcoinService }, - { blockchain: Blockchain.FIRO, enabled: Config.blockchain.firo.allowUnconfirmedUtxos, service: this.payInFiroService }, + { + blockchain: Blockchain.BITCOIN, + enabled: Config.blockchain.default.allowUnconfirmedUtxos, + service: this.payInBitcoinService, + }, + { + blockchain: Blockchain.FIRO, + enabled: Config.blockchain.firo.allowUnconfirmedUtxos, + service: this.payInFiroService, + }, ].filter((c) => c.enabled && c.service.isAvailable()); if (chains.length === 0) return []; @@ -343,8 +351,7 @@ export class PayInService { if (chainCandidates.length === 0) continue; try { - const { nextBlockCandidates, failedPayIns } = - await service.filterUnconfirmedPayInsForForward(chainCandidates); + const { nextBlockCandidates, failedPayIns } = await service.filterUnconfirmedPayInsForForward(chainCandidates); allNextBlockCandidates.push(...nextBlockCandidates); allFailedPayIns.push(...failedPayIns); } catch (e) { From 231b2b479a8e8e8d1f61d81de3f25a0ea9c92744 Mon Sep 17 00:00:00 2001 From: Bernd Date: Mon, 16 Feb 2026 11:45:45 +0100 Subject: [PATCH 3/4] fix: enable UTXO forwarding for Firo PayIn Firo is UTXO-based like Bitcoin and requires forwarding from deposit addresses to the liquidity address before payouts. --- .../supporting/payin/strategies/send/impl/firo.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts b/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts index ceed104a02..3d261dbe7b 100644 --- a/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/send/impl/firo.strategy.ts @@ -28,7 +28,7 @@ export class FiroStrategy extends BitcoinBasedStrategy { } get forwardRequired(): boolean { - return false; + return true; } protected getForwardAddress(): BlockchainAddress { From 28c9a1f79580c30ccfaaa16ddc258c5457d9e08a Mon Sep 17 00:00:00 2001 From: Bernd Date: Mon, 16 Feb 2026 14:52:07 +0100 Subject: [PATCH 4/4] fix: UTXO isolation for Firo send/sendMany and accurate balance - Rewrite send() to use explicit UTXO input via createrawtransaction instead of sendmany (ensures only the specified deposit UTXO is spent) - Rewrite sendMany() with manual coin selection from liquidity address only (listunspent filtered by address) to prevent spending deposit/ payment UTXOs - Extract buildSignAndBroadcast() for shared raw TX building logic - Fix getBalance() to use listunspent on liquidity address instead of getbalance with default account (which returns negative due to Firo's legacy account system) - Remove unused FiroController and clean up FiroModule --- .../blockchain/firo/firo-client.ts | 97 ++++++++++++++----- .../blockchain/firo/firo.controller.ts | 22 ----- .../blockchain/firo/firo.module.ts | 3 +- 3 files changed, 76 insertions(+), 46 deletions(-) delete mode 100644 src/integration/blockchain/firo/firo.controller.ts diff --git a/src/integration/blockchain/firo/firo-client.ts b/src/integration/blockchain/firo/firo-client.ts index b6d36a9566..fac88fd15b 100644 --- a/src/integration/blockchain/firo/firo-client.ts +++ b/src/integration/blockchain/firo/firo-client.ts @@ -43,11 +43,13 @@ export class FiroClient extends BitcoinBasedClient { return this.callNode(() => this.rpc.call('getnewaddress', [label]), true); } - // Firo does not support getbalances (Bitcoin Core 0.19+), use getbalance instead - // Firo's getbalance: (account, minconf, include_watchonly, addlocked) + // Firo's account-based getbalance with '' returns only the default account, which can be negative. + // Use listunspent filtered to the liquidity address for an accurate spendable balance. async getBalance(): Promise { - const minconf = this.nodeConfig.allowUnconfirmedUtxos ? 0 : 1; - return this.callNode(() => this.rpc.call('getbalance', ['', minconf]), true); + const minConf = this.nodeConfig.allowUnconfirmedUtxos ? 0 : 1; + const utxos = await this.callNode(() => this.rpc.listUnspent(minConf, 9999999, [this.walletAddress]), true); + + return utxos?.reduce((sum, u) => sum + u.amount, 0) ?? 0; } // Firo's getblock uses boolean verbose, not int verbosity (0/1/2) @@ -78,39 +80,90 @@ export class FiroClient extends BitcoinBasedClient { } // Firo does not have the 'send' RPC (Bitcoin Core 0.21+). - // Use settxfee + sendmany instead for single sends with specific UTXOs. + // Uses explicit input to ensure only the specified UTXO is spent (forwarding from deposit addresses). async send( addressTo: string, - _txId: string, + txId: string, amount: number, - _vout: number, + vout: number, feeRate: number, ): Promise<{ outTxId: string; feeAmount: number }> { - const feeAmount = (feeRate * 225) / Math.pow(10, 8); + const feeAmount = (feeRate * 225) / 1e8; const sendAmount = this.roundAmount(amount - feeAmount); - // Set fee rate via settxfee (FIRO/kB), feeRate is in sat/vB - const feePerKb = (feeRate * 1000) / Math.pow(10, 8); - await this.callNode(() => this.rpc.call('settxfee', [feePerKb]), true); - - const txid = await this.callNode(() => this.rpc.call('sendmany', ['', { [addressTo]: sendAmount }]), true); + const outTxId = await this.buildSignAndBroadcast([{ txid: txId, vout }], { [addressTo]: sendAmount }); - return { outTxId: txid ?? '', feeAmount }; + return { outTxId, feeAmount }; } - // Firo does not have the 'send' RPC. Use settxfee + sendmany instead. + // Only use UTXOs from the liquidity (wallet) address to avoid spending deposit/payment UTXOs. async sendMany(payload: { addressTo: string; amount: number }[], feeRate: number): Promise { - const amounts = payload.reduce((acc, p) => ({ ...acc, [p.addressTo]: p.amount }), {} as Record); + const outputs = payload.reduce( + (acc, p) => ({ ...acc, [p.addressTo]: this.roundAmount(p.amount) }), + {} as Record, + ); + const outputTotal = payload.reduce((sum, p) => sum + p.amount, 0); + + // Get UTXOs only from the liquidity address + const minConf = this.nodeConfig.allowUnconfirmedUtxos ? 0 : 1; + const utxos = await this.callNode(() => this.rpc.listUnspent(minConf, 9999999, [this.walletAddress]), true); + + if (!utxos || utxos.length === 0) { + throw new Error('No UTXOs available on the liquidity address'); + } + + // Select UTXOs to cover outputs + estimated fee (225 bytes per input, 34 per output, 10 overhead) + const sortedUtxos = utxos.sort((a, b) => b.amount - a.amount); + const selectedInputs: { txid: string; vout: number }[] = []; + let inputTotal = 0; + + for (const utxo of sortedUtxos) { + selectedInputs.push({ txid: utxo.txid, vout: utxo.vout }); + inputTotal += utxo.amount; + + const estimatedSize = selectedInputs.length * 225 + (payload.length + 1) * 34 + 10; + const estimatedFee = (feeRate * estimatedSize) / 1e8; + + if (inputTotal >= outputTotal + estimatedFee) break; + } - // Set fee rate via settxfee (FIRO/kB), feeRate is in sat/vB - const feePerKb = (feeRate * 1000) / Math.pow(10, 8); - await this.callNode(() => this.rpc.call('settxfee', [feePerKb]), true); + // Calculate final fee and change + const txSize = selectedInputs.length * 225 + (payload.length + 1) * 34 + 10; + const fee = (feeRate * txSize) / 1e8; + + if (inputTotal < outputTotal + fee) { + throw new Error(`Insufficient funds on liquidity address: have ${inputTotal}, need ${outputTotal + fee}`); + } + + const change = this.roundAmount(inputTotal - outputTotal - fee); + if (change > 0.00001) { + outputs[this.walletAddress] = (outputs[this.walletAddress] ?? 0) + change; + outputs[this.walletAddress] = this.roundAmount(outputs[this.walletAddress]); + } + + return this.buildSignAndBroadcast(selectedInputs, outputs); + } + + // Creates, signs, and broadcasts a raw transaction with explicit inputs. + private async buildSignAndBroadcast( + inputs: { txid: string; vout: number }[], + outputs: Record, + ): Promise { + const rawTx = await this.callNode(() => this.rpc.call('createrawtransaction', [inputs, outputs]), true); + + const signedResult = await this.callNode( + () => this.rpc.call<{ hex: string; complete: boolean }>('signrawtransaction', [rawTx]), + true, + ); + + if (!signedResult.complete) { + throw new Error('Failed to sign Firo transaction'); + } - return this.callNode(() => this.rpc.call('sendmany', ['', amounts]), true); + return this.callNode(() => this.rpc.call('sendrawtransaction', [signedResult.hex]), true); } - // Firo's sendmany only accepts 5 params (no replaceable/conf_target/estimate_mode). - // Delegates to sendMany to ensure settxfee is called with a current fee rate estimate. + // Delegates to sendMany which uses manual coin selection from the liquidity address only. async sendUtxoToMany(payload: { addressTo: string; amount: number }[]): Promise { if (payload.length > 100) { throw new Error('Too many addresses in one transaction batch, allowed max 100 for UTXO'); diff --git a/src/integration/blockchain/firo/firo.controller.ts b/src/integration/blockchain/firo/firo.controller.ts deleted file mode 100644 index 59a7395c98..0000000000 --- a/src/integration/blockchain/firo/firo.controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { FiroService } from './services/firo.service'; - -@ApiTags('Firo') -@Controller('firo') -export class FiroController { - constructor(private readonly firoService: FiroService) {} - - @Get('info') - async getInfo(): Promise<{ blockHeight: number; headers: number; blocks: number; chain: string }> { - const client = this.firoService.getDefaultClient(); - const info = await client.getInfo(); - - return { - blockHeight: await client.getBlockCount(), - headers: info.headers, - blocks: info.blocks, - chain: info.chain, - }; - } -} diff --git a/src/integration/blockchain/firo/firo.module.ts b/src/integration/blockchain/firo/firo.module.ts index a54bc7259b..fde60a1991 100644 --- a/src/integration/blockchain/firo/firo.module.ts +++ b/src/integration/blockchain/firo/firo.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; import { SharedModule } from 'src/shared/shared.module'; -import { FiroController } from './firo.controller'; import { FiroFeeService } from './services/firo-fee.service'; import { FiroService } from './services/firo.service'; @Module({ imports: [SharedModule], - controllers: [FiroController], + controllers: [], providers: [FiroService, FiroFeeService], exports: [FiroService, FiroFeeService], })