From 0cf87e8c4381a4f496cee954e43ad63ce35a4776 Mon Sep 17 00:00:00 2001 From: Nikita Lebedev Date: Mon, 6 Oct 2025 11:46:32 +0200 Subject: [PATCH 1/2] Add storage abi support & x1000 --- dabi/builtins/types/smart_contract.py | 15 ++++++- dabi/builtins/types/testcase.py | 65 ++++++++++++++++++--------- schema/interfaces/dedust/x1000.yaml | 32 +++++++++++++ schema/tests/dedust/x1000.yaml | 12 +++++ schema/tlb/dedust/x1000.tlb | 2 + 5 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 schema/interfaces/dedust/x1000.yaml create mode 100644 schema/tests/dedust/x1000.yaml create mode 100644 schema/tlb/dedust/x1000.tlb diff --git a/dabi/builtins/types/smart_contract.py b/dabi/builtins/types/smart_contract.py index 72a68ee..42c6956 100644 --- a/dabi/builtins/types/smart_contract.py +++ b/dabi/builtins/types/smart_contract.py @@ -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 @@ -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 @@ -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') @@ -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 diff --git a/dabi/builtins/types/testcase.py b/dabi/builtins/types/testcase.py index b724094..29dea26 100644 --- a/dabi/builtins/types/testcase.py +++ b/dabi/builtins/types/testcase.py @@ -189,34 +189,44 @@ 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: + # For storage tests we just record a flag; data can be mocked/empty. + self.parsed_info['__storage__'] = data['parsed_info']['storage'] def get_tvm(self): @@ -272,6 +282,17 @@ def validate(self): contract = self.abi['by_name'][self.name] + # Storage-only test path: if storage is requested in parsed_info, just verify interface has storage and sources + 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}") + # Mocked data: no live network calls, just succeed + return + for getter in contract['get_methods']: for instance in contract['get_methods'][getter]: diff --git a/schema/interfaces/dedust/x1000.yaml b/schema/interfaces/dedust/x1000.yaml new file mode 100644 index 0000000..396de9a --- /dev/null +++ b/schema/interfaces/dedust/x1000.yaml @@ -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" diff --git a/schema/tests/dedust/x1000.yaml b/schema/tests/dedust/x1000.yaml new file mode 100644 index 0000000..347f5a3 --- /dev/null +++ b/schema/tests/dedust/x1000.yaml @@ -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: 123 + + diff --git a/schema/tlb/dedust/x1000.tlb b/schema/tlb/dedust/x1000.tlb new file mode 100644 index 0000000..3760a71 --- /dev/null +++ b/schema/tlb/dedust/x1000.tlb @@ -0,0 +1,2 @@ +_#_ seqno_offset:uint32 seqno_bitmap:bits256 + public_key:uint256 revoked:(Maybe ^Cell) = Storage; \ No newline at end of file From 6b4dc875b01ffb520ee8d104dca19434c509528c Mon Sep 17 00:00:00 2001 From: Nikita Lebedev Date: Thu, 9 Oct 2025 16:34:32 +0200 Subject: [PATCH 2/2] Add storage abi tests --- dabi/builtins/types/testcase.py | 65 +++++++++++++++++++++++++++++++-- requirements.txt | 2 +- schema/tests/dedust/x1000.yaml | 6 +-- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/dabi/builtins/types/testcase.py b/dabi/builtins/types/testcase.py index 29dea26..78f1086 100644 --- a/dabi/builtins/types/testcase.py +++ b/dabi/builtins/types/testcase.py @@ -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() @@ -225,8 +226,9 @@ def parse(self, data): self.parsed_info[getter][j] = i[j] if has_storage: - # For storage tests we just record a flag; data can be mocked/empty. + # 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): @@ -282,7 +284,7 @@ def validate(self): contract = self.abi['by_name'][self.name] - # Storage-only test path: if storage is requested in parsed_info, just verify interface has storage and sources + # 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") @@ -290,8 +292,63 @@ def validate(self): 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}") - # Mocked data: no live network calls, just succeed - return + + # 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]: diff --git a/requirements.txt b/requirements.txt index fff41e7..d127262 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -tonpy-dev==0.0.0.5.9b1 +tonpy-dev==0.0.0.6.2a1 pytest PyYAML jinja2 \ No newline at end of file diff --git a/schema/tests/dedust/x1000.yaml b/schema/tests/dedust/x1000.yaml index 347f5a3..619a8a2 100644 --- a/schema/tests/dedust/x1000.yaml +++ b/schema/tests/dedust/x1000.yaml @@ -7,6 +7,6 @@ smart_contract: mc_seqno: 52337416 parsed_info: storage: - seqno_offset: 123 - - + seqno_offset: 0 + seqno_bitmap: F000000000000000000000000000000000000000000000000000000000000000 + public_key: 57596659503076668335510973085999114172982845285811582987207858129139951814773