Skip to content

Commit 04aabfa

Browse files
committed
safe stake added to async
1 parent 0fe7c60 commit 04aabfa

File tree

3 files changed

+155
-36
lines changed

3 files changed

+155
-36
lines changed

bittensor/core/extrinsics/asyncex/move_stake.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ async def swap_stake_extrinsic(
160160
amount: Balance,
161161
wait_for_inclusion: bool = True,
162162
wait_for_finalization: bool = False,
163+
safe_staking: bool = False,
164+
allow_partial_stake: bool = False,
165+
rate_threshold: float = 0.005,
163166
) -> bool:
164167
"""
165168
Swaps stake from one subnet to another for a given hotkey in the Bittensor network.
@@ -173,6 +176,9 @@ async def swap_stake_extrinsic(
173176
amount (Balance): The amount of stake to swap as a `Balance` object.
174177
wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to True.
175178
wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False.
179+
safe_staking (bool): If true, enables price safety checks to protect against price impact.
180+
allow_partial_stake (bool): If true, allows partial stake swaps when the full amount would exceed the price threshold.
181+
rate_threshold (float): Maximum allowed increase in price ratio (0.005 = 0.5%).
176182
177183
Returns:
178184
bool: True if the swap was successful, False otherwise.
@@ -205,20 +211,47 @@ async def swap_stake_extrinsic(
205211
return False
206212

207213
try:
208-
logging.info(
209-
f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n"
210-
f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid "
211-
f"[yellow]{destination_netuid}[/yellow]"
212-
)
214+
call_params = {
215+
"hotkey": hotkey_ss58,
216+
"origin_netuid": origin_netuid,
217+
"destination_netuid": destination_netuid,
218+
"alpha_amount": amount.rao,
219+
}
220+
221+
if safe_staking:
222+
origin_pool, destination_pool = await asyncio.gather(
223+
subtensor.subnet(netuid=origin_netuid),
224+
subtensor.subnet(netuid=destination_netuid),
225+
)
226+
swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao
227+
swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_threshold)
228+
229+
logging.info(
230+
f"Swapping stake with safety for hotkey [blue]{hotkey_ss58}[/blue]\n"
231+
f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid "
232+
f"[green]{destination_netuid}[/green]\n"
233+
f"Current price ratio: [green]{swap_rate_ratio:.4f}[/green], "
234+
f"Ratio with tolerance: [green]{swap_rate_ratio_with_tolerance:.4f}[/green]"
235+
)
236+
call_params.update(
237+
{
238+
"limit_price": swap_rate_ratio_with_tolerance,
239+
"allow_partial": allow_partial_stake,
240+
}
241+
)
242+
call_function = "swap_stake_limit"
243+
else:
244+
logging.info(
245+
f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n"
246+
f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid "
247+
f"[green]{destination_netuid}[/green]"
248+
)
249+
call_function = "swap_stake"
250+
213251
call = await subtensor.substrate.compose_call(
214252
call_module="SubtensorModule",
215-
call_function="swap_stake",
216-
call_params={
217-
"hotkey": hotkey_ss58,
218-
"origin_netuid": origin_netuid,
219-
"destination_netuid": destination_netuid,
220-
"alpha_amount": amount.rao,
221-
},
253+
call_function=call_function,
254+
call_params=call_params,
222255
)
223256

224257
success, err_msg = await subtensor.sign_and_send_extrinsic(
@@ -253,7 +286,12 @@ async def swap_stake_extrinsic(
253286

254287
return True
255288
else:
256-
logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}")
289+
if safe_staking and "Custom error: 8" in err_msg:
290+
logging.error(
291+
":cross_mark: [red]Failed[/red]: Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking."
292+
)
293+
else:
294+
logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}")
257295
return False
258296

259297
except Exception as e:

bittensor/core/extrinsics/asyncex/staking.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ async def add_stake_extrinsic(
2121
amount: Optional[Balance] = None,
2222
wait_for_inclusion: bool = True,
2323
wait_for_finalization: bool = False,
24+
safe_staking: bool = False,
25+
allow_partial_stake: bool = False,
26+
rate_threshold: float = 0.005,
2427
) -> bool:
2528
"""
2629
Adds the specified amount of stake to passed hotkey `uid`.
@@ -36,6 +39,9 @@ async def add_stake_extrinsic(
3639
`False` if the extrinsic fails to enter the block within the timeout.
3740
wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
3841
or returns `False` if the extrinsic fails to be finalized within the timeout.
42+
safe_staking: If set, uses safe staking logic
43+
allow_partial_stake: If set, allows partial stake
44+
rate_threshold: The rate threshold for safe staking
3945
4046
Returns:
4147
success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
@@ -97,19 +103,48 @@ async def add_stake_extrinsic(
97103
return False
98104

99105
try:
100-
logging.info(
101-
f":satellite: [magenta]Staking to:[/magenta] "
102-
f"[blue]netuid: {netuid}, amount: {staking_balance} "
103-
f"on {subtensor.network}[/blue] [magenta]...[/magenta]"
104-
)
106+
call_params = {
107+
"hotkey": hotkey_ss58,
108+
"netuid": netuid,
109+
"amount_staked": staking_balance.rao,
110+
}
111+
112+
if safe_staking:
113+
pool = await subtensor.subnet(netuid=netuid)
114+
base_price = pool.price.rao
115+
price_with_tolerance = base_price * (1 + rate_threshold)
116+
call_params.update(
117+
{
118+
"limit_price": price_with_tolerance,
119+
"allow_partial": allow_partial_stake,
120+
}
121+
)
122+
call_function = "add_stake_limit"
123+
124+
# For logging
125+
base_rate = pool.price.tao
126+
rate_with_tolerance = base_rate * (1 + rate_threshold)
127+
logging.info(
128+
f":satellite: [magenta]Safe Staking to:[/magenta] "
129+
f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green], "
130+
f"tolerance percentage: [green]{rate_threshold*100}%[/green], "
131+
f"price limit: [green]{rate_with_tolerance}[/green], "
132+
f"original price: [green]{base_rate}[/green], "
133+
f"with partial stake: [green]{allow_partial_stake}[/green] "
134+
f"on [blue]{subtensor.network}[/blue][/magenta]...[/magenta]"
135+
)
136+
else:
137+
logging.info(
138+
f":satellite: [magenta]Staking to:[/magenta] "
139+
f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green] "
140+
f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]"
141+
)
142+
call_function = "add_stake"
143+
105144
call = await subtensor.substrate.compose_call(
106145
call_module="SubtensorModule",
107-
call_function="add_stake",
108-
call_params={
109-
"hotkey": hotkey_ss58,
110-
"amount_staked": staking_balance.rao,
111-
"netuid": netuid,
112-
},
146+
call_function=call_function,
147+
call_params=call_params,
113148
)
114149
staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
115150
call,
@@ -152,7 +187,12 @@ async def add_stake_extrinsic(
152187
)
153188
return True
154189
else:
155-
logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]")
190+
if safe_staking and "Custom error: 8" in err_msg:
191+
logging.error(
192+
":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking."
193+
)
194+
else:
195+
logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]")
156196
return False
157197

