Skip to content

Commit a669038

Browse files
authored
Merge pull request #657 from opentensor/feat/crowdloans
Feat/crowdloans
2 parents 9879597 + d11d563 commit a669038

File tree

14 files changed

+3591
-4
lines changed

14 files changed

+3591
-4
lines changed

bittensor_cli/cli.py

Lines changed: 625 additions & 0 deletions
Large diffs are not rendered by default.

bittensor_cli/src/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,11 @@ class RootSudoOnly(Enum):
732732
"LIQUIDITY": {
733733
"LIQUIDITY_MGMT": "Liquidity Management",
734734
},
735+
"CROWD": {
736+
"INITIATOR": "Crowdloan Creation & Management",
737+
"PARTICIPANT": "Crowdloan Participation",
738+
"INFO": "Crowdloan Information",
739+
},
735740
}
736741

737742

bittensor_cli/src/bittensor/chain_data.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,3 +1213,51 @@ def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult":
12131213
tao_fee=Balance.from_rao(d["tao_fee"]).set_unit(0),
12141214
alpha_fee=Balance.from_rao(d["alpha_fee"]).set_unit(netuid),
12151215
)
1216+
1217+
1218+
@dataclass
1219+
class CrowdloanData(InfoBase):
1220+
creator: Optional[str]
1221+
funds_account: Optional[str]
1222+
deposit: Balance
1223+
min_contribution: Balance
1224+
cap: Balance
1225+
raised: Balance
1226+
end: int
1227+
finalized: bool
1228+
contributors_count: int
1229+
target_address: Optional[str]
1230+
has_call: bool
1231+
call_details: Optional[dict] = None
1232+
1233+
@classmethod
1234+
def _fix_decoded(cls, decoded: dict[str, Any]) -> "CrowdloanData":
1235+
creator = (
1236+
decode_account_id(creator_raw)
1237+
if (creator_raw := decoded.get("creator"))
1238+
else None
1239+
)
1240+
funds_account = (
1241+
decode_account_id(funds_raw)
1242+
if (funds_raw := decoded.get("funds_account"))
1243+
else None
1244+
)
1245+
target_address = (
1246+
decode_account_id(target_raw)
1247+
if (target_raw := decoded.get("target_address"))
1248+
else None
1249+
)
1250+
return cls(
1251+
creator=creator,
1252+
funds_account=funds_account,
1253+
deposit=Balance.from_rao(int(decoded["deposit"])),
1254+
min_contribution=Balance.from_rao(int(decoded["min_contribution"])),
1255+
cap=Balance.from_rao(int(decoded["cap"])),
1256+
raised=Balance.from_rao(int(decoded["raised"])),
1257+
end=int(decoded["end"]),
1258+
finalized=bool(decoded["finalized"]),
1259+
contributors_count=int(decoded["contributors_count"]),
1260+
target_address=target_address,
1261+
has_call=bool(decoded["call"]),
1262+
call_details=decoded["call_details"],
1263+
)

bittensor_cli/src/bittensor/subtensor_interface.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from bittensor_wallet import Wallet
1515
from bittensor_wallet.bittensor_wallet import Keypair
1616
from bittensor_wallet.utils import SS58_FORMAT
17-
from scalecodec import GenericCall
17+
from scalecodec import GenericCall, ScaleBytes
1818
import typer
1919
import websockets
2020

