Skip to content

Commit 3a614dc

Browse files
authored
Merge pull request #16 from disintar/feature/storage-abi
Add storage abi support
2 parents 3aa91f3 + e0eaae1 commit 3a614dc

File tree

7 files changed

+264
-30700
lines changed

7 files changed

+264
-30700
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def finalize_options(self):
3131

3232
setup(
3333
name="tonpy" if not IS_DEV else "tonpy-dev",
34-
version="0.0.0.1.4c0" if not IS_DEV else "0.0.0.6.1a1",
34+
version="0.0.0.1.4c0" if not IS_DEV else "0.0.0.6.2a1",
3535
author="Disintar LLP",
3636
author_email="andrey@head-labs.com",
3737
description="Types / API for TON blockchain",

src/tonpy/abi/getter.py

Lines changed: 20 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,10 @@
22

33
from tonpy import StackEntry, add_tlb, Address, CellSlice
44
from tonpy.abi.getter_cache import getter_cache
5+
from tonpy.abi.utils import parse_tlb_spec, supported_types
56
from tonpy.tvm import TVM
67
from loguru import logger
78

8-
supported_types = [
9-
'Int8',
10-
'Int16',
11-
'Int32',
12-
'Int64',
13-
'Int128',
14-
'Int256',
15-
'UInt8',
16-
'UInt16',
17-
'UInt32',
18-
'UInt64',
19-
'UInt128',
20-
'UInt256',
21-
'String',
22-
'FixedString(64)',
23-
'Address',
24-
'Boolean',
25-
'Datetime'
26-
]
27-
289

2910
class ABIGetterResultInstance:
3011
def __init__(self, instance):
@@ -85,31 +66,13 @@ def get_columns(self):
8566
}
8667
elif self.type in ['Slice', 'Cell', 'Continuation', 'Builder'] and self.instance.get('tlb', None):
8768
tlb = self.instance.get('tlb')
88-
89-
if 'parse' in tlb:
90-
data = tlb.get('parse')
91-
assert isinstance(data, list)
92-
93-
tmp = {}
94-
95-
if tlb['dump_with_types']:
96-
tmp[f'{self.dton_parse_prefix}{self.name}_type'] = 'String'
97-
98-
for item in data:
99-
path = item.get('path', None)
100-
assert path, "Missing path in TLB"
101-
102-
path = path.split('.')
103-
if tmp.get(path[-1]):
104-
raise ValueError(f'Duplicate path {path[-1]}')
105-
106-
dtype = item.get('labels', {}).get('dton_type', None)
107-
assert dtype in supported_types, f'Unsupported ABI type {dtype}'
108-
109-
name = item.get('labels', {}).get('name', path[-1])
110-
tmp[f'{self.dton_parse_prefix}{self.name}_{name}'] = dtype
111-
112-
return tmp
69+
return parse_tlb_spec(
70+
None,
71+
tlb,
72+
prefix=self.dton_parse_prefix,
73+
name_prefix=f"{self.name}_",
74+
columns_only=True,
75+
)
11376
elif self.type == 'Tuple':
11477
if not self.items or not len(self.items):
11578
return {}
@@ -185,43 +148,18 @@ def parse_stack_item(self, stack_entry: StackEntry, tlb_sources, force_all: bool
185148
data = to_parse.cell_unpack(item, True)
186149

187150
parsed_data = data.dump(with_types=tlb['dump_with_types'])
188-
189-
if 'parse' in tlb:
190-
data = tlb.get('parse')
191-
assert isinstance(data, list)
192-
193-
tmp = {}
194-
if tlb['dump_with_types']:
195-
tmp[f'{self.dton_parse_prefix}{self.name}_type'] = parsed_data['type']
196-
197-
for item in data:
198-
path = item.get('path', None)
199-
assert path, "Missing path in TLB"
200-
201-
path = path.split('.')
202-
if tmp.get(path[-1]):
203-
raise ValueError(f'Duplicate path {path[-1]}')
204-
205-
dtype = item.get('labels', {}).get('dton_type', None)
206-
assert dtype in supported_types, f'Unsupported ABI type {dtype}'
207-
208-
name = item.get('labels', {}).get('name', path[-1])
209-
210-
old = parsed_data.get(path[0], None)
211-
212-
if len(path) > 1:
213-
for item in path[1:]:
214-
if old:
215-
old = old.get(item, None)
216-
if old is not None:
217-
if dtype == 'FixedString(64)':
218-
old = hex(old).upper()[2:].zfill(64)
219-
220-
tmp[f'{self.dton_parse_prefix}{self.name}_{name}'] = old
221-
222-
return tmp
223-
224-
return {f"{self.dton_parse_prefix}{self.name}": stack_entry.get().to_boc()}
151+
result = parse_tlb_spec(
152+
parsed_data,
153+
tlb,
154+
prefix=self.dton_parse_prefix,
155+
name_prefix=f"{self.name}_",
156+
columns_only=False,
157+
)
158+
159+
if not result:
160+
return {f"{self.dton_parse_prefix}{self.name}": stack_entry.get().to_boc()}
161+
162+
return result
225163
elif self.dton_type in ['Int8', 'Int16', 'Int32', 'Int64', 'Int128', 'Int256']:
226164
return {
227165
f"{self.dton_parse_prefix}{self.name}":

src/tonpy/abi/instance.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def abi_for_getters(self, getters: List[int]) -> set[ABIInterfaceInstance]:
5555

5656
return clear_parsers
5757

58-
def get_parsers(self, code_hash: str, getters: List[int]) -> set[ABIInterfaceInstance]:
58+
def get_parsers(self, code_hash: str, getters: List[int] | None) -> set[ABIInterfaceInstance]:
5959
parsers = set()
6060

6161
if code_hash in self.by_code_hash and self.by_code_hash[code_hash]:
@@ -157,3 +157,40 @@ async def aparse_getter_lazy(self, code_hash, get_tvm: Callable, getters: List[i
157157
result.update()
158158

159159
return result
160+
161+
def parse_storage(self, tvm: TVM) -> dict:
162+
parsers = self.get_parsers(tvm.code_hash, getters=None)
163+
result = {}
164+
if len(parsers) > 0:
165+
result['abi_interfaces'] = []
166+
for parser in parsers:
167+
# Always record interface if it declares storage, even if parse yields no fields
168+
if getattr(parser, 'storage', None):
169+
result['abi_interfaces'].append(parser.name)
170+
data = parser.parse_storage(tvm, self.tlb_sources)
171+
if data:
172+
result.update(data)
173+
return result
174+
175+
def parse_storage_lazy(self, code_hash, get_tvm: Callable) -> dict:
176+
parsers = self.get_parsers(code_hash, getters=None)
177+
result = {}
178+
if len(parsers) > 0:
179+
result['abi_interfaces'] = []
180+
tvm = get_tvm()
181+
for parser in parsers:
182+
# Always record interface if it declares storage, even if parse yields no fields
183+
if getattr(parser, 'storage', None):
184+
result['abi_interfaces'].append(parser.name)
185+
data = parser.parse_storage(tvm, self.tlb_sources)
186+
if data:
187+
for key, value in data.items():
188+
if key not in result:
189+
result[key] = value
190+
else:
191+
if result[key] is not None:
192+
result[key] = value
193+
else:
194+
logger.warning(f"Got multiple not null answers for storage field {key} in {parser.name}")
195+
result.update()
196+
return result

src/tonpy/abi/interface.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from typing import List
2-
3-
from tonpy import TVM
1+
from tonpy import TVM, add_tlb
42
from tonpy.abi.getter import ABIGetterInstance
3+
from tonpy.abi.utils import parse_tlb_spec
54
from loguru import logger
6-
import asyncio
75
import traceback
86

97
class ABIInterfaceInstance:
@@ -18,8 +16,10 @@ def __init__(self, instance):
1816
self.dton_parse_prefix = self.instance['labels']['dton_parse_prefix']
1917

2018
self.getters = []
19+
self.storage = self.instance.get('storage')
2120

22-
for i in self.instance['get_methods']:
21+
# get_methods could be absent when only storage is provided
22+
for i in self.instance.get('get_methods', {}):
2323
for getter in self.instance['get_methods'][i]:
2424
self.getters.append(ABIGetterInstance(getter))
2525

@@ -29,13 +29,30 @@ def __hash__(self):
2929
def get_columns(self) -> dict:
3030
columns = {}
3131

32+
# getters columns
3233
for getter in self.getters:
3334
tmp = getter.get_columns()
3435
for c in tmp:
3536
columns[f"{self.dton_parse_prefix}{c}"] = tmp[c]
3637

38+
# storage columns
39+
columns.update(self.get_storage_columns())
40+
3741
return columns
3842

43+
def get_storage_columns(self) -> dict:
44+
if not self.storage:
45+
return {}
46+
47+
tlb = self.storage
48+
return parse_tlb_spec(
49+
None,
50+
tlb,
51+
prefix=self.dton_parse_prefix,
52+
name_prefix="",
53+
columns_only=True,
54+
)
55+
3956
def parse_getters(self, tvm: TVM, tlb_sources):
4057
result = {}
4158

@@ -53,6 +70,11 @@ def parse_getters(self, tvm: TVM, tlb_sources):
5370
logger.warning(f"Can't parse {self.name}, (getter: {getter.method_name}): {e} {traceback.format_exc()}")
5471
return {} # abi should work completely, with a result in each getter
5572

73+
# parse storage too
74+
storage_parsed = self.parse_storage(tvm, tlb_sources)
75+
if storage_parsed:
76+
result.update(storage_parsed)
77+
5678
return result
5779

5880
async def aparse_getters(self, tvm: TVM, tlb_sources):
@@ -73,5 +95,45 @@ async def aparse_getters(self, tvm: TVM, tlb_sources):
7395
logger.warning(f"Can't parse {self.name}, (getter: {getter.method_name}): {e} {traceback.format_exc()}")
7496
return {} # abi should work completely, with a result in each getter
7597

98+
# parse storage too
99+
storage_parsed = self.parse_storage(tvm, tlb_sources)
100+
if storage_parsed:
101+
result.update(storage_parsed)
102+
76103
return result
77104

105+
def parse_storage(self, tvm: TVM, tlb_sources):
106+
if not self.storage:
107+
return {}
108+
109+
try:
110+
tlb = self.storage
111+
# resolve TLB text
112+
tlb_text = None
113+
if 'id' in tlb and tlb['id'] in tlb_sources:
114+
tlb_text = tlb_sources[tlb['id']]['tlb']
115+
elif 'file_path' in tlb and tlb['file_path'] in tlb_sources:
116+
tlb_text = tlb_sources[tlb['file_path']]['tlb']
117+
118+
if tlb.get('use_block_tlb'):
119+
tlb_text = f"{tlb_sources['block_tlb']}\n\n{tlb_text}"
120+
121+
env = {}
122+
add_tlb(tlb_text, env)
123+
to_parse = env[tlb['object']]()
124+
125+
# Unpack the contract storage cell
126+
data = to_parse.cell_unpack(tvm.data, True)
127+
dumped = data.dump(with_types=tlb.get('dump_with_types', False))
128+
129+
return parse_tlb_spec(
130+
dumped,
131+
tlb,
132+
prefix=self.dton_parse_prefix,
133+
name_prefix="",
134+
columns_only=False,
135+
)
136+
except Exception as e:
137+
logger.warning(f"Can't parse storage for {self.name}: {e} {traceback.format_exc()}")
138+
return {}
139+

0 commit comments

Comments
 (0)