diff --git a/allways/chains.py b/allways/chains.py index f60b6d5..f9cc1ca 100644 --- a/allways/chains.py +++ b/allways/chains.py @@ -48,6 +48,22 @@ def get_chain(chain_id: str) -> ChainDefinition: return SUPPORTED_CHAINS[chain_id] +def canonical_pair(chain_a: str, chain_b: str) -> tuple: + """Return (source, dest) in canonical order for consistent commitment storage. + + Determines the rate unit: rate is always 'dest per 1 source' in this ordering. + + Ordering rules: + 1. If TAO is in the pair, TAO is always dest — rates are denominated in TAO. + 2. Otherwise, alphabetical — deterministic fallback for non-TAO pairs (e.g. BTC-ETH). + """ + if chain_b == 'tao': + return (chain_a, chain_b) + if chain_a == 'tao': + return (chain_b, chain_a) + return (chain_a, chain_b) if chain_a < chain_b else (chain_b, chain_a) + + def confirmations_to_subtensor_blocks(chain_id: str) -> int: """How many subtensor blocks a chain's min_confirmations take.""" chain = get_chain(chain_id) diff --git a/allways/classes.py b/allways/classes.py index 6be2d1e..de66951 100644 --- a/allways/classes.py +++ b/allways/classes.py @@ -27,7 +27,12 @@ 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/dest_chain are in canonical order. + rate is for source→dest swaps, counter_rate is for dest→source swaps. + Both rates use the same unit: 'dest per 1 source' in canonical order. + """ uid: int hotkey: str @@ -35,8 +40,16 @@ class MinerPair: 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 # source→dest rate — for display/sorting + rate_str: str = '' # Raw string — for precise dest_amount calculation + counter_rate: float = 0.0 # dest→source rate (same unit as rate) + counter_rate_str: str = '' # Raw string — for precise dest_amount calculation + + def get_rate_for_direction(self, swap_source_chain: str) -> tuple: + """Return (rate, rate_str) for the given swap direction.""" + if swap_source_chain == self.source_chain: + return self.rate, self.rate_str + return self.counter_rate, self.counter_rate_str @dataclass diff --git a/allways/cli/swap_commands/miner_commands.py b/allways/cli/swap_commands/miner_commands.py index 06bc3b9..dbab7cf 100644 --- a/allways/cli/swap_commands/miner_commands.py +++ b/allways/cli/swap_commands/miner_commands.py @@ -80,7 +80,23 @@ 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]') + src_up, dst_up = pair.source_chain.upper(), pair.dest_chain.upper() + fwd_disabled = pair.rate == 0 + ctr_disabled = pair.counter_rate == 0 + if fwd_disabled or ctr_disabled or pair.rate_str != pair.counter_rate_str: + console.print(f' {src_up} ↔ {dst_up}') + if fwd_disabled: + console.print(f' {src_up} → {dst_up}: [yellow]not supported[/yellow]') + else: + console.print(f' {src_up} → {dst_up}: [green]send 1 {src_up}, get {pair.rate:g} {dst_up}[/green]') + if ctr_disabled: + console.print(f' {dst_up} → {src_up}: [yellow]not supported[/yellow]') + else: + console.print( + f' {dst_up} → {src_up}: [green]send {pair.counter_rate:g} {dst_up}, get 1 {src_up}[/green]' + ) + else: + console.print(f' {src_up} ↔ {dst_up} @ [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: diff --git a/allways/cli/swap_commands/pair.py b/allways/cli/swap_commands/pair.py index e26741f..4952602 100644 --- a/allways/cli/swap_commands/pair.py +++ b/allways/cli/swap_commands/pair.py @@ -2,7 +2,7 @@ import rich_click as click -from allways.chains import SUPPORTED_CHAINS +from allways.chains import SUPPORTED_CHAINS, canonical_pair from allways.cli.swap_commands.helpers import console, get_cli_context, loading from allways.constants import COMMITMENT_VERSION @@ -10,6 +10,9 @@ def _prompt_chain(label: str, exclude: str | None = None) -> str: """Prompt the user to pick a chain from SUPPORTED_CHAINS.""" chains = [c for c in SUPPORTED_CHAINS if c != exclude] + if len(chains) == 1: + console.print(f'{label}: [cyan]{chains[0]}[/cyan]') + return chains[0] choices = ', '.join(chains) while True: value = click.prompt(f'{label} ({choices})').strip().lower() @@ -19,13 +22,21 @@ 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(canon_src: str, canon_dest: str) -> tuple: + """Prompt for direction-specific rates. Counter rate defaults to forward; 0 = direction not supported.""" + src_up, dst_up = canon_src.upper(), canon_dest.upper() 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 {src_up} -> {dst_up} ({dst_up} per 1 {src_up})', type=float) + if fwd > 0: + break console.print('[red]Rate must be positive[/red]') + rev = click.prompt( + f'Rate for {dst_up} -> {src_up} ({dst_up} per 1 {src_up}, 0 = not supported)', type=float, default=fwd + ) + if rev < 0: + console.print('[red]Rate cannot be negative, using 0 (not supported)[/red]') + rev = 0.0 + return fwd, rev @click.command('pair') @@ -34,6 +45,7 @@ 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('counter_rate', required=False, default=None, type=float) @click.option('--yes', '-y', is_flag=True, help='Skip confirmation prompt') def post_pair( src_chain: str | None, @@ -41,6 +53,7 @@ def post_pair( dst_chain: str | None, dst_addr: str | None, rate: float | None, + counter_rate: float | None, yes: bool, ): """Post a trading pair to chain via commitment. @@ -50,20 +63,22 @@ 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 source→dest rate (e.g. TAO per 1 BTC for btc-tao pair) + COUNTER_RATE dest→source 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: - src_chain = _prompt_chain('Source chain') + src_chain = _prompt_chain('Source chain (you receive on this chain)') else: src_chain = src_chain.lower() if src_chain not in SUPPORTED_CHAINS: @@ -75,7 +90,7 @@ def post_pair( src_addr = click.prompt(f'Your receiving address on {SUPPORTED_CHAINS[src_chain].name}') if dst_chain is None: - dst_chain = _prompt_chain('Destination chain', exclude=src_chain) + dst_chain = _prompt_chain('Destination chain (you send on this chain)', exclude=src_chain) else: dst_chain = dst_chain.lower() if dst_chain not in SUPPORTED_CHAINS: @@ -89,32 +104,64 @@ def post_pair( if dst_addr is None: dst_addr = click.prompt(f'Your sending address on {SUPPORTED_CHAINS[dst_chain].name}') + canon_src, canon_dest = canonical_pair(src_chain, dst_chain) + rates_from_args = rate is not None + if rate is None: - rate = _prompt_rate() + rate, counter_rate = _prompt_rates(canon_src, canon_dest) elif rate <= 0: console.print('[red]Rate must be positive[/red]') return + else: + if counter_rate is None: + counter_rate = rate + elif counter_rate < 0: + console.print('[red]Rate cannot be negative[/red]') + return - # Normalize to canonical direction: non-TAO → TAO. - # Rate is always "TAO per 1 non-TAO asset" regardless of direction. - if src_chain == 'tao' and dst_chain != 'tao': - console.print('[dim]Normalizing pair direction to canonical form (non-TAO -> TAO).[/dim]') + # Normalize to canonical direction. + # Positional args: RATE = user's source→dest, so swap rates to match canonical order. + # Interactive prompts: already asked in canonical order, no rate swap needed. + if src_chain != canon_src: + console.print(f'[dim]Normalizing pair direction to canonical form ({canon_src} -> {canon_dest}).[/dim]') src_chain, dst_chain = dst_chain, src_chain src_addr, dst_addr = dst_addr, src_addr + if rates_from_args: + rate, counter_rate = counter_rate, rate config, wallet, subtensor, _ = get_cli_context(need_client=False) netuid = config['netuid'] rate_str = f'{rate:g}' - commitment_data = f'v{COMMITMENT_VERSION}:{src_chain}:{src_addr}:{dst_chain}:{dst_addr}:{rate_str}' + counter_rate_str = f'{counter_rate:g}' + commitment_data = ( + f'v{COMMITMENT_VERSION}:{src_chain}:{src_addr}:{dst_chain}:{dst_addr}:{rate_str}:{counter_rate_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 - non_tao = src_chain if src_chain != 'tao' else dst_chain - non_tao_ticker = non_tao.upper() + src_up, dst_up = src_chain.upper(), dst_chain.upper() 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 == counter_rate and rate > 0: + console.print(f' Rate: [green]send 1 {src_up}, get {rate:g} {dst_up} (both directions)[/green]') + else: + if rate > 0: + console.print(f' {src_up} → {dst_up}: [green]send 1 {src_up}, get {rate:g} {dst_up}[/green]') + else: + console.print(f' {src_up} → {dst_up}: [yellow]not supported[/yellow]') + if counter_rate > 0: + console.print(f' {dst_up} → {src_up}: [green]send {counter_rate:g} {dst_up}, get 1 {src_up}[/green]') + else: + console.print(f' {dst_up} → {src_up}: [yellow]not supported[/yellow]') console.print(f' Netuid: {netuid}') console.print(f' Data: [dim]{commitment_data}[/dim]\n') @@ -123,7 +170,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', diff --git a/allways/cli/swap_commands/status.py b/allways/cli/swap_commands/status.py index f39bf09..f7bac7b 100644 --- a/allways/cli/swap_commands/status.py +++ b/allways/cli/swap_commands/status.py @@ -107,7 +107,14 @@ def status_command(netuid: int): my_pairs = [p for p in pairs if p.hotkey == hotkey] if my_pairs: for p in my_pairs: - table.add_row('Miner Pair', f'{p.source_chain.upper()}/{p.dest_chain.upper()} @ {p.rate:g}') + src_up, dst_up = p.source_chain.upper(), p.dest_chain.upper() + if p.rate > 0 and p.counter_rate > 0 and p.rate_str != p.counter_rate_str: + rate_display = f'{src_up}→{dst_up}: {p.rate:g} | {dst_up}→{src_up}: {p.counter_rate:g}' + elif p.rate > 0: + rate_display = f'{p.rate:g}' + else: + rate_display = f'{p.counter_rate:g}' + table.add_row('Miner Pair', f'{src_up} ↔ {dst_up} @ {rate_display}') except ContractError: table.add_row('Miner Status', '[dim]unable to read[/dim]') diff --git a/allways/cli/swap_commands/swap.py b/allways/cli/swap_commands/swap.py index 88e3d53..8f4d350 100644 --- a/allways/cli/swap_commands/swap.py +++ b/allways/cli/swap_commands/swap.py @@ -10,7 +10,7 @@ from rich.table import Table from allways.chain_providers import create_chain_providers -from allways.chains import CHAIN_TAO, SUPPORTED_CHAINS, get_chain +from allways.chains import SUPPORTED_CHAINS, canonical_pair, get_chain from allways.classes import MinerPair, SwapStatus from allways.cli.dendrite_lite import broadcast_synapse, discover_validators, get_ephemeral_wallet from allways.cli.swap_commands.helpers import ( @@ -538,20 +538,23 @@ def swap_now_command( matching_pairs = [] for p in all_pairs: if p.source_chain == source_chain and p.dest_chain == dest_chain: - matching_pairs.append(p) + if p.rate > 0: + matching_pairs.append(p) elif p.source_chain == dest_chain and p.dest_chain == source_chain: - matching_pairs.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=p.rate, - rate_str=p.rate_str, + rev_rate, rev_rate_str = p.get_rate_for_direction(source_chain) + if rev_rate > 0: + matching_pairs.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, + ) ) - ) if not matching_pairs: console.print('[yellow]No miners found for this pair[/yellow]\n') @@ -588,9 +591,11 @@ def swap_now_command( console.print(table) # Step 3: Select miner (default to best rate) - non_tao = dest_chain if source_chain == 'tao' else source_chain + canon_src, canon_dest = canonical_pair(source_chain, dest_chain) best_pair = available_miners[0][0] - console.print(f'\n Best rate: 1 {non_tao.upper()} = {best_pair.rate:g} TAO (Miner UID {best_pair.uid})') + console.print( + f'\n Best rate: send 1 {source_chain.upper()}, get {best_pair.rate:g} {dest_chain.upper()} (Miner UID {best_pair.uid})' + ) if auto_select or len(available_miners) == 1: selected_pair, selected_collateral = available_miners[0] @@ -610,14 +615,13 @@ def swap_now_command( return source_amount = _to_smallest_unit(amount, source_chain) - source_is_tao = source_chain == 'tao' - asset_decimals = get_chain(non_tao).decimals + is_reverse = source_chain != canon_src dest_amount = calculate_dest_amount( source_amount, selected_pair.rate_str, - source_is_tao, - CHAIN_TAO.decimals, - asset_decimals, + is_reverse, + get_chain(canon_dest).decimals, + get_chain(canon_src).decimals, ) # Show estimated receive inline @@ -704,13 +708,12 @@ def swap_now_command( # Step 7: Confirm summary fee_in_dest = dest_amount - user_receives - non_tao_ticker = non_tao.upper() summary = ( f' Send: [red]{amount} {source_chain.upper()}[/red]\n' f' Receive: [green]{_from_smallest_unit(user_receives, dest_chain):.8f} {dest_chain.upper()}[/green]\n' f' Fee: {fee_percent:g}% ({_from_smallest_unit(fee_in_dest, dest_chain):.8f} {dest_chain.upper()})\n' - f' Rate: 1 {non_tao_ticker} = {selected_pair.rate:g} TAO\n' + f' Rate: send 1 {source_chain.upper()}, get {selected_pair.rate:g} {dest_chain.upper()}\n' f' Miner: UID {selected_pair.uid}\n' f' To: {receive_address}' ) diff --git a/allways/cli/swap_commands/view.py b/allways/cli/swap_commands/view.py index d1342b2..bbf8393 100644 --- a/allways/cli/swap_commands/view.py +++ b/allways/cli/swap_commands/view.py @@ -56,27 +56,36 @@ def view_miners(): console.print('[yellow]No miner commitments found[/yellow]\n') return + src_up = pairs[0].source_chain.upper() + dst_up = pairs[0].dest_chain.upper() + 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(f'{src_up}→{dst_up}', style='green') + table.add_column(f'{dst_up}→{src_up}', style='green') 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') - table.add_column(f'{pairs[0].dest_chain.upper()} Addr', style='dim') + table.add_column(f'{src_up} Addr', style='dim') + table.add_column(f'{dst_up} Addr', style='dim') try: for pair in pairs: collateral_rao = client.get_miner_collateral(pair.hotkey) is_active = client.get_miner_active_flag(pair.hotkey) - - pair_str = f'{pair.source_chain.upper()} <-> {pair.dest_chain.upper()}' active_str = '[green]Yes[/green]' if is_active else '[red]No[/red]' + fwd_display = f'{pair.rate:g}' if pair.rate > 0 else '[dim]—[/dim]' + if pair.counter_rate > 0: + ctr_display = f'{pair.counter_rate:g}' + elif pair.counter_rate_str: + ctr_display = '[dim]—[/dim]' + else: + ctr_display = f'{pair.rate:g}' + table.add_row( str(pair.uid), - pair_str, - f'{pair.rate:g}', + fwd_display, + ctr_display, f'{from_rao(collateral_rao):.4f}', active_str, pair.source_address[:16] + '...', @@ -147,16 +156,24 @@ def view_rates(pair: str): src_name = SUPPORTED_CHAINS.get(src, src).name if src in SUPPORTED_CHAINS else src dst_name = SUPPORTED_CHAINS.get(dst, dst).name if dst in SUPPORTED_CHAINS else dst - console.print(f'[bold]{src_name} <-> {dst_name}[/bold]') + console.print(f'[bold]{src_name} ↔ {dst_name}[/bold]') table = Table(show_header=True) table.add_column('UID', style='cyan') - table.add_column('Rate (TAO)', style='green') + table.add_column(f'{src.upper()}→{dst.upper()}', style='green') + table.add_column(f'{dst.upper()}→{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] + '...') + fwd = f'{p.rate:g}' if p.rate > 0 else '—' + if p.counter_rate > 0: + rev = f'{p.counter_rate:g}' + elif p.counter_rate_str: + rev = '—' + else: + rev = fwd + table.add_row(str(p.uid), fwd, rev, p.hotkey[:16] + '...') console.print(table) diff --git a/allways/commitments.py b/allways/commitments.py index 69f3612..3ad71fb 100644 --- a/allways/commitments.py +++ b/allways/commitments.py @@ -4,7 +4,7 @@ import bittensor as bt -from allways.chains import SUPPORTED_CHAINS +from allways.chains import SUPPORTED_CHAINS, canonical_pair from allways.classes import MinerPair from allways.constants import COMMITMENT_VERSION @@ -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}:{counter_rate} + Both rates are 'canonical_dest per 1 canonical_source'. rate is for source→dest, counter_rate for dest→source. + 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] @@ -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) + counter_rate_str = parts[6] + counter_rate = float(counter_rate_str) if src_chain not in SUPPORTED_CHAINS or dst_chain not in SUPPORTED_CHAINS: return None @@ -42,12 +44,14 @@ def parse_commitment_data(raw: str, uid: int = 0, hotkey: str = '') -> Optional[ if src_chain == dst_chain: 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. - if src_chain == 'tao' and dst_chain != 'tao': + # Normalize to canonical direction (alphabetical ordering). + # When swapping direction, swap rates too: the posted "forward" rate becomes "reverse". + canon_src, _ = canonical_pair(src_chain, dst_chain) + if src_chain != canon_src: src_chain, dst_chain = dst_chain, src_chain src_addr, dst_addr = dst_addr, src_addr + rate, counter_rate = counter_rate, rate + rate_str, counter_rate_str = counter_rate_str, rate_str return MinerPair( uid=uid, @@ -58,6 +62,8 @@ def parse_commitment_data(raw: str, uid: int = 0, hotkey: str = '') -> Optional[ dest_address=dst_addr, rate=rate, rate_str=rate_str, + counter_rate=counter_rate, + counter_rate_str=counter_rate_str, ) except (ValueError, IndexError): return None diff --git a/allways/constants.py b/allways/constants.py index c4f0c01..cb3eb5d 100644 --- a/allways/constants.py +++ b/allways/constants.py @@ -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 ──────────────────────────────────── diff --git a/allways/miner/fulfillment.py b/allways/miner/fulfillment.py index 08a6520..a218add 100644 --- a/allways/miner/fulfillment.py +++ b/allways/miner/fulfillment.py @@ -141,9 +141,9 @@ def send_dest_funds(self, swap: Swap, dest_amount: int) -> Optional[Tuple[str, i key = self.wallet if swap.dest_chain == 'tao' else None - # For BTC sends, read the miner's commitment to get their BTC address. - # Commitments are normalized to non-TAO→TAO, so the BTC address is - # always source_address regardless of swap direction. + # For non-TAO sends, read the miner's commitment to get the sending address. + # Commitments are normalized to canonical order, so source_address is + # always the canonical source chain's address. from_address = None if swap.dest_chain != 'tao': try: diff --git a/allways/utils/rate.py b/allways/utils/rate.py index 8cd5650..8c2c12f 100644 --- a/allways/utils/rate.py +++ b/allways/utils/rate.py @@ -3,20 +3,20 @@ from decimal import Decimal from typing import Tuple -from allways.chains import CHAIN_TAO, get_chain +from allways.chains import canonical_pair, get_chain from allways.constants import RATE_PRECISION def calculate_dest_amount( source_amount: int, rate: str, - source_is_tao: bool, - tao_decimals: int, - asset_decimals: int, + is_reverse: bool, + dest_decimals: int, + source_decimals: int, ) -> int: """Calculate dest_amount from source_amount and committed rate using fixed-point arithmetic. - Rate is TAO per 1 non-TAO asset in display units (e.g. 345 means 1 BTC = 345 TAO). + Rate is 'canonical_dest per 1 canonical_source' in display units (e.g. 345 means 1 BTC = 345 TAO). Uses Decimal for rate conversion to avoid IEEE 754 float rounding artifacts. The rate parameter should be the raw string from the miner's commitment. @@ -25,25 +25,25 @@ def calculate_dest_amount( Args: source_amount: Amount in smallest units (sat, rao, wei, etc.) - rate: TAO per 1 non-TAO asset as a string (e.g. '345') - source_is_tao: True when source chain is TAO - tao_decimals: Decimal places for TAO (9) - asset_decimals: Decimal places for non-TAO asset (8 for BTC, 18 for ETH) + rate: Canonical dest per 1 canonical source as a string (e.g. '345') + is_reverse: True when swap direction is opposite of canonical order + dest_decimals: Decimal places for canonical dest chain (e.g. 9 for TAO) + source_decimals: Decimal places for canonical source chain (e.g. 8 for BTC) """ rate_fixed = int(Decimal(rate) * RATE_PRECISION) if rate_fixed == 0: return 0 - decimal_diff = tao_decimals - asset_decimals + decimal_diff = dest_decimals - source_decimals - if source_is_tao: - # TAO → non-TAO: divide by rate, adjust for decimals + if is_reverse: + # Reverse direction: divide by rate, adjust for decimals if decimal_diff >= 0: return source_amount * RATE_PRECISION // (rate_fixed * 10**decimal_diff) else: return source_amount * RATE_PRECISION * 10 ** (-decimal_diff) // rate_fixed else: - # non-TAO → TAO: multiply by rate, adjust for decimals + # Forward direction: multiply by rate, adjust for decimals if decimal_diff >= 0: return source_amount * rate_fixed * 10**decimal_diff // RATE_PRECISION else: @@ -56,16 +56,15 @@ def expected_swap_amounts(swap, fee_divisor: int) -> Tuple[int, int]: Single source of truth used by both miner (fulfillment) and validator (verification). Returns (raw_dest_amount, user_receives) or (0, 0) if the rate is invalid. """ - source_is_tao = swap.source_chain == 'tao' - non_tao_chain = swap.dest_chain if source_is_tao else swap.source_chain - asset_decimals = get_chain(non_tao_chain).decimals + canon_src, canon_dest = canonical_pair(swap.source_chain, swap.dest_chain) + is_reverse = swap.source_chain != canon_src dest_amount = calculate_dest_amount( swap.source_amount, swap.rate, - source_is_tao, - CHAIN_TAO.decimals, - asset_decimals, + is_reverse, + get_chain(canon_dest).decimals, + get_chain(canon_src).decimals, ) if dest_amount == 0: return 0, 0 diff --git a/allways/validator/axon_handlers.py b/allways/validator/axon_handlers.py index c090b44..351ebeb 100644 --- a/allways/validator/axon_handlers.py +++ b/allways/validator/axon_handlers.py @@ -399,6 +399,10 @@ async def handle_swap_confirm( miner_fulfillment_address = ( commitment.dest_address if swap_source_chain == commitment.source_chain else commitment.source_address ) + selected_rate, selected_rate_str = commitment.get_rate_for_direction(swap_source_chain) + if selected_rate <= 0: + _reject(synapse, 'Miner does not support this swap direction', ctx) + return synapse provider = validator.axon_chain_providers.get(swap_source_chain) if provider is None: @@ -428,7 +432,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): @@ -473,7 +477,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}') diff --git a/tests/test_chains.py b/tests/test_chains.py index d5020c9..7f4b9f4 100644 --- a/tests/test_chains.py +++ b/tests/test_chains.py @@ -5,6 +5,7 @@ from allways.chains import ( CHAIN_BTC, CHAIN_TAO, + canonical_pair, confirmations_to_subtensor_blocks, get_chain, ) @@ -36,6 +37,23 @@ def test_tao_block_time(self): assert CHAIN_TAO.seconds_per_block == 12 +class TestCanonicalPair: + def test_already_canonical(self): + assert canonical_pair('btc', 'tao') == ('btc', 'tao') + + def test_reversed_input(self): + assert canonical_pair('tao', 'btc') == ('btc', 'tao') + + def test_tao_always_dest(self): + # TAO preference: even when a chain sorts after "tao", TAO is dest + assert canonical_pair('thor', 'tao') == ('thor', 'tao') + assert canonical_pair('tao', 'thor') == ('thor', 'tao') + + def test_no_tao_alphabetical(self): + assert canonical_pair('eth', 'btc') == ('btc', 'eth') + assert canonical_pair('btc', 'eth') == ('btc', 'eth') + + class TestConfirmationsToSubtensorBlocks: def test_btc(self): # ceil(3 * 600 / 12) = ceil(150) = 150 diff --git a/tests/test_commitments.py b/tests/test_commitments.py index 9ef03ca..a7d9aea 100644 --- a/tests/test_commitments.py +++ b/tests/test_commitments.py @@ -4,8 +4,8 @@ class TestParseCommitmentData: - def test_valid_btc_tao(self): - raw = 'v1:btc:bc1qaddr:tao:5Caddr:0.00015' + def test_valid_two_rates(self): + raw = 'v3:btc:bc1qaddr:tao:5Caddr:340:350' pair = parse_commitment_data(raw, uid=1, hotkey='hk1') assert pair is not None assert pair.uid == 1 @@ -14,50 +14,150 @@ def test_valid_btc_tao(self): assert pair.source_address == 'bc1qaddr' assert pair.dest_chain == 'tao' assert pair.dest_address == '5Caddr' - assert pair.rate == 0.00015 - assert pair.rate_str == '0.00015' + assert pair.rate == 340.0 + assert pair.rate_str == '340' + assert pair.counter_rate == 350.0 + assert pair.counter_rate_str == '350' - def test_valid_tao_btc(self): - raw = 'v1:tao:5Caddr:btc:bc1qaddr:6666.67' + def test_valid_same_rate_both_directions(self): + raw = 'v3:btc:bc1qaddr:tao:5Caddr:345:345' pair = parse_commitment_data(raw) assert pair is not None - assert pair.source_chain == 'tao' - assert pair.dest_chain == 'btc' + assert pair.rate == 345.0 + assert pair.counter_rate == 345.0 + + def test_normalization_swaps_rates(self): + """When posted as tao->btc, normalization flips to btc->tao and swaps rates.""" + raw = 'v3:tao:5Caddr:btc:bc1qaddr:340:350' + pair = parse_commitment_data(raw) + assert pair is not None + assert pair.source_chain == 'btc' + assert pair.dest_chain == 'tao' + assert pair.source_address == 'bc1qaddr' + assert pair.dest_address == '5Caddr' + # Original forward rate (340) was for tao->btc, now becomes reverse + assert pair.rate == 350.0 + assert pair.rate_str == '350' + assert pair.counter_rate == 340.0 + assert pair.counter_rate_str == '340' + + def test_fractional_rates(self): + raw = 'v3:btc:bc1qaddr:tao:5Caddr:345.12:350.45' + pair = parse_commitment_data(raw) + assert pair is not None + assert pair.rate == 345.12 + assert pair.rate_str == '345.12' + assert pair.counter_rate == 350.45 + assert pair.counter_rate_str == '350.45' + + def test_get_rate_for_direction(self): + raw = 'v3:btc:bc1qaddr:tao:5Caddr:340:350' + pair = parse_commitment_data(raw) + # Forward (btc -> tao) + rate, rate_str = pair.get_rate_for_direction('btc') + assert rate == 340.0 + assert rate_str == '340' + # Reverse (tao -> btc) + rate, rate_str = pair.get_rate_for_direction('tao') + assert rate == 350.0 + assert rate_str == '350' + + def test_get_rate_for_direction_after_normalization(self): + """Full pipeline: tao->btc commitment normalizes, then direction lookup works.""" + raw = 'v3:tao:5Caddr:btc:bc1qaddr:340:350' + pair = parse_commitment_data(raw) + # After normalization: source=btc, dest=tao + # Original 340 was tao->btc (now reverse), 350 was btc->tao (now forward) + fwd_rate, fwd_str = pair.get_rate_for_direction('btc') + rev_rate, rev_str = pair.get_rate_for_direction('tao') + assert fwd_rate == 350.0 + assert fwd_str == '350' + assert rev_rate == 340.0 + assert rev_str == '340' def test_wrong_part_count_too_few(self): - assert parse_commitment_data('v1:btc:addr:tao:addr') is None + assert parse_commitment_data('v3:btc:addr:tao:addr:345') is None def test_wrong_part_count_too_many(self): - assert parse_commitment_data('v1:btc:addr:tao:addr:0.1:extra') is None + assert parse_commitment_data('v3:btc:addr:tao:addr:340:350:extra') is None def test_wrong_version(self): - assert parse_commitment_data('v2:btc:addr:tao:addr:0.1') is None + assert parse_commitment_data('v2:btc:addr:tao:addr:340:350') is None def test_no_version_prefix(self): - assert parse_commitment_data('1:btc:addr:tao:addr:0.1') is None + assert parse_commitment_data('3:btc:addr:tao:addr:340:350') is None def test_unsupported_source_chain(self): - assert parse_commitment_data('v1:eth:addr:tao:addr:0.1') is None + assert parse_commitment_data('v3:eth:addr:tao:addr:340:350') is None def test_unsupported_dest_chain(self): - assert parse_commitment_data('v1:btc:addr:eth:addr:0.1') is None + assert parse_commitment_data('v3:btc:addr:eth:addr:340:350') is None def test_invalid_rate_not_a_number(self): - assert parse_commitment_data('v1:btc:addr:tao:addr:abc') is None + assert parse_commitment_data('v3:btc:addr:tao:addr:abc:350') is None + + def test_invalid_reverse_rate_not_a_number(self): + assert parse_commitment_data('v3:btc:addr:tao:addr:340:abc') is None def test_empty_string(self): assert parse_commitment_data('') is None def test_rate_zero(self): - pair = parse_commitment_data('v1:btc:addr:tao:addr:0') + pair = parse_commitment_data('v3:btc:addr:tao:addr:0:0') + assert pair is not None + assert pair.rate == 0.0 + assert pair.counter_rate == 0.0 + + def test_disabled_direction_produces_zero_dest_amount(self): + """Full guard chain: disabled direction → rate=0 → dest_amount=0 → contract rejects.""" + from allways.utils.rate import calculate_dest_amount + + raw = 'v3:btc:bc1qaddr:tao:5Caddr:345:0' + pair = parse_commitment_data(raw) + # Validator calls get_rate_for_direction for the disabled direction + rate, rate_str = pair.get_rate_for_direction('tao') + assert rate == 0.0 + assert rate <= 0 # validator guard: if selected_rate <= 0: reject + # Even if the guard were bypassed, calculate_dest_amount returns 0 + dest_amount = calculate_dest_amount( + 1_000_000_000, rate_str, is_reverse=True, dest_decimals=9, source_decimals=8 + ) + assert dest_amount == 0 # contract would reject with InvalidAmount + + def test_single_direction_forward_only(self): + """Miner supports only BTC→TAO (counter_rate=0 means TAO→BTC not offered).""" + raw = 'v3:btc:bc1qaddr:tao:5Caddr:345:0' + pair = parse_commitment_data(raw) + assert pair is not None + assert pair.rate == 345.0 + assert pair.counter_rate == 0.0 + # Forward direction returns valid rate + rate, rate_str = pair.get_rate_for_direction('btc') + assert rate == 345.0 + # Counter direction returns 0 + rate, rate_str = pair.get_rate_for_direction('tao') + assert rate == 0.0 + + def test_single_direction_counter_only(self): + """Miner posts tao→btc only. After normalization, rate=0, counter_rate has the value.""" + raw = 'v3:tao:5Caddr:btc:bc1qaddr:345:0' + pair = parse_commitment_data(raw) assert pair is not None + # Normalization flips: btc→tao becomes source→dest + # Original rate 345 was tao→btc (now counter), original 0 was btc→tao (now forward) assert pair.rate == 0.0 + assert pair.counter_rate == 345.0 + # BTC→TAO returns 0 (not supported) + rate, _ = pair.get_rate_for_direction('btc') + assert rate == 0.0 + # TAO→BTC returns 345 + rate, _ = pair.get_rate_for_direction('tao') + assert rate == 345.0 def test_default_uid_and_hotkey(self): - pair = parse_commitment_data('v1:btc:addr:tao:addr:1.0') + pair = parse_commitment_data('v3:btc:addr:tao:addr:1.0:2.0') assert pair.uid == 0 assert pair.hotkey == '' - def test_colon_in_address(self): - # Extra colons split into too many parts - assert parse_commitment_data('v1:btc:addr:with:colon:tao:addr:0.1') is None + def test_same_chain(self): + assert parse_commitment_data('v3:btc:addr:btc:addr:340:350') is None diff --git a/tests/test_rate.py b/tests/test_rate.py index 82c6c0f..538720a 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -12,37 +12,37 @@ class TestBtcToTao: - """BTC → TAO: non-TAO source, multiply by rate.""" + """BTC → TAO: forward direction, multiply by rate.""" def test_standard_rate(self): # 0.01 BTC @ rate 345 (1 BTC = 345 TAO) → 3.45 TAO source = int(Decimal('0.01') * BTC_TO_SAT) # 1_000_000 sat - result = calculate_dest_amount(source, '345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(source, '345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) expected = 3_450_000_000 # 3.45 TAO in rao assert result == expected def test_one_btc(self): # 1 BTC @ rate 345 → 345 TAO source = BTC_TO_SAT # 100_000_000 sat - result = calculate_dest_amount(source, '345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(source, '345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == 345 * TAO_TO_RAO def test_round_rate(self): # 1 BTC @ rate 100 → 100 TAO source = BTC_TO_SAT - result = calculate_dest_amount(source, '100', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(source, '100', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == 100 * TAO_TO_RAO def test_small_amount(self): # 1 sat @ rate 345 → 3450 rao - result = calculate_dest_amount(1, '345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(1, '345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == 3450 def test_fractional_rate(self): # 0.01 BTC @ rate 344.827586 → ~3.44827586 TAO source = int(Decimal('0.01') * BTC_TO_SAT) result = calculate_dest_amount( - source, '344.827586', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + source, '344.827586', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) rate_fixed = int(Decimal('344.827586') * RATE_PRECISION) expected = source * rate_fixed * 10 // RATE_PRECISION @@ -50,24 +50,24 @@ def test_fractional_rate(self): class TestTaoToBtc: - """TAO → BTC: TAO source, divide by rate.""" + """TAO → BTC: reverse direction, divide by rate.""" def test_standard_rate(self): # 345 TAO @ rate 345 (1 BTC = 345 TAO) → 1 BTC source = 345 * TAO_TO_RAO - result = calculate_dest_amount(source, '345', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(source, '345', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == BTC_TO_SAT # 100_000_000 sat = 1 BTC def test_small_amount(self): # 3.45 TAO @ rate 345 → 0.01 BTC = 1_000_000 sat source = 3_450_000_000 # 3.45 TAO in rao - result = calculate_dest_amount(source, '345', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(source, '345', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == 1_000_000 def test_round_rate(self): # 100 TAO @ rate 100 → 1 BTC source = 100 * TAO_TO_RAO - result = calculate_dest_amount(source, '100', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(source, '100', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == BTC_TO_SAT @@ -77,48 +77,66 @@ class TestRoundTrip: def test_btc_tao_btc_symmetry(self): source_sat = int(Decimal('0.01') * BTC_TO_SAT) tao_rao = calculate_dest_amount( - source_sat, '345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + source_sat, '345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) back_sat = calculate_dest_amount( - tao_rao, '345', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + tao_rao, '345', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) assert back_sat == source_sat def test_tao_btc_tao_symmetry(self): source_rao = 345 * TAO_TO_RAO btc_sat = calculate_dest_amount( - source_rao, '345', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + source_rao, '345', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) back_rao = calculate_dest_amount( - btc_sat, '345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + btc_sat, '345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) assert back_rao == source_rao +class TestDirectionSpecificRates: + """Different rates for each direction produce different amounts.""" + + def test_forward_vs_reverse_different_amounts(self): + # Forward: 0.01 BTC @ 340 → 3.4 TAO + fwd = calculate_dest_amount(1_000_000, '340', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) + assert fwd == 3_400_000_000 # 3.4 TAO + + # Reverse: 3.5 TAO @ 350 → 0.01 BTC + rev = calculate_dest_amount( + 3_500_000_000, '350', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=BTC_DEC + ) + assert rev == 1_000_000 # 0.01 BTC + + # The rates differ, so round-tripping at different rates loses/gains value + assert fwd != calculate_dest_amount( + 1_000_000, '350', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC + ) + + class TestFutureEth: """ETH ↔ TAO with 18 decimal places (decimal_diff = 9 - 18 = -9).""" def test_eth_to_tao(self): # 1 ETH @ rate 2000 → 2000 TAO source = 10**ETH_DEC # 1 ETH in wei - result = calculate_dest_amount( - source, '2000', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=ETH_DEC - ) + result = calculate_dest_amount(source, '2000', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=ETH_DEC) assert result == 2000 * TAO_TO_RAO def test_tao_to_eth(self): # 2000 TAO @ rate 2000 → 1 ETH source = 2000 * TAO_TO_RAO - result = calculate_dest_amount(source, '2000', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=ETH_DEC) + result = calculate_dest_amount(source, '2000', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=ETH_DEC) assert result == 10**ETH_DEC def test_eth_tao_round_trip(self): source_wei = 10**ETH_DEC # 1 ETH tao_rao = calculate_dest_amount( - source_wei, '2000', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=ETH_DEC + source_wei, '2000', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=ETH_DEC ) back_wei = calculate_dest_amount( - tao_rao, '2000', source_is_tao=True, tao_decimals=TAO_DEC, asset_decimals=ETH_DEC + tao_rao, '2000', is_reverse=True, dest_decimals=TAO_DEC, source_decimals=ETH_DEC ) assert back_wei == source_wei @@ -127,19 +145,17 @@ class TestEdgeCases: """Edge cases and invariants.""" def test_zero_source(self): - result = calculate_dest_amount(0, '345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC) + result = calculate_dest_amount(0, '345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == 0 def test_zero_rate(self): - result = calculate_dest_amount( - 1_000_000, '0', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC - ) + result = calculate_dest_amount(1_000_000, '0', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC) assert result == 0 def test_negative_rate_string(self): # Doesn't crash — contract should reject negative rates result = calculate_dest_amount( - 1_000_000, '-345', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + 1_000_000, '-345', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) assert isinstance(result, int) @@ -159,9 +175,9 @@ def test_determinism_across_calls(self): calculate_dest_amount( 1_000_000, '345', - source_is_tao=False, - tao_decimals=TAO_DEC, - asset_decimals=BTC_DEC, + is_reverse=False, + dest_decimals=TAO_DEC, + source_decimals=BTC_DEC, ) ) assert len(results) == 1 @@ -170,7 +186,7 @@ def test_rate_string_not_float(self): # Decimal('0.1') is exact; float 0.1 is not source = 10 * BTC_TO_SAT # 10 BTC result = calculate_dest_amount( - source, '345.1', source_is_tao=False, tao_decimals=TAO_DEC, asset_decimals=BTC_DEC + source, '345.1', is_reverse=False, dest_decimals=TAO_DEC, source_decimals=BTC_DEC ) rate_fixed = int(Decimal('345.1') * RATE_PRECISION) expected = source * rate_fixed * 10 // RATE_PRECISION @@ -181,9 +197,9 @@ def test_high_precision_rate(self): result = calculate_dest_amount( source, '345.123456789', - source_is_tao=False, - tao_decimals=TAO_DEC, - asset_decimals=BTC_DEC, + is_reverse=False, + dest_decimals=TAO_DEC, + source_decimals=BTC_DEC, ) rate_fixed = int(Decimal('345.123456789') * RATE_PRECISION) expected = source * rate_fixed * 10 // RATE_PRECISION diff --git a/tests/test_scale.py b/tests/test_scale.py index 79c48be..fea92a8 100644 --- a/tests/test_scale.py +++ b/tests/test_scale.py @@ -267,6 +267,9 @@ def _encode_swap_bytes( source_amount=100000, dest_amount=0, tao_amount=1_000_000_000, + miner_source_address='bc1qminer', + miner_dest_address='5Cminer', + rate='345', source_tx_hash='txhash', source_tx_block=50, dest_tx_hash='', @@ -294,6 +297,9 @@ def _encode_swap_bytes( data += client._encode_value(tao_amount, 'u128') data += client._encode_value('bc1quser', 'str') data += client._encode_value('5Cuser', 'str') + data += client._encode_value(miner_source_address, 'str') + data += client._encode_value(miner_dest_address, 'str') + data += client._encode_value(rate, 'str') data += client._encode_value(source_tx_hash, 'str') data += struct.pack('