├── .env.example ├── .gitignore ├── .sessions └── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── app.js ├── config.js ├── index.js ├── log.js ├── output.js └── utils.js /.env.example: -------------------------------------------------------------------------------- 1 | # Private keys or mnemonic phrases 2 | # Example: PRIVATE_KEYS=private_key_1,private_key_2 3 | PRIVATE_KEYS= 4 | 5 | # Smart wallet address 6 | # You can get it from https://arcade.soniclabs.com/, 7 | # click wallet address button from the top navigation and copy the address. 8 | # THE COUNT OF SMART_WALLET_ADDRESS MUST MATCH THE NUMBER OF PRIVATE KEYS!!! 9 | # Example: SMART_WALLET_ADDRESS=0x1919191919191919119191919191919191919191 10 | SMART_WALLET_ADDRESS= 11 | 12 | # Proxies 13 | # You can get it from https://app.proxy-cheap.com/r/ksvW8Z 14 | # THE COUNT OF PROXIES MUST MATCH THE NUMBER OF PRIVATE KEYS!!! 15 | # Example: PROXIES=http://user:password@ip:port,http://user2:password2@ip2:port 16 | PROXIES= 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test.js 3 | sessions/* 4 | package-lock.json 5 | .DS_Store 6 | proxies.txt 7 | .env 8 | *.log 9 | *.out 10 | -------------------------------------------------------------------------------- /.sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 overtrue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Soniclabs Arcade Testnet BOT 2 | 3 | > [!warning] 4 | > This project has been archived and no longer maintained. 5 | 6 | [手把手中文使用教程](https://mirror.xyz/0xe8224b3E9C8d35b34D088BB5A216B733a5A6D9EA/pEf4ou_1otpEkc4V3E4a014cwKmaM8c8s6ODb_b0ipg) 7 | 8 | About Sonic Labs (Prev Fantom) 9 | 10 | - Register: [https://airdrop.soniclabs.com/](https://airdrop.soniclabs.com/?ref=tql092 ) 11 | - Connect Wallet to Sonic Testnet 12 | - Enter Access Code: `tql092` 13 | - Get Faucet : 14 | 15 | ## Features 16 | 17 | - Support multi accounts. 18 | - Support private key and mnemonic. 19 | - Support Proxy for each account. 20 | - Auto play games daily. 21 | 22 | ## Prerequisite 23 | 24 | - Git v2.13 or later 25 | - Node.js v18 or later 26 | 27 | ## Steps 28 | 29 | - Get faucet $S token from 30 | - After you get token, play all game at least once!!! 31 | - Get your smart wallet address from and copy it from top right navigation dropdown. 32 | 33 | > [!tip] 34 | > 35 | > REMEMBER TO CLAIM FAUCET WEEKLY 36 | 37 | ## Installation 38 | 39 | ### For Linux 40 | 41 | 1. Open your `Terminal`. 42 | 43 | 1. clone the repo and install dependencies 44 | 45 | ```bash 46 | git clone https://github.com/web3bothub/soniclabs-arcade-bot.git 47 | cd soniclabs-arcade-bot 48 | npm install 49 | ``` 50 | 51 | 1. configure your accounts 52 | 53 | ```bash 54 | cp .env.example .env 55 | vim .env 56 | ``` 57 | 58 | > [!tip] 59 | > Please read the comments in `.env` file and fill in the required information. 60 | 61 | 1. start the bot 62 | 63 | ```bash 64 | npm run start 65 | ``` 66 | 67 | ### For Windows 68 | 69 | 1. Open your `Command Prompt` or `Power Shell`. 70 | 1. clone the repo and install dependencies 71 | 72 | ```bash 73 | git clone https://github.com/web3bothub/soniclabs-arcade-bot.git 74 | cd soniclabs-arcade-bot 75 | npm install 76 | ``` 77 | 78 | 1. Navigate to `soniclabs-arcade-bot` directory. 79 | 1. cp `.env.example` to file `.env` 80 | 1. Now open `.env` and config your account private key or mnemonic, and optionally you can add proxy for each account. 81 | 1. Back to `soniclabs-arcade-bot` directory. 82 | 1. In your `Command Prompt` or `Power Shell`, run the bot: 83 | 84 | ```bash 85 | node src/index.js 86 | ``` 87 | 88 | ## Keep bot up to date 89 | 90 | To update bot, you need to pull the latest code from this repo and update the dependencies 91 | 92 | 1. pull the latest code 93 | 94 | ```bash 95 | cd soniclabs-arcade-bot 96 | git pull 97 | # or 98 | git pull --rebase 99 | ``` 100 | 101 | if you got conflict, you can stash your changes and pull again: 102 | 103 | ```bash 104 | git stash && git pull 105 | ``` 106 | 107 | 1. update dependencies 108 | 109 | ```bash 110 | npm update 111 | ``` 112 | 113 | 1. restart the bot 114 | 115 | ```bash 116 | npm run start 117 | ``` 118 | 119 | ## Note 120 | 121 | - Run this bot, and it will update your referrer code to my invite code if you don't have one. 122 | - You can just run this bot at your own risk, I'm not responsible for any loss or damage caused by this bot. This bot is for educational purposes only. 123 | 124 | ## Contribution 125 | 126 | Feel free to contribute to this project by creating a pull request. 127 | 128 | ## Support Me 129 | 130 | if you want to support me, you can donate to my address: 131 | 132 | - TRC20: `TMwJhT5iCsQAfmRRKmAfasAXRaUhPWTSCE` 133 | - ERC20: `0xa2f5b8d9689d20d452c5340745a9a2c0104c40de` 134 | - SOLANA: `HCbbrqD9Xvfqx7nWjNPaejYDtXFp4iY8PT7F4i8PpE5K` 135 | - TON: `UQBD-ms1jA9cmoo8O39BXI6jqh8zwRSoBMUAl4yjEPKD6ata` 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soniclabs-arcade-bot", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "soniclabs arcade bot for daily tasks", 6 | "keywords": [ 7 | "soniclabs-arcade", 8 | "soniclabs-arcade-bot", 9 | "soniclabs-arcade-tx-bot", 10 | "soniclabs-arcade-tasks", 11 | "overtrue" 12 | ], 13 | "main": "index.js", 14 | "dependencies": { 15 | "bip39": "^3.1.0", 16 | "dotenv": "^16.4.5", 17 | "ethers": "^6.13.2", 18 | "https-proxy-agent": "^7.0.5", 19 | "twisters": "^1.1.0", 20 | "winston": "^3.13.0" 21 | }, 22 | "scripts": { 23 | "start": "git reset --hard && git pull && node src/index.js", 24 | "dev": "node src/index.js" 25 | }, 26 | "author": "overtrue", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/web3bothub/soniclabs-arcade-bot.git" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { writeFile } from 'fs/promises' 3 | import { HttpsProxyAgent } from 'https-proxy-agent' 4 | import { CONTRACT as CONTRACT_ADDRESS, GAMES, PRIVATE_KEYS, REFERRER_CODE, RPC } from './config.js' 5 | import log from './log.js' 6 | import { getPrivateKeyType, getRandomUserAgent, toHumanTime, wait } from './utils.js' 7 | 8 | export default class App { 9 | constructor(account, smartAddress, proxy) { 10 | this.baseUrl = 'https://arcade.soniclabs.com' 11 | this.host = 'arcade.soniclabs.com' 12 | this.origin = 'https://arcade.soniclabs.com' 13 | this.userAgent = getRandomUserAgent() 14 | this.proxy = proxy 15 | this.sessionId = 1 16 | this.wallet = null 17 | this.today_points = null 18 | this.total_points = null 19 | this.account = account 20 | this.address = null 21 | this.smartAddress = smartAddress 22 | this.permitSignature = null 23 | this.referrerCode = REFERRER_CODE 24 | this.sessionKey = `./.sessions/${account}` 25 | this.limitedGames = { 26 | plinko: false, 27 | singlewheel: false, 28 | mines: false, 29 | } 30 | this.gameStatus = { 31 | plinko: { message: 'pending', waiting: '-' }, 32 | singlewheel: { message: 'pending', waiting: '-' }, 33 | mines: { message: 'pending', waiting: '-' }, 34 | } 35 | 36 | try { 37 | this.provider = new ethers.JsonRpcProvider(RPC.RPCURL, RPC.CHAINID) 38 | } catch (error) { 39 | log.error(this.account, `Failed to connect to testnet: ${error}`) 40 | } 41 | } 42 | 43 | async gameWait(game, milliseconds, message) { 44 | this.gameStatus[game].message = message 45 | this.gameStatus[game].waiting = toHumanTime(milliseconds) 46 | 47 | await wait(milliseconds, message, this) 48 | } 49 | 50 | async connect() { 51 | try { 52 | const cleanPrivateKey = this.account.replace(/^0x/, '') 53 | await wait(1500, 'Connecting to account: ' + (PRIVATE_KEYS.indexOf(this.account) + 1), this) 54 | const accountType = getPrivateKeyType(cleanPrivateKey) 55 | log.info(this.account, 'Account type: ' + accountType) 56 | 57 | if (accountType === 'Mnemonic') { 58 | this.wallet = ethers.Wallet.fromMnemonic(cleanPrivateKey, this.provider) 59 | } else if (accountType === 'Private Key') { 60 | this.wallet = new ethers.Wallet(cleanPrivateKey, this.provider) 61 | } else { 62 | throw new Error('Invalid account Secret Phrase or Private Key') 63 | } 64 | 65 | this.address = this.wallet.address 66 | await wait(1000, 'Wallet address: ' + JSON.stringify(this.address), this) 67 | } catch (error) { 68 | throw error 69 | } 70 | } 71 | 72 | async createSession() { 73 | await wait(1000, 'Creating session', this) 74 | 75 | const response = await this.fetch('https://arcade.hub.soniclabs.com/rpc', 'POST', { 76 | jsonrpc: '2.0', 77 | id: this.sessionId, 78 | method: 'createSession', 79 | params: { 80 | owner: this.wallet.address, 81 | until: Date.now() + 86400000 // 24 hours in milliseconds 82 | } 83 | }, { network: 'SONIC', pragma: 'no-cache', 'X-Owner': this.address }, 'https://arcade.soniclabs.com/', true) 84 | 85 | this.sessionId += 1 86 | if (response.status === 200) { 87 | writeFile(this.sessionKey, Date.now().toString()) 88 | await wait(1000, 'Successfully create session', this) 89 | } else { 90 | throw Error('Failed to create session') 91 | } 92 | } 93 | 94 | async getBalance(refresh = false) { 95 | try { 96 | if (!refresh) { 97 | await wait(500, 'Fetching balance of address: ' + this.wallet.address, this) 98 | } 99 | this.balance = ethers.formatEther(await this.provider.getBalance(this.wallet.address)) 100 | await wait(500, 'Balance updated: ' + this.balance, this) 101 | } catch (error) { 102 | log.error(this.account, `Failed to get balance: ${error}`) 103 | throw error 104 | } 105 | } 106 | 107 | async getUser() { 108 | await wait(1000, 'Fetching user information', this) 109 | const response = await this.fetch(`https://airdrop.soniclabs.com/api/trpc/user.findOrCreate?batch=1&input=${encodeURIComponent(JSON.stringify({ 0: { json: { address: this.wallet.address } } }))}`, 'GET') 110 | if (response.status == 200) { 111 | this.user = response[0].result.data.json 112 | await wait(500, 'User information retrieved successfully', this) 113 | } else { 114 | throw new Error('Failed to get user information') 115 | } 116 | } 117 | 118 | async getPoints() { 119 | if (!this.smartAddress) { 120 | await wait(500, 'Smart address not configured, skip', this) 121 | } 122 | await wait(1000, "Getting user points", this) 123 | const response = await this.fetch(`https://arcade.gateway.soniclabs.com/game/points-by-player?wallet=${this.smartAddress}`, 'GET', undefined, undefined, 'https://arcade.soniclabs.com/', true) 124 | 125 | if (response.status == 200) { 126 | this.today_points = response.today 127 | this.total_points = response.totalPoints 128 | await wait(1500, "Successfully get total points", this) 129 | } else { 130 | //throw Error("Failed to get points") 131 | } 132 | } 133 | 134 | async register() { 135 | try { 136 | wait(15000, 'Registering user key') 137 | const abi = new ethers.Interface([{ 138 | 'inputs': [{ 139 | 'internalType': "address", 140 | 'name': 'spender', 141 | 'type': "address" 142 | }, { 143 | 'internalType': "uint256", 144 | 'name': 'amount', 145 | 'type': "uint256" 146 | }], 147 | 'name': 'approve', 148 | 'outputs': [{ 149 | 'internalType': "bool", 150 | 'name': '', 151 | 'type': "bool" 152 | }], 153 | 'stateMutability': "nonpayable", 154 | 'type': 'function' 155 | }]) 156 | const data = abi.encodeFunctionData("approve", [this.address, ethers.MaxUint256]) 157 | const response = await this.fetch("https://arcade.hub.soniclabs.com/rpc", "POST", { 158 | 'jsonrpc': "2.0", 159 | 'id': 0x7, 160 | 'method': "call", 161 | 'params': { 162 | 'call': { 163 | 'dest': '0x4Cc7b0ddCD0597496E57C5325cf4c73dBA30cdc9', 164 | 'data': data, 165 | 'value': '0n' 166 | }, 167 | 'owner': this.address, 168 | 'part': this.part, 169 | 'permit': this.permitSignature 170 | } 171 | }, { 172 | 'network': "SONIC", 173 | 'pragma': "no-cache", 174 | 'priority': "u=1, i", 175 | 'X-Owner': this.address 176 | }, "https://arcade.soniclabs.com/", true) 177 | this.sessionId += 1 178 | if (response.status == 200) { 179 | await wait(1500, "User key registered", this) 180 | await this.getPoints() 181 | } else { 182 | await wait(1000, "Failed to register user key", this) 183 | await this.register() 184 | } 185 | } catch (error) { 186 | await this.register() 187 | } 188 | } 189 | 190 | async refund(game) { 191 | await wait(1500, `Refunding game ${game} to resolve awaiting random number`, this) 192 | const response = await this.fetch('https://arcade.hub.soniclabs.com/rpc', "POST", { 193 | 'jsonrpc': "2.0", 194 | 'id': this.sessionId, 195 | 'method': "refund", 196 | 'params': { 197 | 'game': game, 198 | 'player': this.smartAddress 199 | } 200 | }, { 201 | 'network': "SONIC", 202 | 'X-Owner': this.address 203 | }, "https://arcade.soniclabs.com/", true) 204 | this.sessionId += 0x1 205 | if (response.status == 0xc8) { 206 | await wait(2000, `Successfully refund game: ${game}`, this) 207 | } else { 208 | throw Error("Failed to Refund Game") 209 | } 210 | } 211 | 212 | async reIterate(game) { 213 | await wait(1500, `Reiterate game ${game} to resolve awaiting random number`, this) 214 | const response = await this.fetch("https://arcade.hub.soniclabs.com/rpc", "POST", { 215 | 'jsonrpc': '2.0', 216 | 'id': this.sessionId, 217 | 'method': "reIterate", 218 | 'params': { 219 | 'game': game, 220 | 'player': this.smartAddress 221 | } 222 | }, { 223 | 'network': "SONIC", 224 | 'X-Owner': this.address 225 | }, "https://arcade.soniclabs.com/", true) 226 | this.sessionId += 0x1 227 | if (response.status == 0xc8) { 228 | await wait(2000, `Successfully reiterate game: ${game}`, this) 229 | } else { 230 | throw Error(`Failed to reiterate game ${game}`) 231 | } 232 | } 233 | 234 | async connectToSonic() { 235 | await wait(500, 'Connecting to Sonic Arcade', this) 236 | 237 | const messageToSign = "I'm joining Sonic Airdrop Dashboard with my wallet, have been referred by " + this.referrerCode + ", and I agree to the terms and conditions.\nWallet address:\n" + this.address + "\n" 238 | log.info(this.account, 'Message to sign: ' + messageToSign) 239 | 240 | this.signatureMessage = await this.wallet.signMessage(messageToSign) 241 | log.info(this.account, 'signature: ' + this.signatureMessage) 242 | 243 | await wait(500, 'Successfully connected to Sonic Dapp', this) 244 | } 245 | 246 | async tryToUpdateReferrer() { 247 | try { 248 | await wait(100, 'Validating invite code', this) 249 | 250 | if (this.user.invitedCode == null) { 251 | const response = await this.fetch('/api/trpc/user.setInvited?batch=1', 'POST', { 252 | json: { address: this.wallet.address, invitedCode: this.invitedCode, signature: this.signatureMessage } 253 | }) 254 | 255 | if (response.status == 200) { 256 | await wait(1000, 'Successfully updated the invite code', this) 257 | await this.getUser() 258 | } 259 | } else { 260 | await wait(1000, 'Invite code already set', this) 261 | } 262 | } catch (error) { 263 | log.error(this.account, `Failed to update user invite code: ${error}`) 264 | } 265 | } 266 | 267 | async permitTypedMessage() { 268 | await wait(1000, 'Try to permit Sonic Arcade contract', this) 269 | const response = await this.fetch('https://arcade.hub.soniclabs.com/rpc', 'POST', { 270 | 'id': this.sessionId, 271 | 'jsonrpc': '2.0', 272 | 'method': 'permitTypedMessage', 273 | 'params': { 274 | 'owner': this.address 275 | } 276 | }, { 277 | 'network': 'SONIC', 278 | 'pragma': 'no-cache', 279 | 'priority': 'u=1, i', 280 | 'X-Owner': this.address 281 | }, 'https://arcade.soniclabs.com/', true) 282 | this.sessionId += 1 283 | 284 | if (!response.error && response.status == 200) { 285 | const message = JSON.parse(response.result.typedMessage) 286 | await wait(500, 'Successfully create permit', this) 287 | await wait(500, 'Approving permit message', this) 288 | this.permitSignature = await this.wallet.signTypedData(message.json.domain, message.json.types, message.json.message) 289 | await this.permit() 290 | } else { 291 | await wait(4000, 'Failed to permit Sonic Arcade contract, Maybe anti-bot protection, try to play the games on the website first.', this) 292 | await this.createNonce() 293 | } 294 | } 295 | 296 | async createNonce() { 297 | await wait(500, 'Creating nonce', this) 298 | const response = await this.fetch('https://arcade.hub.soniclabs.com/rpc', 'POST', { 299 | jsonrpc: '2.0', 300 | id: this.sessionId, 301 | method: 'createNonce', 302 | params: { 303 | owner: this.address 304 | } 305 | }, { network: 'SONIC', pragma: 'no-cache', 'X-Owner': this.address }, 'https://arcade.soniclabs.com/', true) 306 | 307 | this.sessionId += 1 308 | if (response.status == 200) { 309 | await wait(500, 'Successfully created nonce', this) 310 | } else { 311 | throw Error('Failed to create nonce, please play the games on the website first.') 312 | } 313 | } 314 | 315 | async performRpcRequest(method, params, headers, referer) { 316 | return this.fetch('https://arcade.hub.soniclabs.com/rpc', 'POST', { 317 | jsonrpc: '2.0', 318 | id: this.sessionId, 319 | method: method, 320 | params: params 321 | }, headers || { network: 'SONIC', pragma: 'no-cache', 'priority': 'u=1, i', 'X-Owner': this.address }, 'https://arcade.soniclabs.com/') 322 | } 323 | 324 | async permit() { 325 | await wait(500, 'Submitting contract permit', this) 326 | const response = await this.performRpcRequest('permit', { 327 | owner: this.address, 328 | signature: this.permitSignature 329 | }) 330 | this.sessionId += 1 331 | if (!response.error) { 332 | this.part = response.result.hashKey 333 | await wait(500, 'Permit submitted successfully', this) 334 | } else { 335 | throw new Error(`Failed to submit permit: ${response.error.message}`) 336 | } 337 | } 338 | 339 | async playPlinko() { 340 | await this.playGame('plinko') 341 | } 342 | 343 | async playSinglewheel() { 344 | await this.playGame('singlewheel') 345 | } 346 | 347 | async playMines() { 348 | await this.playGame('mines') 349 | 350 | if (this.limitedGames['mines']) { 351 | return 352 | } 353 | 354 | await this.gameWait('mines', 600, "Placed", this) 355 | await this.gameWait('mines', 100, "Claiming mine game reward", this) 356 | 357 | const response = await this.fetch('https://arcade.hub.soniclabs.com/rpc', 'POST', { 358 | 'jsonrpc': "2.0", 359 | 'id': this.sessionId, 360 | 'method': "call", 361 | 'params': { 362 | 'call': { 363 | 'dest': CONTRACT_ADDRESS, 364 | 'data': "0x0d942fd00000000000000000000000008bbd8f37a3349d83c85de1f2e32b3fd2fce2468e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000e328a0b1e0be7043c9141c2073e408d1086e117500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000007656e6447616d65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 365 | 'value': '0n' 366 | }, 367 | 'owner': this.address, 368 | 'part': this.part, 369 | 'permit': this.permitSignature 370 | } 371 | }, { 372 | 'network': "SONIC", 373 | 'pragma': "no-cache", 374 | 'priority': "u=1, i", 375 | 'X-Owner': this.address 376 | }, 'https://arcade.soniclabs.com/', true) 377 | 378 | if (response.error) { 379 | await this.gameWait('mines', 10000, `Failed to claim mine game: ${response.error?.["message"]}`, this) 380 | } 381 | 382 | if (response.result?.["hash"]?.['errorTypes']) { 383 | await this.gameWait('mines', 10000, `Claim failed: ${response.result?.["hash"]?.["actualError"]?.["details"]}`, this) 384 | } else { 385 | await this.gameWait('mines', 1500, "Successfully play and claim mine game.", this) 386 | } 387 | } 388 | 389 | async playGame(name) { 390 | if (!Object.prototype.hasOwnProperty.call(GAMES, name)) { 391 | throw new Error(`Undefined game: [${name}]`) 392 | } 393 | 394 | const callData = GAMES[name] 395 | 396 | await this.gameWait(name, 1000, `Playing game: [${name}]`, this) 397 | 398 | let errorMessage = '' 399 | 400 | try { 401 | const response = await this.performRpcRequest('call', { 402 | call: callData, 403 | owner: this.address, 404 | part: this.part, 405 | permit: this.permitSignature 406 | }) 407 | 408 | this.sessionId += 1 409 | 410 | if (!response.error) { 411 | return await this.gameWait(name, 2000, `Successfully played game: [${name}]`, this) 412 | } 413 | 414 | errorMessage = response.error?.message || '' 415 | 416 | if (response.result?.["hash"]?.["errorTypes"]) { 417 | await wait(3000, `Play game failed: ${response.result?.["hash"]?.["actualError"]?.['details']}`, this) 418 | return 419 | } 420 | } catch (error) { 421 | errorMessage = error.message 422 | } 423 | 424 | log.error(this.account, errorMessage) 425 | 426 | if (errorMessage.includes('Please refresh or try again later')) { 427 | await this.createSession() 428 | await this.createNonce() 429 | return await this.playGame(name) 430 | } 431 | 432 | if (errorMessage.includes('Locked')) { 433 | return await this.gameWait(name, 1.8 * 3600, "Accout has been banned, wait for 1.8 hours", this) 434 | } 435 | 436 | if (errorMessage.includes('limit') || errorMessage.includes('Locked')) { 437 | this.limitedGames[name] = true 438 | return await this.gameWait(name, 1000, errorMessage, this) 439 | } 440 | 441 | if (errorMessage.includes('random number')) { 442 | await this.gameWait(name, 5000, errorMessage, this) 443 | return await this.reIterate(name) 444 | } 445 | 446 | if (errorMessage.includes('Permit could not verify')) { 447 | await this.register() 448 | throw new Error(`Failed to play game: [${name}],error: ${errorMessage}`) 449 | } 450 | 451 | if (errorMessage.length > 0) { 452 | throw new Error(`Failed to play game: [${name}], error: ${errorMessage}`) 453 | } 454 | } 455 | 456 | async fetch(url, method, body = {}, customHeaders = {}, referer) { 457 | log.info(this.account, `Fetching: ${url}`) 458 | const requestUrl = url.startsWith('http') ? url : `${this.baseUrl}${url}` 459 | const headers = { 460 | ...customHeaders, ...{ 461 | 'Accept': 'application/json', 462 | 'Accept-Language': 'en-US,en;q=0.9,id;q=0.8', 463 | 'Content-Type': 'application/json', 464 | 'Sec-Fetch-Dest': 'empty', 465 | 'Sec-Fetch-Site': 'cross-site', 466 | 'Sec-Fetch-Mode': 'cors', 467 | 'Host': this.host, 468 | 'Origin': this.origin, 469 | 'Pragma': 'no-cache', 470 | 'Referer': this.origin, 471 | 'User-Agent': this.userAgent, 472 | } 473 | } 474 | 475 | const options = { method, headers, referer } 476 | 477 | log.info(this.account, `${method} Request URL: ${requestUrl}`) 478 | log.info(this.account, `Request headers: ${JSON.stringify(headers)}`) 479 | 480 | if (method !== 'GET') { 481 | options.body = JSON.stringify(body) 482 | log.info(this.account, `Request body: ${options.body}`) 483 | } 484 | 485 | if (this.proxy) { 486 | options.agent = new HttpsProxyAgent(this.proxy, { rejectUnauthorized: false }) 487 | } 488 | 489 | const response = await fetch(requestUrl, options) 490 | 491 | log.info(this.account, `Response status: ${response.status} ${response.statusText}`) 492 | 493 | const contentType = response.headers.get('content-type') 494 | let responseData = contentType && contentType.includes('application/json') 495 | ? await response.json() 496 | : { status: response.status, message: await response.text() } 497 | 498 | log.info(this.account, `Response data: ${JSON.stringify(responseData)}`) 499 | 500 | if (response.ok) { 501 | responseData.status = 200 // Normalize status to 200 for successful responses 502 | return responseData 503 | } else { 504 | throw new Error(`${response.status} - ${response.statusText}`) 505 | } 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | dotenv.config() 4 | 5 | export const REFERRER_CODE = 'tql092' 6 | export const RPC = { 7 | CHAINID: 64165, 8 | RPCURL: 'https://rpc.testnet.soniclabs.com', 9 | EXPLORER: 'https://testnet.soniclabs.com/', 10 | SYMBOL: 'S' 11 | } 12 | export const PRIVATE_KEYS = process.env.PRIVATE_KEYS.split(',').map(key => key.trim()) 13 | export const SMART_ADDRESSES = process.env.SMART_WALLET_ADDRESS.split(',').map(address => address.trim()) 14 | export const PROXIES = process.env.PROXIES.split(',').map(proxy => proxy.trim()).filter(proxy => proxy.length > 20) 15 | 16 | export const CONTRACT = '0xA7C492D9B3BD057845e3bC080c250ad868518478' 17 | export const GAMES = { 18 | plinko: { 19 | dest: CONTRACT, 20 | data: '0x0d942fd00000000000000000000000001cc5bc5c6d5fbb637164c8924528fb2d611fa5090000000000000000000000000000000000000000000000000000000000000002000000000000000000000000e328a0b1e0be7043c9141c2073e408d1086e117500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000003626574000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a', 21 | value: '0n' 22 | }, 23 | singlewheel: { 24 | dest: CONTRACT, 25 | data: '0x0d942fd000000000000000000000000070e7c3846ac8c4308f7eeb0e6a3ceedc325539a60000000000000000000000000000000000000000000000000000000000000002000000000000000000000000e328a0b1e0be7043c9141c2073e408d1086e117500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000003626574000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002', 26 | value: '0n' 27 | }, 28 | mines: { 29 | dest: CONTRACT, 30 | data: '0x0d942fd00000000000000000000000008bbd8f37a3349d83c85de1f2e32b3fd2fce2468e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000e328a0b1e0be7043c9141c2073e408d1086e117500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000007656e6447616d65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 31 | value: '0n' 32 | }, 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import App from './app.js' 3 | import { PRIVATE_KEYS, PROXIES, SMART_ADDRESSES } from './config.js' 4 | import log from './log.js' 5 | import Output from './output.js' 6 | import { toHumanTime, wait } from './utils.js' 7 | 8 | async function play(app, game) { 9 | while (!app.limitedGames[game]) { 10 | const method = 'play' + game.charAt(0).toUpperCase() + game.slice(1) 11 | try { 12 | await app[method]() 13 | await app.getPoints() 14 | } catch (error) { 15 | await app.gameWait(game, 5000, error) 16 | } 17 | } 18 | } 19 | 20 | async function run(account, smartAddress, proxy) { 21 | const app = new App(account, smartAddress, proxy) 22 | try { 23 | log.info(account, `Initializing account: ${PRIVATE_KEYS.indexOf(account) + 1} (${account})`) 24 | await app.connect() 25 | await app.getBalance() 26 | await app.connectToSonic() 27 | await app.getUser() 28 | await app.getPoints() 29 | await app.tryToUpdateReferrer() 30 | await app.createSession() 31 | await app.permitTypedMessage() 32 | 33 | await play(app, 'plinko') 34 | await play(app, 'singlewheel') 35 | await play(app, 'mines') 36 | 37 | // Schedule next cycle 38 | const duration = 8 * 3600 * 1000 // 8h 39 | log.info(account, `Cycle complete for account ${app.address}. pausing for ${toHumanTime(duration)}`) 40 | await wait(duration, `Delaying for next cycle: ${toHumanTime(duration)}`, app) 41 | 42 | return run(account, smartAddress, proxy) // Restart cycle 43 | } catch (error) { 44 | if (error.message?.includes('Locked')) { 45 | log.info(account, `Account ${app.address} is locked. pausing for 2 hours.`) 46 | await wait(2 * 3600 * 1000, `Account ${app.address} is locked. pausing for 2 hours.`, app) 47 | return run(account, smartAddress, proxy) // Retry operation 48 | } 49 | 50 | log.info(account, `Error encountered. retrying in 120 seconds.`) 51 | await wait(120000, `Error: ${error.message || JSON.stringify(error)}. retrying in 120 seconds`, app) 52 | return run(account, smartAddress, proxy) // Retry operation 53 | } 54 | } 55 | 56 | async function startBot() { 57 | try { 58 | if (PROXIES.length !== PRIVATE_KEYS.length && PROXIES.length !== 0) { 59 | throw new Error(`the number of proxies must match the number of accounts or be empty.`) 60 | } 61 | 62 | const tasks = PRIVATE_KEYS.map((account, index) => { 63 | run(account, SMART_ADDRESSES[index] || undefined, PROXIES[index] || undefined) 64 | log.info(account, `started account: ${account}`) 65 | }) 66 | 67 | await Promise.all(tasks) 68 | } catch (error) { 69 | console.error('Bot halted due to error:', error) 70 | } 71 | } 72 | 73 | (async () => { 74 | try { 75 | fs.rmSync('logs/', { recursive: true }) 76 | console.clear() 77 | console.log('----------------- SonicLabs Arcade BOT ------------------') 78 | console.log(' Ensure your bot is up-to-date by running: ') 79 | console.log(' git reset --hard && git pull') 80 | console.log('---------------------------------------------------------') 81 | 82 | await startBot() 83 | } catch (error) { 84 | Output.clearInfo() 85 | console.error('Critical error encountered, restarting...', error) 86 | await startBot() // Retry after error 87 | } 88 | })() 89 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from "winston" 2 | import { PRIVATE_KEYS } from "./config.js" 3 | 4 | const { combine, timestamp, printf, colorize } = format 5 | const logFormat = printf( 6 | ({ level: level, message: message, timestamp: timestamp }) => { 7 | return `${timestamp} [${level}]: ${message}` 8 | } 9 | ) 10 | 11 | class Log { 12 | constructor() { 13 | this.loggers = { 14 | app: createLogger({ 15 | level: "debug", 16 | format: combine( 17 | timestamp({ 18 | format: "YYYY-MM-DD HH:mm:ss", 19 | }), 20 | colorize(), 21 | logFormat 22 | ), 23 | transports: [new transports.File({ filename: 'logs/app.log' })], 24 | exceptionHandlers: [new transports.File({ filename: 'logs/app.log' })], 25 | rejectionHandlers: [new transports.File({ filename: 'logs/app.log' })], 26 | }) 27 | } 28 | } 29 | 30 | getLogger(account) { 31 | if (account && account.startsWith('0x')) { 32 | account = PRIVATE_KEYS.indexOf(account) 33 | } 34 | 35 | if (!this.loggers[account]) { 36 | this.loggers[account] = createLogger({ 37 | level: "debug", 38 | format: combine( 39 | timestamp({ 40 | format: "YYYY-MM-DD HH:mm:ss", 41 | }), 42 | colorize(), 43 | logFormat 44 | ), 45 | transports: [new transports.File({ filename: `logs/${account}.log` })], 46 | exceptionHandlers: [new transports.File({ filename: `logs/${account}.log` })], 47 | rejectionHandlers: [new transports.File({ filename: `logs/${account}.log` })], 48 | }) 49 | } 50 | 51 | return this.loggers[account] 52 | } 53 | 54 | info(account, message) { 55 | this.getLogger(account).info(message) 56 | } 57 | warn(account, message) { 58 | this.getLogger(account).warn(message) 59 | } 60 | error(account, message) { 61 | this.getLogger(account).error(message) 62 | } 63 | debug(account, message) { 64 | this.getLogger(account).debug(message) 65 | } 66 | setLevel(account, message) { 67 | this.getLogger(account).level = message 68 | } 69 | } 70 | 71 | export default new Log() 72 | -------------------------------------------------------------------------------- /src/output.js: -------------------------------------------------------------------------------- 1 | import { Twisters } from 'twisters' 2 | import { PRIVATE_KEYS, RPC } from './config.js' 3 | 4 | class Output { 5 | constructor() { 6 | this.twisters = new Twisters() 7 | } 8 | 9 | log(app, message, waiting = 'running') { 10 | this.twisters.put(app.account, { 11 | text: `Account ${PRIVATE_KEYS.indexOf(app.account) + 1} 12 | --------------------------------------------------------- 13 | Address : ${app.address ?? '-'} 14 | SmartAddress : ${app.smartAddress ?? '-'} 15 | Balance : ${app.balance ?? '-'} ${RPC.SYMBOL} 16 | Points : today: ${app.today_points ?? '-'} / total: ${app.total_points ?? '-'} 17 | Status : ${message} 18 | Waiting : ${waiting} 19 | Game Status : 20 | - plinko : ${app.gameStatus.plinko.message} (${app.gameStatus.plinko.waiting}) 21 | - singlewheel: ${app.gameStatus.singlewheel.message} (${app.gameStatus.singlewheel.waiting}) 22 | - mines : ${app.gameStatus.mines.message} (${app.gameStatus.mines.waiting}) 23 | ---------------------------------------------------------` 24 | }) 25 | } 26 | 27 | clearInfo() { 28 | this.twisters.remove(2) 29 | } 30 | 31 | clear(message) { 32 | this.twisters.remove(message) 33 | } 34 | } 35 | 36 | export default new Output() 37 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import bip39 from 'bip39' 2 | import log from './log.js' 3 | import output from './output.js' 4 | 5 | export function getPrivateKeyType(input) { 6 | if (bip39.validateMnemonic(input)) return 'Secret Phrase' 7 | if (/^[a-fA-F0-9]{64}$/.test(input)) return 'Private Key' 8 | return 'Unknown' 9 | } 10 | 11 | export function toHumanTime(duration) { 12 | const hours = Math.floor(duration / (1000 * 60 * 60)) 13 | const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60)) 14 | const seconds = Math.floor((duration % (1000 * 60)) / 1000) 15 | return `${hours}h ${minutes}m ${seconds}s` 16 | } 17 | 18 | export function getRandomUserAgent() { 19 | const userAgents = [ 20 | 'Mozilla/5.0 (Linux; Android 8.0.0; Ilium Alpha 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.101 Mobile Safari/537.36', 21 | 'Mozilla/5.0 (Linux; Android 6.0; P027) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.81 Safari/537.36', 22 | 'Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0 anonymized by Abelssoft 2028724395', 23 | 'Mozilla/5.0 (Linux; Android 7.0; F3216) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.116 Mobile Safari/537.36 EdgA/45.07.4.5059', 24 | 'Mozilla/5.0 (Linux; Android 7.0; BG2-U03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36', 25 | 'Mozilla/5.0 (Linux; arm_64; Android 9; ANE-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 YaBrowser/20.8.2.90.00 SA/3 Mobile Safari/537.36', 26 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/609.3.5.1.3 (KHTML, like Gecko) Version/13.1.2 Safari/609.3.5.1.3', 27 | 'Mozilla/5.0 (Linux; Android 10; RMX2030) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Mobile Safari/537.36', 28 | 'Mozilla/5.0 (Linux; Android 7.0; Twist (2018)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.101 Mobile Safari/537.36', 29 | 'Mozilla/5.0 (Linux; Android 9; S9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.92 Safari/537.36', 30 | ] 31 | 32 | return userAgents[Math.floor(Math.random() * userAgents.length)] 33 | } 34 | 35 | export function wait(duration, message, app) { 36 | return new Promise(resolve => { 37 | let remainingTime = duration 38 | 39 | log.info(app.account, `Waiting for ${toHumanTime(remainingTime)}`) 40 | 41 | const showRemainingTime = () => { 42 | output.log(app, message, `${toHumanTime(remainingTime)}`) 43 | } 44 | 45 | showRemainingTime() 46 | const interval = setInterval(() => { 47 | remainingTime -= 1000 48 | showRemainingTime() 49 | if (remainingTime <= 0) { 50 | clearInterval(interval) 51 | resolve() 52 | } 53 | }, 1000) 54 | 55 | setTimeout(async () => { 56 | clearInterval(interval) 57 | await output.clearInfo() 58 | resolve() 59 | }, duration) 60 | }) 61 | } 62 | --------------------------------------------------------------------------------