| """ |
| SPL Token Metadata Decoder |
| =========================== |
| Parses raw Solana account data to definitively extract hidden metadata, |
| mint authorities, and freeze flags, bypassing unreliable third-party APIs. |
| |
| This is a LOCAL/FREE_API provider that reads directly from public Solana RPCs |
| and decodes the binary SPL Token mint layout without relying on external indexers. |
| |
| SPL Token Mint Layout (76 bytes base): |
| Byte 0: mint_authority_option (1 = Some, 0 = None) |
| Bytes 1-32: mint_authority pubkey (if Some, else 32 zeros) |
| Bytes 33-40: supply (u64, little-endian) |
| Byte 41: decimals (u8) |
| Byte 42: is_initialized (bool, 1 byte) |
| Byte 43: freeze_authority_option (1 = Some, 0 = None) |
| Bytes 44-75: freeze_authority pubkey (if Some, else 32 zeros) |
| |
| Token-2022 Extensions: |
| If account data > 76 bytes, parse extension headers to detect: |
| - MintCloseAuthority |
| - PermanentDelegate |
| - TransferFee |
| - ConfidentialTransfer |
| - DefaultAccountState (frozen by default) |
| """ |
|
|
| import base64 |
| import hashlib |
| import logging |
| from typing import Any |
|
|
| import httpx |
|
|
| logger = logging.getLogger("databus.spl_metadata") |
|
|
| |
| PUBLIC_RPC_ENDPOINTS = [ |
| "https://api.mainnet-beta.solana.com", |
| "https://solana-rpc.publicnode.com", |
| "https://solana.drpc.org", |
| ] |
|
|
| |
| METAPLEX_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" |
| TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" |
| TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" |
|
|
| |
| HIGH_RISK_FLAGS = [ |
| "mint_authority_present", |
| "freeze_authority_present", |
| "default_account_state_frozen", |
| "transfer_fee_present", |
| "permanent_delegate_present", |
| ] |
|
|
|
|
| def decode_pubkey(data: bytes, offset: int) -> str | None: |
| """Decode a 32-byte Solana pubkey from bytes.""" |
| if len(data) < offset + 32: |
| return None |
| pubkey_bytes = data[offset : offset + 32] |
| |
| if all(b == 0 for b in pubkey_bytes): |
| return None |
| |
| return _bytes_to_base58(pubkey_bytes) |
|
|
|
|
| def _bytes_to_base58(data: bytes) -> str: |
| """Convert bytes to base58 string (Solana pubkey format).""" |
| alphabet = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" |
| num = int.from_bytes(data, "big") |
| result = bytearray() |
| while num > 0: |
| num, mod = divmod(num, 58) |
| result.append(alphabet[mod]) |
| |
| leading_zeros = len(data) - len(data.lstrip(b"\x00")) |
| result.extend([alphabet[0]] * leading_zeros) |
| return result[::-1].decode("ascii") |
|
|
|
|
| def parse_spl_mint_data(raw_data: bytes) -> dict[str, Any]: |
| """ |
| Parse raw SPL Token mint account data. |
| Returns decoded metadata including authorities, supply, decimals, and flags. |
| |
| SPL Token Mint Layout (82 bytes base): |
| Bytes 0-3: mint_authority_option (u32, 1 = Some, 0 = None) |
| Bytes 4-35: mint_authority pubkey (32 bytes) |
| Bytes 36-43: supply (u64, little-endian) |
| Byte 44: decimals (u8) |
| Byte 45: is_initialized (bool) |
| Bytes 46-49: freeze_authority_option (u32) |
| Bytes 50-81: freeze_authority pubkey (32 bytes) |
| """ |
| result = { |
| "is_valid": False, |
| "decimals": 0, |
| "supply": 0, |
| "is_initialized": False, |
| "mint_authority": None, |
| "mint_authority_revoked": True, |
| "freeze_authority": None, |
| "freeze_authority_revoked": True, |
| "risk_flags": [], |
| "extensions": [], |
| "raw_size": len(raw_data), |
| } |
|
|
| if len(raw_data) < 82: |
| result["error"] = f"Data too short for SPL mint: {len(raw_data)} bytes (expected >= 82)" |
| return result |
|
|
| try: |
| |
| mint_auth_option = int.from_bytes(raw_data[0:4], "little") |
| if mint_auth_option == 1: |
| result["mint_authority"] = decode_pubkey(raw_data, 4) |
| result["mint_authority_revoked"] = False |
| result["risk_flags"].append("mint_authority_present") |
|
|
| |
| supply = int.from_bytes(raw_data[36:44], "little") |
| result["supply"] = supply |
|
|
| |
| result["decimals"] = raw_data[44] |
|
|
| |
| result["is_initialized"] = raw_data[45] == 1 |
|
|
| |
| freeze_auth_option = int.from_bytes(raw_data[46:50], "little") |
| if freeze_auth_option == 1: |
| result["freeze_authority"] = decode_pubkey(raw_data, 50) |
| result["freeze_authority_revoked"] = False |
| result["risk_flags"].append("freeze_authority_present") |
|
|
| result["is_valid"] = result["is_initialized"] |
|
|
| |
| if len(raw_data) > 82: |
| result["extensions"] = _parse_token_extensions(raw_data[82:], result) |
|
|
| except Exception as e: |
| result["error"] = f"Failed to parse mint data: {e!s}" |
|
|
| return result |
|
|
|
|
| def _parse_token_extensions(extension_data: bytes, base_result: dict[str, Any]) -> list[dict[str, Any]]: |
| """ |
| Parse Token-2022 extension data. |
| Extension layout: [extension_type (u16), length (u16), data (variable)] |
| """ |
| extensions = [] |
| offset = 0 |
|
|
| |
| EXTENSION_TYPES = { |
| 1: "Uninitialized", |
| 2: "TransferFeeConfig", |
| 3: "TransferFeeAmount", |
| 4: "MintCloseAuthority", |
| 5: "ConfidentialTransferMint", |
| 6: "ConfidentialTransferAccount", |
| 7: "DefaultAccountState", |
| 8: "ImmutableOwner", |
| 9: "MemoTransfer", |
| 10: "NonTransferable", |
| 11: "InterestBearingConfig", |
| 12: "CpiGuard", |
| 13: "PermanentDelegate", |
| 14: "NonTransferableAccount", |
| 15: "TransferHook", |
| 16: "TransferHookAccount", |
| 17: "ConfidentialTransferFeeConfig", |
| 18: "ConfidentialTransferFeeAmount", |
| 19: "MetadataPointer", |
| 20: "TokenMetadata", |
| 21: "GroupPointer", |
| 22: "GroupMemberPointer", |
| } |
|
|
| while offset + 4 <= len(extension_data): |
| try: |
| ext_type = int.from_bytes(extension_data[offset : offset + 2], "little") |
| ext_length = int.from_bytes(extension_data[offset + 2 : offset + 4], "little") |
|
|
| ext_name = EXTENSION_TYPES.get(ext_type, f"Unknown({ext_type})") |
| ext_info = {"type": ext_type, "name": ext_name, "length": ext_length} |
|
|
| |
| if ext_type == 4: |
| ext_info["risk"] = "high" |
| ext_info["description"] = "Mint can be closed by authority" |
| elif ext_type == 7: |
| |
| if offset + 4 + ext_length <= len(extension_data): |
| state = extension_data[offset + 4] |
| if state == 2: |
| ext_info["risk"] = "critical" |
| ext_info["description"] = "Accounts are frozen by default" |
| base_result["risk_flags"].append("default_account_state_frozen") |
| elif ext_type == 13: |
| ext_info["risk"] = "critical" |
| ext_info["description"] = "Permanent delegate can transfer any tokens" |
| base_result["risk_flags"].append("permanent_delegate_present") |
| elif ext_type == 2: |
| ext_info["risk"] = "medium" |
| ext_info["description"] = "Transfer fees can be modified by authority" |
| base_result["risk_flags"].append("transfer_fee_present") |
|
|
| extensions.append(ext_info) |
| offset += 4 + ext_length |
|
|
| except Exception: |
| break |
|
|
| return extensions |
|
|
|
|
| async def fetch_metaplex_metadata(mint: str, rpc_url: str) -> dict[str, Any] | None: |
| """ |
| Fetch Metaplex Token Metadata for a given mint. |
| Uses getProgramAccounts to find the metadata PDA. |
| """ |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
|
|
| metaplex_bytes = bytes.fromhex("".join(f"{ord(c):02x}" for c in METAPLEX_METADATA_PROGRAM)) |
| mint_bytes = bytes.fromhex("".join(f"{ord(c):02x}" for c in mint)) |
|
|
| |
| |
| seed1 = b"metadata" |
|
|
| |
| seeds = seed1 + metaplex_bytes + mint_bytes |
|
|
| |
| |
| for bump in range(255, -1, -1): |
| test_seeds = seeds + bytes([bump]) |
| hashlib.sha256(test_seeds).digest() |
| |
| |
| |
| pass |
|
|
| |
| payload = { |
| "jsonrpc": "2.0", |
| "id": 1, |
| "method": "getProgramAccounts", |
| "params": [ |
| METAPLEX_METADATA_PROGRAM, |
| { |
| "encoding": "base64", |
| "filters": [ |
| {"dataSize": 679}, |
| {"memcmp": {"offset": 1, "bytes": mint}}, |
| ], |
| }, |
| ], |
| } |
|
|
| try: |
| async with httpx.AsyncClient(timeout=10) as client: |
| response = await client.post(rpc_url, json=payload) |
| if response.status_code == 200: |
| data = response.json() |
| accounts = data.get("result", []) |
| if accounts: |
| account = accounts[0] |
| pubkey = account.get("pubkey") |
| account_data = account.get("account", {}).get("data", [""])[0] |
| raw_bytes = base64.b64decode(account_data) |
|
|
| |
| |
| |
| |
| |
|
|
| return { |
| "address": pubkey, |
| "raw_size": len(raw_bytes), |
| "note": "Full metadata parsing requires borsh deserialization", |
| } |
| except Exception as e: |
| logger.debug(f"Metaplex metadata fetch failed: {e}") |
|
|
| return None |
|
|
|
|
| async def decode_spl_token_metadata(mint: str, rpc_urls: list[str] | None = None) -> dict[str, Any]: |
| """ |
| Main decoder function. Fetches and parses SPL token metadata from public RPCs. |
| |
| Args: |
| mint: Solana token mint address (base58) |
| rpc_urls: List of RPC URLs to try (defaults to PUBLIC_RPC_ENDPOINTS) |
| |
| Returns: |
| Dict containing decoded metadata, authorities, and risk flags. |
| """ |
| if rpc_urls is None: |
| rpc_urls = PUBLIC_RPC_ENDPOINTS |
|
|
| result = { |
| "mint": mint, |
| "success": False, |
| "rpc_used": None, |
| "account_exists": False, |
| "decoded": {}, |
| "metadata": None, |
| "risk_score": 0, |
| "risk_flags": [], |
| "error": None, |
| } |
|
|
| payload = { |
| "jsonrpc": "2.0", |
| "id": 1, |
| "method": "getAccountInfo", |
| "params": [mint, {"encoding": "base64"}], |
| } |
|
|
| for rpc_url in rpc_urls: |
| try: |
| async with httpx.AsyncClient(timeout=8) as client: |
| response = await client.post(rpc_url, json=payload) |
|
|
| if response.status_code != 200: |
| continue |
|
|
| data = response.json() |
| error = data.get("error") |
| if error: |
| if "not found" in str(error.get("message", "")).lower(): |
| result["error"] = "Mint account not found" |
| return result |
| continue |
|
|
| value = data.get("result", {}).get("value") |
| if value is None: |
| result["error"] = "Mint account not found" |
| return result |
|
|
| result["account_exists"] = True |
| result["rpc_used"] = rpc_url |
|
|
| |
| owner = value.get("owner", "") |
| result["owner_program"] = owner |
|
|
| |
| account_data = value.get("data", [""])[0] |
| raw_bytes = base64.b64decode(account_data) |
|
|
| |
| decoded = parse_spl_mint_data(raw_bytes) |
| result["decoded"] = decoded |
| result["risk_flags"] = decoded.get("risk_flags", []) |
|
|
| |
| risk_score = 0 |
| for flag in result["risk_flags"]: |
| if flag == "mint_authority_present": |
| risk_score += 30 |
| elif flag == "freeze_authority_present": |
| risk_score += 40 |
| elif flag == "default_account_state_frozen": |
| risk_score += 50 |
| elif flag == "permanent_delegate_present": |
| risk_score += 60 |
| elif flag == "transfer_fee_present": |
| risk_score += 20 |
|
|
| |
| for ext in decoded.get("extensions", []): |
| if ext.get("risk") == "critical": |
| risk_score += 40 |
| elif ext.get("risk") == "high": |
| risk_score += 25 |
| elif ext.get("risk") == "medium": |
| risk_score += 15 |
|
|
| result["risk_score"] = min(100, risk_score) |
| result["success"] = True |
|
|
| |
| try: |
| meta = await fetch_metaplex_metadata(mint, rpc_url) |
| if meta: |
| result["metadata"] = meta |
| except Exception: |
| pass |
|
|
| return result |
|
|
| except httpx.TimeoutException: |
| continue |
| except Exception as e: |
| logger.debug(f"RPC {rpc_url} failed: {e}") |
| continue |
|
|
| result["error"] = "All RPC endpoints failed or timed out" |
| return result |
|
|
|
|
| |
|
|
|
|
| async def _spl_metadata_decoder_provider(mint: str = "", **kwargs) -> dict[str, Any] | None: |
| """ |
| DataBus provider function for SPL Token Metadata Decoder. |
| """ |
| if not mint: |
| return {"error": "mint address is required"} |
|
|
| |
| if len(mint) < 32 or len(mint) > 44: |
| return {"error": "Invalid mint address format"} |
|
|
| result = await decode_spl_token_metadata(mint) |
|
|
| if result["success"]: |
| return { |
| "mint": mint, |
| "source": "spl_metadata_decoder", |
| "tier": "free_api", |
| "is_local": False, |
| "data": result, |
| } |
|
|
| return {"error": result.get("error", "Failed to decode SPL token metadata")} |
|
|