Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Upgrade to TonAPI v2, Enhance Jetton Integration, Fix bugs and Improve Code Quality #16

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ef93bdb
Add enums classes
naztar0 Apr 25, 2024
3b4572e
Add a list of the 600 most popular jettons
naztar0 Apr 25, 2024
4690bba
Refactor code, minor improvements
naztar0 Apr 25, 2024
83ddf24
Add the AddressForm enum
naztar0 Apr 25, 2024
77e5e30
Refactor code, minor improvements
naztar0 Apr 25, 2024
c2ef352
Remove self.address redefinition
naztar0 Apr 25, 2024
1d59be4
Upgrade logic to interact with API v2, refactor code
naztar0 Apr 25, 2024
b6be1ac
Add helper function to fallback jetton data fields
naztar0 Apr 25, 2024
6305ad9
Fix bugs, improve logic and UX, refactor code
naztar0 Apr 25, 2024
a6a843b
Add SafeLsClient
naztar0 Apr 25, 2024
dece33c
Update requirements and version
naztar0 Apr 25, 2024
eac9106
Update examples
naztar0 Apr 25, 2024
8f4e64f
Update README
naztar0 Apr 25, 2024
47db4a8
Fix crash in the absence of a description
naztar0 Apr 25, 2024
033d014
Fix typo
naztar0 Apr 25, 2024
8c44038
Return last argument due to method's dependencies
naztar0 Apr 26, 2024
9f840ec
Add required methods
naztar0 Apr 26, 2024
6c30736
Add transactions length check
naztar0 Apr 26, 2024
d696fa4
Add Enums package
naztar0 Apr 26, 2024
b8ee9b6
Add required methods
naztar0 Apr 27, 2024
dfc0305
Add additional arguments to transfer_jetton, refactor code
naztar0 Apr 28, 2024
bfd2ec8
Update Tonapi url
naztar0 Apr 28, 2024
3025077
Add TonApi raw and decoded body handling
naztar0 Apr 28, 2024
4b4b689
Add msg_data NoneType checking
naztar0 Apr 28, 2024
b16b551
Fix op_code handling
naztar0 Apr 29, 2024
bcc197f
Add include_package_data argument
naztar0 Apr 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -73,21 +73,17 @@ 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
)
```
**_Note:_** Dton currently doesn't support sending messages to blockchain, so you can't, for example, transfer toncoins using this provider


### TonApiClient - currently v1
### TonApiClient v2

**_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

[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
Expand All @@ -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
Expand Down Expand Up @@ -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"})
Expand All @@ -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')
Expand Down
33 changes: 19 additions & 14 deletions TonTools/Contracts/Contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -17,15 +17,15 @@ 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


def is_boc(b64str: str):
try:
Cell.one_from_boc(b64str_to_bytes(b64str))
return True
except:
except Exception:
return False


Expand All @@ -35,22 +35,27 @@ 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 = base64.b64encode(bytes.fromhex(data['msg_data_hex'])).decode() if 'msg_data_hex' in data else None
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):
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:
return None
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
Expand Down Expand Up @@ -109,7 +114,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 {
Expand All @@ -120,7 +125,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):
Expand All @@ -140,7 +145,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
Expand All @@ -154,4 +159,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)
return await self.provider.get_state(self.address)
2 changes: 1 addition & 1 deletion TonTools/Contracts/Jetton.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 1 addition & 3 deletions TonTools/Contracts/NFT.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -95,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):
Expand Down
52 changes: 34 additions & 18 deletions TonTools/Contracts/Wallet.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -48,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().
"""
Expand All @@ -58,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,
Expand All @@ -71,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")')
Expand Down
4 changes: 2 additions & 2 deletions TonTools/Contracts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions TonTools/Enums/Address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class AddressForm:
RAW = 'raw'
USER_FRIENDLY = 'user_friendly'
34 changes: 34 additions & 0 deletions TonTools/Enums/Exception.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 19 additions & 0 deletions TonTools/Enums/Jetton.py
Original file line number Diff line number Diff line change
@@ -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
Loading