Skip to content

Commit 90046e0

Browse files
authored
Merge pull request #586 from ton-blockchain/dev
Dev
2 parents 14be068 + 96c6f6a commit 90046e0

12 files changed

Lines changed: 287 additions & 41 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- name: Install dependencies
3131
run: |
3232
python -m pip install --upgrade pip
33-
pip install ruff pytest pytest-mock
33+
pip install ruff==0.15.22 pytest pytest-mock
3434
3535
- name: Run Ruff
3636
run: |

modules/controller.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
from mytonctrl.utils import GetItemFromList
1010
from modules.module import MtcModule
1111

12-
from typing import TYPE_CHECKING
13-
if TYPE_CHECKING:
14-
from mytoncore import MyTonCore
15-
1612

1713
class ControllerModule(MtcModule):
1814

@@ -229,18 +225,12 @@ def do_calculate_loan_amount_test(self):
229225
max_loan = self.ton.local.db.get("max_loan", 43000)
230226
max_interest_percent = self.ton.local.db.get("max_interest_percent", 1.5)
231227
max_interest = int(max_interest_percent / 100 * 16777216)
232-
return self.ton.CalculateLoanAmount(min_loan, max_loan, max_interest)
228+
return self.ton.calculate_loan_amount(min_loan, max_loan, max_interest)
233229

234230
def calculate_loan_amount_test(self, args):
235231
t = self.do_calculate_loan_amount_test()
236232
print(t)
237233

238-
@classmethod
239-
def check_enable(cls, ton: "MyTonCore"):
240-
from mytoninstaller.mytoninstaller import InstallerCtrl
241-
installer = InstallerCtrl.from_ton(ton)
242-
installer.enable_ton_http_api()
243-
244234
def add_console_commands(self, console):
245235
add_command(self.local, console, "create_controllers", self.create_controllers)
246236
add_command(self.local, console, "update_controllers", self.create_controllers)

modules/general.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ def run_benchmark(self, args: list[str]):
995995
return
996996

997997
with tempfile.TemporaryDirectory(dir=tmp_parent_dir) as tmp_dir:
998-
tmp_dir = Path(tmp_dir)
998+
tmp_dir = Path(tmp_dir).resolve()
999999
with get_package_resource_path(
10001000
"mytonctrl", "scripts/benchmark.py"
10011001
) as benchmark_path:
@@ -1028,7 +1028,9 @@ def run_benchmark(self, args: list[str]):
10281028
for f in (src_dir / "tl" / "generate" / "scheme").glob("*.tl"):
10291029
shutil.copy(f, tl_dest)
10301030

1031-
subprocess.run(["uv", "add", tontester_dir], cwd=tmp_dir, check=True)
1031+
subprocess.run(
1032+
["uv", "add", "--editable", tontester_dir], cwd=tmp_dir, check=True
1033+
)
10321034

