Skip to content
Merged
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 infrastructure/bicep/container-groups/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Container Instances are:
- hb-deuro-usdt: Hummingbot (dEURO/USDT)
- hb-jusd-usdt: Hummingbot (JUSD/BTC)
- hb-deps-usdt: Hummingbot (dEPS/USDT)
- hb-keep-market: Hummingbot (Cross pair bot to keep a certain 24h volumen)
- rk: RangeKeeper Liquidity Bot

### Fileshare
Expand Down
3 changes: 2 additions & 1 deletion infrastructure/bicep/container-groups/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ environmentOptions=("loc" "dev" "prd")
# "hb-deuro-usdt": Hummingbot (dEURO/USDT)
# "hb-jusd-usdt": Hummingbot (JuiceDollar/USDT)
# "hb-deps-usdt": Hummingbot (dEPS/USDT)
# "hb-keep-market": Hummingbot (Cross pair bot to keep a certain 24h volumen)
# "rk": RangeKeeper Liquidity Bot
instanceNameOptions=("hb-deuro-usdt" "hb-jusd-usdt" "hb-deps-usdt" "rk")
instanceNameOptions=("hb-deuro-usdt" "hb-jusd-usdt" "hb-deps-usdt" "hb-keep-market" "rk")

# --- ARGUMENTS --- #
DOCKER_USERNAME=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"fileShareQuota": {
"value": 100
},
"containerImage": {
"value": "dfxswiss/hummingbot:latest"
},
"containerVolumeMounts": {
"value": [
{
"name": "volume",
"mountPath": "/mnt/hummingbot",
"readOnly": false
}
]
},
"containerCPU": {
"value": "0.5"
},
"containerMemory": {
"value": 1
},
"containerEnv": {
"value": [
{
"name": "BOT_DIR",
"value": "keep-market-dev"
},
{
"name": "STRATEGY_FILE",
"value": "conf_v2_with_controllers_2.yml.yml"
},
{
"name": "SCRIPT_FILE",
"value": "v2_with_controllers.py"
}
]
},
"containerCommand": {
"value": []
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"fileShareQuota": {
"value": 100
},
"containerImage": {
"value": "dfxswiss/hummingbot:latest"
},
"containerVolumeMounts": {
"value": [
{
"name": "volume",
"mountPath": "/mnt/hummingbot",
"readOnly": false
}
]
},
"containerCPU": {
"value": "0.5"
},
"containerMemory": {
"value": 1
},
"containerEnv": {
"value": [
{
"name": "BOT_DIR",
"value": "keep-market"
},
{
"name": "STRATEGY_FILE",
"value": "conf_v2_with_controllers_2.yml.yml"
},
{
"name": "SCRIPT_FILE",
"value": "v2_with_controllers.py"
}
]
},
"containerCommand": {
"value": []
}
}
}
27 changes: 22 additions & 5 deletions infrastructure/config/docker/docker-compose-firo.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
name: 'firo'

services:
autoheal:
image: willfarrell/autoheal:latest
restart: unless-stopped
environment:
- AUTOHEAL_CONTAINER_LABEL=autoheal
- AUTOHEAL_INTERVAL=30
- AUTOHEAL_START_PERIOD=120
volumes:
- /var/run/docker.sock:/var/run/docker.sock

firod:
image: firoorg/firod:0.14.15.2
restart: unless-stopped
labels:
- autoheal=true
deploy:
resources:
limits:
memory: 2048M
memory: 8192M
reservations:
memory: 1024M
memory: 4096M
volumes:
- ./volumes/firo:/home/firod/.firo
ports:
- '8168:8168'
- '8888:8888'
healthcheck:
test: firo-cli -conf=/home/firod/.firo/firo.conf getblockcount || exit 1
test: firo-cli -conf=/home/firod/.firo/firo.conf getblockchaininfo || exit 1
start_period: 120s
interval: 120s
timeout: 10s
interval: 60s
timeout: 30s
retries: 3
logging:
driver: 'json-file'
options:
max-size: '100m'
max-file: '3'
command: >
-conf=/home/firod/.firo/firo.conf
2 changes: 1 addition & 1 deletion infrastructure/config/firo/firo.conf
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ mempoolexpiry=24
maxorphantx=10

# Performance (defaults: dbcache=450MB, rpcthreads=4, rpcworkqueue=16)
dbcache=256
dbcache=1024
rpcthreads=8
rpcworkqueue=64

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,45 @@ 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);

return result?.txid ?? '';
}

async sendManyFromAddress(
fromAddresses: string[],
payload: { addressTo: string; amount: number }[],
feeRate: number,
subtractFeeFromOutputs?: number[],
): Promise<string> {
const utxos = await this.getUtxoForAddresses(fromAddresses, this.nodeConfig.allowUnconfirmedUtxos);
if (!utxos.length) throw new Error('No UTXOs available');

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

// resolve zero-amount entries with full UTXO balance (sweep mode)
const resolvedPayload = payload.map((p) => ({ addressTo: p.addressTo, amount: p.amount || utxoBalance }));

return this.sendMany(resolvedPayload, feeRate, inputs, subtractFeeFromOutputs);
}

async testMempoolAccept(hex: string): Promise<TestMempoolResult[]> {
const result = await this.callNode(() => this.rpc.testMempoolAccept([hex]), 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;
}
}
33 changes: 17 additions & 16 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 this.roundAmount(utxos?.reduce((sum, u) => sum + u.amount, 0) ?? 0);
Expand Down Expand Up @@ -103,24 +102,28 @@ export class FiroClient extends BitcoinBasedClient {
return { outTxId, feeAmount };
}

// Use UTXOs from the liquidity and payment addresses to avoid spending deposit UTXOs.
// Change is sent back to the liquidity address, naturally consolidating funds over time.
// Delegates to sendManyFromAddress using the liquidity and payment addresses.
async sendMany(payload: { addressTo: string; amount: number }[], feeRate: number): Promise<string> {
return this.sendManyFromAddress([this.walletAddress, this.paymentAddress], payload, feeRate);
}

// Use UTXOs from the specified addresses to avoid spending deposit UTXOs.
// Change is sent back to the liquidity address, naturally consolidating funds over time.
async sendManyFromAddress(
fromAddresses: string[],
payload: { addressTo: string; amount: number }[],
feeRate: number,
): Promise<string> {
const outputs = payload.reduce(
(acc, p) => ({ ...acc, [p.addressTo]: this.roundAmount(p.amount) }),
{} as Record<string, number>,
);
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(fromAddresses, this.nodeConfig.allowUnconfirmedUtxos);

if (!utxos || utxos.length === 0) {
throw new Error('No UTXOs available on the liquidity/payment addresses');
throw new Error('No UTXOs available on the specified addresses');
}

// Select UTXOs to cover outputs + estimated fee (225 bytes per input, 34 per output, 10 overhead)
Expand All @@ -143,9 +146,7 @@ export class FiroClient extends BitcoinBasedClient {
const fee = (feeRate * txSize) / 1e8;

if (inputTotal < outputTotal + fee) {
throw new Error(
`Insufficient funds on liquidity/payment addresses: have ${inputTotal}, need ${outputTotal + fee}`,
);
throw new Error(`Insufficient funds on specified addresses: have ${inputTotal}, need ${outputTotal + fee}`);
}

const change = this.roundAmount(inputTotal - outputTotal - fee);
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;
}
}
Loading
Loading