Skip to content

Commit

Permalink
Add dABI support
Browse files Browse the repository at this point in the history
  • Loading branch information
tvorogme committed Oct 14, 2024
1 parent 0ff8f8f commit 8020ad4
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Add BlockId / BlockIdExt python native support in C++, add wrappers
- Allow VmDict initialization from PyDict
- Add `combine_with` in VmDict to fast combine dictionaries
- Add [dABI](https://github.com/disintar/dABI) support
- Add FunC string / sources support (tonpy.func.func)
- Critical change: TLB dump - forced `dump_bin_as_hex` to true. This means if bitsting is `x % 8 == 0` then it'll be
dumped as hex (this is most of use-cases)
Expand Down
Empty file added src/tonpy/abi/__init__.py
Empty file.
133 changes: 133 additions & 0 deletions src/tonpy/abi/getter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from tonpy import StackEntry
from tonpy.tvm import TVM
from loguru import logger

supported_types = [
'UInt8',
'UInt16',
'UInt32',
'UInt64',
'UInt128',
'UInt256',
'String',
'Address'
]


class ABIGetterResultInstance:
def __init__(self, instance):
self.instance = instance

self.labels = self.instance['labels'] if self.instance['labels'] is not None else {}
self.metadata = self.instance['metadata'] if self.instance['metadata'] is not None else {}

self.name = self.labels.get('name')
self.dton_type = self.labels.get('dton_type', self.instance['type'])
self.type = self.instance['type']
self.required = self.instance['required']

if self.dton_type == 'Int':
self.dton_type = 'UInt256'
elif self.dton_type in ['Slice', 'Cell', 'Continuation', 'Builder']:
if self.labels.get('address'):
self.dton_type = 'Address'
else:
self.dton_type = 'String'
elif self.dton_type == 'Null':
self.dton_type = 'UInt8'

if self.dton_type != 'Tuple':
assert self.dton_type in supported_types, f'Unsupported ABI type {self.dton_type}'
else:
self.dton_type = None
self.items = self.instance['items']

if 'dton_parse_prefix' not in self.labels:
self.dton_parse_prefix = f''
else:
self.dton_parse_prefix = self.labels['dton_parse_prefix']

def get_columns(self):
if self.dton_type == 'Address':
return {
f'{self.dton_parse_prefix}{self.name}_workchain': 'UInt8',
f'{self.dton_parse_prefix}{self.name}_address': 'FixedString(64)',
}
else:
return {f'{self.dton_parse_prefix}{self.name}': self.dton_type}

def parse_stack_item(self, stack_entry: StackEntry) -> dict:
if self.dton_type == 'Address':
if stack_entry.get_type() is StackEntry.Type.t_cell:
address = stack_entry.as_cell().begin_parse().load_address()
elif stack_entry.get_type() is StackEntry.Type.t_slice:
address = stack_entry.as_cell_slice().load_address()
elif stack_entry.get_type() is StackEntry.Type.t_builder:
address = stack_entry.as_cell_builder().end_cell().begin_parse().load_address()

return {
f'{self.dton_parse_prefix}{self.name}_workchain': address.workchain,
f'{self.dton_parse_prefix}{self.name}_address': address.address,
}
elif self.type in ['Slice', 'Cell', 'Continuation', 'Builder']:
return {f"{self.dton_parse_prefix}{self.name}": stack_entry.get().to_boc()}
elif self.dton_type in ['UInt8', 'UInt16', 'UInt32', 'UInt64', 'UInt128', 'UInt256']:
return {f"{self.dton_parse_prefix}{self.name}": stack_entry.as_int()}
else:
raise ValueError(f'Unsupported ABI type {self.dton_type}')


class ABIGetterInstance:
def __init__(self, instance):
self.instance = instance

self.method_name = instance['method_name']
self.method_id = instance['method_id']

self.method_args = instance['method_args']
self.method_args_hash = instance['method_args_hash']

self.method_result = [ABIGetterResultInstance(i) for i in instance['method_result']]
self.method_result_hash = instance['method_result_hash']
self.labels = instance['labels'] if instance['labels'] is not None else {}
self.metadata = instance['metadata'] if instance['metadata'] is not None else {}

self.result_strict_type_check = instance['result_strict_type_check']
self.result_length_strict_check = instance['result_length_strict_check']

if 'dton_parse_prefix' not in self.labels:
self.dton_parse_prefix = f''
else:
self.dton_parse_prefix = self.labels['dton_parse_prefix']

def get_columns(self):
tmp = {}

for getter in self.method_result:
columns = getter.get_columns()

for c in columns:
tmp[f"{self.dton_parse_prefix}{c}"] = columns[c]

return tmp

def parse_getters(self, tvm: TVM) -> dict:
tvm.set_stack([self.method_id])
stack = tvm.run(allow_non_success=True, unpack_stack=False)

if self.result_length_strict_check:
assert len(stack) == len(self.method_result)

if self.result_strict_type_check:
my_result_hash = stack.get_abi_hash()
assert my_result_hash == self.method_result_hash

tmp = {}

for getter, stack_entry in zip(self.method_result, stack):
try:
tmp.update(getter.parse_stack_item(stack_entry))
except Exception as e:
logger.error(f"Can't parse {getter}: {e}")

return tmp
50 changes: 50 additions & 0 deletions src/tonpy/abi/instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import List

from tonpy import TVM
from tonpy.abi.getter import ABIGetterInstance
from loguru import logger


class ABIInterfaceInstance:
def __init__(self, instance):
self.instance = instance
self.name = self.instance["labels"]["name"]

if 'dton_parse_prefix' not in self.instance['labels']:
self.dton_parse_prefix = f'parsed_abi_{self.name}_'
else:
self.dton_parse_prefix = self.instance['labels']['dton_parse_prefix']

self.getters = []

for i in self.instance['get_methods']:
for getter in self.instance['get_methods'][i]:
self.getters.append(ABIGetterInstance(getter))

def __hash__(self):
return hash(self.instance['labels']['name'])

def get_columns(self) -> dict:
columns = {}

for getter in self.getters:
tmp = getter.get_columns()
for c in tmp:
columns[f"{self.dton_parse_prefix}{c}"] = tmp[c]

return columns

def parse_getters(self, tvm: TVM):
result = {}

for getter in self.getters:
try:
tmp = getter.parse_getters(tvm)

for i in tmp:
result[f"{self.dton_parse_prefix}{i}"] = tmp[i]

except Exception as e:
logger.info(f"Can't parse {self.name}, (getter: {getter.method_name}): {e}")

return result
61 changes: 61 additions & 0 deletions src/tonpy/abi/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from collections import defaultdict
from typing import List

from tonpy.tvm import TVM
from tonpy.abi.instance import ABIInterfaceInstance
from loguru import logger

class ABIInstance:
def __init__(self, abi_data):
self.abi_data = abi_data

self.by_code_hash = defaultdict(set)
self.by_get_method = defaultdict(set)
self.by_name = {}

for (i, j) in abi_data['by_name'].items():
self.by_name[i] = ABIInterfaceInstance(j)

for code_hash in abi_data['by_code_hash']:
for name in abi_data['by_code_hash'][code_hash]:
self.by_code_hash[code_hash].add(self.by_name[name])

for get_method in abi_data['by_get_method']:
for name in abi_data['by_get_method'][get_method]:
self.by_get_method[get_method].add(self.by_name[name])

def get_columns(self):
columns = {}

for interface in self.by_name.values():
columns.update(interface.get_columns())

return columns

def abi_for_getters(self, getters: List[int]):
tmp = set()

for getter in getters:
tmp.add(self.by_get_method[getter])

return tmp

def parse_getters(self, tvm: TVM, getters: List[int] = None):
parsers = set()

if tvm.code_hash in self.by_code_hash:
for parser in self.by_code_hash[tvm.code_hash]:
parsers.add(parser)
else:
if getters is not None:
for parser in self.abi_for_getters(getters):
parsers.add(parser)
else:
logger.warning("Code hash not found in ABI, provide getters for parse methods")

result = {}

for parser in parsers:
result.update(parser.parse_getters(tvm))

return result
23 changes: 23 additions & 0 deletions src/tonpy/tests/test_abi.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/tonpy/tvm/tvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(self, log_level: int = 0,
same_c3,
skip_c7,
enable_stack_dump)
self.code_hash = code.get_hash()
self.vm_steps_detailed: Optional[List[StepInfo]] = None
self.enable_stack_dump = enable_stack_dump

Expand Down

0 comments on commit 8020ad4

Please sign in to comment.