From ef93bdbcef19d5b47e12f11acbcd2b6c36d9da1c Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 08:35:44 +0200 Subject: [PATCH 01/26] Add enums classes --- TonTools/Enums/Address.py | 3 +++ TonTools/Enums/Exception.py | 34 ++++++++++++++++++++++++++++++++++ TonTools/Enums/Jetton.py | 19 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 TonTools/Enums/Address.py create mode 100644 TonTools/Enums/Exception.py create mode 100644 TonTools/Enums/Jetton.py diff --git a/TonTools/Enums/Address.py b/TonTools/Enums/Address.py new file mode 100644 index 0000000..ff29826 --- /dev/null +++ b/TonTools/Enums/Address.py @@ -0,0 +1,3 @@ +class AddressForm: + RAW = 'raw' + USER_FRIENDLY = 'user_friendly' diff --git a/TonTools/Enums/Exception.py b/TonTools/Enums/Exception.py new file mode 100644 index 0000000..19c6413 --- /dev/null +++ b/TonTools/Enums/Exception.py @@ -0,0 +1,34 @@ +class TVMExitCode(BaseException): + EXIT_CODES = { + 0: 'Standard successful execution exit code.', + 1: 'Alternative successful execution exit code.', + 2: 'Stack underflow. Last op-code consumed more elements than there are on the stacks.', + 3: 'Stack overflow. More values have been stored on a stack than allowed by this version of TVM.', + 4: 'Integer overflow. Integer does not fit into −2256 ≤ x < 2256 or a division by zero has occurred.', + 5: 'Integer out of expected range.', + 6: 'Invalid opcode. Instruction is unknown in the current TVM version.', + 7: 'Type check error. An argument to a primitive is of an incorrect value type.', + 8: 'Cell overflow. Writing to builder is not possible since after operation there would be more than 1023 bits or 4 references.', + 9: 'Cell underflow. Read from slice primitive tried to read more bits or references than there are.', + 10: 'Dictionary error. Error during manipulation with dictionary (hashmaps).', + 11: 'Most often caused by trying to call get-method whose id wasn\'t found in the code (missing method_id modifier or wrong get-method name specified when trying to call it). In TVM docs its described as "Unknown error, may be thrown by user programs".', + 12: 'Thrown by TVM in situations deemed impossible.', + 13: 'Out of gas error. Thrown by TVM when the remaining gas becomes negative.', + -13: 'Contract was not found in the blockchain.', + -14: 'It means out of gas error, same as 13. Negative, because it cannot be faked', + 32: 'Action list is invalid. Set during action phase if c5 register after execution contains unparsable object.', + -32: '(the same as prev 32) - Method ID not found. Returned by TonLib during an attempt to execute non-existent get method.', + 33: 'Action list is too long.', + 34: 'Action is invalid or not supported. Set during action phase if current action cannot be applied.', + 35: 'Invalid Source address in outbound message.', + 36: 'Invalid Destination address in outbound message.', + 37: 'Not enough TON. Message sends too much TON (or there is not enough TON after deducting fees).', + 38: 'Not enough extra-currencies.', + 40: 'Not enough funds to process a message. This error is thrown when there is only enough gas to cover part of the message, but does not cover it completely.', + 43: 'The maximum number of cells in the library is exceeded or the maximum depth of the Merkle tree is exceeded.' + } + + def __init__(self, code: int): + self.code = code + self.message = self.EXIT_CODES.get(code, 'Unknown exit code') + super().__init__(self.message) diff --git a/TonTools/Enums/Jetton.py b/TonTools/Enums/Jetton.py new file mode 100644 index 0000000..6512494 --- /dev/null +++ b/TonTools/Enums/Jetton.py @@ -0,0 +1,19 @@ +import json +from pathlib import Path + + +file = Path(__file__).parent / 'jettons.json' + + +class _JettonMasterMeta(type): + def __getattr__(cls, item): + with open(file, 'r') as f: + jettons = json.load(f) + if item.upper() in jettons: + return jettons[item.upper()] + else: + raise AttributeError(f"'{cls.__name__}' object has no attribute '{item}'") + + +class JettonMasterAddress(metaclass=_JettonMasterMeta): + pass From 3b4572e2ee66ad7d50db2f01485d5357655e301d Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 08:37:18 +0200 Subject: [PATCH 02/26] Add a list of the 600 most popular jettons --- TonTools/Enums/jettons.json | 611 ++++++++++++++++++++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 TonTools/Enums/jettons.json diff --git a/TonTools/Enums/jettons.json b/TonTools/Enums/jettons.json new file mode 100644 index 0000000..1fe58ad --- /dev/null +++ b/TonTools/Enums/jettons.json @@ -0,0 +1,611 @@ +{ + "USDT": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", + "DUREV": "EQB02DJ0cdUD4iQDRbBv4aYG3htePHBRK1tGeRtCnatescK0", + "OPEN": "EQB0apV-NyCYJDVwSBoDL86Xjp0OiwcyD8jJ0J5BVWnnDJu7", + "REDO": "EQBZ_cafPyDr5KUTs0aNxh0ZTDhkpEZONmLJA2SNGlLm4Cko", + "BURN": "EQDNJzbNKA8Ix2X7Tv1_jxdCqehPQgJaNbisoIkSq5srnfLs", + "ANON": "EQDv-yr41_CZ2urg2gfegVfa44PDPjIK9F-MilEDKDUIhlwZ", + "STON": "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO", + "JUSDT": "EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA", + "GRAM": "EQC47093oX5Xhb0xuk2lCr2RhS8rj-vul61u4W2UH5ORmG_O", + "VIRUS": "EQC7zE2BVcLiybBMuQ1giOPYmO4Ji88J12r7duqMJWOtFxtr", + "UTYA": "EQBaCgUwOoc6gHCNln_oJzb0mVs79YG7wYoavh-o1ItaneLA", + "WALL": "EQDdCha_K-Z97lKl599O0GDAt0py2ZUuons4Wuf85tq6NXIO", + "FISH": "EQATcUc69sGSCCMSadsVUKdGwM1BMKS-HKCWGPk60xZGgwsK", + "TOL": "EQCOKRrOezeZfCgkKHO-PBnheexXjrFHZS-eRO5D_8h5YM5N", + "TGRAM": "EQDRlQ8en7A2zsTuF7SdDOxMlZ_wFw0E7Eow3u9c4pSoe4Tg", + "WNOT": "EQCIXQn940RNcOk6GzSorRSiA9WZC9xUz-6lyhl6Ap6na2sh", + "TONY": "EQBiJd6z0WiuiCQdywPOqaLyT2TbTfV79Pzu9K1R3l_qlUZ5", + "DFC": "EQD26zcd6Cqpz7WyLKVH8x_cD6D7tBrom6hKcycv8L8hV0GP", + "@BTC25": "EQC7rnHHtMVBKyhiGnAbtYIlzGxS0dfi3ZbHExFX0LYi9cAH", + "SCAM": "EQABlNM_bH_Cfc8amF0vujfGCNlKIR0pUAxy0PMchVAdqSwT", + "MEM": "EQAWpz2_G0NKxlG2VvgFbgZGPt8Y1qe0cGj-4Yw5BfmYR5iF", + "UTYAB": "EQAw63ZqLmnwRA77PW07CWIEngwg-eIiysaqZ8IWpUL0nP7a", + "MEH": "EQAVw-6sK7NJepSjgH1gW60lYEkHYzSmK9pHbXstCClDY4BV", + "MRDN": "EQCymLRXp1QYxZKek4CTInckB1ey5TkyAJQpPAlNetiO54Vt", + "SCALE": "EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE", + "TONNEL": "EQDNDv54v_TEU5t26rFykylsdPQsv5nsSZaH_v7JSJPtMitv", + "SOX": "EQBB-EMREJkIHVYG5DPiklOhWPcsCaxjL9HKmgRvuGtz_1lu", + "APC": "EQAzBy-W8GJjUPfU-8eV0Jz56Jc5dIvvP-qznHKQhlqzGkoZ", + "PEPE": "EQBP_hhZ-FPTHngC3nD_AbFGwe7VWVSYofkYr9vbPk1eFwtl", + "CPBR": "EQCTWKZUR3BlJ-jnT_GE8RtERDOMQ1los7-Up3ipQkTi700i", + "PLANE": "EQAX9J60va-0wIDMdqGLRMf7imJvG0Ytyi3Yxnq9y-nbNCq2", + "PUN": "EQAhPcpyfkhv2MPUxYm37g-NjZpBrp-hO-XEpOVkgOLAE2hS", + "CRDF": "EQAdz16aPWCmAYldTiX_g-NRzT-spbN8tCk9dJIHfAiC3L4p", + "BUDDY": "EQDZvDW7Cf33YjMpeVr771PMrYgymGFyibTdcL4y-unuFtmA", + "RAFF": "EQCJbp0kBpPwPoBG-U5C-cWfP_jnksvotGfArPF50Q9Qiv9h", + "CATS": "EQBadq9p12uC1KfSiPCAaoEvhpXPHj7hBWq-mqGntuwE2C1C", + "JVT": "EQC8FoZMlBcZhZ6Pr9sHGyHzkFv9y2B5X9tN61RvucLRzFZz", + "UP": "EQCvaf0JMrv6BOvPpAgee08uQM_uRpUd__fhA7Nm8twzvbE_", + "MORFEY": "EQB64Q2183H9XqsrR9h6tV83AnRlFX86MJWRCMQlsgQU2-yv", + "SAD": "EQDcPhhDuFXqz0Usrhku7x7Ye1CWe-2Wq1PKD_WC9hg2A-Hr", + "SHIP": "EQA1jvPqRx6bujYohDny43hwX8FZr3b6OYnI3t2jHd4TXSJs", + "BEAR": "EQBtv7I3pZpMAUChef2Vrk-U1GcCE00rROuHgMJ9CvjodRZW", + "SQD": "EQCtxbfa9bVXn7wsfK5V72fX1RFuc8kFDF7piMioWENgEBx5", + "WIF": "EQAuco5ZEPgB19fSTo7EmtLTJysrKxbu6M_XOFDwWQiNjCsQ", + "MCQUACK": "EQCLyBgzHYnliNOBJ0xlcbwW_kIUnvlacAMQ_33d-KEXqEqF", + "$RECA": "EQBwHOvf3UrPPJB7jeDHaOT-2vP0QQlDoEDBsgfv5XF75J3j", + "NOTNOT": "EQBFTzLthvrKPgQXfgOhhlhVnStDt733UQzdxHjY65SdDUc3", + "GEMSTON": "EQBX6K9aXVl3nXINCyPPL86C4ONVmQ8vK360u6dykFKXpHCa", + "MEMELAND": "EQBaK9EduDjr0WHEB6NJ3JCOb_ITnX09ek5YT1-73toagYCp", + "TWIF": "EQDV3cbziPHz8wEcDYt-9iDOomlc3bFZQSx0WiZFCv7fh7oX", + "$REUTYA": "EQBNQgoEL-vrCmYhHusn-gTs0tRBxOP22WbqB6WnBg4zJWll", + "TPET": "EQAmQGimKRrSHDLllvdUdeDsX1CszGy_SPgNNN8wE2ihIwnP", + "JTOWER": "EQC47YfVLWo-U_z7s6iEOkYAAEKF_C0-gBiB8KJL6s5m4JOP", + "REKT": "EQDQEsrlf5ALa7-Gpz2nDK6t332bcyQoP0BzHj2KKTrXYVYB", + "TONK": "EQDv68b3JaWBO_DrP6u13Oq7KsnKhR926kMxcOJwACDY_uQC", + "GLINT": "EQCBdxpECfEPH2wUxi1a6QiOkSf-5qDjUWqLCUuKtD-GLINT", + "JETTON": "EQAQXlWJvGbbFfE8F3oS8s87lIgdovS455IsWFaRdmJetTon", + "BUFFY": "EQBxbQytL64wuTz51KcpOLdTDSOhqrV1LaPKDrhOqLB3v4qj", + "POTATON": "EQBdysDY0Is_AGNtYMa72nTGl_-_tITOKuoZE4R1TyYfa6py", + "OTON": "EQB-Ci-nfKVZAQSNpiTVOWC9UAaJ_Q6-vS-N93f01tParHvW", + "CHAPA": "EQBenWCdMBAILriH8yPX9bfmjcZlmer1JgRDHDWblDK13TOp", + "AYTU": "EQAMcwJgCTL1oLIwaWHC02ijNz39MN4t8ryYgygpnrYzgEby", + "FNZ": "EQDCJL0iQHofcBBvFBHdVG233Ri2V4kCNFgfRT-gqAd3Oc86", + "STTON": "EQDNhy-nxYFgUqzfUzImBEP67JqsyMIcyk2S5_RwNNEYku0k", + "ROSE": "EQBdr5b7csZbXhDnQ5U364bpLylHfk8AWcug8TwhzCMVcvPA", + "TINU": "EQA6Q3dMgVEfXQ9tNBL2fMljEhI_azQ-R3vvPgjGOXwoF7kt", + "ARBUZ": "EQAM2KWDp9lN0YvxvfSbI0ryjBXwM70rakpNIHbuETatRWA1", + "TSTON": "EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav", + "BOLT": "EQD0vdSA_NedR9uvbgN9EikRX-suesDxGeFg69XQMavfLqIw", + "DYOR": "EQCQZpelevHNsbw5IUtwSa4Cs8kqWww0KsYeDri9kwS18eCz", + "CRYPTON": "EQD36Lxp6p4FMzHQThWzvFNaqhbT8Qip4rMF1NYjCSgY6ksE", + "DEANON": "EQBTUH6cdozp2TWujAqamNjgmCrXcV7FraQ6KieDu7pYsMjm", + "DEPOSIT": "EQBc-L5CzzRKmR3-TdOwBm4l-XyJdxJt3HfAIr_qMiKaB7WM", + "JULIA": "EQDNLrrwm7llKkce0YArDb53jeEP3rRRnJVpvDKA1675T2ft", + "FLAME": "EQBoFT-EFTvn5vD2h3zkfczkhMUI70tJJjC6mp54ACemfZ9y", + "DOGE": "EQBphnWv7EkQvjIkGPjMNacYtXo2dEqFIybPTf8IxyBGjNSz", + "COFE": "EQA9dayEKflrL-wIf-GKGizj26pvX0QCIxwmRgqzg5U_c3YB", + "GREENE ": "EQDxWug7nXqaNd6rfkl8xRm-67qYr5nr8UuVnv9mg9_BSjII", + "DUCK": "EQBTcytZjdZwFLj6TO7CeGvn4aMmqXvbX-nODiApbd011gT3", + "PIZZA": "EQDoqIKkOevVZukmf1L8nGhcjlha4sfvgl8GIRpTj_b53HyR", + "PUNK": "EQCdpz6QhJtDtm2s9-krV2ygl45Pwl-KJJCV1-XrP-Xuuxoq", + "JUSDC": "EQB-MPwrd1G6WKNkLz_VnV6WqBDd142KMQv-g1O-8QUA3728", + "MELLSTROY": "EQCoU3rclK7T7rwCVzBl1QnziTvr45jaKZKfnebTC7qAxhoA", + "REDO2": "EQAWS2XkrSS1mbe63rPDkIlLHQaak6MXs3EWK8lRcdDMXjsk", + "TOGE": "EQAR7L4ntuBUMY0KYAApsqRLD5CePfY0gtM14l6hOLvneBel", + "KINGY": "EQC-tdRjjoYMz3MXKW4pj95bNZgvRyWwZ23Jix3ph7guvHxJ", + "PWJ": "EQB8T1ylyks0DFFkUUC2KeFlzXMm-xDjvv6wDDRXLr6jTPRz", + "REDI": "EQBEbRL29o9A6UbD1nWimXuB-WirO_y0eXceaq9TaR0q5xNW", + "JNANO": "EQCOGp1GAVk51prVBw5DO8QN3lKIlUJKjXbZcl5aXgL5hkwy", + "WEB3": "EQBtcL4JA-PdPiUkB8utHcqdaftmUSTqdL8Z1EeXePLti_nK", + "LAVE": "EQBl3gg6AAdjgjO2ZoNU5Q5EzUIl8XMNZrix8Z5dJmkHUfxI", + "RECO": "EQBjbM7x9cqPCqFCY17FSQr2MRoZGzn9eG5VAeQ7vsqbXPhX", + "SPOTTY": "EQC24zbmyONM-m3DeCCYQpDRiinGjOu2cCHevwUFGWvdotAQ", + "STARS": "EQC35pGY9erPOnN75wZpfpOyU5gwUnmLvUEE61oObpkoilgq", + "DUBAI": "EQBhRoPmOImcU2e3CLKD6eCZ_PfOz2iYFcOY3139HUUx0phP", + "JDOGE": "EQCFWfg1ELLRkNQ1VgxCEOYKqLqxAuNJTrUXFXgkag7D2ssH", + "NOVA": "EQCPIijcaVHuuaPLtuCtiD0mwueZG73beHJ5vdV_l9_vzv5_", + "GAGIK": "EQCYDxYwnbpU5zw0Ult8CQm63R5pA1bYv5yl7fpufm8IKTAu", + "REDU": "EQD9BsupQckDFRLnc1ixVnW-wbpBTVUg3smDK9mkRQ7WPc8d", + "GOY": "EQAmXCLQN6PcgKMhK91_bAPqbSvyiCw4eZRAIStkSi7fUaRA", + "REGI": "EQCcYUWKhjGxwqtk_je9LW2V4RoFmbhOUR9e404Y6FQTEXHh", + "TONUFC": "EQDjsJcteub3Wf3oEyo6m90QtK-oaHlCz2881v8t4IGi26iy", + "MC": "EQCbKMTmEAdSnzsK85LOpaDkDH3HjujEbTePMSeirvEaNq-U", + "ROLLSROYS": "EQBWvWOLrs4OTwiGReDHmbK_9Wj6SFQYe2AT-_yW00MNudjM", + "ANTI-VIRUS": "EQBol7eaAA4ZpI01_pz6jBdtIQlFXDmPr8A95ITHiPcjqgYe", + "FLAM": "EQB1JWp5YwtdMZlGwMp9N_VygWYRj1mDmDdp91Sm84-AJWI_", + "FERRARI": "EQCq_wkxXr65O4GKdUSTxBWJkkcz7PmJ1kJyIRDJ1QhquYEV", + "TOP G": "EQDMeq59KbgJHJ_jKKxxekCCe5Zrnh49eEjpHjDKX7Tc2pfd", + "A4": "EQAyZyER-RJK68IoQnoMlcTud4LkXtIiJ9yRuD_mrNnSkSz7", + "HTON": "EQDPdq8xjAhytYqfGSX8KcFWIReCufsB9Wdg0pLlYSO_h76w", + "GM": "EQB9e2RJaJnX_tALDBSyIb5oj0YHJLa8FncsF-5MjXolbUzC", + "UFC": "EQAB8r7n5VIEh_pNtsDlze8z3z0qzhyXDPP2ickQqPy4Y4Rp", + "DEGEN": "EQBoC1lTWYcHx2Pc7H20Dm4ziGk6dFx7quKTJpLnFj7YJSDF", + "LAMBOTON": "EQAjDbOS2w4b0mVVIRIGp7Yy-eK_-YTLcMR3YPjLhY_ylDY2", + "WHISK": "EQCObh4-ZaghOva8uCz_AMMnvifM3PS-EppUJEJQXXnlP0zX", + "YO": "EQCDkrpCu-FFVPQJnManjlf8XCN75wjX_AKrrEFFQ1mfJqZo", + "REPE": "EQDVLPdRZZxZhXI09ZPuTEUm_Q4BZqxj0aZdRPjlpxLLpNV6", + "TJERRY": "EQCN_4ci9LaYuZH9Q6gIzEC-Gc0XaD_M0lKfDT6BXrXTTPhF", + "TEPE": "EQDN11TTPTxw_xSPJs_zyrzIRml4JXDlppAmYzJq--tmpA6V", + "PYTON": "EQA4IIc8YZE-f2lV20nMZ82ZoZrAfWX2wW8Lw6GOmZA6xgOP", + "MARS": "EQAL6e1UNPFksn8198qOD6KICnplw6f9cMIFuQW3xV9ld3Ro", + "BREDO": "EQCMUI-C7CQjVQanwD5Nrws0o06SKVMjHbLPNw8PXW6t01db", + "STONKS": "EQBng_Ux8DLKeaLZ4kN4eec-U-SJ1fMtYvqQANtL0oxKRQh_", + "TONG": "EQC0KYVZpwR-dTkPwVRqagH2D31he931R7oUbPIBo_77F97K", + "BABYTON": "EQAwq1fQaK21qKFc6WqQ-5IYujhVZwcg4PVwn7_8qJ6Go7fn", + "TONKER": "EQCLg3pVJG8xLy3vDlNuYXkd7LSrgqh41Y1BUnEj4Mgi2IeK", + "$ODER": "EQAyEjcjLF24H2TTrey1Yed3uRs18xbqkFCU3uzphHHr8n3y", + "TFLOKI": "EQD4Vj5a5obPYY2QfgQMfZ0qi3GPlSRxBu_n6qIBJWUmrPZ8", + "LKY": "EQCyDhcASwIm8-eVVTzMEESYAeQ7ettasfuGXUG1tkDwVJbc", + "TPEPE": "EQB9IOqPrqpLu0rCuznZaOUMD5Ms1Hy8jfSj9Ni0fPd5O0Yx", + "GCH": "EQDlamPvKaYOw4DzW74h7RMBGg4YKoUBRNleibpGSffpjjJf", + "STORM": "EQA-jkI1whcZtxGX6l9LDfhIyveacYNeY2v3cBDz8XIytId4", + "UNIC": "EQB2lbt2s_jdUW3OoUIb7-9RoK2UeGXmgC7BnOwgK6P_JTrm", + "PENG": "EQCvbAgRX7iAfNZIZ4r16j-v46KQThh1HdkjZZMltjIWk55z", + "TONALD": "EQCX_vSR8q0OSN74_BSq_ZieLZuNGx7wt0sXq1t5EI0q2SRI", + "ROTGAR": "EQB8RNC7E-0uWcfSP68rk_RReN2JYKQ4nBLq1RsKpaVme0xx", + "CHERRY": "EQDwEJ57L-L70XfiFo7YY_uaam8vMdQEgftxrWeD2Ite1UnX", + "UTN": "EQAPKqRFnQc-2m5Ogg0UUMNM0cZRdK4JUR2gN6wk8PX90_Wf", + "BITCOIN": "EQDAnNNumieW2fZtrXKD0wlSojb5CCmM3rIsJfmWzO5O_t5F", + "MAGIC": "EQDhbM11gCkwVSLu11jTPUGQKXPjfW_J1psPqc8mwZwM7Imf", + "DUROV": "EQB22u_EPF5DyqtZRhjJ2YiZliN-w5ZCq6F_PY6Ia6bvi3Et", + "ICTN": "EQCtnNdPnA4NTt4XQ_M21dKyoUR7da_SX7uqtO-wh1HeqsX4", + "STBL": "EQD5ty5IxV3HECEY1bbbdd7rNNY-ZcA-pAIGQXyyRZRED9v3", + "NANO": "EQDQ2oBzTO4Itn9mmu3TL1v5TUiPS9PuAIVEq1xMpLHeeEsI", + "OR": "EQCJLFpI0QR1QA_CYQ77Zm1_n85RDM4yLS46voZGRWbu9sbX", + "BALU": "EQC8_F0WqQt_RKemE7CFOdo7LOA6BzOrdHwl0Wi45kLFMS3P", + "PITON": "EQCykFK-EgVtKZ2ijq4keIAGAigz9WIyHR1iJEwuZxwHXK5p", + "STXP": "EQAORkhhrXis_XxLAAfyxTyNceNXLtaHcJ5FhXxpNJ8xsDGc", + "KNIFE": "EQAfsLwidEMcdueID6ljb_vwwzR7MjtW7giSA5EQcNJ-cloL", + "BROD": "EQAtnW6DR1zk3z7FgXG7fo3m0riDpNpNOmb56VL4r4NjZyb4", + "TONSK": "EQDP6YYQ_dX7Ah0i5zoVqnBwqUT_StB6OeViWptKxhkzc1Ry", + "GDZ": "EQCiz7h1Y1fmxPOTnvPQm-DjGFX21Uiuqzy2gVQoCi6_JWWn", + "CONDO": "EQDBf1nelephV3FIE64sDMmG4FC3NghpYeHVloc5R3WAiyyL", + "RESHIB": "EQAG3p6kmQtvV71FBOh_xFoH-AmJJXGGI0LjmD24MEXhpSbs", + "CROCO": "EQDDlumgZAxqChDXpcbbGm0A4Q7fKmbXo_Wom-by2imiA-iY", + "HYDRA": "EQD4P32U10snNoIavoq6cYPTQR82ewAjO20epigrWRAup54_", + "SNOOP": "EQDSYg2es_L0xpGfPJ6k39SxGA6MCHwV0EHY2smeF7aFAJW4", + "FROODOO": "EQA7bfaTs0S8dX7_oaRpA5Kf1_x0SkyOChFikTX4A7_m_Lt5", + "TORK": "EQDQMxKXCEWs4ILJltwZKXyeBnqr79evv0ROARFi2NapQthS", + "CMUSICAI": "EQD1GKQXeFy-jPjDO3SG6MG7ziSZ0raw5ozQeuw7E-Jfqsxl", + "TON7DAO": "EQA7nLJI-Eoc5oBzEBnlLzezTEwGNRvyR5tX7iWHT8KPwVy1", + "MOG": "EQBau7pTrSoH8ZFU4TBpGqv0uer5LWO-QyKrzLSqSpjnOnv_", + "HAKI": "EQDjYih3EzJMI1Vh0AtJ5qWo_jTqadPQggRD33-xLgxBKBLf", + "KITTENWIFHAT": "EQD5KvuWp5KBYWt66thxlgcyglBXs_vwFwnK0nf2Pin1WYS_", + "PRCL": "EQBM3nd-blm5uGeXQSh-wmUyf85LphVkgYTaor7BmgdGAt7f", + "NOTFANV": "EQCJcHW_dEa25uLKKR32Jzu71sd6qke8JmM7q-tAlpWUQlD6", + "POT": "EQDoziSttQZ96QJRxHRKdatCm5L9DRL9LKycXcjl8RPXZM0N", + "TOOTON": "EQBlbt6alXu_4P5XHSK1ytcxziifjer4QhptfGeH4JoELi3D", + "TONARDIO": "EQA3JpijO4IRLP2XU4P8AT0KmvbQcp8CV9sIBWyBsyEi-sqr", + "1RUSD": "EQBT2Ee4Lx5w9uI7oMly5upXNs3ABWL_Fwk1uiM74gZCGaYt", + "MONKU": "EQD3-rcY1mvICEkCtBXYQaZjyOJMIqJUkY48w7V_qdCZvHT0", + "MEOWGANGS": "EQAL7qrbCBScO8HMb5Gd6DiVz7YUtfRJssFmirUnQodV6W7J", + "CATCHILIZ": "EQCJ3LoMfU-xRCtHoLaJYoGY3ZrMKfPfgPjn8Dp5tFbRjmL8", + "BUTYA": "EQA0xfI9aKs6OUPtDmmMDmwgeRbChWPKBXjii5oO18edfKeU", + "DGN": "EQAaJybAsdcXbeW_PsrQZElZWE4x8mLkL9s58kIc0OtpCWFO", + "$UTYAS": "EQBJHcIfGhDVWItH2CJ4NBVNmMgMjUdLSoN-P8u0EyafKn8d", + "SAFET": "EQBgLw3RLkELgOJUU1oHtHzwkoTdb4RgfT8IgTG6k_iLneEc", + "PYXH": "EQDNSwAYjVpdKLlrMcDSSsPPdSRUujNmJ30U9vtLuhyey7CU", + "OLX": "EQBur3GRavS07NaIVysuG4mldgkGQkNeCQxuduJbxK_o2QD3", + "GREENCAT": "EQACw-oEVl2cppFxCD3J72uY62a7GZN5HOd3zYqqIox385zY", + "MUFFIN": "EQC2MMtE2jLOVDv6gn_-iLmIcnEwQUQ0pooe_1BEAGXi_Ir4", + "VK": "EQAeIWInq0ymjA83kV1RcYi_FilcXv-1Jw9qU-xkGYUsJTtN", + "$AKITA": "EQD6ikvSPUcpE6HB_OjxCMVkFwrtK73CVV1VLR6NWEykojuQ", + "MF": "EQD1fHR0E9crSTPO_iDFHvcowvCq7Q1Y_zMoogIFzwIdMgRb", + "MIBIBI": "EQBk1V2tcyzmwR7llU1INeFv_HXqNRtDMKoxgvkuTjxRvPwN", + "PET": "EQBJOJ2eL_CUFT_0r9meoqjKUwRttC_-NUJyvWQxszVWe1WY", + "OCKYTON": "EQD7b3kAyqRJlHVcgN8aXQMB_f5L4NvNZjE-9_bZLCt7iMr_", + "RATWIFHAT": "EQDyuCtqF_kc6J83QkGh0dte3JU2NhXECOva_z8ydfVmu-Ce", + "TRIBE": "EQCcXKwYGCCZnzQj9vwwg8Y-F3d8H7cow-Mwgj8pTFruBfP8", + "CATNABIS": "EQBwII7Fs9RB35-k3F8fAv2B3U15oaIAeZlEyfiu_5D7pq_h", + "REMYS": "EQALDLkHEK2_Fwipglges1CXYzZ7t_2lRaYYWdw_gbzQhJYZ", + "TBC": "EQAEUgDwIuuM-pm6Uqmmz7CW5I7E36brVwFXgqrRSTb8SK0O", + "NOTFAN": "EQCyNU1cfEoDKfMaP8haX5Bha7H6z90wa65Tnv_mT8FbcZYd", + "TMZFK": "EQAqFXPjWq0F5BcyL2WVVY2xGN7SRw6_D0YZypSf1ChZEH51", + "PACMOON": "EQAbpZ_b7r7OQGsbRGJ-hY2ZcxnPuXssErQIvfdr0V79SYvQ", + "DUK": "EQCwfT17azp6POMOgc8MpfvezCnSJxVS-ps0aQ0zxhYg78sW", + "BODTON": "EQAjg5zfygxjiw-uqgfhdE6N-jbCb_QiqRFnXh1HBBjFwDsC", + "INFT": "EQAEfUNvB01k3khyyMJeQu6Y609TOPtm_-Mn0-12NJb4SXwR", + "DEER": "EQA79IISXMR8ve9AOipUE3i5htp6z6I7ynSC9DKBo8zHsIYr", + "CATMEOW": "EQAwpRVELvk6R4LBQETdj-XfHrWXd3PlzhB4GvXpUZRr1qIV", + "AIR": "EQDQ7GzHGNgfR5rcAo9RzIP2BRUPjKiF7vurI5XxId_Wzvr6", + "APE": "EQBEBb0OjyNx-V3YY0gsrBKnFs_LHYAaqCGN27_v_QadZAU_", + "KILO": "EQBWloHTmMp0sfprWYySPYjjAIwHs2A6w-s2W92DnBtKLpW1", + "JWBTC": "EQDcBkGHmC4pTf34x3Gm05XvepO5w60DNxZ-XT4I6-UGG5L5", + "SKIMASK": "EQCgeYh47bRJUP0BcqBTLTH4gY3qb2h7H_Afaz0rt8jyujl8", + "BULL": "EQAMZXImRSd7NlzgnmQgoGhgA8IRN61HWnDun-n0K--1joS1", + "$FOMO": "EQB5sPQ3S_SWVqg1duCFJzbP6-rNG3yka3zzo7R5f4jgvaOp", + "PUNDOG": "EQBKVGs-k-XxKzr0Pp1cd84AsWXVuwI7k73jblrZvJ52TEWD", + "$TONY": "EQB9rFI92eYlakDHzgn9bYwhqwGTRejWZ-78QDx5ASZlk6Ha", + "CRASH": "EQAgOBcuRpqw59Kn3hkPBAg6px8GrxnH3o-uv2662pZh_tC_", + "WARPAKA": "EQBnhFgf4ILMOmi7vlp151-Wpr6gBeP57cc_w8LQ3r-w1rHI", + "SPOON": "EQA1tq6y7xaJHxB9aqhzVT_KwpPU6ep9yKFQPXv2O5gOJWh9", + "CURRY": "EQC-gWch1SJFD7NGTtd5ol0KW_-kQqvHwOuJWbm3-5GiqLi7", + "SAU": "EQAty7cyLIjF7U-K0VgG_gcksTAKtILkbJCNjPYlZ85cCZAA", + "TMONK": "EQBikuxOnMkU4XNSVGQCbg46lk5d8vuhzDQtcDOJOcjaIvkV", + "FRANKENCOIN": "EQAopTbS1F-tctVRIOmqi8VwIKEBWpXJYiR9ISJij6vef5Fr", + "OGGY": "EQAmY_ewCVsF9QCaUsv515zPmFbY8_0aSguLqiSxX3qsFRVs", + "BOOP": "EQDVvrW1NtIrTPwJEtlURGT_6S6pjvlJUFllE3dgocj1oe8k", + "KART": "EQDBjLU2-F5CBs8lIy3Rax1A-KXbSh1SBQObOJx1IZoAqxwZ", + "ZOINK": "EQBWDhA6agz-wJ5hA4tLS-94XTYFhMBPjXXFltE4V10GtEGQ", + "DEADINSIDE": "EQBJY5jz2UWKC8XzxIlluoJMcP7WEmIZmLFl4QCaJICJXoKz", + "SHIELD": "EQD9q1hEec1fTJGUCCMXCFWkyEwiioM7gOOd5mqfp-Gznp_Y", + "HOPPY ": "EQCfcQSyC-jYE5HCQbNHFfFVYjTofGJP2PvIbbEd4YIx3Z3R", + "HOPPY": "EQCq_6QCblrgu-fhosJZNqT8TK8qubyYHJ1bijIcybdpCI1_", + "GRT": "EQDSVLCSFIcDPj1j5f0wTPAqP6beGhR3D0YZ1L61c00ahaV3", + "HMSTR": "EQBam5RuB3inYXsUlamTIEqu-tNy5NmX4FBLAcZe_360eWWE", + "1PTON": "EQB64g1YNILqmPFdjG1_uxWqCEP0VY141Y2pP0L1YDBdTKQ3", + "BONNY": "EQCyZW0FpOBxswcJMPVz6mw0BHwXztz0qF2XXlM73c2VtQBO", + "RCAT": "EQAHBPL5Q51kQWMtItT3buuaDz0s_Bq4kN174-RKtlqOu07o", + "KOMPUTAI": "EQAsoD1vs7LbS25nUwPJ9Ua7vQU-St1RYHBmEThGDZs0TBIe", + "$PUPSIK": "EQC0sl23S16hOmxlnR8cdVycXPjo7U2-xeSpfxaABffGaq1t", + "FREE": "EQBhj9Fwtr0ZwftDF13f6d1aqh5Cegdqit8BCTwbjL71KN3Z", + "DODU": "EQDYwXKOLLRIiymZ9FoZoxGg03bewUUNPpoYNe3OqFzNY2N9", + "NIR": "EQDt7h_UJRqE-ghhZY0AjIjK41twXkKUkx3dMU3wTQU4nV5J", + "TGR": "EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y", + "DOME": "EQCw7I8af8An8t6grKxZsuSmArgOUTgqoow81u1S1GC1cpNT", + "TREMP": "EQAQA8M0qdRmSrst4wLW-XXSKTxFWkFX4XjfTiYWslZ9oIN4", + "CHEESE": "EQAl4Kynas-R7PlB42gjJTQeBaA6AGJa8oJKyO4eqRRrpCi9", + "CHEESE ": "EQDcwcuuSGQ7kpWt7rGzQpgxLon0JGTCGqw4i4sxZRRT2YKo", + "PROTON": "EQDgSR_-4FDlXPfMVefXX1IIXMWBH4YfYV1a_8cJ0XsGVdsf", + "UTYAW": "EQDuud_00ZYQ2H2LAgjyxcTm18h5pGSuOTHCaT-lwXuo78jw", + "BFISH": "EQBC7NiIh-33rZre-HivXxOn_Km57iQJmArmiFjOkRKz2mPX", + "JMNT": "EQAEuikLQVh2lDMrV99nTHqFL_TXEyCEJ1xKMuPT60tfvdps", + "MONOSWAP": "EQBrX5zvAbx1-4ZnO9MmTGuLSO3SryEeFYLsG_qsJ9mkn3aL", + "PTJ": "EQAgotSkX06MIW-A0ni5yKqeNlwc3nASnbO1dwGo-kwpg2Zg", + "TEKA": "EQD1m5faBsk-58Tpd_SqbN7gWnoVlZG1inOl5oPpooyQM7yf", + "5000": "EQBovwxoPAIMMPlLgBM6g4Mx9Ga520wx6fCsc5RocDsGfRga", + "GOSLING": "EQBXP-cvi8iocxZ4mooU_RzT6P7znZoQ1BDGCxpuEvrczL95", + "TNX": "EQB-ajMyi5-WKIgOHnbOGApfckUGbl6tDk3Qt8PKmb-xLAvp", + "USDT2049": "EQDt-dqszxdw9lhuq7zlVrqLdNQgB-XkRtfN_Z93AfSss-Ts", + "PEEPO": "EQC25c9UJ09-Cm3hkdYRxvOjcLDgH3aOc3BpinAR6C6QwT2s", + "CEDO": "EQC-qaq_BiZ-zaDck-el_-BXstcLY1mdwh4XJcREJsnk9ITO", + "HEDGE": "EQBiJ8dSbp3_YAb_KuC64zCrFqQTsFbUee5tbzr5el_HEDGE", + "GLOOM": "EQCAIoiLs-NIWK4TazyyYkpAJZEeh12cX6ycTnAAVNSusLwA", + "DREYK": "EQBX8_mURB8f7H2C4lYANcESa55S2rRVAgjnFxY42CBzfd45", + "BTN": "EQCr_RCv1-npD4nOHOvRzmqZLWVCkVhyMzMp5M11aa1QPfOE", + "SLOW": "EQDK3ReFkb6L3uE3GmMDJaxWby4PA_UYGClgFfJuXY52-TOK", + "1RUS": "EQB5Wjo7yXdaB70yBoN2YEv8iVPjAdMObf_Dq40ELLaPllNb", + "PLANKTON": "EQATgD7dao9jWBdz_qKL-UJZCXZGQvV5HJVUC85ssgmHKWll", + "NUDES": "EQBygwrEcGIj5zmEtWeMGsQXbXr1WBgV3coVfqyURQighw2Y", + "MUMBA": "EQCXIHAWDAJEJhMiNVqeUvp3I6O7y_P2sD80zEsJXeUi56mb", + "RABBIT": "EQCJGWFbFgVEkYiQqgu0-GNr3ol-QtBJ-ttDgT2dv8MK5X9O", + "LOLA": "EQB96gKbAqwk5v44QSys4q45D2FlK1AUCMPnRmJQa-D1Tx_V", + "KAREN": "EQBsI84SX5z0tY8UGMxa27g-bfQI6dg7lvsiDJgUsW1EI-Ns", + "MONKEYS": "EQBH7moKCqK0qIFQcwSRnSscmNW4-O89G5Jjd2zJJOz8WGKg", + "HABIBI": "EQA_Wlt-e9AOtuEV3dkyfWE3_iszgS8NDXWP6fCVOS6qGr7V", + "BALL": "EQDgSZ1oMr9IStlX66Dn4kEyp_6VqX5acUs05p3vnhPAD92f", + "BRUSTON": "EQCTu3DTINFaUnubfnp8AAm4Qa7oqElhYh0yVL9h8zN6S9BH", + "SBABET": "EQBuA-_rhkzLmOktImllkrbniTh-jT4EHwS9ujv_rWvBl1Dp", + "SMERF": "EQCmqWE1wIkWyfdKoF41xS_biqNzemeKkSPUuhloXs9R8TN7", + "CFROG": "EQCFSDiwoHbMVAf0P3S8fcpBkCadb3OQetM44FNQkYXlXUqa", + "WHALE": "EQCCVXF9QEQMtxJr16zCnt0Ema-JMU7jZsO1Xw__GNeGWjqN", + "NFTON": "EQBaOSt5XIqPJjJuHfiK1ZZ7VMpE6VZhwJodY_qnK9qIRAUh", + "SPCAT": "EQBCA_XGgysdxxw1ha5CYmxb3N5nxG6WQsHL6_mTXJ3lKUgh", + "BUTTON": "EQBsmj8qiuzGCXITBesA2QV8S1E2yXhd-7_hRK-O0E2zXStG", + "FREDO": "EQCgEoaj0XrKSJs0wKdTBJPsoy2bwYweTd43BKz-_skpbSxK", + "PUMPREZ": "EQABD0B_3DIxF4UhoKzcUeBs9vrBvARu1co_xcAKsAtW1tJT", + "LOWER": "EQAa28vLf89drYiOHabQ56oaCMAftDfOJ13j2sVVul653i45", + "PEOPLE": "EQA0KVd0qQ-fMsnvFU7f2Hg-wlRIq1V2AUxdwSDuiP0g4eJP", + "DGT": "EQBdORhSxB3p86_pcwaHGyoG_J46HryQ9EQmoWGtcyZ9d70b", + "INTRO": "EQABpC7I8u8KKt59FB8iAtndExQJiNqGajJ_kIsKIt8g8tYA", + "DHD": "EQBCFwW8uFUh-amdRmNY9NyeDEaeDYXd9ggJGsicpqVcHq7B", + "LEBONG": "EQCqMfLYXN8d1GAfHqQqfIiWrca8bQAK5vglV-ZLViS8n5BI", + "RAS": "EQCpWgLHTNLOskjr_4ohNpfVWtf7zGh43dLe8BsJMNtoUqFy", + "HOLE": "EQB6Lh9X8WHKbXtskW6783zgBTU1qQ2owbB0O7Glee4iPB02", + "CICADA": "EQDZsdAPpqKvFOWm_E_9m-K9mXhOgF4eE_ln6VYFAmMTrhgH", + "SHIBT": "EQBFwbvCKCUlCgND1aAV6i-6m19nQJ0Wol-gyzUw1K-vxE_b", + "SATOSHI": "EQAG5p1lEYSHvLan7x3JHk2fVCSYHMWE2VsY71TmFr2FqkP9", + "GUMMY": "EQBg81KiagwQQgAs6yFL8lA4BICesXyPyebE9fP47wpOCw3v", + "HULVED": "EQCiSsYB-oqLg_z7-eq308UL9qhIsTnYVEAzRSCyhreJgU0_", + "DTONZ": "EQAwbRb0yT0LhVt478f_zRLwHcuCU4neJx8cU4eKi8_BHHRd", + "AMBR": "EQCcLAW537KnRg_aSPrnQJoyYjOZkzqYp6FVmRUvN1crSazV", + "TAC": "EQDYIz3EJa1GPp78qEvG0ZjFJoSsBbSo2i-n00lb-h5D8HxT", + "MAC": "EQDsnxrNQ4Hn73KE_lEA--Orcbccis9Nr_zqyPLPGXpbuKS_", + "ANDREW TATE": "EQBH6xk3T84ZhFWCWtC9bdoNUHEv-W_ruc6D4n2HLRJsL5Oj", + "OCTO": "EQCIQKCnkpoX05jx3uch3QfkicVycCWN4kAXGLYivaFk_oBV", + "PTON": "EQCM3B12QK1e4yZSf8GtBRT0aLMNyEsBc_DhVfRRtOEffLez", + "BARO": "EQBiHSLVzoicc-LQPDXJc_rF-WRft0aFihccFduA6fQ1ynYM", + "MEOW": "EQC_kG29HfgpJRW2RYIApeQI9PPYdzdsleqiarfo00MvSTSs", + "KELDOR": "EQAKntFlcBvTz1sQDP7h-Er_KfZDWymooG_rVGy_7DEVcU1T", + "KODUCK": "EQCMP0c2TcJgT2JDlMfSav-tc-qefCvzVE2HSEKnux6zK7dA", + "BOP": "EQDllhz1cBTycjWQgVx7UiOVEYp3z21XUzk3yNHcqSOq2f1M", + "KENTO": "EQDmKnwmB7Ex6HxqJ8xaZz0rQIWeFYfISkB-Olg9bNJJ0hJX", + "OORBS": "EQAwr5lcbQcLKTAg_SQ-dpKWNQZpO1MGnrAs53bf1gkKTVHx", + "$CARE": "EQDnt1tFx_wf1xlOCZwTd8PsSsABcCof6p4L6cdl4f3YmuWu", + "ROOSTY": "EQCnhewfpFblzaE-pQpXxHSRgNz7R_YrF1YoAS8HFpwfH6XI", + "$TDOG": "EQAY5OCSHfQNRg-soBjHM0kDP2u2bcW975oTtzTpAFdlikIS", + "BRAD": "EQBLhoan0aTiLK6JTys13c8ZM1qjqpDFOs3UpYQ0B-Kdt3J6", + "KARTON": "EQCfIs0D-Yidis85a2VCC4NTMeg5ek_ycKCc1UjyHpEvpdap", + "VWS": "EQBfX9KO5yIFprHWPpJp3OsX-6cjLjEJF-h5uIQE3eLJY8_h", + "FURBEZ": "EQDbq_HreD2_xnJ5DFgNd6KjG02fTgqevWCyjdyiGJj8gjdX", + "DPUP": "EQCSfqw44KW4BZn3wsyPUBYaCGjK33ijqh_Z8hsvxvbngUKo", + "CODE": "EQDJcZIJXg8xk_i8wN0QAShSXedqFvgWzuL4i50l1YrWN9GN", + "DIGIPOLIS": "EQDcy5kXe7nVBQclGVhthghNStsuskyt3j5tPfAPXDzovVS9", + "SHARE": "EQBwOEkYIrfiTbZ4ohITrLxmMvJqOtCg0QKud-IqMw1P23la", + "BBC": "EQBN2iDBrdAVRXz95RJbcftDCYZYDZmiWB4cbyZDj8h7Xn6c", + "AICAT": "EQBKpN7jWUjzzV5nxgBDI2VTC1XPz1BtJ2xGdQLt20-u7wX_", + "PUTON": "EQBi3G-0zK5GIU9iS_1we5EKJ1x2gHG6nliyEB3K_TYgl10T", + "$NEWTON": "EQC6UCcf__t1-CyO1jxs_6WaOOedu6_Emrb1X9k5n6i6pWiS", + "TON DOG WIF HAT": "EQC44dSqcQdIY2CfsoyniYxIEaba_9w-TOaxK1hziPIdkTxp", + "GOLDFISH": "EQDhnJqZzfnOdDzoaeXzJPeVFwK6AmFYl8XGgnMUG56SKxFi", + "RUS": "EQCvkZyuAAzcQEn2RVGxaCV2ypRDin8PLwZ3Pa480jTeHxV4", + "$DUBAI": "EQD1Fxy8f2wqVIhoZ1FGyhJFs4Ms2E_4KgQ4OjxGRQvPOBkB", + "SQRL": "EQAu8oDM_dXMgQd1COjRnU6sYhmvm5RP32gyyL9R2Rv-8s70", + "SHIV": "EQCcHkucQmtIwKivdWAli7uPguFdPW8qS00lqTIWLBGn9rNT", + "$TPEPE": "EQC6rIVtSn-MtL4mZmkAc0cDkIJma2LSzFXpEowWXTQXKJdq", + "FLUBE": "EQBIIgefUOXhuR6wbUsWce547QVBvP2HIG9P-N0pJsBq3Ugp", + "$TWIF": "EQAybUqG7IFXEIWWDsu2FCtxuZKmS19SpRfo2tVWhvGaMsWt", + "HARIBO": "EQCvW19GXx2aNPD8a50Y5Q1ySlXgYnFUhKqIt4ywLlKt08MY", + "MEMEFUND": "EQAATKSsVhgLZzz-qDI2J9XmQSJ0xuWRfRfmZ2wJ5HyaGdGw", + "LAIKA": "EQC4vUhdAJ9_pk7sLZERGn0vT_vrS_fyAVfWyQ34D6Fk4Ncw", + "RCS": "EQBcDIBsG38XJsPxd4-kdCbhvNwb45ahsgl8OnsUK2kmH2Rt", + "PAPA": "EQCYlvFpi04AFebeF0x4KayKioExdvP23iwtTJ45GaaTTEbP", + "MARGA": "EQBjEw-SOe8yV2kIbGVZGrsPpLTaaoAOE87CGXI2ca4XdzXA", + "COMMIE": "EQB9HV7LnIfyNIpJgczT52TUylBPq68MWA4cCorTIItOPBEG", + "ZBCHU": "EQAH9R5e-abXjn5rKqVZhPtoFH7Yu2gejr1GsVlOJgR83CbW", + "TEREGRAM": "EQCa0wLteCacHeKIL4Ov93irHeOG9BZ8Kpnh-fA7PK2YLHmp", + "NOTCAT": "EQCHGLPnpUwyXdoR6bnZW77vkfxVEX_q-bui2XTKUbw_-t2z", + "BRETT": "EQB-TVlb_TM0cMmXTmM99jrezAR_wQt8DIW3H54Q3oStRuet", + "WSTON": "EQB0SoxuGDx5qjVt0P_bPICFeWdFLBmVopHhjgfs0q-wsTON", + "BRETT2.0": "EQDpXRmv82yf2aWfMNrRcW-KgAZKekUnrpjlO625F14wGGEY", + "UTYAG": "EQC5p0k4t6D7U1uj99OtRhWSAGqhZFVjAHqPf529OHnOx6on", + "JOKER": "EQDSK89vV0Ta3Z45aERfc4nge_qS2jieJ1F5Cf_7Wl5sNCrg", + "420": "EQAtL60Tf3I_dk3ypBe3ow6F62OtmESf9MLEMAIJ9DxnT5ff", + "SHIBA": "EQCcae9Wtj6U276LJgJD91pqgwESQohWLoolYrlv8arhafRT", + "ULT": "EQAQU1jz5ShPaS9mxvI6NVBvwBl5_1b7YDemAzMCluDUMPgT", + "PUTU": "EQBM4iKs-zUNhphCINtSqWxjVm6npNBpHceo3ofi962EBN60", + "ZOMBO": "EQBN3oZvrWZS_5X9bv97dIie59mjjxY8yRoW-G9LGyIdRGIH", + "NOTDOGE": "EQBmlvw0x562sYf0i--DRMH1kATHOZ4YQgXJDEi89d73Yw6W", + "BRUNO": "EQDitV1lu80qFZ3k03HhWjRCW5lV6Q1QHlcCP9Pr48eY_YRB", + "BROGG": "EQBChMgbV4A1HzCmmu5BF_f_EmJQEEOdo36KuOmcEwTQG3N6", + "SATONSHI": "EQDlC7Q24TOQbmTTTZhoLZahvAT-qqYfWzX67FJHWLSfOAKN", + "STATHAM": "EQBHcVEoIwZ22GifL7BtVTGkzJKLQoC-rmqV6_nj7SR-hL2P", + "SALO": "EQDHl2sVhrY4lvdHFLzuSvGogXuSim206UvXHNwFR88hCiyh", + "$RELI": "EQBwHZMhU2YDTTNn67gm9BMg0V52hR0DDMmIT0ove342nAaO", + "UNITON": "EQBDuQkIJavatvmSC03gD4zmLm2rPG_QdnnJARKEyNFmFz2Z", + "THROG": "EQB7CuKGQobrBbbPbIpeDsycRgHkbeYiejSNfz7-p449GB_5", + "MEGATON": "EQC4SyqSFL38UsF-D-1AerTkLGOsMlpZPGxolP0shMxAflTt", + "OTN": "EQCZaonjafRJMGz98bItlACMBLh17Y3bMftkppmebJm2O92Z", + "BEE": "EQCRU1SC3xNf_r33q8NZzLughmvdE21JRYIZj4oRKUdRKRDb", + "MTN": "EQCP_-RAqJKTNM2I5WIbgLd6z_K0UnzuVkk3zEndx9fLi-Be", + "CHARITY TON": "EQCdKXgmX9fA_wvtvzZFSzs_I7v8nrCLIoCfXdjDFE_zQtZj", + "$SLORK": "EQCjXTDfAZ-BNAcTTJQ4RaYY3sAFdbIvLrTF4-ZUq3nQSGiz", + "TONNO": "EQBfJYTXWDpa1wL0wa4wDA7QHLANeLaDSTa1cM7Md1pzZeDZ", + "DOODIE": "EQBNRcBVHlpan383FrWWQMKOjSLLLaoRdktdktSH8d6YCYtz", + "MID": "EQCgW3iMiYqkqBjqYR391w525-3pnFzVstHlbZ4dkulW_oz5", + "TAI": "EQD3zalQrwnhAA5puG-J_tQv3W5n5c6v449jk084Bq4lWuSQ", + "ANONDOGE": "EQA1R1MdRBlsnxA-cWAPHedyq0QY2Pd2PYGOHzWrbou6qHi4", + "POLKA": "EQCkR0AUbI89aF1vj_07K3uxEX64iKQ3wHH5xFkqbFmmyKEp", + "$STEVEN": "EQD4ECL_2sk98sXsklIeKhftqZOP-XfSZ7rq9CJUURYVtd6J", + "VIDAR": "EQDzuaY5QTprByVUBYAqlgFH0EzZDN2eRXvvT3ut7K1-juMf", + "$RELI$": "EQBDez7OgkohH5VBas86_7W5vJhg3btCmc7xTxLTioKklrYi", + "KORM": "EQAzqUYA_QbvX2GkUaUu6wl6TTcwy5gytvUp2gqw9FVTLGfY", + "CHUZHAK": "EQDyrwn6x3MwQ9TGXhNpX1V7iPgjtzrNv9V81ACNjovLGOJ8", + "YGGDRASIL": "EQBUv0SsYb_HpTX-XdOgFJwbmd2ihMcRXnW1KpmBmFC-tiVi", + "LADA": "EQBjNisz_m-sdA9TcosQMmugdhl6hDjGcCMgQFa85p_8jx7p", + "SEED": "EQAtwmCISetV3ncZRb1QDiHcbSgfo-Y37-zPL6NhedGHGaTf", + "TSNAIL": "EQBtb653uaOEd-R7J0iYtfwPVlWJaR25iSN0AKkM_4FVudRf", + "NOTB": "EQAA1yxV_Kjmu7DsrqsffFW3KMtbG8JepWnIzSX7k4PgArcF", + "KOZEL": "EQAwWQMHsCrfFniXCy6UeOE4TiIiaChT4jNibMv538KFG_4J", + "DUREVOLD": "EQAl0ChJyynGWg0tGMCBM6soeKJrKtNm_IATf17jcFEQx0Ww", + "SOFT": "EQCHrhcBGVVlre8ngBlxLtfte37OGsXeJSmFSmABL8x44QH2", + "FNOT": "EQAVluGRC4wEV2aDpiIgqiEvhnH--lmq5EFaL6C9Vfv0VRMZ", + "MARIO": "EQDp1ZjaftohF8LnjXcdBwoWoxU9XmnRoqFFaGonFLYuhBfn", + "SANDY": "EQA5tckAU5dsVzA9uswXypDB0k5XkZEwqs91b8MpRaOFWA8u", + "$TCAT": "EQDeqPOYirvBSJ_JWAAUGMdq2zka5--Xxm5izOQwoAHefPy2", + "KNUCK": "EQD9gJfTRc2HehoTVfYUdxJFheD9RM3ApGy_o7cCrhjiNrCG", + "FID": "EQCsN1IslttnFpGjUabpt3RCnVYca74ZB9yD0L2ZK9CvJ53D", + "PUP": "EQDUAIom04q857tGR90Aru7Bquz8cx450xqLP_LqvxZcJfHe", + "REDA": "EQDnbDrg9wLls7gvlgt2GUPnMTq-3t_OfcOXIfRpJRIwOz6x", + "WAGMI": "EQCQdwkUWSov44v_wpHfl2AyMEGRLy2TfTCqRx7WPBsWj7bU", + "RCT": "EQDsvbmh_rHjpO-FX3pmtPutllynl0OKuqqR9c1RICibsN6l", + "ALPACA": "EQBXQPZMC5HCF5r7XXOMt-MFGFonYY3sZfcOli1TuKqsABPe", + "RUSKI": "EQBh30TdhfyIjprJq8kUVmbM44pLek-zS4ZLuwBygT7ikfHZ", + "BTCJ": "EQDq0jbsORVhjXBuflTZzoVjeCTuXughhfNac1tP9c3w2Kuz", + "CAT": "EQAApzIfA-vfYmqpyzmYCKR15CLlcz33tEJPfvgH68heBITn", + "BIGJOE": "EQA57vdDMCxDoDUp-iqTea__8lkXNGs0I7LkKnMazAsdDx38", + "BLMPKN": "EQCZbrEKQI4NwugmB2zsTXPFFdKwI3cIsiwm8yY0CbClOfw-", + "BOSST": "EQDPyywllQPfWUV5RUISqg6cusOZ5Aitwty7Gd8cG9uSfTYp", + "SFMT": "EQAJZElrDTgkt0exICrVUQPjN19knWOniGsJC_AunyqfPA0w", + "REPTILIAN": "EQDf4Kp8IzQoyMst2XJaTf2CCfpVw8DGYzx7AQDI53UAnYnd", + "KINGRY": "EQDhZr4X0NiWhDMMc5dUELAS0jXWx6v9gIH_NSnEuGEIeNIq", + "TONFI": "EQAlAh45Xeo-YB42zNOep7q0pUMmxHIdKK6c8xFEMDXxlm3l", + "PRIVATE": "EQA3BC27XNeinlQFvvQQipMEoTLupUshAkSVb0W129TH_OIf", + "AIPANDA": "EQBFQXpVeSD7xS2Enf5ypDyo5ie3sr9upCDENPkTl_z2CRBt", + "BLOB": "EQD79khvT-Hsv6aSWjFTw4TtwPJwBvVA_1huonO6c5_NgUzT", + "TGRUM": "EQCGIZ8Oz-1DW1iw3xHGdiOR-im-cC5h0nr-mLtOxEsiLmTb", + "POTATO": "EQBfl_pzi8t_fYXJEygZoz8cId1sdjnPTsBpvqiHjggXS3Ep", + "BIRB": "EQCCaVndeVUSu8Gj-9kh0FiPiv8jZVayvgPs7RjBkUICZJMA", + "REDRA": "EQAJ8R31Ku8Ep66rQGY5bpPvNz8cD9jnS7FPNtrHTfIDzMUF", + "FUCKBOY": "EQC5_Ex5ufcsTyDEuzniv69fi4AM0re-ZPwsHCSMpdvePxJ5", + "DRA": "EQBE_P2bKO9dHGSwUhZbQv6va-YRuEWUEZuCv_iEXgO-psTz", + "HATS": "EQAEaM5Mb5x5orATjZTTSwmnB-3ZXr7UAUcYSO5lfedwx5GW", + "TONS": "EQCCuWzQCREeibmenBAWLNb5h1xHA9Zt-DM_tP9KmqsRXWns", + "RUNEBOUND": "EQAyincAfCbjlHuhLSGIFSF9LU_lblOwqFaJVuGGC6vb6HsH", + "ALL": "EQDlnVo-6abJzyHlU8uB7pRjE7OIKt4HTTDPG4hfwOf5jTPg", + "PEPEAI": "EQAMooY5iZgnZfZ61shXRQMwaNxFFKvdqhdJrrVtZEmgqdHC", + "EGOT": "EQCRKqZUBg6h3FuARwB1wxFKb38sEvAjLZrwOpJaDHzJ3wB7", + "$FUD": "EQBv17mhH-nC1wGtheSl5eqwWABE5ZY8h_VcCuOmbpxRMIQn", + "GREEN BULL": "EQBYxFauvl3vbR-k633TXxJw926Jvx1KDB5TI_OZEbNiVkGq", + "FINE": "EQAQ2v-per0YhbC6leJGcVKdjvoP1o_ASbOMr3OQAn9-u8Pg", + "CATSX": "EQB7z7VPyy0uv9c1sG_S4LJ7Sy_8c4PQ70RQjr_VVW9FWTuG", + "$AIR": "EQCbic_MMH_inYXsdSztwTZ2GURuXeDMH1yIu3CN48boPm0j", + "RESL": "EQBvgia7nVhptB2WHmJBjJFadwaGefJfEM-F_iQZ9pEyYJ-d", + "GTA": "EQBhdFVD9EQ86A4tAvnGNes_zXaB5HqbxuodbQKxZh96mu6c", + "MOONRAT": "EQChrjzMX5vRXo48_USZqRViZw6jz4SNjQN1tMpG7QuXtB1w", + "TPLANT": "EQAEBhAUSSJ_NqYJO3WRNkBUMpfAf7hnd9X7WBhZSqLaxYko", + "TONIC": "EQC4D-qkAaqYXWOnWRJx6xtOhd7Fa9zZQdob8iWD6mS_kPqU", + "DDAO": "EQCW5g1evnQN2OZZEVe-23aSvEsgPauWZlF27ZIz5REhnWRy", + "REBE": "EQBaBlvYNgxlL_UQueXgCvbqeCrpdpF_ppVeUn8sJDbiQTpV", + "TON-JETTON": "EQAI3nuCz-FFt_Keyr5-ZsvxSMefzugaYoC3lZ684YWmJzVv", + "TONFROG": "EQBMj8pasD6BzRm7z3PIzxVxwqorpUnnXLRkYEGfbgNnGzSb", + "LOLON": "EQBlvsuUNXJtqpJtnYNLLzt9AxLbM9Zl7ZdsVir-jAq9P8Fh", + "SUS": "EQC7zueeeWc9-IomeD8n6QF3BBW5xKYUYNa7A8p2z384xLBO", + "SHEEP": "EQD39LNnyg6uEQv5jqkF76F45VY4sszDqqz2N8KbL1QQ3XPr", + "NUPOGODI": "EQD69dvNCI-ufnioXi1xCh8bt1QoGf9uv3Q8NUWCx1d6A6mF", + "REDOLFO": "EQC7RZhhm35znvIXfvh2LWqO6dxJPWGbElG2Nu6sakjPaOaD", + "TOT": "EQAcjv-v76upZgRoRNUM6tCmd6d9Q09sWVTFqCdfLeuP4P3b", + "RUSLON": "EQB087W3GWe2FIJPgoyI30Mf5d1hNBfqLc2s7wFTJJsX6qY_", + "GMC": "EQCRDcjdUDI4VgswjLeR6fQ-yHGvh1PhA-NxvUfNHTFUk-Z2", + "WCAT": "EQDTWt24QpHmW5ZAp0VVbJSPomS0pMkbU-yYqymDOSgkJIj0", + "RETO": "EQDzMClleDRNKk-hdYgD9V95kIfM8S6kCq4d_H0Udj2bp5fc", + "FELLA": "EQBPMGIkGX_kbZB3KRCzqNvpmGQcP6D8IbowffLEl5FqIUCt", + "PENIS": "EQBihOv_TdQ6gXRGHPFAO5lbOe550Yg2ZcCGF6I3lBp3Ima5", + "CC": "EQCjKuFM_TE0MMeQ5wqgAAR3eSrhVirqUiDNZ1TtujNxoyFM", + "REFROG": "EQBBOscfW6Xb-Vi4aCSaeLf8n-slePbo76YiqgMDdPat5Eqo", + "ORIGIN": "EQD0GDdBpVnbnaqiN2nRGpTGYrgtU1u9OGjCIQrnXs8vmLj9", + "PAVUL": "EQChXgdfER2nuLvZP5EiSlCf95oXZ5KDiCEmGBBv_WcNdxz3", + "PIXEL": "EQCgFCvZCV6qqB-2Kel6D_kV4zEW4FFOQfYV4KDDNTEJdItn", + "OWL": "EQC4un7SifufyaDYdz_kxpFl1C2s5lW5q9nB-b1Ez6HutVWU", + "PINCHY": "EQAk0b8tf2hqL-fYAjtPcpEVd_MxaqZIlcsVX2sgepAA70z7", + "MESSI": "EQBMsWhMc9SYw93JbfaSy1DeEsne-fpa8ljlmRWoTQQD0UBY", + "ILLUMINUS": "EQBE1ihGK5zblr6UO6iWlc1Ly4AM6s2rvhAYwdE7K_dbPmi7", + "PONZI": "EQCW4yvgtxof-sQaa2XK4ociGjEkgIgJpuG7Q6JB2BTMOOez", + "TROLL": "EQARB6zfv3LZmkM1ILkEnvDKJh7NcnYyEbrVOTLtYICHlfky", + "$UTYAG": "EQDQf4HWCaSP_PDlT99CkZCqMsOYoRfhOkkGVYtI544WfELj", + "FRAZZLE": "EQAQRX6Ah6QWJ6U2N5iAOETBXs3QAbBcmS7OlCWnySZ2Gx2k", + "JAKEY": "EQDcjwtuGr9XYqvWDKCFMMluE0CrvFG8loHQt3_t4S9uzLFQ", + "TOOTHLESS": "EQDpPWwhhh2jJGmuzQTtwWFpdCap97RF30CTEKeSIfPbKu74", + "HALVING": "EQAlXc3yxe9WWoio4YJcputngaUzBXG7G88PEV1BK2gsFI2N", + "TOM": "EQCSgZhqUSVcHrfNpkJTIbXlzofQSHjv4PmIhHc3wx6TFOPy", + "HOCH": "EQD-l8qEbK1xTF6jDvdbRdBabH29S7A4-ja4NrwWCjlba73U", + "SNAILTON": "EQAlxRV_BOYQ8hidYzEU1HR5wneoAo54xGmttOMsPDhArJeT", + "SQUID": "EQBh6FTV9Ye0F2PibnYcxJDTOXqv1suEt2OtnxQ4pd2cY-GK", + "JSYBZERO": "EQCy_Zo_ahdCCcDyGQazuAVKWa8KSW930j1VI8k9ZQJYB4_V", + "MARSY": "EQD_glIvg-D4t9WpTwF64f_BDYIYIQhBRHbUzyhS2MBe5jPY", + "DANON": "EQCbrv3j1V538QF5vF77QHHKIlDNeFZUHM-e09w5Yl95b3VB", + "BODEN": "EQDt-BjYx3Dz87_bPeZhTCyZe03ZwWw9hrdyUGcOJev8YJ9H", + "SASASASA": "EQArmzrbvoeWzk3vnbKjMzm6nGULCkWcdGBJExMtHuI1mC6i", + "BANANA": "EQDgyXtVBaUTYrNIYEyVTOIUCWItOPlj5eViJH7URrFI8DDR", + "NIRO": "EQDlifT4iNEOwVqzfnKia5TBJLhpmtVeumPf43idJrLbOeQh", + "TOKEN2049": "EQDIWpAIEBaC1d0vDaLYxEJ23Z6XnmOD0lfjYRnCt-RShsqh", + "TBANANA": "EQBIaMFX3zQ8ye0M5pha7ihg5i_e4WbiDWkPO6--FkCPKDsy", + "$NOTINU": "EQC-ammuP7JZLU_2GujAQeqgNqnTbA5FTZvn3Y1VSQAES6dF", + "SOCK": "EQARb2CYeCT73K-a_cWXycgbcWlleEx4fq4RCbZ7cWBtPN8s", + "TMEMERUNE": "EQBj1kNfI_UqoLXT28JJ3BJlhx-viXEkPNXSc1RdVIEMNYx5", + "POINTPAY": "EQCv2Fq5JBHEPb2xBjr5RCVHFnvEaVwThDXxGET9h-hFCJuC", + "MEMELANDIA": "EQBPFnUMSSIvcFFUehwdctOGsjKsFkNI66zlhsn6wqeu9lfB", + "DOGE20": "EQBLkkdMQ5q4KCIXPugKcIsGSGZooD0eNzy4rW4Pjk4Jzg50", + "TIMMYTON": "EQAi0A9qBy40gjZW8gVcuVgeCSlFqb3ZCm0BuaH18G4oCnZd", + "WEED": "EQC6p-6KHdSrfNZp94z8AQda5HtGFS-lLZ1H9Otj_KAs2DYJ", + "$ANTI": "EQD3FdbQVFOGXe_eLFtBTFgEbKy_TKni973tKRYREN3Gvbdw", + "REPL": "EQACTTQNtTp1FB3AnwyY_r7KkwT2XHFe6KgGmV25HoHgcSqL", + "HIPPO": "EQD8_ro5tJDyWm0avPITefZ-9ZkzQ9GRvAaKWLX1SkDKTGUt", + "ENRG": "EQDA6v7olSvZCEr2yNhUlM4eBnRF11lsHLrtkkh7MSMICt4r", + "BBODEN": "EQDq5knifPiixZktk0YHsMqh-J_AFuUjr2VWQjq2SmC1Um_o", + "TRIDER": "EQBWSAKfmB7-s0GUBXnFXFdzCU7z2B_arRGBqi9ZdqHuAckV", + "BONKFA": "EQCeXKmsn2-qaJIg3xDLSDaNTSLsjOGo1LpWTwm1iloS-9Ta", + "CAPYBARA": "EQCnhH6WVFiYBC9v2KiVTaI9RJhoEewU19GHjDRcZBSa5pXw", + "GOAT": "EQD9cutGLYKxPFu0VLrhHDXyroaGa7jgpgZKErboZ0vQna7z", + "END": "EQDmgVHxf-avCIDyU3POfqzXsyU9FzAOdJGVAY-arolFtb7B", + "GODW": "EQDuF0Tczp6JJGbAM8upQWI7GmsnYLp_tOz6EKL4NYaQ15L3", + "REDOGE": "EQAbFMm5zF9e9EJabZlqp7i3DzUNioAEJulmt0xMKZKDNGgb", + "GODZI": "EQDGi4QdZlEMbAVOzh9XvjEi76fghxYkBhHGtDByenGAtjDt", + "TONKA": "EQAQErEDZe6FBQCUCBfH23mE2wimCcIg0TFZmo43485oiKUR", + "FUT": "EQA77kEGzgncHYFpoRQeKb5-9QqFRewM9pRu0lF1CmznE-tr", + "BFS": "EQAtjx37jUhoLoTAVVOEBXqwcHN4kMbQFpeWlcOLJfC_JKTc", + "PC": "EQAj18Ki4sG47AXb14No35FnrMNBxC2uHcuIejJDyrd6VXpQ", + "$BLACKROCK": "EQDEdNoi56gqtDZVT10Q1wVjfy4fv3LXeswxbMxueCxLQ5_E", + "Y": "EQCL6jaTluAI7ET_JgZ3hPV9Ul8MXP2mZWhMQtGMzd-cNt-X", + "$DARE": "EQC_2ChvDjM3dbCZ1PthNO5l3HndK3A0Yi9xwOXDAiEPWx0Z", + "BRRR": "EQBghtl-wfJzoD1bUuwrfQtYt6zGXxB9k-RM-Hev5hlSKlXu", + "FISTY": "EQBpQvTIxfX6k_WtXxqji8Z6_PuOfekGCGHHk9gqjMeuhFte", + "DINJA": "EQByLZOaBbpr9EwNonXKEfkkZL1viNsYHezxw5CVK4Bh0zEb", + "RUSTON": "EQCBl22qpY2-cRNFOxUFqGZOaQt4Fq6y3VzQsUIOL49DjPk2", + "DOCA": "EQA6TjAatyRafrcZ3ICl0ti5RcLRoRX3atOUHfjLQePnIYVW", + "PUNDU": "EQCr9p5uFtaaoMyG1ysknK9yXaTzFKoNTWwOyHbkW-ChhXJ-", + "RUBETON": "EQCweXSZaX5k5xOtXLbGgfF8FY6Dbg6rNiPrd9d0f_-kDSHZ", + "$TONIC": "EQAFQmwcU2v27qIiVvWFlQVCYNE5rp_xTFC5UgXSzyu0_oM4", + "$TDOGGY": "EQAXOyAFyL_1ZDBUOfuBeN5efNfgrJA3VUlJCaTA00ue37hY", + "TONTONTONTONDTONTHON": "EQCPKZmEJgvmkCacqIXJZFRWmzFjs59DnkOOqujvos848-6X", + "RUGPULL": "EQDzvs6ZhNDZMQMeKK2Inc8ymDpORl0H5hAfgtf3dXRbt7I7", + "LFGO": "EQCuwk1Wfr7Kz2GYPNs0dqw0iXHCQ6MYALpmIzxzVjUgDDen", + "RPEPE": "EQA1ee6GStAO36IBK-GxAiCpvLSMknDwKxcE8Wm6llB79VJy", + "IVS": "EQACGPrFHWCy9QmeFXPqvZflyoeBb5grVQxAaucF0hNd1sEs", + "XDATING": "EQBWq51r5OfRqaa08bdqltJ2-HxrXzR2XDw5EdhQW1lemSrc", + "LCT": "EQBgEoDV213WNWiPvFqSPmOhb-Uhg0nBAWsqy-VxoKc0WueP", + "BOO": "EQA2g_29v2qo_Wy43OsUJl7Qh33QnK7Tf3TYoSM9PoiNqucC", + "REB": "EQB9orkYrxLCDP4apnnT_sQreRrR_TXL6GXVJeVyyBYdG0Jp", + "JARBUZ": "EQDsrCYtZgbO57PQ47b9oytjQrUjix-gz2wIP088DEUvh2kC", + "TONION": "EQD1GyhpiDxGDKeh58E94_nieGyUDRVl3RgM50lzdHXJl14b", + "CATFISH": "EQAXY9bhTRx_U5zCHaelrC4k1GbkiyDLfRkLfA626OCHQV02", + "STR": "EQA2A8BicHXYeftDdhXiHEomBReHq4--njMmfVHgQ2g9lWaE", + "CUNRMCRUGR": "EQDzMHK2qmV7uLhbSnU3ZL5SqnFJ8-dJ5m4dO6kMy8T3boiH", + "BITCAT": "EQAFKgTStbQilom8qB9UpeCjeEhDMSTodNItqFv04fHANbfE", + "KITTYKAT": "EQBcRUS4dNRnXaRo1NLPEVi6jFNGN6E1tonwWnfhi6IACuQt", + "INC": "EQBlHnYC0Uk13_WBK4PN-qjB2TiiXixYDTe7EjX17-IV-0eF", + "DWIF": "EQBCFFpx9xfxN_zsTovnl0O1lfiYKBwHLFGFwdiq0__Mxxvy", + "BTON": "EQAjjnIVdwE6J0zgDXiXgOU5odFAvDE4iKvYDZx95wK17a8F", + "SYLVE": "EQB_uCDG-BOTzNSMi0K4MO9SEVImQ9-RI4Vf5yWQvNkj-Ezr", + "BREADTON": "EQCr3QlbuUnHqqNWemGogrA1MED1ZEA1xNIXUnO8bwp2rz3k", + "REAPE": "EQC3QvViqnufNPaXfMRyyFvFYqkFdVR4CojLeeVdasZfqu1l", + "OUSDT": "EQC_1YoM8RBixN95lz7odcF3Vrkc_N8Ne7gQi7Abtlet_Efi", + "WONT": "EQCLX6OvrMkkbts0wg-zg-SP4TP5HNSJrBqBpQ3xFjgVGz9H", + "JEEB": "EQD-ECvwQ0L1T6HH9ycl0_vhENhR5McFPuoiIZnVuJHbXrUl", + "VISHNYA": "EQCDIJKFTf5QoWii6-_KTk8bo6eKiR-6hk5HUQwixCe8SW-I", + "PEPEMOM": "EQD_NgujLw9DgWwS15Bjj2V-MJZu2c28XN2OvsPY-pOva1Ow", + "REMO": "EQAXb_QAys_BStDUxN92uOok710QcwN1rzrsSwdc9Jq_n5W5", + "MMT": "EQCyfdVbfwkcBfBSn57GRnMY_BRO4vYG7HnkWngH41FLM-5Y", + "TSIN": "EQAt9cGW-wcF961fFOwZF-AewpW9QZvPm1UM2lzlFEEZzYz5", + "CRAZYFROG": "EQCFdPKAkjJy1oUPl5TKefNiRyl7U_cRY0eTTzHYO7YhEPAn", + "1000KG": "EQAqgiW1RllbANaZ7g7zbOAWOscIhe_X1Meh795fx6rhCKuM", + "VC": "EQDqQscohYilSqWEyeLyCReOQKOh7KhRX_CPlD-Qdx02tUbG", + "TAMLON": "EQAJSsVIlymb4imPnHoCbHB_fAmNAz0hLLy0s5SjC3jCUglw", + "GOOGLY": "EQCrn-SyRL4a8x3uEUu_3wnXFCEoLfRLyHgvYMVcRv6TImj_", + "SASS": "EQBx4FzrxFutAQMQqxyp3Fy2t-WD5NS7w6yy8zq0oW1cC8E8", + "NEKE": "EQCvJIix-iH98tJs30OnBnmLXKN2sJOaJRsvQ-HsEBZqzYJ8", + "TON JUMPER": "EQDzY8Xd_xEnfu9r-ed3Efgr0FnqjGHJVZKma8E-DZiEa5Az", + "WEIRDO": "EQAgmzk_rIEiKHbxeikG9fxwC2grglP8aoiWVLqORsbiHF6y", + "GAMEFI": "EQAtccCOmp7FWRD6h7pKKjKZ2GyaYLpyD0qWWVgFYX3xivNE", + "QCAT": "EQC2waGcg4AHYbbHyFrSnKSHar8zYTu_cZT2YdN7xy3tnWQq", + "SNACKBOXAI": "EQCobsV_aDFSMXB-KHb0RKqLz9GyaifqoDgvkebja3ZukTf0", + "LIBERTUM": "EQDza8fZEVo9cwbfxTnUrBXz3oP3DF2gNM4D3aZsAWaNajG9", + "FLOKI CAT": "EQBSw7MDTRlhyn6Jh0HuuqI7oe9QgeRTlGfczm40mb46y8dO", + "TBOME": "EQCLtxJQEc1ZdZhWGYDU91jmfPQ-ti8Lcky11v_hy46qUBvt", + "OAAL": "EQCvsARFU_tJCRq7eFTrs2ORasCwDIPLQo7ZlkehsMmgQzd8", + "RARE": "EQDSLNHYkqr1UumqZXEL2-v9B-wz5kH3frBx-ZK6V-mibRTH", + "UTYAC": "EQCP-VD83THjnC9BDUK__VrYMZhlQ0D7r73e42-pWy3keb6i", + "STEC": "EQAMSx9XLYJ6lYB9LL0kAPjZPhGYdlV7QQsPgRrVGv3qGLNt", + "BBY": "EQCAUOhzwnVNooROds6JEpnIDRweQmZ6RkSZhY8BJuv0KscX", + "DORKLORD": "EQB5ajHvSDzUdbV-o22d7qRgDtgVkdzFrpa9w6IxkyC6Gut9", + "DOGKEY": "EQAR2PIJeGcsdEdeiH4c7Eh9n9xjjoUiSEwEnbQC0BVaSCOe", + "STAY TONED": "EQDOnb_oCNM0bkILoKHqOOYAp31qzDKl9yzS4QlGKREsJ_zg", + "TAKE": "EQBzyesZ3p1WGNrggNSJi6JFK3vr0GhqJp4gxker9oujjcuv", + "RAIN": "EQBl1rSkh7SE178QH6aLoI4JXtIZ7Pim1BDTgdjKPTQZyeji", + "LIME": "EQAdU86oV6LXhjlPj7kvfcCNb4z-YVVWtdDh9xDkD5xNxBv-", + "SAKE": "EQCIIUm7dnBgmcrubDfFLZV52oe1F_HFdDZiMfer9riphpQo", + "POE": "EQBB7RWO0qzo6JhikXLjmKJc81IS4JR0cyc5bPAL_h0GGmxW", + "BLYAT": "EQBJnyfpBB62iPgXei0DiijXNt4y7ZyxDCurFoj6OMK61d-F", + "SBOB": "EQAiWumaavaZSn8yw_44-5zEcXie2hP9z29h1rCx5-ymu_An", + "LION": "EQBYXclZSfB2fHBF7CGaMnVmsnRVYGFwpKBLkIh4X8q2ttrP", + "TZILLA": "EQBclgq7OfUd4U528e7yo_HcdFY6Ertd8mxDRfWRCDxMQ_np", + "VINUM": "EQA1uIhaDiy99f5WWh1BACrDrQQrSI6ToaAIN584z3bYAfxg", + "GLEB": "EQBQALxoFHj7QCsozPJCI4CWHx1p5OBUcWlqGZ4pJamr_ELN", + "BLKC": "EQD-J6UqYQezuUm6SlPDnHwTxXqo4uHys_fle_zKvM5nYJkA", + "RUBLE": "EQDtGcmL2A0acApXgSSSeRPzbEY-VV9n_2PvW_-rz_zxnl39", + "REBO": "EQD2J7J3KoVq1pY-hfSXFHXp9PCvorU42WlUA0oXzS0eOOU0", + "READY": "EQCAJ6ZuvdUMC_d723VXFCoj8hnmK2ixoup8sUJnleRoCvGY", + "O2": "EQCHOdp22D9ClcPpV8jx4HerKoXEXdZXE2BkU3FTEoAXpWmd", + "CHNZ": "EQBQQLEDfsrfC1cSVKaC0aj9fkf-zjnNC0JBDbD5_zrpRwSk", + "SIMPS": "EQArmm85Ialy1d5JiGnzn8jms9Aa8gvX6QsqEcgiVnOr11rL", + "COTON": "EQC6GzzTRMIE91kLWklJ22m4jF-YEZOs1oI5wfATZnMkoGde", + "EGG": "EQDCAjHxgyScUPS-Am_2nAH7C5XWM4fc2X9dVyWpFgHXG2rT", + "$ICE": "EQB1qL6kdVYjvmXvLF_pogWk6WLnJ4pY2i-5ZSyMbvgM93hC", + "TUSA": "EQBRQhEdXWcVJ1sHYIqUrsYXzt9KkBqQSkFjiXjIDWwm5VO1", + "KI TON": "EQBZjntYGAiE5vW0R_0ilpaGGvVscQhGy9z0DDcyddZ9n6F3", + "NOMONEY": "EQAnhenTxjuJaRniVhoJmfvnjVfEqqTBgla8UBBHclCa7Epl", + "REBONK": "EQCJvplFE4IU_QPttNSmwOwj-Yd9-KC8aUVc6rKafYhEg26E", + "ANIME": "EQBo2aQ7XPB1Olwe6TkuTxmC8FPaldjcBvRakD0gwn1eTIOU", + "HYPE": "EQDFnE_bJ8nsUKd0hx-74K_aHeYYEvn53IdAUzK82eZpJAv2", + "LOVE": "EQAS2elYb6_hqWyOl7gpuYTzf1sqmjLJQ0lQ4X_4d_MvtMWR", + "RELIO": "EQDOdaQqoqaRIPeHjhPkwk8WRUiBcZeZMSjvyPDTkxNbaUQ2", + "LIFEYT": "EQD_KpO2-iFeHPT4dF0ur9E0iAFts2fwhpR2KjwAmYKpccvH", + "WEX": "EQBtUKPbhABPYtltiKWA547s4ZhJ5wZHvVV1eyfV7sEyGs2J", + "MEWTON": "EQBSDtuc7Jx2Ml0RrcAuw4wLvcyrojhIak8h858xz68ybok4", + "TONKEY": "EQCn9sEMALm9Np1tkKZmKuK9h9z1mSbyDWQOPOup9mhe5pFB", + "$SHIK": "EQDrqddjHHlTiowXD_HWTT7-qwMJD8sZpqTf9hsNY6B_wsya", + "GRBS": "EQBj7uoIVsngmS-ayOz1nHENjZkjTt5mXB4uGa83hmcqq2wA", + "BB": "EQBe8n94X9pm9yc6QRrWATyXwSHDpECSHqZYKhyckrZpIDoa", + "TIGER": "EQBhHP85_nVS5r_1i9A6thspySqeWowbuPeD0FQFxp3aLPVO", + "WHACKD": "EQAB_YgxhxCc9c1PB20c1Vg_qmA8nyUXqkLb5z7lZ9i5E-0y", + "JBCT": "EQAvvEdq4x5K6owjsMI4X6AxKkTBXVKIVkiSHDLt6i2LFnnQ", + "GRIMACE": "EQDODkpfxZ4joe3RxBckjgvCCQH-TXxozbNwTOoCUoOMCfX8", + "SBM": "EQAyLr84qjYtDoAi9eUnF47hOof5zvGikes1xcifoW467Y51", + "BBTON": "EQA4W_aC1dQcznrsSHbNRFL49a7MzTLD0Xl7zWSm4LmbMLlR", + "LRT": "EQAJ29WkjjLqu-ucmAj9A0rRm86U6OvNRz46iqd1XVy8Pryq", + "UFO": "EQDKpmcZLsZ5YY3wgZ1bXICKZDeHxv8vKcEqieN1A2mhUstv", + "NOBL": "EQA8XGj26Nnz31k3sZVH6an7R4UuTxXp3gzeZEP3Og7IkK10", + "KRABS": "EQAaV7Q9M0vrO5HfKrPNnQneXB-S9RyBneMNeX4KzECIYrdk" +} \ No newline at end of file From 4690bbac1061f37a68003a30912dbbf1650218cf Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 08:54:01 +0200 Subject: [PATCH 03/26] Refactor code, minor improvements --- TonTools/Contracts/Contract.py | 26 +++++++++++++-------- TonTools/Contracts/Jetton.py | 2 +- TonTools/Contracts/NFT.py | 3 +-- TonTools/Contracts/Wallet.py | 3 ++- TonTools/Contracts/utils.py | 4 ++-- TonTools/Providers/DtonClient.py | 23 ++++++++---------- TonTools/Providers/TonCenterClient.py | 4 ++-- TonTools/Providers/utils.py | 12 +++------- examples/Jetton/Jetton_data_and_transfer.py | 2 +- examples/Jetton/transfer_jetton.py | 3 ++- examples/NFT/NFTs_data.py | 4 ++-- examples/NFT/get_NFT_data.py | 3 ++- examples/NFT/get_collection_data.py | 3 ++- examples/NFT/transfer_nft.py | 2 +- examples/wallet_creation_and_deployment.py | 2 +- 15 files changed, 48 insertions(+), 48 deletions(-) diff --git a/TonTools/Contracts/Contract.py b/TonTools/Contracts/Contract.py index 93bcf2d..31d7417 100644 --- a/TonTools/Contracts/Contract.py +++ b/TonTools/Contracts/Contract.py @@ -2,8 +2,8 @@ import json import typing -from tonsdk.utils import Address, InvalidAddressError, b64str_to_bytes -from tonsdk.boc import Cell, Slice +from tonsdk.utils import Address, b64str_to_bytes +from tonsdk.boc import Cell from .utils import transaction_status, known_prefixes @@ -17,7 +17,7 @@ def isBase64(sb): else: raise ValueError("Argument must be string or bytes") return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes - except Exception: + except ValueError: return False @@ -25,7 +25,7 @@ def is_boc(b64str: str): try: Cell.one_from_boc(b64str_to_bytes(b64str)) return True - except: + except Exception: return False @@ -35,7 +35,13 @@ def __init__(self, data: dict): self.source = data['source'] self.destination = data['destination'] self.value = data['value'] - self.msg_data = base64.b64decode(data['msg_data']).decode().split('\x00')[-1] if not is_boc(data['msg_data']) else data['msg_data'] + if data.get('msg_data') is None: + self.msg_data = None + else: + if not is_boc(data['msg_data']): + self.msg_data = base64.b64decode(data['msg_data']).decode().split('\x00')[-1] + else: + self.msg_data = data['msg_data'] self.op_code = self.try_get_op() if 'op_code' not in data else data['op_code'] def try_detect_type(self): @@ -48,9 +54,9 @@ def try_get_op(self): if not is_boc(self.msg_data): op = '000000' else: - slice = Cell.one_from_boc(b64str_to_bytes(self.msg_data)).begin_parse() - if len(slice) >= 32: - op = slice.read_bytes(4).hex() + _slice = Cell.one_from_boc(b64str_to_bytes(self.msg_data)).begin_parse() + if len(_slice) >= 32: + op = _slice.read_bytes(4).hex() else: return None return op @@ -140,7 +146,7 @@ def __init__(self, address, provider): self.address = address self.provider = provider - async def get_transactions(self, limit: int = 10**9, limit_per_one_request: int = 100) -> typing.List[Transaction]: + async def get_transactions(self, limit: int = 10**9, limit_per_one_request: int = 100) -> typing.List[Transaction]: return await self.provider.get_transactions(self.address, limit, limit_per_one_request) async def run_get_method(self, method: str, stack: list): # TonCenterClient or LsClient required @@ -154,4 +160,4 @@ async def get_balance(self): # returns nanoTons return await self.provider.get_balance(self.address) async def get_state(self): - return await self.provider.get_state(self.address) \ No newline at end of file + return await self.provider.get_state(self.address) diff --git a/TonTools/Contracts/Jetton.py b/TonTools/Contracts/Jetton.py index 13daabd..0d6477e 100644 --- a/TonTools/Contracts/Jetton.py +++ b/TonTools/Contracts/Jetton.py @@ -41,7 +41,7 @@ async def update(self): self.image = jetton.image self.token_supply = jetton.token_supply - async def get_jetton_wallet(self, owner_address: str): # TonCenterClient or LsClient required + async def get_jetton_wallet(self, owner_address: str): # TonCenterClient or LsClient required jetton_wallet_address = await self.provider.get_jetton_wallet_address(self.address, owner_address) return JettonWallet(jetton_wallet_address, self.provider) diff --git a/TonTools/Contracts/NFT.py b/TonTools/Contracts/NFT.py index 713b9d6..31ae198 100644 --- a/TonTools/Contracts/NFT.py +++ b/TonTools/Contracts/NFT.py @@ -1,7 +1,6 @@ import json -from tonsdk.utils import Address, InvalidAddressError -from ..Contracts.Contract import Contract +from TonTools.Contracts.Contract import Contract class NftCollectionError(BaseException): diff --git a/TonTools/Contracts/Wallet.py b/TonTools/Contracts/Wallet.py index 19f82d8..a7253bc 100644 --- a/TonTools/Contracts/Wallet.py +++ b/TonTools/Contracts/Wallet.py @@ -1,11 +1,12 @@ import tonsdk -from tonsdk.utils import Address, InvalidAddressError +from tonsdk.utils import Address from tonsdk.contract.wallet import WalletVersionEnum, Wallets from ..Contracts.Contract import Contract from tonsdk.utils import bytes_to_b64str import tonsdk.contract.token.ft from tonsdk.contract.token.nft import NFTItem + class WalletError(BaseException): pass diff --git a/TonTools/Contracts/utils.py b/TonTools/Contracts/utils.py index 9c13ff3..ceffec4 100644 --- a/TonTools/Contracts/utils.py +++ b/TonTools/Contracts/utils.py @@ -12,8 +12,8 @@ def transaction_status(tr_data: str): else: cell = deserialize_boc(b64str_to_bytes(tr_data)) tr = PytonlibTransaction(PytonlibSlice(cell)) - if not(tr.description.action and tr.description.action.result_code) and \ - not(tr.description.compute_ph.type == 'tr_phase_compute_vm' and tr.description.compute_ph.exit_code): + if not (tr.description.action and tr.description.action.result_code) and \ + not (tr.description.compute_ph.type == 'tr_phase_compute_vm' and tr.description.compute_ph.exit_code): return True return False diff --git a/TonTools/Providers/DtonClient.py b/TonTools/Providers/DtonClient.py index 0c9e4ad..f5a8e9e 100644 --- a/TonTools/Providers/DtonClient.py +++ b/TonTools/Providers/DtonClient.py @@ -1,14 +1,11 @@ import asyncio -import typing from datetime import datetime -from math import ceil import base64 import aiohttp import requests from graphql_query import Argument, Field, Operation, Query -from tonsdk.boc import Cell -from tonsdk.utils import Address, b64str_to_bytes, bytes_to_b64str +from tonsdk.utils import Address from .utils import get, markets_adresses, is_hex from ..Contracts.NFT import NftItem, NftCollection @@ -23,7 +20,7 @@ class DtonError(BaseException): async def process_response(response: aiohttp.ClientResponse): try: response_dict = await response.json() - except: + except Exception: raise DtonError(f'Failed to parse response: {response.text}') if response.status != 200: raise DtonError(f'dton failed with error: {response_dict}') @@ -70,9 +67,9 @@ def _process_address(self, address): def get_friendly(address: str): return Address(address).to_string(True, True, True) - def get_addr_from_wc_hex(self, wc: int, hex: str): + def get_addr_from_wc_hex(self, wc: int, hex_: str): return self._process_address( - Address(str(wc) + ':' + hex).to_string() + Address(str(wc) + ':' + hex_).to_string() ) async def send_query(self, graphql_query: str, variables=None): @@ -121,7 +118,7 @@ def process_args(self, args: dict): result.append(Argument(name=k, value=v)) return result - async def raw_send_query(self, table_name: str, fields: list, type="query", **kwargs): + async def raw_send_query(self, table_name: str, fields: list, type='query', **kwargs): result_args = self.process_args(kwargs) result_fields = self.process_fields(fields) @@ -301,7 +298,7 @@ async def get_collection(self, collection_address: str): return NftCollection(result, self) - async def get_collection_items(self, collection: NftCollection, limit_per_one_request: int = 0): + async def get_collection_items(self, collection: NftCollection): if not collection.is_full(): await collection.update() @@ -315,7 +312,7 @@ async def get_collection_items(self, collection: NftCollection, limit_per_one_re result.append(NftItem(self._process_address(self.get_addr_from_wc_hex(item['workchain'], item['address'])), self)) return result - async def get_transactions(self, address: str, limit: int = -1, limit_per_one_request: int = 150): + async def get_transactions(self, address: str, limit: int = -1): transactions = await self.raw_get_transactions( fields=[ 'gen_utime', 'total_fees_grams', 'hash', 'lt', 'compute_ph_success', @@ -396,8 +393,8 @@ async def get_wallet_seqno(self, address: str): async def get_balance(self, address: str): data = (await self.raw_get_account_states(['account_storage_balance_grams'], - address=Address(address).hash_part.hex().upper(), - workchain=Address(address).wc))[0] + address=Address(address).hash_part.hex().upper(), + workchain=Address(address).wc))[0] return int(data['account_storage_balance_grams']) @@ -428,7 +425,7 @@ async def get_jetton_wallet_address(self, jetton_master_address: str, owner_addr addr = await self.raw_send_query('getJettonWalletAddress', [], minter_address=jetton_master_address, user_address=owner_address) - return self._process_address((addr)) + return self._process_address(addr) async def get_jetton_wallet(self, jetton_wallet_address: str): data = (await self.raw_get_account_states([ diff --git a/TonTools/Providers/TonCenterClient.py b/TonTools/Providers/TonCenterClient.py index 97849ec..16b029c 100644 --- a/TonTools/Providers/TonCenterClient.py +++ b/TonTools/Providers/TonCenterClient.py @@ -223,7 +223,7 @@ async def get_collection_items(self, collection: NftCollection, limit_per_one_re items = [] for p in range(ceil(collection.next_item_index / limit_per_one_request)): items += await asyncio.gather(*[ - self.run_get_method(address=collection.address, method='get_nft_address_by_index',stack=[['num', i]]) for i in range(p * limit_per_one_request, min(collection.next_item_index, limit_per_one_request * (p + 1)))]) + self.run_get_method(address=collection.address, method='get_nft_address_by_index', stack=[['num', i]]) for i in range(p * limit_per_one_request, min(collection.next_item_index, limit_per_one_request * (p + 1)))]) result = [] for data in items: @@ -331,7 +331,7 @@ async def get_jetton_wallet_address(self, jetton_master_address: str, owner_addr return jetton_wallet_address async def get_jetton_wallet(self, jetton_wallet_address: str): - data = await self.run_get_method(address=jetton_wallet_address, method='get_wallet_data',stack=[]) + data = await self.run_get_method(address=jetton_wallet_address, method='get_wallet_data', stack=[]) wallet = { 'address': jetton_wallet_address, 'balance': int(data[0][1], 16), diff --git a/TonTools/Providers/utils.py b/TonTools/Providers/utils.py index b43d685..212a295 100644 --- a/TonTools/Providers/utils.py +++ b/TonTools/Providers/utils.py @@ -3,20 +3,14 @@ from base64 import b64decode import aiohttp -import asyncio from tonsdk.boc import Cell -from tonsdk.utils import Address, bytes_to_b64str, b64str_to_bytes -from ton.account import Account -from ton import TonlibClient -from ton.utils.cell import read_address - -def is_hex(str): +def is_hex(s: str): try: - int(str, 16) + int(s, 16) return True - except: + except ValueError: return False diff --git a/examples/Jetton/Jetton_data_and_transfer.py b/examples/Jetton/Jetton_data_and_transfer.py index 7ec5958..92092b6 100644 --- a/examples/Jetton/Jetton_data_and_transfer.py +++ b/examples/Jetton/Jetton_data_and_transfer.py @@ -36,4 +36,4 @@ async def main(): if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/Jetton/transfer_jetton.py b/examples/Jetton/transfer_jetton.py index 89c9b35..a87b130 100644 --- a/examples/Jetton/transfer_jetton.py +++ b/examples/Jetton/transfer_jetton.py @@ -8,6 +8,7 @@ # YOUR wallet mnemonic MNEMONICS = ['your', 'mnemonic', '...'] + async def main(): client = TonCenterClient(orbs_access=True) your_wallet = Wallet(provider=client, mnemonics=MNEMONICS, version='v4r2') @@ -21,4 +22,4 @@ async def main(): print('done') if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/NFT/NFTs_data.py b/examples/NFT/NFTs_data.py index e45da01..5f63ad5 100644 --- a/examples/NFT/NFTs_data.py +++ b/examples/NFT/NFTs_data.py @@ -26,7 +26,7 @@ async def main(): item = NftItem(read_address(Cell.one_from_boc(b64str_to_bytes(data[0][1]['bytes']))).to_string(True, True, True), provider=client) # TonCenterClient await item.update() if isinstance(client, LsClient): - data = await nft_collection.run_get_method(method='get_nft_address_by_index', stack=[{"@type": "tvm.stackEntryNumber","number": {"@type": "tvm.numberDecimal","number": str(index)}}]) # LsClient + data = await nft_collection.run_get_method(method='get_nft_address_by_index', stack=[{"@type": "tvm.stackEntryNumber", "number": {"@type": "tvm.numberDecimal", "number": str(index)}}]) # LsClient item = NftItem(read_address(Cell.one_from_boc(b64str_to_bytes(data[0].cell.bytes))).to_string(True, True, True), provider=client) # LsClient await item.update() if isinstance(client, TonApiClient): @@ -48,4 +48,4 @@ async def main(): if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/NFT/get_NFT_data.py b/examples/NFT/get_NFT_data.py index 2d52291..4180e01 100644 --- a/examples/NFT/get_NFT_data.py +++ b/examples/NFT/get_NFT_data.py @@ -4,6 +4,7 @@ NFT = 'EQD6ufFjSIUJSkbVuV7w00ORT8UvoMLQ9RDZ1lJ8sYh3cOIx' + async def main(): client = TonCenterClient(orbs_access=True) @@ -13,4 +14,4 @@ async def main(): if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/NFT/get_collection_data.py b/examples/NFT/get_collection_data.py index 8e765ec..e52da94 100644 --- a/examples/NFT/get_collection_data.py +++ b/examples/NFT/get_collection_data.py @@ -4,6 +4,7 @@ COLLECTION = 'EQDvRFMYLdxmvY3Tk-cfWMLqDnXF_EclO2Fp4wwj33WhlNFT' + async def main(): client = TonCenterClient(orbs_access=True) @@ -17,4 +18,4 @@ async def main(): if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/NFT/transfer_nft.py b/examples/NFT/transfer_nft.py index 935043b..61b5222 100644 --- a/examples/NFT/transfer_nft.py +++ b/examples/NFT/transfer_nft.py @@ -13,4 +13,4 @@ async def main(): print(resp) # 200 if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/wallet_creation_and_deployment.py b/examples/wallet_creation_and_deployment.py index 6fb686b..fee99e9 100644 --- a/examples/wallet_creation_and_deployment.py +++ b/examples/wallet_creation_and_deployment.py @@ -38,4 +38,4 @@ async def main(): if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 83ddf24709dae4249856b6f07455fe43b4316bbf Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 08:56:58 +0200 Subject: [PATCH 04/26] Add the AddressForm enum --- TonTools/Providers/DtonClient.py | 7 ++++--- TonTools/Providers/LsClient.py | 8 +++++--- TonTools/Providers/TonApiClient.py | 7 ++++--- TonTools/Providers/TonCenterClient.py | 7 ++++--- TonTools/__init__.py | 4 ++++ 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/TonTools/Providers/DtonClient.py b/TonTools/Providers/DtonClient.py index f5a8e9e..0c1e7e4 100644 --- a/TonTools/Providers/DtonClient.py +++ b/TonTools/Providers/DtonClient.py @@ -11,6 +11,7 @@ from ..Contracts.NFT import NftItem, NftCollection from ..Contracts.Contract import Transaction from ..Contracts.Jetton import Jetton, JettonWallet +from ..Enums.Address import AddressForm class DtonError(BaseException): @@ -31,7 +32,7 @@ async def process_response(response: aiohttp.ClientResponse): class DtonClient: def __init__(self, key: str = None, # dton api key - addresses_form='user_friendly', # addresses_form could be 'raw' or 'user_friendly' + addresses_form: str = AddressForm.USER_FRIENDLY, testnet=False, private_graphql=False ): @@ -55,9 +56,9 @@ def __init__(self, self.cookies = {} def _process_address(self, address): - if self.form == 'raw': + if self.form == AddressForm.RAW: return Address(address).to_string(is_user_friendly=False) - elif self.form == 'user_friendly': + elif self.form == AddressForm.USER_FRIENDLY: if self.testnet: return Address(address).to_string(True, True, True, True) else: diff --git a/TonTools/Providers/LsClient.py b/TonTools/Providers/LsClient.py index 5831ebc..2634614 100644 --- a/TonTools/Providers/LsClient.py +++ b/TonTools/Providers/LsClient.py @@ -12,6 +12,8 @@ from ..Contracts.Contract import Transaction from ..Contracts.Wallet import Wallet from ..Contracts.Jetton import Jetton, JettonWallet +from ..Enums.Address import AddressForm +from ..Enums.Exception import TVMExitCode from .utils import markets_adresses, get, process_jetton_data @@ -42,16 +44,16 @@ def __init__(self, ls_index=0, workchain_id=0, verbosity_level=0, default_timeout=10, - addresses_form='user_friendly' # or raw + addresses_form: str = AddressForm.USER_FRIENDLY ): super().__init__(ls_index, config, keystore, workchain_id, verbosity_level, default_timeout) TonlibClient.enable_unaudited_binaries() self.form = addresses_form def _process_address(self, address): - if self.form == 'user_friendly': + if self.form == AddressForm.USER_FRIENDLY: return Address(address).to_string(True, True, True) - elif self.form == 'raw': + elif self.form == AddressForm.RAW: return Address(address).to_string(is_user_friendly=False) async def run_get_method(self, method: str, address: str, stack: list): diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index 21338bc..efd3ff7 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -7,6 +7,7 @@ from ..Contracts.Contract import Transaction from ..Contracts.Wallet import Wallet from ..Contracts.Jetton import Jetton +from ..Enums.Address import AddressForm class TonApiError(BaseException): @@ -27,7 +28,7 @@ async def process_response(response: aiohttp.ClientResponse): class TonApiClient: def __init__(self, key: str = None, # api key from tonapi - addresses_form='user_friendly', # addresses_form could be 'raw' or 'user_friendly' + addresses_form: str = AddressForm.USER_FRIENDLY, testnet=False ): self.form = addresses_form @@ -45,9 +46,9 @@ def __init__(self, self.headers = {} def _process_address(self, address): - if self.form == 'raw': + if self.form == AddressForm.RAW: return Address(address).to_string(is_user_friendly=False) - elif self.form == 'user_friendly': + elif self.form == AddressForm.USER_FRIENDLY: if self.testnet: return Address(address).to_string(True, True, True, True) else: diff --git a/TonTools/Providers/TonCenterClient.py b/TonTools/Providers/TonCenterClient.py index 16b029c..bb9a160 100644 --- a/TonTools/Providers/TonCenterClient.py +++ b/TonTools/Providers/TonCenterClient.py @@ -11,6 +11,7 @@ from ..Contracts.Contract import Transaction from ..Contracts.Wallet import Wallet from ..Contracts.Jetton import Jetton, JettonWallet +from ..Enums.Address import AddressForm from .utils import markets_adresses, get, process_jetton_data from ._orbs_ton_access import get_http_endpoint @@ -38,7 +39,7 @@ class TonCenterClient: def __init__(self, key: str = None, - addresses_form='user_friendly', # addresses_form could be 'raw' or 'user_friendly'. + addresses_form: str = AddressForm.USER_FRIENDLY, base_url=None, testnet=False, orbs_access=False # https://www.orbs.com/ton-access/ @@ -72,9 +73,9 @@ def __init__(self, self.headers = {} def _process_address(self, address): - if self.form == 'raw': + if self.form == AddressForm.RAW: return Address(address).to_string(is_user_friendly=False) - elif self.form == 'user_friendly': + elif self.form == AddressForm.USER_FRIENDLY: if self.testnet: return Address(address).to_string(True, True, True, True) else: diff --git a/TonTools/__init__.py b/TonTools/__init__.py index 5f0dbe0..9beb5a3 100644 --- a/TonTools/__init__.py +++ b/TonTools/__init__.py @@ -7,5 +7,9 @@ from .Providers.TonApiClient import * from .Providers.TonCenterClient import * from .Providers.DtonClient import * +from .Providers.SafeLsClient import * + +from .Enums.Address import * +from .Enums.Jetton import * # docs https://github.com/yungwine/TonTools From 77e5e303715a799bad66420df44b4a5d87989f84 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 08:57:56 +0200 Subject: [PATCH 05/26] Refactor code, minor improvements --- TonTools/Providers/TonApiClient.py | 2 +- TonTools/Providers/TonCenterClient.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index efd3ff7..fe1deae 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -17,7 +17,7 @@ class TonApiError(BaseException): async def process_response(response: aiohttp.ClientResponse): try: response_dict = await response.json() - except: + except Exception: raise TonApiError(f'Failed to parse response: {response.text}') if response.status != 200: raise TonApiError(f'TonApi failed with error: {response_dict}') diff --git a/TonTools/Providers/TonCenterClient.py b/TonTools/Providers/TonCenterClient.py index bb9a160..f9e96ec 100644 --- a/TonTools/Providers/TonCenterClient.py +++ b/TonTools/Providers/TonCenterClient.py @@ -27,7 +27,7 @@ class GetMethodError(TonCenterClientError): async def process_response(response: aiohttp.ClientResponse): try: response_dict = await response.json() - except: + except Exception: raise TonCenterClientError(f'Failed to parse response: {response.text}') if response.status != 200: raise TonCenterClientError(f'TonCenter failed with error: {response_dict["error"]}') @@ -36,7 +36,6 @@ async def process_response(response: aiohttp.ClientResponse): class TonCenterClient: - def __init__(self, key: str = None, addresses_form: str = AddressForm.USER_FRIENDLY, From c2ef352449798cbb5cbca6badc881a1d4c351062 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 08:58:47 +0200 Subject: [PATCH 06/26] Remove self.address redefinition --- TonTools/Contracts/NFT.py | 1 - 1 file changed, 1 deletion(-) diff --git a/TonTools/Contracts/NFT.py b/TonTools/Contracts/NFT.py index 31ae198..11090fe 100644 --- a/TonTools/Contracts/NFT.py +++ b/TonTools/Contracts/NFT.py @@ -94,7 +94,6 @@ def __init__(self, data, provider): self.owner = data['owner'] else: super().__init__(data['address'], provider) - self.address = data self.full_data = False def is_full(self): From 1d59be48a55122de13cfa151ca9f43cb27be4550 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 09:03:15 +0200 Subject: [PATCH 07/26] Upgrade logic to interact with API v2, refactor code --- TonTools/Providers/TonApiClient.py | 175 ++++++++++------------------- 1 file changed, 57 insertions(+), 118 deletions(-) diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index fe1deae..05330a2 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -34,10 +34,10 @@ def __init__(self, self.form = addresses_form if testnet: self.testnet = True - self.base_url = 'https://testnet.tonapi.io/v1/' + self.base_url = 'https://testnet.tonapi.io/v2' else: self.testnet = False - self.base_url = 'https://tonapi.io/v1/' + self.base_url = 'https://tonapi.io/v2' if key: self.headers = { 'Authorization': 'Bearer ' + key @@ -56,80 +56,50 @@ def _process_address(self, address): async def get_nft_owner(self, nft_address: str): async with aiohttp.ClientSession() as session: - url = self.base_url + 'nft/getItems' - params = { - 'addresses': [nft_address] - } - response = await session.get(url=url, params=params, headers=self.headers) + url = f'{self.base_url}/nfts/{nft_address}' + response = await session.get(url=url, headers=self.headers) response = await process_response(response) - item = response['nft_items'][0] - if 'sale' in item: - return self._process_address(item['sale']['owner']['address']) - return Wallet(self, self._process_address(item['owner']['address'])) + if 'sale' in response: + return Wallet(self, self._process_address(response['sale']['owner']['address'])) + return Wallet(self, self._process_address(response['owner']['address'])) async def get_nft_items(self, nft_addresses: list): result = [] async with aiohttp.ClientSession() as session: - url = self.base_url + 'nft/getItems' + url = f'{self.base_url}/nfts/_bulk' params = { - 'addresses': ','.join(nft_addresses) + 'account_ids': nft_addresses } - response = await session.get(url=url, params=params, headers=self.headers) + response = await session.post(url=url, json=params, headers=self.headers) response = await process_response(response) for item in response['nft_items']: - temp = { - 'address': self._process_address(item['address']), - 'collection': { - 'address': self._process_address(item['collection']['address']), - 'name': item['collection']['name'], - }, - 'collection_address': self._process_address(item['collection']['address']), - 'index': item['index'], - 'metadata': item['metadata'], - 'owner': self._process_address(item['owner']['address']) - } + item['address'] = self._process_address(item['address']) + item['collection']['address'] = self._process_address(item['collection']['address']) + item['owner']['address'] = self._process_address(item['owner']['address']) + item['collection_address'] = item['collection']['address'] if 'sale' in item: - temp['sale'] = { - 'address': self._process_address(item['sale']['address']), - 'market': { - 'address': self._process_address(item['sale']['market']['address']), - 'name': item['sale']['market']['name'] - }, - 'owner': self._process_address(item['sale']['owner']['address']), - 'price': { - 'token_name': item['sale']['price']['token_name'], - 'value': item['sale']['price']['value'], - } - } - result.append(NftItem(temp, self)) + item['sale']['address'] = self._process_address(item['sale']['address']) + item['sale']['market']['address'] = self._process_address(item['sale']['market']['address']) + item['sale']['owner'] = self._process_address(item['sale']['owner']['address']) + result.append(NftItem(item, self)) return result async def get_collection(self, collection_address): async with aiohttp.ClientSession() as session: - url = self.base_url + 'nft/getCollection' - params = { - 'account': collection_address - } - response = await session.get(url=url, params=params, headers=self.headers) + url = f'{self.base_url}/nfts/collections/{collection_address}' + response = await session.get(url=url, headers=self.headers) response = await process_response(response) - result = { - 'address': self._process_address(response['address']), - 'metadata': response['metadata'], - 'next_item_index': response['next_item_index'], - 'owner': self._process_address(response['owner']['address']) - } - return NftCollection(result, self) + if 'owner' in response: + response['owner']['address'] = self._process_address(response['owner']['address']) + return NftCollection(response, self) - async def get_collection_items(self, collection: NftCollection, limit_per_one_request=1000): + async def get_collection_items(self, collection: NftCollection, limit: int = 10**9, limit_per_one_request=1000): async with aiohttp.ClientSession() as session: - if not limit_per_one_request: - limit_per_one_request = 1000 - url = self.base_url + 'nft/searchItems' + url = f'{self.base_url}/nfts/collections/{collection.address}/items' i = 0 items = [] - while True: + while len(items) < limit: params = { - 'collection': collection.address, 'limit': limit_per_one_request, 'offset': i } @@ -139,66 +109,44 @@ async def get_collection_items(self, collection: NftCollection, limit_per_one_re if len(response['nft_items']) < limit_per_one_request: break i += limit_per_one_request - return items + return items[:limit] - async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one_request: int = 100): + async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one_request: int = 100, before_lt: int = 0, after_lt: int = 0): async with aiohttp.ClientSession() as session: - url = self.base_url + 'blockchain/getTransactions' + url = f'{self.base_url}/blockchain/accounts/{address}/transactions' transactions = [] - params = { - 'account': address, - 'limit': limit_per_one_request, - 'minLt': 0 - } - response = await session.get(url=url, params=params, headers=self.headers) - response = await process_response(response) - transactions += response['transactions'] - while len(response['transactions']) == limit_per_one_request and len(transactions) < limit: + while len(transactions) < limit: params = { - 'account': address, 'limit': limit_per_one_request, - 'maxLt': transactions[-1]['lt'], - 'minLt': 0 + **({'before_lt': before_lt} if before_lt else {}), + **({'after_lt': after_lt} if after_lt else {}) } response = await session.get(url=url, params=params, headers=self.headers) response = await process_response(response) - transactions += response['transactions'][1:] + transactions.extend(response['transactions']) + before_lt = transactions[-1]['lt'] + if len(response['transactions']) < limit_per_one_request: + break result = [] for tr in transactions: - temp = { - 'utime': tr['utime'], - 'fee': tr['fee'], - 'data': tr['data'], - 'hash': base64.b64encode(s=bytearray.fromhex(tr['hash'])).decode(), - 'lt': tr['lt'], - 'in_msg': { - 'created_lt': tr['in_msg']['created_lt'], - 'source': self._process_address(tr['in_msg']['source']['address']) if 'source' in tr['in_msg'] else '', - 'destination': self._process_address(tr['in_msg']['destination']['address']) if 'destination' in tr['in_msg'] else '', - 'value': tr['in_msg']['value'], - 'msg_data': tr['in_msg']['msg_data'] - }, - 'out_msgs': [ - { - 'created_lt': out_msg['created_lt'], - 'source': self._process_address(out_msg['source']['address']) if 'source' in out_msg else '', - 'destination': self._process_address(out_msg['destination']['address']) if 'destination' in out_msg else '', - 'value': out_msg['value'], - 'msg_data': out_msg['msg_data'] - } - for out_msg in tr['out_msgs'] - ] - } - result.append(Transaction(temp)) + tr['data'] = None + tr['status'] = tr['success'] + tr['fee'] = tr['total_fees'] + tr['hash'] = base64.b64encode(s=bytearray.fromhex(tr['hash'])).decode() + tr['in_msg']['source'] = self._process_address(tr['in_msg']['source']['address']) if 'source' in tr['in_msg'] else '' + tr['in_msg']['destination'] = self._process_address(tr['in_msg']['destination']['address']) if 'destination' in tr['in_msg'] else '' + out_msgs = tr['out_msgs'] + for out_msg in out_msgs: + out_msg['source'] = self._process_address(out_msg['source']['address']) if 'source' in out_msg else '' + out_msg['destination'] = self._process_address(out_msg['destination']['address']) if 'destination' in out_msg else '' + tr['out_msgs'] = out_msgs + result.append(Transaction(tr)) return result[:limit] async def get_jetton_data(self, jetton_master_address: str): async with aiohttp.ClientSession() as session: - url = self.base_url + 'jetton/getInfo' - params = { - 'account': jetton_master_address - } - response = await session.get(url=url, params=params, headers=self.headers) + url = f'{self.base_url}/jettons/{jetton_master_address}' + response = await session.get(url=url, headers=self.headers) response = await process_response(response) result = response['metadata'] result['description'] = unicodedata.normalize("NFKD", result['description']) @@ -208,7 +156,7 @@ async def get_jetton_data(self, jetton_master_address: str): async def send_boc(self, boc): async with aiohttp.ClientSession() as session: - url = self.base_url + 'send/boc' + url = f'{self.base_url}/blockchain/message' data = { 'boc': boc } @@ -217,33 +165,24 @@ async def send_boc(self, boc): async def get_wallet_seqno(self, address: str): async with aiohttp.ClientSession() as session: - url = self.base_url + 'wallet/getSeqno' - params = { - 'account': address - } - response = await session.get(url=url, params=params, headers=self.headers) + url = f'{self.base_url}/wallet/{address}/seqno' + response = await session.get(url=url, headers=self.headers) response = await process_response(response) seqno = response['seqno'] return seqno async def get_balance(self, address: str): async with aiohttp.ClientSession() as session: - url = self.base_url + 'account/getInfo' - params = { - 'account': address - } - response = await session.get(url=url, params=params, headers=self.headers) + url = f'{self.base_url}/accounts/{address}' + response = await session.get(url=url, headers=self.headers) response = await process_response(response) balance = response['balance'] return int(balance) async def get_state(self, address: str): async with aiohttp.ClientSession() as session: - url = self.base_url + 'account/getInfo' - params = { - 'account': address - } - response = await session.get(url=url, params=params, headers=self.headers) + url = f'{self.base_url}/accounts/{address}' + response = await session.get(url=url, headers=self.headers) response = await process_response(response) state = response['status'] if state == 'empty' or state == 'uninit': From b6be1ac11e843b2a337f4b4f2d6dcf4163359c0a Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 09:06:09 +0200 Subject: [PATCH 08/26] Add helper function to fallback jetton data fields --- TonTools/Providers/utils.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/TonTools/Providers/utils.py b/TonTools/Providers/utils.py index 212a295..aac2d20 100644 --- a/TonTools/Providers/utils.py +++ b/TonTools/Providers/utils.py @@ -1,4 +1,4 @@ -import logging +import typing import unicodedata from base64 import b64decode import aiohttp @@ -14,17 +14,24 @@ def is_hex(s: str): return False +def _get_refs(callback, default: typing.Any = ''): + try: + return callback() + except IndexError: + return default + + def process_jetton_data(data): if not len(Cell.one_from_boc(b64decode(data)).refs): url = Cell.one_from_boc(b64decode(data)).bits.get_top_upped_array().decode().split('\x01')[-1] return url else: - symbol = Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[0].refs[1].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1] - desc1 = unicodedata.normalize("NFKD", Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1]) # Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[0].refs - desc2 = unicodedata.normalize("NFKD", Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[0].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1]) if len(Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[0].refs[0].refs) else '' - decimals = Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[1].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1] - name = Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[0].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1] - image = Cell.one_from_boc(b64decode(data)).refs[0].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1] + symbol = _get_refs(lambda: Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[0].refs[1].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1]) + desc1 = _get_refs(lambda: unicodedata.normalize("NFKD", Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1])) + desc2 = _get_refs(lambda: unicodedata.normalize("NFKD", Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[0].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1])) + decimals = _get_refs(lambda: Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[1].refs[1].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1], 0) + name = _get_refs(lambda: Cell.one_from_boc(b64decode(data)).refs[0].refs[1].refs[0].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1]) + image = _get_refs(lambda: Cell.one_from_boc(b64decode(data)).refs[0].refs[0].refs[0].bits.get_top_upped_array().decode().split('\x00')[-1]) return { 'name': name, 'description': desc1 + desc2, From 6305ad9c6a34c595947c2beef48f9ef3e3002366 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 09:23:33 +0200 Subject: [PATCH 09/26] Fix bugs, improve logic and UX, refactor code --- TonTools/Providers/LsClient.py | 217 +++++++++++++++++---------------- 1 file changed, 111 insertions(+), 106 deletions(-) diff --git a/TonTools/Providers/LsClient.py b/TonTools/Providers/LsClient.py index 2634614..f745ec3 100644 --- a/TonTools/Providers/LsClient.py +++ b/TonTools/Providers/LsClient.py @@ -1,8 +1,13 @@ +import logging +import typing import asyncio +import random from math import ceil +from pathlib import Path import aiohttp import base64 +import requests from tonsdk.boc import Cell from ton.utils.cell import read_address from tonsdk.utils import Address, bytes_to_b64str, b64str_to_bytes @@ -21,14 +26,14 @@ class LsClientError(BaseException): pass -class GetMethodError(LsClientError): +class GetMethodError(LsClientError, TVMExitCode): pass async def process_response(response: aiohttp.ClientResponse): try: response_dict = await response.json() - except: + except Exception: raise LsClientError(f'Failed to parse response: {response.text}') if response.status != 200: raise LsClientError(f'TonCenter failed with error: {response_dict["error"]}') @@ -37,18 +42,36 @@ async def process_response(response: aiohttp.ClientResponse): class LsClient(TonlibClient): - - def __init__(self, ls_index=0, - config='https://ton.org/global-config.json', - keystore=None, - workchain_id=0, + def __init__(self, ls_index: int = None, # None for random + cdll_path: typing.Union[str, Path] = None, + config: typing.Union[str, dict] = 'https://ton.org/global-config.json', + keystore: typing.Union[str, Path] = None, + workchain_id: int = 0, verbosity_level=0, default_timeout=10, addresses_form: str = AddressForm.USER_FRIENDLY ): + if not cdll_path: + logging.warning('You should provide a path to the tonlibjson library (.dll|.so|.dylib).\n' + 'It can be downloaded from https://github.com/ton-blockchain/ton/releases.\n' + 'Embedded binaries may be outdated and unsafe. Use them only for testing purposes.') + if isinstance(keystore, Path): + keystore = str(keystore) + if isinstance(cdll_path, Path): + cdll_path = str(cdll_path) + self.cdll_path = cdll_path + self.form = addresses_form super().__init__(ls_index, config, keystore, workchain_id, verbosity_level, default_timeout) TonlibClient.enable_unaudited_binaries() - self.form = addresses_form + + async def init(self): + if self.ls_index is None: + if isinstance(self.config, str): + # noinspection HttpUrlsUsage + if self.config.find('http://') == 0 or self.config.find('https://') == 0: + self.config: dict = requests.get(self.config).json() + self.ls_index = random.randrange(0, len(self.config['liteservers'])) + return await super().init_tonlib(self.cdll_path) def _process_address(self, address): if self.form == AddressForm.USER_FRIENDLY: @@ -61,7 +84,8 @@ async def run_get_method(self, method: str, address: str, stack: list): response = await account.run_get_method(method=method, stack=stack) if response.exit_code != 0: - raise GetMethodError(f'get method {method} for address {self._process_address(address)} exit code is {response.exit_code}') + logging.error(f'Failed to run method {method} on {address}. Exit code: {response.exit_code}') + raise GetMethodError(response.exit_code) return response.stack async def get_nft_owner(self, nft_address: str): @@ -120,62 +144,63 @@ async def _get_nft_sale(self, nft_address: str): owner_address = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[3].cell.bytes))).to_string()) try: data = await self.run_get_method(method='get_sale_data', address=owner_address, stack=[]) - if len(data) == 10: - market_address = read_address(Cell.one_from_boc(base64.b64decode(data[3].cell.bytes))).to_string() - market_name = markets_adresses.get(market_address, '') - market_address = self._process_address(market_address) - real_owner = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[5].cell.bytes))).to_string()) - price = int(data[6].number.number) - return { - 'address': owner_address, - 'market': { - 'address': market_address, - 'name': market_name - }, - 'owner': real_owner, - 'price': { - 'token_name': 'TON', - 'value': price, - } + except GetMethodError as e: + logging.info(f'Failed to get sale data for {owner_address}: {e.message}') + return False + if len(data) == 10: + market_address = read_address(Cell.one_from_boc(base64.b64decode(data[3].cell.bytes))).to_string() + market_name = markets_adresses.get(market_address, '') + market_address = self._process_address(market_address) + real_owner = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[5].cell.bytes))).to_string()) + price = int(data[6].number.number) + return { + 'address': owner_address, + 'market': { + 'address': market_address, + 'name': market_name + }, + 'owner': real_owner, + 'price': { + 'token_name': 'TON', + 'value': price, } - elif len(data) == 7: - market_address = read_address(Cell.one_from_boc(base64.b64decode(data[0].cell.bytes))).to_string() - market_name = markets_adresses.get(market_address, '') - market_address = self._process_address(market_address) - real_owner = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[2].cell.bytes))).to_string()) - price = int(data[3].number.number) - return { - 'address': owner_address, - 'market': { - 'address': market_address, - 'name': market_name - }, - 'owner': real_owner, - 'price': { - 'token_name': 'TON', - 'value': price, - } + } + elif len(data) == 7: + market_address = read_address(Cell.one_from_boc(base64.b64decode(data[0].cell.bytes))).to_string() + market_name = markets_adresses.get(market_address, '') + market_address = self._process_address(market_address) + real_owner = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[2].cell.bytes))).to_string()) + price = int(data[3].number.number) + return { + 'address': owner_address, + 'market': { + 'address': market_address, + 'name': market_name + }, + 'owner': real_owner, + 'price': { + 'token_name': 'TON', + 'value': price, } - elif len(data) >= 11: - market_address = read_address(Cell.one_from_boc(base64.b64decode(data[3].cell.bytes))).to_string() - market_name = markets_adresses.get(market_address, '') - market_address = self._process_address(market_address) - real_owner = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[5].cell.bytes))).to_string()) - price = max(int(data[6].number.number), int(data[16].number.number)) if len(data) >= 16 else int(data[6].number.number) - return { - 'address': owner_address, - 'market': { - 'address': market_address, - 'name': market_name - }, - 'owner': real_owner, - 'price': { - 'token_name': 'TON', - 'value': price, - } + } + elif len(data) >= 11: + market_address = read_address(Cell.one_from_boc(base64.b64decode(data[3].cell.bytes))).to_string() + market_name = markets_adresses.get(market_address, '') + market_address = self._process_address(market_address) + real_owner = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[5].cell.bytes))).to_string()) + price = max(int(data[6].number.number), int(data[16].number.number)) if len(data) >= 16 else int(data[6].number.number) + return { + 'address': owner_address, + 'market': { + 'address': market_address, + 'name': market_name + }, + 'owner': real_owner, + 'price': { + 'token_name': 'TON', + 'value': price, } - except GetMethodError: - return False + } async def get_collection(self, collection_address): data = await self.run_get_method(method='get_collection_data', address=collection_address, stack=[]) @@ -195,7 +220,7 @@ async def get_collection_items(self, collection: NftCollection, limit_per_one_re if not collection.is_full(): await collection.update() if not limit_per_one_request: - items = await asyncio.gather(*[self.run_get_method(address=collection.address, method='get_nft_address_by_index', stack=[{"@type": "tvm.stackEntryNumber","number": {"@type": "tvm.numberDecimal","number": str(i)}}]) for i in range(collection.next_item_index)]) + items = await asyncio.gather(*[self.run_get_method(address=collection.address, method='get_nft_address_by_index', stack=[{"@type": "tvm.stackEntryNumber", "number": {"@type": "tvm.numberDecimal", "number": str(i)}}]) for i in range(collection.next_item_index)]) else: items = [] for p in range(ceil(collection.next_item_index / limit_per_one_request)): @@ -206,56 +231,36 @@ async def get_collection_items(self, collection: NftCollection, limit_per_one_re result.append(NftItem(self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[0].cell.bytes)))), self)) return result - async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one_request: int = 100): + async def get_transactions(self, address: str, limit: int = 10**9): account = await self.find_account(address) - transactions = [] - trs = await account.get_transactions(limit=limit_per_one_request) - for tr in trs: - transactions.append(tr.to_json()) - while len(trs) == limit_per_one_request and len(transactions) < limit: - trs = await account.get_transactions(from_transaction_lt=trs[-1].to_json()['transaction_id']['lt'], - from_transaction_hash=trs[-1].to_json()['transaction_id']['hash'], - limit=limit) - for tr in trs[1:]: - transactions.append(tr.to_json()) + # ton lib method does all the work of getting the required tx amount in a loop + transactions = map(lambda x: x.to_json(), await account.get_transactions(limit=limit)) result = [] for tr in transactions: - temp = { - 'utime': tr['utime'], - 'fee': tr['fee'], - 'data': tr['data'], - 'hash': tr['transaction_id']['hash'], - 'lt': tr['transaction_id']['lt'], - 'in_msg': { - 'created_lt': tr['in_msg']['created_lt'], - 'source': self._process_address(tr['in_msg']['source']['account_address']) if tr['in_msg']['source']['account_address'] else '', - 'destination': self._process_address(tr['in_msg']['destination']['account_address']) if tr['in_msg']['destination']['account_address'] else '', - 'value': tr['in_msg']['value'], - 'msg_data': tr['in_msg']['msg_data']['text'] if 'text' in tr['in_msg']['msg_data'] else tr['in_msg']['msg_data']['body'] - }, - 'out_msgs': [ - { - 'created_lt': out_msg['created_lt'], - 'source': self._process_address(out_msg['source']['account_address']) if out_msg['source']['account_address'] else '', - 'destination': self._process_address(out_msg['destination']['account_address']) if out_msg['destination']['account_address'] else '', - 'value': out_msg['value'], - 'msg_data': out_msg['msg_data']['text'] if 'text' in out_msg['msg_data'] else out_msg['msg_data']['body'] - } - for out_msg in tr['out_msgs'] - ] - } - result.append(Transaction(temp)) + tr['hash'] = tr['transaction_id']['hash'] + tr['lt'] = tr['transaction_id']['lt'] + tr['in_msg']['source'] = self._process_address(tr['in_msg']['source']['account_address']) if tr['in_msg']['source']['account_address'] else '' + tr['in_msg']['destination'] = self._process_address(tr['in_msg']['destination']['account_address']) if tr['in_msg']['destination']['account_address'] else '' + tr['in_msg']['msg_data'] = tr['in_msg']['msg_data']['text'] if 'text' in tr['in_msg']['msg_data'] else tr['in_msg']['msg_data']['body'] + out_msgs = tr['out_msgs'] + for out_msg in out_msgs: + out_msg['source'] = self._process_address(out_msg['source']['account_address']) if out_msg['source']['account_address'] else '' + out_msg['destination'] = self._process_address(out_msg['destination']['account_address']) if out_msg['destination']['account_address'] else '' + out_msg['msg_data'] = out_msg['msg_data']['text'] if 'text' in out_msg['msg_data'] else out_msg['msg_data']['body'] + tr['out_msgs'] = out_msgs + result.append(Transaction(tr)) return result[:limit] async def get_jetton_data(self, jetton_master_address: str): data = await self.run_get_method(method='get_jetton_data', address=jetton_master_address, stack=[]) - result = process_jetton_data(data[3].cell.bytes) if isinstance(process_jetton_data(data[3].cell.bytes), dict) else await get(process_jetton_data(data[3].cell.bytes)) + processed = process_jetton_data(data[3].cell.bytes) + result = processed if isinstance(processed, dict) else await get(processed) result['address'] = self._process_address(jetton_master_address) result['supply'] = int(data[0].number.number) return Jetton(result, self) - async def send_boc(self, boc): + async def send_boc(self, boc, **kwargs): response = await super().send_boc(b64str_to_bytes(boc)) return response @@ -292,17 +297,17 @@ async def get_jetton_wallet_address(self, jetton_master_address: str, owner_addr "bytes": bytes_to_b64str(cell.to_boc(False)) } }] - data = await self.run_get_method(address=jetton_master_address, method='get_wallet_address',stack=request_stack) - jetton_wallet_address = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[0].cell.bytes))).to_string()) + data = await self.run_get_method(address=jetton_master_address, method='get_wallet_address', stack=request_stack) + jetton_wallet_address = self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[0].slice.bytes))).to_string()) return jetton_wallet_address async def get_jetton_wallet(self, jetton_wallet_address: str): - data = await self.run_get_method(address=jetton_wallet_address, method='get_wallet_data',stack=[]) + data = await self.run_get_method(address=jetton_wallet_address, method='get_wallet_data', stack=[]) wallet = { 'address': jetton_wallet_address, 'balance': int(data[0].number.number), - 'owner': self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[1].cell.bytes)))), - 'jetton_master_address': self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[2].cell.bytes)))), + 'owner': self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[1].slice.bytes)))), + 'jetton_master_address': self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[2].slice.bytes)))), 'jetton_wallet_code': data[3].cell.bytes, } return JettonWallet(wallet, self) From a6a843b1ba016c618b97756d5811c53dc31d3905 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 09:27:34 +0200 Subject: [PATCH 10/26] Add SafeLsClient --- TonTools/Providers/SafeLsClient.py | 102 +++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 TonTools/Providers/SafeLsClient.py diff --git a/TonTools/Providers/SafeLsClient.py b/TonTools/Providers/SafeLsClient.py new file mode 100644 index 0000000..dc03ced --- /dev/null +++ b/TonTools/Providers/SafeLsClient.py @@ -0,0 +1,102 @@ +import logging +import typing +import inspect +import random +from pathlib import Path + +import requests + +from .DtonClient import DtonClient +from .LsClient import LsClient +from .TonApiClient import TonApiClient +from .TonCenterClient import TonCenterClient +from ..Contracts.NFT import NftCollection +from ..Enums.Address import AddressForm + + +class SafeLsClient: + ls_client: LsClient + + def __init__(self, + fallback_client: typing.Union[DtonClient, TonApiClient, TonCenterClient], + ls_index: int = None, # None for random + cdll_path: typing.Union[str, Path] = None, + config: typing.Union[str, dict] = 'https://ton.org/global-config.json', + keystore: typing.Union[str, Path] = None, + workchain_id: int = 0, + verbosity_level=0, + default_timeout=10, + addresses_form: str = AddressForm.USER_FRIENDLY, + ): + self.fallback = fallback_client + self.ls_index = ls_index + self.cdll_path = cdll_path + self.config = config + self.keystore = keystore + self.workchain_id = workchain_id + self.verbosity_level = verbosity_level + self.default_timeout = default_timeout + self.addresses_form = addresses_form + self._next_ls = False + + async def init(self): + if isinstance(self.config, str): + # noinspection HttpUrlsUsage + if self.config.find('http://') == 0 or self.config.find('https://') == 0: + self.config = requests.get(self.config).json() + self.ls_index = random.randrange(0, len(self.config['liteservers'])) if self.ls_index is None else self.ls_index + + self.ls_client = LsClient(self.ls_index, self.cdll_path, self.config, self.keystore, self.workchain_id, + self.verbosity_level, self.default_timeout, self.addresses_form) + await self.ls_client.init() + + async def next_ls(self): + self.ls_index = (self.ls_index + 1) % len(self.config['liteservers']) + self.ls_client.ls_index = self.ls_index + await self.ls_client.init() + + async def _execute(self, method: str, *args): + try: + if self._next_ls: + self._next_ls = False + await self.next_ls() + return await getattr(self.ls_client, method)(*args) + except Exception as e: + logging.warning(f'Error in {method}: {e}\nTrying the fallback client and switching to another LS for the next request') + self._next_ls = True + method = getattr(self.fallback, method) + argc = len(inspect.signature(method).parameters) + return await method(*args[:argc]) + + async def get_nft_owner(self, nft_address: str): + return await self._execute(self.get_nft_owner.__name__, nft_address) + + async def get_nft_items(self, nft_addresses: list): + return await self._execute(self.get_nft_items.__name__, nft_addresses) + + async def get_collection(self, collection_address): + return await self._execute(self.get_collection.__name__, collection_address) + + async def get_collection_items(self, collection: NftCollection, limit_per_one_request=0): + return await self._execute(self.get_collection_items.__name__, collection, limit_per_one_request) + + async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one_request: int = 100): + return await self._execute(self.get_transactions.__name__, address, limit, limit_per_one_request) + + async def get_jetton_data(self, jetton_master_address: str): + return await self._execute(self.get_jetton_data.__name__, jetton_master_address) + + async def get_wallet_seqno(self, address: str): + return await self._execute(self.get_wallet_seqno.__name__, address) + + async def get_balance(self, address: str): + return await self._execute(self.get_balance.__name__, address) + + async def get_state(self, address: str): + return await self._execute(self.get_state.__name__, address) + + async def get_jetton_wallet_address(self, jetton_master_address: str, owner_address: str): + return await self._execute(self.get_jetton_wallet_address.__name__, jetton_master_address, owner_address) + + async def get_jetton_wallet(self, jetton_wallet_address: str): + return await self._execute(self.get_jetton_wallet.__name__, jetton_wallet_address) From dece33ca579cb35299f353074f5cbf4a233ffef9 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 09:29:54 +0200 Subject: [PATCH 11/26] Update requirements and version --- setup.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a772a9e..85bdbd6 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,18 @@ from setuptools import setup -import setuptools -requirements = ["tonsdk>=1.0.13", "ton>=0.26", "aiohttp>=3.8.1", "setuptools>=65.3.0", "requests>=2.28.1", "pytonlib>=0.0.46", "graphql-query==1.0.3"] +requirements = [ + "setuptools>=69.5", + "tonsdk~=1.0.13", + "ton~=0.26", + "aiohttp~=3.9", + "requests~=2.31", + "pytonlib~=0.0.46", + "graphql-query~=1.3", +] setup( name='TonTools', - version='2.1.2', + version='2.2.0', packages=['TonTools', 'TonTools/Contracts', 'TonTools/Providers'], url='', license='MIT License', From eac910685f4c27274e2e0134df432dbff5b5bf99 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 09:31:53 +0200 Subject: [PATCH 12/26] Update examples --- examples/Jetton/get_jetton_master_data.py | 19 +++++++++++++------ examples/Jetton/get_jetton_wallet_data.py | 6 ++---- examples/wallet_creation_and_deployment.py | 12 ++++++++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/examples/Jetton/get_jetton_master_data.py b/examples/Jetton/get_jetton_master_data.py index a111edf..e6f948f 100644 --- a/examples/Jetton/get_jetton_master_data.py +++ b/examples/Jetton/get_jetton_master_data.py @@ -2,19 +2,26 @@ from TonTools import * -# jetton address -JETTON_MASTER = 'EQBl3gg6AAdjgjO2ZoNU5Q5EzUIl8XMNZrix8Z5dJmkHUfxI' +app_dir: Path = Path(__file__).parent.parent.parent + async def main(): - client = TonCenterClient(orbs_access=True) + fallback_client = TonApiClient('your_key') + client = SafeLsClient(fallback_client, cdll_path=app_dir / 'tonlibjson.dll', addresses_form=AddressForm.USER_FRIENDLY) + + await client.init() - jetton_master_data = await client.get_jetton_data(JETTON_MASTER) + jetton_master_data = await client.get_jetton_data(JettonMasterAddress.GRAM) + # or + # jetton_master_data = await client.get_jetton_data(getattr(JettonMasterAddress, '@BTC25')) + # or + # jetton_master_data = await client.get_jetton_data('custom_jetton_master_address') - # jetton_master = Jetton(JETTON_MASTER, client) + # jetton_master = Jetton(JettonMasterAddress.usdt, client) # await jetton_master.update() # jetton_master_data = jetton_master print(jetton_master_data) if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/Jetton/get_jetton_wallet_data.py b/examples/Jetton/get_jetton_wallet_data.py index 6933320..7f22c17 100644 --- a/examples/Jetton/get_jetton_wallet_data.py +++ b/examples/Jetton/get_jetton_wallet_data.py @@ -2,19 +2,17 @@ from TonTools import * -# jetton address -JETTON_MASTER = 'EQBl3gg6AAdjgjO2ZoNU5Q5EzUIl8XMNZrix8Z5dJmkHUfxI' async def main(): client = TonCenterClient(orbs_access=True) jetton_wallet_data = await client.get_jetton_wallet('Jetton wallet contract address') - # jetton_wallet = await Jetton(JETTON_MASTER, client).get_jetton_wallet(owner_address='EQAc75QKQd4BY4tlBoGJgGbw-z1yIp9Sz9tvJ8yDRIpGc5bE') + # jetton_wallet = await Jetton(JettonMasterAddress.GRAM, client).get_jetton_wallet(owner_address='EQAc75QKQd4BY4tlBoGJgGbw-z1yIp9Sz9tvJ8yDRIpGc5bE') # await jetton_wallet.update() # jetton_wallet_data = jetton_wallet print(jetton_wallet_data) if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/examples/wallet_creation_and_deployment.py b/examples/wallet_creation_and_deployment.py index fee99e9..a3681de 100644 --- a/examples/wallet_creation_and_deployment.py +++ b/examples/wallet_creation_and_deployment.py @@ -1,6 +1,10 @@ import asyncio +from pathlib import Path -from TonTools import * +from TonTools import LsClient, Wallet, Address + + +app_dir: Path = Path(__file__).parent.parent async def main(): @@ -8,14 +12,14 @@ async def main(): # client = TonCenterClient(base_url='http://127.0.0.1:80/') - client = LsClient(ls_index=2, default_timeout=20) - await client.init_tonlib() + client = LsClient(ls_index=1, cdll_path=app_dir / 'tonlibjson.dll') + await client.init() my_wallet_mnemonics = [] my_wallet = Wallet(provider=client, mnemonics=my_wallet_mnemonics, version='v4r2') my_wallet_nano_balance = await my_wallet.get_balance() - if my_wallet_nano_balance / 10**9 >= 0.03: + if client.from_nano(my_wallet_nano_balance) >= 0.03: new_wallet = Wallet(provider=client) print(new_wallet.address, new_wallet.mnemonics, my_wallet_nano_balance) # EQBcMK8CBrZKfSYdvT8FDVo1TxZV_d3Lz-xPyGp8c7mUacko ['federal', 'memory', 'scare', 'exact', 'extend', 'rain', 'private', 'ribbon', 'inspire', 'capital', 'arrow', 'glimpse', 'toy', 'double', 'man', 'speak', 'imitate', 'hint', 'dinner', 'oblige', 'rather', 'answer', 'unfold', 'small'] 496348289 non_bounceable_new_wallet_address = Address(new_wallet.address).to_string(True, True, False) From 8f4e64fa0c6295f54764fbac2e5f82f6334507e1 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 10:09:56 +0200 Subject: [PATCH 13/26] Update README --- README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cbdfce7..9e449d8 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,10 @@ print((await contract.get_transactions(limit=10))[-1].out_msgs[0].destination) To initialize LsClient: ```python -client = LsClient(ls_index=2, default_timeout=30, addresses_form='user_friendly') -await client.init_tonlib() +client = LsClient(ls_index=2, default_timeout=30, addresses_form=AddressForm.USER_FRIENDLY) +await client.init() ``` -*LsClient* is some more advanced, for e.g. you may need to compile binaries to use it. +**LsClient** is some more advanced, for e.g. you may need to compile binaries to use it. ### DtonClient [Dton](https://docs.dton.io/dton) is a high level indexing GraphQL Api. @@ -73,7 +73,7 @@ To initialize DtonClient: ```python client = DtonClient( key: str = None, # dton api key - addresses_form='user_friendly', # addresses_form could be 'raw' or 'user_friendly' + addresses_form=AddressForm.USER_FRIENDLY, # addresses_form could be RAW or USER_FRIENDLY testnet=False, # if testnet, all addresses will be in testnet form and base url will start with https://testnet.dton.io/ private_graphql=False # you can use private_graphql if you have an api key ) @@ -81,11 +81,7 @@ client = DtonClient( **_Note:_** Dton currently doesn't support sending messages to blockchain, so you can't, for example, transfer toncoins using this provider -### TonApiClient - currently v1 - -**_Note:_** in future TonApiClient will be overwritten to use v2 methods -and current TonApiClient will be renamed into TonApiClientV1, because tonapi v1 endpoints -soon will become unsupported +### TonApiClient v2 [TonApi](https://tonapi.io/swagger-ui) is a high level indexing Api. @@ -97,7 +93,17 @@ client = TonApiClient(api_key, addresses_form) you should use it if you want to scan a lot of _transactions_ and _contracts_ +### SafeLsClient +**SafeLsClient** is a wrapper for **LsClient** which accepts a fallback client. +Lite servers can be unstable, so if **LsClient** fails to get data, **SafeLsClient** +will try to get data from the fallback client and change the `ls_index` to the next one for future requests. +```python +fallback_client = TonApiClient(api_key) +client = SafeLsClient(fallback_client, cdll_path=app_dir / 'tonlibjson.dll') +await client.init() +``` +**_Note:_** Provide a fallback client that has methods you need to use. ## Contracts @@ -144,7 +150,7 @@ print(sale.price_value, sale.owner) # 200000000000 EQBZVBXBpirFPOQ5Wmgi5Es2hDCR There are `Jetton and JettonWallet` classes. ```python client = LsClient(ls_index=2, default_timeout=30) -await client.init_tonlib() +await client.init() jetton = Jetton('EQBl3gg6AAdjgjO2ZoNU5Q5EzUIl8XMNZrix8Z5dJmkHUfxI', provider=client) print(jetton) # Jetton({"address": "EQBl3gg6AAdjgjO2ZoNU5Q5EzUIl8XMNZrix8Z5dJmkHUfxI"}) @@ -171,7 +177,7 @@ Currently there is only `Wallet` class (will add HighLoadWallet and MultiSigWall You can create new wallet just calling `Wallet(provider, wallet_version)`, check existing wallet `Wallet(provider, address)` or enter wallet `Wallet(provider, mnemonics, wallet_version)` ```python client = LsClient(ls_index=2, default_timeout=20) -await client.init_tonlib() +await client.init() my_wallet_mnemonics = [] my_wallet = Wallet(provider=client, mnemonics=my_wallet_mnemonics, version='v4r2') From 47db4a8417e312405d403ae6646b8325e9767a34 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 11:06:23 +0200 Subject: [PATCH 14/26] Fix crash in the absence of a description --- TonTools/Providers/TonApiClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index 05330a2..70bda8a 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -149,7 +149,7 @@ async def get_jetton_data(self, jetton_master_address: str): response = await session.get(url=url, headers=self.headers) response = await process_response(response) result = response['metadata'] - result['description'] = unicodedata.normalize("NFKD", result['description']) + result['description'] = unicodedata.normalize("NFKD", result['description']) if 'description' in result else '' result['address'] = self._process_address(result['address']) result['supply'] = response['total_supply'] return Jetton(result, self) From 033d01499042ab3e347df1975b54b238af67b680 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Thu, 25 Apr 2024 11:31:57 +0200 Subject: [PATCH 15/26] Fix typo --- TonTools/Providers/TonApiClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index 70bda8a..fd4736f 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -90,7 +90,7 @@ async def get_collection(self, collection_address): response = await session.get(url=url, headers=self.headers) response = await process_response(response) if 'owner' in response: - response['owner']['address'] = self._process_address(response['owner']['address']) + response['owner'] = self._process_address(response['owner']['address']) return NftCollection(response, self) async def get_collection_items(self, collection: NftCollection, limit: int = 10**9, limit_per_one_request=1000): From 8c44038fa7ed04530f0c9dcff4cc95d1535d75a0 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sat, 27 Apr 2024 00:11:22 +0200 Subject: [PATCH 16/26] Return last argument due to method's dependencies --- TonTools/Providers/LsClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TonTools/Providers/LsClient.py b/TonTools/Providers/LsClient.py index f745ec3..b577d73 100644 --- a/TonTools/Providers/LsClient.py +++ b/TonTools/Providers/LsClient.py @@ -231,7 +231,7 @@ async def get_collection_items(self, collection: NftCollection, limit_per_one_re result.append(NftItem(self._process_address(read_address(Cell.one_from_boc(base64.b64decode(data[0].cell.bytes)))), self)) return result - async def get_transactions(self, address: str, limit: int = 10**9): + async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one_request: int = 100): account = await self.find_account(address) # ton lib method does all the work of getting the required tx amount in a loop transactions = map(lambda x: x.to_json(), await account.get_transactions(limit=limit)) From 9f840ec021f33238e1be88b1d9a64b0adc73ae58 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sat, 27 Apr 2024 00:12:15 +0200 Subject: [PATCH 17/26] Add required methods --- TonTools/Providers/SafeLsClient.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/TonTools/Providers/SafeLsClient.py b/TonTools/Providers/SafeLsClient.py index dc03ced..0b9e87a 100644 --- a/TonTools/Providers/SafeLsClient.py +++ b/TonTools/Providers/SafeLsClient.py @@ -55,18 +55,29 @@ async def next_ls(self): self.ls_client.ls_index = self.ls_index await self.ls_client.init() - async def _execute(self, method: str, *args): + async def _run_method(self, client, method, args, kwargs): + method = getattr(client, method) + params = inspect.signature(method).parameters + args = args[:len(params)] + kwargs = {k: v for k, v in kwargs.items() if k in params} + return await method(*args, **kwargs) + + async def _execute(self, _method: str, *args, **kwargs): try: if self._next_ls: self._next_ls = False await self.next_ls() - return await getattr(self.ls_client, method)(*args) + return await self._run_method(self.ls_client, _method, args, kwargs) except Exception as e: - logging.warning(f'Error in {method}: {e}\nTrying the fallback client and switching to another LS for the next request') + logging.warning(f'Error in {_method}: {e}\nTrying the fallback client and switching to another LS for the next request') self._next_ls = True - method = getattr(self.fallback, method) - argc = len(inspect.signature(method).parameters) - return await method(*args[:argc]) + return await self._run_method(self.fallback, _method, args, kwargs) + + def _process_address(self, address): + return self.ls_client._process_address(address) + + async def run_get_method(self, method: str, address: str, stack: list): + return await self._execute(self.run_get_method.__name__, method=method, address=address, stack=stack) async def get_nft_owner(self, nft_address: str): return await self._execute(self.get_nft_owner.__name__, nft_address) From 6c30736b66d472d75661cdfda64ddc75a7fde604 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sat, 27 Apr 2024 00:13:03 +0200 Subject: [PATCH 18/26] Add transactions length check --- TonTools/Providers/TonApiClient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index fd4736f..a388770 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -124,7 +124,8 @@ async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one response = await session.get(url=url, params=params, headers=self.headers) response = await process_response(response) transactions.extend(response['transactions']) - before_lt = transactions[-1]['lt'] + if transactions: + before_lt = transactions[-1]['lt'] if len(response['transactions']) < limit_per_one_request: break result = [] From d696fa49d93a6d92bc3f90ab01f1a3e356e3c292 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sat, 27 Apr 2024 00:13:22 +0200 Subject: [PATCH 19/26] Add Enums package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85bdbd6..d3a07cd 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='TonTools', version='2.2.0', - packages=['TonTools', 'TonTools/Contracts', 'TonTools/Providers'], + packages=['TonTools', 'TonTools/Contracts', 'TonTools/Providers', 'TonTools/Enums'], url='', license='MIT License', author='yungwine', From b8ee9b6018cc7db17b84b59a83e49f693ca21a46 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sat, 27 Apr 2024 06:42:11 +0200 Subject: [PATCH 20/26] Add required methods --- TonTools/Providers/SafeLsClient.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TonTools/Providers/SafeLsClient.py b/TonTools/Providers/SafeLsClient.py index 0b9e87a..baae905 100644 --- a/TonTools/Providers/SafeLsClient.py +++ b/TonTools/Providers/SafeLsClient.py @@ -97,6 +97,9 @@ async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one async def get_jetton_data(self, jetton_master_address: str): return await self._execute(self.get_jetton_data.__name__, jetton_master_address) + async def send_boc(self, boc): + return await self._execute(self.send_boc.__name__, boc) + async def get_wallet_seqno(self, address: str): return await self._execute(self.get_wallet_seqno.__name__, address) From dfc0305fd60584eb422bab176fecf35a5d4c651c Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sun, 28 Apr 2024 04:08:57 +0200 Subject: [PATCH 21/26] Add additional arguments to transfer_jetton, refactor code --- TonTools/Contracts/Wallet.py | 49 +++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/TonTools/Contracts/Wallet.py b/TonTools/Contracts/Wallet.py index a7253bc..8c673b9 100644 --- a/TonTools/Contracts/Wallet.py +++ b/TonTools/Contracts/Wallet.py @@ -49,7 +49,16 @@ async def transfer_ton(self, destination_address: str, amount: float, message: s response = await self.provider.send_boc(boc) return response - async def transfer_jetton_by_jetton_wallet(self, destination_address: str, jetton_wallet: str, jettons_amount: float, fee: float = 0.06, decimals: int = 9): + async def transfer_jetton_by_jetton_wallet(self, + destination_address: str, + jetton_wallet: str, + jettons_amount: float, + fee: float = 0.06, + decimals: int = 9, + forward_amount: float = 0.0, + comment: str = '', + response_address: str = None + ): """ Better to use .transfer_jetton(). """ @@ -59,7 +68,10 @@ async def transfer_jetton_by_jetton_wallet(self, destination_address: str, jetto seqno = await self.get_seqno() body = tonsdk.contract.token.ft.JettonWallet().create_transfer_body( Address(destination_address), - jettons_amount * 10**decimals + jettons_amount * 10**decimals, + forward_amount * 10**decimals, + b'\x00' * 4 + comment.encode() if comment else None, + Address(response_address) if response_address else None ) query = wallet.create_transfer_message( jetton_wallet, @@ -72,29 +84,32 @@ async def transfer_jetton_by_jetton_wallet(self, destination_address: str, jetto response = await self.provider.send_boc(jettons_boc) return response - async def transfer_jetton(self, destination_address: str, jetton_master_address: str, jettons_amount: float, fee: float = 0.06): + async def transfer_jetton(self, + destination_address: str, + jetton_master_address: str, + jettons_amount: float, + fee: float = 0.06, + forward_amount: float = 0.0, + comment: str = '', + response_address: str = None + ): if not self.has_access(): raise WalletError('Cannot send jettons from wallet without wallet mnemonics\nCreate wallet like Wallet(mnemonics=["your", "mnemonic", "here"...], version="your_wallet_version")') - mnemonics, _pub_k, _priv_k, wallet = Wallets.from_mnemonics(self.mnemonics, WalletVersionEnum(self.version), 0) - seqno = await self.get_seqno() jetton = await self.provider.get_jetton_data(jetton_master_address) - body = tonsdk.contract.token.ft.JettonWallet().create_transfer_body( - Address(destination_address), - jettons_amount * 10**jetton.decimals - ) jetton_wallet = await jetton.get_jetton_wallet(self.address) - query = wallet.create_transfer_message( + + return await self.transfer_jetton_by_jetton_wallet( + destination_address, jetton_wallet.address, - tonsdk.utils.to_nano(fee, "ton"), - seqno, - payload=body + jettons_amount, + fee, + jetton.decimals, + forward_amount, + comment, + response_address ) - jettons_boc = bytes_to_b64str(query["message"].to_boc(False)) - response = await self.provider.send_boc(jettons_boc) - return response - async def deploy(self): if not self.has_access(): raise WalletError('Cannot deploy wallet without wallet mnemonics\nCreate wallet like Wallet(mnemonics=["your", "mnemonic", "here"...], version="your_wallet_version")') From bfd2ec896e35f7140a180dc5e83e4aa1b87d63c7 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sun, 28 Apr 2024 04:11:33 +0200 Subject: [PATCH 22/26] Update Tonapi url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e449d8..675be2b 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ client = DtonClient( ### TonApiClient v2 -[TonApi](https://tonapi.io/swagger-ui) is a high level indexing Api. +[TonApi](https://tonapi.io/api-v2) is a high level indexing Api. To initialize TonApiClient: ```python From 30250770b36162caaabd868de1a5d947ceac7a27 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sun, 28 Apr 2024 10:30:08 +0200 Subject: [PATCH 23/26] Add TonApi raw and decoded body handling --- TonTools/Contracts/Contract.py | 8 ++++---- TonTools/Providers/TonApiClient.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/TonTools/Contracts/Contract.py b/TonTools/Contracts/Contract.py index 31d7417..927415b 100644 --- a/TonTools/Contracts/Contract.py +++ b/TonTools/Contracts/Contract.py @@ -36,12 +36,12 @@ def __init__(self, data: dict): self.destination = data['destination'] self.value = data['value'] if data.get('msg_data') is None: - self.msg_data = None + self.msg_data = base64.b64encode(bytes.fromhex(data['msg_data_hex'])).decode() if 'msg_data_hex' in data else None else: - if not is_boc(data['msg_data']): - self.msg_data = base64.b64decode(data['msg_data']).decode().split('\x00')[-1] - else: + if isinstance(data['msg_data'], dict) or is_boc(data['msg_data']): self.msg_data = data['msg_data'] + else: + self.msg_data = base64.b64decode(data['msg_data']).decode().split('\x00')[-1] self.op_code = self.try_get_op() if 'op_code' not in data else data['op_code'] def try_detect_type(self): diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index a388770..495c7b8 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -134,6 +134,8 @@ async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one tr['status'] = tr['success'] tr['fee'] = tr['total_fees'] tr['hash'] = base64.b64encode(s=bytearray.fromhex(tr['hash'])).decode() + tr['in_msg']['msg_data'] = tr['in_msg']['decoded_body'] if 'decoded_body' in tr['in_msg'] else None + tr['in_msg']['msg_data_hex'] = tr['in_msg']['raw_body'] if 'raw_body' in tr['in_msg'] else None tr['in_msg']['source'] = self._process_address(tr['in_msg']['source']['address']) if 'source' in tr['in_msg'] else '' tr['in_msg']['destination'] = self._process_address(tr['in_msg']['destination']['address']) if 'destination' in tr['in_msg'] else '' out_msgs = tr['out_msgs'] From 4b4b68949440f4c2d1c32df8d7236c0abad6a35b Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Sun, 28 Apr 2024 10:57:32 +0200 Subject: [PATCH 24/26] Add msg_data NoneType checking --- TonTools/Contracts/Contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TonTools/Contracts/Contract.py b/TonTools/Contracts/Contract.py index 927415b..5c0d7b0 100644 --- a/TonTools/Contracts/Contract.py +++ b/TonTools/Contracts/Contract.py @@ -115,7 +115,7 @@ def to_dict_user_friendly(self): 'value': int(self.in_msg.value) / 10**9, 'from': self.in_msg.source, 'to': self.in_msg.destination, - 'comment': self.in_msg.msg_data if 'te6' not in self.in_msg.msg_data else '' + 'comment': self.in_msg.msg_data if 'te6' not in (self.in_msg.msg_data or ()) else '' } else: return { @@ -126,7 +126,7 @@ def to_dict_user_friendly(self): 'value': int(self.out_msgs[0].value) / 10**9 if len(self.out_msgs) == 1 else [int(out_msg.value) / 10**9 for out_msg in self.out_msgs], 'from': self.out_msgs[0].source, 'to': self.out_msgs[0].destination if len(self.out_msgs) == 1 else [out_msg.destination for out_msg in self.out_msgs], - 'comment': (self.out_msgs[0].msg_data if 'te6' not in self.out_msgs[0].msg_data else '') if len(self.out_msgs) == 1 else [out_msg.msg_data if 'te6' not in out_msg.msg_data else '' for out_msg in self.out_msgs], + 'comment': (self.out_msgs[0].msg_data if 'te6' not in (self.out_msgs[0].msg_data or ()) else '') if len(self.out_msgs) == 1 else [out_msg.msg_data if 'te6' not in (out_msg.msg_data or ()) else '' for out_msg in self.out_msgs], } def __str__(self): From b16b55128bc5b81cf1cf34e1b8180763bc205a7e Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Mon, 29 Apr 2024 06:47:17 +0200 Subject: [PATCH 25/26] Fix op_code handling --- TonTools/Contracts/Contract.py | 3 +-- TonTools/Providers/TonApiClient.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/TonTools/Contracts/Contract.py b/TonTools/Contracts/Contract.py index 5c0d7b0..ae5f2c3 100644 --- a/TonTools/Contracts/Contract.py +++ b/TonTools/Contracts/Contract.py @@ -45,8 +45,7 @@ def __init__(self, data: dict): self.op_code = self.try_get_op() if 'op_code' not in data else data['op_code'] def try_detect_type(self): - op = self.try_get_op() - return known_prefixes.get(op) + return known_prefixes.get(self.op_code) def try_get_op(self): if not self.msg_data: diff --git a/TonTools/Providers/TonApiClient.py b/TonTools/Providers/TonApiClient.py index 495c7b8..e0e77c8 100644 --- a/TonTools/Providers/TonApiClient.py +++ b/TonTools/Providers/TonApiClient.py @@ -138,10 +138,12 @@ async def get_transactions(self, address: str, limit: int = 10**9, limit_per_one tr['in_msg']['msg_data_hex'] = tr['in_msg']['raw_body'] if 'raw_body' in tr['in_msg'] else None tr['in_msg']['source'] = self._process_address(tr['in_msg']['source']['address']) if 'source' in tr['in_msg'] else '' tr['in_msg']['destination'] = self._process_address(tr['in_msg']['destination']['address']) if 'destination' in tr['in_msg'] else '' + tr['in_msg']['op_code'] = tr['in_msg']['op_code'].replace('0x', '') if 'op_code' in tr['in_msg'] else '' out_msgs = tr['out_msgs'] for out_msg in out_msgs: out_msg['source'] = self._process_address(out_msg['source']['address']) if 'source' in out_msg else '' out_msg['destination'] = self._process_address(out_msg['destination']['address']) if 'destination' in out_msg else '' + out_msg['op_code'] = out_msg['op_code'].replace('0x', '') if 'op_code' in out_msg else '' tr['out_msgs'] = out_msgs result.append(Transaction(tr)) return result[:limit] From bcc197f788a6aad35013f3a1cba9161daef06565 Mon Sep 17 00:00:00 2001 From: Nazar Taran Date: Tue, 30 Apr 2024 03:15:39 +0200 Subject: [PATCH 26/26] Add include_package_data argument --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d3a07cd..964a175 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup, find_packages requirements = [ "setuptools>=69.5", @@ -13,7 +13,8 @@ setup( name='TonTools', version='2.2.0', - packages=['TonTools', 'TonTools/Contracts', 'TonTools/Providers', 'TonTools/Enums'], + packages=find_packages(), + include_package_data=True, url='', license='MIT License', author='yungwine',