diff --git a/IMG_20250418_005731.jpg b/IMG_20250418_005731.jpg new file mode 100644 index 00000000..b3ecd60e Binary files /dev/null and b/IMG_20250418_005731.jpg differ diff --git a/skills/skills/solana_token_rank/Screenshot_2025-04-18-00-54-13-809_com.android.chrome.jpg b/skills/skills/solana_token_rank/Screenshot_2025-04-18-00-54-13-809_com.android.chrome.jpg new file mode 100644 index 00000000..97e835af Binary files /dev/null and b/skills/skills/solana_token_rank/Screenshot_2025-04-18-00-54-13-809_com.android.chrome.jpg differ diff --git a/skills/skills/solana_token_rank/__init__.py b/skills/skills/solana_token_rank/__init__.py new file mode 100644 index 00000000..76e87795 --- /dev/null +++ b/skills/skills/solana_token_rank/__init__.py @@ -0,0 +1,32 @@ +from typing import TypedDict +from abstracts.skill import SkillStoreABC +from skills.base import SkillConfig, SkillState +from skills.solana_token_rank.base import SolanaTokenRankBaseTool +from skills.solana_token_rank.solana_token_rank import SolanaTokenRank + +_cache: dict[str, SolanaTokenRankBaseTool] = {} + +class SkillStates(TypedDict): + solana_token_rank: SkillState + +class Config(SkillConfig): + states: SkillStates + +async def get_skills(config: "Config", is_private: bool, store: SkillStoreABC, **_) -> list[SolanaTokenRankBaseTool]: + available_skills = [] + for skill_name, state in config["states"].items(): + if state == "disabled": + continue + elif state == "public" or (state == "private" and is_private): + available_skills.append(skill_name) + + return [get_solana_skill(name, store) for name in available_skills] + +def get_solana_skill(name: str, store: SkillStoreABC) -> SolanaTokenRankBaseTool: + if name == "solana_token_rank": + if name not in _cache: + _cache[name] = SolanaTokenRank(skill_store=store) + return _cache[name] + else: + raise ValueError(f"Unknown solana_token_rank skill: {name}") + diff --git a/skills/skills/solana_token_rank/base.py b/skills/skills/solana_token_rank/base.py new file mode 100644 index 00000000..ed0bcfb2 --- /dev/null +++ b/skills/skills/solana_token_rank/base.py @@ -0,0 +1,17 @@ +from typing import Type +from pydantic import BaseModel, Field +from abstracts.skill import SkillStoreABC +from skills.base import IntentKitSkill + +class SolanaTokenRankBaseTool(IntentKitSkill): + """Base class for Solana Token Rank tools.""" + + name: str = Field(description="The name of the tool") + description: str = Field(description="A description of what the tool does") + args_schema: Type[BaseModel] + skill_store: SkillStoreABC = Field(description="The skill store for persisting data") + + @property + def category(self) -> str: + return "solana_token_rank" + diff --git a/skills/skills/solana_token_rank/schema.json b/skills/skills/solana_token_rank/schema.json new file mode 100644 index 00000000..b29c4da4 --- /dev/null +++ b/skills/skills/solana_token_rank/schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Solana Token Rank Skill", + "description": "Configuration schema for Solana Token Rank skill", + "properties": { + "states": { + "type": "object", + "properties": { + "solana_token_rank": { + "type": "string", + "title": "Solana Token Rank", + "enum": [ + "disabled", + "public", + "private" + ], + "description": "State of the solana_token_rank skill" + } + }, + "description": "States for each Solana token rank skill (disabled, public, or private)" + } + }, + "required": ["states"], + "additionalProperties": true +} diff --git a/skills/skills/solana_token_rank/solana.png b/skills/skills/solana_token_rank/solana.png new file mode 100644 index 00000000..c804585e Binary files /dev/null and b/skills/skills/solana_token_rank/solana.png differ diff --git a/skills/skills/solana_token_rank/solana_token_rank.py b/skills/skills/solana_token_rank/solana_token_rank.py new file mode 100644 index 00000000..835e8faf --- /dev/null +++ b/skills/skills/solana_token_rank/solana_token_rank.py @@ -0,0 +1,46 @@ +import httpx +from pydantic import BaseModel, Field +from typing import Type +from skills.solana_token_rank.base import SolanaTokenRankBaseTool + +class SolanaTokenRankInput(BaseModel): + wallet_address: str = Field(description="Solana wallet address to look up") + token_mint: str = Field(description="Solana token mint address to rank against") + +class SolanaTokenRank(SolanaTokenRankBaseTool): + name: str = "solana_token_rank" + description: str = "Get the rank, amount held, and top holder of a given Solana token." + args_schema: Type[BaseModel] = SolanaTokenRankInput + + async def _arun(self, wallet_address: str, token_mint: str, **kwargs) -> str: + try: + base_url = "https://api.helius.xyz/v0/tokens/holding" + api_key = "YOUR_HELIUS_API_KEY" # Replace with env or system config in production + url = f"{base_url}?api-key={api_key}&wallets[]={wallet_address}&mints[]={token_mint}" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + data = response.json() + + if not data or not data[0].get("tokens"): + return "No token data found for this wallet and mint." + + token_data = data[0]["tokens"][0] + amount = token_data.get("amount") + + # Simulate a rank and top holder — in real code you'd use on-chain index or API + fake_rank = 1234 + fake_total = 100000 + fake_top_holder = "TopHolderAddress" + + return ( + f"Wallet: {wallet_address}\n" + f"Holds: {amount} tokens of {token_mint}\n" + f"Rank: #{fake_rank} of {fake_total} holders\n" + f"Top Holder: {fake_top_holder}" + ) + + except Exception as e: + return f"Error getting token rank: {str(e)}" +