diff --git a/allways/cli/dendrite_lite.py b/allways/cli/dendrite_lite.py index 075c8eb..db73e13 100644 --- a/allways/cli/dendrite_lite.py +++ b/allways/cli/dendrite_lite.py @@ -31,7 +31,7 @@ def get_ephemeral_wallet() -> bt.Wallet: hotkey_file = Path(wallet_path) / EPHEMERAL_WALLET_NAME / 'hotkeys' / EPHEMERAL_HOTKEY_NAME if not hotkey_file.exists(): hotkey_file.parent.mkdir(parents=True, exist_ok=True) - wallet.create_if_non_existent(coldkey_use_password=False, hotkey_use_password=False) + wallet.create_if_non_existent(coldkey_use_password=False, hotkey_use_password=False, suppress=True) bt.logging.info('Created ephemeral wallet for dendrite-lite') return wallet diff --git a/allways/cli/swap_commands/claim.py b/allways/cli/swap_commands/claim.py index 9fd59b4..1403759 100644 --- a/allways/cli/swap_commands/claim.py +++ b/allways/cli/swap_commands/claim.py @@ -1,13 +1,17 @@ """alw claim - Claim a pending slash payout for a timed-out swap.""" -import os - import click from allways.classes import SwapStatus from allways.cli.help import StyledCommand -from allways.cli.swap_commands.helpers import console, from_rao, get_cli_context, loading, print_contract_error -from allways.cli.swap_commands.view import DEFAULT_DASHBOARD_URL +from allways.cli.swap_commands.helpers import ( + console, + dashboard_url, + from_rao, + get_cli_context, + loading, + print_contract_error, +) from allways.contract_client import ContractError @@ -24,7 +28,7 @@ def claim_command(swap_id: int, yes: bool): [dim]Examples: $ alw claim 42[/dim] """ - _, wallet, _, client = get_cli_context() + config, wallet, _, client = get_cli_context() console.print(f'\n[bold]Claim Slash — Swap #{swap_id}[/bold]\n') @@ -47,12 +51,12 @@ def claim_command(swap_id: int, yes: bool): ) return - dashboard_url = os.environ.get('ALLWAYS_DASHBOARD_URL', DEFAULT_DASHBOARD_URL).rstrip('/') + dashboard = dashboard_url(config.get('network')) console.print( f'[yellow]Nothing to claim for swap #{swap_id}.[/yellow]\n' '[dim]The slash was either paid directly to the user at timeout, already claimed,\n' 'or the swap never timed out. Only the original swap user can claim a pending slash.\n' - f'Refund history:[/dim] {dashboard_url}/swap/{swap_id}\n' + f'Refund history:[/dim] {dashboard}/swap/{swap_id}\n' ) return diff --git a/allways/cli/swap_commands/helpers.py b/allways/cli/swap_commands/helpers.py index 77168f8..f18ff38 100644 --- a/allways/cli/swap_commands/helpers.py +++ b/allways/cli/swap_commands/helpers.py @@ -163,6 +163,25 @@ def is_local_network(network: str) -> bool: return any(host in network for host in ('127.0.0.1', 'localhost', '0.0.0.0')) +PROD_DASHBOARD_URL = 'https://all-ways.io' +TEST_DASHBOARD_URL = 'https://test.all-ways.io' + + +def dashboard_url(network: Optional[str] = None) -> str: + """Resolve the dashboard base URL for the active network. + + finney maps to the mainnet dashboard; every other network (test, local, + custom endpoints) maps to the testnet dashboard. ALLWAYS_DASHBOARD_URL + overrides everything for staging/local use. + """ + override = os.environ.get('ALLWAYS_DASHBOARD_URL') + if override: + return override.rstrip('/') + if network is None: + network = get_effective_config().get('network', 'finney') + return (PROD_DASHBOARD_URL if network == 'finney' else TEST_DASHBOARD_URL).rstrip('/') + + def to_rao(amount_tao: float) -> int: """Convert TAO to rao.""" return int(amount_tao * TAO_TO_RAO) diff --git a/allways/cli/swap_commands/swap.py b/allways/cli/swap_commands/swap.py index 131f571..d2720e1 100644 --- a/allways/cli/swap_commands/swap.py +++ b/allways/cli/swap_commands/swap.py @@ -20,6 +20,7 @@ blocks_to_minutes_str, clear_pending_swap, console, + dashboard_url, find_matching_miners, from_rao, get_cli_context, @@ -740,33 +741,29 @@ def swap_now_command( return # Show send capability for the source chain (skip in non-interactive mode). - # Only asked once we know a miner can actually fill this swap. - if not skip_confirm: - if from_chain == 'tao': - console.print('\n [green]TAO will be sent automatically from your wallet.[/green]') + # Only asked once we know a miner can actually fill this swap. The TAO + # auto-send notice is deferred to the unlock step, nearer the password prompt. + if not skip_confirm and from_chain != 'tao': + # Prefix-check the value (not just truthy): dotenv can leak an inline comment as the var. + wif_raw = (os.environ.get('BTC_PRIVATE_KEY') or '').strip() + has_private_key = bool(wif_raw) and wif_raw[0] in '5KLc9' + btc_mode = os.environ.get('BTC_MODE', 'lightweight') + is_local = is_local_network(config.get('network', 'finney')) + + if has_private_key and not is_local: + console.print('\n [green]BTC_PRIVATE_KEY set — will sign and attempt BTC sends locally.[/green]') else: - # Prefix-check the value (not just truthy): dotenv can leak an inline comment as the var. - wif_raw = (os.environ.get('BTC_PRIVATE_KEY') or '').strip() - has_private_key = bool(wif_raw) and wif_raw[0] in '5KLc9' - btc_mode = os.environ.get('BTC_MODE', 'lightweight') - is_local = is_local_network(config.get('network', 'finney')) - - if has_private_key and not is_local: - console.print('\n [green]BTC_PRIVATE_KEY set — will sign and attempt BTC sends locally.[/green]') - else: - # External signing path — covers both the lightweight/no-key - # case and any other environment without automatic BTC sending. - # Taproot caveat only applies to the BYO-sig flow. - taproot_note = ( - ' Taproot (bc1p…) unsupported.' if btc_mode == 'lightweight' and not has_private_key else '' - ) - console.print( - f'\n [yellow]BTC signing & sending are external — you will sign at reserve/confirm' - f' and run [cyan]alw swap post-tx [/cyan] after broadcasting.{taproot_note}[/yellow]' - ) - if not click.confirm(' Continue?', default=True): - console.print('[yellow]Cancelled[/yellow]') - return + # External signing path — covers both the lightweight/no-key + # case and any other environment without automatic BTC sending. + # Taproot caveat only applies to the BYO-sig flow. + taproot_note = ' Taproot (bc1p…) unsupported.' if btc_mode == 'lightweight' and not has_private_key else '' + console.print( + f'\n [yellow]BTC signing & sending are external — you will sign at reserve/confirm' + f' and run [cyan]alw swap post-tx [/cyan] after broadcasting.{taproot_note}[/yellow]' + ) + if not click.confirm(' Continue?', default=True): + console.print('[yellow]Cancelled[/yellow]') + return # Rate is TAO/BTC: highest is best when receiving TAO, lowest when sending TAO. canon_from, canon_to = canonical_pair(from_chain, to_chain) @@ -989,8 +986,13 @@ def swap_now_command( console.print('[yellow]Cancelled[/yellow]') return - # Unlock coldkey once (password prompt) — all subsequent signing uses the cached key + # Unlock coldkey (password prompt) to sign the reservation proof. if from_chain == 'tao': + console.print(f"\n [green]TAO sends automatically from your wallet '[bold]{wallet.name}[/bold]'.[/green]") + console.print( + ' [dim]Password = your Bittensor coldkey password. This first unlock signs the reservation ' + 'proof (proving you own the source address).[/dim]' + ) from_key = wallet.coldkey else: from_key = None @@ -1072,9 +1074,7 @@ def swap_now_command( save_pending_swap(state) if request_hash: - from allways.cli.swap_commands.view import DEFAULT_DASHBOARD_URL - - dashboard = os.environ.get('ALLWAYS_DASHBOARD_URL', DEFAULT_DASHBOARD_URL).rstrip('/') + dashboard = dashboard_url(config.get('network')) console.print(f' [dim]Reservation:[/dim] [cyan]{dashboard}/reservations/{request_hash}[/cyan]') # Step 9: Send funds (or use pre-provided tx hash) @@ -1106,6 +1106,9 @@ def swap_now_command( from_tx_hash = None if from_chain == 'tao': + console.print( + ' [dim]Enter your coldkey password again — this unlock signs and broadcasts the TAO transfer.[/dim]' + ) for attempt in range(2): send_result = send_tao_transfer(wallet, subtensor, deposit_address, from_amount) if send_result is not None: @@ -1218,7 +1221,7 @@ def swap_now_command( return # Watch swap through lifecycle - from allways.cli.swap_commands.view import DEFAULT_DASHBOARD_URL, watch_swap + from allways.cli.swap_commands.view import watch_swap final_swap = watch_swap(client, swap_id) @@ -1226,5 +1229,4 @@ def swap_now_command( if final_swap and final_swap.status == SwapStatus.COMPLETED: display_receipt(final_swap) elif final_swap and final_swap.status == SwapStatus.TIMED_OUT: - dashboard_url = os.environ.get('ALLWAYS_DASHBOARD_URL', DEFAULT_DASHBOARD_URL).rstrip('/') - display_timeout_notice(final_swap, dashboard_url) + display_timeout_notice(final_swap, dashboard_url(config.get('network'))) diff --git a/allways/cli/swap_commands/view.py b/allways/cli/swap_commands/view.py index a208805..51d93d5 100644 --- a/allways/cli/swap_commands/view.py +++ b/allways/cli/swap_commands/view.py @@ -1,6 +1,5 @@ """alw view - View swaps, miners, and rates.""" -import os import time from dataclasses import replace @@ -19,6 +18,7 @@ blocks_to_minutes_str, clear_pending_swap, console, + dashboard_url, from_rao, get_cli_context, hydrate_pending_swap, @@ -36,11 +36,9 @@ ) from allways.contract_client import ContractError -DEFAULT_DASHBOARD_URL = 'https://test.all-ways.io' - def _dashboard_url() -> str: - return os.environ.get('ALLWAYS_DASHBOARD_URL', DEFAULT_DASHBOARD_URL).rstrip('/') + return dashboard_url() @click.group('view', cls=StyledGroup)