Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
venv/
.venv/
allways-venv/
.env
*.env
Expand Down
6 changes: 5 additions & 1 deletion allways/cli/swap_commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
from allways.cli.swap_commands.miner_commands import miner_group
from allways.cli.swap_commands.pair import post_pair
from allways.cli.swap_commands.post_tx import post_tx_command
from allways.cli.swap_commands.quote import quote_command
from allways.cli.swap_commands.resume import resume_command
from allways.cli.swap_commands.status import status_command
from allways.cli.swap_commands.swap import swap_group
from allways.cli.swap_commands.view import view_group

# Register post under the miner group
miner_group.add_command(post_pair, 'post')

# Register post-tx under the swap group
# Register post-tx, quote, resume under the swap group
swap_group.add_command(post_tx_command, 'post-tx')
swap_group.add_command(quote_command, 'quote')
swap_group.add_command(resume_command, 'resume')


def register_commands(cli):
Expand Down
31 changes: 30 additions & 1 deletion allways/cli/swap_commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import bittensor as bt
from rich.console import Console

from allways.classes import SwapStatus
from allways.classes import MinerPair, SwapStatus
from allways.commitments import parse_commitment_data, read_miner_commitment, read_miner_commitments # noqa: F401
from allways.constants import CONTRACT_ADDRESS as DEFAULT_CONTRACT_ADDRESS
from allways.constants import NETUID_FINNEY, TAO_TO_RAO
Expand Down Expand Up @@ -207,3 +207,32 @@ def load_pending_swap() -> Optional[PendingSwapState]:
def clear_pending_swap() -> None:
"""Remove the pending swap state file."""
PENDING_SWAP_FILE.unlink(missing_ok=True)


def find_matching_miners(all_pairs, source_chain: str, dest_chain: str):
"""Filter and normalize miner pairs for a given swap direction (bilateral matching).

Handles both direct matches and reverse-direction pairs (using counter_rate for the
reverse direction). Returns list of MinerPair with source/dest matching the requested direction.
"""
matching = []
for p in all_pairs:
if p.source_chain == source_chain and p.dest_chain == dest_chain:
if p.rate > 0:
matching.append(p)
elif p.source_chain == dest_chain and p.dest_chain == source_chain:
rev_rate, rev_rate_str = p.get_rate_for_direction(source_chain)
if rev_rate > 0:
matching.append(
MinerPair(
uid=p.uid,
hotkey=p.hotkey,
source_chain=p.dest_chain,
source_address=p.dest_address,
dest_chain=p.source_chain,
dest_address=p.source_address,
rate=rev_rate,
rate_str=rev_rate_str,
)
)
return matching
123 changes: 123 additions & 0 deletions allways/cli/swap_commands/quote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""alw swap quote - Preview rates and estimated receive amounts before swapping."""

from decimal import Decimal

import rich_click as click
from rich.table import Table

from allways.chains import SUPPORTED_CHAINS, canonical_pair, get_chain
from allways.cli.swap_commands.helpers import (
console,
find_matching_miners,
from_rao,
get_cli_context,
loading,
read_miner_commitments,
)
from allways.constants import DEFAULT_FEE_DIVISOR
from allways.contract_client import ContractError
from allways.utils.rate import apply_fee_deduction, calculate_dest_amount


@click.command('quote')
@click.option('--from', 'source_chain', required=True, type=str, help='Source chain (e.g. btc, tao)')
@click.option('--to', 'dest_chain', required=True, type=str, help='Destination chain (e.g. btc, tao)')
@click.option('--amount', required=True, type=float, help='Amount to send in source chain units')
def quote_command(source_chain: str, dest_chain: str, amount: float):
"""Preview rates and estimated receive amounts for a swap.

\b
Shows all available miners, their rates, and what you would receive
after fees — without committing to a swap.

\b
Examples:
alw swap quote --from btc --to tao --amount 0.1
alw swap quote --from tao --to btc --amount 50
"""
source_chain = source_chain.lower()
dest_chain = dest_chain.lower()

if source_chain not in SUPPORTED_CHAINS:
console.print(f'[red]Unknown source chain: {source_chain}[/red]')
return
if dest_chain not in SUPPORTED_CHAINS:
console.print(f'[red]Unknown destination chain: {dest_chain}[/red]')
return
if source_chain == dest_chain:
console.print('[red]Source and destination chains must be different[/red]')
return
if amount <= 0:
console.print('[red]Amount must be positive[/red]')
return

config, _, subtensor, client = get_cli_context(need_wallet=False)
netuid = config['netuid']

# Convert to smallest units
src_chain_def = get_chain(source_chain)
source_amount = int(Decimal(str(amount)) * (10**src_chain_def.decimals))

# Read fee divisor
try:
fee_divisor = client.get_fee_divisor() or DEFAULT_FEE_DIVISOR
except ContractError:
fee_divisor = DEFAULT_FEE_DIVISOR

fee_pct = 100 / fee_divisor

# Find available miners
with loading('Reading rates...'):
all_pairs = read_miner_commitments(subtensor, netuid)
matching = find_matching_miners(all_pairs, source_chain, dest_chain)

available = []
for pair in matching:
try:
is_active = client.get_miner_active_flag(pair.hotkey)
has_swap = client.get_miner_has_active_swap(pair.hotkey)
collateral = client.get_miner_collateral(pair.hotkey)
if is_active and not has_swap and collateral > 0:
available.append((pair, collateral))
except ContractError:
continue

if not available:
console.print('[yellow]No active miners available for this pair[/yellow]\n')
return

available.sort(key=lambda x: x[0].rate, reverse=True)

# Calculate amounts per miner
canon_src, canon_dest = canonical_pair(source_chain, dest_chain)
is_reverse = source_chain != canon_src
canon_dest_decimals = get_chain(canon_dest).decimals
canon_src_decimals = get_chain(canon_src).decimals
dst_chain_def = get_chain(dest_chain)

console.print(f'\n[bold]Quote: {amount} {source_chain.upper()} -> {dest_chain.upper()}[/bold]\n')

table = Table(show_header=True)
table.add_column('#', style='dim')
table.add_column('UID', style='cyan')
table.add_column(f'Rate ({dest_chain.upper()}/{source_chain.upper()})', style='green')
table.add_column('You Receive', style='bold green')
table.add_column('Collateral', style='yellow')

for idx, (pair, collateral) in enumerate(available, 1):
dest_amount = calculate_dest_amount(
source_amount, pair.rate_str, is_reverse, canon_dest_decimals, canon_src_decimals
)
user_receives = apply_fee_deduction(dest_amount, fee_divisor)
human_receives = user_receives / (10**dst_chain_def.decimals)

table.add_row(
str(idx),
str(pair.uid),
f'{pair.rate:g}',
f'{human_receives:.8f} {dest_chain.upper()}',
f'{from_rao(collateral):.4f} TAO',
)

console.print(table)
console.print(f' [dim](after {fee_pct:g}% fee)[/dim]\n')
Loading
Loading