├── .gitattributes ├── README.md ├── data ├── abi │ └── daily_claim_abi.json ├── private_keys.txt └── proxies.txt ├── main.py ├── modules ├── account.py ├── config.py ├── executor.py ├── generate_wallets.py ├── settings.py ├── utils.py └── withdraw_from_binance.py └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StarryNift自动交互脚本 2 | 3 | 4 | 5 | ## 主要功能 6 | 7 | - 邀请自动Mint 8 | - 每日签到 9 | - 每日抽奖 10 | - 每日Quests任务(关注、在线浏览) 11 | - 账号状态获取 12 | 13 | ## 使用方法 14 | 15 | - 设置代理,推荐直接使用梯子自带的代理,比如我的HTTP代理开放在本地的10809端口,就需要在`/data/proxies.txt`中设置如下代理,注意每个账号对应一个代理,代理可重复。 16 | 17 | ![image-20240507104344725](https://typora-mine.oss-cn-beijing.aliyuncs.com/typoraimage-20240507104344725.png) 18 | 19 | - 安装python环境,并下载该项目 20 | - 安装依赖 `pip install -r requirements.txt ` 21 | - 填入私钥 `/data/private_keys.txt` 22 | - 填入代理 `data/proxies.txt`(注意与私钥数量对应) 23 | - 邀请设置 `/modules/setting.py` 24 | - 运行 `python main.py` 25 | 26 | 27 | 28 | ## 其他说明 29 | 30 | ### 1、关于代理 31 | 32 | 经过实际测试,动态IP的流量消耗过大,推荐直接使用梯子自带的代理,比如我使用的V2ray,我的HTTP代理开放在本地的10809端口,就需要在/data/proxies.txt中设置如下代理 33 | 34 | ![image-20240507103612025](https://typora-mine.oss-cn-beijing.aliyuncs.com/typoraimage-20240507103612025.png) 35 | 36 | ### 2、关于CloudFlare验证码 37 | 38 | 原计划使用yescaptcha来处理CloudFlare验证码,但是实测CloudFlare验证码并没有进行严格的限制,不会造成太大的影响,因此暂不加入 39 | 40 | 41 | 42 | ### 3、原项目 43 | 修改自项目:https://github.com/3asyPe/starrynift-automation 在此致谢 44 | 45 |

46 | X (formerly Twitter) Follow 47 |

