From 87b007de504a3994717a4a1cd1f390e594ad4e6f Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Wed, 25 Feb 2026 19:29:30 +0100
Subject: [PATCH 01/13] feat: add Boltz/Lightning.space adapter for Citrea cBTC
liquidity
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Integrate Lightning.space (Boltz) as an additional liquidity source for
Citrea/cBTC, enabling flexible BTC → cBTC reverse swaps for amounts
smaller than the fixed 10 BTC Clementine Bridge.
---
src/config/config.ts | 3 +
.../blockchain/blockchain.module.ts | 3 +
.../blockchain/boltz/boltz-client.ts | 68 +++++++
.../blockchain/boltz/boltz.module.ts | 10 +
.../blockchain/boltz/boltz.service.ts | 18 ++
.../adapters/actions/boltz.adapter.ts | 172 ++++++++++++++++++
.../core/liquidity-management/enums/index.ts | 2 +
.../liquidity-action-integration.factory.ts | 3 +
.../liquidity-management.module.ts | 2 +
9 files changed, 281 insertions(+)
create mode 100644 src/integration/blockchain/boltz/boltz-client.ts
create mode 100644 src/integration/blockchain/boltz/boltz.module.ts
create mode 100644 src/integration/blockchain/boltz/boltz.service.ts
create mode 100644 src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
diff --git a/src/config/config.ts b/src/config/config.ts
index 70907aba4e..b67f3662e6 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -889,6 +889,9 @@ export class Configuration {
},
certificate: process.env.LIGHTNING_API_CERTIFICATE?.split('
').join('\n'),
},
+ boltz: {
+ apiUrl: process.env.BOLTZ_API_URL ?? 'https://api.lightning.space',
+ },
spark: {
sparkWalletSeed: process.env.SPARK_WALLET_SEED,
},
diff --git a/src/integration/blockchain/blockchain.module.ts b/src/integration/blockchain/blockchain.module.ts
index 34e0ca105a..25840d13c0 100644
--- a/src/integration/blockchain/blockchain.module.ts
+++ b/src/integration/blockchain/blockchain.module.ts
@@ -8,6 +8,7 @@ import { BlockchainApiModule } from './api/blockchain-api.module';
import { ArbitrumModule } from './arbitrum/arbitrum.module';
import { ArweaveModule } from './arweave/arweave.module';
import { BaseModule } from './base/base.module';
+import { BoltzModule } from './boltz/boltz.module';
import { BscModule } from './bsc/bsc.module';
import { CardanoModule } from './cardano/cardano.module';
import { CitreaTestnetModule } from './citrea-testnet/citrea-testnet.module';
@@ -67,6 +68,7 @@ import { ZanoModule } from './zano/zano.module';
CitreaModule,
CitreaTestnetModule,
ClementineModule,
+ BoltzModule,
RealUnitBlockchainModule,
Eip7702DelegationModule,
PimlicoPaymasterModule,
@@ -99,6 +101,7 @@ import { ZanoModule } from './zano/zano.module';
CitreaModule,
CitreaTestnetModule,
ClementineModule,
+ BoltzModule,
CryptoService,
BlockchainRegistryService,
TxValidationService,
diff --git a/src/integration/blockchain/boltz/boltz-client.ts b/src/integration/blockchain/boltz/boltz-client.ts
new file mode 100644
index 0000000000..f7330b74b5
--- /dev/null
+++ b/src/integration/blockchain/boltz/boltz-client.ts
@@ -0,0 +1,68 @@
+import { HttpService } from 'src/shared/services/http.service';
+import { DfxLogger } from 'src/shared/services/dfx-logger';
+
+export interface BoltzConfig {
+ apiUrl: string;
+}
+
+export enum BoltzSwapStatus {
+ CREATED = 'swap.created',
+ INVOICE_SET = 'invoice.set',
+ INVOICE_PENDING = 'invoice.pending',
+ INVOICE_PAID = 'invoice.paid',
+ INVOICE_FAILEDTOPAY = 'invoice.failedToPay',
+ TRANSACTION_MEMPOOL = 'transaction.mempool',
+ TRANSACTION_CLAIMED = 'transaction.claimed',
+ TRANSACTION_CONFIRMED = 'transaction.confirmed',
+ TRANSACTION_REFUNDED = 'transaction.refunded',
+ TRANSACTION_FAILED = 'transaction.failed',
+ SWAP_EXPIRED = 'swap.expired',
+}
+
+export interface BoltzReverseSwapResponse {
+ id: string;
+ invoice: string;
+ lockupAddress: string;
+ onchainAmount: number;
+ timeoutBlockHeight: number;
+ redeemScript?: string;
+}
+
+export interface BoltzSwapStatusResponse {
+ status: BoltzSwapStatus;
+ failureReason?: string;
+ transaction?: {
+ id: string;
+ hex?: string;
+ };
+}
+
+export class BoltzClient {
+ private readonly logger = new DfxLogger(BoltzClient);
+
+ constructor(
+ private readonly http: HttpService,
+ private readonly config: BoltzConfig,
+ ) {}
+
+ async createReverseSwap(claimAddress: string, amount: number): Promise {
+ const url = `${this.config.apiUrl}/v2/swap/reverse`;
+
+ const body = {
+ from: 'BTC',
+ to: 'cBTC',
+ claimAddress,
+ invoiceAmount: amount,
+ };
+
+ this.logger.verbose(`Creating reverse swap: ${amount} sats -> ${claimAddress}`);
+
+ return this.http.post(url, body, { tryCount: 3, retryDelay: 2000 });
+ }
+
+ async getSwapStatus(swapId: string): Promise {
+ const url = `${this.config.apiUrl}/v2/swap/${swapId}`;
+
+ return this.http.get(url, { tryCount: 3, retryDelay: 2000 });
+ }
+}
diff --git a/src/integration/blockchain/boltz/boltz.module.ts b/src/integration/blockchain/boltz/boltz.module.ts
new file mode 100644
index 0000000000..0e1d3fd988
--- /dev/null
+++ b/src/integration/blockchain/boltz/boltz.module.ts
@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+import { SharedModule } from 'src/shared/shared.module';
+import { BoltzService } from './boltz.service';
+
+@Module({
+ imports: [SharedModule],
+ providers: [BoltzService],
+ exports: [BoltzService],
+})
+export class BoltzModule {}
diff --git a/src/integration/blockchain/boltz/boltz.service.ts b/src/integration/blockchain/boltz/boltz.service.ts
new file mode 100644
index 0000000000..648e57ce5e
--- /dev/null
+++ b/src/integration/blockchain/boltz/boltz.service.ts
@@ -0,0 +1,18 @@
+import { Injectable } from '@nestjs/common';
+import { GetConfig } from 'src/config/config';
+import { HttpService } from 'src/shared/services/http.service';
+import { BoltzClient } from './boltz-client';
+
+@Injectable()
+export class BoltzService {
+ private readonly client: BoltzClient;
+
+ constructor(http: HttpService) {
+ const config = GetConfig().blockchain.boltz;
+ this.client = new BoltzClient(http, config);
+ }
+
+ getDefaultClient(): BoltzClient {
+ return this.client;
+ }
+}
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
new file mode 100644
index 0000000000..661e8ef7a5
--- /dev/null
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -0,0 +1,172 @@
+import { Injectable } from '@nestjs/common';
+import { BoltzClient, BoltzSwapStatus } from 'src/integration/blockchain/boltz/boltz-client';
+import { BoltzService } from 'src/integration/blockchain/boltz/boltz.service';
+import { CitreaClient } from 'src/integration/blockchain/citrea/citrea-client';
+import { CitreaService } from 'src/integration/blockchain/citrea/citrea.service';
+import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
+import { isAsset } from 'src/shared/models/active';
+import { 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 { LiquidityManagementOrder } from '../../entities/liquidity-management-order.entity';
+import { LiquidityManagementSystem } from '../../enums';
+import { OrderFailedException } from '../../exceptions/order-failed.exception';
+import { OrderNotProcessableException } from '../../exceptions/order-not-processable.exception';
+import { Command, CorrelationId } from '../../interfaces';
+import { LiquidityActionAdapter } from './base/liquidity-action.adapter';
+
+export enum BoltzCommands {
+ DEPOSIT = 'deposit', // BTC -> cBTC via Boltz Reverse Swap
+}
+
+const CORRELATION_PREFIX = {
+ DEPOSIT: 'boltz:deposit:',
+};
+
+const COMPLETED_STATUSES = [BoltzSwapStatus.TRANSACTION_CLAIMED];
+const FAILED_STATUSES = [
+ BoltzSwapStatus.TRANSACTION_FAILED,
+ BoltzSwapStatus.TRANSACTION_REFUNDED,
+ BoltzSwapStatus.SWAP_EXPIRED,
+ BoltzSwapStatus.INVOICE_FAILEDTOPAY,
+];
+
+interface DepositCorrelationData {
+ swapId: string;
+ claimAddress: string;
+ invoiceAmountSats: number;
+}
+
+@Injectable()
+export class BoltzAdapter extends LiquidityActionAdapter {
+ private readonly logger = new DfxLogger(BoltzAdapter);
+
+ protected commands = new Map();
+
+ private readonly boltzClient: BoltzClient;
+ private readonly citreaClient: CitreaClient;
+
+ constructor(
+ boltzService: BoltzService,
+ citreaService: CitreaService,
+ private readonly assetService: AssetService,
+ ) {
+ super(LiquidityManagementSystem.BOLTZ);
+
+ this.boltzClient = boltzService.getDefaultClient();
+ this.citreaClient = citreaService.getDefaultClient();
+
+ this.commands.set(BoltzCommands.DEPOSIT, this.deposit.bind(this));
+ }
+
+ async checkCompletion(order: LiquidityManagementOrder): Promise {
+ const {
+ action: { command },
+ } = order;
+
+ if (command === BoltzCommands.DEPOSIT) {
+ return this.checkDepositCompletion(order);
+ }
+
+ throw new OrderFailedException(`Unknown command: ${command}`);
+ }
+
+ validateParams(_command: string, _params: Record): boolean {
+ return true;
+ }
+
+ //*** COMMANDS ***//
+
+ /**
+ * Deposit BTC -> cBTC via Boltz Reverse Swap.
+ * Creates a reverse swap on Lightning.space, which will send cBTC to the claim address
+ * once the Lightning invoice is paid.
+ */
+ private async deposit(order: LiquidityManagementOrder): Promise {
+ const {
+ maxAmount,
+ pipeline: {
+ rule: { targetAsset: citreaAsset },
+ },
+ } = order;
+
+ // Validate asset is cBTC on Citrea
+ if (citreaAsset.type !== AssetType.COIN || citreaAsset.blockchain !== Blockchain.CITREA) {
+ throw new OrderNotProcessableException('Boltz deposit only supports cBTC (native coin) on Citrea');
+ }
+
+ const claimAddress = this.citreaClient.walletAddress;
+ const invoiceAmountSats = Math.round(maxAmount * 1e8);
+
+ // Create reverse swap via Boltz API
+ const swap = await this.boltzClient.createReverseSwap(claimAddress, invoiceAmountSats);
+
+ this.logger.info(
+ `Boltz reverse swap created: id=${swap.id}, amount=${invoiceAmountSats} sats, claimAddress=${claimAddress}`,
+ );
+
+ // Get asset names for order tracking
+ const btcAsset = await this.assetService.getBtcCoin();
+
+ order.inputAmount = maxAmount;
+ order.inputAsset = btcAsset.name;
+ order.outputAsset = citreaAsset.name;
+
+ const correlationData: DepositCorrelationData = {
+ swapId: swap.id,
+ claimAddress,
+ invoiceAmountSats,
+ };
+
+ return `${CORRELATION_PREFIX.DEPOSIT}${this.encodeCorrelation(correlationData)}`;
+ }
+
+ //*** COMPLETION CHECKS ***//
+
+ private async checkDepositCompletion(order: LiquidityManagementOrder): Promise {
+ const {
+ pipeline: {
+ rule: { target: asset },
+ },
+ } = order;
+
+ if (!isAsset(asset)) {
+ throw new Error('BoltzAdapter.checkDepositCompletion(...) supports only Asset instances as an input.');
+ }
+
+ try {
+ const correlationData = this.decodeCorrelation(
+ order.correlationId.replace(CORRELATION_PREFIX.DEPOSIT, ''),
+ );
+
+ const status = await this.boltzClient.getSwapStatus(correlationData.swapId);
+
+ this.logger.verbose(`Boltz swap ${correlationData.swapId}: status=${status.status}`);
+
+ if (FAILED_STATUSES.includes(status.status)) {
+ throw new OrderFailedException(
+ `Boltz swap failed: ${status.status}${status.failureReason ? ` (${status.failureReason})` : ''}`,
+ );
+ }
+
+ if (COMPLETED_STATUSES.includes(status.status)) {
+ order.outputAmount = correlationData.invoiceAmountSats / 1e8;
+ return true;
+ }
+
+ return false;
+ } catch (e) {
+ throw e instanceof OrderFailedException ? e : new OrderFailedException(e.message);
+ }
+ }
+
+ //*** HELPERS ***//
+
+ private encodeCorrelation(data: DepositCorrelationData): string {
+ return Buffer.from(JSON.stringify(data)).toString('base64');
+ }
+
+ private decodeCorrelation(encoded: string): DepositCorrelationData {
+ return JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
+ }
+}
diff --git a/src/subdomains/core/liquidity-management/enums/index.ts b/src/subdomains/core/liquidity-management/enums/index.ts
index 5bd5c9cf74..8a7b0af4bd 100644
--- a/src/subdomains/core/liquidity-management/enums/index.ts
+++ b/src/subdomains/core/liquidity-management/enums/index.ts
@@ -17,6 +17,7 @@ export enum LiquidityManagementSystem {
BASE_L2_BRIDGE = 'BaseL2Bridge',
LAYERZERO_BRIDGE = 'LayerZeroBridge',
CLEMENTINE_BRIDGE = 'ClementineBridge',
+ BOLTZ = 'Boltz',
LIQUIDITY_PIPELINE = 'LiquidityPipeline',
FRANKENCOIN = 'Frankencoin',
DEURO = 'dEURO',
@@ -70,4 +71,5 @@ export const LiquidityManagementBridges = [
LiquidityManagementSystem.OPTIMISM_L2_BRIDGE,
LiquidityManagementSystem.LAYERZERO_BRIDGE,
LiquidityManagementSystem.CLEMENTINE_BRIDGE,
+ LiquidityManagementSystem.BOLTZ,
];
diff --git a/src/subdomains/core/liquidity-management/factories/liquidity-action-integration.factory.ts b/src/subdomains/core/liquidity-management/factories/liquidity-action-integration.factory.ts
index bab3922616..cd7b6587a8 100644
--- a/src/subdomains/core/liquidity-management/factories/liquidity-action-integration.factory.ts
+++ b/src/subdomains/core/liquidity-management/factories/liquidity-action-integration.factory.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { ArbitrumL2BridgeAdapter } from '../adapters/actions/arbitrum-l2-bridge.adapter';
import { BaseL2BridgeAdapter } from '../adapters/actions/base-l2-bridge.adapter';
import { BinanceAdapter } from '../adapters/actions/binance.adapter';
+import { BoltzAdapter } from '../adapters/actions/boltz.adapter';
import { ClementineBridgeAdapter } from '../adapters/actions/clementine-bridge.adapter';
import { DEuroAdapter } from '../adapters/actions/deuro.adapter';
import { DfxDexAdapter } from '../adapters/actions/dfx-dex.adapter';
@@ -31,6 +32,7 @@ export class LiquidityActionIntegrationFactory {
readonly baseL2BridgeAdapter: BaseL2BridgeAdapter,
readonly layerZeroBridgeAdapter: LayerZeroBridgeAdapter,
readonly clementineBridgeAdapter: ClementineBridgeAdapter,
+ readonly boltzAdapter: BoltzAdapter,
readonly krakenAdapter: KrakenAdapter,
readonly binanceAdapter: BinanceAdapter,
readonly mexcAdapter: MexcAdapter,
@@ -48,6 +50,7 @@ export class LiquidityActionIntegrationFactory {
this.adapters.set(LiquidityManagementSystem.BASE_L2_BRIDGE, baseL2BridgeAdapter);
this.adapters.set(LiquidityManagementSystem.LAYERZERO_BRIDGE, layerZeroBridgeAdapter);
this.adapters.set(LiquidityManagementSystem.CLEMENTINE_BRIDGE, clementineBridgeAdapter);
+ this.adapters.set(LiquidityManagementSystem.BOLTZ, boltzAdapter);
this.adapters.set(LiquidityManagementSystem.KRAKEN, krakenAdapter);
this.adapters.set(LiquidityManagementSystem.BINANCE, binanceAdapter);
this.adapters.set(LiquidityManagementSystem.MEXC, mexcAdapter);
diff --git a/src/subdomains/core/liquidity-management/liquidity-management.module.ts b/src/subdomains/core/liquidity-management/liquidity-management.module.ts
index 3773d7e876..13b11f53cc 100644
--- a/src/subdomains/core/liquidity-management/liquidity-management.module.ts
+++ b/src/subdomains/core/liquidity-management/liquidity-management.module.ts
@@ -13,6 +13,7 @@ import { PricingModule } from 'src/subdomains/supporting/pricing/pricing.module'
import { ArbitrumL2BridgeAdapter } from './adapters/actions/arbitrum-l2-bridge.adapter';
import { BaseL2BridgeAdapter } from './adapters/actions/base-l2-bridge.adapter';
import { BinanceAdapter } from './adapters/actions/binance.adapter';
+import { BoltzAdapter } from './adapters/actions/boltz.adapter';
import { ClementineBridgeAdapter } from './adapters/actions/clementine-bridge.adapter';
import { LayerZeroBridgeAdapter } from './adapters/actions/layerzero-bridge.adapter';
import { DEuroAdapter } from './adapters/actions/deuro.adapter';
@@ -100,6 +101,7 @@ import { LiquidityManagementService } from './services/liquidity-management.serv
BaseL2BridgeAdapter,
LayerZeroBridgeAdapter,
ClementineBridgeAdapter,
+ BoltzAdapter,
BinanceAdapter,
MexcAdapter,
ScryptAdapter,
From 7ef19b4f4e4a5ab13450706964622a2a8088a80a Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Wed, 25 Feb 2026 19:40:40 +0100
Subject: [PATCH 02/13] fix: correct Boltz reverse swap status handling
Use invoice.settled as success status for reverse swaps (not
transaction.claimed which is for submarine swaps). Add complete
SwapUpdateEvent enum matching boltz-backend. Simplify createReverseSwap
params since EVM chains don't need preimageHash/claimPublicKey.
---
.../blockchain/boltz/boltz-client.ts | 38 ++++++++++++++++---
.../adapters/actions/boltz.adapter.ts | 18 ++++-----
2 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/src/integration/blockchain/boltz/boltz-client.ts b/src/integration/blockchain/boltz/boltz-client.ts
index f7330b74b5..72a50aedca 100644
--- a/src/integration/blockchain/boltz/boltz-client.ts
+++ b/src/integration/blockchain/boltz/boltz-client.ts
@@ -5,32 +5,58 @@ export interface BoltzConfig {
apiUrl: string;
}
+// Boltz swap lifecycle events (from boltz-backend SwapUpdateEvent enum)
export enum BoltzSwapStatus {
CREATED = 'swap.created',
+ EXPIRED = 'swap.expired',
+
INVOICE_SET = 'invoice.set',
INVOICE_PENDING = 'invoice.pending',
INVOICE_PAID = 'invoice.paid',
+ INVOICE_SETTLED = 'invoice.settled',
INVOICE_FAILEDTOPAY = 'invoice.failedToPay',
+ INVOICE_EXPIRED = 'invoice.expired',
+
TRANSACTION_MEMPOOL = 'transaction.mempool',
+ TRANSACTION_CLAIM_PENDING = 'transaction.claim.pending',
TRANSACTION_CLAIMED = 'transaction.claimed',
TRANSACTION_CONFIRMED = 'transaction.confirmed',
TRANSACTION_REFUNDED = 'transaction.refunded',
TRANSACTION_FAILED = 'transaction.failed',
- SWAP_EXPIRED = 'swap.expired',
+ TRANSACTION_LOCKUP_FAILED = 'transaction.lockupFailed',
+
+ MINERFEE_PAID = 'minerfee.paid',
}
+// Reverse Swap success: invoice.settled (Boltz paid the Lightning invoice)
+// Reverse Swap failure: swap.expired, transaction.failed, transaction.refunded
+export const ReverseSwapSuccessStatuses = [BoltzSwapStatus.INVOICE_SETTLED];
+export const ReverseSwapFailedStatuses = [
+ BoltzSwapStatus.EXPIRED,
+ BoltzSwapStatus.TRANSACTION_FAILED,
+ BoltzSwapStatus.TRANSACTION_REFUNDED,
+];
+
export interface BoltzReverseSwapResponse {
id: string;
invoice: string;
+ swapTree: {
+ claimLeaf: { output: string; version: number };
+ refundLeaf: { output: string; version: number };
+ };
lockupAddress: string;
onchainAmount: number;
timeoutBlockHeight: number;
- redeemScript?: string;
+ refundPublicKey?: string;
+ blindingKey?: string;
+ refundAddress?: string;
}
export interface BoltzSwapStatusResponse {
status: BoltzSwapStatus;
failureReason?: string;
+ failureDetails?: string;
+ zeroConfRejected?: boolean;
transaction?: {
id: string;
hex?: string;
@@ -45,17 +71,19 @@ export class BoltzClient {
private readonly config: BoltzConfig,
) {}
- async createReverseSwap(claimAddress: string, amount: number): Promise {
+ async createReverseSwap(claimAddress: string, invoiceAmount: number): Promise {
const url = `${this.config.apiUrl}/v2/swap/reverse`;
+ // For EVM chains (cBTC on Citrea), Boltz handles the claim via smart contracts.
+ // No preimageHash or claimPublicKey needed - Boltz generates these internally.
const body = {
from: 'BTC',
to: 'cBTC',
claimAddress,
- invoiceAmount: amount,
+ invoiceAmount,
};
- this.logger.verbose(`Creating reverse swap: ${amount} sats -> ${claimAddress}`);
+ this.logger.verbose(`Creating reverse swap: ${invoiceAmount} sats -> ${claimAddress}`);
return this.http.post(url, body, { tryCount: 3, retryDelay: 2000 });
}
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 661e8ef7a5..0b3d0959dd 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -1,5 +1,9 @@
import { Injectable } from '@nestjs/common';
-import { BoltzClient, BoltzSwapStatus } from 'src/integration/blockchain/boltz/boltz-client';
+import {
+ BoltzClient,
+ ReverseSwapFailedStatuses,
+ ReverseSwapSuccessStatuses,
+} from 'src/integration/blockchain/boltz/boltz-client';
import { BoltzService } from 'src/integration/blockchain/boltz/boltz.service';
import { CitreaClient } from 'src/integration/blockchain/citrea/citrea-client';
import { CitreaService } from 'src/integration/blockchain/citrea/citrea.service';
@@ -23,14 +27,6 @@ const CORRELATION_PREFIX = {
DEPOSIT: 'boltz:deposit:',
};
-const COMPLETED_STATUSES = [BoltzSwapStatus.TRANSACTION_CLAIMED];
-const FAILED_STATUSES = [
- BoltzSwapStatus.TRANSACTION_FAILED,
- BoltzSwapStatus.TRANSACTION_REFUNDED,
- BoltzSwapStatus.SWAP_EXPIRED,
- BoltzSwapStatus.INVOICE_FAILEDTOPAY,
-];
-
interface DepositCorrelationData {
swapId: string;
claimAddress: string;
@@ -143,13 +139,13 @@ export class BoltzAdapter extends LiquidityActionAdapter {
this.logger.verbose(`Boltz swap ${correlationData.swapId}: status=${status.status}`);
- if (FAILED_STATUSES.includes(status.status)) {
+ if (ReverseSwapFailedStatuses.includes(status.status)) {
throw new OrderFailedException(
`Boltz swap failed: ${status.status}${status.failureReason ? ` (${status.failureReason})` : ''}`,
);
}
- if (COMPLETED_STATUSES.includes(status.status)) {
+ if (ReverseSwapSuccessStatuses.includes(status.status)) {
order.outputAmount = correlationData.invoiceAmountSats / 1e8;
return true;
}
From 5bf5cbc6f9b8aec1a763576fe4f3ec3623a0afaf Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Wed, 25 Feb 2026 19:42:25 +0100
Subject: [PATCH 03/13] fix: use Chain Swap instead of Reverse Swap for
BTC->cBTC
BTC onchain -> cBTC onchain is a Chain Swap (not a Reverse Swap which
is Lightning -> onchain). Switch to POST /v2/swap/chain with
preimageHash, claimAddress, userLockAmount. Use correct final events:
transaction.claimed for success.
---
.../blockchain/boltz/boltz-client.ts | 50 +++++++++++++------
.../adapters/actions/boltz.adapter.ts | 39 +++++++++------
2 files changed, 58 insertions(+), 31 deletions(-)
diff --git a/src/integration/blockchain/boltz/boltz-client.ts b/src/integration/blockchain/boltz/boltz-client.ts
index 72a50aedca..1307daf423 100644
--- a/src/integration/blockchain/boltz/boltz-client.ts
+++ b/src/integration/blockchain/boltz/boltz-client.ts
@@ -25,31 +25,41 @@ export enum BoltzSwapStatus {
TRANSACTION_FAILED = 'transaction.failed',
TRANSACTION_LOCKUP_FAILED = 'transaction.lockupFailed',
+ TRANSACTION_SERVER_MEMPOOL = 'transaction.server.mempool',
+ TRANSACTION_SERVER_CONFIRMED = 'transaction.server.confirmed',
+
MINERFEE_PAID = 'minerfee.paid',
}
-// Reverse Swap success: invoice.settled (Boltz paid the Lightning invoice)
-// Reverse Swap failure: swap.expired, transaction.failed, transaction.refunded
-export const ReverseSwapSuccessStatuses = [BoltzSwapStatus.INVOICE_SETTLED];
-export const ReverseSwapFailedStatuses = [
+// Chain Swap final events (BTC onchain -> cBTC onchain)
+// Success: transaction.claimed
+// Fail: swap.expired, transaction.failed, transaction.refunded
+export const ChainSwapSuccessStatuses = [BoltzSwapStatus.TRANSACTION_CLAIMED];
+export const ChainSwapFailedStatuses = [
BoltzSwapStatus.EXPIRED,
BoltzSwapStatus.TRANSACTION_FAILED,
BoltzSwapStatus.TRANSACTION_REFUNDED,
];
-export interface BoltzReverseSwapResponse {
- id: string;
- invoice: string;
+export interface ChainSwapDetails {
swapTree: {
claimLeaf: { output: string; version: number };
refundLeaf: { output: string; version: number };
};
lockupAddress: string;
- onchainAmount: number;
+ serverPublicKey: string;
timeoutBlockHeight: number;
- refundPublicKey?: string;
+ amount: number;
blindingKey?: string;
refundAddress?: string;
+ claimAddress?: string;
+ bip21?: string;
+}
+
+export interface BoltzChainSwapResponse {
+ id: string;
+ claimDetails: ChainSwapDetails;
+ lockupDetails: ChainSwapDetails;
}
export interface BoltzSwapStatusResponse {
@@ -71,21 +81,29 @@ export class BoltzClient {
private readonly config: BoltzConfig,
) {}
- async createReverseSwap(claimAddress: string, invoiceAmount: number): Promise {
- const url = `${this.config.apiUrl}/v2/swap/reverse`;
+ /**
+ * Create a Chain Swap: BTC (onchain) -> cBTC (Citrea onchain)
+ * For EVM destination chains, only claimAddress is needed (no claimPublicKey).
+ * preimageHash is required by the Boltz API.
+ */
+ async createChainSwap(
+ preimageHash: string,
+ claimAddress: string,
+ userLockAmount: number,
+ ): Promise {
+ const url = `${this.config.apiUrl}/v2/swap/chain`;
- // For EVM chains (cBTC on Citrea), Boltz handles the claim via smart contracts.
- // No preimageHash or claimPublicKey needed - Boltz generates these internally.
const body = {
from: 'BTC',
to: 'cBTC',
+ preimageHash,
claimAddress,
- invoiceAmount,
+ userLockAmount,
};
- this.logger.verbose(`Creating reverse swap: ${invoiceAmount} sats -> ${claimAddress}`);
+ this.logger.verbose(`Creating chain swap: ${userLockAmount} sats, BTC -> cBTC, claim=${claimAddress}`);
- return this.http.post(url, body, { tryCount: 3, retryDelay: 2000 });
+ return this.http.post(url, body, { tryCount: 3, retryDelay: 2000 });
}
async getSwapStatus(swapId: string): Promise {
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 0b3d0959dd..0ddb26b7cf 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -1,8 +1,9 @@
import { Injectable } from '@nestjs/common';
+import { randomBytes, createHash } from 'crypto';
import {
BoltzClient,
- ReverseSwapFailedStatuses,
- ReverseSwapSuccessStatuses,
+ ChainSwapFailedStatuses,
+ ChainSwapSuccessStatuses,
} from 'src/integration/blockchain/boltz/boltz-client';
import { BoltzService } from 'src/integration/blockchain/boltz/boltz.service';
import { CitreaClient } from 'src/integration/blockchain/citrea/citrea-client';
@@ -20,7 +21,7 @@ import { Command, CorrelationId } from '../../interfaces';
import { LiquidityActionAdapter } from './base/liquidity-action.adapter';
export enum BoltzCommands {
- DEPOSIT = 'deposit', // BTC -> cBTC via Boltz Reverse Swap
+ DEPOSIT = 'deposit', // BTC onchain -> cBTC onchain via Boltz Chain Swap
}
const CORRELATION_PREFIX = {
@@ -30,7 +31,8 @@ const CORRELATION_PREFIX = {
interface DepositCorrelationData {
swapId: string;
claimAddress: string;
- invoiceAmountSats: number;
+ lockupAddress: string;
+ userLockAmountSats: number;
}
@Injectable()
@@ -74,9 +76,10 @@ export class BoltzAdapter extends LiquidityActionAdapter {
//*** COMMANDS ***//
/**
- * Deposit BTC -> cBTC via Boltz Reverse Swap.
- * Creates a reverse swap on Lightning.space, which will send cBTC to the claim address
- * once the Lightning invoice is paid.
+ * Deposit BTC -> cBTC via Boltz Chain Swap.
+ * Creates a chain swap on Lightning.space (BTC onchain -> cBTC on Citrea).
+ * Boltz provides a lockup address where BTC must be sent.
+ * After confirmation, Boltz sends cBTC to the claim address on Citrea.
*/
private async deposit(order: LiquidityManagementOrder): Promise {
const {
@@ -92,13 +95,18 @@ export class BoltzAdapter extends LiquidityActionAdapter {
}
const claimAddress = this.citreaClient.walletAddress;
- const invoiceAmountSats = Math.round(maxAmount * 1e8);
+ const userLockAmountSats = Math.round(maxAmount * 1e8);
- // Create reverse swap via Boltz API
- const swap = await this.boltzClient.createReverseSwap(claimAddress, invoiceAmountSats);
+ // Generate preimage hash (required by Boltz Chain Swap API)
+ const preimage = randomBytes(32);
+ const preimageHash = createHash('sha256').update(preimage).digest('hex');
+
+ // Create chain swap via Boltz API
+ const swap = await this.boltzClient.createChainSwap(preimageHash, claimAddress, userLockAmountSats);
this.logger.info(
- `Boltz reverse swap created: id=${swap.id}, amount=${invoiceAmountSats} sats, claimAddress=${claimAddress}`,
+ `Boltz chain swap created: id=${swap.id}, amount=${userLockAmountSats} sats, ` +
+ `lockup=${swap.lockupDetails.lockupAddress}, claim=${claimAddress}`,
);
// Get asset names for order tracking
@@ -111,7 +119,8 @@ export class BoltzAdapter extends LiquidityActionAdapter {
const correlationData: DepositCorrelationData = {
swapId: swap.id,
claimAddress,
- invoiceAmountSats,
+ lockupAddress: swap.lockupDetails.lockupAddress,
+ userLockAmountSats,
};
return `${CORRELATION_PREFIX.DEPOSIT}${this.encodeCorrelation(correlationData)}`;
@@ -139,14 +148,14 @@ export class BoltzAdapter extends LiquidityActionAdapter {
this.logger.verbose(`Boltz swap ${correlationData.swapId}: status=${status.status}`);
- if (ReverseSwapFailedStatuses.includes(status.status)) {
+ if (ChainSwapFailedStatuses.includes(status.status)) {
throw new OrderFailedException(
`Boltz swap failed: ${status.status}${status.failureReason ? ` (${status.failureReason})` : ''}`,
);
}
- if (ReverseSwapSuccessStatuses.includes(status.status)) {
- order.outputAmount = correlationData.invoiceAmountSats / 1e8;
+ if (ChainSwapSuccessStatuses.includes(status.status)) {
+ order.outputAmount = correlationData.userLockAmountSats / 1e8;
return true;
}
From ceb7cdb964258dba5ed5884d50224fb9e58a5b04 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 08:46:11 +0100
Subject: [PATCH 04/13] =?UTF-8?q?fix:=20complete=20BTC=E2=86=92cBTC=20chai?=
=?UTF-8?q?n=20swap=20flow=20with=20BTC=20sending=20and=20claiming?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Previously the Boltz adapter created a chain swap but never sent BTC to
the lockup address and never called helpMeClaim, leaving swaps stuck at
swap.created. This implements the full flow:
- Fix API URL (lightning.space/v1) and endpoint paths (/swap/v2/...)
- Add getChainPairs() for pairHash and helpMeClaim() for server-side claiming
- Send BTC to lockup address via BitcoinService after swap creation
- Add state machine (btc_sent → claiming → done) in checkDepositCompletion
- Store preimage/preimageHash in correlation data for claiming
---
src/config/config.ts | 2 +-
.../blockchain/boltz/boltz-client.ts | 65 +++++++-
.../adapters/actions/boltz.adapter.ts | 140 ++++++++++++++++--
3 files changed, 187 insertions(+), 20 deletions(-)
diff --git a/src/config/config.ts b/src/config/config.ts
index b67f3662e6..4847038a0a 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -890,7 +890,7 @@ export class Configuration {
certificate: process.env.LIGHTNING_API_CERTIFICATE?.split('
').join('\n'),
},
boltz: {
- apiUrl: process.env.BOLTZ_API_URL ?? 'https://api.lightning.space',
+ apiUrl: process.env.BOLTZ_API_URL ?? 'https://lightning.space/v1',
},
spark: {
sparkWalletSeed: process.env.SPARK_WALLET_SEED,
diff --git a/src/integration/blockchain/boltz/boltz-client.ts b/src/integration/blockchain/boltz/boltz-client.ts
index 1307daf423..46646691b1 100644
--- a/src/integration/blockchain/boltz/boltz-client.ts
+++ b/src/integration/blockchain/boltz/boltz-client.ts
@@ -73,6 +73,38 @@ export interface BoltzSwapStatusResponse {
};
}
+export interface ChainPairInfo {
+ hash: string;
+ rate: number;
+ limits: {
+ maximal: number;
+ minimal: number;
+ maximalZeroConf: number;
+ };
+ fees: {
+ percentage: number;
+ minerFees: {
+ server: number;
+ user: {
+ claim: number;
+ lockup: number;
+ };
+ };
+ };
+}
+
+// Response: Record>
+export type ChainPairsResponse = Record>;
+
+export interface HelpMeClaimRequest {
+ preimage: string;
+ preimageHash: string;
+}
+
+export interface HelpMeClaimResponse {
+ txHash: string;
+}
+
export class BoltzClient {
private readonly logger = new DfxLogger(BoltzClient);
@@ -81,17 +113,28 @@ export class BoltzClient {
private readonly config: BoltzConfig,
) {}
+ /**
+ * Fetch available chain swap pairs (includes pairHash needed for createChainSwap).
+ */
+ async getChainPairs(): Promise {
+ const url = `${this.config.apiUrl}/swap/v2/swap/chain/`;
+
+ return this.http.get(url, { tryCount: 3, retryDelay: 2000 });
+ }
+
/**
* Create a Chain Swap: BTC (onchain) -> cBTC (Citrea onchain)
* For EVM destination chains, only claimAddress is needed (no claimPublicKey).
- * preimageHash is required by the Boltz API.
+ * preimageHash and pairHash are required by the Boltz API.
*/
async createChainSwap(
preimageHash: string,
claimAddress: string,
userLockAmount: number,
+ pairHash: string,
+ referralId: string,
): Promise {
- const url = `${this.config.apiUrl}/v2/swap/chain`;
+ const url = `${this.config.apiUrl}/swap/v2/swap/chain/`;
const body = {
from: 'BTC',
@@ -99,6 +142,8 @@ export class BoltzClient {
preimageHash,
claimAddress,
userLockAmount,
+ pairHash,
+ referralId,
};
this.logger.verbose(`Creating chain swap: ${userLockAmount} sats, BTC -> cBTC, claim=${claimAddress}`);
@@ -107,8 +152,22 @@ export class BoltzClient {
}
async getSwapStatus(swapId: string): Promise {
- const url = `${this.config.apiUrl}/v2/swap/${swapId}`;
+ const url = `${this.config.apiUrl}/swap/v2/swap/${swapId}`;
return this.http.get(url, { tryCount: 3, retryDelay: 2000 });
}
+
+ /**
+ * Request Boltz to claim cBTC on behalf of the user (server-side claiming).
+ * The preimage proves payment; Boltz uses it to release cBTC to the claim address.
+ */
+ async helpMeClaim(preimage: string, preimageHash: string): Promise {
+ const url = `${this.config.apiUrl}/claim/help-me-claim`;
+
+ const body: HelpMeClaimRequest = { preimage, preimageHash };
+
+ this.logger.verbose(`Requesting help-me-claim for preimageHash=${preimageHash}`);
+
+ return this.http.post(url, body, { tryCount: 3, retryDelay: 2000 });
+ }
}
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 0ddb26b7cf..4eff813db8 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -2,10 +2,13 @@ import { Injectable } from '@nestjs/common';
import { randomBytes, createHash } from 'crypto';
import {
BoltzClient,
+ BoltzSwapStatus,
ChainSwapFailedStatuses,
- ChainSwapSuccessStatuses,
} from 'src/integration/blockchain/boltz/boltz-client';
import { BoltzService } from 'src/integration/blockchain/boltz/boltz.service';
+import { BitcoinBasedClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client';
+import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service';
+import { BitcoinNodeType, BitcoinService } from 'src/integration/blockchain/bitcoin/services/bitcoin.service';
import { CitreaClient } from 'src/integration/blockchain/citrea/citrea-client';
import { CitreaService } from 'src/integration/blockchain/citrea/citrea.service';
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';
@@ -20,6 +23,8 @@ import { OrderNotProcessableException } from '../../exceptions/order-not-process
import { Command, CorrelationId } from '../../interfaces';
import { LiquidityActionAdapter } from './base/liquidity-action.adapter';
+const BOLTZ_REFERRAL_ID = 'DFX';
+
export enum BoltzCommands {
DEPOSIT = 'deposit', // BTC onchain -> cBTC onchain via Boltz Chain Swap
}
@@ -29,10 +34,15 @@ const CORRELATION_PREFIX = {
};
interface DepositCorrelationData {
+ step: 'btc_sent' | 'claiming';
swapId: string;
claimAddress: string;
lockupAddress: string;
userLockAmountSats: number;
+ preimage: string;
+ preimageHash: string;
+ btcTxId: string;
+ pairHash: string;
}
@Injectable()
@@ -42,16 +52,20 @@ export class BoltzAdapter extends LiquidityActionAdapter {
protected commands = new Map();
private readonly boltzClient: BoltzClient;
+ private readonly bitcoinClient: BitcoinBasedClient;
private readonly citreaClient: CitreaClient;
constructor(
boltzService: BoltzService,
+ bitcoinService: BitcoinService,
citreaService: CitreaService,
private readonly assetService: AssetService,
+ private readonly bitcoinFeeService: BitcoinFeeService,
) {
super(LiquidityManagementSystem.BOLTZ);
this.boltzClient = boltzService.getDefaultClient();
+ this.bitcoinClient = bitcoinService.getDefaultClient(BitcoinNodeType.BTC_OUTPUT);
this.citreaClient = citreaService.getDefaultClient();
this.commands.set(BoltzCommands.DEPOSIT, this.deposit.bind(this));
@@ -77,9 +91,11 @@ export class BoltzAdapter extends LiquidityActionAdapter {
/**
* Deposit BTC -> cBTC via Boltz Chain Swap.
- * Creates a chain swap on Lightning.space (BTC onchain -> cBTC on Citrea).
- * Boltz provides a lockup address where BTC must be sent.
- * After confirmation, Boltz sends cBTC to the claim address on Citrea.
+ * 1. Fetch chain pairs to get pairHash
+ * 2. Generate preimage + preimageHash
+ * 3. Create chain swap via API
+ * 4. Send BTC to the lockup address
+ * 5. Save all data in correlation ID for later claiming
*/
private async deposit(order: LiquidityManagementOrder): Promise {
const {
@@ -97,30 +113,55 @@ export class BoltzAdapter extends LiquidityActionAdapter {
const claimAddress = this.citreaClient.walletAddress;
const userLockAmountSats = Math.round(maxAmount * 1e8);
- // Generate preimage hash (required by Boltz Chain Swap API)
- const preimage = randomBytes(32);
- const preimageHash = createHash('sha256').update(preimage).digest('hex');
+ // Step 1: Get chain pairs to extract pairHash
+ const pairs = await this.boltzClient.getChainPairs();
+ const btcPairs = pairs['BTC'];
+ if (!btcPairs || !btcPairs['cBTC']) {
+ throw new OrderNotProcessableException('BTC -> cBTC chain pair not available on Boltz');
+ }
+ const pairHash = btcPairs['cBTC'].hash;
+
+ // Step 2: Generate preimage and hash
+ const preimage = randomBytes(32).toString('hex');
+ const preimageHash = createHash('sha256').update(Buffer.from(preimage, 'hex')).digest('hex');
- // Create chain swap via Boltz API
- const swap = await this.boltzClient.createChainSwap(preimageHash, claimAddress, userLockAmountSats);
+ // Step 3: Create chain swap via Boltz API
+ const swap = await this.boltzClient.createChainSwap(
+ preimageHash,
+ claimAddress,
+ userLockAmountSats,
+ pairHash,
+ BOLTZ_REFERRAL_ID,
+ );
this.logger.info(
`Boltz chain swap created: id=${swap.id}, amount=${userLockAmountSats} sats, ` +
`lockup=${swap.lockupDetails.lockupAddress}, claim=${claimAddress}`,
);
- // Get asset names for order tracking
+ // Step 4: Send BTC to the lockup address
+ const btcTxId = await this.sendBtcToAddress(swap.lockupDetails.lockupAddress, maxAmount);
+
+ this.logger.info(`BTC sent to lockup address: txId=${btcTxId}, amount=${maxAmount} BTC`);
+
+ // Set order tracking fields
const btcAsset = await this.assetService.getBtcCoin();
order.inputAmount = maxAmount;
order.inputAsset = btcAsset.name;
order.outputAsset = citreaAsset.name;
+ // Step 5: Save correlation data
const correlationData: DepositCorrelationData = {
+ step: 'btc_sent',
swapId: swap.id,
claimAddress,
lockupAddress: swap.lockupDetails.lockupAddress,
userLockAmountSats,
+ preimage,
+ preimageHash,
+ btcTxId,
+ pairHash,
};
return `${CORRELATION_PREFIX.DEPOSIT}${this.encodeCorrelation(correlationData)}`;
@@ -146,7 +187,7 @@ export class BoltzAdapter extends LiquidityActionAdapter {
const status = await this.boltzClient.getSwapStatus(correlationData.swapId);
- this.logger.verbose(`Boltz swap ${correlationData.swapId}: status=${status.status}`);
+ this.logger.verbose(`Boltz swap ${correlationData.swapId}: step=${correlationData.step}, status=${status.status}`);
if (ChainSwapFailedStatuses.includes(status.status)) {
throw new OrderFailedException(
@@ -154,19 +195,86 @@ export class BoltzAdapter extends LiquidityActionAdapter {
);
}
- if (ChainSwapSuccessStatuses.includes(status.status)) {
- order.outputAmount = correlationData.userLockAmountSats / 1e8;
- return true;
- }
+ switch (correlationData.step) {
+ case 'btc_sent':
+ return this.handleBtcSentStep(order, correlationData, status.status);
- return false;
+ case 'claiming':
+ return this.handleClaimingStep(order, correlationData, status.status);
+
+ default:
+ throw new OrderFailedException(`Unknown step: ${correlationData.step}`);
+ }
} catch (e) {
throw e instanceof OrderFailedException ? e : new OrderFailedException(e.message);
}
}
+ /**
+ * Step: btc_sent — waiting for Boltz server to confirm the lockup and prepare cBTC.
+ * When server confirms, call helpMeClaim to trigger claiming.
+ */
+ private async handleBtcSentStep(
+ order: LiquidityManagementOrder,
+ correlationData: DepositCorrelationData,
+ status: BoltzSwapStatus,
+ ): Promise {
+ if (status === BoltzSwapStatus.TRANSACTION_SERVER_CONFIRMED) {
+ // Server has confirmed the lockup — request claiming
+ const claimResult = await this.boltzClient.helpMeClaim(correlationData.preimage, correlationData.preimageHash);
+
+ this.logger.info(
+ `Boltz swap ${correlationData.swapId}: helpMeClaim called, claimTxHash=${claimResult.txHash}`,
+ );
+
+ // Advance to claiming step
+ correlationData.step = 'claiming';
+ order.correlationId = `${CORRELATION_PREFIX.DEPOSIT}${this.encodeCorrelation(correlationData)}`;
+
+ return false;
+ }
+
+ // Still waiting for server confirmation
+ return false;
+ }
+
+ /**
+ * Step: claiming — waiting for the claim transaction to be confirmed.
+ */
+ private async handleClaimingStep(
+ order: LiquidityManagementOrder,
+ correlationData: DepositCorrelationData,
+ status: BoltzSwapStatus,
+ ): Promise {
+ if (status === BoltzSwapStatus.TRANSACTION_CLAIMED) {
+ order.outputAmount = correlationData.userLockAmountSats / 1e8;
+
+ this.logger.info(`Boltz swap ${correlationData.swapId}: claimed successfully`);
+
+ return true;
+ }
+
+ // Still waiting for claim confirmation
+ return false;
+ }
+
//*** HELPERS ***//
+ private async sendBtcToAddress(address: string, amount: number): Promise {
+ if (!address || address.length < 26 || address.length > 90) {
+ throw new OrderFailedException(`Invalid Bitcoin address format: ${address}`);
+ }
+
+ const feeRate = await this.bitcoinFeeService.getRecommendedFeeRate();
+ const txId = await this.bitcoinClient.sendMany([{ addressTo: address, amount }], feeRate);
+
+ if (!txId) {
+ throw new OrderFailedException(`Failed to send BTC to address ${address}`);
+ }
+
+ return txId;
+ }
+
private encodeCorrelation(data: DepositCorrelationData): string {
return Buffer.from(JSON.stringify(data)).toString('base64');
}
From 76d64bf544982fd268dd547031621b4d195f59a1 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:04:21 +0100
Subject: [PATCH 05/13] fix: add 0x prefix to preimageHash in helpMeClaim, use
lockup amount from response
The helpMeClaim endpoint expects preimageHash with 0x prefix (verified
against JuiceSwap frontend's prefix0x() call). Also use the actual
lockup amount from the Boltz response instead of the requested amount
for defensive correctness.
---
.../adapters/actions/boltz.adapter.ts | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 4eff813db8..f115d0b35e 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -139,15 +139,16 @@ export class BoltzAdapter extends LiquidityActionAdapter {
`lockup=${swap.lockupDetails.lockupAddress}, claim=${claimAddress}`,
);
- // Step 4: Send BTC to the lockup address
- const btcTxId = await this.sendBtcToAddress(swap.lockupDetails.lockupAddress, maxAmount);
+ // Step 4: Send BTC to the lockup address (use amount from Boltz response, not our request)
+ const lockupAmountBtc = swap.lockupDetails.amount / 1e8;
+ const btcTxId = await this.sendBtcToAddress(swap.lockupDetails.lockupAddress, lockupAmountBtc);
- this.logger.info(`BTC sent to lockup address: txId=${btcTxId}, amount=${maxAmount} BTC`);
+ this.logger.info(`BTC sent to lockup address: txId=${btcTxId}, amount=${lockupAmountBtc} BTC`);
// Set order tracking fields
const btcAsset = await this.assetService.getBtcCoin();
- order.inputAmount = maxAmount;
+ order.inputAmount = lockupAmountBtc;
order.inputAsset = btcAsset.name;
order.outputAsset = citreaAsset.name;
@@ -221,7 +222,7 @@ export class BoltzAdapter extends LiquidityActionAdapter {
): Promise {
if (status === BoltzSwapStatus.TRANSACTION_SERVER_CONFIRMED) {
// Server has confirmed the lockup — request claiming
- const claimResult = await this.boltzClient.helpMeClaim(correlationData.preimage, correlationData.preimageHash);
+ const claimResult = await this.boltzClient.helpMeClaim(correlationData.preimage, `0x${correlationData.preimageHash}`);
this.logger.info(
`Boltz swap ${correlationData.swapId}: helpMeClaim called, claimTxHash=${claimResult.txHash}`,
From 0f5f9ab77f41d252c7fd9e127bd0a1767c644a10 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:17:15 +0100
Subject: [PATCH 06/13] fix: use claimDetails.amount for outputAmount to
account for Boltz fees
The outputAmount was incorrectly set to userLockAmountSats (BTC sent)
instead of claimAmountSats (cBTC received after Boltz fees). Now stores
swap.claimDetails.amount in correlation data and uses it for the final
outputAmount calculation.
---
.../adapters/actions/boltz.adapter.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index f115d0b35e..f8b8132be3 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -39,6 +39,7 @@ interface DepositCorrelationData {
claimAddress: string;
lockupAddress: string;
userLockAmountSats: number;
+ claimAmountSats: number;
preimage: string;
preimageHash: string;
btcTxId: string;
@@ -135,8 +136,8 @@ export class BoltzAdapter extends LiquidityActionAdapter {
);
this.logger.info(
- `Boltz chain swap created: id=${swap.id}, amount=${userLockAmountSats} sats, ` +
- `lockup=${swap.lockupDetails.lockupAddress}, claim=${claimAddress}`,
+ `Boltz chain swap created: id=${swap.id}, lockupAmount=${swap.lockupDetails.amount} sats, ` +
+ `claimAmount=${swap.claimDetails.amount} sats, lockup=${swap.lockupDetails.lockupAddress}, claim=${claimAddress}`,
);
// Step 4: Send BTC to the lockup address (use amount from Boltz response, not our request)
@@ -159,6 +160,7 @@ export class BoltzAdapter extends LiquidityActionAdapter {
claimAddress,
lockupAddress: swap.lockupDetails.lockupAddress,
userLockAmountSats,
+ claimAmountSats: swap.claimDetails.amount,
preimage,
preimageHash,
btcTxId,
@@ -248,9 +250,10 @@ export class BoltzAdapter extends LiquidityActionAdapter {
status: BoltzSwapStatus,
): Promise {
if (status === BoltzSwapStatus.TRANSACTION_CLAIMED) {
- order.outputAmount = correlationData.userLockAmountSats / 1e8;
+ // Use claimAmountSats (cBTC received after Boltz fees), not userLockAmountSats (BTC sent)
+ order.outputAmount = correlationData.claimAmountSats / 1e8;
- this.logger.info(`Boltz swap ${correlationData.swapId}: claimed successfully`);
+ this.logger.info(`Boltz swap ${correlationData.swapId}: claimed successfully, output=${order.outputAmount} cBTC`);
return true;
}
From db1dc1956c5d8cc96c6f7b73a39a3b3680ae0e24 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:19:19 +0100
Subject: [PATCH 07/13] fix: add missing await in try-catch to resolve ESLint
warnings
return-await is required inside try-catch blocks per
@typescript-eslint/return-await rule.
---
.../liquidity-management/adapters/actions/boltz.adapter.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index f8b8132be3..10e9a37392 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -200,10 +200,10 @@ export class BoltzAdapter extends LiquidityActionAdapter {
switch (correlationData.step) {
case 'btc_sent':
- return this.handleBtcSentStep(order, correlationData, status.status);
+ return await this.handleBtcSentStep(order, correlationData, status.status);
case 'claiming':
- return this.handleClaimingStep(order, correlationData, status.status);
+ return await this.handleClaimingStep(order, correlationData, status.status);
default:
throw new OrderFailedException(`Unknown step: ${correlationData.step}`);
From 1cc8b88927c283f65d372b9a27b7cce9e1b24025 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:33:43 +0100
Subject: [PATCH 08/13] feat: add migration to create Boltz action and activate
cBTC rule
Creates Boltz deposit action and wires it as onFail fallback for
Clementine (Action 236). Activates Rule 320 (Citrea cBTC) with
thresholds: minimal=0, optimal=0.1, maximal=0.5.
Strategy: Clementine stays primary (fee-free, 10 BTC fixed). When it
fails (e.g. balance < 10 BTC), Boltz handles flexible amounts as
fallback.
---
.../1772100000000-AddBoltzLiquidityAction.js | 91 +++++++++++++++++++
1 file changed, 91 insertions(+)
create mode 100644 migration/1772100000000-AddBoltzLiquidityAction.js
diff --git a/migration/1772100000000-AddBoltzLiquidityAction.js b/migration/1772100000000-AddBoltzLiquidityAction.js
new file mode 100644
index 0000000000..88c6df4262
--- /dev/null
+++ b/migration/1772100000000-AddBoltzLiquidityAction.js
@@ -0,0 +1,91 @@
+/**
+ * @typedef {import('typeorm').MigrationInterface} MigrationInterface
+ * @typedef {import('typeorm').QueryRunner} QueryRunner
+ */
+
+/**
+ * Create Boltz deposit action and wire it as onFail fallback for Clementine (Action 236).
+ * Configure and activate Rule 320 (Citrea cBTC) with thresholds.
+ *
+ * Strategy: Clementine (fee-free, 10 BTC fixed) remains primary deficit action.
+ * When Clementine fails (e.g. insufficient balance < 10 BTC), Boltz handles
+ * flexible amounts (with fees) as fallback.
+ *
+ * @class
+ * @implements {MigrationInterface}
+ */
+module.exports = class AddBoltzLiquidityAction1772100000000 {
+ name = 'AddBoltzLiquidityAction1772100000000';
+
+ /**
+ * @param {QueryRunner} queryRunner
+ */
+ async up(queryRunner) {
+ // Step 1: Create Boltz deposit action
+ await queryRunner.query(`
+ INSERT INTO "dbo"."liquidity_management_action" ("system", "command", "tag")
+ VALUES ('Boltz', 'deposit', 'cBTC')
+ `);
+
+ // Step 2: Get the newly created action ID
+ const [boltzAction] = await queryRunner.query(`
+ SELECT "id" FROM "dbo"."liquidity_management_action"
+ WHERE "system" = 'Boltz' AND "command" = 'deposit'
+ `);
+
+ if (!boltzAction) {
+ throw new Error('Failed to create Boltz action');
+ }
+
+ console.log(`Created Boltz action with id=${boltzAction.id}`);
+
+ // Step 3: Set Boltz as onFail fallback for Clementine (Action 236)
+ await queryRunner.query(`
+ UPDATE "dbo"."liquidity_management_action"
+ SET "onFailId" = ${boltzAction.id}
+ WHERE "id" = 236
+ `);
+
+ // Step 4: Configure and activate Rule 320 (Citrea cBTC)
+ await queryRunner.query(`
+ UPDATE "dbo"."liquidity_management_rule"
+ SET "status" = 'Active',
+ "minimal" = 0,
+ "optimal" = 0.1,
+ "maximal" = 0.5,
+ "reactivationTime" = 10
+ WHERE "id" = 320
+ `);
+
+ console.log('Rule 320 activated: minimal=0, optimal=0.1, maximal=0.5');
+ }
+
+ /**
+ * @param {QueryRunner} queryRunner
+ */
+ async down(queryRunner) {
+ // Revert Rule 320 to inactive
+ await queryRunner.query(`
+ UPDATE "dbo"."liquidity_management_rule"
+ SET "status" = 'Inactive',
+ "minimal" = NULL,
+ "optimal" = NULL,
+ "maximal" = NULL,
+ "reactivationTime" = NULL
+ WHERE "id" = 320
+ `);
+
+ // Remove onFail link from Clementine action
+ await queryRunner.query(`
+ UPDATE "dbo"."liquidity_management_action"
+ SET "onFailId" = NULL
+ WHERE "id" = 236
+ `);
+
+ // Delete Boltz action
+ await queryRunner.query(`
+ DELETE FROM "dbo"."liquidity_management_action"
+ WHERE "system" = 'Boltz' AND "command" = 'deposit'
+ `);
+ }
+};
From b2781ae5833d85ae5a7e809a5896ac73c649f9f1 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:41:48 +0100
Subject: [PATCH 09/13] feat: add refundPublicKey to chain swap for BTC refund
on failure
Generate secp256k1 key pair and send compressed public key as
refundPublicKey in createChainSwap request. Store private key in
correlation data for potential refund signing if swap fails.
---
.../blockchain/boltz/boltz-client.ts | 3 +++
.../adapters/actions/boltz.adapter.ts | 21 +++++++++++++------
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/src/integration/blockchain/boltz/boltz-client.ts b/src/integration/blockchain/boltz/boltz-client.ts
index 46646691b1..b4ff6dc9d3 100644
--- a/src/integration/blockchain/boltz/boltz-client.ts
+++ b/src/integration/blockchain/boltz/boltz-client.ts
@@ -125,6 +125,7 @@ export class BoltzClient {
/**
* Create a Chain Swap: BTC (onchain) -> cBTC (Citrea onchain)
* For EVM destination chains, only claimAddress is needed (no claimPublicKey).
+ * refundPublicKey is required for BTC sender side to enable refunds on swap failure.
* preimageHash and pairHash are required by the Boltz API.
*/
async createChainSwap(
@@ -133,6 +134,7 @@ export class BoltzClient {
userLockAmount: number,
pairHash: string,
referralId: string,
+ refundPublicKey: string,
): Promise {
const url = `${this.config.apiUrl}/swap/v2/swap/chain/`;
@@ -144,6 +146,7 @@ export class BoltzClient {
userLockAmount,
pairHash,
referralId,
+ refundPublicKey,
};
this.logger.verbose(`Creating chain swap: ${userLockAmount} sats, BTC -> cBTC, claim=${claimAddress}`);
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 10e9a37392..f5d747362d 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { randomBytes, createHash } from 'crypto';
+import { secp256k1 } from '@noble/curves/secp256k1';
import {
BoltzClient,
BoltzSwapStatus,
@@ -44,6 +45,7 @@ interface DepositCorrelationData {
preimageHash: string;
btcTxId: string;
pairHash: string;
+ refundPrivateKey: string;
}
@Injectable()
@@ -94,9 +96,10 @@ export class BoltzAdapter extends LiquidityActionAdapter {
* Deposit BTC -> cBTC via Boltz Chain Swap.
* 1. Fetch chain pairs to get pairHash
* 2. Generate preimage + preimageHash
- * 3. Create chain swap via API
- * 4. Send BTC to the lockup address
- * 5. Save all data in correlation ID for later claiming
+ * 3. Generate secp256k1 refund key pair
+ * 4. Create chain swap via API
+ * 5. Send BTC to the lockup address
+ * 6. Save all data in correlation ID for later claiming
*/
private async deposit(order: LiquidityManagementOrder): Promise {
const {
@@ -126,13 +129,18 @@ export class BoltzAdapter extends LiquidityActionAdapter {
const preimage = randomBytes(32).toString('hex');
const preimageHash = createHash('sha256').update(Buffer.from(preimage, 'hex')).digest('hex');
- // Step 3: Create chain swap via Boltz API
+ // Step 3: Generate secp256k1 key pair for BTC refund (in case swap fails)
+ const refundPrivateKey = randomBytes(32).toString('hex');
+ const refundPublicKey = Buffer.from(secp256k1.getPublicKey(refundPrivateKey, true)).toString('hex');
+
+ // Step 4: Create chain swap via Boltz API
const swap = await this.boltzClient.createChainSwap(
preimageHash,
claimAddress,
userLockAmountSats,
pairHash,
BOLTZ_REFERRAL_ID,
+ refundPublicKey,
);
this.logger.info(
@@ -140,7 +148,7 @@ export class BoltzAdapter extends LiquidityActionAdapter {
`claimAmount=${swap.claimDetails.amount} sats, lockup=${swap.lockupDetails.lockupAddress}, claim=${claimAddress}`,
);
- // Step 4: Send BTC to the lockup address (use amount from Boltz response, not our request)
+ // Step 5: Send BTC to the lockup address (use amount from Boltz response, not our request)
const lockupAmountBtc = swap.lockupDetails.amount / 1e8;
const btcTxId = await this.sendBtcToAddress(swap.lockupDetails.lockupAddress, lockupAmountBtc);
@@ -153,7 +161,7 @@ export class BoltzAdapter extends LiquidityActionAdapter {
order.inputAsset = btcAsset.name;
order.outputAsset = citreaAsset.name;
- // Step 5: Save correlation data
+ // Step 6: Save correlation data
const correlationData: DepositCorrelationData = {
step: 'btc_sent',
swapId: swap.id,
@@ -165,6 +173,7 @@ export class BoltzAdapter extends LiquidityActionAdapter {
preimageHash,
btcTxId,
pairHash,
+ refundPrivateKey,
};
return `${CORRELATION_PREFIX.DEPOSIT}${this.encodeCorrelation(correlationData)}`;
From 29b323256ef3bb670b07b68ff245bfb8088a8fdd Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:49:25 +0100
Subject: [PATCH 10/13] style: fix Prettier formatting in Boltz adapter
---
.../adapters/actions/boltz.adapter.ts | 23 ++++++++-----------
1 file changed, 10 insertions(+), 13 deletions(-)
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index f5d747362d..1ce0e1a8b1 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -1,11 +1,7 @@
import { Injectable } from '@nestjs/common';
import { randomBytes, createHash } from 'crypto';
import { secp256k1 } from '@noble/curves/secp256k1';
-import {
- BoltzClient,
- BoltzSwapStatus,
- ChainSwapFailedStatuses,
-} from 'src/integration/blockchain/boltz/boltz-client';
+import { BoltzClient, BoltzSwapStatus, ChainSwapFailedStatuses } from 'src/integration/blockchain/boltz/boltz-client';
import { BoltzService } from 'src/integration/blockchain/boltz/boltz.service';
import { BitcoinBasedClient } from 'src/integration/blockchain/bitcoin/node/bitcoin-based-client';
import { BitcoinFeeService } from 'src/integration/blockchain/bitcoin/services/bitcoin-fee.service';
@@ -193,13 +189,13 @@ export class BoltzAdapter extends LiquidityActionAdapter {
}
try {
- const correlationData = this.decodeCorrelation(
- order.correlationId.replace(CORRELATION_PREFIX.DEPOSIT, ''),
- );
+ const correlationData = this.decodeCorrelation(order.correlationId.replace(CORRELATION_PREFIX.DEPOSIT, ''));
const status = await this.boltzClient.getSwapStatus(correlationData.swapId);
- this.logger.verbose(`Boltz swap ${correlationData.swapId}: step=${correlationData.step}, status=${status.status}`);
+ this.logger.verbose(
+ `Boltz swap ${correlationData.swapId}: step=${correlationData.step}, status=${status.status}`,
+ );
if (ChainSwapFailedStatuses.includes(status.status)) {
throw new OrderFailedException(
@@ -233,12 +229,13 @@ export class BoltzAdapter extends LiquidityActionAdapter {
): Promise {
if (status === BoltzSwapStatus.TRANSACTION_SERVER_CONFIRMED) {
// Server has confirmed the lockup — request claiming
- const claimResult = await this.boltzClient.helpMeClaim(correlationData.preimage, `0x${correlationData.preimageHash}`);
-
- this.logger.info(
- `Boltz swap ${correlationData.swapId}: helpMeClaim called, claimTxHash=${claimResult.txHash}`,
+ const claimResult = await this.boltzClient.helpMeClaim(
+ correlationData.preimage,
+ `0x${correlationData.preimageHash}`,
);
+ this.logger.info(`Boltz swap ${correlationData.swapId}: helpMeClaim called, claimTxHash=${claimResult.txHash}`);
+
// Advance to claiming step
correlationData.step = 'claiming';
order.correlationId = `${CORRELATION_PREFIX.DEPOSIT}${this.encodeCorrelation(correlationData)}`;
From d856cfe1fe4b1162de5e2afac9b5688d19dd82b3 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 10:01:30 +0100
Subject: [PATCH 11/13] fix: add transaction.lockupFailed to chain swap failure
statuses
Without this status, a failed lockup would leave the swap polling
indefinitely. Verified against JuiceSwap bapp which includes
lockupFailed in its swapStatusFailed list.
---
src/integration/blockchain/boltz/boltz-client.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/integration/blockchain/boltz/boltz-client.ts b/src/integration/blockchain/boltz/boltz-client.ts
index b4ff6dc9d3..fafd636023 100644
--- a/src/integration/blockchain/boltz/boltz-client.ts
+++ b/src/integration/blockchain/boltz/boltz-client.ts
@@ -33,11 +33,12 @@ export enum BoltzSwapStatus {
// Chain Swap final events (BTC onchain -> cBTC onchain)
// Success: transaction.claimed
-// Fail: swap.expired, transaction.failed, transaction.refunded
+// Fail: swap.expired, transaction.failed, transaction.lockupFailed, transaction.refunded
export const ChainSwapSuccessStatuses = [BoltzSwapStatus.TRANSACTION_CLAIMED];
export const ChainSwapFailedStatuses = [
BoltzSwapStatus.EXPIRED,
BoltzSwapStatus.TRANSACTION_FAILED,
+ BoltzSwapStatus.TRANSACTION_LOCKUP_FAILED,
BoltzSwapStatus.TRANSACTION_REFUNDED,
];
From e770e04da4c1d1ad3885a9fccdb2aed3bdce4e78 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 10:39:44 +0100
Subject: [PATCH 12/13] feat: validate swap amount against Boltz pair limits
before creation
Fetch dynamic min/max limits from Boltz chain pairs API and reject
orders where the amount falls outside the allowed range. Prevents
invalid swap creation and allows the order to retry later.
---
.../adapters/actions/boltz.adapter.ts | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 1ce0e1a8b1..8936f08365 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -119,7 +119,20 @@ export class BoltzAdapter extends LiquidityActionAdapter {
if (!btcPairs || !btcPairs['cBTC']) {
throw new OrderNotProcessableException('BTC -> cBTC chain pair not available on Boltz');
}
- const pairHash = btcPairs['cBTC'].hash;
+ const pairInfo = btcPairs['cBTC'];
+ const pairHash = pairInfo.hash;
+
+ // Validate amount against Boltz pair limits (limits are in sats and change dynamically)
+ if (userLockAmountSats < pairInfo.limits.minimal) {
+ throw new OrderNotProcessableException(
+ `Amount ${userLockAmountSats} sats below Boltz minimum of ${pairInfo.limits.minimal} sats`,
+ );
+ }
+ if (userLockAmountSats > pairInfo.limits.maximal) {
+ throw new OrderNotProcessableException(
+ `Amount ${userLockAmountSats} sats above Boltz maximum of ${pairInfo.limits.maximal} sats`,
+ );
+ }
// Step 2: Generate preimage and hash
const preimage = randomBytes(32).toString('hex');
From a1aba385b3cf5e53a95b12520a87533c27e9a5a7 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 26 Feb 2026 10:52:41 +0100
Subject: [PATCH 13/13] fix: store claimTxHash for idempotent claiming and
include failureDetails in errors
Persist claim transaction hash in correlation data so helpMeClaim is
not called again on restart. Include both failureReason and
failureDetails from Boltz status response in error messages.
---
.../adapters/actions/boltz.adapter.ts | 22 +++++++++++--------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
index 8936f08365..ef8e6e43ec 100644
--- a/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
+++ b/src/subdomains/core/liquidity-management/adapters/actions/boltz.adapter.ts
@@ -42,6 +42,7 @@ interface DepositCorrelationData {
btcTxId: string;
pairHash: string;
refundPrivateKey: string;
+ claimTxHash?: string;
}
@Injectable()
@@ -211,9 +212,8 @@ export class BoltzAdapter extends LiquidityActionAdapter {
);
if (ChainSwapFailedStatuses.includes(status.status)) {
- throw new OrderFailedException(
- `Boltz swap failed: ${status.status}${status.failureReason ? ` (${status.failureReason})` : ''}`,
- );
+ const details = [status.failureReason, status.failureDetails].filter(Boolean).join(' - ');
+ throw new OrderFailedException(`Boltz swap failed: ${status.status}${details ? ` (${details})` : ''}`);
}
switch (correlationData.step) {
@@ -241,13 +241,17 @@ export class BoltzAdapter extends LiquidityActionAdapter {
status: BoltzSwapStatus,
): Promise {
if (status === BoltzSwapStatus.TRANSACTION_SERVER_CONFIRMED) {
- // Server has confirmed the lockup — request claiming
- const claimResult = await this.boltzClient.helpMeClaim(
- correlationData.preimage,
- `0x${correlationData.preimageHash}`,
- );
+ // Server has confirmed the lockup — request claiming (skip if already called)
+ if (!correlationData.claimTxHash) {
+ const claimResult = await this.boltzClient.helpMeClaim(
+ correlationData.preimage,
+ `0x${correlationData.preimageHash}`,
+ );
- this.logger.info(`Boltz swap ${correlationData.swapId}: helpMeClaim called, claimTxHash=${claimResult.txHash}`);
+ this.logger.info(`Boltz swap ${correlationData.swapId}: helpMeClaim called, claimTxHash=${claimResult.txHash}`);
+
+ correlationData.claimTxHash = claimResult.txHash;
+ }
// Advance to claiming step
correlationData.step = 'claiming';