Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion dabi/builtins/types/smart_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dabi.builtins.subtypes.labels import LabelsSubtype
from dabi.builtins.subtypes.selector import SelectorSubtype
from dabi.builtins.types.get_methods import MethodsSubtype
from dabi.builtins.subtypes.tlb import TLBSubtype
from dabi.builtins.types.base import dABIType
import re

Expand All @@ -18,6 +19,7 @@ def __init__(self, context):
self.labels = LabelsSubtype(context)
self.selector = SelectorSubtype(context)
self.get_methods = []
self.storage: TLBSubtype | None = None
self.unique_getters = set()
self.current_unique_index = 0

Expand Down Expand Up @@ -68,6 +70,12 @@ def parse(self, data: dict):

self.selector.parse(data['spec']['selector'])

if 'storage' in data['spec']:
if not isinstance(data['spec']['storage'], dict):
raise ValueError('InterfaceType: storage must be presented as dict')
self.storage = TLBSubtype(self.context)
self.storage.parse(data['spec']['storage'])

if 'get_methods' in data['spec']:
if not isinstance(data['spec']['get_methods'], list):
raise ValueError('InterfaceType: get_methods must be presented as list')
Expand All @@ -93,10 +101,15 @@ def to_dict(self, convert_getters=False):
if self.selector.selector_type == 'by_code':
code_hashes = self.selector.items

return {
result = {
'metadata': self.metadata.to_dict(),
'labels': self.labels.to_dict(),
'selector': self.selector.to_dict(),
'get_methods': getters,
'code_hashes': code_hashes
}

if self.storage is not None:
result['storage'] = self.storage.to_dict()

return result
122 changes: 100 additions & 22 deletions dabi/builtins/types/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self, context, abi):
self.abi = abi
self.name = None
self.libs = []
self.only_storage = False

self.block_cache = dict()

Expand Down Expand Up @@ -189,34 +190,45 @@ def parse(self, data):
except Exception as e:
raise ValueError(f'TestCaseType: address is invalid: {e}')

if 'parsed_info' not in data or not isinstance(data['parsed_info'], dict) or not 'get_methods' in data[
'parsed_info']:
raise ValueError('TestCaseType: parsed_info must contain get_methods as dict')
if 'parsed_info' not in data or not isinstance(data['parsed_info'], dict):
raise ValueError('TestCaseType: parsed_info must be a dict')

for getter in data['parsed_info']['get_methods']:
if not isinstance(data['parsed_info']['get_methods'][getter], dict):
raise ValueError(f'TestCaseType: {getter} is not a dict')
has_getters = 'get_methods' in data['parsed_info'] and isinstance(data['parsed_info']['get_methods'], dict)
has_storage = 'storage' in data['parsed_info'] and isinstance(data['parsed_info']['storage'], dict)

if getter not in self.parsed_info:
self.parsed_info[getter] = {}
if not has_getters and not has_storage:
raise ValueError('TestCaseType: parsed_info must contain get_methods or storage')

if 'result' not in data['parsed_info']['get_methods'][getter]:
raise ValueError(f'TestCaseType: does not contain result for {getter}')
if has_getters:
for getter in data['parsed_info']['get_methods']:
if not isinstance(data['parsed_info']['get_methods'][getter], dict):
raise ValueError(f'TestCaseType: {getter} is not a dict')

if not isinstance(data['parsed_info']['get_methods'][getter]['result'], list):
if data['parsed_info']['get_methods'][getter]['result'] is None:
data['parsed_info']['get_methods'][getter]['result'] = []
else:
raise ValueError(f'TestCaseType: {getter} must contain a list of results')
if getter not in self.parsed_info:
self.parsed_info[getter] = {}

if 'result' not in data['parsed_info']['get_methods'][getter]:
raise ValueError(f'TestCaseType: does not contain result for {getter}')

if not isinstance(data['parsed_info']['get_methods'][getter]['result'], list):
if data['parsed_info']['get_methods'][getter]['result'] is None:
data['parsed_info']['get_methods'][getter]['result'] = []
else:
raise ValueError(f'TestCaseType: {getter} must contain a list of results')

for i in data['parsed_info']['get_methods'][getter]['result']:
if len(i) < 1:
raise ValueError(f'TestCaseType: {getter} must contain at least one result in list of results')
if not isinstance(i, dict):
raise ValueError(f'TestCaseType: {getter} / {i} must be dict')
for i in data['parsed_info']['get_methods'][getter]['result']:
if len(i) < 1:
raise ValueError(f'TestCaseType: {getter} must contain at least one result in list of results')
if not isinstance(i, dict):
raise ValueError(f'TestCaseType: {getter} / {i} must be dict')

for j in i:
self.parsed_info[getter][j] = i[j]
for j in i:
self.parsed_info[getter][j] = i[j]

