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.
- nova_langchain_rustchain-0.1.1/PKG-INFO +71 -0
- nova_langchain_rustchain-0.1.1/README.md +46 -0
- nova_langchain_rustchain-0.1.1/langchain_rustchain/__init__.py +26 -0
- nova_langchain_rustchain-0.1.1/langchain_rustchain/tools.py +236 -0
- nova_langchain_rustchain-0.1.1/nova_langchain_rustchain.egg-info/PKG-INFO +71 -0
- nova_langchain_rustchain-0.1.1/nova_langchain_rustchain.egg-info/SOURCES.txt +10 -0
- nova_langchain_rustchain-0.1.1/nova_langchain_rustchain.egg-info/dependency_links.txt +1 -0
- nova_langchain_rustchain-0.1.1/nova_langchain_rustchain.egg-info/requires.txt +5 -0
- nova_langchain_rustchain-0.1.1/nova_langchain_rustchain.egg-info/top_level.txt +1 -0
- nova_langchain_rustchain-0.1.1/pyproject.toml +40 -0
- nova_langchain_rustchain-0.1.1/setup.cfg +4 -0
- nova_langchain_rustchain-0.1.1/tests/test_tools.py +176 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langchain_rustchain
|
|
@@ -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,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
|