diff --git a/alembic/versions/2026_07_07_alpha_burn_payments.py b/alembic/versions/2026_07_07_alpha_burn_payments.py new file mode 100644 index 00000000..4bb094e6 --- /dev/null +++ b/alembic/versions/2026_07_07_alpha_burn_payments.py @@ -0,0 +1,58 @@ +"""Add alpha burn payment columns + +Revision ID: c8f41e9a2b37 +Revises: e7f3a1b2c905 +Create Date: 2026-07-07 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +revision: str = "c8f41e9a2b37" +down_revision: Union[str, Sequence[str], None] = "e7f3a1b2c905" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +_CHECK_NAME = "ck_amount_rao_xor_amount_alpha_rao" +_CHECK_SQL = "num_nonnulls(amount_rao, amount_alpha_rao) = 1" + + +def upgrade() -> None: + op.add_column("evaluation_payments", sa.Column("amount_alpha_rao", sa.BigInteger(), nullable=True)) + op.add_column("upload_payment_quotes", sa.Column("amount_alpha_rao", sa.BigInteger(), nullable=True)) + op.alter_column("evaluation_payments", "amount_rao", existing_type=sa.Integer(), nullable=True) + op.alter_column("upload_payment_quotes", "amount_rao", existing_type=sa.BigInteger(), nullable=True) + op.alter_column("upload_payment_quotes", "send_address", existing_type=sa.Text(), nullable=True) + op.create_check_constraint(_CHECK_NAME, "evaluation_payments", _CHECK_SQL) + op.create_check_constraint(_CHECK_NAME, "upload_payment_quotes", _CHECK_SQL) + + +def downgrade() -> None: + alpha_row_count = op.get_bind().scalar( + sa.text( + """ + SELECT + (SELECT count(*) FROM upload_payment_quotes WHERE amount_alpha_rao IS NOT NULL) + + + (SELECT count(*) FROM evaluation_payments WHERE amount_alpha_rao IS NOT NULL) + """ + ) + ) + if alpha_row_count: + raise RuntimeError( + "Cannot downgrade alpha burn payments while alpha-denominated quotes or payments exist; " + "the legacy schema cannot represent those rows." + ) + + op.drop_constraint(_CHECK_NAME, "upload_payment_quotes", type_="check") + op.drop_constraint(_CHECK_NAME, "evaluation_payments", type_="check") + op.alter_column("upload_payment_quotes", "send_address", existing_type=sa.Text(), nullable=False) + op.alter_column("upload_payment_quotes", "amount_rao", existing_type=sa.BigInteger(), nullable=False) + op.alter_column("evaluation_payments", "amount_rao", existing_type=sa.Integer(), nullable=False) + op.drop_column("upload_payment_quotes", "amount_alpha_rao") + op.drop_column("evaluation_payments", "amount_alpha_rao") diff --git a/api/config.py b/api/config.py index 46f51840..a6b5c977 100644 --- a/api/config.py +++ b/api/config.py @@ -25,10 +25,7 @@ # Load Bittensor configuration -NETUID = os.getenv("NETUID") -if not NETUID: - logger.fatal("NETUID is not set in .env") -NETUID = int(NETUID) +NETUID = int(os.getenv("NETUID") or "62") SUBTENSOR_ADDRESS = os.getenv("SUBTENSOR_ADDRESS") if not SUBTENSOR_ADDRESS: diff --git a/api/src/endpoints/upload.py b/api/src/endpoints/upload.py index 90cdc742..870f5379 100644 --- a/api/src/endpoints/upload.py +++ b/api/src/endpoints/upload.py @@ -16,12 +16,15 @@ check_agent_banned, check_file_size, check_hotkey_registered, + check_if_extrinsic_failed, check_if_python_file, check_rate_limit, check_signature, + find_alpha_burned_event, + get_alpha_price, get_miner_hotkey, - get_tao_price, timestamp_ms_to_utc_datetime, + verify_burn_extrinsic, ) from models.agent import Agent, AgentCreate, AgentStatus from models.upload import AgentCheckResponse, AgentUploadResponse, ErrorResponse, UploadPriceResponse @@ -94,12 +97,29 @@ async def check_agent_post( check_if_python_file(agent_file.filename) await check_file_size(agent_file) coldkey = await subtensor_client.get_hotkey_owner(miner_hotkey) - miner_balance = (await subtensor_client.get_balance(coldkey)).rao + if coldkey is None: + raise HTTPException(status_code=400, detail="Hotkey owner not found") + + try: + alpha_stake = await subtensor_client.get_alpha_stake_availability( + coldkey=coldkey, + hotkey=miner_hotkey, + netuid=config.NETUID, + ) + except Exception as e: + logger.error(f"Error retrieving burnable alpha stake: {e}") + raise HTTPException(status_code=503, detail="Burnable alpha stake could not be verified") from e + payment_cost = await get_upload_price() - if payment_cost.amount_rao > miner_balance: + if payment_cost.amount_alpha_rao > alpha_stake.burnable_rao: raise HTTPException( status_code=402, - detail=f"Insufficient balance. You need {payment_cost.amount_rao} RAO to upload this agent. You have {miner_balance} RAO.", + detail=( + f"Insufficient alpha. You need {payment_cost.amount_alpha_rao} alpha (1e9 units) " + f"burnable from the miner hotkey position on SN{config.NETUID}. " + f"Position: {alpha_stake.position_rao}; subnet total: {alpha_stake.total_rao}; " + f"locked: {alpha_stake.locked_rao}; burnable: {alpha_stake.burnable_rao}." + ), ) await validate_openrouter_keys( openrouter_api_key=openrouter_api_key, @@ -108,16 +128,15 @@ async def check_agent_post( expires_at = datetime.now(timezone.utc) + timedelta(seconds=UPLOAD_PAYMENT_QUOTE_TTL_SECONDS) quote = await create_payment_quote( miner_hotkey=miner_hotkey, - amount_rao=payment_cost.amount_rao, - send_address=payment_cost.send_address, + amount_alpha_rao=payment_cost.amount_alpha_rao, expires_at=expires_at, ) return AgentCheckResponse( status="success", message="Agent check successful", quote_id=quote.quote_id, - amount_rao=quote.amount_rao, - send_address=quote.send_address, + amount_alpha_rao=quote.amount_alpha_rao, + payment_netuid=config.NETUID, expires_at=quote.expires_at, ) @@ -217,6 +236,9 @@ async def post_agent( if quote.miner_hotkey != miner_hotkey: raise HTTPException(status_code=402, detail="Payment quote does not match upload hotkey") + if quote.amount_alpha_rao is None: + raise HTTPException(status_code=400, detail=OUTDATED_UPLOAD_CLIENT_MESSAGE) + existing_payment = await retrieve_payment_by_hash( payment_block_hash=payment_block_hash, payment_extrinsic_index=payment_extrinsic_index ) @@ -231,7 +253,7 @@ async def post_agent( logger.warning(f"Payment with block hash {payment_block_hash} has been refunded. Rejecting upload.") raise PaymentRefunded() - # Retrieve payment details from the chain + # Retrieve the burn block + events from the chain try: payment_block_info = await subtensor_client.get_block_info(block_hash=payment_block_hash) except Exception as e: @@ -241,45 +263,40 @@ async def post_agent( if payment_block_info is None: raise HTTPException(status_code=402, detail="Payment block not found") - coldkey = await subtensor_client.get_hotkey_owner(miner_hotkey, block=int(payment_block_info.number)) try: payment_extrinsic_index_int = int(payment_extrinsic_index) if payment_extrinsic_index_int < 0: raise ValueError payment_extrinsic = payment_block_info.extrinsics[payment_extrinsic_index_int] - payment_extrinsic_value = payment_extrinsic.value_serialized - payment_call = payment_extrinsic_value["call"] - call_args = {arg["name"]: arg["value"] for arg in payment_call["call_args"]} - payment_value = call_args.get("value") - destination = call_args.get("dest") - payment_address = payment_extrinsic_value["address"] - except (ValueError, TypeError, IndexError, KeyError, AttributeError): - raise HTTPException(status_code=402, detail="Payment extrinsic could not be decoded") from None - - if ( - payment_call.get("call_module") != "Balances" - or payment_call.get("call_function") != "transfer_keep_alive" - ): - raise HTTPException(status_code=402, detail="Payment extrinsic is not a TAO transfer") + except (ValueError, TypeError, IndexError, AttributeError): + raise HTTPException(status_code=402, detail="Burn extrinsic could not be decoded") from None - if payment_value is None or await check_if_extrinsic_failed( - payment_block_hash, payment_extrinsic_index_int - ): - raise HTTPException(status_code=402, detail="Payment value not found") + events = await subtensor_client.get_events(block_hash=payment_block_hash) + if await check_if_extrinsic_failed(payment_extrinsic_index_int, events): + raise HTTPException(status_code=402, detail="Burn extrinsic failed on-chain") + + coldkey = await subtensor_client.get_hotkey_owner(miner_hotkey, block=int(payment_block_info.number)) - # Make sure coldkey is the same as hotkeys owner coldkey - if coldkey != payment_address: + # Cross-check the extrinsic: recognized burn call signed by the miner coldkey. + verify_burn_extrinsic(payment_extrinsic, expected_coldkey=coldkey) + + # Event is the source of truth for amount, netuid, and burner. + burn_event = find_alpha_burned_event( + events, + payment_extrinsic_index_int, + netuid=config.NETUID, + ) + + if burn_event.coldkey != coldkey: raise HTTPException(status_code=402, detail="Coldkey does not match") - # Make sure destination is our upload send address - if destination != quote.send_address: - raise HTTPException( - status_code=402, - detail=f"Destination does not match. The payment should be sent to {quote.send_address}", - ) + if burn_event.hotkey != miner_hotkey: + raise HTTPException(status_code=402, detail="Hotkey does not match") + + if burn_event.alpha_decrease < quote.amount_alpha_rao: + raise HTTPException(status_code=402, detail="Burn amount too low") - if payment_value != quote.amount_rao: - raise HTTPException(status_code=402, detail="Payment amount does not match") + payment_value = burn_event.alpha_decrease payment_block_time = timestamp_ms_to_utc_datetime(payment_block_info.timestamp) if not (as_utc(quote.created_at) <= payment_block_time <= as_utc(quote.expires_at)): @@ -307,7 +324,7 @@ async def post_agent( payment_extrinsic_index=payment_extrinsic_index, miner_hotkey=miner_hotkey, miner_coldkey=coldkey, - amount_rao=payment_value, + amount_alpha_rao=payment_value, quote_id=quote.quote_id, ) @@ -411,30 +428,15 @@ async def post_agent( @router.get("/eval-pricing", tags=["eval-pricing"], response_model=UploadPriceResponse) @hourly_cache() async def get_upload_price() -> UploadPriceResponse: - TAO_PRICE = await get_tao_price() + ALPHA_PRICE = await get_alpha_price(config.NETUID) eval_cost_usd = 5 - # Get the amount of tao required per eval - eval_cost_tao = eval_cost_usd / TAO_PRICE - - # Add a buffer against price fluctuations and eval cost variance. If this is over, we burn the difference. Determined EoD by net eval charges - net amount received - # This also makes production evals more expensive than local by a good margin to discourage testing in production and variance farming - amount_rao = int(eval_cost_tao * 1e9 * 1.4) - - return UploadPriceResponse(amount_rao=amount_rao, send_address=config.UPLOAD_SEND_ADDRESS) - - -async def check_if_extrinsic_failed(block_hash: str, extrinsic_index: int) -> bool: - events = await subtensor_client.get_events(block_hash=block_hash) - - for event in events: - if event.get("extrinsic_idx") != extrinsic_index: - continue - - module = event["event"]["module_id"] - event_id = event["event"]["event_id"] + # Alpha required to cover the eval cost at the current alpha price. + eval_cost_alpha = eval_cost_usd / ALPHA_PRICE - if module == "System" and event_id == "ExtrinsicFailed": - return True + # 1.1x buffer. Burned alpha is destroyed (not reclaimable), so the buffer absorbs + # alpha-price movement between quote and burn while keeping prod uploads a bit more + # expensive than local testing to discourage variance farming. + amount_alpha_rao = int(eval_cost_alpha * 1e9 * 1.1) - return False + return UploadPriceResponse(amount_alpha_rao=amount_alpha_rao, payment_netuid=config.NETUID) diff --git a/api/src/utils/upload_agent_helpers.py b/api/src/utils/upload_agent_helpers.py index 6ba8b91d..18e189da 100644 --- a/api/src/utils/upload_agent_helpers.py +++ b/api/src/utils/upload_agent_helpers.py @@ -1,5 +1,7 @@ import logging +from dataclasses import dataclass from datetime import datetime, timedelta, timezone +from typing import Any import httpx from bittensor_wallet.keypair import Keypair @@ -12,6 +14,17 @@ logger = logging.getLogger(__name__) +@dataclass(slots=True, frozen=True) +class BurnEvent: + coldkey: str + hotkey: str + alpha_decrease: int + netuid: int + + +BURN_CALL_FUNCTIONS = frozenset({"burn_alpha", "add_stake_burn"}) + + def get_miner_hotkey(file_info: str) -> str: logger.debug(f"Getting miner hotkey from file info: {file_info}.") miner_hotkey = file_info.split(":")[0] @@ -47,7 +60,9 @@ async def check_agent_banned(miner_hotkey: str) -> None: logger.debug(f"Miner hotkey {miner_hotkey} is not banned.") -def check_rate_limit(latest_agent_created_at_in_latest_set_id: datetime) -> None: +def check_rate_limit( + latest_agent_created_at_in_latest_set_id: datetime, +) -> None: logger.debug("Checking if miner is rate limited...") earliest_allowed_time = latest_agent_created_at_in_latest_set_id + timedelta( @@ -75,7 +90,10 @@ def timestamp_ms_to_utc_datetime(timestamp_ms: int | None) -> datetime: try: return datetime.fromtimestamp(int(timestamp_ms) / 1000, timezone.utc) except (TypeError, ValueError): - raise HTTPException(status_code=402, detail="Payment block timestamp could not be decoded") from None + raise HTTPException( + status_code=402, + detail="Payment block timestamp could not be decoded", + ) from None def as_utc(dt: datetime) -> datetime: @@ -93,7 +111,10 @@ def check_signature(public_key: str, file_info: str, signature: str, miner_hotke logger.error( f"Attempt to upload an agent with a public key that does not correspond to the miner hotkey. Public key ss58 address: {keypair.ss58_address}, Miner hotkey: {miner_hotkey}." ) - raise HTTPException(status_code=400, detail="Public key does not correspond to miner hotkey") + raise HTTPException( + status_code=400, + detail="Public key does not correspond to miner hotkey", + ) if not keypair.verify(file_info, bytes.fromhex(signature)): logger.error( @@ -149,3 +170,130 @@ async def get_tao_price() -> float: data = r.json() return data["bittensor"]["usd"] + + +async def get_alpha_price(netuid: int) -> float: + """Return a subnet's alpha price in USD: alpha/TAO price times TAO/USD price.""" + alpha_tao = await subtensor_client.get_alpha_price_tao(netuid=netuid) + tao_usd = await get_tao_price() + return alpha_tao * tao_usd + + +async def check_if_extrinsic_failed(extrinsic_index: int, events: list) -> bool: + """Validate if the extrinsic failed based on the events. + + Parameters + ---------- + extrinsic_index : int + Index of the extrinsic in the block. + events : list + List of events to check. + + Returns + ------- + bool + True if the extrinsic failed, False otherwise. + """ + logger.debug(f"Checking if extrinsic at index {extrinsic_index} failed based on events...") + for event in events: + if event.get("extrinsic_idx") != extrinsic_index: + continue + + module = event["event"]["module_id"] + event_id = event["event"]["event_id"] + + if module == "System" and event_id == "ExtrinsicFailed": + return True + + return False + + +def _parse_alpha_burned_attributes(attributes: list | tuple | dict) -> BurnEvent: + """Parse SubtensorModule.AlphaBurned event attributes into a BurnEvent. + + AsyncSubtensor/substrate-interface decodes this event as a positional tuple/list (coldkey, hotkey, actual_alpha_decrease, netuid) rather than named attributes. + + Named attributes are also supported as a fallback. + + Parameters + ---------- + attributes : list|tuple|dict + Event attributes, either as a tuple/list or a dict. + + Returns + ------- + BurnEvent + Parsed burn event with coldkey, hotkey, alpha_decrease, and netuid. + + """ + if isinstance(attributes, (tuple, list)) and len(attributes) >= 4: + coldkey, hotkey, alpha_decrease, netuid = attributes[:4] + return BurnEvent(coldkey=coldkey, hotkey=hotkey, alpha_decrease=int(alpha_decrease), netuid=int(netuid)) + if isinstance(attributes, dict): + try: + return BurnEvent( + coldkey=attributes["Coldkey"], + hotkey=attributes["Hotkey"], + alpha_decrease=int(attributes["Actual Alpha Decrease"]), + netuid=int(attributes["Netuid"]), + ) + except (KeyError, TypeError, ValueError): + raise HTTPException(status_code=402, detail="Burn event attributes could not be decoded") from None + raise HTTPException(status_code=402, detail="Burn event attributes could not be decoded") + + +def find_alpha_burned_event(events: list, extrinsic_index: int, netuid: int) -> BurnEvent: + """Find the AlphaBurned event in the events list and returned a parsed + BurnEvent. + + Parameters + ---------- + events : list + List of events to search for the AlphaBurned event. + extrinsic_index : int + Index of the extrinsic to search for. + netuid : int + Netuid the burn must be on. + + Returns + ------- + BurnEvent + Parsed burn event with coldkey, hotkey, alpha_decrease, and netuid. + """ + for event in events: + if event.get("extrinsic_idx") != extrinsic_index: + continue + inner = event.get("event", {}) + if inner.get("module_id") == "SubtensorModule" and inner.get("event_id") == "AlphaBurned": + alpha_burned_event = _parse_alpha_burned_attributes(inner.get("attributes", {})) + if alpha_burned_event.netuid != netuid: + continue + logger.debug(f"Found AlphaBurned event: {alpha_burned_event}") + return alpha_burned_event + raise HTTPException(status_code=402, detail="Burn event not found") + + +def verify_burn_extrinsic(extrinsic: Any, expected_coldkey: str) -> None: + """Validate that the extrinsic is a recognized alpha burn and that it was signed by the expected miner coldkey. + + Parameters + ---------- + extrinsic : Any + Extrinsic data to validate. + expected_coldkey : str + The expected coldkey of the miner. + """ + try: + value = extrinsic.value_serialized + call = value["call"] + signer = value["address"] + except (KeyError, TypeError, AttributeError): + raise HTTPException(status_code=402, detail="Burn extrinsic could not be decoded") from None + logger.debug("Verifying call module and function for burn extrinsic...") + if call.get("call_module") != "SubtensorModule" or call.get("call_function") not in BURN_CALL_FUNCTIONS: + raise HTTPException(status_code=402, detail="Extrinsic is not a recognized alpha burn") + + logger.debug("Verifying that the burn extrinsic was signed by the expected miner coldkey...") + if signer != expected_coldkey: + raise HTTPException(status_code=402, detail="Burn was not signed by the miner coldkey") + logger.debug("Burn extrinsic is valid and signed by the expected miner coldkey.") diff --git a/db/models/payment.py b/db/models/payment.py index 949d331c..eaf48fa4 100644 --- a/db/models/payment.py +++ b/db/models/payment.py @@ -28,9 +28,16 @@ class EvaluationPayment(Base, CreatedAtMixin): ) miner_hotkey: Mapped[str] = mapped_column(sa.Text, nullable=False) miner_coldkey: Mapped[str] = mapped_column(sa.Text, nullable=False) - amount_rao: Mapped[int] = mapped_column(sa.Integer, nullable=False) - - __table_args__ = (sa.PrimaryKeyConstraint("payment_block_hash", "payment_extrinsic_index"),) + amount_rao: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + amount_alpha_rao: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True) + + __table_args__ = ( + sa.PrimaryKeyConstraint("payment_block_hash", "payment_extrinsic_index"), + sa.CheckConstraint( + "num_nonnulls(amount_rao, amount_alpha_rao) = 1", + name="ck_amount_rao_xor_amount_alpha_rao", + ), + ) class UploadPaymentQuote(Base, CreatedAtMixin): @@ -42,6 +49,14 @@ class UploadPaymentQuote(Base, CreatedAtMixin): server_default=sa.text("gen_random_uuid()"), ) miner_hotkey: Mapped[str] = mapped_column(sa.Text, nullable=False) - amount_rao: Mapped[int] = mapped_column(sa.BigInteger, nullable=False) - send_address: Mapped[str] = mapped_column(sa.Text, nullable=False) + amount_rao: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True) + amount_alpha_rao: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True) + send_address: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True) expires_at: Mapped[datetime] = mapped_column(sa.TIMESTAMP(timezone=True), nullable=False) + + __table_args__ = ( + sa.CheckConstraint( + "num_nonnulls(amount_rao, amount_alpha_rao) = 1", + name="ck_amount_rao_xor_amount_alpha_rao", + ), + ) diff --git a/miners/cli/commands/upload.py b/miners/cli/commands/upload.py index 9a8194d5..0e1f9ac6 100644 --- a/miners/cli/commands/upload.py +++ b/miners/cli/commands/upload.py @@ -194,11 +194,13 @@ def _unlock_coldkey(wallet) -> None: def _confirm_payment(payment_method_details: dict) -> bool: + amount_alpha = payment_method_details["amount_alpha_rao"] / 1e9 + payment_netuid = payment_method_details["payment_netuid"] confirm_payment = Prompt.ask( ( - f"\n[bold yellow]Proceed with payment of {payment_method_details['amount_rao']} RAO " - f"({payment_method_details['amount_rao'] / 1e9} TAO) to " - f"{payment_method_details['send_address']}?[/bold yellow]" + f"\n[bold yellow]Proceed with an IRREVERSIBLE burn of {amount_alpha:,.4f} alpha " + f"({payment_method_details['amount_alpha_rao']} in 1e9 units) " + f"on SN{payment_netuid}?[/bold yellow]" ), choices=["y", "n"], default="n", @@ -211,11 +213,12 @@ def _submit_eval_payment(*, wallet, payment_method_details: dict) -> PaymentRece subtensor = Subtensor(network=os.environ.get("SUBTENSOR_NETWORK", "finney")) payment_payload = subtensor.substrate.compose_call( - call_module="Balances", - call_function="transfer_keep_alive", + call_module="SubtensorModule", + call_function="burn_alpha", call_params={ - "dest": payment_method_details["send_address"], - "value": payment_method_details["amount_rao"], + "hotkey": wallet.hotkey.ss58_address, + "amount": payment_method_details["amount_alpha_rao"], + "netuid": payment_method_details["payment_netuid"], }, ) @@ -224,6 +227,10 @@ def _submit_eval_payment(*, wallet, payment_method_details: dict) -> PaymentRece keypair=wallet.coldkey, ) receipt = subtensor.substrate.submit_extrinsic(payment_extrinsic, wait_for_finalization=True) + if not receipt.is_success: + error_message = receipt.error_message or "Unknown chain error" + raise click.ClickException(f"Alpha burn failed on-chain: {error_message}") + return PaymentReceipt( block_hash=receipt.block_hash, extrinsic_index=receipt.extrinsic_idx, @@ -233,8 +240,8 @@ def _submit_eval_payment(*, wallet, payment_method_details: dict) -> PaymentRece def _print_payment_receipt(receipt: PaymentReceipt) -> None: console.print( - "\n[yellow]Payment extrinsic submitted. If something goes wrong with the upload, " - "you can use this information to get a refund[/yellow]" + "\n[yellow]Burn extrinsic submitted. This fee is not refundable and burns are irreversible; " + "if the upload fails, use this info with `ridges resume-upload` to retry[/yellow]" ) if receipt.quote_id: console.print(f"[cyan]Payment Quote ID:[/cyan] {receipt.quote_id}") diff --git a/models/payments.py b/models/payments.py index 7c92b904..2acc3d9a 100644 --- a/models/payments.py +++ b/models/payments.py @@ -14,7 +14,8 @@ class Payment(BaseModel): miner_hotkey: str miner_coldkey: str - amount_rao: int + amount_rao: Optional[int] = None + amount_alpha_rao: Optional[int] = None created_at: datetime @@ -22,7 +23,8 @@ class Payment(BaseModel): class PaymentQuote(BaseModel): quote_id: UUID miner_hotkey: str - amount_rao: int - send_address: str + amount_rao: Optional[int] = None + amount_alpha_rao: Optional[int] = None + send_address: Optional[str] = None created_at: datetime expires_at: datetime diff --git a/models/upload.py b/models/upload.py index 20bc7dc7..296bae01 100644 --- a/models/upload.py +++ b/models/upload.py @@ -14,17 +14,17 @@ class AgentUploadResponse(BaseModel): class UploadPriceResponse(BaseModel): """Response model for upload pricing""" - amount_rao: int = Field(..., description="Amount to send for evaluation (in RAO)") - send_address: str = Field(..., description="TAO address to send evaluation payment to") + amount_alpha_rao: int = Field(..., description="Amount of SN62 alpha to burn (in 1e9 units)") + payment_netuid: int = Field(..., description="Subnet whose alpha must be burned") class AgentCheckResponse(AgentUploadResponse): """Response model for successful agent upload preflight checks""" quote_id: UUID = Field(..., description="Quote ID to include when uploading or resuming") - amount_rao: int = Field(..., description="Amount to send for evaluation (in RAO)") - send_address: str = Field(..., description="TAO address to send evaluation payment to") - expires_at: datetime = Field(..., description="Latest on-chain payment timestamp accepted for this quote") + amount_alpha_rao: int = Field(..., description="Amount of SN62 alpha to burn (in 1e9 units)") + payment_netuid: int = Field(..., description="Subnet whose alpha must be burned") + expires_at: datetime = Field(..., description="Latest on-chain burn timestamp accepted for this quote") class ErrorResponse(BaseModel): diff --git a/pyproject.toml b/pyproject.toml index 3029e601..4bfdb294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "alembic>=1.13", "asgi-correlation-id>=4.3.4", "asyncpg>=0.30.0", - "bittensor==10.2.1", + "bittensor==10.5.0", "click>=8.1.0", "cryptography==46.0.0", "docker>=7.1.0", diff --git a/queries/payments.py b/queries/payments.py index d7b4906e..a9ca2923 100644 --- a/queries/payments.py +++ b/queries/payments.py @@ -13,7 +13,7 @@ async def reserve_payment( payment_extrinsic_index: str, miner_hotkey: str, miner_coldkey: str, - amount_rao: int, + amount_alpha_rao: int, quote_id: Optional[UUID] = None, ) -> Optional[Payment]: """Reserve a payment for an upload agent operation. It creates a new payment record with the given details, but with a NULL agent_id or it retrieves an existing payment row. The payment is considered reserved until the agent_id is set, which happens when the upload is completed. @@ -30,8 +30,8 @@ async def reserve_payment( Hotkey of the miner. miner_coldkey : str Coldkey of the miner. - amount_rao : int - Amount of RAO associated with the payment. + amount_alpha_rao : int + Amount of SN62 alpha (1e9 units) burned. quote_id : Optional[UUID], optional Server-issued upload payment quote used to validate the payment. @@ -48,7 +48,7 @@ async def reserve_payment( agent_id, miner_hotkey, miner_coldkey, - amount_rao, + amount_alpha_rao, quote_id ) VALUES ($1, $2, NULL, $3, $4, $5, $6) ON CONFLICT DO NOTHING @@ -57,7 +57,7 @@ async def reserve_payment( payment_extrinsic_index, miner_hotkey, miner_coldkey, - amount_rao, + amount_alpha_rao, quote_id, ) return await retrieve_payment_by_hash( @@ -130,23 +130,20 @@ async def retrieve_payment_by_hash( async def create_payment_quote( conn: DatabaseConnection, miner_hotkey: str, - amount_rao: int, - send_address: str, + amount_alpha_rao: int, expires_at: datetime, ) -> PaymentQuote: result = await conn.fetchrow( """ INSERT INTO upload_payment_quotes ( miner_hotkey, - amount_rao, - send_address, + amount_alpha_rao, expires_at - ) VALUES ($1, $2, $3, $4) + ) VALUES ($1, $2, $3) RETURNING * """, miner_hotkey, - amount_rao, - send_address, + amount_alpha_rao, expires_at, ) return PaymentQuote(**result) diff --git a/tests/api/test_bittensor.py b/tests/api/test_bittensor.py new file mode 100644 index 00000000..bf13e6b1 --- /dev/null +++ b/tests/api/test_bittensor.py @@ -0,0 +1,89 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from utils.bittensor import SubtensorClient + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("position_rao", "total_rao", "locked_rao", "available_rao", "expected_burnable"), + [ + (5_000, 20_000, 0, 20_000, 5_000), + (20_000, 30_000, 25_000, 5_000, 5_000), + (20_000, 20_000, 25_000, 0, 0), + ], +) +async def test_alpha_stake_availability_applies_position_and_subnet_lock_limits( + position_rao: int, + total_rao: int, + locked_rao: int, + available_rao: int, + expected_burnable: int, +) -> None: + block_hash = "0xchain-head" + netuid = 62 + coldkey = "coldkey" + hotkey = "hotkey" + + substrate = SimpleNamespace(get_chain_head=AsyncMock(return_value=block_hash)) + fake_subtensor = SimpleNamespace( + substrate=substrate, + get_stake=AsyncMock(return_value=SimpleNamespace(rao=position_rao)), + get_stake_availability_for_coldkeys=AsyncMock( + return_value={ + coldkey: { + netuid: { + "total": total_rao, + "locked": locked_rao, + "available": available_rao, + } + } + } + ), + ) + client = SubtensorClient() + client._subtensor = fake_subtensor + + result = await client.get_alpha_stake_availability(coldkey=coldkey, hotkey=hotkey, netuid=netuid) + + assert result.block_hash == block_hash + assert result.position_rao == position_rao + assert result.total_rao == total_rao + assert result.locked_rao == locked_rao + assert result.available_rao == available_rao + assert result.burnable_rao == expected_burnable + fake_subtensor.get_stake.assert_awaited_once_with( + coldkey_ss58=coldkey, + hotkey_ss58=hotkey, + netuid=netuid, + block_hash=block_hash, + ) + fake_subtensor.get_stake_availability_for_coldkeys.assert_awaited_once_with( + [coldkey], + netuids=[netuid], + block_hash=block_hash, + ) + + +@pytest.mark.anyio +async def test_alpha_stake_availability_treats_omitted_zero_subnet_as_zero() -> None: + block_hash = "0xchain-head" + coldkey = "coldkey" + netuid = 62 + fake_subtensor = SimpleNamespace( + substrate=SimpleNamespace(get_chain_head=AsyncMock(return_value=block_hash)), + get_stake=AsyncMock(return_value=SimpleNamespace(rao=0)), + get_stake_availability_for_coldkeys=AsyncMock(return_value={coldkey: {}}), + ) + client = SubtensorClient() + client._subtensor = fake_subtensor + + result = await client.get_alpha_stake_availability(coldkey=coldkey, hotkey="hotkey", netuid=netuid) + + assert result.position_rao == 0 + assert result.total_rao == 0 + assert result.locked_rao == 0 + assert result.available_rao == 0 + assert result.burnable_rao == 0 diff --git a/tests/api/test_config.py b/tests/api/test_config.py new file mode 100644 index 00000000..2c8a75fc --- /dev/null +++ b/tests/api/test_config.py @@ -0,0 +1,24 @@ +import os +import subprocess +import sys +from pathlib import Path + + +def test_netuid_defaults_to_62_when_environment_value_is_missing() -> None: + env = os.environ.copy() + env.pop("NETUID", None) + repo_root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [ + sys.executable, + "-c", + "import dotenv; dotenv.load_dotenv = lambda: False; import api.config; print(api.config.NETUID)", + ], + cwd=repo_root, + env=env, + check=True, + capture_output=True, + text=True, + ) + + assert result.stdout.strip() == "62" diff --git a/tests/api/test_openrouter_secrets.py b/tests/api/test_openrouter_secrets.py index 53ea219e..60c85a23 100644 --- a/tests/api/test_openrouter_secrets.py +++ b/tests/api/test_openrouter_secrets.py @@ -122,10 +122,22 @@ async def _fake_get_hotkey_owner(*args, **kwargs): async def _fake_get_balance(*args, **kwargs): return SimpleNamespace(rao=10**12) + async def _fake_get_alpha_stake_availability(*args, **kwargs): + return SimpleNamespace( + position_rao=10**12, + total_rao=10**12, + locked_rao=0, + burnable_rao=10**12, + ) + monkeypatch.setattr( upload_endpoint, "subtensor_client", - SimpleNamespace(get_hotkey_owner=_fake_get_hotkey_owner, get_balance=_fake_get_balance), + SimpleNamespace( + get_hotkey_owner=_fake_get_hotkey_owner, + get_balance=_fake_get_balance, + get_alpha_stake_availability=_fake_get_alpha_stake_availability, + ), ) async def fake_validate_openrouter_keys(*, openrouter_api_key: str | None, openrouter_management_key: str | None): @@ -152,14 +164,13 @@ async def fake_check_agent_banned(*, miner_hotkey: str) -> None: return None async def fake_get_upload_price(*args, **kwargs): - return SimpleNamespace(amount_rao=1, send_address="send-address") + return SimpleNamespace(amount_alpha_rao=1, payment_netuid=upload_endpoint.config.NETUID) - async def fake_create_payment_quote(*, miner_hotkey: str, amount_rao: int, send_address: str, expires_at): + async def fake_create_payment_quote(*, miner_hotkey: str, amount_alpha_rao: int, expires_at): return SimpleNamespace( quote_id=uuid4(), miner_hotkey=miner_hotkey, - amount_rao=amount_rao, - send_address=send_address, + amount_alpha_rao=amount_alpha_rao, expires_at=expires_at, ) diff --git a/tests/api/test_upload.py b/tests/api/test_upload.py index 0a30df8d..a0c23622 100644 --- a/tests/api/test_upload.py +++ b/tests/api/test_upload.py @@ -24,8 +24,7 @@ FAKE_EXTRINSIC_INDEX = "1" FAKE_HOTKEY = "5FHneTesthKey123" FAKE_COLDKEY = "5FColdKey456" -FAKE_AMOUNT_RAO = 100_000_000 -FAKE_SEND_ADDRESS = "5FUploadWalletAddress" +FAKE_AMOUNT_ALPHA_RAO = 120_344_620_287_164 FAKE_OWNER_HOTKEY = upload_module.config.OWNER_HOTKEY FAKE_BLOCK_TIME = datetime(2026, 6, 9, 18, 0, tzinfo=timezone.utc) @@ -36,12 +35,9 @@ def upload_prod_mode(): """Run all tests in this module against the prod code path.""" original_env = upload_module.config.ENV - original_send_address = upload_module.config.UPLOAD_SEND_ADDRESS upload_module.config.ENV = "prod" - upload_module.config.UPLOAD_SEND_ADDRESS = FAKE_SEND_ADDRESS yield upload_module.config.ENV = original_env - upload_module.config.UPLOAD_SEND_ADDRESS = original_send_address @pytest.fixture(autouse=True) @@ -100,26 +96,36 @@ def _make_fake_timestamp_extrinsic() -> MagicMock: return ext -def _make_fake_extrinsic(coldkey: str, amount_rao: int, dest: str) -> MagicMock: +def _make_fake_burn_extrinsic(coldkey: str) -> MagicMock: ext = MagicMock() ext.value_serialized = { "address": coldkey, - "call": { - "call_module": "Balances", - "call_function": "transfer_keep_alive", - "call_args": [ - {"name": "dest", "value": dest}, - {"name": "value", "value": amount_rao}, - ], - }, + "call": {"call_module": "SubtensorModule", "call_function": "burn_alpha", "call_args": []}, } - ext.value = ext.value_serialized - ext.__getitem__ = MagicMock(side_effect=lambda key: coldkey if key == "address" else None) return ext +def _fake_events( + extrinsic_idx: int, + coldkey: str, + netuid: int, + amount: int, + hotkey: str = FAKE_HOTKEY, +) -> list: + return [ + { + "extrinsic_idx": extrinsic_idx, + "event": { + "module_id": "SubtensorModule", + "event_id": "AlphaBurned", + "attributes": (coldkey, hotkey, amount, netuid), + }, + } + ] + + def _install_mocks(monkeypatch) -> None: - """Patch blockchain + S3. prod flag and UPLOAD_SEND_ADDRESS are set by upload_prod_mode.""" + """Patch blockchain + S3. prod flag is set by upload_prod_mode.""" monkeypatch.setattr(upload_module, "check_signature", MagicMock()) monkeypatch.setattr(upload_module, "check_hotkey_registered", AsyncMock()) monkeypatch.setattr(upload_module, "check_agent_banned", AsyncMock()) @@ -137,11 +143,23 @@ def _install_mocks(monkeypatch) -> None: timestamp=int(FAKE_BLOCK_TIME.timestamp() * 1000), extrinsics=[ _make_fake_timestamp_extrinsic(), - _make_fake_extrinsic(FAKE_COLDKEY, FAKE_AMOUNT_RAO, FAKE_SEND_ADDRESS), + _make_fake_burn_extrinsic(FAKE_COLDKEY), ], ) ), ) + monkeypatch.setattr( + upload_module.subtensor_client, + "get_events", + AsyncMock( + return_value=_fake_events( + 1, + FAKE_COLDKEY, + upload_module.config.NETUID, + FAKE_AMOUNT_ALPHA_RAO, + ) + ), + ) monkeypatch.setattr( upload_module.subtensor_client, "get_hotkey_owner", @@ -149,13 +167,25 @@ def _install_mocks(monkeypatch) -> None: ) monkeypatch.setattr( upload_module.subtensor_client, - "get_balance", - AsyncMock(return_value=MagicMock(rao=FAKE_AMOUNT_RAO * 10)), + "get_alpha_stake_availability", + AsyncMock( + return_value=SimpleNamespace( + position_rao=FAKE_AMOUNT_ALPHA_RAO * 10, + total_rao=FAKE_AMOUNT_ALPHA_RAO * 10, + locked_rao=0, + burnable_rao=FAKE_AMOUNT_ALPHA_RAO * 10, + ) + ), ) monkeypatch.setattr( upload_module, "get_upload_price", - AsyncMock(return_value=MagicMock(amount_rao=FAKE_AMOUNT_RAO, send_address=FAKE_SEND_ADDRESS)), + AsyncMock( + return_value=MagicMock( + amount_alpha_rao=FAKE_AMOUNT_ALPHA_RAO, + payment_netuid=upload_module.config.NETUID, + ) + ), ) monkeypatch.setattr("queries.agent.upload_text_file_to_s3", AsyncMock()) response_validate_open_router_keys = MagicMock() @@ -175,8 +205,7 @@ def _install_mocks(monkeypatch) -> None: async def _insert_quote( *, hotkey: str = FAKE_HOTKEY, - amount_rao: int = FAKE_AMOUNT_RAO, - send_address: str = FAKE_SEND_ADDRESS, + amount_alpha_rao: int = FAKE_AMOUNT_ALPHA_RAO, created_at: datetime = FAKE_BLOCK_TIME - timedelta(minutes=1), expires_at: datetime = FAKE_BLOCK_TIME + timedelta(minutes=15), ) -> uuid.UUID: @@ -185,13 +214,12 @@ async def _insert_quote( await conn.execute( """ INSERT INTO upload_payment_quotes - (quote_id, miner_hotkey, amount_rao, send_address, created_at, expires_at) - VALUES ($1, $2, $3, $4, $5, $6) + (quote_id, miner_hotkey, amount_alpha_rao, created_at, expires_at) + VALUES ($1, $2, $3, $4, $5) """, quote_id, hotkey, - amount_rao, - send_address, + amount_alpha_rao, created_at, expires_at, ) @@ -268,24 +296,81 @@ async def test_check_agent_persists_payment_quote(): ) assert response.status == "success" - assert response.amount_rao == FAKE_AMOUNT_RAO - assert response.send_address == FAKE_SEND_ADDRESS + assert response.amount_alpha_rao == FAKE_AMOUNT_ALPHA_RAO + assert response.payment_netuid == upload_module.config.NETUID + upload_module.subtensor_client.get_alpha_stake_availability.assert_awaited_once_with( + coldkey=FAKE_COLDKEY, + hotkey=FAKE_HOTKEY, + netuid=upload_module.config.NETUID, + ) async with _db.pool.acquire() as conn: row = await conn.fetchrow( """ - SELECT miner_hotkey, amount_rao, send_address, expires_at, created_at + SELECT miner_hotkey, amount_alpha_rao, expires_at, created_at FROM upload_payment_quotes WHERE quote_id = $1 """, response.quote_id, ) assert row["miner_hotkey"] == FAKE_HOTKEY - assert row["amount_rao"] == FAKE_AMOUNT_RAO - assert row["send_address"] == FAKE_SEND_ADDRESS + assert row["amount_alpha_rao"] == FAKE_AMOUNT_ALPHA_RAO assert row["expires_at"] > row["created_at"] +@pytest.mark.anyio +@pytest.mark.parametrize( + ("position_rao", "total_rao", "locked_rao", "burnable_rao"), + [ + (FAKE_AMOUNT_ALPHA_RAO - 1, FAKE_AMOUNT_ALPHA_RAO * 10, 0, FAKE_AMOUNT_ALPHA_RAO - 1), + ( + FAKE_AMOUNT_ALPHA_RAO * 10, + FAKE_AMOUNT_ALPHA_RAO * 10, + FAKE_AMOUNT_ALPHA_RAO * 10 - FAKE_AMOUNT_ALPHA_RAO + 1, + FAKE_AMOUNT_ALPHA_RAO - 1, + ), + ], +) +async def test_check_agent_rejects_position_or_lock_limited_alpha( + monkeypatch, + position_rao: int, + total_rao: int, + locked_rao: int, + burnable_rao: int, +): + from fastapi import HTTPException + + monkeypatch.setattr( + upload_module.subtensor_client, + "get_alpha_stake_availability", + AsyncMock( + return_value=SimpleNamespace( + position_rao=position_rao, + total_rao=total_rao, + locked_rao=locked_rao, + burnable_rao=burnable_rao, + ) + ), + ) + + with pytest.raises(HTTPException) as exc_info: + await upload_module.check_agent_post( + request=_make_request(), + agent_file=_make_upload_file(), + public_key="deadbeef", + file_info=f"{FAKE_HOTKEY}:0", + signature="fakesig", + name="test-agent", + openrouter_api_key="sk-or-v1-runtime", + openrouter_management_key="sk-or-v1-management", + ) + + assert exc_info.value.status_code == 402 + assert f"Position: {position_rao}" in exc_info.value.detail + assert f"locked: {locked_rao}" in exc_info.value.detail + assert f"burnable: {burnable_rao}" in exc_info.value.detail + + @pytest.mark.anyio async def test_fresh_upload_creates_completed_payment(): """Happy path: payment row is created and linked to the deterministic agent_id.""" @@ -327,14 +412,14 @@ async def test_partial_failure_retry_succeeds(): await conn.execute( """ INSERT INTO evaluation_payments - (payment_block_hash, payment_extrinsic_index, agent_id, miner_hotkey, miner_coldkey, amount_rao, quote_id) + (payment_block_hash, payment_extrinsic_index, agent_id, miner_hotkey, miner_coldkey, amount_alpha_rao, quote_id) VALUES ($1, $2, NULL, $3, $4, $5, $6) """, FAKE_BLOCK_HASH, FAKE_EXTRINSIC_INDEX, FAKE_HOTKEY, FAKE_COLDKEY, - FAKE_AMOUNT_RAO, + FAKE_AMOUNT_ALPHA_RAO, quote_id, ) @@ -363,13 +448,13 @@ async def test_refunded_payment_raises_402(): uuid.uuid4(), "0xdeadbeef1235", "1", - FAKE_AMOUNT_RAO, + FAKE_AMOUNT_ALPHA_RAO, "0xrefundtxhash", "0xuploadtxhash", FAKE_BLOCK_HASH, FAKE_EXTRINSIC_INDEX, FAKE_COLDKEY, - FAKE_AMOUNT_RAO, + FAKE_AMOUNT_ALPHA_RAO, ) with pytest.raises(HTTPException) as exc_info: @@ -384,11 +469,23 @@ async def test_refunded_payment_raises_402(): @pytest.mark.anyio -async def test_amount_mismatch_raises_402(monkeypatch): - """A payment with the wrong on-chain amount is rejected before reservation.""" +async def test_burn_below_quote_raises_402(monkeypatch): + """A burn event with an amount below the quoted amount is rejected before reservation.""" from fastapi import HTTPException - quote_id = await _insert_quote(amount_rao=FAKE_AMOUNT_RAO + 1) + monkeypatch.setattr( + upload_module.subtensor_client, + "get_events", + AsyncMock( + return_value=_fake_events( + 1, + FAKE_COLDKEY, + upload_module.config.NETUID, + FAKE_AMOUNT_ALPHA_RAO - 1, + ) + ), + ) + quote_id = await _insert_quote(amount_alpha_rao=FAKE_AMOUNT_ALPHA_RAO) with pytest.raises(HTTPException) as exc_info: await _call_post_agent(quote_id=quote_id) @@ -401,6 +498,58 @@ async def test_amount_mismatch_raises_402(monkeypatch): assert payment is None +@pytest.mark.anyio +async def test_burn_wrong_coldkey_raises_402(monkeypatch): + """A burn event signed/attributed to a different coldkey than the miner's is rejected.""" + from fastapi import HTTPException + + monkeypatch.setattr( + upload_module.subtensor_client, + "get_events", + AsyncMock( + return_value=_fake_events( + 1, + "5Fimposter", + upload_module.config.NETUID, + FAKE_AMOUNT_ALPHA_RAO, + ) + ), + ) + quote_id = await _insert_quote() + + with pytest.raises(HTTPException) as exc_info: + await _call_post_agent(quote_id=quote_id) + + assert exc_info.value.status_code == 402 + + +@pytest.mark.anyio +async def test_burn_wrong_hotkey_raises_402(monkeypatch): + """A burn from another stake position cannot pay for this miner hotkey's upload.""" + from fastapi import HTTPException + + monkeypatch.setattr( + upload_module.subtensor_client, + "get_events", + AsyncMock( + return_value=_fake_events( + 1, + FAKE_COLDKEY, + upload_module.config.NETUID, + FAKE_AMOUNT_ALPHA_RAO, + hotkey="5FOtherHotkey", + ) + ), + ) + quote_id = await _insert_quote() + + with pytest.raises(HTTPException) as exc_info: + await _call_post_agent(quote_id=quote_id) + + assert exc_info.value.status_code == 402 + assert exc_info.value.detail == "Hotkey does not match" + + @pytest.mark.anyio async def test_missing_quote_id_raises_clean_400(): """Old clients are rejected with a clear error instead of stale pricing behavior.""" @@ -426,6 +575,24 @@ async def test_quote_for_different_hotkey_raises_402(): assert exc_info.value.status_code == 402 +@pytest.mark.anyio +async def test_burn_on_different_subnet_raises_402(monkeypatch): + """The AlphaBurned event must match the subnet persisted on the quote.""" + from fastapi import HTTPException + + monkeypatch.setattr( + upload_module.subtensor_client, + "get_events", + AsyncMock(return_value=_fake_events(1, FAKE_COLDKEY, 63, FAKE_AMOUNT_ALPHA_RAO)), + ) + quote_id = await _insert_quote() + + with pytest.raises(HTTPException) as exc_info: + await _call_post_agent(quote_id=quote_id) + + assert exc_info.value.status_code == 402 + + @pytest.mark.anyio async def test_payment_outside_quote_window_raises_402(): """The on-chain payment timestamp, not upload wall-clock time, must fit the quote window.""" diff --git a/tests/api/test_upload_agent_helpers.py b/tests/api/test_upload_agent_helpers.py index 5e3142d2..59e343ae 100644 --- a/tests/api/test_upload_agent_helpers.py +++ b/tests/api/test_upload_agent_helpers.py @@ -1,8 +1,14 @@ +from unittest.mock import AsyncMock + import pytest from bittensor_wallet.keypair import Keypair from fastapi import HTTPException -from api.src.utils.upload_agent_helpers import check_signature +from api.src.utils import upload_agent_helpers +from api.src.utils.upload_agent_helpers import check_signature, find_alpha_burned_event, verify_burn_extrinsic + +COLDKEY = "5C8769orColdkey" +HOTKEY = "5FHhot" def test_check_signature_accepts_matching_hotkey() -> None: @@ -24,3 +30,119 @@ def test_check_signature_rejects_invalid_signature() -> None: assert exc_info.value.status_code == 400 assert exc_info.value.detail == "Invalid signature" + + +@pytest.mark.anyio +async def test_get_alpha_price_composes_chain_and_tao(monkeypatch) -> None: + # alpha price in TAO from chain, TAO price in USD from CoinGecko -> alpha price in USD + monkeypatch.setattr( + upload_agent_helpers.subtensor_client, + "get_alpha_price_tao", + AsyncMock(return_value=0.002), + ) + monkeypatch.setattr(upload_agent_helpers, "get_tao_price", AsyncMock(return_value=400.0)) + + price = await upload_agent_helpers.get_alpha_price(netuid=62) + + assert price == pytest.approx(0.8) # 0.002 TAO/alpha * 400 USD/TAO + upload_agent_helpers.subtensor_client.get_alpha_price_tao.assert_awaited_once_with(netuid=62) + + +# ── AlphaBurned event parsing + burn extrinsic verification ──────────────────── + + +def _burn_event_tuple(idx, coldkey=COLDKEY, netuid=62, amount=120_344_620_287_164): + """A burn event with positional-tuple attributes — the confirmed live shape from AsyncSubtensor.""" + return { + "extrinsic_idx": idx, + "event": { + "module_id": "SubtensorModule", + "event_id": "AlphaBurned", + "attributes": (coldkey, HOTKEY, amount, netuid), + }, + } + + +def _burn_event_dict(idx, coldkey=COLDKEY, netuid=62, amount=120_344_620_287_164): + """A burn event with dict-shaped attributes (fallback shape, not the confirmed live one).""" + return { + "extrinsic_idx": idx, + "event": { + "module_id": "SubtensorModule", + "event_id": "AlphaBurned", + "attributes": { + "Coldkey": coldkey, + "Hotkey": HOTKEY, + "Actual Alpha Decrease": amount, + "Netuid": netuid, + }, + }, + } + + +def _extrinsic(coldkey=COLDKEY, call_function="burn_alpha", call_module="SubtensorModule"): + class Ext: + value_serialized = { + "address": coldkey, + "call": {"call_module": call_module, "call_function": call_function, "call_args": []}, + } + + return Ext() + + +def test_find_alpha_burned_event_parses_tuple_shape(): + """Confirmed live shape: a positional tuple (coldkey, hotkey, actual_alpha_decrease, netuid).""" + events = [_burn_event_tuple(2, amount=115_259_028_589, netuid=62)] + burn_event = find_alpha_burned_event(events, 2, netuid=62) + assert burn_event.coldkey == COLDKEY + assert burn_event.hotkey == HOTKEY + assert burn_event.alpha_decrease == 115_259_028_589 + assert burn_event.netuid == 62 + + +def test_find_alpha_burned_event_parses_dict_shape(): + """Dict-shaped attributes are supported as a fallback.""" + events = [_burn_event_dict(2, amount=120_344_620_287_164, netuid=62)] + burn_event = find_alpha_burned_event(events, 2, netuid=62) + assert burn_event.coldkey == COLDKEY + assert burn_event.hotkey == HOTKEY + assert burn_event.alpha_decrease == 120_344_620_287_164 + assert burn_event.netuid == 62 + + +def test_find_alpha_burned_event_missing_raises_402(): + events = [_burn_event_tuple(2)] + with pytest.raises(HTTPException) as exc: + find_alpha_burned_event(events, 5, netuid=62) + assert exc.value.status_code == 402 + + +def test_find_alpha_burned_event_skips_wrong_netuid(): + """Events on a different netuid are ignored; only the matching netuid is returned.""" + events = [ + _burn_event_tuple(2, amount=111, netuid=1), + _burn_event_tuple(2, amount=222, netuid=62), + ] + burn_event = find_alpha_burned_event(events, 2, netuid=62) + assert burn_event.netuid == 62 + assert burn_event.alpha_decrease == 222 + + +def test_verify_burn_extrinsic_accepts_burn_alpha(): + verify_burn_extrinsic(_extrinsic(call_function="burn_alpha"), COLDKEY) + + +def test_verify_burn_extrinsic_accepts_add_stake_burn(): + verify_burn_extrinsic(_extrinsic(call_function="add_stake_burn"), COLDKEY) + + +def test_verify_burn_extrinsic_rejects_non_burn_call(): + with pytest.raises(HTTPException) as exc: + verify_burn_extrinsic(_extrinsic(call_function="transfer_keep_alive", call_module="Balances"), COLDKEY) + assert exc.value.status_code == 402 + + +def test_verify_burn_extrinsic_rejects_wrong_signer(): + with pytest.raises(HTTPException) as exc: + verify_burn_extrinsic(_extrinsic(coldkey="5Fother"), COLDKEY) + assert exc.value.status_code == 402 diff --git a/tests/miners/test_upload_command.py b/tests/miners/test_upload_command.py index 56f46bd9..60d8387d 100644 --- a/tests/miners/test_upload_command.py +++ b/tests/miners/test_upload_command.py @@ -2,6 +2,8 @@ from pathlib import Path +import pytest + import miners.cli.commands.upload as upload_module @@ -64,8 +66,8 @@ def post(self, url: str, *, files=None, data=None, json=None, timeout=None): def test_check_upload_allowed_sends_both_openrouter_keys(tmp_path: Path) -> None: quote_response = { "quote_id": "quote-123", - "amount_rao": 123, - "send_address": "5Send", + "amount_alpha_rao": 120_344_620_287_164, + "payment_netuid": 62, "expires_at": "2026-06-10T00:00:00Z", } client = _FakeClient(_FakeResponse(200, json_data=quote_response)) @@ -124,3 +126,71 @@ def test_upload_payload_includes_both_openrouter_keys() -> None: assert payload["openrouter_management_key"] == "sk-or-v1-management" assert payload["quote_id"] == "quote-123" assert "payment_time" not in payload + + +def test_submit_eval_payment_composes_burn_alpha(monkeypatch): + from unittest.mock import MagicMock + + calls = {} + + fake_substrate = MagicMock() + fake_substrate.compose_call = MagicMock(side_effect=lambda **kw: calls.update(kw) or "payload") + fake_substrate.create_signed_extrinsic = MagicMock(return_value="signed") + fake_substrate.submit_extrinsic = MagicMock( + return_value=MagicMock(block_hash="0xblock", extrinsic_idx=4, is_success=True) + ) + fake_subtensor = MagicMock(substrate=fake_substrate) + monkeypatch.setattr(upload_module, "Subtensor", MagicMock(return_value=fake_subtensor), raising=False) + + import sys + + fake_bt = MagicMock() + fake_bt.Subtensor = MagicMock(return_value=fake_subtensor) + monkeypatch.setitem(sys.modules, "bittensor", fake_bt) + + wallet = MagicMock() + wallet.coldkey = "ck" + wallet.hotkey.ss58_address = "5FHhot" + + details = {"amount_alpha_rao": 120_344_620_287_164, "payment_netuid": 777, "quote_id": "q1"} + receipt = upload_module._submit_eval_payment(wallet=wallet, payment_method_details=details) + + assert calls["call_module"] == "SubtensorModule" + assert calls["call_function"] == "burn_alpha" + assert calls["call_params"]["netuid"] == 777 + assert calls["call_params"]["amount"] == 120_344_620_287_164 + assert receipt.block_hash == "0xblock" + assert receipt.extrinsic_index == 4 + assert receipt.quote_id == "q1" + + +def test_submit_eval_payment_surfaces_failed_extrinsic(monkeypatch): + from unittest.mock import MagicMock + + fake_substrate = MagicMock() + fake_substrate.compose_call.return_value = "payload" + fake_substrate.create_signed_extrinsic.return_value = "signed" + fake_substrate.submit_extrinsic.return_value = MagicMock( + block_hash="0xblock", + extrinsic_idx=4, + is_success=False, + error_message="NotEnoughBalanceToPayFees", + ) + fake_subtensor = MagicMock(substrate=fake_substrate) + + import sys + + fake_bt = MagicMock() + fake_bt.Subtensor.return_value = fake_subtensor + monkeypatch.setitem(sys.modules, "bittensor", fake_bt) + + wallet = MagicMock() + wallet.coldkey = "ck" + wallet.hotkey.ss58_address = "5FHhot" + details = {"amount_alpha_rao": 1_000, "payment_netuid": 62, "quote_id": "q1"} + + with pytest.raises( + upload_module.click.ClickException, + match="Alpha burn failed on-chain: NotEnoughBalanceToPayFees", + ): + upload_module._submit_eval_payment(wallet=wallet, payment_method_details=details) diff --git a/utils/bittensor.py b/utils/bittensor.py index 376706a9..d7d156df 100644 --- a/utils/bittensor.py +++ b/utils/bittensor.py @@ -1,18 +1,31 @@ import logging +from dataclasses import dataclass from typing import TYPE_CHECKING from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.utils.balance import Balance from bittensor_wallet.keypair import Keypair import api.config as config if TYPE_CHECKING: from bittensor.core.types import BlockInfo - from bittensor.utils.balance import Balance logger = logging.getLogger(__name__) +@dataclass(frozen=True, slots=True) +class AlphaStakeAvailability: + """Alpha that can be burned from one stake position at a single chain head.""" + + block_hash: str + position_rao: int + total_rao: int + locked_rao: int + available_rao: int + burnable_rao: int + + class SubtensorClient: """Subtensor client for interacting with the Subtensor network. Provides methods to check hotkey registration, get hotkey owner, and retrieve wallet balance. @@ -89,7 +102,7 @@ async def get_hotkey_owner(self, hotkey: str, block: int | None = None) -> str | assert self._subtensor is not None, "Subtensor client is not initialized" return await self._subtensor.get_hotkey_owner(hotkey_ss58=hotkey, block=block) - async def get_balance(self, address: str) -> "Balance": + async def get_balance(self, address: str) -> Balance: """Retrieve the balance of a wallet with a specific address. @@ -106,6 +119,92 @@ async def get_balance(self, address: str) -> "Balance": assert self._subtensor is not None, "Subtensor client is not initialized" return await self._subtensor.get_balance(address=address) + async def get_alpha_stake_availability( + self, + coldkey: str, + hotkey: str, + netuid: int, + ) -> AlphaStakeAvailability: + """Return burnable alpha for an exact ``(coldkey, hotkey, netuid)`` position. + + Subtensor's alpha lock applies to the coldkey's total stake on a subnet, + while ``burn_alpha`` is capped by the selected hotkey position. All values + are therefore read at one chain head and both limits are applied. + + Parameters + ---------- + coldkey : str + Coldkey ss58 address which owns the stake. + hotkey : str + Hotkey ss58 address identifying the stake position to burn from. + netuid : int + Subnet whose alpha will be burned. + + Returns + ------- + AlphaStakeAvailability + Position, subnet-wide, locked, available, and burnable amounts in rao. + """ + assert self._subtensor is not None, "Subtensor client is not initialized" + + block_hash = await self._subtensor.substrate.get_chain_head() + position = await self._subtensor.get_stake( + coldkey_ss58=coldkey, + hotkey_ss58=hotkey, + netuid=netuid, + block_hash=block_hash, + ) + availability = await self._subtensor.get_stake_availability_for_coldkeys( + [coldkey], + netuids=[netuid], + block_hash=block_hash, + ) + + if not isinstance(availability, dict): + raise ValueError("Invalid stake availability response") + by_netuid = availability.get(coldkey) + if not isinstance(by_netuid, dict): + raise ValueError(f"Missing stake availability for {coldkey}") + raw = by_netuid.get(netuid) + if raw is None: + total_rao = 0 + locked_rao = 0 + available_rao = 0 + elif not isinstance(raw, dict) or not {"total", "locked", "available"}.issubset(raw): + raise ValueError(f"Invalid stake availability for {coldkey} on subnet {netuid}") + else: + total_rao = int(raw["total"]) + locked_rao = int(raw["locked"]) + available_rao = int(raw["available"]) + + position_rao = position.rao + + return AlphaStakeAvailability( + block_hash=block_hash, + position_rao=position_rao, + total_rao=total_rao, + locked_rao=locked_rao, + available_rao=available_rao, + burnable_rao=min(position_rao, available_rao), + ) + + async def get_alpha_price_tao(self, netuid: int, block: int | None = None) -> float: + """Return the current alpha price (in TAO) for a subnet. + + Parameters + ---------- + block : int | None, optional + Block at which to read, by default latest. + + Returns + ------- + float + Alpha price denominated in TAO. + """ + assert self._subtensor is not None, "Subtensor client is not initialized" + price = await self._subtensor.get_subnet_price(netuid=netuid, block=block) + return float(price.tao) + async def get_block(self, block_hash: str) -> dict | None: """Retrieve a block by its hash. diff --git a/uv.lock b/uv.lock index 1ab269a4..88c45e80 100644 --- a/uv.lock +++ b/uv.lock @@ -211,19 +211,17 @@ wheels = [ [[package]] name = "async-substrate-interface" -version = "1.6.4" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, - { name = "bt-decode" }, - { name = "scalecodec" }, + { name = "cyscale" }, { name = "websockets" }, - { name = "wheel" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/48/b53f7aa0f4e63ea8f33378e0d6bafb457de3b40b6dd4e426286159438af5/async_substrate_interface-1.6.4.tar.gz", hash = "sha256:982fd9c7102176d509a5bc31a1cbee0ba6c6dff7629328a94b08cad155520aad", size = 93715, upload-time = "2026-04-02T17:48:51.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/9b/29b1d609ed59bb768f913853e664dbac9d6490207a0ada154cbd5a1c7883/async_substrate_interface-2.2.1.tar.gz", hash = "sha256:bd5ca091dfbbbb27d0bba6fc0e24aea3ca5a00c7439e23d0bacd0c8270c85ffd", size = 106484, upload-time = "2026-06-29T18:31:50.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f0/bc336a092bfbece8379d28571452d3a46936a87c75603cbdd2b09fe13be8/async_substrate_interface-1.6.4-py3-none-any.whl", hash = "sha256:7f127f5fc2a66cfd0b9bd232809f5af7ef36f545679d35b7fb71b476887dafd4", size = 97176, upload-time = "2026-04-02T17:48:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f9/bfb9103ce9c991196367468aa203f658494b83413e6b5c6668a9cb11cfce/async_substrate_interface-2.2.1-py3-none-any.whl", hash = "sha256:448a5a9843d2498bb57b28fd91b131cec01d3e8a710886fcec550283a00180cf", size = 110613, upload-time = "2026-06-29T18:31:49.63Z" }, ] [[package]] @@ -268,18 +266,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "base58" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528, upload-time = "2021-10-30T22:12:17.858Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" }, -] - [[package]] name = "bittensor" -version = "10.2.1" +version = "10.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -288,9 +277,9 @@ dependencies = [ { name = "bittensor-drand" }, { name = "bittensor-wallet" }, { name = "colorama" }, + { name = "cyscale" }, { name = "fastapi" }, { name = "msgpack-numpy-opentensor" }, - { name = "munch" }, { name = "netaddr" }, { name = "numpy" }, { name = "packaging" }, @@ -300,50 +289,47 @@ dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "retry" }, - { name = "scalecodec" }, - { name = "setuptools" }, { name = "uvicorn" }, - { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/6e/c0e97ff6b6d8f4215ee80b5f454a86290ac55965fb82e57b4e7b3d20b1ab/bittensor-10.2.1.tar.gz", hash = "sha256:e547ce6c31fe221db34bc7761969124d0fc9217d760545a25e036650db67f37c", size = 395005, upload-time = "2026-04-22T18:28:38.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f9/c76d4ef4a9a0fcbdce3ed2d57c04d9b1bfb6048d0fd99768ed272562897c/bittensor-10.5.0.tar.gz", hash = "sha256:ee2d7f98a8ce9dda07e8110571de8cc12c86cb18151aeb6dfe06aa35c25ec37f", size = 395239, upload-time = "2026-06-25T22:05:48.288Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a3/9e9b18ca244b264b3f03e959e1ed3bb0ead609429c800006af4ce76d2aa7/bittensor-10.2.1-py3-none-any.whl", hash = "sha256:2f0587caf51172cbfd56ad8d7fd6c34c473e5e5218ca94a7bc61af611a827ded", size = 469025, upload-time = "2026-04-22T18:28:36.915Z" }, + { url = "https://files.pythonhosted.org/packages/a6/99/f8434450a2ea6bea4e805553d39a705a257c091823d9be9762c95310fb1f/bittensor-10.5.0-py3-none-any.whl", hash = "sha256:0003daae373f821436be256061690f6ded1dd5f0c32539c71c0916dea61c8dbb", size = 469821, upload-time = "2026-06-25T22:05:46.842Z" }, ] [[package]] name = "bittensor-drand" -version = "1.3.0" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/13/36a587abc84cfa5a855879e247c3a763fe05cae02ff007f71f895ec933e2/bittensor_drand-1.3.0.tar.gz", hash = "sha256:ec3694c2226d66e2637168c8b31082d5cbbf991e350c254e340e1eb0255142fd", size = 52052, upload-time = "2026-02-19T20:54:55.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/c8/f1fdee0f0f5088585d6804a0099554f66834fd6d7347b921a0fedfc18e73/bittensor_drand-2.0.0.tar.gz", hash = "sha256:517cd8cabb4634a980e26aaf85ac0a8481f62fefadc0499ddbadb6467ae030c7", size = 53572, upload-time = "2026-06-18T17:12:55.498Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/f8/2bcfb2aecdd98e9bfc7d2f2e2fef4f340d71779645f4ab39206a85d2b009/bittensor_drand-1.3.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e573ad16ebe12c218f5ad7d00a1919fa3602b3527e6bd2cd419255e374584abf", size = 1988663, upload-time = "2026-02-19T20:54:51.173Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/6569a37da607a63519ea19f020034ecab3a3d3631389e829c6ccc9e98178/bittensor_drand-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2e8351e53e20b299b6c03c26ea82be5e0480e5fe043b4c29fd64fba233c46be", size = 1912005, upload-time = "2026-02-19T20:54:44.515Z" }, - { url = "https://files.pythonhosted.org/packages/4b/24/46030b9ec766eee279f5eb95050ec91b212f2ab8469b26b17f654657ecf1/bittensor_drand-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4ba5e553248c2fbc61b6c240260e7dd75b8a655006a30307e07a4038526e07", size = 2146672, upload-time = "2026-02-19T20:54:14.512Z" }, - { url = "https://files.pythonhosted.org/packages/34/1d/4582fb3b27c4689408c2209d4a69c910e64634438104c45ae9060f4ea2e2/bittensor_drand-1.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69f7d246c6bb85089b6829ce08836de00856ff3954290dcd35cb238b06a610f5", size = 2244072, upload-time = "2026-02-19T20:54:25.142Z" }, - { url = "https://files.pythonhosted.org/packages/0d/23/bb35315766d82d063a57ce9123b9ed778630571dd2fb11ffb540de45784c/bittensor_drand-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd32683bf035f6122782fe77d7ed6c7c99319f33252248feff7b466f468962fd", size = 2160988, upload-time = "2026-02-19T20:54:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/78033f58af1df4669b7537434f34f462bd09822e7f67d9b5a0bbc1dbbd7b/bittensor_drand-1.3.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:db664c3d4923e66df5cfab4469e21a923ce5402df7bc09b1b1492fc05539b6ac", size = 1988394, upload-time = "2026-02-19T20:54:52.688Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e6/f0f1b4b0ccc071b674ce8f99ff087e9a8bedd491fd07f7a0bd86c8632395/bittensor_drand-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45330eca12ff79be137b7cae75cd2647e8accdd8215417bf6b29419575b31b3b", size = 1912081, upload-time = "2026-02-19T20:54:46.258Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3f/15f4e1dec69f8279a7f11b13093f1bc4272ab84d9ec1bb587b7f639e4d0a/bittensor_drand-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:166b9d8f5139006368d4f31692e92c08689e88bc5e8a56b5ca408324e48c69fb", size = 2145656, upload-time = "2026-02-19T20:54:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/7d/51/f17a345024313b871be74db17d2d8cba6f6fdbb7347d1edb4cb8fa092db7/bittensor_drand-1.3.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb8ad08bc1123addbe5e9ac4238829f779b61117cc5a27b16a08d4fdc5660376", size = 2243517, upload-time = "2026-02-19T20:54:27.005Z" }, - { url = "https://files.pythonhosted.org/packages/68/3f/8bbd8a1268fdfdf01da335e177ea47b8eaa10f909941fe429b8f093d03e0/bittensor_drand-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f239c8b7be222cfbc752050fe609d5653a913f3a8b62484bd7b3da616c61ba00", size = 2160560, upload-time = "2026-02-19T20:54:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/67d5a2718b1c26774fde2a24eb58686c41d4947229fe504ffa84c1f86143/bittensor_drand-2.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f492650fe5fb9c70eb612f886d687c010faddde10f1557d1a598b486bd6dcaf6", size = 1967245, upload-time = "2026-06-18T17:12:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a7/221beaea41ba74d27061f9057df336b373fbc1f42cf564af969f642233de/bittensor_drand-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:77e4b605b513db5a02056358f1697ea5bda5debf35530d97f6dd35b063ee8d2d", size = 1906318, upload-time = "2026-06-18T17:12:45.541Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c5/19214245ca3f43dfd1bf5d37f5dedad0207ea607c5bd86269289d2ce5713/bittensor_drand-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:579f6209c902f012b2bdaa6a55aebc95d8937fb47183482233ea75d0cbae90ca", size = 2152991, upload-time = "2026-06-18T17:12:19.275Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6c/3183802e40b61ec11768a023c3cf9de4fd81091e7910049daa7792b876c3/bittensor_drand-2.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e000eea5b2908357e70992fbed081fbc7ce55349a7dc2a6d22bc30f5a57b257", size = 2257457, upload-time = "2026-06-18T17:12:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b0/e9aa90650f538a9e594f3aa1b1d7da2a6fd636777be7984e24fc5cc2283c/bittensor_drand-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:867fa67a2971f925119215c6c75b8c707984d01516edec4c5e1f3cb224acd665", size = 2145024, upload-time = "2026-06-18T17:12:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/58a286ce03e9c01184064140417acff848384d90b7b0fbe0c9342022ef6a/bittensor_drand-2.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e76222d927c18eaa7bf28fa3130bbe55d9d88d8146233182b1d8e5209dce2d86", size = 1966905, upload-time = "2026-06-18T17:12:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/e90c48c234ab3984f70cf2c5d4c3787dfd211fd42cef288109223cb82774/bittensor_drand-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6811e7a272e88a5a2f864efdac5a00f50f692d77c9e1124a477af4f33b1fca08", size = 1906076, upload-time = "2026-06-18T17:12:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/6ccc89677aa5f83865ef1782d4672b9f34ea038e78e1242534dbf1a22518/bittensor_drand-2.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1809d01fedffc40484df10c57f7713984ba80fb436e2aaed734a39684dc54e74", size = 2152387, upload-time = "2026-06-18T17:12:20.976Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/2df62decb435f8d47183f87f167dc395a4501365a165f60f77035e465822/bittensor_drand-2.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdb890fd763d826d5e521c5f48106e45220e2d4ae99e237ba605ca5dd7b6f185", size = 2257868, upload-time = "2026-06-18T17:12:29.61Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/cfd0a3d0a8d78f9355af6d28125406f42e7ccd25ffebcc3c71e6321be9d0/bittensor_drand-2.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1c96a56bfedd7ce534414761b6067a3ee41c317b7034f3ee24c753170bed10", size = 2144426, upload-time = "2026-06-18T17:12:38.309Z" }, ] [[package]] name = "bittensor-wallet" -version = "4.0.1" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/69/8e5eb1131e3fb750ead1c1b1d2b628dede7b41edfad835cb78764dd88ceb/bittensor_wallet-4.0.1.tar.gz", hash = "sha256:edc2588d5e272835285e4171dd3daf862149f617015bf52e43d433d8e5c297c5", size = 83080, upload-time = "2025-10-28T20:19:33.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/30/7eb06cfd5d901d2cd3760a8b85d66c7b84f96f03d6d0402b306fdf8b6a2d/bittensor_wallet-4.1.0.tar.gz", hash = "sha256:f0f34641a4b9110def9e35fe22498195fcb31d143dc4f76dd9022db374ccd484", size = 63711, upload-time = "2026-05-28T16:36:54.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8a/5dd08f82bf91f5f01e84a6b9816b45884229ffdb55087ce7a359b9678b34/bittensor_wallet-4.0.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7e942c37b065a940a16c19f33976d74a1ff4540676649508e55b75ed10e9c996", size = 841134, upload-time = "2025-10-28T20:19:28.92Z" }, - { url = "https://files.pythonhosted.org/packages/65/76/fcab1522e9235c6c3dfed2d17387e031078846627a84008cb9b09a6dc212/bittensor_wallet-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d858e359d854f0c104107f6498382a779d75f7978c9292c835e580ca01be257", size = 779909, upload-time = "2025-10-28T20:19:20.896Z" }, - { url = "https://files.pythonhosted.org/packages/dc/71/4871f9830f2a864f5aa15d730774a5e6a5a6132c2bfb884c4acc99d3a4da/bittensor_wallet-4.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0620781a14c8a95ef1b61d6d965881da7c8cc42d8785531cbf84ef6b1b1fbf1e", size = 3001664, upload-time = "2025-10-28T20:18:53.357Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/e0948e3f2fc091a3907d3c057eae6a493fb53d06c350141525fa0988dd42/bittensor_wallet-4.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8421ed1728bee3f656dfa699a3d54ce4e2c0801f612e88d4879b87367d2e619a", size = 3194509, upload-time = "2025-10-28T20:19:12.449Z" }, - { url = "https://files.pythonhosted.org/packages/99/b9/eab745ff2a9cab165ad9cd0940fc12dafde975c9b06d79549a9512b5158f/bittensor_wallet-4.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ae1b11939a2e4f2ff976a2ccc4031c329d729668955802d133b7de044c68fa57", size = 2986852, upload-time = "2025-10-28T20:19:03.324Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/2712bac4378937b3bd9d65a10f16d853f2659b10033e9f13ca025eefcf02/bittensor_wallet-4.0.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ae561a2450f3ae48cec16b0a9c3de9c4c6763802518f50ca3a6dc23c62073405", size = 840875, upload-time = "2025-10-28T20:19:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/52/5a/53f7da5c7961f1b875739e84d86974321c50aba16e963eaca6f2a1044ac2/bittensor_wallet-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:821aa6e20ded2c42c5f050751c582ef18ae58a6398acd1c96f0710b70a83f8f1", size = 780489, upload-time = "2025-10-28T20:19:22.192Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5f/014d24b338b7319490b04c147899401210bbe02c7600d9e2246a175ef248/bittensor_wallet-4.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89f9440bf712f7783c1c40d97b378ca9fce65ce9c649dfa980e564bd5b39eb5d", size = 3001451, upload-time = "2025-10-28T20:18:55.226Z" }, - { url = "https://files.pythonhosted.org/packages/a8/27/b68c6ad137029d8c3c38cc8dab9b2a6a19db7246eb82af06773b58814585/bittensor_wallet-4.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:780a8a4a75987ad6fd9a829cf85952ae0462bccc06320532e1a8f246616b9e4c", size = 3194202, upload-time = "2025-10-28T20:19:14.138Z" }, - { url = "https://files.pythonhosted.org/packages/9c/74/3ee0013ffadacf34a1d2c565e8ea289208a0ea83f5aaa1f3d8bdb42155d2/bittensor_wallet-4.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0e6b28d0f5bffd0065ec424d0b4838e06e24c7e92d149df7240b879073ee9dd9", size = 2986662, upload-time = "2025-10-28T20:19:05.198Z" }, + { url = "https://files.pythonhosted.org/packages/3c/34/4e25a73bc54689c02a0915ebcd08ac4c514e42dd3d473967fcfa7fb11c7d/bittensor_wallet-4.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:89c21a657f8f40b495b098d16aa77a58eb7e62f68879ea58febd8ca7536a4463", size = 944313, upload-time = "2026-05-28T16:36:50.963Z" }, + { url = "https://files.pythonhosted.org/packages/dd/95/6406e9331b88a071529287e290ea64d37ab9c217be8a07f594a49d49fda3/bittensor_wallet-4.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eccee4cdef228ec4ef351ee5334fe2bd83a2db5ffc112b4e8c626056dff4d45", size = 889004, upload-time = "2026-05-28T16:36:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/8a31bf6b5c8b0827ad001c58ec548fcce14c6b9d2c830b879cb8985ddccd/bittensor_wallet-4.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:856dd7e5f4bac933da3304cd5d29101d424b97ebd39996405b46882b0085079c", size = 3154691, upload-time = "2026-05-28T16:36:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/14/cb/ab34edd41c5ab8ee86c41b6343696bfb59bf75802fca3452c2150ca66026/bittensor_wallet-4.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b91e4c8b336ec5739c3c6262ea21cbff5e005d4e38963097f0ed0fed2bf80b", size = 3174987, upload-time = "2026-05-28T16:36:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/1c/2d/2035f3929a3827adaf69534d7ce43866d7e131398ad3a47d9c958a115b9f/bittensor_wallet-4.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:796b924712e5306bd93bcf17483012a01fae8ba526d55d2424933ffb70a462a7", size = 3364450, upload-time = "2026-05-28T16:36:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/e0/54/46e32add1a0fa54fe4beca3bc9fdee333e3fc2d99e98fc8837f0bcffe704/bittensor_wallet-4.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e79ee6b95ee197902b38e0a768503bfd574412bfa982988ca4aa1011a5998434", size = 943851, upload-time = "2026-05-28T16:36:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/10/ea/58e10f9e46490ac5f00e06c0449b8322a810f7ca121475a96a3cf732592b/bittensor_wallet-4.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96d8110410e110e7f1ebdaae892ccea2a4cda79b1664c6b2513cc3f4f30f3dfd", size = 888130, upload-time = "2026-05-28T16:36:45.763Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/17c7a4f91c111d2dc5dd3fa79c98846c911b9c74c7269bae8beb198b0ee2/bittensor_wallet-4.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c8a5d366a2010663103306a9b508d2efe0b5bf6b90b542a01fcb61f6215de13", size = 3154746, upload-time = "2026-05-28T16:36:23.509Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c6/d4feb8ac136ee063985e389c8f398be3afea85b41787de0b730e75140a3a/bittensor_wallet-4.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ec09575fb03cbd1ee39b8a74b9cf1726f3f37ebe523a990214c9cb167c5d2c6", size = 3174162, upload-time = "2026-05-28T16:36:31.125Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c3/0d393670a1bd59e6478b944a75845a75c0685261c201c6d750945b1195e6/bittensor_wallet-4.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758e23c38bcfb2184a3a9f79a667d16f90004c642cbbb0735fbaebefce01c8a", size = 3364516, upload-time = "2026-05-28T16:36:38.916Z" }, ] [[package]] @@ -383,31 +369,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] -[[package]] -name = "bt-decode" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "toml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/d6/f30b65454ff3f78b698ec9e0b18fcd22299b43c5581f1e913f77657761db/bt_decode-0.8.0.tar.gz", hash = "sha256:deb6b798bea703c9b9e40267f6cddcfb45f7f4c884bbb3d2280143b18095eb09", size = 1200411, upload-time = "2025-10-28T21:07:11.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/53c6ff30b5cc63d269aaba8a68eae9c06f71b92affadde1d93446e8c155f/bt_decode-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4ca5c01d2b1d3edfe2430f45b9e13c5f0ac78af7047d3b702d0bcf6307348a93", size = 596467, upload-time = "2025-10-28T21:07:07.199Z" }, - { url = "https://files.pythonhosted.org/packages/78/cd/186857054f12796f13b614921599750979c59636f34afa186ff76c257106/bt_decode-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:30e2c68dbcc69da901c3bc3a20ece66c0e90867fcb54ff46a90b506d92a81143", size = 579138, upload-time = "2025-10-28T21:06:59.367Z" }, - { url = "https://files.pythonhosted.org/packages/68/ac/f4df2de63c5f90bea084ddbcff02c0a7f8ea8018cbd952e5368c8170c39f/bt_decode-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad5c36325d0ad7e597b0f3f4e28ea7a49d5587123224d1c07c7e705d366563df", size = 638952, upload-time = "2025-10-28T21:06:36.583Z" }, - { url = "https://files.pythonhosted.org/packages/38/98/65e2ed447369a6a5f2597dbec79b0fcb7e2516c4b053d49f12894cfec557/bt_decode-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfcff566afd5083ca6091ece4aef00728de25871d8e5499fa03669e15cf0625a", size = 648694, upload-time = "2025-10-28T21:06:52.463Z" }, - { url = "https://files.pythonhosted.org/packages/e2/bf/7b9e6feb4c282f6af29e6932926da237a09353c7424802e9e67059d5b717/bt_decode-0.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46519b293d1338660b0d12c5bf0cc6442204d0e3129f16d450bff66de55b4a70", size = 714298, upload-time = "2025-10-28T21:06:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/e8/78/74de03c3f964234e8fda67b98f8bd928be3bb8179d51a6be5b3f730140ec/bt_decode-0.8.0-cp312-cp312-win32.whl", hash = "sha256:202a28a42bd972c701850a8bbbb197fcf370ea11c85a265319036503c8584425", size = 420339, upload-time = "2025-10-28T21:07:21.599Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2f/49865c7a45e20f0b71f7c80c57354e883eccb7daa711b4c0d100b6621c3b/bt_decode-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fd7201a9ddd4c44d27023f4aa9174f4a7a1ea94fed310294020d2638e8976b86", size = 439667, upload-time = "2025-10-28T21:07:15.126Z" }, - { url = "https://files.pythonhosted.org/packages/f2/01/b6eab67d288f52b0c732194db85e8787bb2994690f0f0d1744cf873e12ec/bt_decode-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:13fbbfa4ebe60df27bc4b4bb32de8969182d24239b56a2cf56b0a933e88b2529", size = 596597, upload-time = "2025-10-28T21:07:08.228Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fd/938ace0d01136ca4bc800b746a9a8ec58b908f1db20fa0233b6095362e92/bt_decode-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8f7bb0b887531a560a71c761bbc8032b4bc44e1d456ac1ae693daed78d40c4de", size = 579396, upload-time = "2025-10-28T21:07:00.352Z" }, - { url = "https://files.pythonhosted.org/packages/43/78/7cfa3eb15ab5174e8c929519e4d6b139a903d3ba9c5e24cf3ec8b11d7160/bt_decode-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b6670e6c7f3278dd7a7df237feec632849c40b525cb16d68468de04d88a332", size = 638695, upload-time = "2025-10-28T21:06:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/78/1f/199195c6589142dfa317f4c112525ed32de251f47792f6eb27166c30fb89/bt_decode-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9473338d99c339d84175f957177b49976b67aa1e50fa67ffe652e7fff4d3621e", size = 648005, upload-time = "2025-10-28T21:06:53.452Z" }, - { url = "https://files.pythonhosted.org/packages/07/39/13140ea0f97acc1c4e7eadd0eeeac5eb2a92c53e39bd345f1e4fafd5c2f7/bt_decode-0.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0fb6709709faf753110c19b22f44a6ffe64e95de2c435cec0f41f3fa54b81a4f", size = 714488, upload-time = "2025-10-28T21:06:46.015Z" }, - { url = "https://files.pythonhosted.org/packages/50/9d/a0993816e12cba61a86008a4926d77693dabac84d86040687c8130587aa3/bt_decode-0.8.0-cp313-cp313-win32.whl", hash = "sha256:dbddd1d2e393467d01d708454944733030b449cfc0d40ef6ac5a3b726ea2bffe", size = 420244, upload-time = "2025-10-28T21:07:22.642Z" }, - { url = "https://files.pythonhosted.org/packages/88/da/9c36a3ba0afe61874a525ca922fda952cde4975a2eda4a9234ce925734c7/bt_decode-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:14a1a57eae0ad31c4e9ac0b1c862484225577d361082d0ae0a6054a7fca0f4cb", size = 439647, upload-time = "2025-10-28T21:07:16.137Z" }, -] - [[package]] name = "cachetools" version = "6.2.6" @@ -616,6 +577,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/55/009497b2ae7375db090b41f9fe7a1a7362f804ddfe17ed9e34f748fcb0e5/cryptography-46.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966", size = 2923145, upload-time = "2025-09-16T21:07:25.74Z" }, ] +[[package]] +name = "cyscale" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/02/b77c019e7d697ede8bd7bcd9472569a45e3ff7df6aaa8eb90abde5ac190f/cyscale-0.5.0.tar.gz", hash = "sha256:57bf8fb401c71d5d8c7dbadd948c1fbb239b014ad58eb2f43722beb39ebc50e9", size = 1323311, upload-time = "2026-06-10T17:34:41.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/6d/2b1c4ecfe8c6673e4e05af88da2ad8938e0276f2ac0be928e0744c9dba9c/cyscale-0.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8048386723df3020a437a1392b00860fe91216a50fb0c6237c353d47b7a80a5", size = 2128495, upload-time = "2026-06-10T17:33:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/71/c3/23761b1d11cfe56db242257faa28e62cdf1c8f0a5ffa91dedfd04ab0c08c/cyscale-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5eee3e478852195eeed054dfbd068ac9d15efd1d5e5e6dcb80bf9318cbe49b5", size = 2070592, upload-time = "2026-06-10T17:33:48.532Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/c3aa88a6f40d96c71d0be372491f40f0e6242c5bed439b3e69e15202ebc9/cyscale-0.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d918b1d8535d7f62861b9592043a87a8b3179afe67b48aad3639599dd687f172", size = 7053803, upload-time = "2026-06-10T17:33:50.418Z" }, + { url = "https://files.pythonhosted.org/packages/ed/90/c6ce6d6de4231387a0235541349d97e3de8ade66b63ea6acc5760c37c2b1/cyscale-0.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c540daa377d3749f25d7d8d92ca9048bcd274aa497f0eb3c1161c7bcad18312", size = 7184829, upload-time = "2026-06-10T17:33:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/fb/18/4e8f609588ffa9b19e833e368e3f986b06b6aac609d6d23675542e7ec37f/cyscale-0.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e8a7bf307f6543c342513b46bbdcef8519cb879516327f8555cdb06ed974b1d9", size = 6854586, upload-time = "2026-06-10T17:33:55.028Z" }, + { url = "https://files.pythonhosted.org/packages/d1/20/19e8f129690303718af9df8c1916b6b23ab127320ebb72af9761f40fe950/cyscale-0.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8d342b50b57cc421b40dbf2f1fc0e837f14562ed99afa7aebec46424b34ecb96", size = 7049994, upload-time = "2026-06-10T17:33:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/9ec0b52e359441882e495ac151cca625a6e3f97c8a88f58ecdb75aaaa18d/cyscale-0.5.0-cp312-cp312-win32.whl", hash = "sha256:23ec3f58b66b4aacda14b6ff23716215b9904246c3f85a24d5704a76453930b9", size = 1860742, upload-time = "2026-06-10T17:33:59.04Z" }, + { url = "https://files.pythonhosted.org/packages/07/99/49c84eb4ea2e0e885e2028cc0b5b948a2b3aca4dfa8d6fbedd4a49462a63/cyscale-0.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:250ac9bbe764a6a931a3d4740e7fe711ba209ed8824760eac29eceac620849fd", size = 1973323, upload-time = "2026-06-10T17:34:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/ea/14/c30e35a9f27d228abc8be6779fb712f4a76d4db437e03bfa31c30384a1c3/cyscale-0.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:808045f9cb7fe12f59940d8be72abb16344d5dee3f98d41d935f36229eff2be7", size = 1854287, upload-time = "2026-06-10T17:34:03.074Z" }, + { url = "https://files.pythonhosted.org/packages/2f/73/efe4ae738e54db775cc918766dad340b3e2aa1cd60a29611d51eceda2739/cyscale-0.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bd0ce72785767309aa21a197aabb71f9d90215dd096a048681e18bdb92af32d", size = 2122718, upload-time = "2026-06-10T17:34:04.808Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4c/c9f354c7eb3a80757829c18e2f0b78064070e3da18a3ac9aa74ab2b1bc23/cyscale-0.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e14e3c7cdee4d913fccfd19316d8d7eccf208283ebedd936f1e4a958a2a25963", size = 2065243, upload-time = "2026-06-10T17:34:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/02/74/150f5fd0d1672ac46375f559318d770768c3dd6c6eb1a3311e9a34368fac/cyscale-0.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6b9c99831c1e16b3baae09680d384728900a3b775081a9fff0d51b3a4a33db8", size = 7025949, upload-time = "2026-06-10T17:34:08.497Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0d/d4a2a19a50f49cbf22d194e7f4bc52956c48ec08c1f899095d83025132ab/cyscale-0.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e9cc5ca5072a1a3a70215a4850e2d7c05c70ae87703beccce0451ceb971271", size = 7114224, upload-time = "2026-06-10T17:34:10.734Z" }, + { url = "https://files.pythonhosted.org/packages/07/f9/5d340dfeedd713e459835b1356fe62b925727f17630defe2e3f5923652fb/cyscale-0.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a32692f6c62173ca1a3c00b6a0deafec02afffecd21f2ad05d4d71ce7967035d", size = 6847049, upload-time = "2026-06-10T17:34:12.819Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d9/44d1101df46c5adfb023150d48956833da272297f29695a12acfb0b84222/cyscale-0.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:277bcc2fb0d040c2901b6d3090429a3e9a89b89ee4e13565f443233c431ba0a6", size = 6991379, upload-time = "2026-06-10T17:34:15.064Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0d/00ba15391c0ff4b773a35080e6e97a5bfed0ef703e2281ae72d9553bc422/cyscale-0.5.0-cp313-cp313-win32.whl", hash = "sha256:5f067d045b4cbeea785d2acac99f26328224363e67dfb8bd403159b23305ef52", size = 1857383, upload-time = "2026-06-10T17:34:17.449Z" }, + { url = "https://files.pythonhosted.org/packages/fd/78/518d19d2b8ec05d6f928fa5d1fa2e128aac8c809e915c05de2226dbe6058/cyscale-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba5288d7d4d6648e46e3bffc032e21763d0fed07a3313fe68eb7b47ef049ec62", size = 1974447, upload-time = "2026-06-10T17:34:19.147Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b3/53f6f286be075be869178a55b0c3350361497f64c790fdf39bbf56ca2c98/cyscale-0.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:689b0b1ddf912083350e0c1f57fe6020ac4c53e2f001fd8dc858dcae00efbb62", size = 1853051, upload-time = "2026-06-10T17:34:20.842Z" }, +] + [[package]] name = "datasets" version = "4.4.1" @@ -1562,15 +1549,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/65/fa87a72b7bf150dbf9949e503b8a45e278789201ac3691b8af1cd861a7d6/modal-1.4.2-py3-none-any.whl", hash = "sha256:6993874476dfd51057e36c778dbd7aae58811802515532447336eced00c81e28", size = 802775, upload-time = "2026-04-16T20:27:58.065Z" }, ] -[[package]] -name = "more-itertools" -version = "11.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, -] - [[package]] name = "msgpack" version = "1.1.2" @@ -1699,15 +1677,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478, upload-time = "2025-04-17T03:11:26.253Z" }, ] -[[package]] -name = "munch" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/45098135b5f9f13221820d90f9e0516e11a2a0f55012c13b081d202b782a/munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235", size = 19089, upload-time = "2023-07-01T09:49:35.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/b3/7c69b37f03260a061883bec0e7b05be7117c1b1c85f5212c72c8c2bc3c8c/munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4", size = 9950, upload-time = "2023-07-01T09:49:34.472Z" }, -] - [[package]] name = "netaddr" version = "1.3.0" @@ -2608,7 +2577,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.13" }, { name = "asgi-correlation-id", specifier = ">=4.3.4" }, { name = "asyncpg", specifier = ">=0.30.0" }, - { name = "bittensor", specifier = "==10.2.1" }, + { name = "bittensor", specifier = "==10.5.0" }, { name = "click", specifier = ">=8.1.0" }, { name = "cryptography", specifier = "==46.0.0" }, { name = "docker", specifier = ">=7.1.0" }, @@ -2741,20 +2710,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, ] -[[package]] -name = "scalecodec" -version = "1.2.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "base58" }, - { name = "more-itertools" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/3c/4c3e3fa0efd75eb1e00b9bd6ccce8e0018e0789bff35d76cc9ce554354d0/scalecodec-1.2.12.tar.gz", hash = "sha256:aa54cc901970289fe64ae01edf076f25f60f8d7e4682979b827cab73dde74393", size = 150568, upload-time = "2025-10-16T14:01:55.231Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/e0/a080f62ccb71ace2330081badae678d2f5349078388459a60af927814695/scalecodec-1.2.12-py3-none-any.whl", hash = "sha256:b9de1a2d3d98b9e4285804478d8f5f13b3787ebc4d05625eb0054add7feebe45", size = 99164, upload-time = "2025-10-16T14:01:53.517Z" }, -] - [[package]] name = "scantree" version = "0.0.4" @@ -2781,15 +2736,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/41/f2b800b7f12a05dd48c2a6280d4dd812d1425fc66ed3fe3fd99420c41d1a/sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803", size = 475616, upload-time = "2026-05-13T13:34:50.259Z" }, ] -[[package]] -name = "setuptools" -version = "70.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/d8/10a70e86f6c28ae59f101a9de6d77bf70f147180fbf40c3af0f64080adc3/setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", size = 2333112, upload-time = "2024-07-09T16:08:06.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/15/88e46eb9387e905704b69849618e699dc2f54407d8953cc4ec4b8b46528d/setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc", size = 931070, upload-time = "2024-07-09T16:07:58.829Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -3342,18 +3288,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "wheel" -version = "0.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, -] - [[package]] name = "wrapt" version = "1.17.3"