if has_storage:
# Record storage expectations; may be storage-only test
self.parsed_info['__storage__'] = data['parsed_info']['storage']
self.only_storage = not has_getters

def get_tvm(self):

Expand Down Expand Up @@ -272,6 +284,72 @@ def validate(self):

contract = self.abi['by_name'][self.name]

# Storage validation: if storage is requested in parsed_info, verify storage values against TLB-parsed data
if '__storage__' in self.parsed_info:
if 'storage' not in contract:
raise ValueError(f"Test Case: {self.name} has no storage in interface")
storage = contract['storage']
tlb_id = storage.get('id')
if not tlb_id or tlb_id not in self.abi['tlb_sources']:
raise ValueError(f"Test Case: storage TLB id not registered for {self.name}")

# Prepare TLB parser for storage
tlb_text = self.abi['tlb_sources'][tlb_id]['tlb']
if storage.get('use_block_tlb', False):
tlb_text = f"{tlb_text}\n\n{self.abi['tlb_sources']['block_tlb']}"
parsed_tlb = {}
add_tlb(tlb_text, parsed_tlb)
to_parse = parsed_tlb[storage['object']]()

# Load current account data cell
bid = BlockId(
workchain=self.smart_contract['workchain'],
shard=self.smart_contract['shard'],
seqno=self.smart_contract['seqno']
)
if bid not in self.block_cache:
not_loaded = True
while not_loaded:
try:
self.block_cache[bid] = self.client.lookup_block(workchain=bid.workchain, shard=bid.shard, seqno=bid.seqno).blk_id
not_loaded = False
except Exception:
sleep(0.1)

block = self.block_cache[bid]
account_state = self.client.get_account_state(self.address, block).get_parsed()
data_cell = account_state.storage.state.x.data.value

# Parse data cell and dump to dict
parsed_obj = to_parse.cell_unpack(data_cell)
dump = parsed_obj.dump(with_types=storage.get('dump_with_types', False))

# Helper to traverse dump by dotted path
def _get_by_path(root, path_str):
cur = root
for part in path_str.split('.'):
if isinstance(cur, dict) and part in cur:
cur = cur[part]
else:
raise AssertionError(f"Test Case: {self.name} storage path not found: {path_str}, dump: {root}")
return cur

expected_map = self.parsed_info['__storage__']
parse_items = {item.get('path'): item for item in storage.get('parse', [])} if storage.get('parse') else {}

# Compare only provided expected keys; skip paths with skipParse label
for key, expected_value in expected_map.items():
labels = parse_items.get(key, {}).get('labels', {}) if parse_items else {}
if labels.get('skipParse', False):
continue
actual_value = _get_by_path(dump, key)
assert expected_value == actual_value, \
f"Test Case: {self.name} storage mismatch at '{key}': expected {expected_value}, got {actual_value}"

# If this test is storage-only, do not proceed with getters
if self.only_storage:
return

for getter in contract['get_methods']:
for instance in contract['get_methods'][getter]:

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
tonpy-dev==0.0.0.5.9b1
tonpy-dev==0.0.0.6.2a1
pytest
PyYAML
jinja2
32 changes: 32 additions & 0 deletions schema/interfaces/dedust/x1000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: dabi/v0
type: Interface
metadata:
name: "DeDust x1000 storage"
labels:
name: x1000_wallet
dton_parse_prefix: abi_x1000_wallet_
spec:
storage:
version: tlb/v0
dump_with_types: false
file_path: "dedust/x1000.tlb"
object: "Storage"
use_block_tlb: true
parse:
- path: "seqno_offset"
labels:
dton_type: UInt32
- path: "seqno_bitmap"
labels:
dton_type: UInt256
- path: "public_key"
labels:
dton_type: FixedString(64)
- path: "revoked"
labels:
skipParse: true
dton_type: Int32

selector:
by_code:
- hash: "43D44716D326A832CED56A6B60A02CB67630E04F5785FE89F8784A110CF36151"
12 changes: 12 additions & 0 deletions schema/tests/dedust/x1000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: dabi/v0
type: TestCase
smart_contract:
name: x1000_wallet
address: "EQDP5uHBEMdxjA_zACkOUZIinRsKFkT-UvFfBRdMpfghVsD_"
block:
mc_seqno: 52337416
parsed_info:
storage:
seqno_offset: 0
seqno_bitmap: F000000000000000000000000000000000000000000000000000000000000000
public_key: 57596659503076668335510973085999114172982845285811582987207858129139951814773
2 changes: 2 additions & 0 deletions schema/tlb/dedust/x1000.tlb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
_#_ seqno_offset:uint32 seqno_bitmap:bits256
public_key:uint256 revoked:(Maybe ^Cell) = Storage;
Loading