Skip to content
Open
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
18 changes: 15 additions & 3 deletions allways/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,28 @@ class ReservationStatus(IntEnum):

@dataclass
class MinerPair:
"""A miner's posted exchange pair from on-chain commitments."""
"""A miner's posted exchange pair from on-chain commitments.

After normalization, source_chain is always non-TAO and dest_chain is always TAO.
rate/rate_str is the non-TAO->TAO rate, rate_reverse/rate_reverse_str is TAO->non-TAO.
"""

uid: int
hotkey: str
source_chain: str
source_address: str
dest_chain: str
dest_address: str
rate: float # TAO per 1 non-TAO asset — for display/sorting
rate_str: str = '' # Raw string from commitment — used for precise dest_amount calculation
rate: float # non-TAO->TAO rate (TAO per 1 non-TAO asset) — for display/sorting
rate_str: str = '' # Raw string — for precise dest_amount calculation
rate_reverse: float = 0.0 # TAO->non-TAO rate (TAO per 1 non-TAO asset)
rate_reverse_str: str = '' # Raw string — for precise dest_amount calculation

def get_rate_for_direction(self, source_is_tao: bool) -> tuple:
"""Return (rate, rate_str) for the given swap direction."""
if source_is_tao:
return self.rate_reverse, self.rate_reverse_str
return self.rate, self.rate_str


@dataclass
Expand Down
8 changes: 7 additions & 1 deletion allways/cli/swap_commands/miner_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ def miner_status(hotkey: str):

if pair:
console.print('[bold]Committed Pair[/bold]\n')
console.print(f' {pair.source_chain.upper()}/{pair.dest_chain.upper()} @ [green]{pair.rate:g}[/green]')
non_tao = pair.source_chain.upper()
if pair.rate_reverse_str and pair.rate != pair.rate_reverse:
console.print(f' {non_tao}/TAO')
console.print(f' {non_tao} -> TAO: [green]{pair.rate:g}[/green]')
console.print(f' TAO -> {non_tao}: [green]{pair.rate_reverse:g}[/green]')
else:
console.print(f' {non_tao}/TAO @ [green]{pair.rate:g}[/green]')
console.print(f' Source address: [dim]{pair.source_address}[/dim]')
console.print(f' Dest address: [dim]{pair.dest_address}[/dim]')
else:
Expand Down
70 changes: 50 additions & 20 deletions allways/cli/swap_commands/pair.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@ def _prompt_chain(label: str, exclude: str | None = None) -> str:
console.print(f'[red]Invalid: {reason}. Choose from: {choices}[/red]')


def _prompt_rate() -> float:
"""Prompt for a positive rate."""
def _prompt_rates(non_tao_ticker: str) -> tuple:
"""Prompt for direction-specific rates. Second defaults to first."""
while True:
value = click.prompt('Rate (TAO per 1 non-TAO asset)', type=float)
if value > 0:
return value
fwd = click.prompt(f'Rate for {non_tao_ticker} -> TAO (TAO per 1 {non_tao_ticker})', type=float)
if fwd > 0:
break
console.print('[red]Rate must be positive[/red]')
rev = click.prompt(f'Rate for TAO -> {non_tao_ticker} (TAO per 1 {non_tao_ticker})', type=float, default=fwd)
if rev <= 0:
console.print('[red]Rate must be positive, using forward rate[/red]')
rev = fwd
return fwd, rev


