Swap from mainnet rust demo - #15
Conversation
WalkthroughAdded a Rust command-line workflow for creating or resuming a Boltz BTC-to-ARK swap on Mutinynet. The workflow validates swap data, reconstructs Taproot addresses, claims funded Arkade VTXOs, signs transactions, and broadcasts the finalized claim. ChangesBTC-to-ARK swap
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Main as main.rs
participant Mempool as Mempool API
participant Boltz as Boltz API
participant Arkade as Arkade operator
Main->>Arkade: connect and discover delegator
Main->>Mempool: fetch recommended fee
Main->>Boltz: create or retrieve chain swap
Boltz-->>Main: return swap parameters and scripts
Main->>Main: reconstruct and verify swap addresses
Main->>Arkade: submit signed claim transaction
Arkade-->>Main: return signed checkpoints
Main->>Arkade: finalize and broadcast transaction
Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
old/swap-from-mainnet/rust/src/main.rs (3)
581-587: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorrect the log wording for the checkpoint signing loop.
The loop calls
sign_checkpoint_transaction, so it signs. The finalize step happens at line 597. Change the two messages to "Signing checkpoint transactions..." and "Signing checkpoint transaction...".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@old/swap-from-mainnet/rust/src/main.rs` around lines 581 - 587, Update the two println! messages surrounding the sign_checkpoint_transaction loop to use “Signing checkpoint transactions...” and “Signing checkpoint transaction...”; leave the signing logic and later finalization behavior unchanged.
190-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused refund script, or use it.
Line 197 computes
_mainnet_pk_scriptand discards it. The swap request on lines 324-337 sends onlyrefundPublicKey. If the refund path needs an address, pass it to Boltz. If it does not, delete lines 197 and keep only the address validation.The name
MAINNET_ADDRESSis also misleading.networkcomes from the operator, which is Mutinynet, and the value on line 42 is a signet address. Rename it to something likeONCHAIN_REFUND_ADDRESS.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@old/swap-from-mainnet/rust/src/main.rs` around lines 190 - 197, Update the MAINNET_ADDRESS validation flow in main to remove the discarded _mainnet_pk_script, retaining only address parsing and network validation unless the refund request requires the script. Rename MAINNET_ADDRESS to ONCHAIN_REFUND_ADDRESS consistently, including its declaration, validation, error messages, and all references, while preserving the configured network validation.
353-409: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGroup the persisted swap parameters into one struct.
Six consecutive
if is_new_swap { … } else { … }expressions select the same set of values. Define oneSwapParamsstruct with two constructors, one fromCreateChainSwapResponseand one from the constants. The main flow then reads a single value, and adding a parameter changes one place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@old/swap-from-mainnet/rust/src/main.rs` around lines 353 - 409, Introduce a SwapParams struct encapsulating the persisted swap values currently selected by the consecutive is_new_swap branches, including keys, locktime, delays, and claim/refund scripts. Add constructors for CreateChainSwapResponse and the existing constants, select the appropriate constructor once in the main flow, and replace the duplicated conditional expressions with fields from that single SwapParams value.old/swap-from-mainnet/rust/Cargo.toml (1)
17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPin the
arkade-os/rust-sdkgit dependencies.The three git dependencies track the default branch. Any upstream commit changes the build result, and the example can break without a local change. Pin a
tagorrev.♻️ Proposed change
-ark-core = { git = "https://github.com/arkade-os/rust-sdk" } -ark-delegator = { git = "https://github.com/arkade-os/rust-sdk" } -ark-rest = { git = "https://github.com/arkade-os/rust-sdk" } +ark-core = { git = "https://github.com/arkade-os/rust-sdk", rev = "<commit-sha>" } +ark-delegator = { git = "https://github.com/arkade-os/rust-sdk", rev = "<commit-sha>" } +ark-rest = { git = "https://github.com/arkade-os/rust-sdk", rev = "<commit-sha>" }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@old/swap-from-mainnet/rust/Cargo.toml` around lines 17 - 19, Pin the git dependencies ark-core, ark-delegator, and ark-rest to a specific stable tag or commit revision of the arkade-os/rust-sdk repository. Keep all three dependencies aligned to the same pinned SDK version instead of tracking the default branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@old/swap-from-mainnet/rust/Cargo.toml`:
- Around line 9-15: Keep the musig dependency in Cargo.toml pinned to the locked
secp256k1 version 0.32.0-beta.2; do not change it to a stable 0.32.x version
until one is available, and preserve its existing package alias and serde
feature configuration.
In `@old/swap-from-mainnet/rust/src/main.rs`:
- Around line 27-38: Remove the hardcoded per-swap secrets and resume-state
constants, including PREIMAGE, REFUND_LOCKTIME, and the Arkade/lockup values,
from main.rs. Load these values from environment variables or a git-ignored
local state file through the existing startup flow, and document the required
local configuration in the example without committing actual swap values.
- Around line 313-343: Separate the chain-swap request flow by `is_new_swap`:
keep the POST to `/v2/swap/chain`, response parsing, and creation-time
validations only in the new-swap branch. For resume runs, skip swap creation (or
use the read endpoint with the stored swap ID) and rely on the existing resume
constants for swap data, ensuring later `swap` reads such as `lockup_amount` do
not execute without a fetched swap.
- Around line 498-518: Clone claim_address before passing it to std::iter::once
in the list_vtxos request, preserving the original value for the later
claim_address.encode() error message. Follow the existing user_address handling
pattern nearby.
---
Nitpick comments:
In `@old/swap-from-mainnet/rust/Cargo.toml`:
- Around line 17-19: Pin the git dependencies ark-core, ark-delegator, and
ark-rest to a specific stable tag or commit revision of the arkade-os/rust-sdk
repository. Keep all three dependencies aligned to the same pinned SDK version
instead of tracking the default branch.
In `@old/swap-from-mainnet/rust/src/main.rs`:
- Around line 581-587: Update the two println! messages surrounding the
sign_checkpoint_transaction loop to use “Signing checkpoint transactions...” and
“Signing checkpoint transaction...”; leave the signing logic and later
finalization behavior unchanged.
- Around line 190-197: Update the MAINNET_ADDRESS validation flow in main to
remove the discarded _mainnet_pk_script, retaining only address parsing and
network validation unless the refund request requires the script. Rename
MAINNET_ADDRESS to ONCHAIN_REFUND_ADDRESS consistently, including its
declaration, validation, error messages, and all references, while preserving
the configured network validation.
- Around line 353-409: Introduce a SwapParams struct encapsulating the persisted
swap values currently selected by the consecutive is_new_swap branches,
including keys, locktime, delays, and claim/refund scripts. Add constructors for
CreateChainSwapResponse and the existing constants, select the appropriate
constructor once in the main flow, and replace the duplicated conditional
expressions with fields from that single SwapParams value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 59318896-2ae3-4d9b-b12b-f2b2f317f557
📒 Files selected for processing (2)
old/swap-from-mainnet/rust/Cargo.tomlold/swap-from-mainnet/rust/src/main.rs
| bitcoin = { version = "0.32.7", features = ["rand", "base64"] } | ||
| hex = "0.4" | ||
| tokio = { version = "1", features = ["rt-multi-thread", "macros"] } | ||
| reqwest = { version = "0.12", features = ["rustls-tls", "json"] } | ||
| serde = { version = "1", features = ["derive"] } | ||
| serde_json = "1" | ||
| musig = { package = "secp256k1", version = "0.32.0-beta.2", features = ["serde"] } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
secp256k1 Rust crate 0.32.0-beta.2 musig module KeyAggCache::new signature docs.rs
💡 Result:
In the secp256k1 Rust crate version 0.32.0-beta.2, the musig module was introduced as part of the implementation of MuSig2 [1][2]. The MusigKeyAggCache struct exists within the secp256k1-sys crate (which provides the underlying FFI bindings) as an opaque data structure (pub struct MusigKeyAggCache(/* private fields */)) [3][4]. Regarding KeyAggCache::new: 1. This specific constructor is not exposed as a standard or public new() function for the MusigKeyAggCache type in the secp256k1 crate's high-level API documentation [5][3][6]. 2. The MusigKeyAggCache is generally intended to be initialized or populated via the underlying C library's MuSig2 workflow (e.g., key aggregation processes), rather than manual instantiation [2][7][8]. 3. If you encounter a KeyAggCache::new() signature in related projects (such as ark-secp256k1-zkp-sys), note that this is distinct from the official rust-bitcoin/rust-secp256k1 implementation [2][9]. For high-level usage, you should interact with the musig module provided by the secp256k1 crate, which provides a type-safe wrapper around the FFI functions [5][2]. Consult the examples/musig.rs file within the rust-secp256k1 repository for the intended usage pattern of the MuSig2 interface [2].
Citations:
- 1: https://github.com/rust-bitcoin/rust-secp256k1/blob/master/CHANGELOG.md
- 2: Add Musig2 module rust-bitcoin/rust-secp256k1#716
- 3: https://docs.rs/secp256k1-sys/latest/secp256k1_sys/struct.MusigKeyAggCache.html
- 4: https://docs.rs/secp256k1-sys/
- 5: https://docs.rs/crate/secp256k1/0.32.0-beta.2
- 6: https://docs.rs/secp256k1/latest/secp256k1/
- 7: https://github.com/bitcoin-core/secp256k1/blob/c9362664e672efeee8a0c1ee8779071432b78394/src/modules/musig/keyagg_impl.h
- 8: https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/musig/keyagg_impl.h
- 9: https://docs.rs/ark-secp256k1-zkp-sys/latest/ark_secp256k1_zkp_sys/struct.MusigKeyAggCache.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tracked files of interest =="
git ls-files | rg '(^|/)(Cargo\.toml|Cargo\.lock|main\.rs|lib\.rs)$' | sed -n '1,120p'
echo
echo "== Cargo.toml dependency section =="
cat -n old/swap-from-mainnet/rust/Cargo.toml | sed -n '1,80p'
echo
echo "== Cargo.lock secp256k1 entries =="
python3 - <<'PY'
from pathlib import Path
p=Path('old/swap-from-mainnet/rust/Cargo.lock')
if not p.exists():
print('Cargo.lock missing')
raise SystemExit(0)
text=p.read_text()
for i,line in enumerate(text.splitlines(),1):
if line.strip().startswith('name = "secp256k1"'):
start=max(1,i-8); end=min(len(text.splitlines()), i+25)
print(f'-- entry around line {i} --')
for n in range(start,end+1):
print(f'{n}: {text.splitlines()[n-1]}')
PY
echo
echo "== musig usages =="
rg -n 'musig|KeyAggCache|musig' old/swap-from-mainnet/rust -S || true
echo
echo "== docs.rs source availability for 0.32.0-beta.2 =="
python3 - <<'PY'
import requests
base='https://docs.rs/crate/secp256k1/0.32.0-beta.2/source/src/'
paths=[
'lib.rs',
'musig/mod.rs',
'musig/keyagg.rs',
]
for path in paths:
url=base+path
r=requests.get(url, timeout=20)
print(f'--- {path}: {r.status_code} ---')
if r.status_code == 200:
lines=r.text.splitlines()
for j,l in enumerate(lines[:160],1):
if 'musig' in l or 'KeyAggCache' in l or 'pub mod musig' in l or 'musig' in l.lower() or 'new(' in l and 'KeyAgg' in l:
print(f'{j}: {l}')
else:
print(r.text[:500])
PYRepository: arkade-os/demos
Length of output: 2268
🌐 Web query:
docs.rs secp256k1 0.32.0-beta.2 musig KeyAggCache::new source
💡 Result:
In the rust-secp256k1 crate (version 0.32.0-beta.2), the MuSig2 key aggregation functionality is part of the musig module [1][2]. The KeyAggCache struct is used for storing the state required during the key aggregation process [2][3]. Regarding the KeyAggCache::new source: 1. Binding and Implementation: The secp256k1 crate provides type-safe Rust wrappers around the underlying C library, libsecp256k1 [4][5]. The musig functionality in the Rust crate interacts with the MuSig module of the C library [1]. 2. C-level Source: At the C level (libsecp256k1), the core logic for key aggregation and caching is located in files such as src/modules/musig/keyagg_impl.h [6]. This includes internal functions like secp256k1_keyagg_cache_save and secp256k1_keyagg_cache_load, which manage the internal data structures for the cache [6]. 3. Rust Usage: In the secp256k1 crate, KeyAggCache::new (or equivalent constructors depending on the specific API version) is used to initialize the cache with the participant public keys [2][7]. The implementation typically involves calling the corresponding C-FFI functions to initialize the secp256k1_musig_keyagg_cache structure [1][6]. If you are looking for the exact lines of code, you can inspect the src/musig.rs file within the rust-secp256k1 repository on GitHub, as it contains the Rust-side implementation that maps to the C-FFI [1]. The underlying C logic is found in the bitcoin-core/secp256k1 repository under src/modules/musig/ [6].
Citations:
- 1: Add Musig2 module rust-bitcoin/rust-secp256k1#716
- 2: BoltzExchange/boltz-backend@56aec71
- 3: BoltzExchange/boltz-backend@a236a47
- 4: https://docs.rs/crate/secp256k1/0.32.0-beta.2
- 5: https://crates.io/crates/secp256k1
- 6: https://github.com/bitcoin-core/secp256k1/blob/c9362664e672efeee8a0c1ee8779071432b78394/src/modules/musig/keyagg_impl.h
- 7: BTCDecoded/blvm-secp256k1@c1e07c9
🌐 Web query:
github rust-bitcoin rust-secp256k1 0.32.0-beta.2 musig KeyAggCache::new new
💡 Result:
In the rust-secp256k1 crate, version 0.32.0-beta.2 includes a MuSig2 module that provides high-level Rust bindings for the underlying libsecp256k1 MuSig2 functionality [1][2][3][4]. The KeyAggCache struct is a key component within this module, used for handling public key aggregation [5][6]. Based on the library's API design and usage patterns, KeyAggCache::new is typically invoked by passing a slice of public keys (e.g., &[[u8; 33]]) [5][6]. For example, the usage follows this pattern: let cache = KeyAggCache::new(&pubkeys).unwrap; In this context, pubkeys is expected to be a collection of compressed public keys that are to be aggregated [6]. After creating the cache, you can access the aggregated public key using methods such as agg_pk or agg_pk_xonly [6]. Technical notes: - The MuSig2 module was introduced to provide formal support for these operations, replacing the need for manual or private fork implementations [7][8][4]. - Ensure your project includes the necessary features (typically rand and std) if required by your specific implementation, as the MuSig2 API relies on the underlying C library's session and key management structures [9][4]. - Always consult the documentation specifically for version 0.32.0-beta.2 on docs.rs for the most accurate and version-specific type signatures, as the API may have evolved significantly in later releases [1][10].
Citations:
- 1: https://docs.rs/crate/secp256k1/0.32.0-beta.2
- 2: rust-bitcoin/rust-secp256k1@6e5c556
- 3: https://crates.io/crates/secp256k1
- 4: Add Musig2 module rust-bitcoin/rust-secp256k1#716
- 5: BoltzExchange/boltz-backend@a236a47
- 6: BTCDecoded/blvm-secp256k1@c1e07c9
- 7: https://github.com/rust-bitcoin/rust-secp256k1/blob/master/CHANGELOG.md
- 8: Upgrade rust-secp256k1 dependency in rust-bitcoin 0.32.x to allow for musig use rust-bitcoin/rust-bitcoin#6117
- 9: https://docs.rs/secp256k1/
- 10: https://docs.rs/secp256k1-sys/latest/secp256k1_sys/struct.MusigKeyAggCache.html
🌐 Web query:
github repository rust-bitcoin rust-secp256k1 tag v0.32.0-beta.2 src musig KeyAggCache::new
💡 Result:
In the rust-secp256k1 library, specifically version v0.32.0-beta.2, the MuSig2 functionality is implemented within the musig module [1][2]. Regarding your query about KeyAggCache::new: 1. Location: In the high-level rust-secp256k1 crate, the musig module exposes an API for MuSig2 operations [2]. While the underlying C library (libsecp256k1) uses a struct called secp256k1_musig_keyagg_cache [3][4], the Rust interface typically abstracts this interaction [2]. 2. KeyAggCache::new: The specific method KeyAggCache::new is generally found in the low-level FFI (Foreign Function Interface) crate, secp256k1-sys, which provides direct bindings to the C structures (often as MusigKeyAggCache) [5]. In the high-level rust-secp256k1 crate, which aims to provide a safe, idiomatic Rust API, functionality like key aggregation is usually handled through safe wrappers that manage context and cache initialization internally, rather than requiring the user to manually instantiate a new cache via a constructor like new [6]. 3. Context: The introduction of the musig module in version 0.32.0-beta.2 [1] was a significant update, allowing for the implementation of BIP 327 [2][4]. If you are looking to use this functionality in your application, you should consult the examples provided in the rust-secp256k1 repository (e.g., examples/musig.rs) [2], which demonstrate the idiomatic way to perform key aggregation and signing without needing to interact with the raw KeyAggCache::new method directly.
Citations:
- 1: https://github.com/rust-bitcoin/rust-secp256k1/blob/master/CHANGELOG.md
- 2: Add Musig2 module rust-bitcoin/rust-secp256k1#716
- 3: https://github.com/bitcoin-core/secp256k1/blob/c9362664e672efeee8a0c1ee8779071432b78394/src/modules/musig/keyagg_impl.h
- 4: https://doxygen.bitcoincore.org/keyagg_8h_source.html
- 5: https://docs.rs/secp256k1-sys/latest/secp256k1_sys/struct.MusigKeyAggCache.html
- 6: https://docs.rs/secp256k1/latest/secp256k1/
🌐 Web query:
docs.rs secp256k1 0.32.0-beta.2 source src/musig.rs KeyAggCache::new
💡 Result:
In the secp256k1 crate version 0.32.0-beta.2, the KeyAggCache::new function is part of the musig module [1]. It is used to initialize a cache for MuSig2 key aggregation [2]. The function signature typically accepts a slice of public keys, which are used to compute the aggregated public key [2]. According to usage patterns in the ecosystem, the function is called as KeyAggCache::new(pubkeys), where pubkeys is a slice containing the participant public keys (often as compressed byte arrays) [2]. The musig module was introduced to the secp256k1 crate to provide type-safe Rust bindings for MuSig2 functionality [1][3]. Because 0.32.0-beta.2 is a beta release, the API reflects the implementation of MuSig2 as defined in the underlying libsecp256k1 bindings [1][3]. You can find examples of its implementation in the examples/musig.rs file within the rust-secp256k1 repository [1][4].
Citations:
- 1: Add Musig2 module rust-bitcoin/rust-secp256k1#716
- 2: BTCDecoded/blvm-secp256k1@c1e07c9
- 3: https://docs.rs/crate/secp256k1/0.32.0-beta.2
- 4: https://github.com/rust-bitcoin/rust-secp256k1/blob/master/Cargo.toml
🌐 Web query:
crates.io secp256k1 versions 0.32 stable after 0.32.0-beta.2
💡 Result:
As of August 1, 2026, the secp256k1 crate on crates.io has not released a stable 0.32.0 version [1][2]. The latest version available is 0.32.0-beta.2, which was published on October 30, 2025 [1][3]. There have been no stable releases in the 0.32.x series following the beta releases [1][2]. The current stable version remains 0.31.1 [1].
Citations:
- 1: https://crates.io/crates/secp256k1
- 2: https://crates.io/crates/secp256k1/versions
- 3: https://lib.rs/crates/secp256k1
🌐 Web query:
https://docs.rs/crate/secp256k1/0.32.0-beta.2/docs/src/secp256k1/musig.rs.html KeyAggCache::new
💡 Result:
In the context of the Rust secp256k1 crate and the underlying libsecp256k1 library, KeyAggCache (specifically the C type secp256k1_musig_keyagg_cache) is an opaque data structure used to cache information required for MuSig2 public key aggregation and signing [1][2]. Purpose and Usage: The KeyAggCache structure is primarily used to store the results of public key aggregation, which is necessary for subsequent operations like generating partial signatures [1][3]. When performing MuSig2 operations (as defined in BIP 327), functions such as secp256k1_musig_pubkey_agg initialize this cache with the aggregated public key and other related data [1][3]. This cached information is then consumed by other MuSig functions to ensure consistency and efficiency, avoiding the need to re-compute or re-derive values during each step of the signing or verification process [2][3]. Technical Details: - The structure is opaque, meaning its internal representation is implementation-defined and should not be accessed or manipulated directly [1]. - It is designed to be a 197-byte fixed-size structure [1]. - Internally, it stores critical state including the aggregated public key, the "second" public key (used for MuSig2), a hash of all participating public keys, the parity of the key (if tweaked), and the tweak itself [2]. - Because it holds computed state, it can be safely copied or moved, but users should utilize provided serialization/parsing functions or initialization functions (like musig_pubkey_agg) rather than attempting to construct or modify the object manually [1][3]. In the Rust wrapper (the secp256k1 crate), these low-level C bindings are typically exposed via the secp256k1-sys crate or wrapped in higher-level, safe Rust abstractions within the main secp256k1 crate [4][5]. The new() method associated with such structures in these wrappers generally initializes the object, often ensuring it is in a blank or "safe" state before use by the library's FFI functions [6][5].
Citations:
- 1: https://doxygen.bitcoincore.org/structsecp256k1__musig__keyagg__cache.html
- 2: https://github.com/bitcoin-core/secp256k1/blob/c9362664e672efeee8a0c1ee8779071432b78394/src/modules/musig/keyagg_impl.h
- 3: https://doxygen.bitcoincore.org/keyagg__impl_8h.html
- 4: https://docs.rs/crate/secp256k1/0.32.0-beta.2
- 5: https://docs.rs/secp256k1-sys/latest/secp256k1_sys/struct.MusigKeyAggCache.html
- 6: https://docs.rs/ark-secp256k1-zkp-sys/latest/ark_secp256k1_zkp_sys/struct.MusigKeyAggCache.html
Keep the MuSig dependency fixed and aware that it has no stable release.
secp256k1 still has no stable 0.32.x release; use the locked 0.32.0-beta.2 until a stable API is available, because beta-only crypto crates can still change APIs or behavior without notice.
[maintainabili
ty_and_code_quality]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@old/swap-from-mainnet/rust/Cargo.toml` around lines 9 - 15, Keep the musig
dependency in Cargo.toml pinned to the locked secp256k1 version 0.32.0-beta.2;
do not change it to a stable 0.32.x version until one is available, and preserve
its existing package alias and serde feature configuration.
| const PREIMAGE: &str = "65d4240a1fa11515f70e8e7f96d287f8583bb701858551bebdafdc78873a619f"; | ||
| const REFUND_LOCKTIME: u32 = 1783959716; | ||
| const ARKADE_BOLTZ_PUBKEY_COMPRESSED: &str = | ||
| "03f06b63aed9643c3a726f3973c3bfbaf2ac1a5bed618966e14f11de19746885a0"; | ||
| const ARKADE_UNILATERAL_CLAIM_DELAY_SECONDS: u32 = 4096; | ||
| const ARKADE_UNILATERAL_REFUND_DELAY_SECONDS: u32 = 4608; | ||
| const ARKADE_UNILATERAL_REFUND_WITHOUT_RECEIVER_DELAY_SECONDS: u32 = 5120; | ||
| const LOCKUP_PUBKEY_COMPRESSED: &str = | ||
| "030d6a0a348fde9e9597001cba8b7d9aa8756b82ad75507009b243d71b30c704a7"; | ||
| const LOCKUP_CLAIM_LEAF_SCRIPT: &str = "82012088a914751a19caf711b64618ca8cf892edaa370a01d9af88200d6a0a348fde9e9597001cba8b7d9aa8756b82ad75507009b243d71b30c704a7ac"; | ||
| const LOCKUP_REFUND_LEAF_SCRIPT: &str = | ||
| "20cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115ad03e0b731b1"; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Do not commit the swap preimage.
PREIMAGE is the claim secret for the swap. Any reader of the repository can claim the Arkade VTXO of that swap. The same applies to the other per-swap values on lines 28-38, which are resume state and not configuration.
Read these values from environment variables or from a local, git-ignored state file, and document that in the example.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@old/swap-from-mainnet/rust/src/main.rs` around lines 27 - 38, Remove the
hardcoded per-swap secrets and resume-state constants, including PREIMAGE,
REFUND_LOCKTIME, and the Arkade/lockup values, from main.rs. Load these values
from environment variables or a git-ignored local state file through the
existing startup flow, and document the required local configuration in the
example without committing actual swap values.
| println!( | ||
| "{}", | ||
| if is_new_swap { "Creating chain swap..." } else { "Fetching chain swap details..." } | ||
| ); | ||
|
|
||
| let client = reqwest::Client::new(); | ||
| let preimage_hash_for_request = if is_new_swap { | ||
| sha256::Hash::hash(&preimage) | ||
| } else { | ||
| sha256::Hash::hash(&rand::random::<[u8; 32]>()) | ||
| }; | ||
| let response = client | ||
| .post(format!("{BOLTZ_API}/v2/swap/chain")) | ||
| .json(&serde_json::json!({ | ||
| "from": "BTC", | ||
| "to": "ARK", | ||
| "feeSatsPerByte": fee_rate, | ||
| "claimPublicKey": hex::encode(user_pk_compressed.to_bytes()), | ||
| "refundPublicKey": hex::encode(user_pk_compressed.to_bytes()), | ||
| // Amount Boltz should lock on Arkade. | ||
| "serverLockAmount": SWAP_AMOUNT, | ||
| "preimageHash": hex::encode(preimage_hash_for_request.as_byte_array()), | ||
| })) | ||
| .send() | ||
| .await?; | ||
|
|
||
| if !response.status().is_success() { | ||
| anyhow::bail!("failed to create chain swap: {}", response.text().await?); | ||
| } | ||
|
|
||
| let swap: CreateChainSwapResponse = response.json().await?; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
The resume path creates a new swap instead of fetching the existing one.
Lines 318-337 POST /v2/swap/chain on both paths. When is_new_swap is false, this creates a second swap with a random preimage hash (lines 319-323) and with feeSatsPerByte = 1, because the fee refresh on lines 304-310 runs only for new swaps. The log line 315 says "Fetching chain swap details", which does not match the request.
Three consequences follow:
- Each resume run leaves an orphan swap at Boltz that nobody can ever claim, because the preimage for the random hash is discarded.
- Line 410 reads
lockup_amountfrom the new response, and line 492 prints it next to resume constants. The reported swap data is then inconsistent. - The amount check on lines 345-350 validates the new swap, not the swap being claimed.
The resume path already has every value it needs in the constants. Skip the Boltz call, or call the read endpoint for the stored swap id.
🐛 Proposed structure
- let client = reqwest::Client::new();
- let preimage_hash_for_request = if is_new_swap {
- sha256::Hash::hash(&preimage)
- } else {
- sha256::Hash::hash(&rand::random::<[u8; 32]>())
- };
- let response = client
- .post(format!("{BOLTZ_API}/v2/swap/chain"))
- .json(&serde_json::json!({
- "from": "BTC",
- "to": "ARK",
- "feeSatsPerByte": fee_rate,
- "claimPublicKey": hex::encode(user_pk_compressed.to_bytes()),
- "refundPublicKey": hex::encode(user_pk_compressed.to_bytes()),
- // Amount Boltz should lock on Arkade.
- "serverLockAmount": SWAP_AMOUNT,
- "preimageHash": hex::encode(preimage_hash_for_request.as_byte_array()),
- }))
- .send()
- .await?;
-
- if !response.status().is_success() {
- anyhow::bail!("failed to create chain swap: {}", response.text().await?);
- }
-
- let swap: CreateChainSwapResponse = response.json().await?;
+ // Only a new swap requires a Boltz request. A resumed swap uses the persisted constants.
+ let swap: Option<CreateChainSwapResponse> = if is_new_swap {
+ let client = reqwest::Client::new();
+ let response = client
+ .post(format!("{BOLTZ_API}/v2/swap/chain"))
+ .json(&serde_json::json!({
+ "from": "BTC",
+ "to": "ARK",
+ "feeSatsPerByte": fee_rate,
+ "claimPublicKey": hex::encode(user_pk_compressed.to_bytes()),
+ "refundPublicKey": hex::encode(user_pk_compressed.to_bytes()),
+ // Amount Boltz should lock on Arkade.
+ "serverLockAmount": SWAP_AMOUNT,
+ "preimageHash": hex::encode(sha256::Hash::hash(&preimage).as_byte_array()),
+ }))
+ .send()
+ .await?;
+
+ if !response.status().is_success() {
+ anyhow::bail!("failed to create chain swap: {}", response.text().await?);
+ }
+ Some(response.json().await?)
+ } else {
+ None
+ };Every later swap.… read then moves inside the is_new_swap branch, and the resume path reads the constants only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@old/swap-from-mainnet/rust/src/main.rs` around lines 313 - 343, Separate the
chain-swap request flow by `is_new_swap`: keep the POST to `/v2/swap/chain`,
response parsing, and creation-time validations only in the new-swap branch. For
resume runs, skip swap creation (or use the read endpoint with the stored swap
ID) and rely on the existing resume constants for swap data, ensuring later
`swap` reads such as `lockup_amount` do not execute without a fetched swap.
| let vtxos_response = operator | ||
| .list_vtxos( | ||
| GetVtxosRequest::new_for_addresses(std::iter::once(claim_address)) | ||
| .spendable_only() | ||
| .map_err(|e| anyhow::anyhow!("{e}"))?, | ||
| ) | ||
| .await | ||
| .map_err(|e| anyhow::anyhow!("{e}"))?; | ||
|
|
||
| // Filter out inputs with Arkade assets. | ||
| let inputs: Vec<_> = vtxos_response | ||
| .vtxos | ||
| .into_iter() | ||
| .filter(|input| input.assets.is_empty()) | ||
| .collect(); | ||
| let input_total: u64 = inputs.iter().map(|v| v.amount.to_sat()).sum(); | ||
| println!("Contract balance: [{input_total}]"); | ||
|
|
||
| if input_total == 0 { | ||
| anyhow::bail!("Claim address not funded (address: {})", claim_address.encode()); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Resolve the ArkAddress definition and the new_for_addresses signature in the ark SDK sources.
set -uo pipefail
# Locate any vendored/checked-out ark-core sources in the repo.
fd -t f -e rs . | xargs rg -n --no-messages -C 4 'pub struct ArkAddress|impl .*Copy.*for ArkAddress|derive\([^)]*Copy[^)]*\)\s*\n\s*pub struct ArkAddress'
# Show how sibling examples pass Arkade addresses into requests.
rg -n -C 3 'new_for_addresses'Repository: arkade-os/demos
Length of output: 347
🏁 Script executed:
#!/bin/bash
set -uo pipefail
echo "== files =="
git ls-files | sed -n '1,120p'
echo
echo "== target file lines 460-570 =="
fd -a 'main\.rs$' . | grep 'old/swap-from-mainnet/rust/src/main.rs' | xargs sed -n '460,570p'
echo
echo "== ArkAddress definitions usages =="
rg -n --no-messages -C 3 'ArkAddress|new_for_addresses|use .*address|mod address' old/swap-from-mainnet/rust/src || true
echo
echo "== copy derives/impls =="
rg -n --no-messages --multiline -C 3 'derive\([^)]*Copy[^)]*\)[[:space:]]*\n[[:space:]]*pub struct ArkAddress|impl .*Copy.*for ArkAddress|pub struct ArkAddress' . || trueRepository: arkade-os/demos
Length of output: 11824
🏁 Script executed:
#!/bin/bash
set -uo pipefail
echo "== target file 1-80 =="
sed -n '1,80p' old/swap-from-mainnet/rust/src/main.rs
echo
echo "== target file full address/tokens =="
rg -n --no-messages 'ArkAddress|address::|new_for_addresses|claim_address|user_address|GetVtxosRequest' old/swap-from-mainnet/rust/src/main.rs
echo
echo "== dependencies mentioning ark/sdk =="
for f in old/swap-from-mainnet/rust/Cargo.toml Cargo.toml Cargo.lock; do
if [ -f "$f" ]; then
echo "--- $f ---"
rg -n --no-messages -C 3 'ark|arkade|ark-deps|ark-core|ark-wallet' "$f" || true
fi
doneRepository: arkade-os/demos
Length of output: 4717
🏁 Script executed:
#!/bin/bash
set -uo pipefail
echo "== rust-sdk ref/config locations =="
git ls-files | rg 'Cargo\.toml$|Cargo\.lock$' | xargs -r rg -n --no-messages -C 4 'ark-core|ark-delegator|ark-rest|rust-sdk|rev =|tag =' || true
echo
echo "== lockfile ark sections =="
sed -n '1,220p' Cargo.lock | rg -n --no-messages -C 5 'ark-core|ark-delegator|ark-rest|name = "ark' || true
sed -n '221,440p' Cargo.lock | rg -n --no-messages -C 5 'ark-core|ark-delegator|ark-rest|name = "ark' || true
echo
echo "== try inspect lockfile git refs =="
python3 - <<'PY'
from pathlib import Path
p=Path('Cargo.lock')
if p.exists():
for i,line in enumerate(p.read_text().splitlines(),1):
if 'rust-sdk' in line or 'ark-core' in line or 'ark-delegator' in line or 'ark-rest' in line:
start=max(1,i-8); end=min(p.read_text().splitlines().__len__(), i+20)
all=p.read_text().splitlines()
print(f'--- around line {i} ---')
for n in range(start,end+1):
print(f'{n}: {all[n-1]}')
PYRepository: arkade-os/demos
Length of output: 4532
🌐 Web query:
github arkade-os rust-sdk ArkAddress derive Copy GetVtxosRequest new_for_addresses
💡 Result:
The arkade-os/rust-sdk is a collection of Rust crates designed to facilitate building Bitcoin wallets using the Arkade protocol [1][2]. Regarding your specific code references: ArkAddress and Copy The ArkAddress type in the ark-core crate is used to represent Ark addresses [1][3]. In the repository's codebase, ArkAddress is often used in conjunction with other types that implement traits such as Clone and Copy. For example, in the ark-client-sample crate, custom wrappers around ArkAddress use #[derive(Clone)] [3]. Historical commit history indicates that various internal types related to key management and addresses have implemented Clone and Copy to facilitate easier handling within the wallet logic [4]. GetVtxosRequest and new_for_addresses The GetVtxosRequest::new_for_addresses method is a constructor used to create a request for retrieving VTXOs (Virtual UTXOs) associated with a list of specific addresses from an Arkade server [5]. It is typically used during wallet synchronization or key discovery processes, where the client needs to query the server for all VTXOs belonging to a batch of addresses [5]. Example Usage As seen in recent implementation patterns within the SDK, it is used as follows: // Create a list of addresses let addresses: Vec<> = batch.iter.map(|(, _, addr)| addr.clone).collect; // Construct the request for the addresses let request = GetVtxosRequest::new_for_addresses(&addresses); // Use the request to list VTXOs from the network client let list = self.network_client.list_vtxos(request).await?; The latest stable version of the SDK as of July 2026 is v0.10.0 [6]. Documentation is available on docs.rs [1][2].
Citations:
- 1: https://github.com/arkade-os/rust-sdk
- 2: https://github.com/arkade-os/rust-sdk/blob/master/README.md
- 3: https://github.com/arkade-os/rust-sdk/blob/master/ark-client-sample/src/main.rs
- 4: arkade-os/rust-sdk@d1b1e41
- 5: arkade-os/rust-sdk@7043351
- 6: https://github.com/arkade-os/rust-sdk/releases/tag/v0.10.0
🌐 Web query:
github arkade-os rust-sdk ark-core address ark_address.rs ArkAddress impl Copy Address
💡 Result:
In the ark-core crate (part of the Arkade Rust SDK), ArkAddress is a core type used to represent Arkade addresses [1][2][3]. Regarding your query about ArkAddress implementing the Copy trait, current usage patterns and documentation indicate that ArkAddress is typically designed to be clonable and passed around in application code [4]. In the ark-client-sample crate, for example, ArkAddress is wrapped in structs deriving Clone and is frequently passed to functions and used in logic [4]. While specific source code for ark_address.rs would need to be checked in the repository for the definitive trait bounds, the standard library and common Rust patterns for such address types (which typically contain data like public keys or script information) generally favor Clone over Copy unless the type is very small (like a fixed-size byte array) [4]. You can find the ArkAddress definition and its implementations in the ark-core crate, which is published on crates.io and documented on docs.rs [1][5]. Key references: - Repository: https://github.com/arkade-os/rust-sdk [2] - Crate: https://crates.io/crates/ark-core [1] - Documentation: https://docs.rs/ark-core [5]
Citations:
- 1: https://crates.io/crates/ark-core
- 2: https://github.com/arkade-os/rust-sdk
- 3: https://docs.rs/ark-core/latest/ark_core/all.html
- 4: https://github.com/arkade-os/rust-sdk/blob/master/ark-client-sample/src/main.rs
- 5: https://docs.rs/ark-core
🌐 Web query:
github arkade-os rust-sdk ArkAddress Copy impl Copy for Address
💡 Result:
The Arkade protocol Rust SDK, maintained in the arkade-os/rust-sdk repository [1][2], defines the ArkAddress type within the ark-core crate [2][3]. Regarding the implementation of the Copy trait for ArkAddress: Based on current repository usage and public documentation, the ArkAddress type typically derives or implements Clone, which is often sufficient for most use cases in the SDK, as seen in examples like the ArkAddressCli wrapper [4]. There is no standard requirement or widespread implementation of the Copy trait for ArkAddress in the official ark-core crate (version 0.10.1) [2]. If your specific development requires Copy, you may need to check if the underlying fields of ArkAddress (which typically include protocol-specific identifiers or keys) satisfy the requirements for Copy in your specific version, or consider using.clone if the type is intended to remain non-Copy for design reasons [4]. You can verify the exact trait implementations for the current version by checking the Trait Implementations section of the ArkAddress struct in the official documentation at docs.rs/ark-core [3].
Citations:
- 1: https://github.com/arkade-os/rust-sdk
- 2: https://crates.io/crates/ark-core
- 3: https://docs.rs/ark-core
- 4: https://github.com/arkade-os/rust-sdk/blob/master/ark-client-sample/src/main.rs
🌐 Web query:
github arkade-os rust-sdk ark-core src vhtlc.rs VhtlcScript address()
💡 Result:
In the Arkade protocol's Rust SDK, specifically within the ark-core crate (which handles core protocol types and transaction utilities), the VhtlcScript struct resides in the vhtlc module [1][2]. The VhtlcScript is used to construct the script tree for VHTLC (Verifiable Hashed Timelock Contract) outputs. While the exact signature may vary by version, the address() method on VhtlcScript typically follows the pattern observed in the Arkade ecosystem's SDKs (including TypeScript and Go) [3][4][5]. It is generally used to derive a Taproot address from the script by providing network parameters (such as the Human Readable Part, or HRP) and a public key (often the server's public key) used to tweak the script for address generation [3][4][6]. For the most accurate and up-to-date signature, you should refer to the API documentation generated for the specific version of ark-core you are using (available on docs.rs/ark-core) [1]. The ark-core crate is a core dependency of the Arkade Rust SDK [7][1].
Citations:
- 1: https://crates.io/crates/ark-core
- 2: https://docs.rs/ark-core/latest/ark_core/all.html
- 3: https://github.com/arkade-os/ts-sdk/blob/244b10dd/test/e2e/vhtlc.test.ts
- 4: https://github.com/arkade-os/ts-sdk/blob/244b10dd/test/vhtlc.test.ts
- 5: VHTLC contract handler and standalone vhtlc package go-sdk#190
- 6: https://docs.arkadeos.com/contracts/hashlock
- 7: https://github.com/arkade-os/rust-sdk
Clone claim_address before passing it to new_for_addresses.
claim_address is copied by std::iter::once(...) on line 500, so claim_address.encode() is still a moved value at line 517. Clone the address before collecting it, as shown for user_address on line 547.
🐛 Proposed fix
- GetVtxosRequest::new_for_addresses(std::iter::once(claim_address))
+ GetVtxosRequest::new_for_addresses(std::iter::once(claim_address.clone()))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let vtxos_response = operator | |
| .list_vtxos( | |
| GetVtxosRequest::new_for_addresses(std::iter::once(claim_address)) | |
| .spendable_only() | |
| .map_err(|e| anyhow::anyhow!("{e}"))?, | |
| ) | |
| .await | |
| .map_err(|e| anyhow::anyhow!("{e}"))?; | |
| // Filter out inputs with Arkade assets. | |
| let inputs: Vec<_> = vtxos_response | |
| .vtxos | |
| .into_iter() | |
| .filter(|input| input.assets.is_empty()) | |
| .collect(); | |
| let input_total: u64 = inputs.iter().map(|v| v.amount.to_sat()).sum(); | |
| println!("Contract balance: [{input_total}]"); | |
| if input_total == 0 { | |
| anyhow::bail!("Claim address not funded (address: {})", claim_address.encode()); | |
| } | |
| let vtxos_response = operator | |
| .list_vtxos( | |
| GetVtxosRequest::new_for_addresses(std::iter::once(claim_address.clone())) | |
| .spendable_only() | |
| .map_err(|e| anyhow::anyhow!("{e}"))?, | |
| ) | |
| .await | |
| .map_err(|e| anyhow::anyhow!("{e}"))?; | |
| // Filter out inputs with Arkade assets. | |
| let inputs: Vec<_> = vtxos_response | |
| .vtxos | |
| .into_iter() | |
| .filter(|input| input.assets.is_empty()) | |
| .collect(); | |
| let input_total: u64 = inputs.iter().map(|v| v.amount.to_sat()).sum(); | |
| println!("Contract balance: [{input_total}]"); | |
| if input_total == 0 { | |
| anyhow::bail!("Claim address not funded (address: {})", claim_address.encode()); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@old/swap-from-mainnet/rust/src/main.rs` around lines 498 - 518, Clone
claim_address before passing it to std::iter::once in the list_vtxos request,
preserving the original value for the later claim_address.encode() error
message. Follow the existing user_address handling pattern nearby.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Draft noted and the "pending confirmation of swap logic from boltz" caveat is understood, but several issues here cause permanent fund loss and must be fixed before this can land in any state — even merged-as-reference-only.
🔴 Fund-loss bugs
1. Resume path sends a random preimage hash — VHTLC funds permanently locked
src/main.rs:302-307
let preimage_hash_for_request = if is_new_swap {
sha256::Hash::hash(&preimage)
} else {
sha256::Hash::hash(&rand::random::<[u8; 32]>()) // ← unrelated hash
};When is_new_swap = false (i.e. the constants are filled in), the POST to Boltz receives a freshly-randomised preimage hash that has no relationship to the stored PREIMAGE. Boltz creates a brand-new swap whose VHTLC payment hash can never be satisfied by the real preimage. Any funds Boltz locks into that VHTLC are unclaimable until the refund timeout.
The resume flow should POST with sha256::Hash::hash(&preimage) unconditionally, then verify the swap ID / VHTLC address matches what was originally negotiated (ideally looked up by stored swap ID, not re-created).
2. REFUND_LOCKTIME is already in the past
src/main.rs:33
const REFUND_LOCKTIME: u32 = 1783959716;1783959716 = 2026-07-13 16:21:56 UTC. Today is 2026-08-01. The check at main.rs:226-231:
if REFUND_LOCKTIME == 0 || (now as u32) > REFUND_LOCKTIME {
anyhow::bail!("REFUND_LOCKTIME must be set to a valid future timestamp …");
}…immediately bails on every run. The resume path is dead code in its current form.
🔴 Security — key material
3. ALICE_MNEMONIC is the universal test mnemonic
src/main.rs:41
const ALICE_MNEMONIC: &str =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";The private key derived from this mnemonic at m/86'/0'/0'/0/0 is published in countless blog posts, test suites, and toolkits. Any satoshis funded to the derived address are immediately sweepable by the general public. Even for a demo the wallet derivation should use a randomly generated key (or at minimum a clearly non-standard mnemonic that is not committed to git).
4. PREIMAGE hardcoded in source
src/main.rs:32
const PREIMAGE: &str = "65d4240a1fa11515f70e8e7f96d287f8583bb701858551bebdafdc78873a619f";The preimage is the HTLC secret. Committing it to git exposes it to anyone with repo read access, enabling a front-running claim against the VHTLC before the legitimate script path fires. The preimage must be generated at runtime and stored out-of-band (env var, encrypted file, etc.).
🟡 High
5. sign_schnorr_no_aux_rand — deterministic signing without entropy
src/main.rs:289
let sig = secp.sign_schnorr_no_aux_rand(&msg, &user_keypair);BIP-340 strongly recommends auxiliary randomness to prevent fault-injection attacks. sign_schnorr (with aux rand) should be used here instead.
6. Unpinned git deps — non-reproducible, silently breaks on SDK churn
Cargo.toml:17-19
ark-core = { git = "https://github.com/arkade-os/rust-sdk" }
ark-delegator = { git = "https://github.com/arkade-os/rust-sdk" }
ark-rest = { git = "https://github.com/arkade-os/rust-sdk" }No rev = or tag = pin. A future SDK commit that alters signing, VTXO construction, or VHTLC options will silently change protocol behaviour for anyone who rebuilds the demo. Pin to a specific commit hash.
🟡 Medium
7. Script-byte length checks use magic numbers, not semantic validation
src/main.rs:252-261
if hex::decode(LOCKUP_CLAIM_LEAF_SCRIPT)?.len() != 61 { … }
if hex::decode(LOCKUP_REFUND_LEAF_SCRIPT)?.len() != 39 { … }A malformed script of the correct byte length passes silently. The proper guard is to reconstruct the lockup address and compare it against swap.lockup_details.lockup_address (the same check done in the new-swap path). That already happens for is_new_swap = true; do it here too instead of the length heuristic.
8. No check that input_total meets dust before sweep
src/main.rs:384-386
let receivers = vec![SendReceiver::bitcoin(user_address.clone(), Amount::from_sat(input_total))];input_total > 0 is asserted above, but a 1-sat VTXO would be below the standard dust threshold and the transaction will be rejected downstream. Add a minimum-amount guard (or let build_offchain_transactions return a useful error that is surfaced here).
9. set_condition_witness injected into checkpoint inputs as well as ark inputs
src/main.rs:275-291 + signing loop at src/main.rs:490-496 and src/main.rs:501-504
The same sign_fn (which calls set_condition_witness) is passed to both sign_ark_transaction and sign_checkpoint_transaction. The SDK reference (ark-client/src/boltz.rs:1768-1785) uses a dedicated inject_preimage_into_psbt call separately from checkpoint signing. Injecting the preimage into checkpoint inputs is likely harmless (operator ignores unknown PSBT fields) but it's inconsistent with the SDK's own pattern and should be made explicit.
🔵 Low / style
10. anyhow::bail! as user-facing setup instructions
src/main.rs:376-396
Using bail! to tell the user to fill in constants exits with code 1 and writes to stderr. It reads as a crash. A println! followed by return Ok(()) would communicate the expected workflow without looking like an error.
11. Variable naming: mainnet_address / MAINNET_ADDRESS is mutinynet
src/main.rs:36,198-202
The operator is mutinynet, not Bitcoin mainnet. The naming is misleading; refund_address or l1_address is clearer.
12. LOCKUP_CLAIM_LEAF_SCRIPT length (61 bytes) will change when script template changes
src/main.rs:36
The current script is for a specific Boltz template version. Document the script template version this was derived from, so a future Boltz upgrade doesn't silently pass the length check with a different encoding.
13. No tests — Danger flag is correct
There are zero tests for the claim-address reconstruction, the preimage hash derivation path, or the signing pipeline. Given that this code will be used as a reference implementation by downstream integrators, at least one integration test against a regtest Boltz instance is expected.
Summary
| # | Severity | File:Line | Issue |
|---|---|---|---|
| 1 | 🔴 fund-loss | main.rs:302-307 |
Resume path sends random preimage hash → unclaimable VHTLC |
| 2 | 🔴 fund-loss | main.rs:33 |
REFUND_LOCKTIME expired (2026-07-13); resume path bails immediately |
| 3 | 🔴 security | main.rs:41 |
Known-public "abandon" mnemonic; all derived keys publicly sweepable |
| 4 | 🔴 security | main.rs:32 |
Preimage hardcoded in git; front-run vector |
| 5 | 🟡 high | main.rs:289 |
sign_schnorr_no_aux_rand — BIP-340 entropy skipped |
| 6 | 🟡 high | Cargo.toml:17-19 |
SDK git deps unpinned; non-reproducible builds |
| 7 | 🟡 medium | main.rs:252-261 |
Script length magic-number guards instead of address reconstruction |
| 8 | 🟡 medium | main.rs:384-386 |
No dust check on sweep amount |
| 9 | 🟡 medium | main.rs:275-504 |
Preimage injected into checkpoint inputs via shared sign_fn |
| 10–13 | 🔵 low | various | bail! as UX, misleading names, hardcoded length, no tests |
Resolve items 1–4 before requesting re-review; 5–9 should be addressed in the same pass.
Addresses #2
Pending confirmation of swap logic from boltz
Summary by CodeRabbit