├── datas └── axie_list.yaml ├── img ├── qr.PNG ├── axies.PNG ├── infos.PNG ├── Pyaxie.png ├── all_mmr.PNG ├── all_rank.PNG ├── transfer.PNG ├── all_axies.PNG └── axies │ ├── egg.png │ └── you.png ├── requirements.txt ├── install_ubuntu.sh ├── pyaxie_utils.py ├── secret.yaml ├── README.md ├── slp_abi.json ├── pyaxie-bot.py └── pyaxie.py /datas/axie_list.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/qr.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/qr.PNG -------------------------------------------------------------------------------- /img/axies.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/axies.PNG -------------------------------------------------------------------------------- /img/infos.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/infos.PNG -------------------------------------------------------------------------------- /img/Pyaxie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/Pyaxie.png -------------------------------------------------------------------------------- /img/all_mmr.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/all_mmr.PNG -------------------------------------------------------------------------------- /img/all_rank.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/all_rank.PNG -------------------------------------------------------------------------------- /img/transfer.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/transfer.PNG -------------------------------------------------------------------------------- /img/all_axies.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/all_axies.PNG -------------------------------------------------------------------------------- /img/axies/egg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/axies/egg.png -------------------------------------------------------------------------------- /img/axies/you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shraknard/pyaxie-bot/HEAD/img/axies/you.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | qrcode==7.1 3 | web3==5.21.0 4 | mnemonic==0.20 5 | discord.py==1.7.3 6 | eth_account==0.5.4 7 | discord==1.7.3 8 | Pillow==8.3.2 9 | PyYAML==5.4.1 10 | quickchart==0.0.1 11 | -------------------------------------------------------------------------------- /install_ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Installing the requirements ... " 3 | 4 | appending source list required for the application 5 | sudo echo "deb http://archive.ubuntu.com/ubuntu bionic main universe" >> /etc/apt/sources.list 6 | sudo echo "deb http://archive.ubuntu.com/ubuntu bionic-security main universe" >> /etc/apt/sources.list 7 | sudo echo "deb http://archive.ubuntu.com/ubuntu bionic-updates main universe" >> /etc/apt/sources.list 8 | 9 | echo "Updating source lists" 10 | sudo apt update 11 | 12 | echo "Installing python" 13 | sudo apt install -y python3-pip 14 | sudo apt-get install -y libcairo2-dev libgirepository1.0-dev 15 | 16 | echo "Updating requirements file" 17 | pw=$(pwd | tr -d '\r') 18 | 19 | cd $pw 20 | pip3 freeze > requirements.txt 21 | 22 | echo "Installing application requirements" 23 | pip3 install -r requirements.txt 24 | pip3 install discord 25 | pip3 install web3 26 | 27 | echo "Giving user permissions to the bot directory" 28 | sudo chown -R $name:$name $pw 29 | 30 | nohup python3 ./pyaxie-bot.py & 31 | -------------------------------------------------------------------------------- /pyaxie_utils.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.7.4 2 | # Python bytecode 3.8 (3413) 3 | # Decompiled from: Python 3.8.10 (default, Jun 2 2021, 10:49:15) 4 | # [GCC 9.4.0] 5 | # Embedded file name: /home/vi/pyaxie/pyaxie_utils.py 6 | # Compiled at: 2021-08-31 23:29:51 7 | # Size of source mod 2**32: 1305 bytes 8 | from mnemonic import Mnemonic 9 | from PIL import Image 10 | import string, random 11 | 12 | def gen_pass_phrase(): 13 | mnemo = Mnemonic('english') 14 | return mnemo.generate(strength=128) 15 | 16 | 17 | def gen_password(n=0): 18 | """ 19 | Generate a random password 20 | :param n: 21 | :return: 22 | """ 23 | n = 20 if n <= 10 else n 24 | chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation.replace(';', '') 25 | res = '' 26 | for i in range(n): 27 | res += random.choice(chars) 28 | else: 29 | return res 30 | 31 | 32 | def merge_images(path1, path2, path3, name): 33 | """ 34 | Merge 3 axies pictures into one image in line 35 | :param path1: Path of the 1st axie image (usually './img/axies/AXIE-ID.jpg') 36 | :param path2: Path of the 2nd axie image 37 | :param path3: Path of the 3rd axie image 38 | :param name: Name of the scholar 39 | :return: Path of the freshly created image 40 | """ 41 | img = Image.open(path1).convert('RGBA') 42 | img2 = Image.open(path2).convert('RGBA') 43 | img3 = Image.open(path3).convert('RGBA') 44 | dst = Image.new('RGB', (img.width * 3, img.height)).convert('RGBA') 45 | final = './img/axies/' + name + '.png' 46 | dst.paste(img, (0, 0)) 47 | dst.paste(img2, (img.width, 0)) 48 | dst.paste(img3, (img.width * 2, 0)) 49 | dst.save(final) 50 | return final 51 | # okay decompiling pyaxie_utils.cpython-38.pyc 52 | -------------------------------------------------------------------------------- /secret.yaml: -------------------------------------------------------------------------------- 1 | appName: "secret" 2 | 3 | discord_token: "THE DISCORD TOKEN OF YOUR BOT. Example : 0djzH879ODc2NzAw.ZDj876.HKHV9DHiZercwTQXyBCZKos5" 4 | 5 | url_api: "API for MMR, rank, infos, etc... Most of the functions will not work if you don't set it. You have to find it by yourself as it is forbidden to make it public in in Axie's ToS." 6 | 7 | paths: 8 | axie_list_path: "datas/axie_list.yaml" 9 | account_log_path: "datas/account_log.yaml" 10 | slp_track_path: "datas/slp_track.yaml" 11 | 12 | personal: 13 | ronin_address: "Manager ronin address. Make sure you replace the 'ronin:' part by '0x' exemple : 0x0000000000000000000000000000000001" 14 | private_key: "Manager private key without the '0x' example: alcsdfsdqhl65f5zu8iop4h8fakez4zh8e7a8sd1az48F8ds4gsd6g7qsdqsdz" 15 | discord_id: 12346269824881831 16 | 17 | scholars: 18 | EXAMPLE: 19 | ronin_address: "Scholar account ronin address. Remove 'ronin:' with '0x' Example : 0x0000000000000000000000000000000002" 20 | private_key: "Scholar account private key. Remove the '0x' part. Example : plqsdqhl65df5zu8iop48fakez4zh8ec7a8sd148F8ds4gsd6g78ezr8978f6745po3" 21 | discord_id: 123456789452669 22 | personal_ronin: "Scholar personnal ronin (for payout) example : 0x000000000000000000000000000000000000000002" 23 | payout_percentage: 0.6 24 | payout_description: "Value between 0 and 1. Represents the percentage the scholar will receive. The rest is sent to the academy." 25 | tips: "Delete the EXAMPLE before starting and copy/paste scholar1 block for each of your scholars" 26 | scholar1: 27 | ronin_address: "0x0000000000000000000000000000000002" 28 | private_key: "plqsdqhl65df5zu8iop48fakez4zh8ec7a8sd148F8ds4gsd6g78ezr8978f6745po3" 29 | discord_id: 123456789452669 30 | personal_ronin: "0x00000000000000000000000000000000003" 31 | payout_percentage: 0.6 32 | scholar2: 33 | ronin_address: "0x00000000000000000000000000000000004" 34 | private_key: "plqsdqhl65f5zu8iop48fakez4zh8e7a8sd1s48F8cds4gsd6g78ezr8978f6745po3" 35 | discord_id: 123456786971378652 36 | personal_ronin: "0x00000000000000000000000000000000005" 37 | payout_percentage: 0.6 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyaxie bot 2 | 3 | logo 4 | 5 | ## THIS BOT IS NOT UPDATED / SUPPORTED ANYMORE 6 | 7 | ## Description 8 | 9 | A python discord bot to help you manage your **Axie Infinity** scholarships. 10 | This is based on my other Axie project : [Pyaxie](https://github.com/vmercadi/pyaxie) which is a python lib to interact with Axie world. 11 | 12 | If you don't have the API URL for Axies informations, some of the functionalities will not work. 13 | You will have to find this by yourself and then add it in secret.yaml as it is forbidden to make it public in Axie ToS. 14 | 15 | If you encounter a problem or have a question, you can join the discord server : https://discord.gg/gqaSv2PZbF 16 | 17 | ## [Follow this step-by-step guide to install and use the bot](https://github.com/vmercadi/pyaxie-bot/wiki) 18 | 19 | As I'm adding a bit every day, you'll have to update the project as much as you can (infos in the discord). 20 | Here is a command to easily update : `mv secret.yaml asecret.yaml; git pull; mv asecret.yaml secret.yaml` 21 | With this command, you'll not have to write the config in secret.yaml again 22 | 23 | ## BOT commands 24 | 25 | "**Commands for everybody :**" 26 | 27 | "`$infos` = Send all the infos about your account " 28 | "`$qr` = Send your QR code " 29 | "`$mmr` = Send your current MMR " 30 | "`$rank` = Send your current rank" 31 | "`$axies` = Send the list of axies of your account" 32 | "`$axies 506011891353903121` = Send axies list of given discord ID" 33 | "`$profile` = Send the link of your Axie Infinity profile" 34 | "`$all_profile` = Send a link of every Axie account in the scholarship" 35 | "`$self_payout` = To claim and payout for yourself. Send to the personal address you gave to your manager." 36 | 37 | "**Commands for manager :**" 38 | 39 | "`$claim 506011891353903121` = Claim for the given discord ID" 40 | "`$all_claim` = Claim for all the scholars" 41 | "`$all_mmr` = Send scholar list sorted by MMR " 42 | "`$all_rank` = Send scholar list sorted by rank " 43 | "`$payout` = Send the available SLP to manager and scholars " 44 | "`$payout me` = Send all scholarship SLP directly to manager account with no split" 45 | "`$transfer 0xfrom_address 0xto_address amount` = Transfer amount SLP from from_address to to_address" 46 | "`$breed_infos` = How much does it cost to breed now. You can also specify a breed lvl (0-6)" 47 | "`$breed_cost 123456` = How much did you spent breeding an axie. (take AXS/SLP prices from time of breed)" 48 | "`$account_balance ronin_address` = Balance of specified account" 49 | "`$all_account_balance` = Balance of all the accounts in the scholarship" 50 | "`$all_address` = Get all the addresses in the scholarship" 51 | 52 | ## Donation 53 | 54 | Thanks to [ZracheSs](https://github.com/ZracheSs-xyZ) for his Payout and QR script code. 55 | And thanks to Andy for his help with debug ! 56 | 57 | **My ronin address :** ronin:f561bb92d33e4feaae617a182264a4c7d7272948 58 | **My ethereum address :** 0x60900b1740E9e164137Db9b982d9681A2E74446c 59 | 60 | **ZracheSs ronin address :** ronin:a04d88fbd1cf579a83741ad6cd51748a4e2e2b6a 61 | **ZracheSs ethereum address :** 0x3C133521589daFa7213E5c98C74464a9577bEE01 62 | 63 | ## Examples 64 | 65 | ![](https://github.com/vmercadi/pyaxie-bot/blob/master/img/qr.PNG) 66 | ![](https://github.com/vmercadi/pyaxie-bot/blob/master/img/infos.PNG) 67 | ![](https://github.com/vmercadi/pyaxie-bot/blob/master/img/transfer.PNG) 68 | ![](https://github.com/vmercadi/pyaxie-bot/blob/master/img/all_axies.PNG) 69 | ![](https://github.com/vmercadi/pyaxie-bot/blob/master/img/all_rank.PNG) 70 | ![](https://github.com/vmercadi/pyaxie-bot/blob/master/img/all_mmr.PNG) 71 | 72 | 73 | -------------------------------------------------------------------------------- /slp_abi.json: -------------------------------------------------------------------------------- 1 | [{ "inputs": [{ "internalType": "uint256", "name": "_dailyPotionLimit", "type": "uint256" }], "payable": false , "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_oldAdmin", "type": "address" }, { "indexed": true, "internalType": "address", "name": "_newAdmin", "type": "address" }], "name": "AdminChanged", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_oldAdmin", "type": "address" }], "name": "AdminRemoved", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "_spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_owner", "type": "address" }, { "indexed": true, "internalType": "uint256", "name": "_amount", "type": "uint256" }], "name": "BreedingPotionCheckpoint", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_minter", "type": "address" }], "name": "MinterAdded", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_minter", "type": "address" }], "name": "MinterRemoved", "type": "event" }, { "anonymous": false, "inputs": [], "name": "Paused", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_spender", "type": "address" }], "name": "SpenderUnwhitelisted", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_spender", "type": "address" }], "name": "SpenderWhitelisted", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "_from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "_to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "Transfer", "type": "event" }, { "anonymous": false, "inputs": [], "name": "Unpaused", "type": "event" }, { "constant": false, "inputs": [{ "internalType": "address[]", "name": "_addedMinters", "type": "address[]" }], "name": "addMinters", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "admin", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "address", "name": "_owner", "type": "address" }, { "internalType": "address", "name": "_spender", "type": "address" }], "name": "allowance", "outputs": [{ "internalType": "uint256", "name": "_value", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_spender", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "approve", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "contract IERC20Receiver", "name": "_spender", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }, { "internalType": "bytes", "name": "_data", "type": "bytes" }], "name": "approveAndCall", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "contract IERC20PayableReceiver", "name": "_spender", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }, { "internalType": "bytes", "name": "_data", "type": "bytes" }], "name": "approveAndCallPayable", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "balanceOf", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "burn", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_from", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "burnFrom", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "cappedSupply", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_newAdmin", "type": "address" }], "name": "changeAdmin", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_owner", "type": "address" }, { "internalType": "uint256", "name": "_amount", "type": "uint256" }, { "internalType": "uint256", "name": "_createdAt", "type": "uint256" }, { "internalType": "bytes", "name": "_signature", "type": "bytes" }], "name": "checkpoint", "outputs": [{ "internalType": "uint256", "name": "_balance", "type": "uint256" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }, { "internalType": "uint256", "name": "_createdAt", "type": "uint256" }, { "internalType": "bytes", "name": "_signature", "type": "bytes" }, { "internalType": "contract IERC20PayableReceiver", "name": "_spender", "type": "address" }, { "internalType": "uint256", "name": "_approvedAmount", "type": "uint256" }, { "internalType": "bytes", "name": "_data", "type": "bytes" }], "name": "checkpointAndCall", "outputs": [{ "internalType": "uint256", "name": "_balance", "type": "uint256" }], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": true, "inputs": [], "name": "dailyPotionLimit", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "address", "name": "_owner", "type": "address" }], "name": "getCheckpoint", "outputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }, { "internalType": "uint256", "name": "_createdAt", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "address", "name": "_addr", "type": "address" }], "name": "isMinter", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_to", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "mint", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "minter", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "name": "minters", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "pause", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "paused", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "removeAdmin", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address[]", "name": "_removedMinters", "type": "address[]" }], "name": "removeMinters", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "uint256", "name": "_dailyPotionLimit", "type": "uint256" }], "name": "setDailyPotionLimit", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_to", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "transfer", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_from", "type": "address" }, { "internalType": "address", "name": "_to", "type": "address" }, { "internalType": "uint256", "name": "_value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "internalType": "bool", "name": "_success", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [], "name": "unpause", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_spender", "type": "address" }], "name": "unwhitelist", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "internalType": "address", "name": "_spender", "type": "address" }], "name": "whitelist", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "whitelisted", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "payable": false, "stateMutability": "view", "type": "function" }] -------------------------------------------------------------------------------- /pyaxie-bot.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import yaml 3 | import os 4 | import sys 5 | import time 6 | import json 7 | import requests 8 | 9 | from web3 import Web3 10 | from datetime import datetime 11 | from pyaxie import pyaxie 12 | from datetime import timedelta 13 | from pprint import pprint 14 | 15 | now = datetime.now() 16 | client = discord.Client(intents=discord.Intents.all()) 17 | current_time = now.strftime("%d/%m/%Y %H:%M:%S") 18 | 19 | 20 | def create_info_message(pyax): 21 | """ 22 | Create a message for the SLP infos (bot command : $wen) 23 | :param pyax: a pyaxie object with scholar informations 24 | :return: The response with the infos 25 | """ 26 | balance = pyax.get_claimed_slp() 27 | last_claim = pyax.get_last_claim() 28 | 29 | try: 30 | if datetime.utcnow() + timedelta(days=-14) < datetime.fromtimestamp(last_claim): 31 | wait = datetime.fromtimestamp(last_claim) - (datetime.utcnow() + timedelta(days=-14)) 32 | mod = wait.seconds 33 | hour = str(int(mod // 3600)) 34 | mod %= 3600 35 | m = str(int(mod // 60)) 36 | mod %= 60 37 | s = str(int(mod)) 38 | response = "❌ **NOT ABLE TO CLAIM** : **" + str(wait.days) + " day(s) " + hour + " hour(s) " + m + " min " + s + " sec\n**" 39 | else: 40 | response = "✅ **ABLE TO CLAIM**\n" 41 | 42 | perc = 1 if pyax.ronin_address == config['personal']['ronin_address'] else pyax.payout_percentage 43 | 44 | unclaimed = pyax.get_unclaimed_slp() 45 | total = int(balance + unclaimed) 46 | response += "\nBalance : **" + str(balance) + " SLP**" + \ 47 | "\nUnclaimed : **" + str(unclaimed) + " SLP**" + \ 48 | "\nAfter we split, you'll have : **" + str(int(total * perc)) + " SLP** or **" + str(int((total * pyax.get_price('slp') * perc))) + "$**" + \ 49 | "\nApproximate daily ratio : **" + str(pyax.get_daily_slp()) + " SLP**\n---" 50 | except ValueError as e: 51 | return "Error creating message : " + str(e) 52 | return response 53 | 54 | 55 | def get_account_from_id(id): 56 | """ 57 | Get a pyaxie object depending on config 58 | :param id: discord_id 59 | :return: Scholar account or Manager account or None 60 | """ 61 | scholar = None 62 | id = int(id) 63 | if id == config['personal']['discord_id']: 64 | scholar = pyaxie(config['personal']['ronin_address'], config['personal']['private_key']) 65 | else: 66 | for sch in config['scholars']: 67 | if config['scholars'][sch]['discord_id'] == id: 68 | scholar = pyaxie(config['scholars'][sch]['ronin_address'], config['scholars'][sch]['private_key']) 69 | scholar.name = str(client.get_user(config['scholars'][sch]['discord_id'])).split('#', -1)[0] 70 | break 71 | return scholar 72 | 73 | 74 | def get_account_from_ronin(ronin_address): 75 | """ 76 | Get a pyaxie object depending on config 77 | :param id: ronin address 78 | :return: Scholar account or Manager account or None 79 | """ 80 | scholar = None 81 | if ronin_address == config['personal']['ronin_address']: 82 | scholar = pyaxie(config['personal']['ronin_address'], config['personal']['private_key']) 83 | else: 84 | for sch in config['scholars']: 85 | if config['scholars'][sch]['ronin_address'] == ronin_address: 86 | scholar = pyaxie(config['scholars'][sch]['ronin_address'], config['scholars'][sch]['private_key']) 87 | scholar.name = str(client.get_user(config['scholars'][sch]['discord_id'])).split('#', -1)[0] 88 | break 89 | return scholar 90 | 91 | 92 | def log(message="", end="\n"): 93 | pprint(message + end) 94 | f = open('pyaxie.log', "a") 95 | og = sys.stdout 96 | print(message, end=end, flush=True) 97 | sys.stdout = f 98 | print(message, end=end) 99 | f.flush() 100 | f.close() 101 | sys.stdout = og 102 | 103 | 104 | @client.event 105 | async def on_ready(): 106 | print('\nWe are logged in as {0.user}'.format(client)) 107 | 108 | 109 | @client.event 110 | async def on_message(message): 111 | if message.author == client.user or len(message.content) < 2 or message.content[0] != '$': 112 | return 113 | 114 | with open("secret.yaml", "r") as file: 115 | config = yaml.safe_load(file) 116 | 117 | scholar = get_account_from_id(message.author.id) 118 | if scholar is None: 119 | print("\nNon scholar tried to use the bot : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 120 | return await message.channel.send("You are not part of the scholarship. Check with your manager to be added to the bot.") 121 | 122 | ############################## 123 | # Send the list of commands # 124 | ############################## 125 | if message.content == "$help": 126 | await message.channel.send("\n\n**Commands for everybody :**\n" + 127 | "\n`$infos` = Send all the infos about your account " + 128 | "\n`$qr` = Send your QR code " + 129 | "\n`$mmr` = Send your current MMR " + 130 | "\n`$rank` = Send your current rank" + 131 | "\n`$axies` = Send the list of axies of your account" + 132 | "\n`$axies 506011891353903121` = Send axies list of given discord ID" + 133 | "\n`$profile` = Send the link of your Axie Infinity profile" + 134 | "\n`$all_profile` = Send a link of every Axie account in the scholarship" + 135 | "\n`$self_payout` = To claim and payout for yourself. Send to the personal address you gave to your manager." + 136 | 137 | "\n\n**Commands for manager :**\n" + 138 | "\n`$claim 506011891353903121` = Claim for the given discord ID (Manager only) " + 139 | "\n`$all_claim` = Claim for all the scholars (Manager only) " + 140 | "\n`$all_mmr` = Send scholar list sorted by MMR " + 141 | "\n`$all_rank` = Send scholar list sorted by rank " + 142 | "\n`$payout` = Send the available SLP to manager and scholars " + 143 | "\n`$payout me` = Send all scholarship SLP directly to manager account with no split" + 144 | "\n`$transfer 0xfrom_address 0xto_address amount` = Transfer amount SLP from from_address to to_address" + 145 | "\n`$breed_infos` = How much does it cost to breed now. You can also specify a breed lvl (0-6)" + 146 | "\n`$breed_cost 123456` = How much did you spent breeding an axie. (take AXS/SLP prices from time of breed)" + 147 | "\n`$account_balance ronin_address` = Balance of specified account" + 148 | "\n`$all_account_balance` = Balance of all the accounts in the scholarship" + 149 | "\n`$all_address` = Get all the addresses in the scholarship") 150 | 151 | 152 | return 153 | 154 | ############################## 155 | # Send a QR code # 156 | ############################## 157 | if message.content == "$qr": 158 | print("\nGet QR code for : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 159 | try: 160 | qr_path = scholar.get_qr_code() 161 | await message.author.send("\nHello " + message.author.name + " ! 😃 \nHere is your new QR Code to login : ") 162 | await message.author.send(file=discord.File(qr_path)) 163 | os.remove(qr_path) 164 | except ValueError as e: 165 | await message.channel.send("Error getting QR code : " + str(e)) 166 | return 167 | 168 | ############################## 169 | # Get MMR of author # 170 | ############################## 171 | if message.content == "$mmr": 172 | if config['url_api'] == '': 173 | return await message.channel.send("No api_url set in secret.yaml. You have to FIND and add it by yourself as it is private and I can't make it public.") 174 | 175 | print("\nGet MMR for : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 176 | try: 177 | await message.channel.send("\nHello " + message.author.name + " !\n" 178 | "Your MMR is : {} 🥇".format(scholar.get_rank_mmr()['mmr'])) 179 | except ValueError as e: 180 | await message.channel.send("Error getting MMR : " + str(e)) 181 | return 182 | 183 | ############################## 184 | # Get rank of author # 185 | ############################## 186 | if message.content == "$rank": 187 | if config['url_api'] == '': 188 | return await message.channel.send("No api_url set in secret.yaml. You have to FIND and add it by yourself as it is private and I can't make it public.") 189 | 190 | print("\nGet rank for : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 191 | try: 192 | await message.channel.send("\nHello " + message.author.name + " !\n" 193 | "Your rank is : {} 🎖️".format(scholar.get_rank_mmr()['rank'])) 194 | except ValueError as e: 195 | await message.channel.send("Error getting rank : " + str(e)) 196 | return 197 | 198 | ################################## 199 | # Get all infos about the author # 200 | ################################## 201 | if "$infos" in message.content: 202 | if config['url_api'] == '': 203 | return await message.channel.send("No api_url set in secret.yaml. You have to FIND and add it by yourself as it is private and I can't make it public.") 204 | if "$infos " in message.content: 205 | try: 206 | id = message.content.split(" ")[1] 207 | if id.isnumeric(): 208 | scholar = get_account_from_id(id) 209 | else: 210 | return await message.channel.send("Error in ID. Example: $infos 496061891353903121") 211 | except ValueError as e: 212 | await message.channel.send("Error : " + str(e)) 213 | return e 214 | 215 | if scholar is None: 216 | return await message.channel.send("Error: No scholar found with this ID") 217 | 218 | rank_mmr = scholar.get_rank_mmr() 219 | print("\nGet infos for : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 220 | try: 221 | imgline = scholar.get_axies_imageline() 222 | await message.channel.send("\nHere are the infos for **" + scholar.name + "** account [**" + scholar.ronin_address + "**] \n" + 223 | "NB of axies : **" + str(scholar.get_number_of_axies()) + "**\n---\n" + 224 | "Claim status : {}\n".format(create_info_message(scholar).replace("\n", "", 1)) + 225 | "MMR : **{}** 🥇\n".format(rank_mmr['mmr']) + 226 | "Rank : **{}** 🎖️".format(rank_mmr['rank']), file=discord.File(imgline)) 227 | except ValueError as e: 228 | await message.channel.send("Error getting infos : " + str(e)) 229 | return 230 | 231 | ################################### 232 | # Get MMR/rank for all scholars # 233 | ################################### 234 | if "$all_mmr" in message.content or "$all_rank" in message.content: 235 | if config['url_api'] == '': 236 | return await message.channel.send("No api_url set in secret.yaml. You have to FIND and add it by yourself as it is private and I can't make it public.") 237 | if message.author.id != config['personal']['discord_id']: 238 | return await message.channel.send("This command is only available for manager") 239 | 240 | print("\nGet all MMR/RANK, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 241 | await message.channel.send("🥇 Getting MMR/RANK for all scholars, this can take some time...") 242 | try: 243 | rm = message.content.split('_')[1] 244 | rm = rm.split(' ')[0] if ' ' in rm else rm 245 | l = list() 246 | 247 | for account in config['scholars']: 248 | rank_mmr = scholar.get_rank_mmr(config['scholars'][account]['ronin_address']) 249 | rank_mmr['name'] = str(client.get_user(config['scholars'][account]['discord_id'])).split('#', -1)[0] 250 | l.append(rank_mmr) 251 | 252 | mmrs = reversed(sorted(l, key=lambda k: k[rm])) 253 | nb = message.content.split(' ')[1] if ' ' in message.content else 99999 254 | if nb != 99999 and nb.isdigit(): 255 | nb = int(nb) 256 | 257 | msg = "" 258 | i = 1 259 | for m in mmrs: 260 | if i > nb: 261 | break 262 | msg += "\nTop [**{}**] : **{}** | {}".format(i, m[rm], m['name']) 263 | if i % 20 == 0: 264 | await message.channel.send(msg) 265 | msg = "" 266 | i += 1 267 | 268 | except ValueError as e: 269 | return await message.channel.send("Error getting MMRs : " + str(e)) 270 | return await message.channel.send(msg) 271 | 272 | 273 | ################################## 274 | # Claim for the current scholars # 275 | ################################## 276 | if "$claim" in message.content: 277 | if config['url_api'] == '': 278 | return await message.channel.send("No api_url set in secret.yaml. You have to FIND and add it by yourself as it is private and I can't make it public.") 279 | 280 | print("\nClaim, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 281 | try: 282 | if message.content == "$claim": 283 | return await message.channel.send("You have to specify the discord ID of the scholar you want to claim for.\n" + 284 | "Example: `$claim 506011891353903121`") 285 | 286 | to_id = message.content.split(' ')[1] 287 | scholar = get_account_from_id(to_id) 288 | if scholar is None: 289 | return await message.channel.send("The Discord ID you specified is not in the scholarship.\n") 290 | 291 | amount = scholar.get_unclaimed_slp() 292 | if amount > 0: 293 | await message.channel.send("{} SLP claimed for {} !\nTransaction hash of the claim : {} ".format(amount, scholar.name, str(scholar.claim_slp()))) 294 | else: 295 | await message.channel.send("No SLP to claim for {} at this moment !\n".format(message.author.name)) 296 | 297 | except ValueError as e: 298 | await message.channel.send("Error while claiming : " + str(e)) 299 | return 300 | 301 | ############################## 302 | # Claim for all scholars # 303 | ############################## 304 | if message.content == "$all_claim": 305 | print("\nAll claim, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 306 | if message.author.id != config['personal']['discord_id']: 307 | return await message.channel.send("This command is only available for manager") 308 | 309 | await message.channel.send("\nClaiming for all scholars... This can take some time.\n") 310 | 311 | try: 312 | l = list() 313 | for account in config['scholars']: 314 | scholar = pyaxie(config['scholars'][account]['ronin_address'], config['scholars'][account]['private_key']) 315 | amount = scholar.get_unclaimed_slp() 316 | if datetime.utcnow() + timedelta(days=-14) < datetime.fromtimestamp(scholar.get_last_claim()) or amount < 0: 317 | l.append("**No SLP to claim for {} at this moment** \n".format(scholar.name)) 318 | else: 319 | l.append("**{} SLP claimed for {} !** Transaction hash : {} \n".format(amount, scholar.name, str(scholar.claim_slp()))) 320 | except ValueError as e: 321 | return await message.channel.send("Error getting QR code : " + str(e)) 322 | return await message.channel.send("--------\n".join(l)) 323 | 324 | ############################## 325 | # Payout for all scholars # 326 | ############################## 327 | if "payout" in message.content: 328 | print("\nPayout, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 329 | # Self payout for scholars 330 | if "$self_payout" in message.content: 331 | await message.channel.send("\nSelf payout for **" + message.author.name + "**. Please wait...\n") 332 | if not ' ' in message.content: 333 | to_address = scholar.personal_ronin 334 | else: 335 | to_address = message.content.split(' ')[1].replace('ronin:', '0x') 336 | if not Web3.isAddress(to_address): 337 | return await message.channel.send("\nError in the address, make sure you try to send to the right one.\n") 338 | 339 | if to_address == scholar.ronin_address: 340 | return await message.channel.send("Your from_address and to_address are the same.") 341 | 342 | tx = scholar.payout() 343 | claimed = scholar.get_claimed_slp() 344 | msg = "Sent **{} SLP**\nFrom : **{}**\nTo : **{}**\nTransaction : \n".format(claimed * (1 - scholar.payout_percentage), scholar.ronin_address, config['personal']['ronin_address'], tx[0]) 345 | msg += "-----\nSent **{} SLP**\nFrom : **{}**\nTo : **{}**\nTansaction : \n".format(claimed * scholar.payout_percentage, scholar.ronin_address, to_address, tx[1]) 346 | return await message.channel.send(msg) 347 | 348 | # Payout for all scholars 349 | if message.content == "$payout": 350 | await message.channel.send("\nPayout for all scholar ! This can take some time.\n") 351 | if message.author.id != config['personal']['discord_id']: 352 | return await message.channel.send("This command is only available for manager") 353 | try: 354 | for account in config['scholars']: 355 | scholar = pyaxie(config['scholars'][account]['ronin_address'], config['scholars'][account]['private_key']) 356 | unclaimed = scholar.get_unclaimed_slp() 357 | claimed = scholar.get_claimed_slp() 358 | 359 | if datetime.utcnow() + timedelta(days=-14) < datetime.fromtimestamp(scholar.get_last_claim()) or unclaimed <= 0: 360 | await message.channel.send("**No SLP to claim for {} at this moment** \n".format(scholar.name)) 361 | 362 | if claimed <= 0: 363 | await message.channel.send("**No SLP to send for {} account.**\n".format(scholar.name)) 364 | elif "me" in message.content: 365 | tx = scholar.transfer_slp(config['personal']['ronin_address'], claimed) 366 | await message.channel.send("**All the {} SLP are sent to you !**\n Transaction : \n".format(claimed, str(tx))) 367 | else: 368 | res = scholar.payout() 369 | msg = "Sent **{} SLP**\nFrom : **{}**\nTo : **{}**\nTransaction : \n".format(claimed * (1 - scholar.payout_percentage), scholar.ronin_address, config['personal']['ronin_address'], res[0]) 370 | msg += "-----\nSent **{} SLP**\nFrom : **{}**\nTo : **{}**\nTransaction : \n".format(claimed * scholar.payout_percentage, scholar.ronin_address, scholar.personal_ronin, res[1]) 371 | await message.channel.send(msg) 372 | 373 | await message.channel.send("\n-------------\n") 374 | await message.channel.send("\n\n--- END OF PAYOUT ---") 375 | except ValueError as e: 376 | await message.channel.send("Error while paying out : " + str(e)) 377 | return 378 | 379 | ############################################## 380 | # Transfer SLP from an account to another # 381 | ############################################## 382 | if "$transfer" in message.content and " " in message.content: 383 | print("\nTransfer, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 384 | if message.author.id != config['personal']['discord_id']: 385 | return await message.channel.send("This command is only available for manager") 386 | 387 | cmd = message.content.split(' ') 388 | if ("0x" not in cmd[1] and not cmd[1].isnumeric()) or ("0x" not in cmd[2] and not cmd[2].isnumeric()) or not cmd[3].isnumeric(): 389 | return await message.channel.send("Error in the command. Should look like this : $transfer 0xfrom_address 0xto_address 100") 390 | 391 | try: 392 | scholar = get_account_from_id(cmd[1]) if cmd[0] == "$transfer_id" else get_account_from_ronin(cmd[1]) 393 | scholar2 = get_account_from_id(cmd[2]) if cmd[0] == "$transfer_id" else get_account_from_ronin(cmd[2]) 394 | if scholar is None: 395 | return await message.channel.send("The from address or discord ID that you specified is not in the scholarship.") 396 | 397 | if scholar2 is None and cmd[0] != "$transfer_id": 398 | ronin_address = cmd[2] 399 | else: 400 | ronin_address = scholar2.ronin_address 401 | 402 | try: 403 | tx = scholar.transfer_slp(ronin_address, int(cmd[3])) 404 | except ValueError as e: 405 | return e 406 | await message.channel.send("Sent **{} SLP**\nFrom : ** {}\n**To : ** {}** \nTransaction : \n".format(cmd[3], cmd[1], ronin_address, tx)) 407 | except ValueError as e: 408 | await message.channel.send("Error while transfering SLP : " + str(e)) 409 | return 410 | 411 | ############################################## 412 | # Get list of axie of the account # 413 | ############################################## 414 | if "$axies" in message.content: 415 | print("\nAxie list, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 416 | if message.content == "$axies": 417 | scholar = get_account_from_id(message.author.id) 418 | elif "$axies " in message.content: 419 | try: 420 | id = message.content.split(" ")[1] 421 | if id.isnumeric(): 422 | scholar = get_account_from_id(id) 423 | else: 424 | return await message.channel.send("Error in ID. Example: $axies 496061891353903121") 425 | except ValueError as e: 426 | await message.channel.send("Error : " + str(e)) 427 | return e 428 | 429 | if scholar is None: 430 | return await message.channel.send("Error: No scholar found with this ID") 431 | 432 | try: 433 | axies = scholar.get_axie_list() 434 | await message.channel.send("\nHere is the axie list for " + scholar.name + " account :\n") 435 | for axie in axies: 436 | await message.channel.send(scholar.axie_link(int(axie['id']))) 437 | await message.channel.send(file=discord.File(scholar.download_axie_image(int(axie['id'])))) 438 | except ValueError as e: 439 | await message.channel.send("Error while getting axies : " + str(e)) 440 | return 441 | elif "$all_axies" in message.content: 442 | if "$all_axies " in message.content: 443 | axie_class = message.content.split(' ')[1] 444 | if axie_class.lower() in ["reptile", "plant", "dusk", "aquatic", "bird", "dawn", "beast", "bug"]: 445 | axies = scholar.get_all_axie_class(axie_class) 446 | else: 447 | return await message.channel.send(axie_class + " is not a class. Class list : Reptile, Plant, Dusk, Aquatic, Bird, Dawn, Beast, Bug ") 448 | 449 | print("\nListing of all " + axie_class + " axies in the scholarship, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 450 | await message.channel.send("Getting list of all the " + axie_class + " axies in the scholarship ! This can take some time.\n") 451 | for axie in axies: 452 | await message.channel.send("\n" + scholar.axie_link(int(axie['id'])) + "\n") 453 | await message.channel.send(file=discord.File(scholar.download_axie_image(int(axie['id'])))) 454 | await message.channel.send("\n----------- END OF AXIES LIST ----------") 455 | else: 456 | print("\nListing of all axies in the scholarship, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 457 | await message.channel.send("Getting list of all the axies in the scholarship ! This can take some time.\n") 458 | try: 459 | axies = scholar.get_all_axie_list() 460 | for axie in axies: 461 | await message.channel.send("\n" + scholar.axie_link(int(axie['id'])) + "\n") 462 | await message.channel.send(file=discord.File(scholar.download_axie_image(int(axie['id'])))) 463 | await message.channel.send("\n----------- END OF AXIES LIST ----------") 464 | except ValueError as e: 465 | await message.channel.send("Error while getting axies : " + str(e)) 466 | return 467 | 468 | ################################################# 469 | # Get a graph for burned/minted SLPs # 470 | ################################################# 471 | if message.content == "$graph": 472 | print("\nGraph, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 473 | try: 474 | path = scholar.get_mint_burn_graph() 475 | discord.File(path, filename=path) 476 | embed = discord.Embed(title="SLP data", description=" \n Usefull SLP data like prices and minted vs burned SLP.", color=0xf5f542) 477 | total = json.loads(requests.get("token/0xa8754b9fa15fc18bb59458815510e40a12cd2014").content) 478 | 479 | embed.add_field(name="Total SLP supply", value=total, inline=False) 480 | embed.add_field(name="SLP/USD", value=scholar.get_price("usd"), inline=True) 481 | embed.add_field(name="SLP/EUR", value=scholar.get_price("eur"), inline=True) 482 | embed.add_field(name="SLP/PHP", value=scholar.get_price("php"), inline=True) 483 | embed.set_image(url="attachment://" + path) 484 | await message.channel.send(embed) 485 | except ValueError as e: 486 | await message.channel.send("Error while getting burned/minted SLP graph : " + str(e)) 487 | return 488 | 489 | ################################################ 490 | # Breed prices infos # 491 | ################################################ 492 | if "$breed_infos" in message.content: 493 | print("\nBreed_infos, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 494 | if message.author.id != config['personal']['discord_id']: 495 | return await message.channel.send("This command is only available for manager") 496 | costs = scholar.get_breed_cost() 497 | if " " in message.content: 498 | nb = message.content.split(" ")[1] 499 | if not nb.isdigit(): 500 | return await message.channel.send("Error breed level should be between **0** and 6. Example : `$breed_cost 2`") 501 | 502 | nb = int(nb) 503 | if nb > 0: 504 | msg = "Breeding cost infos for breed level **{}**. **AXS** : **${}** -|- **SLP** : **${}**\n\n".format(nb, scholar.get_price('axs'),scholar.get_price('slp')) 505 | msg += "Breed price : **${}**\nAverage price from breed **0** to **{}** : **${}**\n".format(costs[nb]['price'], nb, costs[nb]['average_price']) 506 | msg += "Total price for **{}** breed : **${}**".format(nb, costs[nb]['total_breed_price']) 507 | return await message.channel.send(msg) 508 | 509 | else: 510 | msg = list() 511 | nb = 0 512 | for c in costs: 513 | msg.append("**Breed level {}**\nBreed price : **${}**\nAverage price from breed **0** to **{}** : **${}**\nTotal price for **{}** breed : **${}**".format( 514 | nb, costs[c]['price'], nb, costs[c]['average_price'], nb, costs[c]['total_breed_price'])) 515 | nb += 1 516 | return await message.channel.send("Breeding cost for all breed levels :\n\n" + '\n-----\n'.join(msg)) 517 | 518 | ################################################ 519 | # GEt the price an axie cost to breed # 520 | ################################################ 521 | if "$breed_cost " in message.content: 522 | print("\nBreed_costs, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 523 | if message.author.id != config['personal']['discord_id']: 524 | return await message.channel.send("This command is only available for manager") 525 | if " " in message.content: 526 | id = message.content.split(" ")[1] 527 | if not id.isdigit(): 528 | return await message.channel.send("Error in axie ID. Exampe: `$breed_costs 123456`") 529 | id = int(id) 530 | costs = scholar.get_axie_total_breed_cost(id) 531 | 532 | m = "You spent **${}** while breeding axie number **{}**.\nThe average cost for 1 breed is : **${}**.\n\n".format(costs['total_breed_cost'], id, costs['average_breed_cost']) 533 | m += "**CHILDREN**\n-------------\n" 534 | nb = 0 535 | for a in costs['details']: 536 | m += "Child : https://marketplace.axieinfinity.com/axie/{}. Cost **${}**.\n".format(str(a['axie_id']), str(a['breed_cost'])) 537 | m += "SLP cost : **$" + str(a['slp_cost']) + "** | SLP price was : **$" + str(a['slp_price']) + "**\n" 538 | m += "AXS cost : **$" + str(a['axs_cost']) + "** | AXS price was : **$" + str(a['axs_price']) + "**\n-----\n" 539 | nb += 1 540 | return await message.channel.send(m) 541 | 542 | ################################################ 543 | # Get account balance # 544 | ################################################ 545 | if "account_balance" in message.content: 546 | if message.author.id != config['personal']['discord_id']: 547 | return await message.channel.send("This command is only available for manager") 548 | print("\nAccount balance, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 549 | await message.channel.send("\nGetting account balance. This cna take some time.\n") 550 | if message.content == '$account_balance': 551 | datas = scholar.get_account_balances(config['personal']['ronin_address']) 552 | msg = "Balances for account **{}**\n".format(datas['ronin_address']) 553 | msg += "WETH : **{}** | AXS : **{}** | SLP : **{}** | Axies : **{}**\n".format(datas['WETH'], datas['AXS'], datas['SLP'], datas['axies']) 554 | return await message.channel.send(msg) 555 | elif "$account_balance " in message.content: 556 | if not ' ' in message.content: 557 | ronin_address = config['personal']['ronin_address'] 558 | else: 559 | ronin_address = message.content.split(' ')[1].replace('ronin:', '0x') 560 | if not Web3.isAddress(ronin_address): 561 | return await message.channel.send("\nError in the address.\n") 562 | 563 | datas = scholar.get_account_balances(ronin_address) 564 | msg = "Balances for account **{}**\n".format(datas['ronin_address']) 565 | msg += "WETH : **{}** | AXS : **{}** | SLP : **{}** | Axies : **{}**\n".format(datas['WETH'], datas['AXS'], datas['SLP'], datas['axies']) 566 | return await message.channel.send(msg) 567 | elif '$all' in message.content: 568 | datas = scholar.get_all_accounts_balances() 569 | msg = "" 570 | total_slp = 0 571 | total_axs = 0 572 | total_axies = 0 573 | total_weth = 0 574 | for data in datas: 575 | msg += "Balances for account **{}**\n".format(data['ronin_address']) 576 | msg += "WETH : **{}** | AXS : **{}** | SLP : **{}** | Axies : **{}**\n".format(data['WETH'], data['AXS'], data['SLP'], data['axies']) 577 | msg += "-----\n" 578 | total_slp += data['SLP'] 579 | total_axs += data['AXS'] 580 | total_axies += data['axies'] 581 | total_weth += data['WETH'] 582 | s = "Balances for all infos in the scholarship.\nTotal WETH : **{}** | Total AXS : **{}** | Total SLP : **{}** | Total Axies : **{}**\n\n".format(total_weth, total_axs, total_slp, total_axies) 583 | return await message.channel.send(s + msg) 584 | 585 | 586 | 587 | ################################################ 588 | # Get all the ronin_address in the scholarship # 589 | ################################################ 590 | if message.content == "$all_address": 591 | if message.author.id != config['personal']['discord_id']: 592 | return await message.channel.send("This command is only available for manager") 593 | print("\nall_address, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 594 | l = list() 595 | i = 0 596 | await message.channel.send("\n Here is the list of address :\n") 597 | l.append('**You** : ' + config['personal']['ronin_address']) 598 | for scholar in config['scholars']: 599 | l.append("**" + scholar + "** : " + config['scholars'][scholar]['ronin_address']) 600 | if i == 20: 601 | await message.channel.send('\n'.join(l)) 602 | l = list() 603 | i = 0 604 | i += 1 605 | if len(l) > 0: 606 | await message.channel.send('\n'.join(l)) 607 | return 608 | 609 | ################################################# 610 | # Get profiles links # 611 | ################################################# 612 | if "profile" in message.content: 613 | print("\nProfile, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 614 | url = "https://marketplace.axieinfinity.com/profile/ronin:" 615 | if message.content == "$all_profiles": 616 | try: 617 | l = list() 618 | for account in config['scholars']: 619 | address = config['scholars'][account]['ronin_address'] 620 | l.append(account + " : " + url + address.replace('0x', '') + "/axie") 621 | await message.channel.send("\n".join(l) + "\n-----------\n") 622 | except ValueError as e: 623 | await message.channel.send("Error while getting profile : " + str(e)) 624 | return e 625 | return 626 | 627 | elif message.content == "$profile": 628 | return await message.channel.send("Here is the link for your profile **" + message.author.name + "** : " + url + scholar.ronin_address.replace('0x', '') + "/axie") 629 | 630 | elif " " in message.content: 631 | try: 632 | id = message.content.split(" ")[1] 633 | if id.isnumeric(): 634 | scholar = get_account_from_id(id) 635 | else: 636 | return await message.channel.send("Error in discord ID. Example: $profile 496061891353903121") 637 | except ValueError as e: 638 | await message.channel.send("Error : " + str(e)) 639 | return e 640 | 641 | if scholar is None: 642 | return await message.channel.send("Error: No scholar found with this ID") 643 | await message.channel.send("Here is the link for " + scholar.name + " profile : " + url + scholar.ronin_address.replace('0x', 'ronin:') + "/axie") 644 | return 645 | 646 | ################################################# 647 | # Rename an axie # 648 | ################################################# 649 | if "$rename" in message.content: 650 | print("\nRename, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 651 | if message.author.id != config['personal']['discord_id']: 652 | await message.channel.send("This command is for managers only.") 653 | return 654 | if "$rename_axie " in message.content: 655 | try: 656 | id = message.content.split(" ")[1] 657 | if id.isdigit(): 658 | axie = scholar.get_axie_detail(id) 659 | scholar = get_account_from_ronin(str(axie['owner'])) 660 | else: 661 | return await message.channel.send("Error in axie ID. Example: $rename_axie 1391353 new_name") 662 | name = message.content.split(" ")[2] 663 | except ValueError as e: 664 | await message.channel.send("Error : " + str(e)) 665 | return e 666 | 667 | if scholar is None: 668 | return await message.channel.send("Error: No scholar found with this ID") 669 | if scholar.rename_axie(id, name): 670 | return await message.channel.send("Axie name of " + str(id) + " changed to : **" + name + "**") 671 | else: 672 | return await message.channel.send("Error renaming axie : **" + str(id) + "** to : **" + name + "**") 673 | 674 | ################################################ 675 | # Rename an account # 676 | ################################################ 677 | if "$rename_account " in message.content: 678 | print("\nRename account, asked by : " + message.author.name + " : " + str(message.author.id) + " at " + now.strftime("%d/%m/%Y %H:%M:%S")) 679 | await message.channel.send("This functionality is under construction") 680 | """ 681 | try: 682 | id = message.content.split(" ")[1] 683 | if id.isnumeric(): 684 | scholar = get_account_from_id(id) 685 | else: 686 | await message.channel.send("Error in discord ID. Example: $rename_account 496061891353903121 new_name") 687 | return 688 | name = message.content.split(" ")[2] 689 | except ValueError as e: 690 | await message.channel.send("Error : " + str(e)) 691 | return e 692 | if scholar is None: 693 | await message.channel.send("Error: No scholar found with this ID") 694 | return 695 | # A CREER : scholar.rename_account(name) 696 | old_name = scholar.get_profile_name() 697 | if 'error' in old_name.lower(): 698 | await message.channel.send("Error while renaming account. Maybe try again later ?") 699 | return 700 | await message.channel.send("Successfully renamed the account of from : **" + old_name + "** to : **" + name + "**") 701 | return 702 | """ 703 | return 704 | 705 | 706 | 707 | 708 | 709 | 710 | # Loads secret.yaml datas 711 | with open("secret.yaml", "r") as file: 712 | config = yaml.safe_load(file) 713 | 714 | # Run the client (This runs first) 715 | client.run(config['discord_token']) 716 | -------------------------------------------------------------------------------- /pyaxie.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import yaml 4 | import qrcode 5 | import os 6 | import datetime 7 | import time 8 | import math 9 | import pyaxie_utils 10 | 11 | from datetime import timedelta, date 12 | from web3 import Web3, exceptions 13 | from web3.auto import w3 14 | from eth_account.messages import encode_defunct 15 | from pprint import pprint 16 | from pycoingecko import CoinGeckoAPI 17 | 18 | class pyaxie(object): 19 | 20 | def __init__(self, ronin_address="", private_key=""): 21 | """ 22 | Init the class variables, we need a ronin address and its private key 23 | :param ronin_address: The ronin address 24 | :param private_key: Private key belonging to the ronin account 25 | """ 26 | with open("secret.yaml", "r") as file: 27 | config = yaml.safe_load(file) 28 | 29 | self.config = config 30 | self.ronin_address = ronin_address.replace('ronin:', '0x') 31 | self.private_key = private_key.replace('0x', '') 32 | self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 'authorization': ""} 33 | self.url = "https://axieinfinity.com/graphql-server-v2/graphql" 34 | self.url_api = config['url_api'] 35 | self.access_token = self.get_access_token() 36 | self.account_id = 0 37 | self.email = "" 38 | self.slp_contract = None 39 | self.ronin_web3 = self.get_ronin_web3() 40 | self.axie_list_path = config['paths']['axie_list_path'] 41 | self.slp_track_path = config['paths']['slp_track_path'] 42 | self.slp_abi_path = 'slp_abi.json' 43 | self.axie_abi_path = 'slp_abi.json' 44 | self.slp_contract = self.get_slp_contract(self.ronin_web3, self.slp_abi_path) 45 | self.name = "you" 46 | 47 | for scholar in config['scholars']: 48 | if config['scholars'][scholar]['ronin_address'] == ronin_address: 49 | self.payout_percentage = config['scholars'][scholar]['payout_percentage'] 50 | self.personal_ronin = config['scholars'][scholar]['personal_ronin'].replace('ronin:', '0x') 51 | self.name = scholar 52 | break 53 | else: 54 | self.payout_percentage = 0 55 | self.personal_ronin = None 56 | 57 | ############################ 58 | # Authentication functions # 59 | ############################ 60 | 61 | def get_raw_message(self): 62 | """ 63 | Ask the API for a message to sign with the private key (authenticate) 64 | :return: message to sign 65 | """ 66 | body = {"operationName": "CreateRandomMessage", "variables": {}, "query": "mutation CreateRandomMessage {\n createRandomMessage\n}\n"} 67 | 68 | r = requests.post(self.url, headers=self.headers, data=body) 69 | try: 70 | json_data = json.loads(r.text) 71 | except ValueError as e: 72 | return e 73 | return json_data['data']['createRandomMessage'] 74 | 75 | def sign_message(self, raw_message, private=''): 76 | """ 77 | Sign a raw message 78 | :param raw_message: The raw message from get_raw_message() 79 | :param private: The private key of the account 80 | :return: The signed message 81 | """ 82 | if not private: 83 | private = self.private_key 84 | pk = bytearray.fromhex(private) 85 | try: 86 | message = encode_defunct(text=raw_message) 87 | hex_signature = w3.eth.account.sign_message(message, private_key=pk) 88 | return hex_signature 89 | except 'JSONDecodeError' as e: 90 | return e + " | Maybe a problem with the axie request" 91 | 92 | def submit_signature(self, signed_message, raw_message, ronin_address=''): 93 | """ 94 | Function to submit the signature and get authorization 95 | :param signed_message: The signed message from sign_message() 96 | :param raw_message: The raw message from get_row_message() 97 | :param ronin_address: THe ronin address of the account 98 | :return: The access token 99 | """ 100 | if not ronin_address: 101 | ronin_address = self.ronin_address 102 | 103 | body = {"operationName": "CreateAccessTokenWithSignature", "variables": {"input": {"mainnet": "ronin", "owner": "User's Eth Wallet Address", "message": "User's Raw Message", "signature": "User's Signed Message"}}, "query": "mutation CreateAccessTokenWithSignature($input: SignatureInput!) { createAccessTokenWithSignature(input: $input) { newAccount result accessToken __typename }}"} 104 | body['variables']['input']['signature'] = signed_message['signature'].hex() 105 | body['variables']['input']['message'] = raw_message 106 | body['variables']['input']['owner'] = ronin_address 107 | r = requests.post(self.url, headers=self.headers, json=body) 108 | 109 | try: 110 | json_data = json.loads(r.text) 111 | except ValueError as e: 112 | return e 113 | return json_data['data']['createAccessTokenWithSignature']['accessToken'] 114 | 115 | def get_access_token(self): 116 | """ 117 | Get an access token as proof of authentication 118 | :return: The access token in string 119 | """ 120 | if not self.private_key: 121 | return 122 | msg = self.get_raw_message() 123 | signed = self.sign_message(msg) 124 | if "JSONDecodeError" in signed: 125 | print("Error getting the signed message, trying again.") 126 | token = self.get_access_token() 127 | else: 128 | token = self.submit_signature(signed, msg) 129 | self.access_token = token 130 | self.headers['authorization'] = 'Bearer ' + token 131 | return token 132 | 133 | def get_qr_code(self): 134 | """ 135 | Function to create a QRCode from an access_token 136 | """ 137 | img = qrcode.make(self.access_token) 138 | name = 'QRCode-' + str(datetime.datetime.now()) + '.png' 139 | img.save(name) 140 | return name 141 | 142 | ################################# 143 | # Account interaction functions # 144 | ################################# 145 | 146 | def get_price(self, currency): 147 | """ 148 | Get the price in USD for 1 ETH / SLP / AXS 149 | :return: The price in US of 1 token 150 | """ 151 | body = {"operationName": "NewEthExchangeRate", "variables": {}, "query": "query NewEthExchangeRate {\n exchangeRate {\n " + currency.lower() + " {\n usd\n __typename\n }\n __typename\n }\n}\n"} 152 | r = requests.post(self.url, headers=self.headers, json=body) 153 | try: 154 | json_data = json.loads(r.text) 155 | except ValueError as e: 156 | return e 157 | return json_data['data']['exchangeRate'][currency.lower()]['usd'] 158 | 159 | def get_profile_data(self): 160 | """ 161 | Retrieve your profile/account data 162 | :return: Your profil data as dict 163 | """ 164 | body = {"operationName": "GetProfileBrief", "variables": {}, "query": "query GetProfileBrief {\n profile {\n ...ProfileBrief\n __typename\n }\n}\n\nfragment ProfileBrief on AccountProfile {\n accountId\n addresses {\n ...Addresses\n __typename\n }\n email\n activated\n name\n settings {\n unsubscribeNotificationEmail\n __typename\n }\n __typename\n}\n\nfragment Addresses on NetAddresses {\n ethereum\n tomo\n loom\n ronin\n __typename\n}\n"} 165 | r = requests.post(self.url, headers=self.headers, json=body) 166 | try: 167 | json_data = json.loads(r.text) 168 | except ValueError as e: 169 | return e['data']['profile'] 170 | 171 | self.account_id = json_data['data']['profile']['accountId'] 172 | self.email = json_data['data']['profile']['email'] 173 | self.name = json_data['data']['profile']['name'] 174 | return json_data 175 | 176 | def get_activity_log(self): 177 | """ 178 | Get datas about the activity log 179 | :return: activity log 180 | """ 181 | body = {"operationName": "GetActivityLog", "variables": {"from": 0, "size": 6}, "query": "query GetActivityLog($from: Int, $size: Int) {\n profile {\n activities(from: $from, size: $size) {\n ...Activity\n __typename\n }\n __typename\n }\n}\n\nfragment Activity on Activity {\n activityId\n accountId\n action\n timestamp\n seen\n data {\n ... on ListAxie {\n ...ListAxie\n __typename\n }\n ... on UnlistAxie {\n ...UnlistAxie\n __typename\n }\n ... on BuyAxie {\n ...BuyAxie\n __typename\n }\n ... on GiftAxie {\n ...GiftAxie\n __typename\n }\n ... on MakeAxieOffer {\n ...MakeAxieOffer\n __typename\n }\n ... on CancelAxieOffer {\n ...CancelAxieOffer\n __typename\n }\n ... on SyncExp {\n ...SyncExp\n __typename\n }\n ... on MorphToPetite {\n ...MorphToPetite\n __typename\n }\n ... on MorphToAdult {\n ...MorphToAdult\n __typename\n }\n ... on BreedAxies {\n ...BreedAxies\n __typename\n }\n ... on BuyLand {\n ...BuyLand\n __typename\n }\n ... on ListLand {\n ...ListLand\n __typename\n }\n ... on UnlistLand {\n ...UnlistLand\n __typename\n }\n ... on GiftLand {\n ...GiftLand\n __typename\n }\n ... on MakeLandOffer {\n ...MakeLandOffer\n __typename\n }\n ... on CancelLandOffer {\n ...CancelLandOffer\n __typename\n }\n ... on BuyItem {\n ...BuyItem\n __typename\n }\n ... on ListItem {\n ...ListItem\n __typename\n }\n ... on UnlistItem {\n ...UnlistItem\n __typename\n }\n ... on GiftItem {\n ...GiftItem\n __typename\n }\n ... on MakeItemOffer {\n ...MakeItemOffer\n __typename\n }\n ... on CancelItemOffer {\n ...CancelItemOffer\n __typename\n }\n ... on ListBundle {\n ...ListBundle\n __typename\n }\n ... on UnlistBundle {\n ...UnlistBundle\n __typename\n }\n ... on BuyBundle {\n ...BuyBundle\n __typename\n }\n ... on MakeBundleOffer {\n ...MakeBundleOffer\n __typename\n }\n ... on CancelBundleOffer {\n ...CancelBundleOffer\n __typename\n }\n ... on AddLoomBalance {\n ...AddLoomBalance\n __typename\n }\n ... on WithdrawFromLoom {\n ...WithdrawFromLoom\n __typename\n }\n ... on AddFundBalance {\n ...AddFundBalance\n __typename\n }\n ... on WithdrawFromFund {\n ...WithdrawFromFund\n __typename\n }\n ... on TopupRoninWeth {\n ...TopupRoninWeth\n __typename\n }\n ... on WithdrawRoninWeth {\n ...WithdrawRoninWeth\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment ListAxie on ListAxie {\n axieId\n priceFrom\n priceTo\n duration\n txHash\n __typename\n}\n\nfragment UnlistAxie on UnlistAxie {\n axieId\n txHash\n __typename\n}\n\nfragment BuyAxie on BuyAxie {\n axieId\n price\n owner\n txHash\n __typename\n}\n\nfragment GiftAxie on GiftAxie {\n axieId\n destination\n txHash\n __typename\n}\n\nfragment MakeAxieOffer on MakeAxieOffer {\n axieId\n price\n txHash\n __typename\n}\n\nfragment CancelAxieOffer on CancelAxieOffer {\n axieId\n txHash\n __typename\n}\n\nfragment SyncExp on SyncExp {\n axieId\n exp\n txHash\n __typename\n}\n\nfragment MorphToPetite on MorphToPetite {\n axieId\n txHash\n __typename\n}\n\nfragment MorphToAdult on MorphToAdult {\n axieId\n txHash\n __typename\n}\n\nfragment BreedAxies on BreedAxies {\n sireId\n matronId\n lovePotionAmount\n txHash\n __typename\n}\n\nfragment BuyLand on BuyLand {\n row\n col\n price\n owner\n txHash\n __typename\n}\n\nfragment ListLand on ListLand {\n row\n col\n priceFrom\n priceTo\n duration\n txHash\n __typename\n}\n\nfragment UnlistLand on UnlistLand {\n row\n col\n txHash\n __typename\n}\n\nfragment GiftLand on GiftLand {\n row\n col\n destination\n txHash\n __typename\n}\n\nfragment MakeLandOffer on MakeLandOffer {\n row\n col\n price\n txHash\n __typename\n}\n\nfragment CancelLandOffer on CancelLandOffer {\n row\n col\n txHash\n __typename\n}\n\nfragment BuyItem on BuyItem {\n tokenId\n itemAlias\n price\n owner\n txHash\n __typename\n}\n\nfragment ListItem on ListItem {\n tokenId\n itemAlias\n priceFrom\n priceTo\n duration\n txHash\n __typename\n}\n\nfragment UnlistItem on UnlistItem {\n tokenId\n itemAlias\n txHash\n __typename\n}\n\nfragment GiftItem on GiftItem {\n tokenId\n itemAlias\n destination\n txHash\n __typename\n}\n\nfragment MakeItemOffer on MakeItemOffer {\n tokenId\n itemAlias\n price\n txHash\n __typename\n}\n\nfragment CancelItemOffer on CancelItemOffer {\n tokenId\n itemAlias\n txHash\n __typename\n}\n\nfragment BuyBundle on BuyBundle {\n listingIndex\n price\n owner\n txHash\n __typename\n}\n\nfragment ListBundle on ListBundle {\n numberOfItems\n priceFrom\n priceTo\n duration\n txHash\n __typename\n}\n\nfragment UnlistBundle on UnlistBundle {\n listingIndex\n txHash\n __typename\n}\n\nfragment MakeBundleOffer on MakeBundleOffer {\n listingIndex\n price\n txHash\n __typename\n}\n\nfragment CancelBundleOffer on CancelBundleOffer {\n listingIndex\n txHash\n __typename\n}\n\nfragment AddLoomBalance on AddLoomBalance {\n amount\n senderAddress\n receiverAddress\n txHash\n __typename\n}\n\nfragment WithdrawFromLoom on WithdrawFromLoom {\n amount\n senderAddress\n receiverAddress\n txHash\n __typename\n}\n\nfragment AddFundBalance on AddFundBalance {\n amount\n senderAddress\n txHash\n __typename\n}\n\nfragment WithdrawFromFund on WithdrawFromFund {\n amount\n receiverAddress\n txHash\n __typename\n}\n\nfragment WithdrawRoninWeth on WithdrawRoninWeth {\n amount\n receiverAddress\n txHash\n receiverAddress\n __typename\n}\n\nfragment TopupRoninWeth on TopupRoninWeth {\n amount\n receiverAddress\n txHash\n receiverAddress\n __typename\n}\n"} 182 | r = requests.post(self.url, headers=self.headers, json=body) 183 | try: 184 | json_data = json.loads(r.text) 185 | except ValueError as e: 186 | return e 187 | return json_data['data']['profile']['activities'] 188 | 189 | def get_profile_name(self, ronin_address=''): 190 | """ 191 | Get the profile name of a ronin address 192 | :param ronin_address: The target ronin account 193 | :return: The name of the account 194 | """ 195 | if ronin_address == '': 196 | ronin_address = self.ronin_address 197 | body = {"operationName": "GetProfileNameByRoninAddress", "variables": {"roninAddress": ronin_address}, "query": "query GetProfileNameByRoninAddress($roninAddress: String!) {\n publicProfileWithRoninAddress(roninAddress: $roninAddress) {\n accountId\n name\n __typename\n }\n}\n"} 198 | r = requests.post(self.url, headers=self.headers, json=body) 199 | try: 200 | json_data = json.loads(r.text) 201 | except ValueError as e: 202 | return e 203 | return json_data['data']['publicProfileWithRoninAddress']['name'] 204 | 205 | def rename_account(self, new_name): 206 | body = {"operationName": "RenameAxie", "variables": {"axieId": str(new_name),"name": str(new_name) }, "query": "mutation RenameAxie($axieId: ID!, $name: String!) {\n renameAxie(axieId: $axieId, name: $name) {\n result\n __typename\n }\n}\n"} 207 | try: 208 | r = requests.post(self.url, headers=self.headers, json=body) 209 | json_data = json.loads(r.text) 210 | except ValueError as e: 211 | return e 212 | if json_data['data'] is None: 213 | return json_data['errors']['message'] 214 | return json_data['data']['renameAxie']['result'] 215 | 216 | def get_public_profile(self, ronin_address=''): 217 | """ 218 | Get infos about the given ronin address 219 | :param ronin_address: The target ronin account 220 | :return: Public datas of the ronin account 221 | """ 222 | if ronin_address == '': 223 | ronin_address = self.ronin_address 224 | body = {"operationName": "GetProfileByRoninAddress", "variables": {"roninAddress": ronin_address}, "query": "query GetProfileByRoninAddress($roninAddress: String!) {\n publicProfileWithRoninAddress(roninAddress: $roninAddress) {\n ...Profile\n __typename\n }\n}\n\nfragment Profile on PublicProfile {\n accountId\n name\n addresses {\n ...Addresses\n __typename\n }\n __typename\n}\n\nfragment Addresses on NetAddresses {\n ethereum\n tomo\n loom\n ronin\n __typename\n}\n"} 225 | r = requests.post(self.url, headers=self.headers, json=body) 226 | try: 227 | json_data = json.loads(r.text) 228 | except ValueError as e: 229 | return e 230 | return json_data['data']['publicProfileWithRoninAddress'] 231 | 232 | def get_rank_mmr(self, ronin_address=''): 233 | """ 234 | Get the mmr and rank of the current account 235 | :return: Dict with MMR and rank 236 | """ 237 | if ronin_address == '': 238 | ronin_address = self.ronin_address 239 | params = {"client_id": ronin_address, "offset": 0, "limit": 0} 240 | 241 | # Try multiple times to avoid return 0 242 | for i in range(0, 5): 243 | try: 244 | r = requests.get(self.url_api + "last-season-leaderboard", params=params) 245 | json_data = json.loads(r.text) 246 | if json_data['success']: 247 | return {'mmr': int(json_data['items'][1]['elo']), 'rank': int(json_data['items'][1]['rank'])} 248 | except ValueError as e: 249 | return e 250 | return {'mmr': 0, 'rank': 0} 251 | 252 | 253 | def get_daily_slp(self): 254 | """ 255 | Get the daily SLP ratio based on SLP farmed between now and last claim 256 | :return: Dict with ratio and date 257 | """ 258 | unclaimed = self.get_unclaimed_slp() 259 | t = datetime.datetime.fromtimestamp(self.get_last_claim()) 260 | days = (t - datetime.datetime.utcnow()).days * -1 261 | if days <= 0: 262 | return unclaimed 263 | return int(unclaimed / days) 264 | 265 | ############################################# 266 | # Functions to interact with axies from web # 267 | ############################################# 268 | 269 | def get_axie_list(self, ronin_address=''): 270 | """ 271 | Get informations about the axies in a specific account 272 | :param ronin_address: The ronin address of the target account 273 | :return: Data about the axies 274 | """ 275 | if ronin_address == '': 276 | ronin_address = self.ronin_address 277 | body = {"operationName": "GetAxieBriefList", "variables": {"from": 0, "size": 24, "sort": "IdDesc", "auctionType": "All", "owner": ronin_address, "criteria": {"region": None, "parts": None, "bodyShapes": None, "classes": None, "stages": None, "numMystic": None, "pureness": None, "title": None, "breedable": None, "breedCount": None, "hp":[],"skill":[],"speed":[],"morale":[]}},"query":"query GetAxieBriefList($auctionType: AuctionType, $criteria: AxieSearchCriteria, $from: Int, $sort: SortBy, $size: Int, $owner: String) {\n axies(auctionType: $auctionType, criteria: $criteria, from: $from, sort: $sort, size: $size, owner: $owner) {\n total\n results {\n ...AxieBrief\n __typename\n }\n __typename\n }\n}\n\nfragment AxieBrief on Axie {\n id\n name\n stage\n class\n breedCount\n image\n title\n battleInfo {\n banned\n __typename\n }\n auction {\n currentPrice\n currentPriceUSD\n __typename\n }\n parts {\n id\n name\n class\n type\n specialGenes\n __typename\n }\n __typename\n}\n"} 278 | try: 279 | r = requests.post(self.url, headers=self.headers, json=body) 280 | json_data = json.loads(r.text) 281 | except ValueError as e: 282 | return e 283 | return json_data['data']['axies']['results'] 284 | 285 | def get_all_axie_list(self): 286 | """ 287 | Get informations about the axies in all the accounts 288 | :return: List with all axies datas 289 | """ 290 | res = list() 291 | for account in self.config['scholars']: 292 | axies = self.get_axie_list(self.config['scholars'][account]['ronin_address']) 293 | for axie in axies: 294 | res.append(axie) 295 | for axie in self.get_axie_list(self.config['personal']['ronin_address']): 296 | res.append(axie) 297 | return res 298 | 299 | def get_all_axie_class(self, axie_class, axies_datas=[]): 300 | """ 301 | Return all the axies of a specific class present in the scholarship 302 | :param axie_class: Plant, Bast, Bird, etc... 303 | :return: List of axie object of the specific class 304 | """ 305 | if not axies_datas: 306 | axies_datas = self.get_all_axie_list() 307 | l = list() 308 | for axie in axies_datas: 309 | if axie['class'] is not None and axie['class'].lower() == axie_class.lower(): 310 | l.append(axie) 311 | return l 312 | 313 | def get_axie_image(self, axie_id): 314 | """ 315 | Get the image link to an axie 316 | :param axie_id: String ID of the axie you are targeting 317 | :return: Link to the image 318 | """ 319 | body = {"operationName": "GetAxieMetadata", "variables": {"axieId": axie_id}, "query": "query GetAxieMetadata($axieId: ID!) {\n axie(axieId: $axieId) {\n id\n image\n __typename\n }\n}\n"} 320 | try: 321 | r = requests.post(self.url, headers=self.headers, json=body) 322 | json_data = json.loads(r.text) 323 | except ValueError as e: 324 | return e 325 | return json_data['data']['axie']['image'] 326 | 327 | def get_number_of_axies(self): 328 | """ 329 | Get the number of axies in the account 330 | :return: the number of axies 331 | """ 332 | axies = self.get_axie_list() 333 | return len(axies) 334 | 335 | def download_axie_image(self, axie_id): 336 | """ 337 | Download the image of an axie and return the path 338 | :param axie_id: ID of the axie 339 | :return: Path of the image 340 | """ 341 | axie_id = str(axie_id) 342 | path = './img/axies/' + axie_id + '.png' 343 | dir = os.path.join(".", "img", "axies") 344 | if not os.path.exists(dir): 345 | os.mkdir(dir) 346 | 347 | if os.path.exists(path): 348 | return path 349 | 350 | img_data = requests.get('https://storage.googleapis.com/assets.axieinfinity.com/axies/'+axie_id+'/axie/axie-full-transparent.png').content 351 | if len(img_data) <= 500: 352 | return './img/axies/egg.png' 353 | with open(path, 'ab') as img: 354 | img.write(img_data) 355 | return path 356 | 357 | def get_axies_imageline(self): 358 | """ 359 | Get the path to the picture containing the 3 axies merged horizontally 360 | :return: Path of the new picture 361 | """ 362 | try: 363 | axies = self.get_axie_list() 364 | l = list() 365 | for axie in axies: 366 | l.append(self.download_axie_image(axie['id'])) 367 | if len(l) < 3: 368 | return 'Error: not enough axies on the account' 369 | except ValueError as e: 370 | return e 371 | return pyaxie_utils.merge_images(l[0], l[1], l[2], self.name) 372 | 373 | def get_axie_detail(self, axie_id): 374 | """ 375 | Get informations about an Axie based on its ID 376 | :param axie_id: string ID of the axie 377 | :return: A dict with the adatas of the axie 378 | """ 379 | body = {"operationName": "GetAxieDetail", "variables": {"axieId": axie_id}, "query": "query GetAxieDetail($axieId: ID!) {\n axie(axieId: $axieId) {\n ...AxieDetail\n __typename\n }\n}\n\nfragment AxieDetail on Axie {\n id\n image\n class\n chain\n name\n genes\n owner\n birthDate\n bodyShape\n class\n sireId\n sireClass\n matronId\n matronClass\n stage\n title\n breedCount\n level\n figure {\n atlas\n model\n image\n __typename\n }\n parts {\n ...AxiePart\n __typename\n }\n stats {\n ...AxieStats\n __typename\n }\n auction {\n ...AxieAuction\n __typename\n }\n ownerProfile {\n name\n __typename\n }\n battleInfo {\n ...AxieBattleInfo\n __typename\n }\n children {\n id\n name\n class\n image\n title\n stage\n __typename\n }\n __typename\n}\n\nfragment AxieBattleInfo on AxieBattleInfo {\n banned\n banUntil\n level\n __typename\n}\n\nfragment AxiePart on AxiePart {\n id\n name\n class\n type\n specialGenes\n stage\n abilities {\n ...AxieCardAbility\n __typename\n }\n __typename\n}\n\nfragment AxieCardAbility on AxieCardAbility {\n id\n name\n attack\n defense\n energy\n description\n backgroundUrl\n effectIconUrl\n __typename\n}\n\nfragment AxieStats on AxieStats {\n hp\n speed\n skill\n morale\n __typename\n}\n\nfragment AxieAuction on Auction {\n startingPrice\n endingPrice\n startingTimestamp\n endingTimestamp\n duration\n timeLeft\n currentPrice\n currentPriceUSD\n suggestedPrice\n seller\n listingIndex\n state\n __typename\n}\n"} 380 | try: 381 | r = requests.post(self.url, headers=self.headers, json=body) 382 | json_data = json.loads(r.text) 383 | except ValueError as e: 384 | return None 385 | return json_data['data']['axie'] 386 | 387 | def get_axie_name(self, axie_id): 388 | """ 389 | Get the name of an axie based on his ID 390 | :param axie_id: The id of the axie 391 | :return: Name of the axie 392 | """ 393 | body = {"operationName": "GetAxieName", "variables": {"axieId": axie_id}, "query": "query GetAxieName($axieId: ID!) {\n axie(axieId: $axieId) {\n ...AxieName\n __typename\n }\n}\n\nfragment AxieName on Axie {\n name\n __typename\n}\n"} 394 | try: 395 | r = requests.post(self.url, headers=self.headers, json=body) 396 | json_data = json.loads(r.text) 397 | except ValueError as e: 398 | return e 399 | return json_data['data']['axie']['name'] 400 | 401 | def get_axie_stats(self, axie_id): 402 | """ 403 | Get axie 4 basic stats (HP, morale, skill, speed) 404 | :param axie_id: String ID of the axie 405 | :return: Dict with the stats 406 | """ 407 | data = self.get_axie_detail(axie_id) 408 | return data['stats'] 409 | 410 | def get_axie_parts(self, axie_id): 411 | """ 412 | Get axies body parts from an axie ID 413 | :param axie_id: String ID of the axie 414 | :return: Dict with the differents body parts 415 | """ 416 | data = self.get_axie_detail(axie_id) 417 | return data['parts'] 418 | 419 | def get_axie_class(self, axie_id): 420 | """ 421 | Get the class of an axie based on it's ID 422 | :param axie_id: String ID of the axie 423 | :return: Dict with the differents body parts 424 | """ 425 | data = self.get_axie_detail(axie_id) 426 | return data['class'] 427 | 428 | def get_axie_children(self, id=0, axie_data={}): 429 | """ 430 | Get the children of an axie on given id OR given axie datas 431 | :param id: id of the axie 432 | :param axie_data: axie_datas 433 | :return: list of id of the children 434 | """ 435 | axie = self.get_axie_detail(id) if axie_data == {} else axie_data 436 | l = list() 437 | for children in axie['children']: 438 | l.append(int(children['id'])) 439 | return l 440 | 441 | def rename_axie(self, axie_id, new_name): 442 | """ 443 | Rename an axie 444 | :param axie_id: The id of the axie to rename 445 | :param new_name: The new name of the axie 446 | :return: True/False or error 447 | """ 448 | body = {"operationName": "RenameAxie", "variables": {"axieId": str(axie_id),"name": str(new_name) }, "query": "mutation RenameAxie($axieId: ID!, $name: String!) {\n renameAxie(axieId: $axieId, name: $name) {\n result\n __typename\n }\n}\n"} 449 | try: 450 | r = requests.post(self.url, headers=self.headers, json=body) 451 | json_data = json.loads(r.text) 452 | pprint(json_data) 453 | except ValueError as e: 454 | return e 455 | if json_data['data'] is None: 456 | return False 457 | return json_data['data']['renameAxie']['result'] 458 | 459 | ############################################### 460 | # Functions to interact with stored axie data # 461 | ############################################### 462 | 463 | def save_axie(self, axie_data): 464 | """ 465 | Save an axie details to the local axie_list 466 | :param axie_data: 467 | :param axie_list: Path of the axie_list file 468 | """ 469 | axie_list = self.axie_list() 470 | axie_id = axie_data['id'] 471 | if axie_list: 472 | axie_list.update({axie_id: axie_data}) 473 | else: 474 | axie_list = {axie_id: axie_data} 475 | f = open(self.axie_list_path, 'a') 476 | yaml.safe_dump(axie_list, f) 477 | f.close() 478 | 479 | def check_axie(self, axie_id): 480 | """ 481 | Check if we have this axie data locally 482 | :param axie_id: String of the ID of the axie to check 483 | :return: Data of the axie or None if not locally 484 | """ 485 | with open(self.axie_list_path) as f: 486 | data = yaml.safe_load(f) 487 | 488 | for val in data: 489 | if val == str(axie_id): 490 | return val 491 | return [] 492 | 493 | def update_axie_list(self): 494 | """ 495 | Update the local axie_list with the new datas 496 | """ 497 | axie_list = self.axie_list() 498 | new = {} 499 | 500 | for axie in axie_list: 501 | new[axie['id']] = self.get_axie_detail(axie['id']) 502 | with open(self.axie_list_path, 'w') as outfile: 503 | yaml.safe_dump(new, outfile) 504 | 505 | def axie_list(self): 506 | """ 507 | Get the list of axies stored locally 508 | :return: List of axie data 509 | """ 510 | if os.stat(self.axie_list_path).st_size > 3: 511 | f = open(self.axie_list_path, 'r') 512 | data = yaml.safe_load(f) 513 | f.close() 514 | if data: 515 | return data 516 | return None 517 | 518 | def axie_detail(self, axie_id): 519 | """ 520 | Retrieve local details about an axie 521 | :param axie_id: The ID of the axie 522 | :return: Informations about the axie 523 | """ 524 | data = self.axie_list() 525 | if data: 526 | return data[str(axie_id)] 527 | return None 528 | 529 | def axie_infos(self, axie_id, key): 530 | """ 531 | Retrieve locally specific informations (key) about axie 532 | :param axie_id: String ID of the axie 533 | :param key: 'parts' or 'class' or 'stats' (list on documentation) 534 | :return: Information about the axie 535 | """ 536 | if self.check_axie(axie_id): 537 | return self.axie_detail(axie_id)[key] 538 | return "This axie is not registered : " + str(axie_id) 539 | 540 | def axie_link(self, axie_id): 541 | """ 542 | Return an URL to the axie page 543 | :param axie_id: Id of the axie 544 | :return: URL of the axie 545 | """ 546 | url = 'https://marketplace.axieinfinity.com/axie/' 547 | return url + str(axie_id) 548 | 549 | ################### 550 | # Ronin functions # 551 | ################### 552 | 553 | def get_ronin_web3(self): 554 | """ 555 | :return: Return the ronin web3 556 | """ 557 | web3 = Web3(Web3.HTTPProvider('https://proxy.roninchain.com/free-gas-rpc')) 558 | return web3 559 | 560 | def get_slp_contract(self, ronin_web3, slp_abi_path): 561 | """ 562 | :param ronin_web3: ronin web3 object 563 | :param slp_abi_path: ABI for SLP 564 | :return: The contract to interact with 565 | """ 566 | slp_contract_address = "0xa8754b9fa15fc18bb59458815510e40a12cd2014" 567 | with open(slp_abi_path) as f: 568 | try: 569 | slp_abi = json.load(f) 570 | except ValueError as e: 571 | return e 572 | contract = ronin_web3.eth.contract(address=w3.toChecksumAddress(slp_contract_address), abi=slp_abi) 573 | self.slp_contract = contract 574 | return contract 575 | 576 | def get_axie_contract(self, ronin_web3): 577 | slp_contract_address = "0x32950db2a7164ae833121501c797d79e7b79d74c" 578 | with open(self.axie_abi_path) as f: 579 | try: 580 | slp_abi = json.load(f) 581 | except ValueError as e: 582 | return e 583 | contract = ronin_web3.eth.contract(address=w3.toChecksumAddress(slp_contract_address), abi=slp_abi) 584 | self.slp_contract = contract 585 | return contract 586 | 587 | 588 | def get_claimed_slp(self, address=''): 589 | """ 590 | :param address: Ronin address to check 591 | :return: The amount of claimed SLP 592 | """ 593 | if address == '': 594 | address = self.ronin_address 595 | try: 596 | response = requests.get(self.url_api + f"clients/{address}/items/1", headers=self.headers, data="") 597 | data = json.loads(response.text) 598 | except ValueError as e: 599 | return e 600 | 601 | balance = data['blockchain_related']['balance'] 602 | if balance is None: 603 | return 0 604 | 605 | return int(balance) 606 | 607 | def get_unclaimed_slp(self, address=''): 608 | """ 609 | :param address: Ronin address to check 610 | :return: The amount of unclaimed SLP 611 | """ 612 | if address == '': 613 | address = self.ronin_address 614 | try: 615 | response = requests.get(self.url_api + f"clients/{address}/items/1", headers=self.headers, data="") 616 | result = response.json() 617 | except ValueError as e: 618 | return e 619 | if result is None: 620 | return 0 621 | 622 | balance = -1 623 | if 'blockchain_related' in result: 624 | balance = result['blockchain_related']['balance'] 625 | else: 626 | return balance 627 | if balance is None: 628 | balance = 0 629 | 630 | res = result["total"] 631 | if res is None: 632 | res = 0 633 | return int(res - balance) 634 | 635 | def get_last_claim(self, address=''): 636 | """ 637 | Return the last time SLP was claimed for this account 638 | :param address: Ronin address 639 | :return: Time in sec 640 | """ 641 | if address == '': 642 | address = self.ronin_address 643 | 644 | try: 645 | response = requests.get(self.url_api + f"clients/{address}/items/1", headers=self.headers, data="") 646 | result = response.json() 647 | except ValueError as e: 648 | return e 649 | 650 | return int(result["last_claimed_item_at"]) 651 | 652 | def claim_slp(self): 653 | """ 654 | Claim SLP on the account. 655 | :return: Transaction of the claim 656 | """ 657 | print("\nClaiming SLP for : ", self.name) 658 | 659 | if datetime.datetime.utcnow() + timedelta(days=-14) < datetime.datetime.fromtimestamp(self.get_last_claim()): 660 | return 'Error: Too soon to claim or already claimed' 661 | 662 | slp_claim = { 663 | 'address': self.ronin_address, 664 | 'private_key': self.private_key, 665 | 'state': {"signature": None} 666 | } 667 | access_token = self.access_token 668 | custom_headers = self.headers.copy() 669 | custom_headers["authorization"] = f"Bearer {access_token}" 670 | response = requests.post(self.url_api + f"clients/{self.ronin_address}/items/1/claim", headers=custom_headers, json="") 671 | 672 | if response.status_code != 200: 673 | print(response.text) 674 | return 675 | 676 | result = response.json()["blockchain_related"]["signature"] 677 | if result is None: 678 | return 'Error: Nothing to claim' 679 | 680 | checksum_address = w3.toChecksumAddress(self.ronin_address) 681 | nonce = self.ronin_web3.eth.get_transaction_count(checksum_address) 682 | slp_claim['state']["signature"] = result["signature"].replace("0x", "") 683 | claim_txn = self.slp_contract.functions.checkpoint(checksum_address, result["amount"], result["timestamp"], 684 | slp_claim['state']["signature"]).buildTransaction({'gas': 1000000, 'gasPrice': 0, 'nonce': nonce}) 685 | signed_txn = self.ronin_web3.eth.account.sign_transaction(claim_txn, private_key=bytearray.fromhex(self.private_key.replace("0x", ""))) 686 | 687 | self.ronin_web3.eth.send_raw_transaction(signed_txn.rawTransaction) 688 | txn = self.ronin_web3.toHex(self.ronin_web3.keccak(signed_txn.rawTransaction)) 689 | return txn if self.wait_confirmation(txn) else "Error : Transaction " + str(txn) + "reverted by EVM (Ethereum Virtual machine)" 690 | 691 | def transfer_slp(self, to_address, amount): 692 | """ 693 | Transfer SLP from pyaxie ronin address to the to_address 694 | :param to_address: Receiver of the SLP. Format : 0x 695 | :param amount: Amount of SLP to send 696 | :return: Transaction hash 697 | """ 698 | if amount < 1 or not Web3.isAddress(to_address): 699 | return {"error": "Make sure that the amount is not under 1 and the **to_address** is correct."} 700 | 701 | transfer_txn = self.slp_contract.functions.transfer(w3.toChecksumAddress(to_address), amount).buildTransaction({ 702 | 'chainId': 2020, 703 | 'gas': 100000, 704 | 'gasPrice': Web3.toWei('0', 'gwei'), 705 | 'nonce': self.ronin_web3.eth.get_transaction_count(w3.toChecksumAddress(self.ronin_address)) 706 | }) 707 | private_key = bytearray.fromhex(self.private_key.replace("0x", "")) 708 | signed_txn = self.ronin_web3.eth.account.sign_transaction(transfer_txn, private_key=private_key) 709 | 710 | self.ronin_web3.eth.send_raw_transaction(signed_txn.rawTransaction) 711 | txn = self.ronin_web3.toHex(self.ronin_web3.keccak(signed_txn.rawTransaction)) 712 | return txn if self.wait_confirmation(txn) else "Error : Transaction " + str(txn) + " reverted by EVM (Ethereum Virtual machine)" 713 | 714 | def wait_confirmation(self, txn): 715 | """ 716 | Wait for a transaction to finish 717 | :param txn: the transaction to wait 718 | :return: True or False depending if transaction succeed 719 | """ 720 | while True: 721 | try: 722 | recepit = self.ronin_web3.eth.get_transaction_receipt(txn) 723 | success = True if recepit["status"] == 1 else False 724 | break 725 | except exceptions.TransactionNotFound: 726 | time.sleep(5) 727 | return success 728 | 729 | def payout(self): 730 | """ 731 | Send money to the scholar and to the manager/academy or directly to manager if manager called 732 | :return: List of 2 transactions hash : scholar and manager 733 | """ 734 | self.claim_slp() 735 | 736 | txns = list() 737 | slp_balance = self.get_claimed_slp() 738 | scholar_payout_amount = math.ceil(slp_balance * self.payout_percentage) 739 | academy_payout_amount = slp_balance - scholar_payout_amount 740 | 741 | if slp_balance < 1: 742 | return ["Error: Nothing to send.", "Error: Nothing to send."] 743 | 744 | if self.payout_percentage == 0: 745 | print("Sending all the {} SLP to you : {} ".format(academy_payout_amount, self.config['personal']['ronin_address'])) 746 | txns.append(str(self.transfer_slp(self.config['personal']['ronin_address'], academy_payout_amount + scholar_payout_amount))) 747 | txns.append("Nothing to send to scholar") 748 | return txns 749 | else: 750 | print("Sending {} SLP to {} : {} ".format(academy_payout_amount, "You", self.config['personal']['ronin_address'])) 751 | txns.append(str(self.transfer_slp(self.config['personal']['ronin_address'], academy_payout_amount))) 752 | 753 | print("Sending {} SLP to {} : {} ".format(scholar_payout_amount, self.name, self.personal_ronin)) 754 | txns.append(str(self.transfer_slp(self.personal_ronin, scholar_payout_amount))) 755 | return txns 756 | 757 | def get_mint_burn_graph(self): 758 | """ 759 | Create a chart in /img with SLP burned/minted 760 | :return: path of the image 761 | """ 762 | """ 763 | try: 764 | slp_supply = json.loads(requests.get("https://www.axieworld.com/api/charts/slp-issuance").content) 765 | dates, mints, burns = ([],) * 3 766 | today = date.today() 767 | qc = QuickChart() 768 | qc.width = 500 769 | qc.height = 300 770 | qc.device_pixel_ratio = 2.0 771 | 772 | for i in range(0, 6): 773 | dates.append(today) if i == 0 else dates.append(today - timedelta(days=i)) 774 | mints.append(round(slp_supply["data"]["minted"][-(i + 1)] / 1000000, 1)) 775 | burns.append(round(slp_supply["data"]["burned"][-(i + 1)] / 1000000, 1)) 776 | 777 | qc.config = { 778 | "type": "bar", 779 | "data": { 780 | "labels": dates, 781 | "datasets": [{ 782 | "label": 'Minted', 783 | "backgroundColor": 'rgb(35, 90, 155)', 784 | "stack": 'Stack 0', 785 | "data": mints, 786 | }, 787 | { 788 | "label": 'Burned', 789 | "backgroundColor": 'rgb(245, 158, 27)', 790 | "stack": 'Stack 1', 791 | "data": burns, 792 | }, 793 | ] 794 | }, 795 | "options": { 796 | "plugins": { 797 | "datalabels": { 798 | "anchor": 'end', 799 | "align": 'top', 800 | "color": '#fff', 801 | "backgroundColor": 'rgba(54, 57, 63, 1.0)' 802 | }, 803 | }, 804 | "title": { 805 | "display": "true", 806 | "text": "SLP minted vs burned (in millions)", 807 | }, 808 | "tooltips": { 809 | "mode": "index", 810 | "intersect": "false", 811 | }, 812 | "responsive": "true", 813 | }, 814 | } 815 | path = 'img/slpMintedVsBurned.png' 816 | qc.to_file(path) 817 | except ValueError as e: 818 | return e 819 | return path 820 | """ 821 | return 822 | 823 | def get_breed_cost(self, nb=-1): 824 | """ 825 | Get the breeding cost 826 | :param nb: breed lvl (0-6) 827 | :return: dict with datas about the breeding costs 828 | """ 829 | breeds = {0: 150, 1: 300, 2: 450, 3: 750, 4: 1200, 5: 1950, 6: 3150} 830 | axs = self.get_price('axs') 831 | slp = self.get_price('slp') 832 | total = 0 833 | res = dict() 834 | 835 | for i in range(0, 7): 836 | breed_price = int((breeds[i] * slp) * 2 + (axs * 2)) 837 | total += breed_price 838 | res[i] = {'price': breed_price, 'total_breed_price': total, 'average_price': int(total/(1+i))} 839 | 840 | if nb <= -1: 841 | return res 842 | return {nb, res[nb]} 843 | 844 | def get_prices_from_timestamp(self, timestamp): 845 | """ 846 | Get prices for AXS, SLP and ETH at given date 847 | :param timestamp: date in unix timestamp format 848 | :return: Dict with the prices of currencies at given date 849 | """ 850 | cg = CoinGeckoAPI() 851 | dt = datetime.datetime.fromtimestamp(timestamp) 852 | 853 | price_history = cg.get_coin_history_by_id(id='smooth-love-potion', date=dt.date().strftime('%d-%m-%Y'), vsCurrencies=['usd']) 854 | slp_price = price_history['market_data']['current_price']['usd'] 855 | 856 | price_history = cg.get_coin_history_by_id(id='axie-infinity', date=dt.date().strftime('%d-%m-%Y'), vsCurrencies=['usd']) 857 | axs_price = price_history['market_data']['current_price']['usd'] 858 | 859 | price_history = cg.get_coin_history_by_id(id='ethereum', date=dt.date().strftime('%d-%m-%Y'), vsCurrencies=['usd']) 860 | eth_price = price_history['market_data']['current_price']['usd'] 861 | 862 | return {'slp': slp_price, 'axs': axs_price, 'eth': eth_price, 'date': timestamp} 863 | 864 | def ronin_txs(self, ronin_address=''): 865 | if ronin_address == '': 866 | ronin_address = self.config['personal']['ronin_address'] 867 | 868 | url = "https://explorer.roninchain.com/api/txs/" + str(ronin_address) + "?size=10000" 869 | h = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36'} 870 | response = requests.get(url, headers=h) 871 | 872 | try: 873 | json_data = json.loads(response.text) 874 | except ValueError as e: 875 | return e 876 | return json_data['results'] 877 | 878 | def get_axie_total_breed_cost(self, axie_id, txs={}): 879 | if not isinstance(axie_id, int): 880 | return "Error in axie ID." 881 | if txs == {}: 882 | txs = self.ronin_txs() 883 | 884 | children = self.get_axie_children(axie_id) 885 | total = 0 886 | l = list() 887 | for i in txs: 888 | if len(i['logs']) == 4 and len(i['logs'][3]['topics']) > 1 and int(i['logs'][3]['topics'][1], 16) in children: 889 | prices = self.get_prices_from_timestamp(i['timestamp']) 890 | slp_price = int(i['logs'][1]['data'], 16) * prices['slp'] 891 | axs_price = prices['axs'] * 2 892 | l.append({'date': datetime.datetime.fromtimestamp(i['timestamp']).strftime('%d-%m-%Y'), 893 | 'axs_price': round(prices['axs'], 2), 'slp_price': round(prices['slp'], 2), 894 | 'breed_cost': round(slp_price + axs_price, 2), 'axs_cost': round(axs_price, 2), 895 | 'slp_cost': round(slp_price, 2), 'axie_id': int(i['logs'][3]['topics'][1], 16)}) 896 | total += slp_price + axs_price 897 | res = dict() 898 | res['total_breed_cost'] = round(total, 2) 899 | res['average_breed_cost'] = round(total / len(children), 2) 900 | res['details'] = l 901 | return res 902 | 903 | def get_account_balances(self, ronin_address=''): 904 | """ 905 | Get the different balances for a given account (AXS, SLP, WETH, AXIES) 906 | :param ronin_address: ronin address of the account 907 | :return: dict with currencies and amount 908 | """ 909 | if not ronin_address: 910 | ronin_address = self.config['personal']['ronin_address'] 911 | 912 | url = "https://explorer.roninchain.com/api/tokenbalances/" + str(ronin_address).replace('ronin:', '0x') 913 | headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36'} 914 | response = requests.get(url, headers=headers) 915 | 916 | try: 917 | json_data = json.loads(response.text) 918 | except ValueError as e: 919 | return {'WETH': -1, 'AXS': -1, 'SLP': -1, 'axies': -1, 'ronin_address': ronin_address} 920 | 921 | res = {'WETH': 0, 'AXS': 0, 'SLP': 0, 'axies': 0, 'ronin_address': ronin_address} 922 | for data in json_data['results']: 923 | if data['token_symbol'] == 'WETH': 924 | res['WETH'] = round(int(data['balance']) / math.pow(10, 18), 6) 925 | elif data['token_symbol'] == 'AXS': 926 | res['AXS'] = round(int(data['balance']) / math.pow(10, 18), 2) 927 | elif data['token_symbol'] == 'SLP': 928 | res['SLP'] = int(data['balance']) 929 | elif data['token_symbol'] == 'AXIE': 930 | res['axies'] = int(data['balance']) 931 | return res 932 | 933 | def get_all_accounts_balances(self): 934 | l = list() 935 | l.append(self.config['personal']['ronin_address']) 936 | for account in self.config['scholars']: 937 | l.append(self.config['scholars'][account]['ronin_address']) 938 | 939 | res = list() 940 | for r in l: 941 | res.append(self.get_account_balances(r)) 942 | return res 943 | 944 | 945 | 946 | --------------------------------------------------------------------------------