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
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,20 @@ export abstract class BitcoinBasedClient extends NodeClient implements CoinOnly
return { outTxId: result?.txid ?? '', feeAmount };
}

async sendMany(payload: { addressTo: string; amount: number }[], feeRate: number): Promise<string> {
async sendMany(
payload: { addressTo: string; amount: number }[],
feeRate: number,
inputs?: Array<{ txid: string; vout: number }>,
subtractFeeFromOutputs?: number[],
): Promise<string> {
const outputs = payload.map((p) => ({ [p.addressTo]: p.amount }));

const options = {
replaceable: true,
change_address: this.walletAddress,
...(this.nodeConfig.allowUnconfirmedUtxos && { include_unsafe: true }),
...(inputs && { inputs, add_inputs: false }),
...(subtractFeeFromOutputs && { subtract_fee_from_outputs: subtractFeeFromOutputs }),
};

const result = await this.callNode(() => this.rpc.send(outputs, null, null, feeRate, options), true);
Expand Down
5 changes: 5 additions & 0 deletions src/integration/blockchain/bitcoin/node/node-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ export abstract class NodeClient extends BlockchainClient {
return this.callNode(() => this.rpc.listUnspent(minConf), true);
}

async getUtxoForAddresses(addresses: string[], includeUnconfirmed = false): Promise<UTXO[]> {
const minConf = includeUnconfirmed ? 0 : 1;
return this.callNode(() => this.rpc.listUnspent(minConf, 9999999, addresses), true);
}

async getBalance(): Promise<number> {
// Include unconfirmed UTXOs when configured
// Bitcoin Core's getbalances returns: trusted (confirmed + own unconfirmed), untrusted_pending (others' unconfirmed), immature (coinbase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export interface TxFeeRateResult {
feeRate?: number;
}

export interface FeeConfig {
allowUnconfirmedUtxos: boolean;
cpfpFeeMultiplier: number;
defaultFeeMultiplier: number;
}

export abstract class BitcoinBasedFeeService {
private readonly logger = new DfxLogger(BitcoinBasedFeeService);

Expand All @@ -17,6 +23,8 @@ export abstract class BitcoinBasedFeeService {

constructor(protected readonly client: NodeClient) {}

protected abstract get feeConfig(): FeeConfig;

async getRecommendedFeeRate(): Promise<number> {
return this.feeRateCache.get(
'fastestFee',
Expand Down Expand Up @@ -74,4 +82,13 @@ export abstract class BitcoinBasedFeeService {

return results;
}

async getSendFeeRate(): Promise<number> {
const baseRate = await this.getRecommendedFeeRate();

const { allowUnconfirmedUtxos, cpfpFeeMultiplier, defaultFeeMultiplier } = this.feeConfig;
const multiplier = allowUnconfirmedUtxos ? cpfpFeeMultiplier : defaultFeeMultiplier;

return baseRate * multiplier;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { BitcoinBasedFeeService } from './bitcoin-based-fee.service';
import { Config } from 'src/config/config';
import { BitcoinBasedFeeService, FeeConfig } from './bitcoin-based-fee.service';
import { BitcoinNodeType, BitcoinService } from './bitcoin.service';

export { TxFeeRateResult, TxFeeRateStatus } from './bitcoin-based-fee.service';
Expand All @@ -9,4 +10,8 @@ export class BitcoinFeeService extends BitcoinBasedFeeService {
constructor(bitcoinService: BitcoinService) {
super(bitcoinService.getDefaultClient(BitcoinNodeType.BTC_INPUT));
}

protected get feeConfig(): FeeConfig {
return Config.blockchain.default;
}
}
14 changes: 6 additions & 8 deletions src/integration/blockchain/firo/firo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,9 @@ export class FiroClient extends BitcoinBasedClient {
// Firo's account-based getbalance with '' returns only the default account, which can be negative.
// Use listunspent filtered to the liquidity and payment addresses for an accurate spendable balance.
async getBalance(): Promise<number> {
const minConf = this.nodeConfig.allowUnconfirmedUtxos ? 0 : 1;
const utxos = await this.callNode(
() => this.rpc.listUnspent(minConf, 9999999, [this.walletAddress, this.paymentAddress]),
true,
const utxos = await this.getUtxoForAddresses(
[this.walletAddress, this.paymentAddress],
this.nodeConfig.allowUnconfirmedUtxos,
);

return utxos?.reduce((sum, u) => sum + u.amount, 0) ?? 0;
Expand Down Expand Up @@ -113,10 +112,9 @@ export class FiroClient extends BitcoinBasedClient {
const outputTotal = payload.reduce((sum, p) => sum + p.amount, 0);

// Get UTXOs from liquidity and payment addresses (excludes deposit address UTXOs)
const minConf = this.nodeConfig.allowUnconfirmedUtxos ? 0 : 1;
const utxos = await this.callNode(
() => this.rpc.listUnspent(minConf, 9999999, [this.walletAddress, this.paymentAddress]),
true,
const utxos = await this.getUtxoForAddresses(
[this.walletAddress, this.paymentAddress],
this.nodeConfig.allowUnconfirmedUtxos,
);

if (!utxos || utxos.length === 0) {
Expand Down
11 changes: 3 additions & 8 deletions src/integration/blockchain/firo/services/firo-fee.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Config } from 'src/config/config';
import { BitcoinBasedFeeService } from '../../bitcoin/services/bitcoin-based-fee.service';
import { BitcoinBasedFeeService, FeeConfig } from '../../bitcoin/services/bitcoin-based-fee.service';
import { FiroService } from './firo.service';

@Injectable()
Expand All @@ -9,12 +9,7 @@ export class FiroFeeService extends BitcoinBasedFeeService {
super(firoService.getDefaultClient());
}

async getSendFeeRate(): Promise<number> {
const baseRate = await this.getRecommendedFeeRate();

const { allowUnconfirmedUtxos, cpfpFeeMultiplier, defaultFeeMultiplier } = Config.blockchain.firo;
const multiplier = allowUnconfirmedUtxos ? cpfpFeeMultiplier : defaultFeeMultiplier;

return baseRate * multiplier;
protected get feeConfig(): FeeConfig {
return Config.blockchain.firo;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Config } from 'src/config/config';
import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service';
import { BitcoinNodeType } from 'src/integration/blockchain/bitcoin/services/bitcoin.service';
import { CardanoUtil } from 'src/integration/blockchain/cardano/cardano.util';
import { BlockchainTokenBalance } from 'src/integration/blockchain/shared/dto/blockchain-token-balance.dto';
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
Expand Down Expand Up @@ -43,6 +45,7 @@ export class PaymentBalanceService implements OnModuleInit {
constructor(
private readonly assetService: AssetService,
private readonly blockchainRegistryService: BlockchainRegistryService,
private readonly bitcoinFeeService: BitcoinFeeService,
) {}

onModuleInit() {
Expand Down Expand Up @@ -151,7 +154,7 @@ export class PaymentBalanceService implements OnModuleInit {
}

async forwardDeposits() {
const chainsWithoutForwarding = [Blockchain.BITCOIN, Blockchain.FIRO, ...this.chainsWithoutPaymentBalance];
const chainsWithoutForwarding = [Blockchain.FIRO, ...this.chainsWithoutPaymentBalance];

const paymentAssets = await this.assetService
.getPaymentAssets()
Expand All @@ -171,6 +174,10 @@ export class PaymentBalanceService implements OnModuleInit {
}

private async forwardDeposit(asset: Asset, balance: number): Promise<string> {
if (asset.blockchain === Blockchain.BITCOIN) {
return this.forwardBitcoinDeposit();
}

const account = this.getPaymentAccount(asset.blockchain);
const client = this.blockchainRegistryService.getClient(asset.blockchain) as EvmClient | SolanaClient | TronClient;

Expand All @@ -179,6 +186,23 @@ export class PaymentBalanceService implements OnModuleInit {
: client.sendTokenFromAccount(account, client.walletAddress, asset, balance);
}

private async forwardBitcoinDeposit(): Promise<string> {
const client = this.blockchainRegistryService.getBitcoinClient(Blockchain.BITCOIN, BitcoinNodeType.BTC_INPUT);
const paymentAddress = Config.payment.bitcoinAddress;
const outputAddress = Config.blockchain.default.btcOutput.address;
const feeRate = await this.bitcoinFeeService.getSendFeeRate();

// only use UTXOs from the payment address (not deposit UTXOs on the same wallet)
const utxos = await client.getUtxoForAddresses([paymentAddress], true);
if (!utxos.length) return '';

const inputs = utxos.map((u) => ({ txid: u.txid, vout: u.vout }));
const utxoBalance = utxos.reduce((sum, u) => sum + u.amount, 0);

// sweep all UTXOs: send full balance and let Bitcoin Core subtract the fee from the output
return client.sendMany([{ addressTo: outputAddress, amount: utxoBalance }], feeRate, inputs, [0]);
Comment on lines +195 to +203
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is already in FiroClient.sendMany. Maybe we should add a method sendManyFromAddress to BitcoinBasedClient and reuse it in both places? Is this possible?

}

private getPaymentAccount(chain: Blockchain): WalletAccount {
switch (chain) {
case Blockchain.ETHEREUM:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class DexBitcoinService {
}

async sendUtxoToMany(payout: { addressTo: string; amount: number }[]): Promise<string> {
const feeRate = await this.feeService.getRecommendedFeeRate();
const feeRate = await this.feeService.getSendFeeRate();
return this.client.sendMany(payout, feeRate);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class DexFiroService {
}

async sendUtxoToMany(payout: { addressTo: string; amount: number }[]): Promise<string> {
const feeRate = await this.getFeeRate();
const feeRate = await this.feeService.getSendFeeRate();
return this.client.sendMany(payout, feeRate);
}

Expand All @@ -44,10 +44,6 @@ export class DexFiroService {

//*** HELPER METHODS ***//

private async getFeeRate(): Promise<number> {
return this.feeService.getSendFeeRate();
}

private async getPendingAmount(): Promise<number> {
const pendingOrders = await this.liquidityOrderRepo.findBy({
isComplete: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class PayInBitcoinService extends PayInBitcoinBasedService {
input.inTxId,
input.sendingAmount,
input.txSequence,
await this.feeService.getRecommendedFeeRate(),
await this.feeService.getSendFeeRate(),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class PayInFiroService extends PayInBitcoinBasedService {
}

async sendTransfer(input: CryptoInput): Promise<{ outTxId: string; feeAmount: number }> {
const feeRate = await this.feeService.getRecommendedFeeRate();
const feeRate = await this.feeService.getSendFeeRate();
return this.client.send(
input.destinationAddress.address,
input.inTxId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Config } from 'src/config/config';
import { BitcoinClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-client';
import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service';
import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service';
Expand Down Expand Up @@ -44,12 +43,6 @@ export class PayoutBitcoinService extends PayoutBitcoinBasedService {
}

async getCurrentFeeRate(): Promise<number> {
const baseRate = await this.feeService.getRecommendedFeeRate();

// Use higher multiplier when unconfirmed UTXOs are enabled (CPFP effect)
const { allowUnconfirmedUtxos, cpfpFeeMultiplier, defaultFeeMultiplier } = Config.blockchain.default;
const multiplier = allowUnconfirmedUtxos ? cpfpFeeMultiplier : defaultFeeMultiplier;

return baseRate * multiplier;
return this.feeService.getSendFeeRate();
}
}
Loading