10331035
subprocess.run(
10341036
["uv", "run", tontester_dir / "generate_tl.py"],

mytoncore/background_runner.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ def _offers(self):
5858
self._ton.VoteOffer(offer)
5959

6060
def _complaints(self):
61+
return
6162
validator_index = self._ton.GetValidatorIndex()
6263
if validator_index < 0:
6364
return
64-
if time.time() < 1776643200:
65-
return
6665

6766
# Voting for complaints
6867
config32 = self._ton.get_config_32()

mytoncore/clients.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,20 @@ def run(self, cmd: str, timeout: int | None = None, index: int | None = None, us
6969
if index is not None:
7070
args += ["-i", str(index)]
7171
elif use_local and self.pub_key_path and self.addr and out_of_sync is not None and out_of_sync < 20:
72-
args = ["--addr", self.addr, "--pub", self.pub_key_path, "--verbosity", "0", "--cmd", cmd]
72+
return self.run_local(cmd, timeout)
7373
else:
7474
ls_list = self.local.db.get("liteServers")
7575
if ls_list:
7676
args += ["-i", str(random.choice(ls_list))]
7777
return self._run(args, timeout)
7878

79+
def run_local(self, cmd: str, timeout: int | None = None) -> str:
80+
"""Connect only to the local liteserver, with no fallback to public ones."""
81+
if not self.pub_key_path or not self.addr:
82+
raise Exception(f"{self.tool_name} error: local liteserver is not configured")
83+
args = ["--addr", self.addr, "--pub", self.pub_key_path, "--verbosity", "0", "--cmd", cmd]
84+
return self._run(args, timeout)
85+
7986

8087
@dataclass
8188
class ValidatorConsole(CliTool):

mytoncore/mytoncore.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
Dict, int2ip, MyPyClass,
6262
parse_int_forced
6363
)
64-
from mytoncore.vm_stack import parse_result_stack
64+
from mytoncore.vm_stack import parse_result_stack, parse_remote_result_stack
6565

6666

6767
class MyTonCore:
@@ -180,11 +180,18 @@ def get_paths(self) -> Paths:
180180
return Paths()
181181
return Paths.from_dict(paths)
182182

183-
def run_get_method(self, addr: str, method: str):
183+
def run_get_method(self, addr: str, method: str) -> list[str]:
184184
cmd = f"runmethodfull {addr} {method}"
185185
result = self.liteClient.run(cmd)
186186
return parse_result_stack(result)
187187

188+
def run_get_method_local(self, addr: str, method: str, params: list | None = None) -> list[str]:
189+
cmd = f"runmethod {addr} {method}"
190+
if params:
191+
cmd += " " + " ".join(map(str, params))
192+
result = self.liteClient.run_local(cmd)
193+
return parse_remote_result_stack(result)
194+
188195
def get_seqno(self, wallet: Wallet) -> int:
189196
seqno = int(self.run_get_method(wallet.addrB64, "seqno")[0])
190197
wallet.seqno = seqno
@@ -2531,7 +2538,7 @@ def CreateLoanRequest(self, controllerAddr):
25312538
return
25322539

25332540
# Проверить наличие средств у ликвидного пула
2534-
if self.CalculateLoanAmount(min_loan, max_loan, max_interest) == '-0x1':
2541+
if self.calculate_loan_amount(min_loan, max_loan, max_interest) == -1:
25352542
raise Exception("CreateLoanRequest error: The liquid pool cannot issue the required amount of credit")
25362543

25372544
# Проверить хватает ли ставки валидатора
@@ -2548,25 +2555,30 @@ def CreateLoanRequest(self, controllerAddr):
25482555
self.SendFile(resultFilePath, wallet)
25492556
self.WaitLoan(controllerAddr)
25502557

2551-
def CalculateLoanAmount(self, min_loan, max_loan, max_interest):
2552-
data = dict()
2553-
data["address"] = self.GetLiquidPoolAddr()
2554-
data["method"] = "calculate_loan_amount"
2555-
data["stack"] = [
2556-
["num", min_loan*10**9],
2557-
["num", max_loan*10**9],
2558-
["num", max_interest],
2559-
]
2560-
print(f"CalculateLoanAmount data: {data}")
2561-
2558+
def calculate_loan_amount(self, min_loan: int, max_loan: int, max_interest: int) -> int:
2559+
pool_addr = self.GetLiquidPoolAddr()
2560+
params = [min_loan*10**9, max_loan*10**9, max_interest]
2561+
try:
2562+
result = self.run_get_method_local(pool_addr, "calculate_loan_amount", params)
2563+
return int(result[-1])
2564+
except Exception as e:
2565+
self.local.add_log(f"Failed to calculate loan amount: {e}, params: {params}. Falling back to local ton-http-api", "warning")
2566+
return self.calculate_loan_amount_tha(pool_addr, params)
2567+
2568+
def calculate_loan_amount_tha(self, pool_addr: str, params: list) -> int:
2569+
data = {
2570+
"address": pool_addr,
2571+
"method": "calculate_loan_amount",
2572+
"stack": [["num", param] for param in params],
2573+
}
25622574
url = "http://127.0.0.1:8801/runGetMethod"
25632575
res = requests.post(url, json=data, timeout=3)
25642576
res_data = res.json()
25652577
if res_data.get("ok") is False:
25662578
error = res_data.get("error")
25672579
raise Exception(error)
25682580
result = res_data.get("result").get("stack").pop().pop()
2569-
return result
2581+
return int(result, 16)
25702582

25712583
def WaitLoan(self, controllerAddr):
25722584
self.local.add_log("start WaitLoan function", "debug")

mytoncore/vm_stack.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,33 @@
33
import re
44

55

6+
_RESULT_RE = re.compile(r"(?m)^[^\S\n]*result:[^\S\n]*")
7+
_REMOTE_RESULT_RE = re.compile(r"(?m)^[^\S\n]*remote result(?: \(not to be trusted\))?:[^\S\n]*")
8+
9+
610
def parse_result_stack(output: str) -> list[str]:
7-
match = re.compile(r"(?m)^[^\S\n]*result:[^\S\n]*").search(output)
11+
return _parse_section_stack(output, _RESULT_RE, "result")
12+
13+
14+
def parse_remote_result_stack(output: str) -> list[str]:
15+
return _parse_section_stack(output, _REMOTE_RESULT_RE, "remote result")
16+
17+
18+
def _parse_section_stack(output: str, section_re: re.Pattern[str], section_name: str) -> list[str]:
19+
match = section_re.search(output)
820
if not match:
9-
raise ValueError("'result' section was not found")
21+
raise ValueError(f"'{section_name}' section was not found")
1022

1123
pos = _skip_ws(output, match.end())
1224
if output.startswith("error", pos):
1325
raise ValueError(output[pos:].splitlines()[0].strip())
1426
if output.startswith("<none>", pos):
15-
raise ValueError("result section does not contain a stack")
27+
raise ValueError(f"{section_name} section does not contain a stack")
1628

1729
values, pos = _parse_stack_as_strings(output, pos)
1830
pos = _skip_ws(output, pos)
1931
if pos != len(output):
20-
raise ValueError(f"unexpected text after result stack: {output[pos:pos + 40]!r}")
32+
raise ValueError(f"unexpected text after {section_name} stack: {output[pos:pos + 40]!r}")
2133
return values
2234

2335

tests/integration/test_benchmark.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,72 @@ class FakeStat:
7373
assert root_calls == []
7474

7575

76+
def test_benchmark_resolves_symlinked_tmp_dir(cli, monkeypatch, tmp_path):
77+
# Regression: when the temp dir path contains a symlink (e.g. ton_work is a
78+
# symlink like /var/ton-work -> /mnt/data/ton-work), the cwd passed to uv must be
79+
# resolved. Otherwise uv's getcwd() (real path) differs from the symlink-prefixed
80+
# tontester_dir argument, its in-tree workspace-member check fails, and tontester
81+
# installs non-editable -- hiding the generated `tonapi` package (ModuleNotFoundError).
82+
# Real filesystem setup FIRST, before Path.mkdir is patched to a no-op below,
83+
# otherwise real_dir is never created and `link` becomes a dangling symlink.
84+
real_dir = tmp_path / "real"
85+
real_dir.mkdir()
86+
link_dir = tmp_path / "link"
87+
link_dir.symlink_to(real_dir) # symlink prefix, like a symlinked ton_work
88+
89+
monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/uv" if name == "uv" else None)
90+
monkeypatch.setattr(shutil, "copytree", lambda src, dst: None)
91+
monkeypatch.setattr(shutil, "copy", lambda src, dst: None)
92+
93+
# Only pretend the benchmark temp parent is missing; delegate every other path to the
94+
# real lstat, otherwise a globally-broken os.lstat would also break Path.resolve()
95+
# (which follows symlinks via lstat) -- the very behavior under test.
96+
real_lstat = general_module.os.lstat
97+
98+
def fake_lstat(path):
99+
if str(path) == "/var/ton-work/tmp":
100+
raise FileNotFoundError
101+
return real_lstat(path)
102+
103+
monkeypatch.setattr(general_module.os, "lstat", fake_lstat)
104+
monkeypatch.setattr(general_module, "run_as_root", lambda args: 0)
105+
monkeypatch.setattr(Path, "mkdir", lambda *a, **kw: None)
106+
monkeypatch.setattr(Path, "glob", lambda self, pattern: [])
107+
108+
class FakeTemporaryDirectory:
109+
def __init__(self, dir=None):
110+
pass
111+
112+
def __enter__(self):
113+
return str(link_dir)
114+
115+
def __exit__(self, exc_type, exc_val, exc_tb):
116+
return None
117+
118+
monkeypatch.setattr(general_module.tempfile, "TemporaryDirectory", FakeTemporaryDirectory)
119+
120+
calls = []
121+
122+
def fake_subprocess_run(args, **kwargs):
123+
calls.append({"args": [str(a) for a in args], "kwargs": kwargs})
124+
return subprocess.CompletedProcess(args, 0)
125+
126+
monkeypatch.setattr(subprocess, "run", fake_subprocess_run)
127+
128+
cli.execute("benchmark", no_color=True)
129+
130+
resolved = str(real_dir.resolve())
131+
assert resolved != str(link_dir) # sanity: symlink path really differs from its target
132+
# every uv invocation must run from the resolved real path, never the symlink prefix
133+
for call in calls:
134+
assert str(call["kwargs"].get("cwd")) == resolved
135+
# and the tontester path argument lives under that same resolved dir, so uv's
136+
# in-tree containment check succeeds and it installs editable
137+
add_args = calls[1]["args"]
138+
assert add_args[:3] == ["uv", "add", "--editable"]
139+
assert add_args[3].startswith(resolved)
140+
141+
76142
def test_benchmark_runs(cli, monkeypatch, tmp_path):
77143
monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/uv" if name == "uv" else None)
78144
monkeypatch.setattr(shutil, "copytree", lambda src, dst: None)
@@ -134,10 +200,10 @@ def fake_subprocess_run(args, **kwargs):
134200
assert "cwd" in calls[0]["kwargs"]
135201
tmp_dir = str(calls[0]["kwargs"]["cwd"])
136202

137-
# uv add tontester
203+
# uv add --editable tontester
138204
add_args = calls[1]["args"]
139-
assert add_args[:2] == ["uv", "add"]
140-
assert "tontester" in add_args[2]
205+
assert add_args[:3] == ["uv", "add", "--editable"]
206+
assert "tontester" in add_args[3]
141207

142208
# uv run generate_tl.py
143209
assert calls[2]["args"][:2] == ["uv", "run"]

tests/unit/test_clients.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,30 @@ def test_liteclient_run_falls_back_to_db_liteservers_when_out_of_sync(ton, mocke
9898
assert run_mock.call_args.args[0][-2:] == ["-i", "7"]
9999

100100

101+
def test_liteclient_run_local_connects_only_to_local_liteserver(ton, mocker: MockerFixture):
102+
run_mock = mocker.patch(
103+
"mytoncore.clients.subprocess.run", return_value=_completed(stdout=b"ok")
104+
)
105+
106+
output = ton.liteClient.run_local("runmethodx addr method")
107+
108+
assert output == "ok"
109+
assert run_mock.call_args.args[0] == [
110+
ton.liteClient.app_path,
111+
"--addr", ton.liteClient.addr,
112+
"--pub", ton.liteClient.pub_key_path,
113+
"--verbosity", "0",
114+
"--cmd", "runmethodx addr method",
115+
]
116+
117+
118+
def test_liteclient_run_local_raises_when_local_liteserver_not_configured(ton):
119+
ton.liteClient.addr = None
120+
121+
with pytest.raises(Exception, match="local liteserver is not configured"):
122+
ton.liteClient.run_local("last")
123+
124+
101125
def test_liteclient_run_raises_on_stderr(ton, mocker: MockerFixture):
102126
_stub_status(ton, 0)
103127
mocker.patch(

0 commit comments

Comments
 (0)