Skip to content

Conversation

@sekulicd
Copy link
Contributor

@sekulicd sekulicd commented Jan 30, 2026

Overview

  • Added chain swap feature (Ark↔BTC) end‑to‑end: API, domain, storage, swap engine, monitoring, refunds, and recovery on restart.
  • Introduced a mock Boltz server + test harness to deterministically exercise unhappy paths.
  • Expanded e2e coverage with real Boltz happy paths and mock‑driven refund paths, plus restart/recovery scenarios.

RPCs Added

Service RPCs

  • CreateChainSwap — start Ark→BTC or BTC→Ark swap
  • ListChainSwaps — list chain swap records
  • RefundChainSwap — initiate refund

Domain & Persistence

  • New chain swap domain model in chainswap.go:
    • Status enum: pending, user_locked, server_locked, claimed, failed, refund_failed, refunded, refunded_unilaterally.
    • Helpers: IsTerminalChainSwapStatus, ShouldRefundChainSwapStatus, ShouldResumeChainSwapStatus.
  • New chain_swap table + SQLC repo:
    • Stores swap direction, amount, status, lockup txids, refund/claim txids, preimage, Boltz response JSON, BTC address, etc.
    • Status CHECK now includes 8 (refunded_unilaterally).
  • Repository + repo manager wiring added.

Core App Logic

Create flow

  • CreateChainSwapArkToBtc and CreateBtcToArkChainSwap in service.go.

Refund flow

  • RefundChainSwap supports:
    • Ark→BTC cooperative refund via Boltz + schedules unilateral fallback.
    • BTC→ARK refund via script‑path spend + boarding + settle.

Recovery on restart

  • On unlock, service now loads all chain swaps:
    • Refunds failed / refund_failed / user_locked_failed.
    • Resumes monitoring for in‑flight swaps.
  • Resumption rebuilds swap state from DB and starts monitoring.

Decision Points

  • Persist Boltz response JSON + preimage to enable full rehydration after restart.
  • Recovery splits into “refund vs resume” via domain helpers for clarity.

Chain Swap (pkg/swap)

  • New chain swap implementation:
    • Ark→BTC and BTC→Ark handlers with explicit state transitions.
    • Cooperative claim via MuSig2, fallback script‑path spend.
    • Refund flows for both directions.
  • New monitoring framework with unified WS event loop + swap‑specific handlers.
  • Validation:
    • VHTLC validation against expected address to prevent mismatch/scam.
    • BTC lockup address validation against swap tree.
  • BTC tooling:
    • btc_util.go, explorer.go for tx building, fee calc, and esplora access.
  • chainswap_resume.go: rehydrate swap from DB and re‑start monitoring.

Boltz Integration (pkg/boltz)

  • Added chain swap types + endpoints + websocket updates to support new flows.

Mock Boltz

  • New stateful mock server (server.go):
    • REST + WS support, admin endpoints to reset/config/push events.
    • Used for unhappy‑path flows (refunds, quote negotiation, lockup failure).
  • New Docker image build file (renamed) mockboltz.Dockerfile.

Tests

  • New e2e chain swap suite in chainswap_test.go:

    • Real Boltz happy paths.
    • Mocked: script‑path claim, cooperative refund, unilateral refund, quote mismatch, refund RPC.
    • Recovery tests: restart fulmine / fulmine‑mock to verify resume/refund behavior.
  • Tests added:
    ark to btc happy path with cooperative claim
    ark to btc happy path with unilateral claim via script path spend
    ark to btc swap fails with cooperative refund
    ark to btc with unilaterl refund which is scheduled after some timeout depending on transaction locktime
    ark to btc with wrong amount locked up by user which triggers quote negotiation flow
    btc to ark flow happy path
    btc to ark unilateral refund

  • Test utilities:

    • Unified Nigiri helpers.
    • Restart helpers (docker compose).
    • Auto‑unlock + auto‑settle after restart.

Infra / Docker / Setup

  • test.docker-compose.yml:
    • Added mock‑boltz + fulmine‑mock stack.
    • Switched fulmine & fulmine‑mock to named volumes (persistent DB for recovery tests).
  • scripts/setup expanded to provision mock‑boltz + fulmine‑mock.
  • arkd.Dockerfile / arkdwallet.Dockerfile updated to Go 1.25.6 (aligns with arkd go.mod).

Misc

  • .gitignore now ignores .worktrees (local dev).

@altafan @louisinger @bordalix please review.

Summary by CodeRabbit

  • New Features
    • Added ARK↔BTC chain-swap support with create, list, and refund endpoints and lifecycle tracking.
    • Support for both directions, cooperative MuSig2 claims and script-path claims, recovery/resume, and refund flows.
  • Persistence & APIs
    • Swap state persisted and queryable; public API types/responses added.
  • Tests
    • End-to-end test suite and a configurable mock Boltz server for swap scenarios.
  • Chores
    • Test compose and tooling updates to run mock services and local integration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Ark↔BTC chain-swap functionality: new API endpoints and protobuf RPCs, domain model and SQLite persistence, Boltz client chain-swap methods, swap monitoring/handlers (MuSig2 & Taproot claim paths), validation/explorer utilities, recovery/refund flows, extensive E2E tests and a mock Boltz server.

Changes

