From 452f876e6a01b1a53b171993b258d10483be3f4e Mon Sep 17 00:00:00 2001 From: Landyn Date: Wed, 1 Apr 2026 00:29:08 -0500 Subject: [PATCH 1/2] Verify dest transaction sender matches miner's posted address A miner could claim a third-party tx to the user's dest address as their own fulfillment. Store miner_dest_address on the swap struct at initiation and validate tx_info.sender against it during dest-side verification. --- allways/classes.py | 1 + allways/contract_client.py | 5 +++++ allways/validator/axon_handlers.py | 5 +++++ allways/validator/chain_verification.py | 19 +++++++++++++++++-- allways/validator/forward.py | 1 + allways/validator/pending_confirms.py | 1 + smart-contracts/ink/lib.rs | 4 +++- smart-contracts/ink/types.rs | 1 + 8 files changed, 34 insertions(+), 3 deletions(-) diff --git a/allways/classes.py b/allways/classes.py index 580589e..6be2d1e 100644 --- a/allways/classes.py +++ b/allways/classes.py @@ -62,6 +62,7 @@ class Swap: user_source_address: str user_dest_address: str miner_source_address: str = '' + miner_dest_address: str = '' rate: str = '' source_tx_hash: str = '' source_tx_block: int = 0 diff --git a/allways/contract_client.py b/allways/contract_client.py index 1c65ed9..442a263 100644 --- a/allways/contract_client.py +++ b/allways/contract_client.py @@ -113,6 +113,7 @@ ('source_tx_block', 'u32'), ('dest_amount', 'u128'), ('miner_source_address', 'str'), + ('miner_dest_address', 'str'), ('rate', 'str'), ], 'vote_activate': [('miner', 'AccountId')], @@ -584,6 +585,7 @@ def _decode_swap_data(self, data: bytes, offset: int = 0) -> Optional[Swap]: user_source_address, o = self._decode_string(data, o) user_dest_address, o = self._decode_string(data, o) miner_source_address, o = self._decode_string(data, o) + miner_dest_address, o = self._decode_string(data, o) rate, o = self._decode_string(data, o) source_tx_hash, o = self._decode_string(data, o) source_tx_block = struct.unpack_from(' Optional[Swap]: user_source_address=user_source_address, user_dest_address=user_dest_address, miner_source_address=miner_source_address, + miner_dest_address=miner_dest_address, rate=rate, source_tx_hash=source_tx_hash, source_tx_block=source_tx_block, @@ -997,6 +1000,7 @@ def vote_initiate( source_tx_block: int = 0, dest_amount: int = 0, miner_source_address: str = '', + miner_dest_address: str = '', rate: str = '', ) -> str: """Vote to initiate a swap. On quorum, swap is created on contract.""" @@ -1017,6 +1021,7 @@ def vote_initiate( 'source_tx_block': source_tx_block, 'dest_amount': dest_amount, 'miner_source_address': miner_source_address, + 'miner_dest_address': miner_dest_address, 'rate': rate, }, keypair=wallet.hotkey, diff --git a/allways/validator/axon_handlers.py b/allways/validator/axon_handlers.py index 864abf5..c090b44 100644 --- a/allways/validator/axon_handlers.py +++ b/allways/validator/axon_handlers.py @@ -396,6 +396,9 @@ async def handle_swap_confirm( miner_deposit_address = ( commitment.source_address if swap_source_chain == commitment.source_chain else commitment.dest_address ) + miner_fulfillment_address = ( + commitment.dest_address if swap_source_chain == commitment.source_chain else commitment.source_address + ) provider = validator.axon_chain_providers.get(swap_source_chain) if provider is None: @@ -424,6 +427,7 @@ async def handle_swap_confirm( source_amount=res_source_amount, dest_amount=res_dest_amount, miner_deposit_address=miner_deposit_address, + miner_dest_address=miner_fulfillment_address, rate_str=commitment.rate_str, reserved_until=reserved_until, ) @@ -468,6 +472,7 @@ async def handle_swap_confirm( source_tx_block=tx_info.block_number or 0, dest_amount=res_dest_amount, miner_source_address=miner_deposit_address, + miner_dest_address=miner_fulfillment_address, rate=commitment.rate_str, ) synapse.accepted = True diff --git a/allways/validator/chain_verification.py b/allways/validator/chain_verification.py index c276e1e..aacd23e 100644 --- a/allways/validator/chain_verification.py +++ b/allways/validator/chain_verification.py @@ -33,7 +33,14 @@ def __init__( self._last_logged_confs: Dict[str, int] = {} # swap_id:chain -> confs def _verify_tx( - self, swap: Swap, chain: str, tx_hash: str, expected_recipient: str, expected_amount: int, block_hint: int = 0 + self, + swap: Swap, + chain: str, + tx_hash: str, + expected_recipient: str, + expected_amount: int, + block_hint: int = 0, + expected_sender: str = '', ) -> bool: """Verify a confirmed transaction on a specific chain.""" provider = self.providers.get(chain) @@ -67,7 +74,14 @@ def _verify_tx( f'(confs={tx_info.confirmations} tx={tx_hash[:16]}... ' f'addr={expected_recipient[:16]}... expected={expected_amount})' ) - return tx_info is not None and tx_info.confirmed + if tx_info is None or not tx_info.confirmed: + return False + if expected_sender and tx_info.sender != expected_sender: + bt.logging.warning( + f'Swap {swap.id}: sender mismatch on {chain} — expected {expected_sender}, got {tx_info.sender}' + ) + return False + return True except Exception as e: bt.logging.error(f'Swap {swap.id}: verification error on {chain}: {e}') return False @@ -113,6 +127,7 @@ async def is_swap_complete(self, swap: Swap) -> bool: swap.user_dest_address, expected_user_receives, swap.dest_tx_block, + swap.miner_dest_address, ) return source_ok and dest_ok diff --git a/allways/validator/forward.py b/allways/validator/forward.py index fb6e784..efbf85c 100644 --- a/allways/validator/forward.py +++ b/allways/validator/forward.py @@ -188,6 +188,7 @@ def _process_pending_confirms(self: Validator) -> None: source_tx_block=tx_info.block_number or 0, dest_amount=item.dest_amount, miner_source_address=item.miner_deposit_address, + miner_dest_address=item.miner_dest_address, rate=item.rate_str, ) bt.logging.success( diff --git a/allways/validator/pending_confirms.py b/allways/validator/pending_confirms.py index c94bd4c..7750a24 100644 --- a/allways/validator/pending_confirms.py +++ b/allways/validator/pending_confirms.py @@ -26,6 +26,7 @@ class PendingConfirm: source_amount: int dest_amount: int miner_deposit_address: str + miner_dest_address: str rate_str: str reserved_until: int queued_at: float = field(default_factory=time.time) diff --git a/smart-contracts/ink/lib.rs b/smart-contracts/ink/lib.rs index 703cafb..b3329fa 100644 --- a/smart-contracts/ink/lib.rs +++ b/smart-contracts/ink/lib.rs @@ -546,6 +546,7 @@ mod allways_swap_manager { source_tx_block: u32, dest_amount: Balance, miner_source_address: String, + miner_dest_address: String, rate: String, ) -> Result<(), Error> { self.ensure_validator()?; @@ -567,7 +568,7 @@ mod allways_swap_manager { if source_amount == 0 || tao_amount == 0 { return Err(Error::InvalidAmount); } - if source_tx_hash.is_empty() || miner_source_address.is_empty() || rate.is_empty() { + if source_tx_hash.is_empty() || miner_source_address.is_empty() || miner_dest_address.is_empty() || rate.is_empty() { return Err(Error::InputEmpty); } if source_tx_hash.len() > 128 { @@ -625,6 +626,7 @@ mod allways_swap_manager { user_source_address, user_dest_address, miner_source_address, + miner_dest_address, rate, source_tx_hash: source_tx_hash.clone(), source_tx_block, diff --git a/smart-contracts/ink/types.rs b/smart-contracts/ink/types.rs index 125787c..3c7719c 100644 --- a/smart-contracts/ink/types.rs +++ b/smart-contracts/ink/types.rs @@ -42,6 +42,7 @@ pub struct SwapData { pub user_source_address: String, pub user_dest_address: String, pub miner_source_address: String, + pub miner_dest_address: String, pub rate: String, pub source_tx_hash: String, pub source_tx_block: u32, From 9e17459009bc618c032a35a5122c094ebe494e16 Mon Sep 17 00:00:00 2001 From: Landyn Date: Wed, 1 Apr 2026 01:11:45 -0500 Subject: [PATCH 2/2] Support direction-specific rates for miner pair posting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Miners can now post separate rates for each swap direction (e.g., 340 TAO/BTC for BTC->TAO, 350 TAO/BTC for TAO->BTC) to capture tx fee asymmetry. Commitment format bumped to v3 with two rate fields. Backend unchanged — each swap still stores one rate, selected by direction at initiation time. --- allways/classes.py | 18 ++++- allways/cli/swap_commands/miner_commands.py | 8 +- allways/cli/swap_commands/pair.py | 70 +++++++++++++----- allways/cli/swap_commands/swap.py | 5 +- allways/cli/swap_commands/view.py | 14 +++- allways/commitments.py | 17 +++-- allways/constants.py | 2 +- allways/validator/axon_handlers.py | 5 +- tests/test_commitments.py | 81 ++++++++++++++++----- 9 files changed, 161 insertions(+), 59 deletions(-) diff --git a/allways/classes.py b/allways/classes.py index 6be2d1e..67e4841 100644 --- a/allways/classes.py +++ b/allways/classes.py @@ -27,7 +27,11 @@ 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 @@ -35,8 +39,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 # 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 diff --git a/allways/cli/swap_commands/miner_commands.py b/allways/cli/swap_commands/miner_commands.py index 06bc3b9..92ca66a 100644 --- a/allways/cli/swap_commands/miner_commands.py +++ b/allways/cli/swap_commands/miner_commands.py @@ -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: diff --git a/allways/cli/swap_commands/pair.py b/allways/cli/swap_commands/pair.py index e26741f..62b3c55 100644 --- a/allways/cli/swap_commands/pair.py +++ b/allways/cli/swap_commands/pair.py @@ -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') @@ -34,6 +39,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('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, @@ -41,6 +47,7 @@ def post_pair( 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. @@ -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: @@ -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 @@ -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') @@ -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', diff --git a/allways/cli/swap_commands/swap.py b/allways/cli/swap_commands/swap.py index 88e3d53..a8096c3 100644 --- a/allways/cli/swap_commands/swap.py +++ b/allways/cli/swap_commands/swap.py @@ -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, @@ -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, ) ) diff --git a/allways/cli/swap_commands/view.py b/allways/cli/swap_commands/view.py index d1342b2..e53e12a 100644 --- a/allways/cli/swap_commands/view.py +++ b/allways/cli/swap_commands/view.py @@ -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') @@ -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] + '...', @@ -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) diff --git a/allways/commitments.py b/allways/commitments.py index 69f3612..ceb9dc6 100644 --- a/allways/commitments.py +++ b/allways/commitments.py @@ -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] @@ -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 @@ -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, @@ -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 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/validator/axon_handlers.py b/allways/validator/axon_handlers.py index c090b44..f3cc4a5 100644 --- a/allways/validator/axon_handlers.py +++ b/allways/validator/axon_handlers.py @@ -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: @@ -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): @@ -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}') diff --git a/tests/test_commitments.py b/tests/test_commitments.py index 9ef03ca..102a2bd 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,91 @@ 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.rate_reverse == 350.0 + assert pair.rate_reverse_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.rate_reverse == 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.rate_reverse == 340.0 + assert pair.rate_reverse_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.rate_reverse == 350.45 + assert pair.rate_reverse_str == '350.45' + + def test_get_rate_for_direction(self): + raw = 'v3:btc:bc1qaddr:tao:5Caddr:340:350' + pair = parse_commitment_data(raw) + # non-TAO -> TAO (forward) + rate, rate_str = pair.get_rate_for_direction(source_is_tao=False) + assert rate == 340.0 + assert rate_str == '340' + # TAO -> non-TAO (reverse) + rate, rate_str = pair.get_rate_for_direction(source_is_tao=True) + assert rate == 350.0 + assert rate_str == '350' 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.rate_reverse == 0.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