158198
except NotRegisteredError:

bittensor/core/extrinsics/asyncex/unstaking.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ async def unstake_extrinsic(
2020
amount: Optional[Balance] = None,
2121
wait_for_inclusion: bool = True,
2222
wait_for_finalization: bool = False,
23+
safe_staking: bool = False,
24+
allow_partial_stake: bool = False,
25+
rate_threshold: float = 0.005,
2326
) -> bool:
2427
"""Removes stake into the wallet coldkey from the specified hotkey ``uid``.
2528
@@ -34,6 +37,9 @@ async def unstake_extrinsic(
3437
returns ``False`` if the extrinsic fails to enter the block within the timeout.
3538
wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning
3639
``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout.
40+
safe_staking: If true, enables price safety checks
41+
allow_partial_stake: If true, allows partial unstaking if price threshold exceeded
42+
rate_threshold: Maximum allowed price decrease percentage (0.005 = 0.5%)
3743
3844
Returns:
3945
success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for
@@ -83,19 +89,49 @@ async def unstake_extrinsic(
8389
return False
8490

8591
try:
86-
logging.info(
87-
f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: "
88-
f"[blue]{netuid}[/blue]"
89-
)
92+
call_params = {
93+
"hotkey": hotkey_ss58,
94+
"netuid": netuid,
95+
"amount_unstaked": unstaking_balance.rao,
96+
}
97+
if safe_staking:
98+
pool = await subtensor.subnet(netuid=netuid)
99+
base_price = pool.price.rao
100+
price_with_tolerance = base_price * (1 - rate_threshold)
101+
102+
# For logging
103+
base_rate = pool.price.tao
104+
rate_with_tolerance = base_rate * (1 - rate_threshold)
105+
106+
logging.info(
107+
f":satellite: [magenta]Safe Unstaking from:[/magenta] "
108+
f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], "
109+
f"tolerance percentage: [green]{rate_threshold*100}%[/green], "
110+
f"price limit: [green]{rate_with_tolerance}[/green], "
111+
f"original price: [green]{base_rate}[/green], "
112+
f"with partial unstake: [green]{allow_partial_stake}[/green] "
113+
f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]"
114+
)
115+
116+
call_params.update(
117+
{
118+
"limit_price": price_with_tolerance,
119+
"allow_partial": allow_partial_stake,
120+
}
121+
)
122+
call_function = "remove_stake_limit"
123+
else:
124+
logging.info(
125+
f":satellite: [magenta]Unstaking from:[/magenta] "
126+
f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green] "
127+
f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]"
128+
)
129+
call_function = "remove_stake"
90130

91131
call = await subtensor.substrate.compose_call(
92132
call_module="SubtensorModule",
93-
call_function="remove_stake",
94-
call_params={
95-
"hotkey": hotkey_ss58,
96-
"amount_unstaked": unstaking_balance.rao,
97-
"netuid": netuid,
98-
},
133+
call_function=call_function,
134+
call_params=call_params,
99135
)
100136
staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
101137
call,
@@ -138,7 +174,12 @@ async def unstake_extrinsic(
138174
)
139175
return True
140176
else:
141-
logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]")
177+
if safe_staking and "Custom error: 8" in err_msg:
178+
logging.error(
179+
":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking."
180+
)
181+
else:
182+
logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]")
142183
return False
143184

144185
except NotRegisteredError:

0 commit comments

Comments
 (0)