diff --git a/allways/cli/swap_commands/admin.py b/allways/cli/swap_commands/admin.py index ebc08df..2d933ec 100644 --- a/allways/cli/swap_commands/admin.py +++ b/allways/cli/swap_commands/admin.py @@ -20,10 +20,9 @@ def admin_group(): set-min-swap Set minimum swap amount (0 = no minimum) set-max-swap Set maximum swap amount (0 = no maximum) set-votes Set required validator votes - set-recycle-address Set address for fee recycling transfers add-vali Add a validator remove-vali Remove a validator - recycle-fees Recycle accumulated fees (stake + burn alpha) + recycle-fees Stake accumulated fees on-chain via chain extension transfer-ownership Transfer contract ownership danger halt Halt the system (block new reservations) danger resume Resume the system (allow new reservations) @@ -411,42 +410,28 @@ def remove_vali(hotkey: str): console.print(f'[red]Failed to remove validator: {e}[/red]\n') -@admin_group.command('set-recycle-address') -@click.argument('account_id', type=str) -def set_recycle_address(account_id: str): - """Set the address where recycled fees are transferred. - - Example: - alw admin set-recycle-address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY - """ - _, wallet, _, client = get_cli_context() - - console.print('\n[bold]Set Recycle Address[/bold]\n') - console.print(f' Address: {account_id}\n') - - if not click.confirm('Confirm?'): - console.print('[yellow]Cancelled[/yellow]') - return - - try: - with loading('Submitting transaction...'): - client.set_recycle_address(wallet=wallet, address=account_id) - console.print(f'[green]Recycle address set to {account_id}[/green]\n') - except ContractError as e: - console.print(f'[red]Failed to set recycle address: {e}[/red]\n') - - @admin_group.command('recycle-fees') def recycle_fees(): - """Transfer accumulated fees to the designated recycle address. + """Stake accumulated fees on-chain via chain extension. Example: alw admin recycle-fees """ _, wallet, _, client = get_cli_context() + try: + accumulated = client.get_accumulated_fees() + except ContractError: + accumulated = None + + if accumulated is not None and accumulated == 0: + console.print('\n[yellow]No accumulated fees to recycle[/yellow]\n') + return + + fee_display = f'{from_rao(accumulated):.4f} TAO' if accumulated else 'unknown' console.print('\n[bold]Recycle Fees[/bold]\n') - console.print(' Action: transfer all accumulated fees to recycle address\n') + console.print(f' Accumulated fees: {fee_display}') + console.print(' Action: stake fees on-chain via chain extension\n') if not click.confirm('Confirm recycling fees?'): console.print('[yellow]Cancelled[/yellow]') @@ -457,7 +442,10 @@ def recycle_fees(): client.recycle_fees(wallet=wallet) console.print('[green]Fees recycled successfully[/green]\n') except ContractError as e: - console.print(f'[red]Failed to recycle fees: {e}[/red]\n') + console.print(f'[red]Failed to recycle fees: {e}[/red]') + if 'ContractReverted' in str(e): + console.print('[dim]Hint: treasury hotkey may not be registered on the subnet[/dim]') + console.print() @admin_group.command('transfer-ownership') diff --git a/allways/cli/swap_commands/view.py b/allways/cli/swap_commands/view.py index d1342b2..7a60514 100644 --- a/allways/cli/swap_commands/view.py +++ b/allways/cli/swap_commands/view.py @@ -430,7 +430,6 @@ def _read(fn, default=None): accumulated_fees_rao = _read(client.get_accumulated_fees) total_recycled_rao = _read(client.get_total_recycled_fees) owner = _read(client.get_owner) - recycle_address = _read(client.get_recycle_address, default=None) except ContractError as e: console.print(f'[red]Failed to read contract parameters: {e}[/red]') return @@ -457,8 +456,6 @@ def _read(fn, default=None): table.add_row('Accumulated Fees', f'{from_rao(accumulated_fees_rao):.4f} TAO') table.add_row('Total Recycled Fees', f'{from_rao(total_recycled_rao):.4f} TAO') table.add_row('Owner', owner) - if recycle_address: - table.add_row('Recycle Address', recycle_address) console.print(table) console.print() diff --git a/allways/contract_client.py b/allways/contract_client.py index 1c65ed9..9cfb62d 100644 --- a/allways/contract_client.py +++ b/allways/contract_client.py @@ -47,7 +47,6 @@ 'set_consensus_threshold': bytes.fromhex('c0d8ec47'), 'set_min_swap_amount': bytes.fromhex('800e1573'), 'set_max_swap_amount': bytes.fromhex('3e868f32'), - 'set_recycle_address': bytes.fromhex('50dfe685'), 'set_reservation_ttl': bytes.fromhex('3143d9e3'), 'set_fee_divisor': bytes.fromhex('8832de41'), 'recycle_fees': bytes.fromhex('97756ea1'), @@ -65,7 +64,6 @@ 'get_accumulated_fees': bytes.fromhex('bf3b5d4e'), 'get_total_recycled_fees': bytes.fromhex('9910e939'), 'get_owner': bytes.fromhex('07fcd0b1'), - 'get_recycle_address': bytes.fromhex('3847e06c'), 'get_pending_slash': bytes.fromhex('48c78c4a'), 'get_min_swap_amount': bytes.fromhex('fca7daa4'), 'get_max_swap_amount': bytes.fromhex('97826e04'), @@ -130,7 +128,6 @@ 'set_consensus_threshold': [('percent', 'u8')], 'set_min_swap_amount': [('amount', 'u128')], 'set_max_swap_amount': [('amount', 'u128')], - 'set_recycle_address': [('address', 'AccountId')], 'set_reservation_ttl': [('blocks', 'u32')], 'set_fee_divisor': [('divisor', 'u128')], 'recycle_fees': [], @@ -243,6 +240,7 @@ class ContractErrorKind(Enum): 24: ('HashMismatch', 'Request hash does not match computed hash'), 25: ('PendingConflict', 'A pending vote exists for a different request'), 26: ('SameChain', 'Source and destination chains must be different'), + 27: ('SystemHalted', 'System is halted — no new activity allowed'), } @@ -843,9 +841,6 @@ def get_owner(self) -> str: def get_halted(self) -> bool: return self._read_bool('get_halted') - def get_recycle_address(self) -> str: - return self._read_account_id('get_recycle_address') - def is_validator(self, account: str) -> bool: return self._read_bool('is_validator', {'account': account}) @@ -1112,12 +1107,6 @@ def set_min_swap_amount(self, wallet: bt.Wallet, amount_rao: int) -> str: bt.logging.info(f'Min swap amount set to {amount_rao}: {tx_hash}') return tx_hash - def set_recycle_address(self, wallet: bt.Wallet, address: str) -> str: - self._ensure_initialized() - tx_hash = self._exec_contract_raw('set_recycle_address', args={'address': address}, keypair=wallet.hotkey) - bt.logging.info(f'Recycle address set to {address}: {tx_hash}') - return tx_hash - def set_reservation_ttl(self, wallet: bt.Wallet, blocks: int) -> str: self._ensure_initialized() tx_hash = self._exec_contract_raw('set_reservation_ttl', args={'blocks': blocks}, keypair=wallet.hotkey) diff --git a/allways/metadata/allways_swap_manager.json b/allways/metadata/allways_swap_manager.json index 7b94053..306916e 100644 --- a/allways/metadata/allways_swap_manager.json +++ b/allways/metadata/allways_swap_manager.json @@ -1,6 +1,6 @@ { "source": { - "hash": "0x30b3f2dc49bdb3ba3843f7c0851a8036e6b4f89e76132515d88653301a41ff15", + "hash": "0x15206c9262e680d8c0d5ce92e3b880e2d4c0c30d6e2e489c706be5d3904405cc", "language": "ink! 5.1.1", "compiler": "rustc 1.91.1", "build_info": { @@ -6419,9 +6419,8 @@ "variant": {} }, "path": [ - "ink_env", - "types", - "NoChainExtension" + "allways_swap_manager", + "SubtensorExtension" ] } } diff --git a/smart-contracts/ink/errors.rs b/smart-contracts/ink/errors.rs index 33ea324..4812c78 100644 --- a/smart-contracts/ink/errors.rs +++ b/smart-contracts/ink/errors.rs @@ -58,6 +58,6 @@ pub enum Error { PendingConflict, /// Source and destination chains must be different SameChain, - /// System is halted — no new reservations allowed + /// System is halted — no new activity allowed SystemHalted, } diff --git a/smart-contracts/ink/events.rs b/smart-contracts/ink/events.rs index de9299d..b5dac19 100644 --- a/smart-contracts/ink/events.rs +++ b/smart-contracts/ink/events.rs @@ -118,7 +118,7 @@ pub struct ConfigUpdated { pub value: u128, } -/// Event emitted when accumulated fees are recycled (transferred to recycle address) +/// Event emitted when accumulated fees are staked on-chain via chain extension #[ink::event] pub struct FeesRecycled { pub tao_amount: u128, diff --git a/smart-contracts/ink/lib.rs b/smart-contracts/ink/lib.rs index 703cafb..02210b3 100644 --- a/smart-contracts/ink/lib.rs +++ b/smart-contracts/ink/lib.rs @@ -7,7 +7,48 @@ mod events; use types::{SwapData, SwapStatus, VoteType}; use errors::Error; -#[ink::contract] +#[ink::chain_extension(extension = 0x1000)] +pub trait SubtensorExtension { + type ErrorCode = SubtensorError; + + #[ink(function = 18)] + fn add_stake_recycle( + hotkey: ::AccountId, + netuid: u16, + amount: u64, + ) -> u64; +} + +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum SubtensorError { + ChainExtensionFailed, +} + +impl ink::env::chain_extension::FromStatusCode for SubtensorError { + fn from_status_code(status_code: u32) -> Result<(), Self> { + match status_code { + 0 => Ok(()), + _ => Err(Self::ChainExtensionFailed), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[ink::scale_derive(TypeInfo)] +pub enum CustomEnvironment {} + +impl ink::env::Environment for CustomEnvironment { + const MAX_EVENT_TOPICS: usize = + ::MAX_EVENT_TOPICS; + type AccountId = ::AccountId; + type Balance = ::Balance; + type Hash = ::Hash; + type Timestamp = ::Timestamp; + type BlockNumber = ::BlockNumber; + type ChainExtension = SubtensorExtension; +} + +#[ink::contract(env = crate::CustomEnvironment)] mod allways_swap_manager { use super::*; use events::*; @@ -21,7 +62,6 @@ mod allways_swap_manager { // Configuration owner: AccountId, treasury_hotkey: AccountId, - recycle_address: AccountId, netuid: u16, fulfillment_timeout_blocks: u32, reservation_ttl: u32, @@ -224,7 +264,6 @@ mod allways_swap_manager { #[ink(constructor)] pub fn new( treasury_hotkey: AccountId, - recycle_address: AccountId, netuid: u16, fulfillment_timeout_blocks: u32, reservation_ttl: u32, @@ -237,7 +276,6 @@ mod allways_swap_manager { Self { owner: Self::env().caller(), treasury_hotkey, - recycle_address, netuid, fulfillment_timeout_blocks, reservation_ttl, @@ -1082,13 +1120,6 @@ mod allways_swap_manager { Ok(()) } - #[ink(message)] - pub fn set_recycle_address(&mut self, address: AccountId) -> Result<(), Error> { - self.ensure_owner()?; - self.recycle_address = address; - Ok(()) - } - #[ink(message)] pub fn set_reservation_ttl(&mut self, blocks: u32) -> Result<(), Error> { self.ensure_owner()?; @@ -1134,7 +1165,10 @@ mod allways_swap_manager { return Err(Error::ZeroAmount); } - self.env().transfer(self.recycle_address, fees) + let amount: u64 = fees.try_into().map_err(|_| Error::TransferFailed)?; + self.env() + .extension() + .add_stake_recycle(self.treasury_hotkey, self.netuid, amount) .map_err(|_| Error::TransferFailed)?; self.accumulated_fees = 0; @@ -1222,11 +1256,6 @@ mod allways_swap_manager { self.halted } - #[ink(message)] - pub fn get_recycle_address(&self) -> AccountId { - self.recycle_address - } - #[ink(message)] pub fn get_pending_slash(&self, swap_id: u64) -> Balance { self.pending_slashes.get(swap_id).map(|(_, amount)| amount).unwrap_or(0)