@@ -30,6 +30,7 @@
3030
SubnetState,
3131
MetagraphInfo,
3232
SimSwapResult,
33+
CrowdloanData,
3334
)
3435
from bittensor_cli.src import DelegatesDetails
3536
from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float
@@ -167,6 +168,44 @@ async def query(
167168
else:
168169
return result
169170

171+
async def _decode_inline_call(
172+
self,
173+
call_option: Any,
174+
block_hash: Optional[str] = None,
175+
) -> Optional[dict[str, Any]]:
176+
"""
177+
Decode an `Option<BoundedCall>` returned from storage into a structured dictionary.
178+
"""
179+
if not call_option or "Inline" not in call_option:
180+
return None
181+
inline_bytes = bytes(call_option["Inline"][0][0])
182+
call_obj = await self.substrate.create_scale_object(
183+
"Call",
184+
data=ScaleBytes(inline_bytes),
185+
block_hash=block_hash,
186+
)
187+
call_value = call_obj.decode()
188+
189+
if not isinstance(call_value, dict):
190+
return None
191+
192+
call_args = call_value.get("call_args") or []
193+
args_map: dict[str, dict[str, Any]] = {}
194+
for arg in call_args:
195+
if isinstance(arg, dict) and arg.get("name"):
196+
args_map[arg["name"]] = {
197+
"type": arg.get("type"),
198+
"value": arg.get("value"),
199+
}
200+
201+
return {
202+
"call_index": call_value.get("call_index"),
203+
"pallet": call_value.get("call_module"),
204+
"method": call_value.get("call_function"),
205+
"args": args_map,
206+
"hash": call_value.get("call_hash"),
207+
}
208+
170209
async def get_all_subnet_netuids(
171210
self, block_hash: Optional[str] = None
172211
) -> list[int]:
@@ -1693,6 +1732,101 @@ async def get_scheduled_coldkey_swap(
16931732
keys_pending_swap.append(decode_account_id(ss58))
16941733
return keys_pending_swap
16951734

1735+
async def get_crowdloans(
1736+
self, block_hash: Optional[str] = None
1737+
) -> list[CrowdloanData]:
1738+
"""Retrieves all crowdloans from the network.
1739+
1740+
Args:
1741+
block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1742+
1743+
Returns:
1744+
dict[int, CrowdloanData]: A dictionary mapping crowdloan IDs to CrowdloanData objects
1745+
containing details such as creator, deposit, cap, raised amount, and finalization status.
1746+
1747+
This function fetches information about all crowdloans
1748+
"""
1749+
crowdloans_data = await self.substrate.query_map(
1750+
module="Crowdloan",
1751+
storage_function="Crowdloans",
1752+
block_hash=block_hash,
1753+
fully_exhaust=True,
1754+
)
1755+
crowdloans = {}
1756+
async for fund_id, fund_info in crowdloans_data:
1757+
decoded_call = await self._decode_inline_call(
1758+
fund_info["call"],
1759+
block_hash=block_hash,
1760+
)
1761+
info_dict = dict(fund_info.value)
1762+
info_dict["call_details"] = decoded_call
1763+
crowdloans[fund_id] = CrowdloanData.from_any(info_dict)
1764+
1765+
return crowdloans
1766+
1767+
async def get_single_crowdloan(
1768+
self,
1769+
crowdloan_id: int,
1770+
block_hash: Optional[str] = None,
1771+
) -> Optional[CrowdloanData]:
1772+
"""Retrieves detailed information about a specific crowdloan.
1773+
1774+
Args:
1775+
crowdloan_id (int): The unique identifier of the crowdloan to retrieve.
1776+
block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1777+
1778+
Returns:
1779+
Optional[CrowdloanData]: A CrowdloanData object containing the crowdloan's details if found,
1780+
None if the crowdloan does not exist.
1781+
1782+
The returned data includes crowdloan details such as funding targets,
1783+
contribution minimums, timeline, and current funding status
1784+
"""
1785+
crowdloan_info = await self.query(
1786+
module="Crowdloan",
1787+
storage_function="Crowdloans",
1788+
params=[crowdloan_id],
1789+
block_hash=block_hash,
1790+
)
1791+
if crowdloan_info:
1792+
decoded_call = await self._decode_inline_call(
1793+
crowdloan_info.get("call"),
1794+
block_hash=block_hash,
1795+
)
1796+
crowdloan_info["call_details"] = decoded_call
1797+
return CrowdloanData.from_any(crowdloan_info)
1798+
return None
1799+
1800+
async def get_crowdloan_contribution(
1801+
self,
1802+
crowdloan_id: int,
1803+
contributor: str,
1804+
block_hash: Optional[str] = None,
1805+
) -> Optional[Balance]:
1806+
"""Retrieves a user's contribution to a specific crowdloan.
1807+
1808+
Args:
1809+
crowdloan_id (int): The ID of the crowdloan.
1810+
contributor (str): The SS58 address of the contributor.
1811+
block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1812+
1813+
Returns:
1814+
Optional[Balance]: The contribution amount as a Balance object if found, None otherwise.
1815+
1816+
This function queries the Contributions storage to find the amount a specific address
1817+
has contributed to a given crowdloan.
1818+
"""
1819+
contribution = await self.query(
1820+
module="Crowdloan",
1821+
storage_function="Contributions",
1822+
params=[crowdloan_id, contributor],
1823+
block_hash=block_hash,
1824+
)
1825+
1826+
if contribution:
1827+
return Balance.from_rao(contribution)
1828+
return None
1829+
16961830
async def get_coldkey_swap_schedule_duration(
16971831
self,
16981832
block_hash: Optional[str] = None,

bittensor_cli/src/bittensor/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,11 +1504,11 @@ async def print_extrinsic_id(
15041504
query = await substrate.rpc_request("system_chainType", [])
15051505
if query.get("result") == "Live":
15061506
console.print(
1507-
f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}: "
1507+
f":white_heavy_check_mark: Your extrinsic has been included as {ext_id}: "
15081508
f"[blue]https://tao.app/extrinsic/{ext_id}[/blue]"
15091509
)
15101510
return
15111511
console.print(
1512-
f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}"
1512+
f":white_heavy_check_mark: Your extrinsic has been included as {ext_id}"
15131513
)
15141514
return

bittensor_cli/src/commands/crowd/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)