Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dfip",
"dilisense",
"ebics",
"firo",
"firstname",
"forex",
"frankencoin",
Expand Down
19 changes: 19 additions & 0 deletions infrastructure/config/docker/docker-compose-firo.yml
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions infrastructure/config/firo/firo.conf
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions migration/seed/asset.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
53 changes: 36 additions & 17 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}';
Expand All @@ -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}';
Expand All @@ -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 = '.*';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -882,6 +889,21 @@ export class Configuration {
},
certificate: process.env.LIGHTNING_API_CERTIFICATE?.split('<br>').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,
Expand All @@ -892,6 +914,17 @@ export class Configuration {
walletAddress: process.env.MONERO_WALLET_ADDRESS,
certificate: process.env.MONERO_RPC_CERTIFICATE?.split('<br>').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,
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/integration/blockchain/bitcoin/bitcoin.module.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/integration/blockchain/bitcoin/node/node.controller.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 () => {
Expand All @@ -24,6 +25,7 @@ describe('CryptoService', () => {
{ provide: BitcoinService, useValue: createMock<BitcoinService>() },
{ provide: LightningService, useValue: createMock<LightningService>() },
{ provide: SparkService, useValue: createMock<SparkService>() },
{ provide: FiroService, useValue: createMock<FiroService>() },
{ provide: MoneroService, useValue: createMock<MoneroService>() },
{ provide: ZanoService, useValue: createMock<ZanoService>() },
{ provide: SolanaService, useValue: createMock<SolanaService>() },
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<number>(CacheItemResetPeriod.EVERY_30_SECONDS);
private readonly txFeeRateCache = new AsyncCache<TxFeeRateResult>(CacheItemResetPeriod.EVERY_30_SECONDS);

constructor(protected readonly client: NodeClient) {}

async getRecommendedFeeRate(): Promise<number> {
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<TxFeeRateResult> {
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<Map<string, TxFeeRateResult>> {
const results = new Map<string, TxFeeRateResult>();

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;
}
}
Loading
Loading