Skip to content
Open
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
46 changes: 46 additions & 0 deletions integrations/langchain-rustchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# langchain-rustchain

LangChain tools for the [RustChain](https://rustchain.org) DePIN blockchain — Proof of Antiquity mining on vintage hardware.

## Quick Start

```bash
pip install langchain-rustchain
```

Then use with any LangChain agent:

```python
from langchain_rustchain import get_tools

tools = get_tools()
agent = create_react_agent(llm, tools) # or any LangChain agent
```

## Tools

| Tool | Description | Source |
|------|-------------|--------|
| `rustchain_check_balance` | Check RTC balance of any wallet (`/wallet/balance`) | Node API |
| `rustchain_list_bounties` | List open ecosystem bounties (GitHub issues) | GitHub API |
| `rustchain_get_node_health` | Read node health status (`/health`) | Node API |
| `rustchain_get_current_epoch` | Read current epoch / slot (`/epoch`) | Node API |

All tools are read-only, no auth required, wallet = any string.

## Why RustChain?

- **Agent-native**: no auth, no captcha, wallet = any string, same-day RTC payout.
- **DePIN for vintage hardware**: old machines outmine new ones (Proof of Antiquity).
- **Solana bridge (wRTC)**: cross-chain liquidity.
- 55+ ecosystem repos, 5+ languages, 15+ CPU architectures.

## Example

See [example.py](example.py) for a complete agent script.

## Links

- [RustChain](https://rustchain.org)
- [Bounties](https://github.com/Scottcjn/rustchain-bounties)
- [Source](https://github.com/Scottcjn/Rustchain)
56 changes: 56 additions & 0 deletions integrations/langchain-rustchain/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Example: Use the RustChain LangChain tools to check a wallet balance
and list open bounties.

Usage:
pip install langchain-rustchain
python example.py

This script works with any LangChain-compatible LLM (OpenAI, Anthropic,
Ollama, etc.) or you can just call the tools directly without an LLM.
"""

from langchain_rustchain import (
RustChainBalanceTool,
RustChainBountiesTool,
RustChainCurrentEpochTool,
RustChainNodeHealthTool,
)


def direct_tool_demo():
"""Demonstrate each tool by calling it directly (no LLM needed)."""

print("=" * 60)
print("RustChain LangChain Tools — Direct Demo")
print("=" * 60)

# 1. Node health
print("\n--- 1. Node Health ---")
health = RustChainNodeHealthTool()._run()
print(health)

# 2. Current epoch
print("\n--- 2. Current Epoch ---")
epoch = RustChainCurrentEpochTool()._run()
print(epoch)

# 3. Wallet balance (any string wallet)
print("\n--- 3. Wallet Balance (test wallet) ---")
balance = RustChainBalanceTool()._run("test")
print(balance)

# 4. Open bounties
print("\n--- 4. Open Bounties (top 5) ---")
bounties = RustChainBountiesTool()._run(limit=5)
print(bounties)

print("\n" + "=" * 60)
print("All tools work. Integrate into your LangChain agent with:")
print(" from langchain_rustchain import get_tools")
print(" tools = get_tools()")
print("=" * 60)


if __name__ == "__main__":
direct_tool_demo()
26 changes: 26 additions & 0 deletions integrations/langchain-rustchain/langchain_rustchain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: MIT
"""RustChain LangChain integration — check balances, list bounties, inspect node health, read current epoch.

Exposes ``RustChainBalanceTool``, ``RustChainBountiesTool``,
``RustChainNodeHealthTool``, and ``RustChainCurrentEpochTool`` as
LangChain ``BaseTool`` subclasses, plus a convenience ``get_tools()``
factory and ``ALL_RUSTCHAIN_TOOLS`` list.
"""

from langchain_rustchain.tools import (
ALL_RUSTCHAIN_TOOLS,
RustChainBalanceTool,
RustChainBountiesTool,
RustChainCurrentEpochTool,
RustChainNodeHealthTool,
get_tools,
)

__all__ = [
"RustChainBalanceTool",
"RustChainBountiesTool",
"RustChainNodeHealthTool",
"RustChainCurrentEpochTool",
"ALL_RUSTCHAIN_TOOLS",
"get_tools",
]
236 changes: 236 additions & 0 deletions integrations/langchain-rustchain/langchain_rustchain/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# SPDX-License-Identifier: MIT
"""RustChain LangChain tools.

A LangChain ``BaseTool`` exposing read-only RustChain node endpoints so an
LLM agent can check wallet balances, list ecosystem bounties, inspect node
health, and read the current epoch — all against the public RustChain node
(https://rustchain.org, wallet = any string, no auth).

Dependencies: ``langchain-core`` and ``httpx`` (or ``requests``).
"""

from __future__ import annotations

import json
import os
from typing import Any, Dict, List, Optional

from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field

try: # httpx preferred (async-capable), fall back to requests
import httpx

_HTTPX = True
except ImportError: # pragma: no cover - environment fallback
httpx = None # type: ignore[assignment]
_HTTPX = False
import requests

DEFAULT_NODE = "https://rustchain.org"
DEFAULT_BOUNTIES_REPO = "Scottcjn/rustchain-bounties"
TIMEOUT_S = 15.0


class _Client:
"""Tiny sync HTTP helper so the tools don't drag in a full SDK."""

def __init__(self, node_url: str, timeout: float = TIMEOUT_S, headers: Optional[Dict[str, str]] = None):
self.node_url = node_url.rstrip("/")
self.timeout = timeout
self.headers = headers or {}

def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = f"{self.node_url}{path}"
try:
if _HTTPX:
resp = httpx.get(url, params=params, headers=self.headers, timeout=self.timeout)
else:
resp = requests.get(url, params=params, headers=self.headers, timeout=self.timeout)
resp.raise_for_status()
except Exception as exc:
return {"ok": False, "error": str(exc)}
try:
return resp.json()
except ValueError:
return {"ok": False, "error": "non_json_response", "status": resp.status_code}


# ---------------------------------------------------------------------------
# Schemas (pydantic) — one input schema per tool improves tool-call reliability.
# ---------------------------------------------------------------------------

class CheckBalanceInput(BaseModel):
wallet_id: str = Field(
description="The RustChain wallet id / miner address to look up. "
"Any string wallet is accepted (agent-native, no auth)."
)


class ListBountiesInput(BaseModel):
limit: int = Field(
default=10,
ge=1,
le=50,
description="Maximum number of open bounties to return.",
)


class GetNodeHealthInput(BaseModel):
pass


class GetCurrentEpochInput(BaseModel):
pass


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------

class RustChainBalanceTool(BaseTool):
"""Check the RTC balance of a RustChain wallet."""

name: str = "rustchain_check_balance"
description: str = (
"Check the RTC balance of a RustChain wallet id. "
"Returns the balance in RTC (and the raw integer amount). "
"Use for questions like 'how much RTC does wallet X hold?'."
)
args_schema: type[BaseModel] = CheckBalanceInput

node_url: str = DEFAULT_NODE

def _run(self, wallet_id: str) -> str:
client = _Client(self.node_url)
data = client.get(
"/wallet/balance", params={"miner_id": wallet_id}
)
if not data.get("ok", True):
return json.dumps({"ok": False, "error": data.get("error", "unknown")})
return json.dumps(
{
"ok": True,
"wallet_id": data.get("miner_id", wallet_id),
"balance_rtc": data.get("amount_rtc", 0.0),
"balance_raw": data.get("amount_i64", 0),
}
)


class RustChainBountiesTool(BaseTool):
"""List open RustChain ecosystem bounties."""

name: str = "rustchain_list_bounties"
description: str = (
"List currently open RustChain ecosystem bounties (Earn RTC by "
"contributing: code, docs, security, community). Returns the most "
"recent open bounties with their issue number, title, and labels."
)
args_schema: type[BaseModel] = ListBountiesInput

bounties_repo: str = DEFAULT_BOUNTIES_REPO

def _run(self, limit: int = 10) -> str:
# Bounties are tracked as GitHub issues labelled ``bounty`` in the
# rustchain-bounties repo. Use the public GitHub REST API.
# Reads GH_TOKEN or GITHUB_TOKEN from the environment for higher
# rate limits (optional — the endpoint is unauthenticated-friendly).
headers = {}
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") or ""
if token:
headers["Authorization"] = f"Bearer {token}"
client = _Client("https://api.github.com", headers=headers)
data = client.get(
f"/repos/{self.bounties_repo}/issues",
params={"state": "open", "labels": "bounty", "per_page": limit},
)
if isinstance(data, list):
items = []
for issue in data:
items.append(
{
"number": issue.get("number"),
"title": issue.get("title"),
"url": issue.get("html_url"),
"labels": [
lbl.get("name")
for lbl in issue.get("labels", [])
],
}
)
return json.dumps({"ok": True, "count": len(items), "bounties": items})
return json.dumps({"ok": False, "error": str(data)})


class RustChainNodeHealthTool(BaseTool):
"""Read RustChain node health status."""

name: str = "rustchain_get_node_health"
description: str = (
"Get the current health status of the RustChain public node: "
"database read/write availability, tip age, uptime, and version. "
"Use to answer 'is RustChain up / healthy?'."
)
args_schema: type[BaseModel] = GetNodeHealthInput

node_url: str = DEFAULT_NODE

def _run(self) -> str:
client = _Client(self.node_url)
data = client.get("/health")
return json.dumps(data)


class RustChainCurrentEpochTool(BaseTool):
"""Read the current RustChain epoch."""

name: str = "rustchain_get_current_epoch"
description: str = (
"Get the current RustChain epoch, current slot, enrolled miner count, "
"and per-epoch RTC emission. Use for questions about the current "
"consensus epoch / mining round."
)
args_schema: type[BaseModel] = GetCurrentEpochInput

node_url: str = DEFAULT_NODE

def _run(self) -> str:
client = _Client(self.node_url)
data = client.get("/epoch")
return json.dumps(data)


# Handy collection for loading all tools at once.
ALL_RUSTCHAIN_TOOLS: List[BaseTool] = [
RustChainBalanceTool(),
RustChainBountiesTool(),
RustChainNodeHealthTool(),
RustChainCurrentEpochTool(),
]


def get_tools(node_url: str = DEFAULT_NODE, bounties_repo: str = DEFAULT_BOUNTIES_REPO) -> List[BaseTool]:
"""Return a fresh list of all RustChain tools with custom endpoints.

Args:
node_url: RustChain node base URL (defaults to the public node).
bounties_repo: ``owner/repo`` of the bounty issue tracker
(defaults to Scottcjn/rustchain-bounties).
"""
return [
RustChainBalanceTool(node_url=node_url),
RustChainBountiesTool(bounties_repo=bounties_repo),
RustChainNodeHealthTool(node_url=node_url),
RustChainCurrentEpochTool(node_url=node_url),
]


__all__ = [
"RustChainBalanceTool",
"RustChainBountiesTool",
"RustChainNodeHealthTool",
"RustChainCurrentEpochTool",
"ALL_RUSTCHAIN_TOOLS",
"get_tools",
]
Loading