48 | 49 | 50 | -------------------------------------------------------------------------------- /data/abi/daily_claim_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "address", 13 | "name": "user", 14 | "type": "address" 15 | }, 16 | { 17 | "indexed": false, 18 | "internalType": "uint256", 19 | "name": "date", 20 | "type": "uint256" 21 | }, 22 | { 23 | "indexed": false, 24 | "internalType": "uint256", 25 | "name": "consecutiveDays", 26 | "type": "uint256" 27 | }, 28 | { 29 | "indexed": false, 30 | "internalType": "uint256", 31 | "name": "rewardXP", 32 | "type": "uint256" 33 | } 34 | ], 35 | "name": "UserSignedIn", 36 | "type": "event" 37 | }, 38 | { 39 | "inputs": [], 40 | "name": "admin", 41 | "outputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "", 45 | "type": "address" 46 | } 47 | ], 48 | "stateMutability": "view", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [ 53 | { 54 | "internalType": "address", 55 | "name": "user", 56 | "type": "address" 57 | } 58 | ], 59 | "name": "getConsecutiveSignInDays", 60 | "outputs": [ 61 | { 62 | "internalType": "uint256", 63 | "name": "", 64 | "type": "uint256" 65 | } 66 | ], 67 | "stateMutability": "view", 68 | "type": "function" 69 | }, 70 | { 71 | "inputs": [ 72 | { 73 | "internalType": "address", 74 | "name": "user", 75 | "type": "address" 76 | } 77 | ], 78 | "name": "getLastSignInDate", 79 | "outputs": [ 80 | { 81 | "internalType": "uint256", 82 | "name": "", 83 | "type": "uint256" 84 | } 85 | ], 86 | "stateMutability": "view", 87 | "type": "function" 88 | }, 89 | { 90 | "inputs": [ 91 | { 92 | "internalType": "uint256", 93 | "name": "day", 94 | "type": "uint256" 95 | } 96 | ], 97 | "name": "getRewardXP", 98 | "outputs": [ 99 | { 100 | "internalType": "uint256", 101 | "name": "", 102 | "type": "uint256" 103 | } 104 | ], 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "inputs": [], 110 | "name": "getSignInInterval", 111 | "outputs": [ 112 | { 113 | "internalType": "uint256", 114 | "name": "", 115 | "type": "uint256" 116 | } 117 | ], 118 | "stateMutability": "view", 119 | "type": "function" 120 | }, 121 | { 122 | "inputs": [ 123 | { 124 | "internalType": "address", 125 | "name": "user", 126 | "type": "address" 127 | } 128 | ], 129 | "name": "getTimeUntilNextSignIn", 130 | "outputs": [ 131 | { 132 | "internalType": "uint256", 133 | "name": "", 134 | "type": "uint256" 135 | } 136 | ], 137 | "stateMutability": "view", 138 | "type": "function" 139 | }, 140 | { 141 | "inputs": [ 142 | { 143 | "internalType": "address", 144 | "name": "user", 145 | "type": "address" 146 | } 147 | ], 148 | "name": "hasBrokenStreak", 149 | "outputs": [ 150 | { 151 | "internalType": "bool", 152 | "name": "", 153 | "type": "bool" 154 | } 155 | ], 156 | "stateMutability": "view", 157 | "type": "function" 158 | }, 159 | { 160 | "inputs": [], 161 | "name": "setDefaultRewards", 162 | "outputs": [], 163 | "stateMutability": "nonpayable", 164 | "type": "function" 165 | }, 166 | { 167 | "inputs": [ 168 | { 169 | "internalType": "uint256", 170 | "name": "_maxDays", 171 | "type": "uint256" 172 | } 173 | ], 174 | "name": "setMaxConsecutiveDays", 175 | "outputs": [], 176 | "stateMutability": "nonpayable", 177 | "type": "function" 178 | }, 179 | { 180 | "inputs": [ 181 | { 182 | "internalType": "uint256", 183 | "name": "_interval", 184 | "type": "uint256" 185 | } 186 | ], 187 | "name": "setSignInInterval", 188 | "outputs": [], 189 | "stateMutability": "nonpayable", 190 | "type": "function" 191 | }, 192 | { 193 | "inputs": [ 194 | { 195 | "internalType": "uint256", 196 | "name": "day", 197 | "type": "uint256" 198 | }, 199 | { 200 | "internalType": "uint256", 201 | "name": "rewardXP", 202 | "type": "uint256" 203 | } 204 | ], 205 | "name": "setSignInReward", 206 | "outputs": [], 207 | "stateMutability": "nonpayable", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": [], 212 | "name": "signIn", 213 | "outputs": [], 214 | "stateMutability": "nonpayable", 215 | "type": "function" 216 | }, 217 | { 218 | "inputs": [ 219 | { 220 | "internalType": "uint256", 221 | "name": "day", 222 | "type": "uint256" 223 | }, 224 | { 225 | "internalType": "uint256", 226 | "name": "reward", 227 | "type": "uint256" 228 | } 229 | ], 230 | "name": "updateSignReward", 231 | "outputs": [], 232 | "stateMutability": "nonpayable", 233 | "type": "function" 234 | } 235 | ] -------------------------------------------------------------------------------- /data/private_keys.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b1n4he/StarryNiftAuto/9591687d8f9b5b6a3f9a8e5d074dea51061ae992/data/private_keys.txt -------------------------------------------------------------------------------- /data/proxies.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b1n4he/StarryNiftAuto/9591687d8f9b5b6a3f9a8e5d074dea51061ae992/data/proxies.txt -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from questionary import Choice 4 | import questionary 5 | from modules.config import PRIVATE_KEYS, PROXIES 6 | from modules.executor import Executor 7 | 8 | 9 | def get_module(executor: Executor): 10 | choices = [ 11 | Choice(f"{i}) {key}", value) 12 | for i, (key, value) in enumerate( 13 | { 14 | # "Generate wallets": executor.generate_wallets, 15 | # "Withdraw BNB from Binance": executor.withdraw_from_binance, 16 | "签到StarryNift任务": executor.run_starrynift, 17 | "获取账号状态": executor.get_accounts_stats, 18 | "退出": "exit", 19 | }.items(), 20 | start=1, 21 | ) 22 | ] 23 | result = questionary.select( 24 | "选择并回车执行:", 25 | choices=choices, 26 | qmark="🛠 ", 27 | pointer="✅ ", 28 | ).ask() 29 | if result == "exit": 30 | sys.exit() 31 | return result 32 | 33 | 34 | async def main(module): 35 | await module() 36 | 37 | 38 | if __name__ == "__main__": 39 | executor = Executor(PRIVATE_KEYS, PROXIES) 40 | module = get_module(executor) 41 | asyncio.run(main(module)) 42 | -------------------------------------------------------------------------------- /modules/account.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import time 4 | from eth_account import Account as EthAccount 5 | from eth_account.messages import encode_defunct 6 | from loguru import logger 7 | from modules.utils import retry 8 | from modules.settings import BNB_RPC, DISABLE_SSL, OPBNB_RPC, REF_LINK, USER_IDS_TO_FOLLOW 9 | from modules.config import DAILY_CLAIM_ABI 10 | import aiohttp 11 | 12 | import datetime 13 | 14 | from web3 import AsyncWeb3 15 | from web3.middleware import async_geth_poa_middleware 16 | from web3.exceptions import TransactionNotFound 17 | 18 | 19 | class Account: 20 | def __init__(self, id: int, key: str, proxy: str, user_agent: str): 21 | self.headers = { 22 | "Accept-Encoding": "gzip, deflate, br", 23 | "Accept-Language": "ru;q=0.9,en-US;q=0.8,en;q=0.7", 24 | "Connection": "keep-alive", 25 | "Content-Type": "application/json;charset=UTF-8", 26 | "Host": "api.starrynift.art", 27 | "Origin": "https://starrynift.art", 28 | "Referer": "https://starrynift.art/", 29 | "Sec-Ch-Ua-Mobile": "?0", 30 | "Sec-Ch-Ua-Platform": '"Windows"', 31 | "Sec-Fetch-Dest": "empty", 32 | "Sec-Fetch-Mode": "cors", 33 | "Sec-Fetch-Site": "same-site", 34 | "User-Agent": user_agent, 35 | } 36 | 37 | self.id = id 38 | self.key = key 39 | self.proxy = proxy 40 | self.account = EthAccount.from_key(self.key) 41 | self.address = self.account.address 42 | self.user_agent = user_agent 43 | 44 | self.referral_code = REF_LINK.split("=")[1] 45 | 46 | self.w3 = AsyncWeb3( 47 | AsyncWeb3.AsyncHTTPProvider(BNB_RPC), 48 | middlewares=[async_geth_poa_middleware], 49 | ) 50 | 51 | self.quests_mapping = { 52 | "Follow": self.follow_user, 53 | "Online": self.complete_online_quest, 54 | # "GM": self.good_morning, 55 | # "Invite": self.invite_10, 56 | # "Explore": self.explore_user, 57 | } 58 | 59 | self.user_id = None 60 | 61 | async def make_request(self, method, url, **kwargs): 62 | if DISABLE_SSL: 63 | kwargs["ssl"] = False 64 | 65 | async with aiohttp.ClientSession( 66 | headers=self.headers, trust_env=True 67 | ) as session: 68 | return await session.request( 69 | method, url, proxy=f"http://{self.proxy}", **kwargs 70 | ) 71 | 72 | def get_current_date(self, utc=False): 73 | if utc: 74 | return datetime.datetime.utcnow().strftime("%Y%m%d") 75 | return datetime.datetime.now().strftime("%Y-%m-%d") 76 | 77 | def get_utc_timestamp(self): 78 | return ( 79 | datetime.datetime.now(datetime.timezone.utc).strftime( 80 | "%Y-%m-%dT%H:%M:%S.%f" 81 | )[:-3] 82 | + "Z" 83 | ) 84 | 85 | async def wait_until_tx_finished( 86 | self, hash: str, max_wait_time=480, web3=None 87 | ) -> None: 88 | if web3 is None: 89 | web3 = self.w3 90 | 91 | start_time = time.time() 92 | while True: 93 | try: 94 | receipts = await web3.eth.get_transaction_receipt(hash) 95 | status = receipts.get("status") 96 | if status == 1: 97 | logger.success(f"[{self.address}] {hash.hex()} successfully!") 98 | return receipts["transactionHash"].hex() 99 | elif status is None: 100 | await asyncio.sleep(0.3) 101 | else: 102 | logger.error( 103 | f"[{self.id}][{self.address}] {hash.hex()} transaction failed! {receipts}" 104 | ) 105 | return None 106 | except TransactionNotFound: 107 | if time.time() - start_time > max_wait_time: 108 | logger.error( 109 | f"[{self.id}][{self.address}]{hash.hex()} transaction failed!" 110 | ) 111 | return None 112 | await asyncio.sleep(1) 113 | 114 | async def send_data_tx( 115 | self, to, from_, data, gas_price=None, gas_limit=None, nonce=None, chain_id=None 116 | ): 117 | if chain_id == 56: 118 | web3 = self.w3 119 | elif chain_id == 204: 120 | web3 = AsyncWeb3( 121 | AsyncWeb3.AsyncHTTPProvider(OPBNB_RPC), 122 | middlewares=[async_geth_poa_middleware], 123 | ) 124 | else: 125 | raise ValueError("Invalid chain id") 126 | 127 | transaction = { 128 | "to": to, 129 | "from": from_, 130 | "data": data, 131 | "gasPrice": gas_price or web3.to_wei(await web3.eth.gas_price, "gwei"), 132 | "gas": gas_limit or await web3.eth.estimate_gas({"to": to, "data": data}), 133 | "nonce": nonce or await web3.eth.get_transaction_count(self.address), 134 | "chainId": chain_id or await web3.eth.chain_id, 135 | } 136 | 137 | signed_transaction = web3.eth.account.sign_transaction(transaction, self.key) 138 | try: 139 | transaction_hash = await web3.eth.send_raw_transaction( 140 | signed_transaction.rawTransaction 141 | ) 142 | tx_hash = await self.wait_until_tx_finished( 143 | transaction_hash, max_wait_time=480, web3=web3 144 | ) 145 | if tx_hash is None: 146 | return False, None 147 | return True, tx_hash 148 | except Exception as e: 149 | logger.error(f"[{self.id}][{self.address}] Error while sending tx | {e}") 150 | return e, None 151 | 152 | def sign_msg(self, msg): 153 | return self.w3.eth.account.sign_message( 154 | (encode_defunct(text=msg)), self.key 155 | ).signature.hex() 156 | 157 | async def get_login_signature_message(self): 158 | req = await self.make_request( 159 | "get", 160 | f"https://api.starrynift.art/api-v2/starryverse/auth/wallet/challenge?address={self.address}", 161 | ) 162 | message = (await req.json()).get("message") 163 | if message is None: 164 | raise RuntimeError("Error while getting signature message") 165 | return message 166 | 167 | @retry 168 | async def get_mint_signature(self): 169 | response = await self.make_request( 170 | "post", 171 | "https://api.starrynift.art/api-v2/citizenship/citizenship-card/sign", 172 | json={"category": 1}, 173 | ) 174 | 175 | if response.status not in (200, 201): 176 | raise RuntimeError( 177 | f"Error while getting mint signature message | {await response.text()}" 178 | ) 179 | 180 | return (await response.json()).get("signature") 181 | 182 | @retry 183 | async def login(self): 184 | logger.info(f"[{self.id}][{self.address}] Logging in...") 185 | 186 | signature = self.sign_msg(await self.get_login_signature_message()) 187 | 188 | response = await self.make_request( 189 | "post", 190 | "https://api.starrynift.art/api-v2/starryverse/auth/wallet/evm/login", 191 | json={ 192 | "address": self.address, 193 | "signature": signature, 194 | "referralCode": self.referral_code, 195 | "referralSource": 0, 196 | }, 197 | ) 198 | res_json = await response.json() 199 | auth_token = res_json.get("token") 200 | 201 | if auth_token: 202 | self.headers["Authorization"] = f"Bearer {auth_token}" 203 | else: 204 | raise RuntimeError(f"Error while logging in") 205 | 206 | info = await self.get_current_user_info() 207 | self.user_id = info["userId"] 208 | 209 | return bool(auth_token) 210 | 211 | async def mint_nft_pass(self): 212 | logger.info(f"[{self.id}][{self.address}] Minting pass...") 213 | 214 | signature = await self.get_mint_signature() 215 | 216 | status, tx_hash = await self.send_mint_tx(signature) 217 | 218 | if status is True and await self.send_mint_tx_hash(tx_hash): 219 | logger.success(f"[{self.id}][{self.address}] | Pass minted: {tx_hash}") 220 | return True 221 | 222 | logger.error( 223 | f"[{self.id}][{self.address}] | Error while minting pass: {status}" 224 | ) 225 | return False 226 | 227 | @retry 228 | async def send_mint_tx(self, signature): 229 | return await self.send_data_tx( 230 | to="0xC92Df682A8DC28717C92D7B5832376e6aC15a90D", 231 | from_=self.address, 232 | data=f"0xf75e03840000000000000000000000000000000000000000000000000000000000000020000000000000000000000000{self.address[2:]}000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041{signature[2:]}00000000000000000000000000000000000000000000000000000000000000", 233 | gas_price=self.w3.to_wei(2, "gwei"), 234 | gas_limit=210000, 235 | chain_id=56, 236 | ) 237 | 238 | @retry 239 | async def check_if_pass_is_minted(self): 240 | logger.info( 241 | f"[{self.id}][{self.address}] Checking if pass has already been minted..." 242 | ) 243 | 244 | response = await self.make_request( 245 | "get", 246 | f"https://api.starrynift.art/api-v2/citizenship/citizenship-card/check-card-minted?address={self.address}", 247 | ) 248 | 249 | if response.status not in (200, 201): 250 | raise RuntimeError( 251 | f"Error while checking if pass is minted | {await response.text()}" 252 | ) 253 | 254 | return (await response.json()).get("isMinted") 255 | 256 | @retry 257 | async def send_mint_tx_hash(self, tx_hash): 258 | resp = await self.make_request( 259 | "post", 260 | "https://api.starrynift.art/api-v2/webhook/confirm/citizenship/mint", 261 | json={"txHash": tx_hash}, 262 | ) 263 | if resp.status not in (200, 201) or (await resp.json()).get("ok") != 1: 264 | raise RuntimeError( 265 | f"Error while sending mint tx hash | {await resp.text()}" 266 | ) 267 | 268 | return True 269 | 270 | async def daily_claim(self): 271 | logger.info(f"[{self.id}][{self.address}] Checking in...") 272 | 273 | time_to_claim = await self.get_daily_claim_time() 274 | if time_to_claim > 0: 275 | logger.info( 276 | f"[{self.id}][{self.address}] Next claim in {datetime.timedelta(seconds=time_to_claim)}" 277 | ) 278 | return 279 | 280 | result = await self.send_daily_tx() 281 | if result is None: 282 | logger.error(f"[{self.id}][{self.address}] Failed daily check in") 283 | return 284 | 285 | status, tx_hash = result 286 | 287 | if status is True and await self.send_daily_tx_hash(tx_hash): 288 | logger.success(f"[{self.id}][{self.address}] Successfully daily checked in") 289 | else: 290 | logger.error(f"[{self.id}][{self.address}] Failed daily check in") 291 | 292 | @retry 293 | async def send_daily_tx(self): 294 | status, hash = await self.send_data_tx( 295 | to="0xE3bA0072d1da98269133852fba1795419D72BaF4", 296 | from_=self.address, 297 | data=f"0x9e4cda43", 298 | gas_price=self.w3.to_wei(2, "gwei"), 299 | gas_limit=100000, 300 | chain_id=56, 301 | ) 302 | 303 | if not status: 304 | raise RuntimeError(f"Error while sending daily tx | {hash}") 305 | 306 | return status, hash 307 | 308 | @retry 309 | async def send_daily_tx_hash(self, tx_hash): 310 | resp = await self.make_request( 311 | "post", 312 | "https://api.starrynift.art/api-v2/webhook/confirm/daily-checkin/checkin", 313 | json={"txHash": tx_hash}, 314 | ) 315 | 316 | if resp.status not in (200, 201) or (await resp.json()).get("ok") != 1: 317 | raise RuntimeError( 318 | f"Error while sending daily tx hash | {await resp.text()}" 319 | ) 320 | 321 | return True 322 | 323 | @retry 324 | async def get_daily_claim_time(self): 325 | contract = self.w3.eth.contract( 326 | address=self.w3.to_checksum_address( 327 | "0xe3ba0072d1da98269133852fba1795419d72baf4" 328 | ), 329 | abi=DAILY_CLAIM_ABI, 330 | ) 331 | return await contract.functions.getTimeUntilNextSignIn(self.address).call() 332 | 333 | async def complete_quests(self): 334 | quests = await self.get_quests() 335 | 336 | for item in quests: 337 | quest = item["name"] 338 | if not item["completed"]: 339 | logger.info(f"[{self.id}][{self.address}] Completing quest: {quest}") 340 | try: 341 | func = self.quests_mapping.get(quest) 342 | if func is None: 343 | logger.warning(f"Quest {quest} is not supported") 344 | continue 345 | result = await func() 346 | if result is False: 347 | logger.error( 348 | f"[{self.id}][{self.address}] {quest} Quest Failed" 349 | ) 350 | else: 351 | logger.success( 352 | f"[{self.id}][{self.address}] {quest} Quest Completed" 353 | ) 354 | except RuntimeError as e: 355 | logger.error( 356 | f"[{self.id}][{self.address}] {quest} Quest Failed | {e}" 357 | ) 358 | else: 359 | logger.info( 360 | f"[{self.id}][{self.address}] {quest} Quest Already Compeleted" 361 | ) 362 | 363 | @retry 364 | async def get_quests(self): 365 | response = await self.make_request( 366 | "get", 367 | f"https://api.starrynift.art/api-v2/citizenship/citizenship-card/daily-tasks?page=1&page_size=10", 368 | ) 369 | 370 | if response.status not in (200, 201) or "items" not in await response.json(): 371 | raise RuntimeError(f"Error while getting quests | {await response.text()}") 372 | 373 | return (await response.json()).get("items") 374 | 375 | @retry 376 | async def follow_user(self): 377 | user_to_follow = None 378 | for user_id in USER_IDS_TO_FOLLOW: 379 | info = await self.get_user_info(user_id) 380 | if info["userId"] != self.user_id and not info["isFollow"]: 381 | user_to_follow = user_id 382 | break 383 | 384 | if user_to_follow is None: 385 | logger.error( 386 | f"[{self.id}][{self.address}] Already followed all users. Can't complete quest" 387 | ) 388 | return False 389 | 390 | response = await self.make_request( 391 | "post", 392 | "https://api.starrynift.art/api-v2/starryverse/user/follow", 393 | json={"userId": user_to_follow}, 394 | ) 395 | 396 | if response.status not in (200, 201) or (await response.json()).get("ok") != 1: 397 | raise RuntimeError(f"Error while following user | {await response.text()}") 398 | 399 | return True 400 | 401 | @retry 402 | async def get_user_info(self, user_id): 403 | response = await self.make_request( 404 | "get", 405 | f"https://api.starrynift.art/api-v2/starryverse/character/user/{user_id}", 406 | ) 407 | 408 | if response.status not in (200, 201): 409 | raise RuntimeError( 410 | f"Error while getting user info | {await response.text()}" 411 | ) 412 | 413 | return await response.json() 414 | 415 | @retry 416 | async def get_current_user_info(self): 417 | response = await self.make_request( 418 | "get", 419 | f"https://api.starrynift.art/api-v2/starryverse/character", 420 | ) 421 | 422 | if response.status not in (200, 201): 423 | raise RuntimeError( 424 | f"Error while getting user info | {await response.text()}" 425 | ) 426 | 427 | return await response.json() 428 | 429 | async def complete_online_quest(self): 430 | logger.info(f"[{self.id}][{self.address}] It would take about 10 minutes...") 431 | for i in range(21): 432 | await self.send_ping() 433 | await asyncio.sleep(30) 434 | 435 | return True 436 | 437 | @retry 438 | async def send_ping(self): 439 | response = await self.make_request( 440 | "get", 441 | f"https://api.starrynift.art/api-v2/space/online/ping", 442 | ) 443 | 444 | if response.status not in (200, 201): 445 | raise RuntimeError(f"Error while sending ping | {await response.text()}") 446 | 447 | return True 448 | 449 | async def get_if_already_ruffled_today(self): 450 | response = await self.make_request( 451 | "post", 452 | f"https://api.starrynift.art/api-v2/citizenship/raffle/status", 453 | json={}, 454 | ) 455 | 456 | if response.status not in (200, 201): 457 | raise RuntimeError( 458 | f"Error while checking if ruffled today | {await response.text()}" 459 | ) 460 | 461 | return (await response.json()).get("used") 462 | 463 | async def ruffle(self): 464 | logger.info(f"[{self.id}][{self.address}] Ruffling...") 465 | 466 | await asyncio.sleep(random.randint(3, 10)) 467 | info = await self.get_ruffle_info() 468 | if info["used"]: 469 | logger.info(f"[{self.id}][{self.address}] Already used free ruffle today") 470 | return 471 | 472 | if not info["signature"]: 473 | logger.error(f"[{self.id}][{self.address}] Daily wasn't completed") 474 | return 475 | logger.info(f"[{self.id}][{self.address}] Ruffle xp: {info['xp']}") 476 | 477 | result = await self.send_ruffle_tx( 478 | xp=info["xp"], 479 | signature=info["signature"], 480 | nonce=info["nonce"], 481 | ) 482 | if result is None: 483 | logger.error(f"[{self.id}][{self.address}] Ruffle failed") 484 | return 485 | 486 | status, tx_hash = result 487 | 488 | await self.send_ruffle_hash(tx_hash) 489 | 490 | logger.success(f"[{self.id}][{self.address}] Ruffle success") 491 | return True 492 | 493 | @retry 494 | async def send_ruffle_tx(self, xp, nonce, signature): 495 | data = ( 496 | "0x9fc96c7e" 497 | "0000000000000000000000000000000000000000000000000000000000000020" 498 | f"000000000000000000000000{self.address[2:]}" 499 | f"{format(xp, '064x')}" 500 | f"{format(int(nonce), '064x')}" 501 | "0000000000000000000000000000000000000000000000000000000000000080" 502 | f"0000000000000000000000000000000000000000000000000000000000000041{signature[2:]}" 503 | "00000000000000000000000000000000000000000000000000000000000000" 504 | ) 505 | 506 | status, tx_hash = await self.send_data_tx( 507 | to=self.w3.to_checksum_address( 508 | "0x557764618fc2f4eca692d422ba79c70f237113e6" 509 | ), 510 | from_=self.address, 511 | data=data, 512 | gas_price=self.w3.to_wei("0.00002", "gwei"), 513 | gas_limit=100000, 514 | chain_id=204, 515 | ) 516 | if not status: 517 | raise RuntimeError(f"Error while sending ruffle tx | {tx_hash}") 518 | 519 | return status, tx_hash 520 | 521 | @retry 522 | async def send_ruffle_hash(self, tx_hash): 523 | response = await self.make_request( 524 | "post", 525 | "https://api.starrynift.art/api-v2/webhook/confirm/raffle/mint", 526 | json={"txHash": tx_hash}, 527 | ) 528 | 529 | if response.status not in (200, 201) or (await response.json()).get("ok") != 1: 530 | raise RuntimeError( 531 | f"Error while sending ruffle tx hash | {await response.text()}" 532 | ) 533 | 534 | return True 535 | 536 | @retry 537 | async def get_ruffle_info(self): 538 | response = await self.make_request( 539 | "post", 540 | f"https://api.starrynift.art/api-v2/citizenship/raffle/status", 541 | json={}, 542 | ) 543 | 544 | if response.status not in (200, 201): 545 | raise RuntimeError( 546 | f"Error while getting ruffle info | {await response.text()}" 547 | ) 548 | 549 | return await response.json() 550 | 551 | ################完善其他任务################################### 552 | # 每日任务——进入频道发送GM 553 | @retry 554 | async def good_morning(): 555 | return 556 | 557 | # 每日任务——邀请10个人进自己的空间(即使是使用自己的ID访问空间10次,依旧可以完成) 558 | @retry 559 | async def invite_10(): 560 | return 561 | 562 | # 每日任务——探索5个不同的空间(疑似访问自己的空间5次也可以) 563 | @retry 564 | async def explore_user(): 565 | return -------------------------------------------------------------------------------- /modules/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | with open("data/private_keys.txt", "r") as f: 6 | PRIVATE_KEYS = f.read().splitlines() 7 | 8 | with open("data/proxies.txt", "r") as f: 9 | PROXIES = f.read().splitlines() 10 | 11 | 12 | with open("data/abi/daily_claim_abi.json", "r") as f: 13 | DAILY_CLAIM_ABI = json.load(f) 14 | 15 | try: 16 | with open("data/cached_user_agents.json", "r") as f: 17 | CACHED_USER_AGENTS = json.load(f) 18 | except FileNotFoundError: 19 | CACHED_USER_AGENTS = {} 20 | -------------------------------------------------------------------------------- /modules/executor.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import random 4 | from loguru import logger 5 | from fake_useragent import UserAgent 6 | from modules.account import Account 7 | from modules.generate_wallets import generate_wallets 8 | from modules.withdraw_from_binance import withdraw_from_binance 9 | from modules.settings import SHUFFLE_ACCOUNTS, THREADS 10 | from modules.config import CACHED_USER_AGENTS 11 | from modules.utils import sleep 12 | 13 | 14 | class Executor: 15 | def __init__(self, wallets: list[str], proxies: list[str]): 16 | self.accounts = self._load_accounts(wallets, proxies) 17 | 18 | async def generate_wallets(self): 19 | await generate_wallets() 20 | 21 | async def withdraw_from_binance(self): 22 | for i, account in enumerate(self.accounts, start=1): 23 | await withdraw_from_binance(address=account.address, proxy=account.proxy) 24 | 25 | if i != len(self.accounts): 26 | await sleep(account) 27 | 28 | async def run_starrynift(self): 29 | groups= self._generate_groups() 30 | 31 | tasks= [] 32 | for id, group in enumerate(groups): 33 | tasks.append( 34 | asyncio.create_task( 35 | self._run_starrynift(group, id), 36 | name=f"group - {id}", 37 | ) 38 | ) 39 | 40 | await asyncio.gather(*tasks) 41 | 42 | async def _run_starrynift(self, group: list[Account], group_id: int): 43 | for i, account in enumerate(group): 44 | if i != 0 or group_id != 0: 45 | await sleep(account) 46 | 47 | logger.info(f"Running #{account.id} account: {account.address}") 48 | if await account.login(): 49 | if not await account.check_if_pass_is_minted(): 50 | if not await account.mint_nft_pass(): 51 | continue 52 | 53 | await account.daily_claim() 54 | await account.ruffle() 55 | await account.complete_quests() 56 | 57 | async def get_accounts_stats(self): 58 | stats= {} 59 | for i, account in enumerate(self.accounts, start=1): 60 | logger.info(f"[{account.id}][{account.address}] Getting stats...") 61 | if await account.login(): 62 | info= await account.get_current_user_info() 63 | stats[account.address]= { 64 | "userId": info["userId"], 65 | "level": info["level"], 66 | "xp": info["xp"], 67 | "referralCode": info["referralCode"], 68 | } 69 | 70 | with open("data/stats.json", "w") as f: 71 | f.write(json.dumps(stats, indent=4)) 72 | 73 | logger.info("Stats saved to data/stats.json") 74 | 75 | def _generate_groups(self): 76 | global THREADS 77 | 78 | if THREADS <= 0: 79 | THREADS = 1 80 | elif THREADS > len(self.accounts): 81 | THREADS = len(self.accounts) 82 | 83 | group_size = len(self.accounts) // THREADS 84 | remainder = len(self.accounts) % THREADS 85 | 86 | groups = [] 87 | start = 0 88 | for i in range(THREADS): 89 | # Add an extra account to some groups to distribute the remainder 90 | end = start + group_size + (1 if i < remainder else 0) 91 | groups.append(self.accounts[start:end]) 92 | start = end 93 | 94 | return groups 95 | 96 | def _load_accounts(self, wallets: list[str], proxies: list[str]) -> list[Account]: 97 | #print(wallets, proxies) 98 | accounts = [] 99 | for i, (wallet, proxy) in enumerate(zip(wallets, proxies), start=1): 100 | 101 | user_agent = CACHED_USER_AGENTS.get(wallet) 102 | 103 | if user_agent is not None: 104 | accounts.append( 105 | Account(id=i, key=wallet, proxy=proxy, user_agent=user_agent) 106 | ) 107 | else: 108 | user_agent = UserAgent(os="windows").random 109 | CACHED_USER_AGENTS[wallet] = user_agent 110 | accounts.append( 111 | Account(id=i, key=wallet, proxy=proxy, user_agent=user_agent) 112 | ) 113 | 114 | with open("data/cached_user_agents.json", "w") as f: 115 | f.write(json.dumps(CACHED_USER_AGENTS, indent=4)) 116 | 117 | if SHUFFLE_ACCOUNTS: 118 | random.shuffle(accounts.wallet) 119 | 120 | return accounts 121 | -------------------------------------------------------------------------------- /modules/generate_wallets.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from loguru import logger 3 | 4 | from hdwallet import BIP44HDWallet 5 | from hdwallet.cryptocurrencies import EthereumMainnet 6 | from hdwallet.derivations import BIP44Derivation 7 | from hdwallet.utils import generate_mnemonic 8 | 9 | 10 | async def generate_wallets(): 11 | """ 创建钱包/私钥 12 | :return: 13 | """ 14 | num = input("How many wallets do you want to generate? ") 15 | logger.info(f"Generating {num} wallets") 16 | 17 | wallets = { 18 | "mnemonics": [], 19 | "addresses": [], 20 | "keys": [], 21 | } 22 | 23 | for i in range(int(num)): 24 | # Generate english mnemonic words 25 | MNEMONIC: str = generate_mnemonic(language="english", strength=128) 26 | # Secret passphrase/password for mnemonic 27 | PASSPHRASE: Optional[str] = None # "meherett" 28 | 29 | # Initialize Ethereum mainnet BIP44HDWallet 30 | bip44_hdwallet: BIP44HDWallet = BIP44HDWallet(cryptocurrency=EthereumMainnet) 31 | # Get Ethereum BIP44HDWallet from mnemonic 32 | bip44_hdwallet.from_mnemonic( 33 | mnemonic=MNEMONIC, language="english", passphrase=PASSPHRASE 34 | ) 35 | # Clean default BIP44 derivation indexes/paths 36 | bip44_hdwallet.clean_derivation() 37 | mnemonics = bip44_hdwallet.mnemonic() 38 | 39 | # Derivation from Ethereum BIP44 derivation path 40 | bip44_derivation: BIP44Derivation = BIP44Derivation( 41 | cryptocurrency=EthereumMainnet, account=0, change=False, address=0 42 | ) 43 | # Drive Ethereum BIP44HDWallet 44 | bip44_hdwallet.from_path(path=bip44_derivation) 45 | # Print address_index, path, address and private_key 46 | address = bip44_hdwallet.address() 47 | key = bip44_hdwallet.private_key() 48 | # Clean derivation indexes/paths 49 | bip44_hdwallet.clean_derivation() 50 | 51 | wallets["mnemonics"].append(mnemonics) 52 | wallets["addresses"].append(address) 53 | wallets["keys"].append(f"{key}") 54 | 55 | with open(f"data/generated_keys.txt", "w+") as f: 56 | for x in wallets["keys"]: 57 | f.write(f"0x{str(x)}\n") 58 | 59 | with open(f"data/generated_addresses.txt", "w+") as f: 60 | for x in wallets["addresses"]: 61 | f.write(f"{str(x)}\n") 62 | 63 | with open(f"data/generated_seeds.txt", "w+") as f: 64 | for x in wallets["mnemonics"]: 65 | f.write(f"{str(x)}\n") 66 | 67 | logger.success("Done generating wallets") 68 | -------------------------------------------------------------------------------- /modules/settings.py: -------------------------------------------------------------------------------- 1 | # StarryNift 邀请地址 2 | REF_LINK = "https://starrynift.art?referralCode=TDf8VqByWS" 3 | 4 | # 关注永续需要设置用户的ID,一下USER_IDS_TO_FOLLOW为需要关注的用户ID列表,你也可以设置自己的ID列表 5 | # 每个帐户每天将关注1个未关注的用户 6 | # 您可以使用“获取帐户统计信息”选项获取用户ID,设置自己的小号来关注 7 | USER_IDS_TO_FOLLOW = [ 8 | "6bwN6gPKwE", 9 | "wLCvTVtzC7", 10 | "gTSV3gtflS", 11 | "sZbhJ3m-S5", 12 | "qZiaScTXcp", 13 | "BvDh6oWLvv", 14 | "9ZkHSyATB0", 15 | "TciUEa7WV9", 16 | "GMC8KbdEXP", 17 | "k6r-Eeh1Ha", 18 | "sA9e90ApWU", 19 | "dgs7fK5YXN", 20 | "89B1uZG_a2", 21 | "zmnnh1mxzl", 22 | "oLxFYpMvTz", 23 | "lC2f0PduqS", 24 | "QgHXExKJKK", 25 | "KnK6dxUQRn", 26 | "7mT6s4-iVS", 27 | "_EiFdqJV1l", 28 | "3KElmquZzz", 29 | "eU2P0WiyLu", 30 | "E4MdLCmlHs", 31 | "hwTe4ekmCo", 32 | "LmeEU_m8Sq", 33 | "_YpzxoraSq", 34 | "bjyp7uE5sY", 35 | "Ut2-SP9vkK", 36 | "-KJVzigOzP", 37 | "NhZDgdRyRN", 38 | "AnzFNDe6_T", 39 | "tBXXXWMLUs", 40 | "z8tPhANWbZ", 41 | "G64Y2HZBix", 42 | "ENRSYgxcbg", 43 | "06LU5nZx_n", 44 | "ZY39pnH0sO", 45 | "BCO7TAo3YJ", 46 | "Lk47x2VK_C", 47 | "EKs0VON7v_", 48 | "gZoM7GBaIT", 49 | "CeJWB9d878", 50 | "dBt_77bSbq", 51 | "Na80WID3Ou", 52 | "rYpJRluc_l", 53 | "tGg7b_OL4x", 54 | "C-CbELi8RU", 55 | "aO7SJbh3Fi", 56 | "d71c3nmbg0", 57 | "OwZtyv3SxZ", 58 | ] 59 | 60 | DISABLE_SSL = False 61 | 62 | SHUFFLE_ACCOUNTS = False 63 | RETRIES = 2 64 | 65 | THREADS = 5 66 | 67 | MIN_SLEEP = 30 68 | MAX_SLEEP = 50 69 | 70 | BNB_RPC = "https://bsc.publicnode.com" 71 | OPBNB_RPC = "https://opbnb-rpc.publicnode.com" 72 | 73 | 74 | # 以下无用,无需修改 75 | #___________________________________________ 76 | # | BINANCE 划转 | 77 | 78 | BINANCE_API_KEY = "" 79 | BINANCE_SECRET_KEY = "" 80 | 81 | MIN_WITHDRAW = 0.01001 82 | MAX_WITHDRAW = 0.0105 83 | 84 | USE_PROXY_FOR_BINANCE = ( 85 | False # 如果为True你需要在binance的api中设置代理白名单,否则无法使用 86 | ) 87 | -------------------------------------------------------------------------------- /modules/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import traceback 4 | from loguru import logger 5 | 6 | from modules.settings import MAX_SLEEP, MIN_SLEEP, RETRIES 7 | 8 | async def sleep(account=None): 9 | sleep_time = random.randint(MIN_SLEEP, MAX_SLEEP) 10 | if account: 11 | logger.info( 12 | f"[{account.id}][{account.address}] Sleeping for {sleep_time} seconds" 13 | ) 14 | else: 15 | logger.info(f"Sleeping for {sleep_time} seconds") 16 | await asyncio.sleep(sleep_time) 17 | 18 | 19 | def retry(func): 20 | async def wrapper(*args, **kwargs): 21 | retries = 0 22 | while retries <= RETRIES: 23 | try: 24 | result = await func(*args, **kwargs) 25 | return result 26 | except Exception as e: 27 | traceback.print_exc() 28 | retries += 1 29 | logger.error(f"Error | {e}") 30 | if retries <= RETRIES: 31 | logger.info(f"Retrying... {retries}/{RETRIES}") 32 | await sleep() 33 | 34 | return wrapper 35 | -------------------------------------------------------------------------------- /modules/withdraw_from_binance.py: -------------------------------------------------------------------------------- 1 | import random 2 | import ccxt 3 | from loguru import logger 4 | 5 | from modules.settings import ( 6 | BINANCE_API_KEY, 7 | BINANCE_SECRET_KEY, 8 | MAX_WITHDRAW, 9 | MIN_WITHDRAW, 10 | USE_PROXY_FOR_BINANCE, 11 | ) 12 | 13 | 14 | async def withdraw_from_binance(address, proxy): 15 | client_params = { 16 | "apiKey": BINANCE_API_KEY, 17 | "secret": BINANCE_SECRET_KEY, 18 | "enableRateLimit": True, 19 | "options": {"defaultType": "spot"}, 20 | } 21 | 22 | amount = round(random.uniform(MIN_WITHDRAW, MAX_WITHDRAW), 6) 23 | 24 | if USE_PROXY_FOR_BINANCE: 25 | client_params["proxies"] = { 26 | "http": f"http://{proxy}", 27 | } 28 | 29 | ccxt_client = ccxt.binance(client_params) 30 | 31 | try: 32 | withdraw = ccxt_client.withdraw( 33 | code="BNB", 34 | amount=amount, 35 | address=address, 36 | tag=None, 37 | params={"network": "BEP20"}, 38 | ) 39 | logger.success( 40 | f"{ccxt_client.name} - {address} | withdraw {amount} BNB to BNB network)" 41 | ) 42 | 43 | except Exception as error: 44 | logger.error(f"{ccxt_client.name} - {address} | withdraw error : {error}") 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiodns==3.1.1 ; python_version >= "3.9" and python_version < "4.0" 2 | aiohttp==3.9.1 ; python_version >= "3.9" and python_version < "4.0" 3 | aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "4.0" 4 | async-timeout==4.0.3 ; python_version >= "3.9" and python_version < "3.11" 5 | attrs==23.2.0 ; python_version >= "3.9" and python_version < "4.0" 6 | base58==2.1.1 ; python_version >= "3.9" and python_version < "4" 7 | bitarray==2.9.2 ; python_version >= "3.9" and python_version < "4" 8 | ccxt==4.2.21 ; python_version >= "3.9" and python_version < "4.0" 9 | certifi==2023.11.17 ; python_version >= "3.9" and python_version < "4.0" 10 | cffi==1.16.0 ; python_version >= "3.9" and python_version < "4.0" 11 | charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "4.0" 12 | colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" 13 | cryptography==42.0.0 ; python_version >= "3.9" and python_version < "4.0" 14 | cytoolz==0.12.2 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython" 15 | ecdsa==0.18.0 ; python_version >= "3.9" and python_version < "4" 16 | eth-abi==5.0.0 ; python_version >= "3.9" and python_version < "4" 17 | eth-account==0.10.0 ; python_version >= "3.9" and python_version < "4" 18 | eth-hash==0.6.0 ; python_version >= "3.9" and python_version < "4" 19 | eth-hash[pycryptodome]==0.6.0 ; python_version >= "3.9" and python_version < "4" 20 | eth-keyfile==0.7.0 ; python_version >= "3.9" and python_version < "4" 21 | eth-keys==0.5.0 ; python_version >= "3.9" and python_version < "4" 22 | eth-rlp==1.0.0 ; python_version >= "3.9" and python_version < "4" 23 | eth-typing==4.0.0 ; python_version >= "3.9" and python_version < "4" 24 | eth-utils==3.0.0 ; python_version >= "3.9" and python_version < "4" 25 | fake-useragent==1.4.0 ; python_version >= "3.9" and python_version < "4.0" 26 | frozenlist==1.4.1 ; python_version >= "3.9" and python_version < "4.0" 27 | hdwallet==2.2.1 ; python_version >= "3.9" and python_version < "4" 28 | hexbytes==0.3.1 ; python_version >= "3.9" and python_version < "4" 29 | idna==3.6 ; python_version >= "3.9" and python_version < "4.0" 30 | importlib-resources==6.1.1 ; python_version >= "3.9" and python_version < "3.10" 31 | jsonschema-specifications==2023.12.1 ; python_version >= "3.9" and python_version < "4.0" 32 | jsonschema==4.21.1 ; python_version >= "3.9" and python_version < "4.0" 33 | loguru==0.7.2 ; python_version >= "3.9" and python_version < "4.0" 34 | lru-dict==1.2.0 ; python_version >= "3.9" and python_version < "4.0" 35 | mnemonic==0.21 ; python_version >= "3.9" and python_version < "4" 36 | multidict==6.0.4 ; python_version >= "3.9" and python_version < "4.0" 37 | parsimonious==0.9.0 ; python_version >= "3.9" and python_version < "4" 38 | prompt-toolkit==3.0.36 ; python_version >= "3.9" and python_version < "4.0" 39 | protobuf==4.25.2 ; python_version >= "3.9" and python_version < "4.0" 40 | pycares==4.4.0 ; python_version >= "3.9" and python_version < "4.0" 41 | pycparser==2.21 ; python_version >= "3.9" and python_version < "4.0" 42 | pycryptodome==3.20.0 ; python_version >= "3.9" and python_version < "4" 43 | pyunormalize==15.1.0 ; python_version >= "3.9" and python_version < "4.0" 44 | pywin32==306 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" 45 | questionary==2.0.1 ; python_version >= "3.9" and python_version < "4.0" 46 | referencing==0.32.1 ; python_version >= "3.9" and python_version < "4.0" 47 | regex==2023.12.25 ; python_version >= "3.9" and python_version < "4" 48 | requests==2.31.0 ; python_version >= "3.9" and python_version < "4.0" 49 | rlp==4.0.0 ; python_version >= "3.9" and python_version < "4" 50 | rpds-py==0.17.1 ; python_version >= "3.9" and python_version < "4.0" 51 | setuptools==69.0.3 ; python_version >= "3.9" and python_version < "4.0" 52 | six==1.16.0 ; python_version >= "3.9" and python_version < "4" 53 | toolz==0.12.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "pypy" or implementation_name == "cpython") 54 | typing-extensions==4.9.0 ; python_version >= "3.9" and python_version < "4.0" 55 | urllib3==2.1.0 ; python_version >= "3.9" and python_version < "4.0" 56 | wcwidth==0.2.13 ; python_version >= "3.9" and python_version < "4.0" 57 | web3==6.14.0 ; python_version >= "3.9" and python_version < "4.0" 58 | websockets==12.0 ; python_version >= "3.9" and python_version < "4.0" 59 | win32-setctime==1.1.0 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" 60 | yarl==1.9.4 ; python_version >= "3.9" and python_version < "4.0" 61 | zipp==3.17.0 ; python_version >= "3.9" and python_version < "3.10" 62 | --------------------------------------------------------------------------------