Cohort / File(s) Summary
API Spec & Protobuf
api-spec/openapi/.../service.swagger.json, api-spec/protobuf/fulmine/v1/service.proto
Adds CreateChainSwap, ListChainSwaps, RefundChainSwap endpoints/RPCs and associated request/response types and SwapDirection enum.
Domain & Repo API
internal/core/domain/chainswap.go, internal/core/ports/repo_manager.go
New ChainSwap domain model, status enum, state transition helpers, and ChainSwapRepository interface; RepoManager extended with ChainSwaps().
Service Layer
internal/core/application/service.go, internal/config/permissions.go, internal/interface/grpc/handlers/service_handler.go
Service methods for creating/listing/refunding chain swaps, event handling/recovery/scheduling, permission entries, and gRPC handlers mapping domain↔proto.
Database & Migrations
internal/infrastructure/db/sqlite/migration/*, internal/infrastructure/db/sqlite/sqlc/*, internal/infrastructure/db/sqlite/chainswap_repo.go, internal/infrastructure/db/service.go
Adds chain_swap table migration, sqlc queries/models for full CRUD/listing, SQLite ChainSwapRepository implementation, and wiring into DB service.
Boltz Client
pkg/boltz/boltz.go, pkg/boltz/types.go, pkg/boltz/ws.go, pkg/boltz/go.mod
Adds chain-swap Boltz API methods, many new chain-swap types, generic callApi helper and HTTPError, plus websocket panic logging and dependency tweaks.
Swap Core & Monitor
pkg/swap/chainswap.go, pkg/swap/chainswap_monitor.go, pkg/swap/chainswap_resume.go
Implements in-process ChainSwap model with event callbacks, generic monitoring framework, resume capability and unilateral refund hooks.
Direction-specific Handlers
pkg/swap/chainswap_arkt_btc_handler.go, pkg/swap/chainswap_btc_ark_handler.go
Ark→BTC and BTC→Ark handlers implementing funding, lockup handling, cooperative MuSig2 claim and script-path claim, and refund flows.
Crypto & BTC Utilities
pkg/swap/musig.go, pkg/swap/btc_util.go, pkg/swap/chainswap_validate.go, pkg/swap/explorer.go
MuSig2 helpers, Taproot/MuSig2 signing/verification, transaction/fee utilities, swap-tree Merkle utilities, HTLC/script parsing/validation, and an ExplorerClient implementation.
Swap Handler Integration
pkg/swap/swap.go, pkg/swap/batch_handler.go, pkg/swap/go.mod
SwapHandler now accepts esploraURL & privateKey, adds SwapTypeChain, dynamic refund routing; small batch session handler stub and dependency updates.
E2E Tests & Mocks
internal/test/e2e/chainswap_test.go, internal/test/e2e/*, internal/test/mockboltz/server.go, mockboltz.Dockerfile
Comprehensive E2E suite for chain swaps, helper utilities, docker-compose-aware test helpers, and a feature-rich mock Boltz server plus Dockerfile.
Tooling & Scripts
test.docker-compose.yml, scripts/setup, .gitignore, arkd.Dockerfile, arkdwallet.Dockerfile
Adds mock services (mock-boltz, fulmine-mock), setup script provisioning for mocks, minor Dockerfile and docker-compose updates, and .worktrees ignore.
Other
go.mod, pkg/boltz/go.mod, pkg/swap/go.mod
Dependency updates and replace directives (logrus, btcec, vhtlc, gorilla/websocket, ltcsuite/ltcd transport).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FulmineService
    participant BoltzAPI
    participant BTCNetwork
    participant ARKNetwork

    rect rgba(200,150,255,0.5)
    Note over Client,FulmineService: Ark→BTC flow
    Client->>FulmineService: CreateChainSwap(amount, btcAddress)
    FulmineService->>BoltzAPI: CreateChainSwap(Ark→BTC)
    BoltzAPI-->>FulmineService: {id, preimage, lockupAddr,...}
    FulmineService->>ARKNetwork: Fund VHTLC
    FulmineService->>FulmineService: Monitor swap
    BoltzAPI->>FulmineService: UserLocked
    FulmineService->>BoltzAPI: Get claim details / submit partial sigs
    BoltzAPI->>BTCNetwork: Server locks BTC
    FulmineService->>BTCNetwork: Broadcast claim tx
    BTCNetwork-->>FulmineService: Confirmations
    end

    rect rgba(150,200,255,0.5)
    Note over Client,FulmineService: BTC→Ark flow
    Client->>FulmineService: CreateChainSwap(amount)
    FulmineService->>BoltzAPI: CreateChainSwap(BTC→Ark)
    BoltzAPI-->>FulmineService: {id, preimage,...}
    Client->>BTCNetwork: Lock BTC to address
    BoltzAPI->>FulmineService: UserLocked
    FulmineService->>ARKNetwork: Claim VTXO
    FulmineService->>BoltzAPI: Submit cooperative signature
    BoltzAPI-->>FulmineService: Claim finalized
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • PR #328: Overlaps submarine/chain swap control flows and refund handling — strong code-level overlap.
  • PR #235: Related swap package refactors and swap-handling additions affecting the same modules.
  • PR #330: Changes cooperative refund/PSBT flow used by chain-swap refund paths.

Suggested labels

enhancement

Suggested reviewers

  • altafan

Poem

🐰 I found a bridge of hashes and keys—
Ark hops to Bitcoin on taprooted breeze.
MuSig2 twinkles, Boltz hums the tune,
Swaps dance at dusk, and nodes sing by moon.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Chain Swap' directly and concisely summarizes the main feature introduced: a complete chain swap (ARK↔BTC) implementation with API, persistence, monitoring, refunds, and recovery.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Chain-Swap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
pkg/boltz/ws.go (1)

233-247: ⚠️ Potential issue | 🟠 Major

Guard against zero/negative retryInterval to avoid tight retry loops.

Retry uses time.After(interval), so retryInterval <= 0 will effectively spin and hammer the backend/CPU. Add a guard (default or error) before the first retry loop.

🛠️ Suggested fix
 func (boltz *Websocket) ConnectAndSubscribe(ctx context.Context, swapIds []string, retryInterval time.Duration) error {
+	if retryInterval <= 0 {
+		retryInterval = 5 * time.Second
+	}
 	err := Retry(ctx, retryInterval, func(ctx context.Context) (bool, error) {
 		err := boltz.Connect()
 		if err != nil {
 			return false, nil
pkg/swap/go.mod (1)

10-15: ⚠️ Potential issue | 🔴 Critical

Do not use unreleased go-sdk versions without documented justification.

This commit locks pkg/swap to an unreleased pseudo-version (v0.8.2-0.20260121113217-12e71e4566e6, dated 2026-01-21) of go-sdk, yet the latest published tag is only v0.8.1 (Oct 2025). The pseudo-version has no release notes, changelog, or documented reason for the bump. While go-sdk is actively used across swap handlers (swap.go, utils.go, batch_handler.go, chainswap_arkt_btc_handler.go) for client, indexer, and types imports, pinning to an undocumented unreleased commit creates an unmaintainable and unverifiable dependency. Either release the pinned go-sdk version with documented changes, or revert to the latest stable release (v0.8.1) and defer the bump to a future PR with clear justification.

pkg/swap/swap.go (2)

71-94: ⚠️ Potential issue | 🟠 Major

Validate privateKey before calling PubKey() to prevent panic.

privateKey.PubKey() will panic if privateKey is nil. Add a guard and return an explicit error.

🛠️ Proposed fix
 func NewSwapHandler(
 	arkClient arksdk.ArkClient,
 	transportClient client.TransportClient,
 	indexerClient indexer.Indexer,
 	boltzSvc *boltz.Api,
 	esploraURL string,
 	privateKey *btcec.PrivateKey,
 	timeout uint32,
 ) (*SwapHandler, error) {
+	if privateKey == nil {
+		return nil, fmt.Errorf("private key is required")
+	}
 	cfg, err := arkClient.GetConfigData(context.Background())
 	if err != nil {
 		return nil, fmt.Errorf("failed to get config data: %w", err)
 	}

1083-1113: ⚠️ Potential issue | 🟠 Major

Handle errors/unknown swap types before dereferencing refundResp.

refundResp can be nil if the RPC call fails or if swapType is unsupported, leading to a nil deref. Check err and add a default case.

🛠️ Proposed fix
 	switch swapType {
 	case SwapTypeSubmarine:
 		refundResp, err = h.boltzSvc.RefundSubmarine(swapId, boltz.RefundSwapRequest{
 			Transaction: refundTx,
 			Checkpoint:  checkpointTx,
 		})
 	case SwapTypeChain:
 		refundResp, err = h.boltzSvc.RefundChainSwap(swapId, boltz.RefundSwapRequest{
 			Transaction: refundTx,
 			Checkpoint:  checkpointTx,
 		})
+	default:
+		return nil, nil, fmt.Errorf("unknown swapType %q", swapType)
 	}
+	if err != nil {
+		return nil, nil, err
+	}
+	if refundResp == nil {
+		return nil, nil, fmt.Errorf("empty refund response")
+	}
internal/infrastructure/db/service.go (2)

214-220: ⚠️ Potential issue | 🟠 Major

Missing chainSwapRepo.Close() in the Close() method.

The ChainSwapRepository interface includes a Close() method, but service.Close() doesn't call it. This could lead to resource leaks.

🐛 Proposed fix
 func (s *service) Close() {
 	s.settingsRepo.Close()
 	s.vhtlcRepo.Close()
 	s.vtxoRolloverRepo.Close()
 	s.swapRepo.Close()
 	s.subscribedScriptRepo.Close()
+	s.chainSwapRepo.Close()
 }

57-93: ⚠️ Potential issue | 🟠 Major

Missing chainSwapRepo initialization for badger database type will cause nil pointer dereference.

The badger case (lines 58-93) initializes all repositories except chainSwapRepo, leaving it nil. The sqlite case properly initializes it (line 171), but badger lacks both the initialization and any corresponding NewChainSwapRepository implementation. When ChainSwaps() is called with badger as the database type, it will return nil, causing crashes in any consuming code.

Either implement NewChainSwapRepository for badger or return an error during initialization if chain swaps are required.

🤖 Fix all issues with AI agents
In `@internal/core/application/service.go`:
- Around line 1437-1446: The function CreateChainSwapArkToBtc currently discards
the caller's context by creating context.Background(); instead, use the
passed-in context parameter so cancellation, deadlines, and tracing propagate.
Replace ctx := context.Background() with using the existing context argument for
calls such as s.isInitializedAndUnlocked(ctx) and any downstream operations
inside CreateChainSwapArkToBtc (and ensure any nested goroutines derive Contexts
via ctx). Also update any references within the method that expect a local ctx
to use the passed context variable name to preserve caller signals.
- Around line 1629-1636: The RefundChainSwap method on type Service is currently
a no-op; replace the TODO with either a proper implementation following the
listed flow (load chain swap from DB, locate the BTX transaction, claim it,
forward to boarding address, and call settle) using existing helpers (e.g.,
functions/methods that interact with the DB, find BTX txs, claim transactions,
send to boarding address, and settle) or, if the behavior is intentionally
unimplemented, return an explicit sentinel error (e.g.,
errors.New("RefundChainSwap: not implemented")) instead of nil so callers
receive a clear failure; update RefundChainSwap to return that error and ensure
callers handle it.
- Around line 2155-2164: The log and failure message when vtxos[0].Spent is true
are misleading; update the message used in log.Warn and chainSwap.Failed to
reflect ambiguity (e.g., "VTXO already spent; swap may have been claimed by
counterparty or refund failed") so it doesn't assume a refund failure, and keep
the existing DB update call via s.dbSvc.ChainSwaps().Update to persist that
clarified status; locate the branch handling vtxos[0].Spent (references: vtxos,
chainSwap.Failed, log.Warn, s.dbSvc.ChainSwaps().Update) and replace the current
hardcoded "failed to refund chain swap" text with the clarified message.

In `@internal/core/domain/chainswap.go`:
- Around line 48-65: IsComplete currently only marks ChainSwapClaimed,
ChainSwapRefunded, and ChainSwapFailed as terminal, but omits
ChainSwapRefundFailed and ChainSwapUserLockedFailed; update the
ChainSwap.IsComplete method to also return true for ChainSwapRefundFailed and
ChainSwapUserLockedFailed so these failure states are treated as terminal (this
will automatically make IsPending() return false for them); ensure this change
is made only in IsComplete and that CanRefund/other logic still behaves as
intended given these states.

In `@internal/infrastructure/db/sqlite/chainswap_repo.go`:
- Around line 154-158: The repository Close() method
(chainSwapRepository.Close()) incorrectly closes the shared *sql.DB used by
multiple repositories (settings, VHTLC, VtxoRollover, Swap, SubscribedScript,
ChainSwap); remove or change these per-repo Close implementations so they do not
call r.db.Close() and instead have the service-level Close() perform the single
db.Close(), or implement reference counting if independent lifecycles are
required; specifically, update chainSwapRepository.Close() (and the other repo
Close methods) to be no-ops or delegates and ensure only the service.Close()
invokes db.Close() once.

In `@internal/interface/grpc/handlers/service_handler.go`:
- Line 671: TimeoutBlockHeight is being hardcoded to 0 for BTC→ARK swaps;
instead read the real timeout from the swap details and set it on the response.
Locate where TimeoutBlockHeight is assigned in the BTC→ARK handling path (the
code that builds the response struct containing TimeoutBlockHeight) and replace
the literal 0 with the swap's timeout field (e.g., swap.TimeoutBlockHeight or
swapDetails.TimeoutBlockHeight), including a safe nil/zero check so you only set
a value when present. Ensure the response uses that extracted value so clients
receive the actual lockup expiry.
- Around line 645-650: The CreateChainSwapArkToBtc error path currently returns
a successful gRPC response with an Error string (CreateChainSwapArkToBtc
branch), which is inconsistent with the BTC_TO_ARK branch that returns a gRPC
status error; change the Ark->BTC branch to return a proper gRPC error (e.g.,
status.Error with appropriate codes.Internal and the err message) instead of
embedding the error in CreateChainSwapResponse so both directions use the same
status.Error pattern for failures.

In `@internal/test/e2e/chainswap_test.go`:
- Around line 30-42: The helper getBtcAddressBalance currently does an unsafe
type assertion raw["total_amount"].(float64) which can panic if the key is
missing or not a float64; update getBtcAddressBalance to safely read the value
using the comma-ok idiom or a type switch: check that raw["total_amount"]
exists, assert it to float64 (or handle numeric types like json.Number/int by
converting), and return a descriptive error if the key is missing or has an
unexpected type instead of letting the test panic.

In `@internal/test/e2e/main_test.go`:
- Around line 14-30: The commented-out TestMain disables test setup; restore
TestMain by uncommenting the function so that refillArkd, refillFulmineBoltz,
and refillFulmineClient run before tests (calling os.Exit(m.Run()) at the end),
or if you intentionally removed it delete the dead code; ensure the TestMain
signature (func TestMain(m *testing.M)) and required imports (context, os, log)
remain so failures call log.Fatalf with the error messages from refillArkd,
refillFulmineBoltz, and refillFulmineClient.

In `@pkg/boltz/types.go`:
- Around line 182-213: GetSwapTree currently dereferences ClaimDetails.SwapTree
and LockupDetails.SwapTree and can panic if those are nil; update
CreateChainSwapResponse.GetSwapTree to first check the relevant SwapTree
pointers (c.ClaimDetails.SwapTree and c.LockupDetails.SwapTree) and their
ClaimLeaf/RefundLeaf before accessing fields, and if any are nil return a
zero-value SwapTree (or a clearly empty SwapTree) instead of panicking; keep the
isArkToBtc branch and ensure you reference SwapTree, SwapTreeLeaf, ClaimDetails,
LockupDetails, ClaimLeaf and RefundLeaf when adding the checks so the guard
covers all dereferences.

In `@pkg/swap/btc_util.go`:
- Around line 30-79: The ConstructClaimTransaction function incorrectly computes
feeAmount using computeVSize(tx) which underestimates pre-signing (witness not
present) and then subtracts feeAmount from params.LockupAmount without guarding
against underflow; update the fee estimation to include an estimated witness
size (add typical witness bytes for the redeeming input) when calling
computeVSize or by adding a fixed witness overhead before multiplying by
feeRate, and add a defensive check after computing feeAmount to ensure feeAmount
< params.LockupAmount (return an error like "insufficient funds to cover network
fees" if not) before performing params.LockupAmount - feeAmount and assigning
tx.TxOut[0].Value; reference ConstructClaimTransaction, computeVSize,
explorerClient.GetFeeRate, feeAmount and tx.TxOut in your change.

In `@pkg/swap/chainswap_arkt_btc_handler.go`:
- Around line 62-66: The handler currently only logs and returns; update
arkToBtcHandler.HandleLockupFailed to transition the swap state to a
terminal/failed lockup state on h.chainSwapState (e.g., "lockup_failed"),
persist that state, and kick off the refund flow instead of returning nil—call
or add a helper like h.triggerRefund(ctx, update) or h.startRefundProcess(ctx,
update) to enqueue the refund operations and surface the error by returning a
non-nil error; keep the log but include the swap id from h.chainSwapState.SwapID
and any underlying error details when available.

In `@pkg/swap/chainswap_btc_ark_handler.go`:
- Around line 175-252: In signBoltzBtcClaim ensure the decoded transaction hash
is exactly 32 bytes before copying into msg: after txHashBytes, validate
len(txHashBytes)==32 and return a clear error (e.g., "invalid transaction hash
length") if not; only then copy into var msg [32]byte and proceed with signing
so our partial signature generation (OurPartialSign, AggregateNonces, etc.)
never uses a truncated/padded hash.
- Around line 64-66: The server-confirmed lock handler currently is a no-op;
change btcToArkHandler.HandleServerLocked to delegate to the existing mempool
lock handler so confirmed events trigger the same claim flow (e.g., call
b.HandleMempoolLocked(ctx, update) or otherwise invoke the same logic used for
mempool updates) ensuring Ark VTXOs are claimed when only the confirmed event
arrives.

In `@pkg/swap/chainswap_monitor.go`:
- Around line 32-37: Replace the direct status assignment on pubkey-parse
failure with the swap failure helper so failure hooks/events run: when
parsePubkey(swapResp.ClaimDetails.ServerPublicKey) returns an error, call
swap.Fail(...) (passing the error or a descriptive message including swapId)
instead of setting swap.Status = ChainSwapFailed, and then return; this keeps
transitions consistent and preserves error context for logging and downstream
handlers.
- Around line 95-155: Replace the direct status set and missing error details
when the WebSocket connect fails: instead of assigning ChainSwapFailed, call
chainSwapState.Swap.Fail(fmt.Sprintf("websocket subscribe failed: %v", err))
after ws.ConnectAndSubscribe fails (keep the same context and return). Also
change the monitor loop receive to check for channel closure by using the ok
pattern on ws.Updates (e.g., update, ok := <-ws.Updates) and handle closed
channel by logging/Failing the swap and returning; retain existing handler
switch and error handling after successful receives.

In `@pkg/swap/chainswap_validate.go`:
- Around line 20-83: The VHTLC mismatch error in validateVHTLC prints
swapResp.LockupDetails.LockupAddress regardless of isArkToBtc, which can be
wrong; update the fmt.Errorf call that constructs the "VHTLC address mismatch"
message to use the vhtlcAddr variable (the value actually compared) as the "Got"
value instead of swapResp.LockupDetails.LockupAddress so the error reports the
real compared address (refer to validateVHTLC, vhtlcAddr and vhtlcAddress).

In `@pkg/swap/explorer.go`:
- Around line 25-29: NewExplorerClient currently constructs an http.Client with
no timeout and GetFeeRate bypasses that client and decodes body before checking
status and assumes response["1"] exists; fix by setting a sensible timeout when
creating explorerClient in NewExplorerClient (e.g., time.Second * N), update
GetFeeRate to call e.client.Get(...) instead of http.Get, check resp.StatusCode
immediately and return an error on non-200 before decoding the body, and
validate the map key (e.g., check presence of "1" and handle missing/invalid
values) before converting to a fee rate.
- Around line 15-26: Add Go doc comments for the exported interface
ExplorerClient and the constructor NewExplorerClient: place a comment
immediately above the ExplorerClient declaration describing its purpose (e.g.,
"ExplorerClient provides methods to broadcast transactions and query fee rates
from a blockchain explorer") and a comment above NewExplorerClient describing
what it returns and the meaning of the baseURL parameter (e.g.,
"NewExplorerClient creates an ExplorerClient that talks to the explorer at
baseURL"). Ensure both comments are full sentences and start with the exported
name.

In `@test.docker-compose.yml`:
- Around line 74-77: The ARKD_VTXO_TREE_EXPIRY was reduced from 1024 to 512
which may let VTXOs expire before long-running claim/refund flows; restore or
increase ARKD_VTXO_TREE_EXPIRY so it is >= ARKD_UNILATERAL_EXIT_DELAY and also
validate it against FULMINE_SWAP_TIMEOUT (ensure ARKD_VTXO_TREE_EXPIRY covers
the maximum of ARKD_UNILATERAL_EXIT_DELAY and FULMINE_SWAP_TIMEOUT plus safety
margin); update the docker-compose env block where ARKD_VTXO_TREE_EXPIRY and
ARKD_UNILATERAL_EXIT_DELAY are defined and adjust FULMINE_SWAP_TIMEOUT if needed
to keep all three aligned.
🧹 Nitpick comments (17)
pkg/swap/batch_handler.go (1)

106-106: Clarify intentional no‑op handler.

Consider a short comment to document why this hook does nothing (e.g., interface requirement), to prevent future “missing logic” confusion.

💬 Possible clarification
-func (h *batchSessionHandler) OnStreamStartedEvent(event client.StreamStartedEvent) {}
+// OnStreamStartedEvent is a no-op; required by the stream handler interface.
+func (h *batchSessionHandler) OnStreamStartedEvent(event client.StreamStartedEvent) {}
pkg/boltz/ws.go (1)

233-233: Add a Go doc comment for exported ConnectAndSubscribe.

This exported method lacks a doc comment.

✍️ Suggested addition
+// ConnectAndSubscribe connects to the Boltz websocket and subscribes to swap updates, retrying until ctx is done.
 func (boltz *Websocket) ConnectAndSubscribe(ctx context.Context, swapIds []string, retryInterval time.Duration) error {

As per coding guidelines "Document exported functions, types, and complex logic with comments in Go code".

test.docker-compose.yml (1)

185-187: Pin the Boltz image to a specific stable release instead of :latest.

Line 186 uses :latest, which is a moving tag that makes tests non-reproducible and risks silent breakage when new releases are published. The current stable release is v3.12.1 (Jan 9, 2026); pin this or another specific semver tag.

Note: The ark tag is a development branch, not a stable release, and would not improve reproducibility.

📌 Suggested pin
-    image: boltz/boltz:latest
+    image: boltz/boltz:v3.12.1
pkg/swap/musig.go (1)

241-301: Add GoDoc comments for exported helper functions.
ParsePubNonce, SerializePubNonce, ParsePartialSignatureScalar32, SerializePartialSignatureScalar32, and NewPrevOutputFetcher need doc comments to satisfy GoDoc expectations.

As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

pkg/boltz/types.go (1)

144-287: Add GoDoc comments for newly exported chain-swap types.
The new public structs (e.g., CreateChainSwapRequest, ChainSwapLockupDetails, SwapTree, SwapLeg, ChainSwapClaimDetailsResponse, etc.) should have GoDoc comments.

As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

pkg/swap/chainswap.go (1)

20-59: Add GoDoc comments for exported types.
ChainSwapStatus and ChainSwap are exported but undocumented.

As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

internal/test/e2e/chainswap_test.go (2)

67-67: Consider polling with timeout instead of fixed sleep.

Fixed time.Sleep calls can cause flaky tests. A polling loop with timeout would be more reliable:

require.Eventually(t, func() bool {
    swaps, err := client.ListChainSwaps(ctx, &pb.ListChainSwapsRequest{SwapIds: []string{swapID}})
    return err == nil && swaps.GetSwaps()[0].GetStatus() == "claimed"
}, 30*time.Second, 1*time.Second, "swap should reach claimed status")

This pattern applies to lines 67, 107, and 151.


123-165: Test is nearly identical to TestChainSwapBTCtoARK.

The only difference from TestChainSwapBTCtoARK is the faucet amount (0.00015500 vs 0.00003000). Consider consolidating into a single test with subtests or a table-driven approach to reduce duplication.

♻️ Example consolidation
func TestChainSwapBTCtoARK(t *testing.T) {
    tests := []struct {
        name         string
        faucetAmount float64
    }{
        {"standard", 0.00003000},
        {"with_quote", 0.00015500},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            // ... shared test logic with tc.faucetAmount
        })
    }
}
internal/infrastructure/db/sqlite/chainswap_repo.go (2)

162-178: Simplify toChainSwap — error return is unused.

The function signature returns (*domain.ChainSwap, error) but the error is always nil. Either:

  • Remove the error from the return type
  • Add validation that could return an error (e.g., validating currency values)
Option 1: Remove unused error return
-func toChainSwap(row queries.ChainSwap) (*domain.ChainSwap, error) {
+func toChainSwap(row queries.ChainSwap) *domain.ChainSwap {
 	return &domain.ChainSwap{
 		Id:                   row.ID,
 		From:                 boltz.Currency(row.FromCurrency),
 		To:                   boltz.Currency(row.ToCurrency),
 		Amount:               uint64(row.Amount),
 		Status:               domain.ChainSwapStatus(row.Status),
 		UserLockupTxId:       fromNullableString(row.UserLockupTxID),
 		ServerLockupTxId:     fromNullableString(row.ServerLockupTxID),
 		ClaimTxId:            fromNullableString(row.ClaimTxID),
 		ClaimPreimage:        row.ClaimPreimage,
 		RefundTxId:           fromNullableString(row.RefundTxID),
 		UserBtcLockupAddress: fromNullableString(row.UserBtcLockupAddress),
 		ErrorMessage:         fromNullableString(row.ErrorMessage),
 		CreatedAt:            fromNullableInt64(row.CreatedAt),
-	}, nil
+	}
 }

72-83: Silent error skipping may hide data corruption.

When toChainSwap fails, the code logs a warning and skips the row. While this provides resilience, it could mask underlying data issues. The same pattern appears in GetByIDs and GetByStatus.

Consider returning an error or at least including a count of skipped rows in the response to alert callers.

api-spec/openapi/swagger/fulmine/v1/service.swagger.json (2)

65-96: Consider adding consumes field for POST operation.

Static analysis (Checkov CKV_OPENAPI_17) flags that the POST operation is missing a consumes field. While the global consumes: ["application/json"] at the document level (lines 12-14) typically applies, explicitly adding it to this operation improves clarity.


155-165: Consider adding maxItems to the swapIds array parameter.

Static analysis (Checkov CKV_OPENAPI_21) recommends defining a maximum number of items for array parameters to prevent unbounded queries that could impact performance.

Suggested fix
           {
             "name": "swapIds",
             "in": "query",
             "required": false,
             "type": "array",
             "items": {
               "type": "string"
             },
-            "collectionFormat": "multi"
+            "collectionFormat": "multi",
+            "maxItems": 100
           }
internal/core/domain/chainswap.go (1)

67-107: State transition methods don't validate preconditions.

The transition methods (e.g., UserLocked, Claimed) unconditionally update the state without checking if the transition is valid from the current state. For example, Claimed() could be called even when the swap is already ChainSwapRefunded.

While this may be acceptable if callers are trusted to call methods in the correct order, adding validation would make the domain model more robust.

pkg/swap/chainswap_btc_ark_handler.go (1)

27-41: Add a Go doc comment for NewBtcToArkHandler.

✏️ Suggested doc comment
-func NewBtcToArkHandler(
+// NewBtcToArkHandler builds a ChainSwapEventHandler for BTC→ARK swaps.
+func NewBtcToArkHandler(

As per coding guidelines: Document exported functions, types, and complex logic with comments in Go code.

pkg/swap/chainswap_arkt_btc_handler.go (1)

33-56: Add a Go doc comment for NewArkToBtcHandler.

✏️ Suggested doc comment
-func NewArkToBtcHandler(
+// NewArkToBtcHandler builds a ChainSwapEventHandler for ARK→BTC swaps.
+func NewArkToBtcHandler(

As per coding guidelines: Document exported functions, types, and complex logic with comments in Go code.

api-spec/protobuf/fulmine/v1/service.proto (1)

173-192: Consider a server‑streaming RPC for real‑time swap updates.

Chain swaps are monitored live; a streaming gRPC endpoint would give clients a native, real‑time option without polling.

💡 Possible shape
 service Service {
+  // WatchChainSwap streams status updates for a swap.
+  rpc WatchChainSwap(WatchChainSwapRequest) returns (stream ChainSwapResponse);
 }
+
+message WatchChainSwapRequest {
+  string id = 1;
+}

As per coding guidelines: Use streaming in gRPC services where appropriate for real-time data.

pkg/swap/chainswap_monitor.go (1)

202-208: Add a Go doc comment for ChainSwapState.

✏️ Suggested doc comment
-type ChainSwapState struct {
+// ChainSwapState holds swap metadata and callbacks used by the monitor loop.
+type ChainSwapState struct {

As per coding guidelines: Document exported functions, types, and complex logic with comments in Go code.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/swap/chainswap_arkt_btc_handler.go`:
- Around line 33-56: Add a Go doc comment above the exported constructor
NewArkToBtcHandler describing what the constructor returns and its purpose
(e.g., constructs and initializes an arkToBtcHandler for handling ARK→BTC
swaps), and briefly mention key parameters like swapHandler, state, network, and
swapResp to clarify intent; ensure the comment starts with "NewArkToBtcHandler
..." to follow Go documentation conventions.

Copy link
Contributor

@altafan altafan left a comment

Choose a reason for hiding this comment

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

please revert all changes to pkg/vhtlc as they are not necessary

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/swap/swap.go (2)

71-95: ⚠️ Potential issue | 🟠 Major

Add nil check for privateKey to prevent nil pointer dereference.

If privateKey is nil, line 91 will panic when calling privateKey.PubKey(). Add validation at the start of the function.

🛡️ Proposed fix
 func NewSwapHandler(
 	arkClient arksdk.ArkClient,
 	transportClient client.TransportClient,
 	indexerClient indexer.Indexer,
 	boltzSvc *boltz.Api,
 	esploraURL string,
 	privateKey *btcec.PrivateKey,
 	timeout uint32,
 ) (*SwapHandler, error) {
+	if privateKey == nil {
+		return nil, fmt.Errorf("privateKey cannot be nil")
+	}
 	cfg, err := arkClient.GetConfigData(context.Background())

1092-1114: ⚠️ Potential issue | 🟠 Major

Check refundResp.Error before using response fields.

Per the RefundSwapResponse type, the response includes an Error field. If Boltz returns an error, Transaction and Checkpoint may be empty or invalid, leading to confusing parse errors.

🐛 Proposed fix
 	refundResp, err := requestRefund(swapId, boltz.RefundSwapRequest{
 		Transaction: refundTx,
 		Checkpoint:  checkpointTx,
 	})
 	if err != nil {
 		return nil, nil, err
 	}
+	if refundResp.Error != "" {
+		return nil, nil, fmt.Errorf("boltz refund failed: %s", refundResp.Error)
+	}

 	refundPtx, err := psbt.NewFromRawBytes(strings.NewReader(refundResp.Transaction), true)
🤖 Fix all issues with AI agents
In `@pkg/swap/chainswap_validate.go`:
- Around line 20-83: In validateVHTLC, add a hard length check for
preimageHashHASH160 (expecting 20 bytes for HASH160) before using it or passing
to h.getVHTLC; if len(preimageHashHASH160) != 20 return an explanatory error
(e.g. "invalid preimage hash length") so malformed inputs fail fast and avoid
misleading address derivation or panics. Ensure the check is placed at the start
of validateVHTLC (before the call to h.getVHTLC and before slicing
preimageHashHASH160[:]) and reference the same variable name and function
getVHTLC in the error path.
- Around line 508-549: In validateBtcLockupAddress, add a defensive nil check
for the clientPubKey before calling musig2.AggregateKeys: if clientPubKey == nil
return a descriptive error (e.g., "client pubkey is nil") so the function fails
fast instead of allowing musig2.AggregateKeys to panic; follow the same error
style used in NewMuSigContext (wrap/return fmt.Errorf) and keep the check placed
before the AggregateKeys call that uses clientPubKey and serverPubKey.

In `@pkg/swap/swap.go`:
- Around line 447-457: The switch that sets refundFunc for SwapTypeSubmarine and
SwapTypeChain can leave refundFunc nil for unknown swapType, causing a panic
when calling h.collaborativeRefund; update the switch in the handler to handle
other values (e.g., add a default case that returns a clear error) or check for
nil refundFunc before calling collaborativeRefund and return an error; reference
the refundFunc variable, the switch on swapType, the constants
SwapTypeSubmarine/SwapTypeChain, and the call to h.collaborativeRefund to locate
and fix the code.
🧹 Nitpick comments (3)
pkg/swap/swap.go (1)

1087-1090: Consider moving swap type constants near other constants at the top of the file.

For consistency with SwapStatus constants (lines 52-56), consider relocating SwapTypeSubmarine and SwapTypeChain to the same area.

internal/core/application/service.go (2)

535-541: Track the crash-recovery TODO for chain swaps.

This TODO leaves pending swaps in limbo after restart. Consider creating an issue or implementing recovery now.

If you want, I can sketch a recovery flow aligned with the current chain-swap state model.


1617-1627: Add a doc comment for the exported ListChainSwaps method.

Public APIs should have GoDoc comments for clarity.

🛠️ Suggested comment
+// ListChainSwaps returns all chain swaps, optionally filtered by IDs.
 func (s *Service) ListChainSwaps(ctx context.Context, swapIDs []string) ([]domain.ChainSwap, error) {

As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@internal/core/application/service.go`:
- Around line 1749-1757: The parsePubkey function currently returns (nil, nil)
for empty input which allows silent nil propagation; modify parsePubkey to
return a descriptive error (e.g., fmt.Errorf("empty pubkey")) when pubkey == ""
instead of nil, nil, and keep the existing hex.DecodeString error handling; then
update callers (e.g., in service_handler.go, swap.go, service.go) to handle the
returned error path rather than assuming a nil PublicKey is acceptable so they
surface a clear "empty pubkey" error upstream.
- Around line 1458-1486: The swap is persisted only after calling
swapHandler.ChainSwapArkToBtc which starts monitoring goroutines that may invoke
handleChainSwapEvent before the DB record exists; to fix, ensure persistence
happens before any background monitors start: either move the call to
s.dbSvc.ChainSwaps().Add(...) into the swap handler implementations
(ChainSwapArkToBtc and ChainSwapBtcToArk) so they persist the domain.ChainSwap
before launching goroutines, or modify the service layer to persist the swap
immediately after receiving chainSwap and before passing/attaching
eventCallback/unilateralRefund or before any goroutine startup; update
references: handleChainSwapEvent, swapHandler.ChainSwapArkToBtc,
swapHandler.ChainSwapBtcToArk, and dbSvc.ChainSwaps().Add.

In `@internal/infrastructure/db/sqlite/sqlc/schema.sql`:
- Around line 55-70: The status CHECK on the chain_swap table currently
restricts status to 0–7, but the domain includes value 8
(ChainSwapRefundedUnilaterally); update the CHECK constraint on the status
column in the chain_swap CREATE TABLE (status INTEGER NOT NULL CHECK(...)) to
allow value 8 (i.e., include 8 in the IN list) so the
ChainSwapRefundedUnilaterally enum can be persisted.

In `@internal/test/mockboltz/server.go`:
- Around line 675-685: The code mutates client.subs while only holding s.wsMu,
which protects s.wsClients but not the nested map; fix by acquiring the client's
own mutex (client.mu) before modifying client.subs in the subscribe branch
(where msg.Op == "subscribe" && msg.Channel == "swap.update") and release it
after updates, and ensure all reads of client.subs (e.g., the access at the
earlier read site around where client.subs is iterated or checked) also take
client.mu; keep using s.wsMu only to safely access s.wsClients but always lock
client.mu around accesses to the subs map.

In `@MockBoltz.Dockerfile`:
- Around line 1-25: The dockerfile name casing mismatches the docker-compose
reference: the repo has MockBoltz.Dockerfile but test.docker-compose.yml
references mockboltz.Dockerfile; on case-sensitive systems the build will fail.
Fix by renaming MockBoltz.Dockerfile to mockboltz.Dockerfile (or alternatively
update the reference in test.docker-compose.yml to match exactly
"MockBoltz.Dockerfile"), ensuring the filename used by docker-compose and the
actual file name are byte-for-byte identical.
- Line 1: The docker-compose reference and Dockerfile name mismatch: update
either test.docker-compose.yml to reference "MockBoltz.Dockerfile" (matching the
current file) or rename "MockBoltz.Dockerfile" to "mockboltz.Dockerfile" so the
case matches on case-sensitive CI; also verify and fix the base image in the
Dockerfile by replacing FROM golang:1.25.5-alpine (which may not exist) with a
valid Go image (e.g., a supported 1.25.x or 1.24.x tag) that meets the project
requirement (Go >=1.24.6).

In `@pkg/boltz/ws.go`:
- Around line 240-242: In ConnectAndSubscribe, guard against zero or negative
retryInterval before calling Retry by clamping it to a sane default (e.g.,
time.Second or a configured constant) so Retry's time.After isn't driven with
<=0; update the ConnectAndSubscribe method to check retryInterval (and if <=0
set retryInterval = defaultRetryInterval) and then pass the clamped value into
Retry to prevent tight CPU loops.

In `@pkg/swap/btc_util.go`:
- Around line 209-233: The control block is using the hardcoded BaseLeafVersion
and the TapLeaf created via NewBaseTapLeaf, which mismatches the merkle root
that used actual leaf versions; update the control block construction to use the
real leaf version from swapTree (swapTree.ClaimLeaf.Version or
swapTree.RefundLeaf.Version depending on isClaimPath) when building
leafVersionByte (leafVersionByte := byte(actualVersion) | parity) and compute
siblingHash with the same version via tapLeafHash(actualVersion, siblingScript)
instead of siblingLeaf.TapHash(); keep using internalKeyBytes and parity as
before so the control block and merkle root use the same leaf versions.

In `@pkg/swap/chainswap_monitor.go`:
- Around line 134-136: The constant boltz.TransactionServerMempoool is
misspelled (extra 'o'); rename the constant to boltz.TransactionServerMempool in
its definition (pkg/boltz/status.go) and update all references (e.g., switch
cases like the one calling handler.HandleServerLockedMempool, tests, imports,
and any string mappings) to use the correct identifier TransactionServerMempool
so it still maps to "transaction.server.mempool"; ensure you run tests/compile
to catch any remaining references.

In `@pkg/swap/chainswap.go`:
- Around line 573-595: The RefundArkToBTCSwap function currently calls
RefundSwap with context.Background(), ignoring the provided ctx; update the call
to pass the incoming ctx so cancellation/timeouts propagate (i.e., change the
RefundSwap call in RefundArkToBTCSwap to use ctx instead of
context.Background()), leaving the rest of the parameters (SwapTypeChain,
swapId, true, vhtlcOpts) and the unilateralRefundCallback handling unchanged.
- Around line 159-169: The RefundUnilaterally method currently sets s.Status =
ChainSwapRefundedUnilaterally but emits a generic RefundEvent; update the
emission to emit the unilateral-specific event type (RefundEventUnilaterally) so
listeners can distinguish it. In the RefundUnilaterally method of ChainSwap,
replace the onEvent(RefundEvent{...}) call with
onEvent(RefundEventUnilaterally{SwapID: s.Id, TxID: txid}) (preserving the
SwapID and TxID fields) and keep the existing status and RefundTxid assignment.

In `@pkg/swap/explorer.go`:
- Around line 111-206: The three methods GetCurrentBlockHeight,
GetTransactionStatus, and GetTransaction are calling http.Get directly (lines
shown in the diff) which bypasses the configured e.client (with its timeout and
transport); replace those http.Get(...) calls with e.client.Get(...) and keep
the existing error handling and resp.Body.Close defer logic (ensure you only
defer after err==nil and resp non-nil) so all explorer requests use the shared
HTTP client created in NewExplorerClient.

In `@scripts/setup`:
- Around line 355-366: The addr variable assigned from the onboard POST
(addr=$(curl -s -X POST ... | jq -r .address)) is unused and the script claims
funding even though no funding command is run; either use addr to perform the
funding step (call nigiri faucet $addr 0.001 after obtaining addr and before the
settle check) and keep the existing success echo, or remove addr and change the
success message to not claim funding; also ensure the error handling around the
curl calls uses the curl exit status and/or response fields consistently
(replace the current bogus status/err references) and apply the same fix to the
Client Fulmine flow which mirrors this block.
🧹 Nitpick comments (13)
test.docker-compose.yml (1)

236-236: Pin to a specific image version tag instead of latest for reproducible test behavior.

The boltz/boltz:latest tag is mutable and can cause inconsistent test results as upstream updates are released. Multiple stable versions are available (e.g., v3.12.1, v3.12.0, v3.11.0); pin the image to one of these to ensure reproducible builds.

pkg/swap/btc_util.go (1)

19-25: Add Go doc for exported ClaimTransactionParams.
Exported types should have a doc comment for Go tooling and readability.

✍️ Suggested fix
-type ClaimTransactionParams struct {
+// ClaimTransactionParams holds inputs for building a BTC claim transaction.
+type ClaimTransactionParams struct {
 	LockupTxid      string
 	LockupVout      uint32
 	LockupAmount    uint64
 	DestinationAddr string
 	Network         *chaincfg.Params
 }

As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

pkg/boltz/go.mod (1)

8-11: Confirm logrus/x/sys pins are intentional and consistent.

Line 11 pins golang.org/x/sys v0.13.0, which is far behind the root module’s v0.37.0; this kind of skew can cause confusing resolutions and miss security fixes when this submodule is replaced into the root build. Please verify the older pin is required; otherwise align it with the root version.

go.mod (1)

13-40: Verify dependency bumps and the ltcd replace are compatible.

The new replace directive plus multiple version bumps (logrus, btcec/v2, blake3, websocket) can impact transitive resolution. Please confirm these versions are intentional and compatible across the repo (especially when pkg/* modules are replaced into the root build).

Also applies to: 73-74, 183-183, 200-200

pkg/swap/go.mod (1)

7-14: Confirm submodule dependency versions align with root go.mod.

These bumps (logrus, vhtlc, go-sdk, btcec/v2) should match the root module’s versions to avoid resolution skew when using replace. Please verify this alignment and compatibility.

Also applies to: 68-68

pkg/swap/chainswap.go (2)

27-210: Add doc comments for exported ChainSwap API surface.

ChainSwapStatus, ChainSwap, NewChainSwap, the exported state mutators (UserLock/ServerLock/Claim/Refund/RefundUnilaterally/Fail/RefundFailed/UserLockedFailed), and RefundArkToBTCSwap are exported but lack doc comments.

As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

Also applies to: 573-595


796-817: Allow ctx cancellation during confirmation polling.

The refund confirmation loop can block for up to 2 hours even if ctx is canceled. Consider checking ctx.Done() in the loop to allow prompt cancellation.

internal/core/application/service.go (2)

61-75: Avoid silently defaulting unknown networks to regtest.

Falling back to regtest can lead to signing/broadcasting on the wrong network if a new or misconfigured network name appears. Prefer returning an error and stopping swap creation.

🛠️ Safer mapping with explicit error
-// networkNameToParams converts arklib network name to chaincfg.Params
-func networkNameToParams(networkName string) *chaincfg.Params {
+// networkNameToParams converts arklib network name to chaincfg.Params.
+// Returns an error for unknown networks to avoid silent misconfiguration.
+func networkNameToParams(networkName string) (*chaincfg.Params, error) {
 	switch networkName {
 	case arklib.Bitcoin.Name:
-		return &chaincfg.MainNetParams
+		return &chaincfg.MainNetParams, nil
 	case arklib.BitcoinTestNet.Name:
-		return &chaincfg.TestNet3Params
+		return &chaincfg.TestNet3Params, nil
 	case arklib.BitcoinRegTest.Name:
-		return &chaincfg.RegressionNetParams
+		return &chaincfg.RegressionNetParams, nil
 	case arklib.BitcoinSigNet.Name, arklib.BitcoinMutinyNet.Name:
-		return &chaincfg.SigNetParams
+		return &chaincfg.SigNetParams, nil
 	default:
-		// Default to regtest for safety
-		return &chaincfg.RegressionNetParams
+		return nil, fmt.Errorf("unsupported network: %s", networkName)
 	}
 }
-	network := networkNameToParams(config.Network.Name)
+	network, err := networkNameToParams(config.Network.Name)
+	if err != nil {
+		return nil, err
+	}
-	network := networkNameToParams(config.Network.Name)
+	network, err := networkNameToParams(config.Network.Name)
+	if err != nil {
+		return nil, err
+	}

Also applies to: 1456-1507


1634-1647: Add Go doc comments for exported chain-swap APIs.

ListChainSwaps and RefundChainSwap are exported but undocumented.

✏️ Suggested doc comments
+// ListChainSwaps returns chain swaps, optionally filtered by IDs.
 func (s *Service) ListChainSwaps(ctx context.Context, swapIDs []string) ([]domain.ChainSwap, error) {
 	if err := s.isInitializedAndUnlocked(ctx); err != nil {
 		return nil, err
 	}
@@
 	return s.dbSvc.ChainSwaps().GetByIDs(ctx, swapIDs)
 }
 
+// RefundChainSwap triggers a refund flow for the specified chain swap.
 func (s *Service) RefundChainSwap(ctx context.Context, id string) error {
 	if err := s.isInitializedAndUnlocked(ctx); err != nil {
 		return err
 	}
As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.
pkg/swap/chainswap_btc_ark_handler.go (1)

27-33: Add a Go doc comment for the exported constructor.

✏️ Suggested comment
+// NewBtcToArkHandler constructs a ChainSwapEventHandler for BTC→ARK swaps.
 func NewBtcToArkHandler(
 	swapHandler *SwapHandler,
 	chainSwapState ChainSwapState,
 	preimage []byte,
As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.
internal/test/mockboltz/server.go (1)

48-267: Add Go doc comments for exported types and methods.

Config, Server, New, Start, Stop, and String are exported but undocumented.

✏️ Suggested doc comments
+// Config defines runtime settings for the mock Boltz server.
 type Config struct {
 	ListenAddr                  string
 	ArkdURL                     string
@@
 }
 
+// Server is an in-process mock Boltz API server used in tests.
 type Server struct {
 	cfg Config
@@
 }
 
+// New constructs a new mock Boltz server with the provided configuration.
 func New(cfg Config) (*Server, error) {
@@
 }
 
+// Start launches the HTTP and WebSocket endpoints.
 func (s *Server) Start() error {
@@
 }
 
+// Stop gracefully shuts down the mock server.
 func (s *Server) Stop() error {
@@
 }
@@
+// String returns the base URL for the running mock server.
 func (s *Server) String() string {
As per coding guidelines, Document exported functions, types, and complex logic with comments in Go code.

Also applies to: 1384-1400

internal/test/e2e/chainswap_test.go (2)

444-467: Well-structured polling helper with clear timeout handling.

The waitChainSwapStatus helper properly implements polling with a deadline and provides a clear final assertion with the actual status for debugging. Consider adding a small log statement when the expected status is reached to aid debugging in CI.

💡 Optional: Add success logging
 		if err == nil && len(resp.GetSwaps()) > 0 {
 			if resp.GetSwaps()[0].GetStatus() == expected {
+				t.Logf("Swap %s reached expected status: %s", swapID, expected)
 				return
 			}
 		}

536-548: HTTP request without context.

The http.Get call uses //nolint:noctx but passing context is preferred for consistency with the rest of the codebase and proper cancellation support.

💡 Optional: Use context-aware HTTP request
+func waitForEsploraTxIndexed(t *testing.T, ctx context.Context, txID string, timeout time.Duration) {
-func waitForEsploraTxIndexed(t *testing.T, txID string, timeout time.Duration) {
 	t.Helper()
 	require.NotEmpty(t, txID)
 	...
 	for time.Now().Before(deadline) {
-		resp, err := http.Get(endpoint) //nolint:noctx
+		req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+		require.NoError(t, err)
+		resp, err := http.DefaultClient.Do(req)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@internal/core/application/service.go`:
- Around line 1688-1711: GetConfigData's error is being ignored which can leave
cfg nil and cause a panic when accessing cfg.SignerPubKey; change the call in
the function to capture and check the error from s.GetConfigData(ctx) (e.g. cfg,
err := s.GetConfigData(ctx)) and if err != nil return a descriptive error (for
example "failed to get config data: %w"); ensure subsequent use of cfg (used to
populate vhtlc.Opts.Server) only happens after this error check.

In `@internal/interface/grpc/handlers/service_handler.go`:
- Around line 750-753: The switch handling in service_handler.go maps
domain.ChainSwapRefunded and domain.ChainSwapRefundedUnilaterally to the same
string ("refunded"), preventing clients from distinguishing cooperative vs
unilateral refunds; update the case for domain.ChainSwapRefundedUnilaterally in
the function that returns the status string (the switch handling these domain
constants) to return a distinct value such as "refunded_unilaterally" while
leaving domain.ChainSwapRefunded returning "refunded", and update any related
documentation/tests that expect the status string.
- Around line 652-658: The response is sending the raw hex-encoded preimage into
the proto field named PreimageHash; fix by computing the HTLC preimage hash
(SHA-256) and send that instead: in the code that builds the
CreateChainSwapResponse (the block that constructs
&pb.CreateChainSwapResponse{... PreimageHash: ...}) decode
chainSwap.ClaimPreimage from hex, compute sha256.Sum256(decodedPreimage),
hex-encode the hash and assign that value to PreimageHash; ensure you import
crypto/sha256 and encoding/hex and handle decoding errors where
chainSwap.ClaimPreimage is used (matching the places using
hex.EncodeToString/hex.DecodeString for chainSwap.Preimage).

In `@internal/test/e2e/main_test.go`:
- Around line 30-40: The log messages for the refillFulmine calls are swapped
and one call has a formatting issue: when calling refillFulmine(ctx,
clientFulmineURL) update the log.Fatalf message to say "failed to refill Fulmine
used by Client" (using clientFulmineURL) and when calling refillFulmine(ctx,
boltzFulmineURL) update its log.Fatalf to say "failed to refill Fulmine used by
Boltz" (using boltzFulmineURL); also fix the missing space after the comma in
the second call's argument list. Ensure you update the three sites referencing
refillFulmine, clientFulmineURL, boltzFulmineURL, mockFulmineURL and log.Fatalf
accordingly.

In `@pkg/swap/chainswap_btc_ark_handler.go`:
- Around line 59-61: Fix the two comment typos: in the comment inside the
btcToArkHandler.HandleServerLockedMempool method change "out BTC lockup" to "our
BTC lockup", and elsewhere where the comment reads "locup" change it to "lockup"
(search for occurrences near handleBtcToArkServerLocked and other BTC
lockup-related comments). Keep the comment text otherwise unchanged.
🧹 Nitpick comments (10)
internal/test/e2e/main_test.go (1)

17-21: Inconsistent alignment in const block.

mockFulmineURL has extra whitespace for alignment but clientFulmineURL and boltzFulmineURL do not use the same padding. This is a nitpick — consider aligning all three consistently or using no alignment.

internal/infrastructure/db/sqlite/migration/20260120000000_add_chain_swaps_table.up.sql (1)

6-6: Hardcoded status range in CHECK constraint couples schema to application enum.

CHECK(status IN(0,1,2,3,4,5,6,7,8)) must be kept in sync with the Go ChainSwapStatus iota. Adding a new status requires a new migration to alter this constraint. This is acceptable for SQLite migrations but worth noting — consider a comment in the migration file documenting which status each integer maps to.

test.docker-compose.yml (1)

234-234: Using boltz/boltz:latest tag is non-deterministic for CI.

Changing from a pinned tag (boltz/boltz:ark) to latest means test results can vary depending on when the image was pulled. Consider pinning to a specific version or digest for reproducible builds.

pkg/swap/chainswap_btc_ark_handler.go (2)

126-126: Hardcoded time.Sleep(5 * time.Second) in the claim path.

This blocks the goroutine for 5 seconds between claiming VTXOs and providing the cooperative signature. If the intent is to wait for the claim to propagate, consider polling for confirmation or using an event-driven mechanism instead. A hardcoded sleep is fragile — too short on slow networks, unnecessarily slow on fast ones.


27-41: Missing doc comment on exported constructor NewBtcToArkHandler.

Per Go conventions, exported functions should have a doc comment. As per coding guidelines, exported functions should be documented with comments.

internal/test/e2e/utils_test.go (2)

38-45: Duplicated gRPC connection logic; connections are never closed.

newFulmineWalletClient is nearly identical to newFulmineClient (lines 29–36). Consider extracting a shared dialFulmine(url) (*grpc.ClientConn, error) helper that both can use, which also makes it easier to defer-close the connection at call sites.

Sketch
func dialFulmine(url string) (*grpc.ClientConn, error) {
	return grpc.NewClient(url, grpc.WithTransportCredentials(insecure.NewCredentials()))
}

func newFulmineClient(url string) (pb.ServiceClient, error) {
	conn, err := dialFulmine(url)
	if err != nil {
		return nil, err
	}
	return pb.NewServiceClient(conn), nil
}

func newFulmineWalletClient(url string) (pb.WalletServiceClient, error) {
	conn, err := dialFulmine(url)
	if err != nil {
		return nil, err
	}
	return pb.NewWalletServiceClient(conn), nil
}

178-197: unlockAndSettle opens two separate gRPC connections to the same address.

Both newFulmineWalletClient(addr) and newFulmineClient(addr) dial the same URL. A single grpc.ClientConn can serve both WalletServiceClient and ServiceClient.

Sketch
 func unlockAndSettle(addr string, pass string) error {
-	walletClient, err := newFulmineWalletClient(addr)
+	conn, err := dialFulmine(addr)
 	if err != nil {
-		return fmt.Errorf("wallet client connect %s: %w", addr, err)
+		return fmt.Errorf("connect %s: %w", addr, err)
 	}
+	defer conn.Close()
 
+	walletClient := pb.NewWalletServiceClient(conn)
 	if _, err = walletClient.Unlock(context.Background(), &pb.UnlockRequest{Password: pass}); err != nil {
 		return err
 	}
 
-	serviceClient, err := newFulmineClient(addr)
-	if err != nil {
-		return err
-	}
-
+	serviceClient := pb.NewServiceClient(conn)
 	if _, err = serviceClient.Settle(context.Background(), &pb.SettleRequest{}); err != nil {
 		return err
 	}
 
 	return nil
 }
pkg/swap/chainswap_resume.go (1)

82-100: Inline closures for address/key selection add unnecessary complexity.

The two func() string { ... }() IIFEs on lines 84–89 and 90–95 could be replaced with a simple if/else block before the call, making the code easier to read.

Sketch
+	var lockupAddr, serverPubKey string
+	if arkToBtc {
+		lockupAddr = swapResp.ClaimDetails.LockupAddress
+		serverPubKey = swapResp.ClaimDetails.ServerPublicKey
+	} else {
+		lockupAddr = swapResp.LockupDetails.LockupAddress
+		serverPubKey = swapResp.LockupDetails.ServerPublicKey
+	}
+
 	if err := validateBtcLockupAddress(
 		params.Network,
-		func() string {
-			if arkToBtc {
-				return swapResp.ClaimDetails.LockupAddress
-			}
-			return swapResp.LockupDetails.LockupAddress
-		}(),
-		func() string {
-			if arkToBtc {
-				return swapResp.ClaimDetails.ServerPublicKey
-			}
-			return swapResp.LockupDetails.ServerPublicKey
-		}(),
+		lockupAddr,
+		serverPubKey,
 		h.privateKey.PubKey(),
 		swapResp.GetSwapTree(arkToBtc),
 	); err != nil {
internal/core/application/service.go (1)

1536-1632: handleChainSwapEvent contains heavy boilerplate — consider a small helper.

All eight cases follow the identical get→apply→update pattern. A helper like the one below would cut ~70 lines and centralise the error-handling logic.

Suggested helper
// applyChainSwapMutation fetches, mutates, and persists a chain swap in one step.
func (s *Service) applyChainSwapMutation(ctx context.Context, swapID, eventName string, mutate func(*domain.ChainSwap)) {
	cs, err := s.dbSvc.ChainSwaps().Get(ctx, swapID)
	if err != nil {
		log.WithError(err).Errorf("Failed to get chain swap %s for %s", swapID, eventName)
		return
	}
	mutate(cs)
	if err := s.dbSvc.ChainSwaps().Update(ctx, *cs); err != nil {
		log.WithError(err).Errorf("Failed to update chain swap %s after %s", swapID, eventName)
	}
}

Each case then becomes:

case swap.UserLockEvent:
	s.applyChainSwapMutation(ctx, e.SwapID, "UserLockEvent", func(cs *domain.ChainSwap) {
		cs.UserLocked(e.TxID)
	})
internal/core/domain/chainswap.go (1)

77-123: Consider adding lightweight transition guards on mutation methods.

The mutation methods accept any current Status, so nothing prevents illogical transitions (e.g., calling Claimed() after Refunded()). In practice, the handler enforces sequencing, but a simple guard in the domain layer would make invariants self-documenting and catch integration bugs early.

Example guard (one method shown)
 func (cs *ChainSwap) Claimed(txid string) {
+	if IsTerminalChainSwapStatus(cs.Status) {
+		return // already in terminal state, ignore
+	}
 	cs.ClaimTxId = txid
 	cs.Status = ChainSwapClaimed
 }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/test/e2e/chainswap_test.go`:
- Around line 531-533: The call to unlockAndSettle is ignoring its returned
error—replace the incorrect assertion (require.NotEmpty(t, swapID)) after
restartDockerComposeServices with an explicit check of the error from
unlockAndSettle: assert no error (e.g., require.NoError or require.Nil) on the
variable err returned by unlockAndSettle(mockFulmineURL, fulminePass); keep or
remove the swapID presence check as appropriate since swapID was already
asserted earlier. Ensure you reference the unlockAndSettle call and its err
variable so failures during recovery fail the test.
🧹 Nitpick comments (4)
internal/test/e2e/chainswap_test.go (4)

25-59: No assertion on the balance difference after swap.

The test logs the BTC balance before and after but only asserts > 0. Consider also asserting the final swap status more defensively (e.g., check the full list length) or verifying the balance delta is within an expected range to catch fee regressions.


96-98: Balance diff is computed but never asserted.

diff is logged but not checked. If the intent is to verify the user received funds, add an assertion (e.g., require.Greater(t, diff, int64(0))). Same applies to TestChainSwapBTCtoARKWithQuote at Line 136.


542-565: Polling loop does not respect context cancellation.

If the parent context is cancelled before deadline, the loop continues polling with a dead context. The gRPC calls will fail, but the final assertion at Line 564 will report a misleading gRPC error instead of a clear timeout message. Consider adding a ctx.Done() check:

select {
case <-ctx.Done():
    require.Failf(t, "context cancelled", "while waiting for swap %s to reach %q", swapID, expected)
default:
}

Same pattern applies to refundChainSwapRPCWithRetry (Line 579) and waitForEsploraTxIndexed (Line 620).


736-739: Float-to-int truncation may cause off-by-one in sat conversion.

int(float64 * 100_000_000) truncates toward zero. Due to IEEE 754, values like 0.00003 * 1e8 could become 2999 instead of 3000. Use math.Round for safety:

-	return int(nigiriScanAddressBalanceBTC(t, ctx, addr) * 100_000_000)
+	return int(math.Round(nigiriScanAddressBalanceBTC(t, ctx, addr) * 100_000_000))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants