Skip to content
Merged

Dev #586

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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff pytest pytest-mock
pip install ruff==0.15.22 pytest pytest-mock

- name: Run Ruff
run: |
Expand Down
12 changes: 1 addition & 11 deletions modules/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
from mytonctrl.utils import GetItemFromList
from modules.module import MtcModule

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mytoncore import MyTonCore


class ControllerModule(MtcModule):

Expand Down Expand Up @@ -229,18 +225,12 @@ def do_calculate_loan_amount_test(self):
max_loan = self.ton.local.db.get("max_loan", 43000)
max_interest_percent = self.ton.local.db.get("max_interest_percent", 1.5)
max_interest = int(max_interest_percent / 100 * 16777216)
return self.ton.CalculateLoanAmount(min_loan, max_loan, max_interest)
return self.ton.calculate_loan_amount(min_loan, max_loan, max_interest)

def calculate_loan_amount_test(self, args):
t = self.do_calculate_loan_amount_test()
print(t)

@classmethod
def check_enable(cls, ton: "MyTonCore"):
from mytoninstaller.mytoninstaller import InstallerCtrl
installer = InstallerCtrl.from_ton(ton)
installer.enable_ton_http_api()

def add_console_commands(self, console):
add_command(self.local, console, "create_controllers", self.create_controllers)
add_command(self.local, console, "update_controllers", self.create_controllers)
Expand Down
6 changes: 4 additions & 2 deletions modules/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ def run_benchmark(self, args: list[str]):
return

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

subprocess.run(["uv", "add", tontester_dir], cwd=tmp_dir, check=True)
subprocess.run(
["uv", "add", "--editable", tontester_dir], cwd=tmp_dir, check=True
)

subprocess.run(
["uv", "run", tontester_dir / "generate_tl.py"],
Expand Down
3 changes: 1 addition & 2 deletions mytoncore/background_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,10 @@ def _offers(self):
self._ton.VoteOffer(offer)

def _complaints(self):
return
validator_index = self._ton.GetValidatorIndex()
Comment on lines 60 to 62
if validator_index < 0:
return
if time.time() < 1776643200:
return

# Voting for complaints
config32 = self._ton.get_config_32()
Expand Down
9 changes: 8 additions & 1 deletion mytoncore/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,20 @@ def run(self, cmd: str, timeout: int | None = None, index: int | None = None, us
if index is not None:
args += ["-i", str(index)]
elif use_local and self.pub_key_path and self.addr and out_of_sync is not None and out_of_sync < 20:
args = ["--addr", self.addr, "--pub", self.pub_key_path, "--verbosity", "0", "--cmd", cmd]
return self.run_local(cmd, timeout)
else:
ls_list = self.local.db.get("liteServers")
if ls_list:
args += ["-i", str(random.choice(ls_list))]
return self._run(args, timeout)

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


@dataclass
class ValidatorConsole(CliTool):
Expand Down
42 changes: 27 additions & 15 deletions mytoncore/mytoncore.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
Dict, int2ip, MyPyClass,
parse_int_forced
)
from mytoncore.vm_stack import parse_result_stack
from mytoncore.vm_stack import parse_result_stack, parse_remote_result_stack


class MyTonCore:
Expand Down Expand Up @@ -180,11 +180,18 @@ def get_paths(self) -> Paths:
return Paths()
return Paths.from_dict(paths)

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

def run_get_method_local(self, addr: str, method: str, params: list | None = None) -> list[str]:
cmd = f"runmethod {addr} {method}"
if params:
cmd += " " + " ".join(map(str, params))
result = self.liteClient.run_local(cmd)
return parse_remote_result_stack(result)

def get_seqno(self, wallet: Wallet) -> int:
seqno = int(self.run_get_method(wallet.addrB64, "seqno")[0])
wallet.seqno = seqno
Expand Down Expand Up @@ -2531,7 +2538,7 @@ def CreateLoanRequest(self, controllerAddr):
return

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

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

def CalculateLoanAmount(self, min_loan, max_loan, max_interest):
data = dict()
data["address"] = self.GetLiquidPoolAddr()
data["method"] = "calculate_loan_amount"
data["stack"] = [
["num", min_loan*10**9],
["num", max_loan*10**9],
["num", max_interest],
]
print(f"CalculateLoanAmount data: {data}")

def calculate_loan_amount(self, min_loan: int, max_loan: int, max_interest: int) -> int:
pool_addr = self.GetLiquidPoolAddr()
params = [min_loan*10**9, max_loan*10**9, max_interest]
try:
result = self.run_get_method_local(pool_addr, "calculate_loan_amount", params)
return int(result[-1])
except Exception as e:
self.local.add_log(f"Failed to calculate loan amount: {e}, params: {params}. Falling back to local ton-http-api", "warning")
return self.calculate_loan_amount_tha(pool_addr, params)

def calculate_loan_amount_tha(self, pool_addr: str, params: list) -> int:
data = {
"address": pool_addr,
"method": "calculate_loan_amount",
"stack": [["num", param] for param in params],
}
url = "http://127.0.0.1:8801/runGetMethod"
res = requests.post(url, json=data, timeout=3)
res_data = res.json()
if res_data.get("ok") is False:
error = res_data.get("error")
raise Exception(error)
result = res_data.get("result").get("stack").pop().pop()
return result
return int(result, 16)

def WaitLoan(self, controllerAddr):
self.local.add_log("start WaitLoan function", "debug")
Expand Down
20 changes: 16 additions & 4 deletions mytoncore/vm_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,33 @@
import re


_RESULT_RE = re.compile(r"(?m)^[^\S\n]*result:[^\S\n]*")
_REMOTE_RESULT_RE = re.compile(r"(?m)^[^\S\n]*remote result(?: \(not to be trusted\))?:[^\S\n]*")


def parse_result_stack(output: str) -> list[str]:
match = re.compile(r"(?m)^[^\S\n]*result:[^\S\n]*").search(output)
return _parse_section_stack(output, _RESULT_RE, "result")


def parse_remote_result_stack(output: str) -> list[str]:
return _parse_section_stack(output, _REMOTE_RESULT_RE, "remote result")


def _parse_section_stack(output: str, section_re: re.Pattern[str], section_name: str) -> list[str]:
match = section_re.search(output)
if not match:
raise ValueError("'result' section was not found")
raise ValueError(f"'{section_name}' section was not found")

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

values, pos = _parse_stack_as_strings(output, pos)
pos = _skip_ws(output, pos)
if pos != len(output):
raise ValueError(f"unexpected text after result stack: {output[pos:pos + 40]!r}")
raise ValueError(f"unexpected text after {section_name} stack: {output[pos:pos + 40]!r}")
return values


Expand Down
72 changes: 69 additions & 3 deletions tests/integration/test_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,72 @@ class FakeStat:
assert root_calls == []


def test_benchmark_resolves_symlinked_tmp_dir(cli, monkeypatch, tmp_path):
# Regression: when the temp dir path contains a symlink (e.g. ton_work is a
# symlink like /var/ton-work -> /mnt/data/ton-work), the cwd passed to uv must be
# resolved. Otherwise uv's getcwd() (real path) differs from the symlink-prefixed
# tontester_dir argument, its in-tree workspace-member check fails, and tontester
# installs non-editable -- hiding the generated `tonapi` package (ModuleNotFoundError).
# Real filesystem setup FIRST, before Path.mkdir is patched to a no-op below,
# otherwise real_dir is never created and `link` becomes a dangling symlink.
real_dir = tmp_path / "real"
real_dir.mkdir()
link_dir = tmp_path / "link"
link_dir.symlink_to(real_dir) # symlink prefix, like a symlinked ton_work

monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/uv" if name == "uv" else None)
monkeypatch.setattr(shutil, "copytree", lambda src, dst: None)
monkeypatch.setattr(shutil, "copy", lambda src, dst: None)

# Only pretend the benchmark temp parent is missing; delegate every other path to the
# real lstat, otherwise a globally-broken os.lstat would also break Path.resolve()
# (which follows symlinks via lstat) -- the very behavior under test.
real_lstat = general_module.os.lstat

def fake_lstat(path):
if str(path) == "/var/ton-work/tmp":
raise FileNotFoundError
return real_lstat(path)

monkeypatch.setattr(general_module.os, "lstat", fake_lstat)
monkeypatch.setattr(general_module, "run_as_root", lambda args: 0)
monkeypatch.setattr(Path, "mkdir", lambda *a, **kw: None)
monkeypatch.setattr(Path, "glob", lambda self, pattern: [])

class FakeTemporaryDirectory:
def __init__(self, dir=None):
pass

def __enter__(self):
return str(link_dir)

def __exit__(self, exc_type, exc_val, exc_tb):
return None

monkeypatch.setattr(general_module.tempfile, "TemporaryDirectory", FakeTemporaryDirectory)

calls = []

def fake_subprocess_run(args, **kwargs):
calls.append({"args": [str(a) for a in args], "kwargs": kwargs})
return subprocess.CompletedProcess(args, 0)

monkeypatch.setattr(subprocess, "run", fake_subprocess_run)

cli.execute("benchmark", no_color=True)

resolved = str(real_dir.resolve())
assert resolved != str(link_dir) # sanity: symlink path really differs from its target
# every uv invocation must run from the resolved real path, never the symlink prefix
for call in calls:
assert str(call["kwargs"].get("cwd")) == resolved
# and the tontester path argument lives under that same resolved dir, so uv's
# in-tree containment check succeeds and it installs editable
add_args = calls[1]["args"]
assert add_args[:3] == ["uv", "add", "--editable"]
assert add_args[3].startswith(resolved)


def test_benchmark_runs(cli, monkeypatch, tmp_path):
monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/uv" if name == "uv" else None)
monkeypatch.setattr(shutil, "copytree", lambda src, dst: None)
Expand Down Expand Up @@ -134,10 +200,10 @@ def fake_subprocess_run(args, **kwargs):
assert "cwd" in calls[0]["kwargs"]
tmp_dir = str(calls[0]["kwargs"]["cwd"])

# uv add tontester
# uv add --editable tontester
add_args = calls[1]["args"]
assert add_args[:2] == ["uv", "add"]
assert "tontester" in add_args[2]
assert add_args[:3] == ["uv", "add", "--editable"]
assert "tontester" in add_args[3]

# uv run generate_tl.py
assert calls[2]["args"][:2] == ["uv", "run"]
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ def test_liteclient_run_falls_back_to_db_liteservers_when_out_of_sync(ton, mocke
assert run_mock.call_args.args[0][-2:] == ["-i", "7"]


def test_liteclient_run_local_connects_only_to_local_liteserver(ton, mocker: MockerFixture):
run_mock = mocker.patch(
"mytoncore.clients.subprocess.run", return_value=_completed(stdout=b"ok")
)

output = ton.liteClient.run_local("runmethodx addr method")

assert output == "ok"
assert run_mock.call_args.args[0] == [
ton.liteClient.app_path,
"--addr", ton.liteClient.addr,
"--pub", ton.liteClient.pub_key_path,
"--verbosity", "0",
"--cmd", "runmethodx addr method",
]


def test_liteclient_run_local_raises_when_local_liteserver_not_configured(ton):
ton.liteClient.addr = None

with pytest.raises(Exception, match="local liteserver is not configured"):
ton.liteClient.run_local("last")


def test_liteclient_run_raises_on_stderr(ton, mocker: MockerFixture):
_stub_status(ton, 0)
mocker.patch(
Expand Down
Loading
Loading