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..12a19d9fc6 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,15 @@ 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 +889,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 +914,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 +971,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..69cc0f5b9d 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,10 @@ 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