nova-langchain-rustchain 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: nova-langchain-rustchain
3
+ Version: 0.1.1
4
+ Summary: LangChain integration for RustChain — check balances, list bounties, node health, current epoch
5
+ Author-email: NOVA LAB <oussamabouleghlem43@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://rustchain.org
8
+ Project-URL: Source, https://github.com/Scottcjn/Rustchain
9
+ Project-URL: Bounty Program, https://github.com/Scottcjn/rustchain-bounties
10
+ Project-URL: BugTracker, https://github.com/Scottcjn/rustchain-bounties/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: langchain-core>=0.1.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Provides-Extra: requests
24
+ Requires-Dist: requests>=2.31.0; extra == "requests"
25
+
26
+ # nova-langchain-rustchain
27
+
28
+ LangChain tools for the [RustChain](https://rustchain.org) DePIN blockchain — Proof of Antiquity mining on vintage hardware.
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ pip install nova-langchain-rustchain
34
+ ```
35
+
36
+ Then use with any LangChain agent:
37
+
38
+ ```python
39
+ from langchain_rustchain import get_tools
40
+
41
+ tools = get_tools()
42
+ agent = create_react_agent(llm, tools) # or any LangChain agent
43
+ ```
44
+
45
+ ## Tools
46
+
47
+ | Tool | Description | Source |
48
+ |------|-------------|--------|
49
+ | `rustchain_check_balance` | Check RTC balance of any wallet (`/wallet/balance`) | Node API |
50
+ | `rustchain_list_bounties` | List open ecosystem bounties (GitHub issues) | GitHub API |
51
+ | `rustchain_get_node_health` | Read node health status (`/health`) | Node API |
52
+ | `rustchain_get_current_epoch` | Read current epoch / slot (`/epoch`) | Node API |
53
+
54
+ All tools are read-only, no auth required, wallet = any string.
55
+
56
+ ## Why RustChain?
57
+
58
+ - **Agent-native**: no auth, no captcha, wallet = any string, same-day RTC payout.
59
+ - **DePIN for vintage hardware**: old machines outmine new ones (Proof of Antiquity).
60
+ - **Solana bridge (wRTC)**: cross-chain liquidity.
61
+ - 55+ ecosystem repos, 5+ languages, 15+ CPU architectures.
62
+
63
+ ## Example
64
+
65
+ See [example.py](example.py) for a complete agent script.
66
+
67
+ ## Links
68
+
69
+ - [RustChain](https://rustchain.org)
70
+ - [Bounties](https://github.com/Scottcjn/rustchain-bounties)
71
+ - [Source](https://github.com/Scottcjn/Rustchain)
@@ -0,0 +1,46 @@
1
+ # nova-langchain-rustchain
2
+
3
+ LangChain tools for the [RustChain](https://rustchain.org) DePIN blockchain — Proof of Antiquity mining on vintage hardware.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pip install nova-langchain-rustchain
9
+ ```
10
+
11
+ Then use with any LangChain agent:
12
+
13
+ ```python
14
+ from langchain_rustchain import get_tools
15
+
16
+ tools = get_tools()
17
+ agent = create_react_agent(llm, tools) # or any LangChain agent
18
+ ```
19
+
20
+ ## Tools
21
+
22
+ | Tool | Description | Source |
23
+ |------|-------------|--------|
24
+ | `rustchain_check_balance` | Check RTC balance of any wallet (`/wallet/balance`) | Node API |
25
+ | `rustchain_list_bounties` | List open ecosystem bounties (GitHub issues) | GitHub API |
26
+ | `rustchain_get_node_health` | Read node health status (`/health`) | Node API |
27
+ | `rustchain_get_current_epoch` | Read current epoch / slot (`/epoch`) | Node API |
28
+
29
+ All tools are read-only, no auth required, wallet = any string.
30
+
31
+ ## Why RustChain?
32
+
33
+ - **Agent-native**: no auth, no captcha, wallet = any string, same-day RTC payout.
34
+ - **DePIN for vintage hardware**: old machines outmine new ones (Proof of Antiquity).
35
+ - **Solana bridge (wRTC)**: cross-chain liquidity.
36
+ - 55+ ecosystem repos, 5+ languages, 15+ CPU architectures.
37
+
38
+ ## Example
39
+
40
+ See [example.py](example.py) for a complete agent script.
41
+
42
+ ## Links
43
+
44
+ - [RustChain](https://rustchain.org)
45
+ - [Bounties](https://github.com/Scottcjn/rustchain-bounties)
46
+ - [Source](https://github.com/Scottcjn/Rustchain)
@@ -0,0 +1,26 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """RustChain LangChain integration — check balances, list bounties, inspect node health, read current epoch.
3
+
4
+ Exposes ``RustChainBalanceTool``, ``RustChainBountiesTool``,
5
+ ``RustChainNodeHealthTool``, and ``RustChainCurrentEpochTool`` as
6
+ LangChain ``BaseTool`` subclasses, plus a convenience ``get_tools()``
7
+ factory and ``ALL_RUSTCHAIN_TOOLS`` list.
8
+ """
9
+
10
+ from langchain_rustchain.tools import (
11
+ ALL_RUSTCHAIN_TOOLS,
12
+ RustChainBalanceTool,
13
+ RustChainBountiesTool,
14
+ RustChainCurrentEpochTool,
15
+ RustChainNodeHealthTool,
16
+ get_tools,
17
+ )
18
+
19
+ __all__ = [
20
+ "RustChainBalanceTool",
21
+ "RustChainBountiesTool",
22
+ "RustChainNodeHealthTool",
23
+ "RustChainCurrentEpochTool",
24
+ "ALL_RUSTCHAIN_TOOLS",
25
+ "get_tools",
26
+ ]
@@ -0,0 +1,236 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """RustChain LangChain tools.
3
+
4
+ A LangChain ``BaseTool`` exposing read-only RustChain node endpoints so an
5
+ LLM agent can check wallet balances, list ecosystem bounties, inspect node
6
+ health, and read the current epoch — all against the public RustChain node
7
+ (https://rustchain.org, wallet = any string, no auth).
8
+
9
+ Dependencies: ``langchain-core`` and ``httpx`` (or ``requests``).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from langchain_core.tools import BaseTool
19
+ from pydantic import BaseModel, Field
20
+
21
+ try: # httpx preferred (async-capable), fall back to requests
22
+ import httpx
23
+
24
+ _HTTPX = True
25
+ except ImportError: # pragma: no cover - environment fallback
26
+ httpx = None # type: ignore[assignment]
27
+ _HTTPX = False
28
+ import requests
29
+
30
+ DEFAULT_NODE = "https://rustchain.org"
31
+ DEFAULT_BOUNTIES_REPO = "Scottcjn/rustchain-bounties"
32
+ TIMEOUT_S = 15.0
33
+
34
+
35
+ class _Client:
36
+ """Tiny sync HTTP helper so the tools don't drag in a full SDK."""
37
+
38
+ def __init__(self, node_url: str, timeout: float = TIMEOUT_S, headers: Optional[Dict[str, str]] = None):
39
+ self.node_url = node_url.rstrip("/")
40
+ self.timeout = timeout
41
+ self.headers = headers or {}
42
+
43
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
44
+ url = f"{self.node_url}{path}"
45
+ try:
46
+ if _HTTPX:
47
+ resp = httpx.get(url, params=params, headers=self.headers, timeout=self.timeout)
48
+ else:
49
+ resp = requests.get(url, params=params, headers=self.headers, timeout=self.timeout)
50
+ resp.raise_for_status()
51
+ except Exception as exc:
52
+ return {"ok": False, "error": str(exc)}
53
+ try:
54
+ return resp.json()
55
+ except ValueError:
56
+ return {"ok": False, "error": "non_json_response", "status": resp.status_code}
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Schemas (pydantic) — one input schema per tool improves tool-call reliability.
61
+ # ---------------------------------------------------------------------------
62
+
63
+ class CheckBalanceInput(BaseModel):
64
+ wallet_id: str = Field(
65
+ description="The RustChain wallet id / miner address to look up. "
66
+ "Any string wallet is accepted (agent-native, no auth)."
67
+ )
68
+
69
+
70
+ class ListBountiesInput(BaseModel):
71
+ limit: int = Field(
72
+ default=10,
73
+ ge=1,
74
+ le=50,
75
+ description="Maximum number of open bounties to return.",
76
+ )
77
+
78
+
79
+ class GetNodeHealthInput(BaseModel):
80
+ pass
81
+
82
+
83
+ class GetCurrentEpochInput(BaseModel):
84
+ pass
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Tools
89
+ # ---------------------------------------------------------------------------
90
+
91
+ class RustChainBalanceTool(BaseTool):
92
+ """Check the RTC balance of a RustChain wallet."""
93
+
94
+ name: str = "rustchain_check_balance"
95
+ description: str = (
96
+ "Check the RTC balance of a RustChain wallet id. "
97
+ "Returns the balance in RTC (and the raw integer amount). "
98
+ "Use for questions like 'how much RTC does wallet X hold?'."
99
+ )
100
+ args_schema: type[BaseModel] = CheckBalanceInput
101
+
102
+ node_url: str = DEFAULT_NODE
103
+
104
+ def _run(self, wallet_id: str) -> str:
105
+ client = _Client(self.node_url)
106
+ data = client.get(
107
+ "/wallet/balance", params={"miner_id": wallet_id}
108
+ )
109
+ if not data.get("ok", True):
110
+ return json.dumps({"ok": False, "error": data.get("error", "unknown")})
111
+ return json.dumps(
112
+ {
113
+ "ok": True,
114
+ "wallet_id": data.get("miner_id", wallet_id),
115
+ "balance_rtc": data.get("amount_rtc", 0.0),
116
+ "balance_raw": data.get("amount_i64", 0),
117
+ }
118
+ )
119
+
120
+
121
+ class RustChainBountiesTool(BaseTool):
122
+ """List open RustChain ecosystem bounties."""
123
+
124
+ name: str = "rustchain_list_bounties"
125
+ description: str = (
126
+ "List currently open RustChain ecosystem bounties (Earn RTC by "
127
+ "contributing: code, docs, security, community). Returns the most "
128
+ "recent open bounties with their issue number, title, and labels."
129
+ )
130
+ args_schema: type[BaseModel] = ListBountiesInput
131
+
132
+ bounties_repo: str = DEFAULT_BOUNTIES_REPO
133
+
134
+ def _run(self, limit: int = 10) -> str:
135
+ # Bounties are tracked as GitHub issues labelled ``bounty`` in the
136
+ # rustchain-bounties repo. Use the public GitHub REST API.
137
+ # Reads GH_TOKEN or GITHUB_TOKEN from the environment for higher
138
+ # rate limits (optional — the endpoint is unauthenticated-friendly).
139
+ headers = {}
140
+ token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") or ""
141
+ if token:
142
+ headers["Authorization"] = f"Bearer {token}"
143
+ client = _Client("https://api.github.com", headers=headers)
144
+ data = client.get(
145
+ f"/repos/{self.bounties_repo}/issues",
146
+ params={"state": "open", "labels": "bounty", "per_page": limit},
147
+ )
148
+ if isinstance(data, list):
149
+ items = []
150
+ for issue in data:
151
+ items.append(
152
+ {
153
+ "number": issue.get("number"),
154
+ "title": issue.get("title"),
155
+ "url": issue.get("html_url"),
156
+ "labels": [
157
+ lbl.get("name")
158
+ for lbl in issue.get("labels", [])
159
+ ],
160
+ }
161
+ )
162
+ return json.dumps({"ok": True, "count": len(items), "bounties": items})
163
+ return json.dumps({"ok": False, "error": str(data)})
164
+
165
+
166
+ class RustChainNodeHealthTool(BaseTool):
167
+ """Read RustChain node health status."""
168
+
169
+ name: str = "rustchain_get_node_health"
170
+ description: str = (
171
+ "Get the current health status of the RustChain public node: "
172
+ "database read/write availability, tip age, uptime, and version. "
173
+ "Use to answer 'is RustChain up / healthy?'."
174
+ )
175
+ args_schema: type[BaseModel] = GetNodeHealthInput
176
+
177
+ node_url: str = DEFAULT_NODE
178
+
179
+ def _run(self) -> str:
180
+ client = _Client(self.node_url)
181
+ data = client.get("/health")
182
+ return json.dumps(data)
183
+
184
+
185
+ class RustChainCurrentEpochTool(BaseTool):
186
+ """Read the current RustChain epoch."""
187
+
188
+ name: str = "rustchain_get_current_epoch"
189
+ description: str = (
190
+ "Get the current RustChain epoch, current slot, enrolled miner count, "
191
+ "and per-epoch RTC emission. Use for questions about the current "
192
+ "consensus epoch / mining round."
193
+ )
194
+ args_schema: type[BaseModel] = GetCurrentEpochInput
195
+
196
+ node_url: str = DEFAULT_NODE
197
+
198
+ def _run(self) -> str:
199
+ client = _Client(self.node_url)
200
+ data = client.get("/epoch")
201
+ return json.dumps(data)
202
+
203
+
204
+ # Handy collection for loading all tools at once.
205
+ ALL_RUSTCHAIN_TOOLS: List[BaseTool] = [
206
+ RustChainBalanceTool(),
207
+ RustChainBountiesTool(),
208
+ RustChainNodeHealthTool(),
209
+ RustChainCurrentEpochTool(),
210
+ ]
211
+
212
+
213
+ def get_tools(node_url: str = DEFAULT_NODE, bounties_repo: str = DEFAULT_BOUNTIES_REPO) -> List[BaseTool]:
214
+ """Return a fresh list of all RustChain tools with custom endpoints.
215
+
216
+ Args:
217
+ node_url: RustChain node base URL (defaults to the public node).
218
+ bounties_repo: ``owner/repo`` of the bounty issue tracker
219
+ (defaults to Scottcjn/rustchain-bounties).
220
+ """
221
+ return [
222
+ RustChainBalanceTool(node_url=node_url),
223
+ RustChainBountiesTool(bounties_repo=bounties_repo),
224
+ RustChainNodeHealthTool(node_url=node_url),
225
+ RustChainCurrentEpochTool(node_url=node_url),
226
+ ]
227
+
228
+
229
+ __all__ = [
230
+ "RustChainBalanceTool",
231
+ "RustChainBountiesTool",
232
+ "RustChainNodeHealthTool",
233
+ "RustChainCurrentEpochTool",
234
+ "ALL_RUSTCHAIN_TOOLS",
235
+ "get_tools",
236
+ ]
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: nova-langchain-rustchain
3
+ Version: 0.1.1
4
+ Summary: LangChain integration for RustChain — check balances, list bounties, node health, current epoch
5
+ Author-email: NOVA LAB <oussamabouleghlem43@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://rustchain.org
8
+ Project-URL: Source, https://github.com/Scottcjn/Rustchain
9
+ Project-URL: Bounty Program, https://github.com/Scottcjn/rustchain-bounties
10
+ Project-URL: BugTracker, https://github.com/Scottcjn/rustchain-bounties/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: langchain-core>=0.1.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Provides-Extra: requests
24
+ Requires-Dist: requests>=2.31.0; extra == "requests"
25
+
26
+ # nova-langchain-rustchain
27
+
28
+ LangChain tools for the [RustChain](https://rustchain.org) DePIN blockchain — Proof of Antiquity mining on vintage hardware.
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ pip install nova-langchain-rustchain
34
+ ```
35
+
36
+ Then use with any LangChain agent:
37
+
38
+ ```python
39
+ from langchain_rustchain import get_tools
40
+
41
+ tools = get_tools()
42
+ agent = create_react_agent(llm, tools) # or any LangChain agent
43
+ ```
44
+
45
+ ## Tools
46
+
47
+ | Tool | Description | Source |
48
+ |------|-------------|--------|
49
+ | `rustchain_check_balance` | Check RTC balance of any wallet (`/wallet/balance`) | Node API |
50
+ | `rustchain_list_bounties` | List open ecosystem bounties (GitHub issues) | GitHub API |
51
+ | `rustchain_get_node_health` | Read node health status (`/health`) | Node API |
52
+ | `rustchain_get_current_epoch` | Read current epoch / slot (`/epoch`) | Node API |
53
+
54
+ All tools are read-only, no auth required, wallet = any string.
55
+
56
+ ## Why RustChain?
57
+
58
+ - **Agent-native**: no auth, no captcha, wallet = any string, same-day RTC payout.
59
+ - **DePIN for vintage hardware**: old machines outmine new ones (Proof of Antiquity).
60
+ - **Solana bridge (wRTC)**: cross-chain liquidity.
61
+ - 55+ ecosystem repos, 5+ languages, 15+ CPU architectures.
62
+
63
+ ## Example
64
+
65
+ See [example.py](example.py) for a complete agent script.
66
+
67
+ ## Links
68
+
69
+ - [RustChain](https://rustchain.org)
70
+ - [Bounties](https://github.com/Scottcjn/rustchain-bounties)
71
+ - [Source](https://github.com/Scottcjn/Rustchain)
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ langchain_rustchain/__init__.py
4
+ langchain_rustchain/tools.py
5
+ nova_langchain_rustchain.egg-info/PKG-INFO
6
+ nova_langchain_rustchain.egg-info/SOURCES.txt
7
+ nova_langchain_rustchain.egg-info/dependency_links.txt
8
+ nova_langchain_rustchain.egg-info/requires.txt
9
+ nova_langchain_rustchain.egg-info/top_level.txt
10
+ tests/test_tools.py
@@ -0,0 +1,5 @@
1
+ langchain-core>=0.1.0
2
+ httpx>=0.27.0
3
+
4
+ [requests]
5
+ requests>=2.31.0
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nova-langchain-rustchain"
7
+ version = "0.1.1"
8
+ description = "LangChain integration for RustChain — check balances, list bounties, node health, current epoch"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ {name = "NOVA LAB", email = "oussamabouleghlem43@gmail.com"},
14
+ ]
15
+ dependencies = [
16
+ "langchain-core>=0.1.0",
17
+ "httpx>=0.27.0",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://rustchain.org"
32
+ Source = "https://github.com/Scottcjn/Rustchain"
33
+ "Bounty Program" = "https://github.com/Scottcjn/rustchain-bounties"
34
+ BugTracker = "https://github.com/Scottcjn/rustchain-bounties/issues"
35
+
36
+ [project.optional-dependencies]
37
+ requests = ["requests>=2.31.0"]
38
+
39
+ [tool.setuptools.packages.find]
40
+ include = ["langchain_rustchain*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,176 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Offline tests for langchain_rustchain tools.
3
+
4
+ These tests stub the HTTP layer, so they run without network access and
5
+ without a live RustChain node.
6
+ """
7
+
8
+ import json
9
+
10
+ import pytest
11
+
12
+ import langchain_rustchain.tools as tools
13
+ from langchain_rustchain import (
14
+ ALL_RUSTCHAIN_TOOLS,
15
+ RustChainBalanceTool,
16
+ RustChainBountiesTool,
17
+ RustChainCurrentEpochTool,
18
+ RustChainNodeHealthTool,
19
+ get_tools,
20
+ )
21
+
22
+
23
+ class _FakeResp:
24
+ def __init__(self, payload=None, status=200, bad_json=False):
25
+ self._payload = payload
26
+ self.status_code = status
27
+ self._bad_json = bad_json
28
+
29
+ def raise_for_status(self):
30
+ if self.status_code >= 400:
31
+ raise RuntimeError(f"HTTP {self.status_code}")
32
+
33
+ def json(self):
34
+ if self._bad_json:
35
+ raise ValueError("not json")
36
+ return self._payload
37
+
38
+
39
+ def _patch_httpx(monkeypatch, resp, capture=None):
40
+ def fake_get(url, params=None, headers=None, timeout=None):
41
+ if capture is not None:
42
+ capture.update(url=url, params=params, headers=headers, timeout=timeout)
43
+ if isinstance(resp, Exception):
44
+ raise resp
45
+ return resp
46
+
47
+ monkeypatch.setattr(tools.httpx, "get", fake_get)
48
+
49
+
50
+ def test_balance_tool_maps_node_response(monkeypatch):
51
+ capture = {}
52
+ _patch_httpx(
53
+ monkeypatch,
54
+ _FakeResp({"miner_id": "alice", "amount_rtc": 12.5, "amount_i64": 12_500_000_000}),
55
+ capture,
56
+ )
57
+
58
+ out = json.loads(RustChainBalanceTool()._run("alice"))
59
+
60
+ assert out["ok"] is True
61
+ assert out["wallet_id"] == "alice"
62
+ assert out["balance_rtc"] == 12.5
63
+ assert out["balance_raw"] == 12_500_000_000
64
+ assert capture["url"].endswith("/wallet/balance")
65
+ assert capture["params"] == {"miner_id": "alice"}
66
+
67
+
68
+ def test_balance_tool_handles_transport_error(monkeypatch):
69
+ _patch_httpx(monkeypatch, ConnectionError("boom"))
70
+
71
+ out = json.loads(RustChainBalanceTool()._run("alice"))
72
+
73
+ assert out["ok"] is False
74
+ assert "boom" in out["error"]
75
+
76
+
77
+ def test_bounties_tool_maps_issue_list(monkeypatch):
78
+ capture = {}
79
+ issues = [
80
+ {
81
+ "number": 3074,
82
+ "title": "Integrate RustChain as a native LangChain tool",
83
+ "html_url": "https://github.com/Scottcjn/rustchain-bounties/issues/3074",
84
+ "labels": [{"name": "bounty"}, {"name": "agent-welcome"}],
85
+ }
86
+ ]
87
+ _patch_httpx(monkeypatch, _FakeResp(issues), capture)
88
+
89
+ out = json.loads(RustChainBountiesTool()._run(limit=5))
90
+
91
+ assert out["ok"] is True
92
+ assert out["count"] == 1
93
+ assert out["bounties"][0]["number"] == 3074
94
+ assert out["bounties"][0]["labels"] == ["bounty", "agent-welcome"]
95
+ assert capture["url"].endswith("/repos/Scottcjn/rustchain-bounties/issues")
96
+ assert capture["params"]["labels"] == "bounty"
97
+ assert capture["params"]["state"] == "open"
98
+
99
+
100
+ def test_bounties_tool_surfaces_api_error_object(monkeypatch):
101
+ _patch_httpx(monkeypatch, _FakeResp({"message": "rate limited"}))
102
+
103
+ out = json.loads(RustChainBountiesTool()._run())
104
+
105
+ assert out["ok"] is False
106
+ assert "rate limited" in out["error"]
107
+
108
+
109
+ def test_bounties_tool_sends_token_header(monkeypatch):
110
+ capture = {}
111
+ _patch_httpx(monkeypatch, _FakeResp([]), capture)
112
+ monkeypatch.setenv("GH_TOKEN", "ghp_example")
113
+
114
+ RustChainBountiesTool()._run()
115
+
116
+ assert capture["headers"].get("Authorization") == "Bearer ghp_example"
117
+
118
+
119
+ def test_health_tool_passes_through(monkeypatch):
120
+ capture = {}
121
+ payload = {"ok": True, "db_rw": True, "tip_age_slots": 0, "version": "2.2.1-rip200"}
122
+ _patch_httpx(monkeypatch, _FakeResp(payload), capture)
123
+
124
+ out = json.loads(RustChainNodeHealthTool()._run())
125
+
126
+ assert out == payload
127
+ assert capture["url"].endswith("/health")
128
+
129
+
130
+ def test_epoch_tool_passes_through(monkeypatch):
131
+ capture = {}
132
+ payload = {"epoch": 288, "slot": 41586, "enrolled_miners": 17}
133
+ _patch_httpx(monkeypatch, _FakeResp(payload), capture)
134
+
135
+ out = json.loads(RustChainCurrentEpochTool()._run())
136
+
137
+ assert out == payload
138
+ assert capture["url"].endswith("/epoch")
139
+
140
+
141
+ def test_non_json_response_is_reported(monkeypatch):
142
+ _patch_httpx(monkeypatch, _FakeResp(status=200, bad_json=True))
143
+
144
+ out = json.loads(RustChainNodeHealthTool()._run())
145
+
146
+ assert out["ok"] is False
147
+ assert out["error"] == "non_json_response"
148
+
149
+
150
+ def test_get_tools_returns_expected_four():
151
+ names = {t.name for t in get_tools()}
152
+ assert names == {
153
+ "rustchain_check_balance",
154
+ "rustchain_list_bounties",
155
+ "rustchain_get_node_health",
156
+ "rustchain_get_current_epoch",
157
+ }
158
+
159
+
160
+ def test_get_tools_custom_endpoints():
161
+ t = get_tools(node_url="https://example.test", bounties_repo="me/repo")
162
+ assert t[0].node_url == "https://example.test"
163
+ assert t[1].bounties_repo == "me/repo"
164
+
165
+
166
+ def test_all_tools_collection_is_instantiated():
167
+ assert len(ALL_RUSTCHAIN_TOOLS) == 4
168
+ assert all(isinstance(t, tools.BaseTool) for t in ALL_RUSTCHAIN_TOOLS)
169
+
170
+
171
+ def test_langchain_tool_invoke(monkeypatch):
172
+ _patch_httpx(monkeypatch, _FakeResp({"miner_id": "z", "amount_rtc": 1.0, "amount_i64": 1_000_000_000}))
173
+
174
+ result = RustChainBalanceTool().invoke({"wallet_id": "z"})
175
+
176
+ assert json.loads(result)["balance_rtc"] == 1.0