@click.command('pair')
Expand All @@ -34,13 +39,15 @@ def _prompt_rate() -> float:
@click.argument('dst_chain', required=False, default=None, type=str)
@click.argument('dst_addr', required=False, default=None, type=str)
@click.argument('rate', required=False, default=None, type=float)
@click.argument('rate_reverse', required=False, default=None, type=float)
@click.option('--yes', '-y', is_flag=True, help='Skip confirmation prompt')
def post_pair(
src_chain: str | None,
src_addr: str | None,
dst_chain: str | None,
dst_addr: str | None,
rate: float | None,
rate_reverse: float | None,
yes: bool,
):
"""Post a trading pair to chain via commitment.
Expand All @@ -50,16 +57,18 @@ def post_pair(

\b
Arguments:
SRC_CHAIN Source chain ID (e.g. btc, tao)
SRC_ADDR Your receiving address on source chain
DST_CHAIN Destination chain ID (e.g. tao, btc)
DST_ADDR Your sending address on destination chain
RATE TAO per 1 non-TAO asset (e.g. 345 means 1 BTC = 345 TAO)
SRC_CHAIN Source chain ID (e.g. btc, tao)
SRC_ADDR Your receiving address on source chain
DST_CHAIN Destination chain ID (e.g. tao, btc)
DST_ADDR Your sending address on destination chain
RATE non-TAO->TAO rate (TAO per 1 non-TAO asset)
RATE_REVERSE TAO->non-TAO rate (optional, defaults to RATE)

\b
Examples:
alw miner post (interactive wizard)
alw miner post btc bc1q...abc tao 5Cxyz...def 345 (all at once)
alw miner post (interactive wizard)
alw miner post btc bc1q...abc tao 5Cxyz...def 340 350 (direction-specific rates)
alw miner post btc bc1q...abc tao 5Cxyz...def 345 (same rate both ways)
"""
# --- Prompt for any missing arguments ---
if src_chain is None:
Expand Down Expand Up @@ -89,14 +98,24 @@ def post_pair(
if dst_addr is None:
dst_addr = click.prompt(f'Your sending address on {SUPPORTED_CHAINS[dst_chain].name}')

non_tao = src_chain if src_chain != 'tao' else dst_chain
non_tao_ticker = non_tao.upper()

if rate is None:
rate = _prompt_rate()
rate, rate_reverse = _prompt_rates(non_tao_ticker)
elif rate <= 0:
console.print('[red]Rate must be positive[/red]')
return
else:
if rate_reverse is None:
rate_reverse = rate
elif rate_reverse <= 0:
console.print('[red]Rate must be positive[/red]')
return

# Normalize to canonical direction: non-TAO → TAO.
# Rate is always "TAO per 1 non-TAO asset" regardless of direction.
# Rates are NOT swapped — prompts and help text already define them in canonical order
# (RATE = non-TAO->TAO, RATE_REVERSE = TAO->non-TAO).
if src_chain == 'tao' and dst_chain != 'tao':
console.print('[dim]Normalizing pair direction to canonical form (non-TAO -> TAO).[/dim]')
src_chain, dst_chain = dst_chain, src_chain
Expand All @@ -106,15 +125,27 @@ def post_pair(
netuid = config['netuid']

rate_str = f'{rate:g}'
commitment_data = f'v{COMMITMENT_VERSION}:{src_chain}:{src_addr}:{dst_chain}:{dst_addr}:{rate_str}'

non_tao = src_chain if src_chain != 'tao' else dst_chain
non_tao_ticker = non_tao.upper()
rate_reverse_str = f'{rate_reverse:g}'
commitment_data = (
f'v{COMMITMENT_VERSION}:{src_chain}:{src_addr}:{dst_chain}:{dst_addr}:{rate_str}:{rate_reverse_str}'
)

data_bytes = commitment_data.encode('utf-8')
if len(data_bytes) > 128:
console.print(
f'[red]Commitment too long ({len(data_bytes)} bytes, max 128). '
f'Try a shorter address format (e.g. P2WPKH instead of P2TR).[/red]'
)
return

console.print('\n[bold]Posting trading pair commitment[/bold]\n')
console.print(f' Source: [cyan]{SUPPORTED_CHAINS[src_chain].name}[/cyan] ({src_addr})')
console.print(f' Destination: [cyan]{SUPPORTED_CHAINS[dst_chain].name}[/cyan] ({dst_addr})')
console.print(f' Rate: [green]1 {non_tao_ticker} = {rate:g} TAO[/green]')
if rate == rate_reverse:
console.print(f' Rate: [green]1 {non_tao_ticker} = {rate:g} TAO (both directions)[/green]')
else:
console.print(f' Rate ({non_tao_ticker}->TAO): [green]1 {non_tao_ticker} = {rate:g} TAO[/green]')
console.print(f' Rate (TAO->{non_tao_ticker}): [green]1 {non_tao_ticker} = {rate_reverse:g} TAO[/green]')
console.print(f' Netuid: {netuid}')
console.print(f' Data: [dim]{commitment_data}[/dim]\n')

Expand All @@ -123,7 +154,6 @@ def post_pair(
return

try:
data_bytes = commitment_data.encode('utf-8')
with loading('Submitting commitment...'):
call = subtensor.substrate.compose_call(
call_module='Commitments',
Expand Down
5 changes: 3 additions & 2 deletions allways/cli/swap_commands/swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ def swap_now_command(
if p.source_chain == source_chain and p.dest_chain == dest_chain:
matching_pairs.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_is_tao=True)
matching_pairs.append(
MinerPair(
uid=p.uid,
Expand All @@ -548,8 +549,8 @@ def swap_now_command(
source_address=p.dest_address,
dest_chain=p.source_chain,
dest_address=p.source_address,
rate=p.rate,
rate_str=p.rate_str,
rate=rev_rate,
rate_str=rev_rate_str,
)
)

Expand Down
14 changes: 10 additions & 4 deletions allways/cli/swap_commands/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def view_miners():
table = Table(show_header=True)
table.add_column('UID', style='cyan')
table.add_column('Pair', style='green')
table.add_column('Rate (TAO)', style='yellow')
table.add_column('Rate (TAO/1 non-TAO)', style='yellow')
table.add_column('Collateral (TAO)', style='magenta')
table.add_column('Active', style='bold')
table.add_column(f'{pairs[0].source_chain.upper()} Addr', style='dim')
Expand All @@ -73,10 +73,14 @@ def view_miners():
pair_str = f'{pair.source_chain.upper()} <-> {pair.dest_chain.upper()}'
active_str = '[green]Yes[/green]' if is_active else '[red]No[/red]'

if pair.rate_reverse_str and pair.rate != pair.rate_reverse:
rate_display = f'{pair.rate:g} / {pair.rate_reverse:g}'
else:
rate_display = f'{pair.rate:g}'
table.add_row(
str(pair.uid),
pair_str,
f'{pair.rate:g}',
rate_display,
f'{from_rao(collateral_rao):.4f}',
active_str,
pair.source_address[:16] + '...',
Expand Down Expand Up @@ -151,12 +155,14 @@ def view_rates(pair: str):

table = Table(show_header=True)
table.add_column('UID', style='cyan')
table.add_column('Rate (TAO)', style='green')
table.add_column(f'{src.upper()}->TAO', style='green')
table.add_column(f'TAO->{src.upper()}', style='green')
table.add_column('Hotkey', style='dim')

pair_list.sort(key=lambda x: x.rate, reverse=True)
for p in pair_list:
table.add_row(str(p.uid), f'{p.rate:g}', p.hotkey[:16] + '...')
rev = f'{p.rate_reverse:g}' if p.rate_reverse_str else f'{p.rate:g}'
table.add_row(str(p.uid), f'{p.rate:g}', rev, p.hotkey[:16] + '...')

console.print(table)

Expand Down
17 changes: 11 additions & 6 deletions allways/commitments.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
def parse_commitment_data(raw: str, uid: int = 0, hotkey: str = '') -> Optional[MinerPair]:
"""Parse a commitment string into a MinerPair.

Format: v{VERSION}:{src_chain}:{src_addr}:{dst_chain}:{dst_addr}:{rate}
Rate is TAO per 1 non-TAO asset (e.g. 345 means 1 BTC = 345 TAO).
Example: v2:btc:bc1q...:tao:5C...:345
Format: v{VERSION}:{src_chain}:{src_addr}:{dst_chain}:{dst_addr}:{rate}:{rate_reverse}
Both rates are TAO per 1 non-TAO asset. rate is for non-TAO->TAO, rate_reverse is for TAO->non-TAO.
Example: v3:btc:bc1q...:tao:5C...:340:350
"""
try:
parts = raw.split(':')
if len(parts) != 6:
if len(parts) != 7:
return None

version_str = parts[0]
Expand All @@ -35,6 +35,8 @@ def parse_commitment_data(raw: str, uid: int = 0, hotkey: str = '') -> Optional[
dst_addr = parts[4]
rate_str = parts[5]
rate = float(rate_str)
rate_reverse_str = parts[6]
rate_reverse = float(rate_reverse_str)

if src_chain not in SUPPORTED_CHAINS or dst_chain not in SUPPORTED_CHAINS:
return None
Expand All @@ -43,11 +45,12 @@ def parse_commitment_data(raw: str, uid: int = 0, hotkey: str = '') -> Optional[
return None

# Normalize to canonical direction: non-TAO → TAO.
# Rate is always "TAO per 1 non-TAO asset" regardless of posted direction,
# so swapping source/dest doesn't change rate interpretation.
# When swapping direction, swap rates too: the posted "forward" rate becomes "reverse".
if src_chain == 'tao' and dst_chain != 'tao':
src_chain, dst_chain = dst_chain, src_chain
src_addr, dst_addr = dst_addr, src_addr
rate, rate_reverse = rate_reverse, rate
rate_str, rate_reverse_str = rate_reverse_str, rate_str

return MinerPair(
uid=uid,
Expand All @@ -58,6 +61,8 @@ def parse_commitment_data(raw: str, uid: int = 0, hotkey: str = '') -> Optional[
dest_address=dst_addr,
rate=rate,
rate_str=rate_str,
rate_reverse=rate_reverse,
rate_reverse_str=rate_reverse_str,
)
except (ValueError, IndexError):
return None
Expand Down
2 changes: 1 addition & 1 deletion allways/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
VALIDATOR_POLL_INTERVAL_SECONDS = 12 # Match Bittensor block time for lowest latency

# ─── Commitment Format ────────────────────────────────────
COMMITMENT_VERSION = 2
COMMITMENT_VERSION = 3
COMMITMENT_REVEAL_BLOCKS = 360 # ~72 min at 12s/block

# ─── Unit Conversions ────────────────────────────────────
Expand Down
5 changes: 3 additions & 2 deletions allways/validator/axon_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ async def handle_swap_confirm(
miner_fulfillment_address = (
commitment.dest_address if swap_source_chain == commitment.source_chain else commitment.source_address
)
_, selected_rate_str = commitment.get_rate_for_direction(swap_source_chain == 'tao')

provider = validator.axon_chain_providers.get(swap_source_chain)
if provider is None:
Expand Down Expand Up @@ -428,7 +429,7 @@ async def handle_swap_confirm(
dest_amount=res_dest_amount,
miner_deposit_address=miner_deposit_address,
miner_dest_address=miner_fulfillment_address,
rate_str=commitment.rate_str,
rate_str=selected_rate_str,
reserved_until=reserved_until,
)
if validator.pending_confirms.enqueue(pending):
Expand Down Expand Up @@ -473,7 +474,7 @@ async def handle_swap_confirm(
dest_amount=res_dest_amount,
miner_source_address=miner_deposit_address,
miner_dest_address=miner_fulfillment_address,
rate=commitment.rate_str,
rate=selected_rate_str,
)
synapse.accepted = True
bt.logging.info(f'Voted to initiate swap for miner {miner}')
Expand Down
Loading
Loading