The Problem With Predictable Timing
You queue up some payments in Wasabi, start coinjoin, and wait. The wallet joins round after round, back to back, until your payments clear. Fast and efficient.
But there's a problem: chain analysts don't just look at transaction graphs. They look at timing.
If someone coinjoins continuously for 6 hours, then stops, that's a fingerprint. If payments always clear within minutes of each other, that's a pattern. Timing analysis can correlate activity across rounds, cluster likely-related transactions, and narrow down the anonymity set you thought you had.
The fix is simple: don't be predictable.
Randomized Round Timing
Instead of continuous coinjoin, these scripts introduce random delays between rounds. You configure an average frequency - say, "2 rounds per day" - and the script uses an exponential distribution to generate natural-looking intervals.
Most waits cluster around your average, but occasionally you get shorter or longer gaps. This is the same statistical pattern that models when humans naturally do things: check email, make purchases, send messages. It looks organic because it is organic.
│
│█████████████████████
│██████████████
│█████████
│██████
│████
│██
│█
└─────────────────────
2h 8h 16h 30h
(most) (tail)The exponential distribution creates this long tail - mostly reasonable waits, occasionally longer pauses that break up any pattern.
Two Scripts, Two Use Cases
wrcj.sh - Standalone privacy mixing
Run coinjoin rounds with random delays until you hit a target: a specific number of rounds, a privacy percentage, or just keep going until you hit Ctrl+C. Use this when you're mixing coins without pending payments.
$ ./wrcj.sh
=== Wasabi Intermittent CoinJoin ===
Available wallets:
[1] savings
Select wallet: 1
[14:32:01] Loading wallet savings...
Coinjoin frequency (average):
[1] 1/day [4] 1/week
[2] 2/day [5] 2/week
[3] 4/day [6] Custom
Select [1-6]: 2
[14:32:05] Avg 12h 0m between rounds
Stop condition:
[1] 1 round [4] 10 rounds
[2] 3 rounds [5] Until % private
[3] 5 rounds [6] Forever (Ctrl+C)
[7] Custom rounds
Select [1-7]: 5
Current privacy: 23%
Target privacy %: 80
[14:32:12] Target: 80% private (currently 23%)
=== Starting ===
[14:32:12] Starting coinjoin...
[14:32:12] Waiting for round...
[14:51:23] Round complete: 7a3b9c4d2e1f8a5b...
[14:51:23] Privacy: 34% (target: 80%)
[14:51:23] Next round in 9h 23m...wcj.sh - Payment runner with optional delays
Queue payments with wpay.sh, then run them through coinjoin. Choose continuous mode for speed, or intermittent mode for privacy. The script stops automatically when all payments clear.
$ ./wcj.sh
=== Wasabi CoinJoin ===
Wallets:
[1] spending
Select wallet: 1
[14:32:01] Loading wallet spending...
=== Wallet: spending ===
Pending payments:
100000 sats -> tb1qxxx...
50000 sats -> tb1qyyy...
Coinjoin mode:
[1] Continuous (fast, less private timing)
[2] Intermittent (random delays, better privacy)
Select [1-2]: 2
Coinjoin frequency (average):
[1] 1/day [4] 1/week
[2] 2/day [5] 2/week
[3] 4/day [6] Custom
Select [1-6]: 3
[14:32:10] Avg 6h 0m between rounds
=== Starting ===
[14:32:10] Starting coinjoin...
[14:32:10] Waiting for round...
[14:51:23] Round complete: 7a3b9c4d2e1f8a5b...
[14:51:23] Sent: 50000 sats -> tb1qyyy...
[14:51:23] 1 payment(s) remaining
[14:51:23] Next round in 4h 17m...The Tradeoff
Random delays mean slower completion. A payment that would clear in an hour with continuous coinjoin might take a day or more with intermittent timing.
But privacy isn't free. The question is whether your threat model includes timing analysis. If you're just breaking transaction graph links, continuous coinjoin is fine. If you're concerned about sophisticated analysts correlating your activity patterns, random delays are worth the wait.
Like Dalí's melting clocks, time becomes fluid - and that fluidity is your cover.
Setup
- Enable RPC in Wasabi's
Config.json:
"JsonRpcServerEnabled": true- Make scripts executable:
chmod +x wrcj.sh wcj.sh- Requirements:
curl,jq,bc
Appendix: Full Scripts
wrcj.sh
#!/usr/bin/env bash
set -uo pipefail
#===============================================================================
# Wasabi Intermittent CoinJoin
#===============================================================================
#
# PURPOSE:
# Automates Wasabi Wallet coinjoin with randomized delays between rounds.
# This reduces timing fingerprinting compared to continuous coinjoin.
#
# HOW IT WORKS:
# 1. Starts coinjoin via RPC
# 2. Watches Wasabi's log file for "Coinjoin TxId" (indicates successful round)
# 3. Stops coinjoin
# 4. Sleeps for a random duration (exponential distribution around configured average)
# 5. Repeats until target is reached (round count or privacy percentage)
#
# ROUND DETECTION:
# Monitors ~/.walletwasabi/client/Logs.txt for new "Coinjoin TxId" entries.
# This is the definitive signal that a coinjoin transaction was broadcast.
# More reliable than UTXO tracking (which would false-trigger on regular payments).
#
# PRIVACY CALCULATION:
# Wasabi uses "anonymity score weighted amounts" for privacy progress.
# Each UTXO contributes proportionally: min(score, target) / target * amount
# Score <= 1 is treated as 0% (non-private). Score >= target is 100%.
#
# REQUIREMENTS:
# - Wasabi Wallet running with RPC enabled (JsonRpcServerEnabled: true in Config.json)
# - curl, jq, bc
#
# USAGE:
# ./wasabi-intermittent-cj.sh
# (Interactive prompts for wallet, frequency, and target)
#
#===============================================================================
RPC=" http://127.0.0.1:37128"
LOG_FILE="$HOME/.walletwasabi/client/Logs.txt"
#-------------------------------------------------------------------------------
# Utilities
#-------------------------------------------------------------------------------
die() { echo "Error: $1" >&2; exit 1; }
log() { echo "[$(date '+%H:%M:%S')] $1"; }
require() { command -v "$1" &>/dev/null || die "$1 is required"; }
# JSON-RPC call with error handling
rpc() {
local endpoint=$1 method=$2; shift 2
local params="" result
[[ $# -gt 0 ]] && params="$*"
if [[ -z "$params" ]]; then
result=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"'"$method"'"}' "$endpoint")
else
result=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"'"$method"'","params":['"$params"']}' "$endpoint")
fi
local err
err=$(echo "$result" | jq -r '.error.message // empty')
[[ -n "$err" ]] && die "RPC error: $err"
echo "$result"
}
# Convert seconds to human-readable format (e.g., "2d 5h" or "3h 30m")
format_duration() {
local s=${1:-0} d h m
d=$((s/86400)) h=$(((s%86400)/3600)) m=$(((s%3600)/60))
if ((d > 0)); then echo "${d}d ${h}h"
elif ((h > 0)); then echo "${h}h ${m}m"
else echo "${m}m"
fi
}
#-------------------------------------------------------------------------------
# Privacy calculation
#-------------------------------------------------------------------------------
# Returns the weighted privacy percentage of the wallet.
#
# Wasabi uses "anonymity score weighted amounts" rather than a binary threshold.
# Each UTXO contributes proportionally based on how close its score is to target:
# - Score >= target: contributes 100% of its value
# - Score < target: contributes (score / target) of its value
# - Score <= 1: contributes 0% (considered non-private)
#
# Example with target=5:
# - 0.1 BTC at score 5 contributes 0.1 BTC (100%)
# - 0.1 BTC at score 3 contributes 0.06 BTC (60%)
# - 0.1 BTC at score 1 contributes 0 BTC (0%)
#
get_privacy_pct() {
local coins target total_sum weighted_sum
target=$(rpc "$RPC/$WALLET" "getwalletinfo" | jq '.result.anonScoreTarget')
coins=$(rpc "$RPC/$WALLET" "listunspentcoins" | jq '.result')
total_sum=$(echo "$coins" | jq '[.[].amount] | add // 0')
((total_sum == 0)) && { echo "0"; return; }
# Calculate weighted sum: each coin contributes min(score, target) / target of its amount
# Score <= 1 contributes 0 (non-private)
weighted_sum=$(echo "$coins" | jq --argjson t "$target" '
[.[] |
if .anonymityScore <= 1 then 0
elif .anonymityScore >= $t then .amount
else .amount * .anonymityScore / $t
end
] | add // 0
')
# Calculate percentage using bc for floating point
echo "$weighted_sum * 100 / $total_sum" | bc -l | cut -d. -f1
}
#-------------------------------------------------------------------------------
# Interactive configuration
#-------------------------------------------------------------------------------
select_wallet() {
local wallets wallet_count
wallets=$(rpc "$RPC" "listwallets" | jq -r '.result')
wallet_count=$(echo "$wallets" | jq 'length')
((wallet_count == 0)) && die "No wallets found"
echo -e "\nAvailable wallets:"
for ((i=0; i<wallet_count; i++)); do
echo " [$((i+1))] $(echo "$wallets" | jq -r ".[$i].walletName")"
done
read -rp $'\nSelect wallet: ' choice
local idx=$((choice - 1))
((idx < 0 || idx >= wallet_count)) && die "Invalid selection"
WALLET=$(echo "$wallets" | jq -r ".[$idx].walletName")
log "Loading wallet $WALLET..."
local result err
result=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"loadwallet","params":["'"$WALLET"'"]}' "$RPC") || true
err=$(echo "$result" | jq -r '.error.message // empty')
[[ -n "$err" && "$err" != *"already"* ]] && die "Failed to load wallet: $err"
}
select_frequency() {
cat <<EOF
Coinjoin frequency (average):
[1] 1/day [4] 1/week
[2] 2/day [5] 2/week
[3] 4/day [6] Custom
EOF
read -rp "Select [1-6]: " choice
case $choice in
1) AVG_WAIT=$((24*3600)) ;;
2) AVG_WAIT=$((12*3600)) ;;
3) AVG_WAIT=$((6*3600)) ;;
4) AVG_WAIT=$((168*3600)) ;;
5) AVG_WAIT=$((84*3600)) ;;
6)
read -rp "Frequency (e.g. 3/day, 5/week): " custom
custom=${custom,,}
if [[ "$custom" =~ ^([0-9]+)/day$ ]]; then
((BASH_REMATCH[1] > 0)) || die "Invalid frequency"
AVG_WAIT=$((24*3600 / BASH_REMATCH[1]))
elif [[ "$custom" =~ ^([0-9]+)/week$ ]]; then
((BASH_REMATCH[1] > 0)) || die "Invalid frequency"
AVG_WAIT=$((168*3600 / BASH_REMATCH[1]))
else
die "Could not parse '$custom'"
fi
;;
*) die "Invalid selection" ;;
esac
# Minimum 2 hours (coinjoin rounds take time)
((AVG_WAIT < 7200)) && AVG_WAIT=7200
log "Avg $(format_duration $AVG_WAIT) between rounds"
}
select_target() {
local current_pct
current_pct=$(get_privacy_pct)
cat <<EOF
Stop condition:
[1] 1 round [4] 10 rounds
[2] 3 rounds [5] Until % private
[3] 5 rounds [6] Forever (Ctrl+C)
[7] Custom rounds
EOF
read -rp "Select [1-7]: " choice
# MODE: "rounds" or "privacy"
case $choice in
1) MODE="rounds"; MAX_ROUNDS=1 ;;
2) MODE="rounds"; MAX_ROUNDS=3 ;;
3) MODE="rounds"; MAX_ROUNDS=5 ;;
4) MODE="rounds"; MAX_ROUNDS=10 ;;
5)
MODE="privacy"
echo "Current privacy: ${current_pct}%"
read -rp "Target privacy %: " TARGET_PCT
[[ "$TARGET_PCT" =~ ^[0-9]+$ ]] || die "Invalid percentage"
((TARGET_PCT > 100)) && TARGET_PCT=100
((TARGET_PCT <= current_pct)) && { log "Already at ${current_pct}%!"; exit 0; }
;;
6) MODE="rounds"; MAX_ROUNDS=0 ;;
7)
MODE="rounds"
read -rp "Number of rounds (0=forever): " MAX_ROUNDS
[[ "$MAX_ROUNDS" =~ ^[0-9]+$ ]] || die "Invalid number"
;;
*) die "Invalid selection" ;;
esac
if [[ "$MODE" == "privacy" ]]; then
log "Target: ${TARGET_PCT}% private (currently ${current_pct}%)"
elif ((MAX_ROUNDS == 0)); then
log "Running until Ctrl+C"
else
log "Target: $MAX_ROUNDS round(s)"
fi
}
#-------------------------------------------------------------------------------
# Core logic
#-------------------------------------------------------------------------------
# Generates random wait time using exponential distribution.
# This creates natural-looking intervals: mostly near the average,
# occasionally shorter or longer, mimicking human behavior.
random_wait() {
local r wait max
r=$(awk 'BEGIN{srand(); r=rand(); if(r<0.001)r=0.001; print r}')
wait=$(echo "-$AVG_WAIT * l($r)" | bc -l)
wait=${wait%%.*} # Remove decimal part
wait=${wait:-0} # Default to 0 if empty
# Clamp to [2 hours, min(2.5x average, 3 days)]
max=$((AVG_WAIT * 5 / 2))
((max > 259200)) && max=259200
((wait > max)) && wait=$max
((wait < 7200)) && wait=7200
echo "$wait"
}
# Watches log file for "Coinjoin TxId" indicating a successful round.
# Only reads new content appended since function start.
wait_for_round() {
local start_pos poll=0
start_pos=$(wc -c < "$LOG_FILE")
log "Waiting for round..."
while true; do
sleep 30
((++poll))
local current_pos
current_pos=$(wc -c < "$LOG_FILE")
if ((current_pos > start_pos)); then
if tail -c +"$((start_pos + 1))" "$LOG_FILE" | grep -qi "Coinjoin TxId"; then
local txid
txid=$(tail -c +"$((start_pos + 1))" "$LOG_FILE" | grep -oi "Coinjoin TxId.*" | grep -oE '[a-f0-9]{64}' | tail -1 || true)
[[ -n "$txid" ]] && log "Round complete: ${txid:0:16}..." || log "Round complete!"
return
fi
fi
# Progress update every 5 minutes
((poll % 10 == 0)) && log "Still waiting... (~$((poll/2))m)"
done
}
# Sleep with periodic progress updates
do_sleep() {
local remaining=$1
local interval=$((remaining > 7200 ? 3600 : 1800))
while ((remaining > 0)); do
local chunk=$((remaining > interval ? interval : remaining))
sleep "$chunk"
((remaining -= chunk))
((remaining > 0)) && log "$(format_duration $remaining) remaining..."
done
}
# Check if target is reached. Returns 0 if done, 1 if should continue.
check_target() {
if [[ "$MODE" == "privacy" ]]; then
local pct
pct=$(get_privacy_pct)
if ((pct >= TARGET_PCT)); then
log "Privacy target reached: ${pct}%"
return 0
fi
log "Privacy: ${pct}% (target: ${TARGET_PCT}%)"
else
if ((MAX_ROUNDS > 0)); then
log "Completed $ROUND_COUNT/$MAX_ROUNDS"
((ROUND_COUNT >= MAX_ROUNDS)) && return 0
else
log "Completed round #$ROUND_COUNT"
fi
fi
return 1
}
cleanup() {
echo
log "Stopping..."
[[ -n "$WALLET" ]] && curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"stopcoinjoin"}' "$RPC/$WALLET" &>/dev/null || true
((ROUND_COUNT > 0)) && log "Completed $ROUND_COUNT round(s)"
exit 0
}
#-------------------------------------------------------------------------------
# Main
#-------------------------------------------------------------------------------
main() {
require curl
require jq
require bc
# Initialize globals
WALLET="" AVG_WAIT=0 MAX_ROUNDS=0 ROUND_COUNT=0 MODE="rounds" TARGET_PCT=0
echo "=== Wasabi Intermittent CoinJoin ==="
# Verify RPC is accessible
curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"getstatus"}' "$RPC" &>/dev/null \
|| die "Cannot connect to Wasabi RPC at $RPC"
# Verify log file exists
[[ -f "$LOG_FILE" ]] || die "Log file not found: $LOG_FILE"
select_wallet
select_frequency
select_target
trap cleanup SIGINT SIGTERM
echo -e "\n=== Starting ===\n"
while true; do
log "Starting coinjoin..."
curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"startcoinjoin","params":["","false","true"]}' "$RPC/$WALLET" &>/dev/null \
|| die "Failed to start coinjoin"
wait_for_round
curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"stopcoinjoin"}' "$RPC/$WALLET" &>/dev/null || true
((++ROUND_COUNT))
check_target && { log "Done!"; exit 0; }
local wait
wait=$(random_wait)
log "Next round in $(format_duration "$wait")..."
do_sleep "$wait"
echo
done
}
main "$@"wcj.sh
#!/usr/bin/env bash
set -uo pipefail
#===============================================================================
# Wasabi CoinJoin Payment Runner
#===============================================================================
#
# PURPOSE:
# Runs coinjoin until all queued payments complete. Supports two modes:
# - Continuous: coinjoin runs non-stop (faster, but predictable timing)
# - Intermittent: random delays between rounds (slower, better privacy)
#
# PAYMENT DETECTION:
# Polls listpaymentsincoinjoin every 30 seconds to detect completed/added payments.
#
# ROUND DETECTION (intermittent mode):
# Watches ~/.walletwasabi/client/Logs.txt for "Coinjoin TxId" entries.
#
# REQUIREMENTS:
# - Wasabi Wallet with RPC enabled (JsonRpcServerEnabled: true)
# - curl, jq, bc (bc only needed for intermittent mode)
#
#===============================================================================
RPC=" http://127.0.0.1:37128"
LOG_FILE="$HOME/.walletwasabi/client/Logs.txt"
# Globals (initialized in main)
WALLET=""
MODE=""
AVG_WAIT=0
EVER_HAD_PAYMENTS=false
#-------------------------------------------------------------------------------
# Utilities
#-------------------------------------------------------------------------------
die() { echo "Error: $1" >&2; exit 1; }
log() { echo "[$(date '+%H:%M:%S')] $1"; }
rpc() {
local method=$1; shift
local params="" result
[[ $# -gt 0 ]] && params="$*"
if [[ -z "$params" ]]; then
result=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"'"$method"'"}' "$RPC/$WALLET")
else
result=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"'"$method"'","params":['"$params"']}' "$RPC/$WALLET")
fi
local err
err=$(echo "$result" | jq -r '.error.message // empty')
[[ -n "$err" ]] && die "RPC error: $err"
echo "$result"
}
# Convert seconds to human-readable format
format_duration() {
local s=${1:-0} d h m
d=$((s/86400)) h=$(((s%86400)/3600)) m=$(((s%3600)/60))
if ((d > 0)); then echo "${d}d ${h}h"
elif ((h > 0)); then echo "${h}h ${m}m"
else echo "${m}m"
fi
}
#-------------------------------------------------------------------------------
# Payment tracking
#-------------------------------------------------------------------------------
get_pending() {
curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"listpaymentsincoinjoin"}' "$RPC/$WALLET" \
| jq '[.result[] | select(.state[0].status == "Pending")] | sort_by(.address)'
}
show_pending() {
local payments="$1"
local count
count=$(echo "$payments" | jq 'length')
if ((count == 0)); then
echo " (none)"
else
echo "$payments" | jq -r '.[] | " \(.amount) sats -> \(.address)"'
fi
}
# Compare payment lists, log completed and added payments
# Sets global PREV_ADDRS and PREV_PAYMENTS for next call
check_payments() {
local curr curr_addrs curr_count
curr=$(get_pending)
curr_count=$(echo "$curr" | jq 'length')
curr_addrs=$(echo "$curr" | jq -r '.[].address' | sort | tr '\n' ' ')
((curr_count > 0)) && EVER_HAD_PAYMENTS=true
# Check for completed payments
for addr in $PREV_ADDRS; do
if ! echo "$curr_addrs" | grep -q "$addr"; then
local amount
amount=$(echo "$PREV_PAYMENTS" | jq -r ".[] | select(.address == \"$addr\") | .amount")
log "Sent: $amount sats -> $addr"
fi
done
# Check for new payments
for addr in $curr_addrs; do
if [[ -n "$addr" ]] && ! echo "$PREV_ADDRS" | grep -q "$addr"; then
local amount
amount=$(echo "$curr" | jq -r ".[] | select(.address == \"$addr\") | .amount")
log "Added: $amount sats -> $addr"
EVER_HAD_PAYMENTS=true
fi
done
# Update state for next call
PREV_PAYMENTS="$curr"
PREV_ADDRS="$curr_addrs"
# Return: 0 if done, 1 if payments remain
if [[ "$EVER_HAD_PAYMENTS" == true ]] && ((curr_count == 0)); then
return 0
fi
return 1
}
#-------------------------------------------------------------------------------
# Interactive configuration
#-------------------------------------------------------------------------------
select_wallet() {
local wallets wallet_count
wallets=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"listwallets"}' "$RPC" | jq -r '.result')
wallet_count=$(echo "$wallets" | jq 'length')
((wallet_count == 0)) && die "No wallets found"
echo "Wallets:"
echo ""
for ((i=0; i<wallet_count; i++)); do
echo " [$((i+1))] $(echo "$wallets" | jq -r ".[$i].walletName")"
done
echo ""
read -rp "Select wallet: " choice
local idx=$((choice - 1))
((idx < 0 || idx >= wallet_count)) && die "Invalid selection"
WALLET=$(echo "$wallets" | jq -r ".[$idx].walletName")
echo ""
log "Loading wallet $WALLET..."
local result err
result=$(curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"loadwallet","params":["'"$WALLET"'"]}' "$RPC") || true
err=$(echo "$result" | jq -r '.error.message // empty')
[[ -n "$err" && "$err" != *"already"* ]] && die "Failed to load wallet: $err"
}
select_mode() {
cat <<EOF
Coinjoin mode:
[1] Continuous (fast, less private timing)
[2] Intermittent (random delays, better privacy)
EOF
read -rp "Select [1-2]: " choice
case $choice in
1) MODE="continuous" ;;
2) MODE="intermittent" ;;
*) die "Invalid selection" ;;
esac
}
select_frequency() {
cat <<EOF
Coinjoin frequency (average):
[1] 1/day [4] 1/week
[2] 2/day [5] 2/week
[3] 4/day [6] Custom
EOF
read -rp "Select [1-6]: " choice
case $choice in
1) AVG_WAIT=$((24*3600)) ;;
2) AVG_WAIT=$((12*3600)) ;;
3) AVG_WAIT=$((6*3600)) ;;
4) AVG_WAIT=$((168*3600)) ;;
5) AVG_WAIT=$((84*3600)) ;;
6)
read -rp "Frequency (e.g. 3/day, 5/week): " custom
custom=${custom,,}
if [[ "$custom" =~ ^([0-9]+)/day$ ]]; then
((BASH_REMATCH[1] > 0)) || die "Invalid frequency"
AVG_WAIT=$((24*3600 / BASH_REMATCH[1]))
elif [[ "$custom" =~ ^([0-9]+)/week$ ]]; then
((BASH_REMATCH[1] > 0)) || die "Invalid frequency"
AVG_WAIT=$((168*3600 / BASH_REMATCH[1]))
else
die "Could not parse '$custom'"
fi
;;
*) die "Invalid selection" ;;
esac
# Minimum 2 hours
((AVG_WAIT < 7200)) && AVG_WAIT=7200
log "Avg $(format_duration $AVG_WAIT) between rounds"
}
#-------------------------------------------------------------------------------
# Core logic
#-------------------------------------------------------------------------------
# Exponential distribution for natural-looking random intervals
random_wait() {
local r wait max
r=$(awk 'BEGIN{srand(); r=rand(); if(r<0.001)r=0.001; print r}')
wait=$(echo "-$AVG_WAIT * l($r)" | bc -l)
wait=${wait%%.*} # Remove decimal part
wait=${wait:-0} # Default to 0 if empty
# Clamp to [2 hours, min(2.5x average, 3 days)]
max=$((AVG_WAIT * 5 / 2))
((max > 259200)) && max=259200
((wait > max)) && wait=$max
((wait < 7200)) && wait=7200
echo "$wait"
}
# Watches log file for "Coinjoin TxId" indicating a successful round
wait_for_round() {
local start_pos poll=0
start_pos=$(wc -c < "$LOG_FILE")
log "Waiting for round..."
while true; do
sleep 30
((++poll))
local current_pos
current_pos=$(wc -c < "$LOG_FILE")
if ((current_pos > start_pos)); then
if tail -c +"$((start_pos + 1))" "$LOG_FILE" | grep -qi "Coinjoin TxId"; then
local txid
txid=$(tail -c +"$((start_pos + 1))" "$LOG_FILE" | grep -oi "Coinjoin TxId.*" | grep -oE '[a-f0-9]{64}' | tail -1 || true)
[[ -n "$txid" ]] && log "Round complete: ${txid:0:16}..." || log "Round complete!"
return
fi
fi
((poll % 10 == 0)) && log "Still waiting... (~$((poll/2))m)"
done
}
# Sleep with periodic progress updates, checking payments periodically
do_sleep() {
local remaining=$1
local interval=$((remaining > 7200 ? 3600 : 1800))
while ((remaining > 0)); do
local chunk=$((remaining > interval ? interval : remaining))
sleep "$chunk"
((remaining -= chunk))
((remaining > 0)) && log "$(format_duration $remaining) remaining..."
done
}
start_coinjoin() {
curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"startcoinjoin","params":["","false","true"]}' "$RPC/$WALLET" &>/dev/null \
|| die "Failed to start coinjoin"
}
stop_coinjoin() {
curl -sf -d '{"jsonrpc":"2.0","id":"1","method":"stopcoinjoin"}' "$RPC/$WALLET" &>/dev/null || true
}
cleanup() {
echo ""
log "Stopping..."
stop_coinjoin
log "CoinJoin stopped"
exit 0
}
#-------------------------------------------------------------------------------
# Main loops
#-------------------------------------------------------------------------------
run_continuous() {
log "Starting coinjoin..."
start_coinjoin
while true; do
sleep 30
check_payments && break
done
}
run_intermittent() {
[[ -f "$LOG_FILE" ]] || die "Log file not found: $LOG_FILE"
command -v bc &>/dev/null || die "bc is required for intermittent mode"
local first_round=true
while true; do
log "Starting coinjoin..."
start_coinjoin
wait_for_round
stop_coinjoin
# Check payments after each round
check_payments && break
local remaining
remaining=$(echo "$PREV_PAYMENTS" | jq 'length')
log "$remaining payment(s) remaining"
local wait
wait=$(random_wait)
log "Next round in $(format_duration "$wait")..."
do_sleep "$wait"
echo ""
done
}
main() {
# Check dependencies
command -v curl &>/dev/null || die "curl is required"
command -v jq &>/dev/null || die "jq is required"
echo "=== Wasabi CoinJoin ==="
# Check RPC
curl -sf --connect-timeout 3 -d '{"jsonrpc":"2.0","id":"1","method":"getstatus"}' "$RPC" &>/dev/null \
|| die "Cannot connect to Wasabi RPC at $RPC"
select_wallet
# Show pending payments
PREV_PAYMENTS=$(get_pending)
PREV_ADDRS=$(echo "$PREV_PAYMENTS" | jq -r '.[].address' | sort | tr '\n' ' ')
local count
count=$(echo "$PREV_PAYMENTS" | jq 'length')
echo ""
echo "=== Wallet: $WALLET ==="
echo ""
echo "Pending payments:"
show_pending "$PREV_PAYMENTS"
((count > 0)) && EVER_HAD_PAYMENTS=true
if ((count == 0)); then
echo ""
read -rp "No pending payments. Start coinjoin anyway? [y/N]: " confirm
[[ "${confirm^^}" != "Y" ]] && exit 0
fi
select_mode
[[ "$MODE" == "intermittent" ]] && select_frequency
trap cleanup SIGINT SIGTERM
echo ""
echo "=== Starting ==="
echo ""
if [[ "$MODE" == "continuous" ]]; then
run_continuous
else
run_intermittent
fi
echo ""
echo "=== All payments done ==="
stop_coinjoin
log "CoinJoin stopped"
}
main "$@"