├── .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 | 
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 | 
35 |
36 | ### 2、关于CloudFlare验证码
37 |
38 | 原计划使用yescaptcha来处理CloudFlare验证码,但是实测CloudFlare验证码并没有进行严格的限制,不会造成太大的影响,因此暂不加入
39 |
40 |
41 |
42 | ### 3、原项目
43 | 修改自项目:https://github.com/3asyPe/starrynift-automation 在此致谢
44 |
45 |
46 |
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 |
--------------------------------------------------------------------------------