envarpay 0.1.0a6__py3-none-any.whl
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.
- envarpay/__init__.py +20 -0
- envarpay/__main__.py +3 -0
- envarpay/backend.py +55 -0
- envarpay/chain.py +128 -0
- envarpay/cli.py +315 -0
- envarpay/config.py +244 -0
- envarpay/directory.py +93 -0
- envarpay/host_config.py +74 -0
- envarpay/receiving.py +65 -0
- envarpay/runtime_http.py +152 -0
- envarpay/seller.py +402 -0
- envarpay/service.py +155 -0
- envarpay/setup.py +367 -0
- envarpay/storage.py +170 -0
- envarpay/tasks/__init__.py +1 -0
- envarpay/tasks/chain.py +277 -0
- envarpay/tasks/cli.py +115 -0
- envarpay/tasks/client.py +235 -0
- envarpay/tasks/config.py +109 -0
- envarpay/tasks/escrow.json +394 -0
- envarpay/tasks/models.py +60 -0
- envarpay/tasks/server.py +233 -0
- envarpay/tasks/store.py +109 -0
- envarpay/transport.py +56 -0
- envarpay/wallet.py +344 -0
- envarpay-0.1.0a6.dist-info/METADATA +100 -0
- envarpay-0.1.0a6.dist-info/RECORD +30 -0
- envarpay-0.1.0a6.dist-info/WHEEL +4 -0
- envarpay-0.1.0a6.dist-info/entry_points.txt +2 -0
- envarpay-0.1.0a6.dist-info/licenses/LICENSE +22 -0
envarpay/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Standard-protocol payment adapters and read-only configuration/receipt utilities."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0a6"
|
|
4
|
+
|
|
5
|
+
from .config import Config, load_config
|
|
6
|
+
|
|
7
|
+
__all__ = ["Config", "PaidServer", "WalletService", "load_config", "__version__"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __getattr__(name):
|
|
11
|
+
# Reading configuration or receipts does not load signer/runtime dependencies.
|
|
12
|
+
if name == "PaidServer":
|
|
13
|
+
from .seller import PaidServer
|
|
14
|
+
|
|
15
|
+
return PaidServer
|
|
16
|
+
if name == "WalletService":
|
|
17
|
+
from .wallet import WalletService
|
|
18
|
+
|
|
19
|
+
return WalletService
|
|
20
|
+
raise AttributeError(name)
|
envarpay/__main__.py
ADDED
envarpay/backend.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Thin adapters to existing Agent services. No Agent is instantiated here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mcp.types import CallToolResult, Tool
|
|
6
|
+
|
|
7
|
+
from .config import Backend
|
|
8
|
+
from .storage import PaymentError
|
|
9
|
+
from .transport import connect
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentBackend:
|
|
13
|
+
def __init__(self, config: Backend, timeout: int):
|
|
14
|
+
self.config, self.timeout = config, timeout
|
|
15
|
+
|
|
16
|
+
async def list_tools(self) -> list[Tool]:
|
|
17
|
+
if self.config.kind == "http":
|
|
18
|
+
from .runtime_http import RuntimeHTTP
|
|
19
|
+
|
|
20
|
+
RuntimeHTTP(self.config, self.timeout).credential()
|
|
21
|
+
return [
|
|
22
|
+
Tool(
|
|
23
|
+
name="ask_agent",
|
|
24
|
+
description="Ask the connected agent to perform a single task",
|
|
25
|
+
inputSchema={
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"question": {"type": "string", "minLength": 1, "maxLength": 32000}
|
|
29
|
+
},
|
|
30
|
+
"required": ["question"],
|
|
31
|
+
"additionalProperties": False,
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
async with connect(self.config.upstream, self.timeout) as session:
|
|
36
|
+
tools, cursor = [], None
|
|
37
|
+
for _ in range(4):
|
|
38
|
+
page = await session.list_tools(cursor=cursor)
|
|
39
|
+
tools.extend(page.tools)
|
|
40
|
+
if len(tools) > 64:
|
|
41
|
+
raise PaymentError("Expose at most 64 selected capabilities")
|
|
42
|
+
cursor = page.nextCursor
|
|
43
|
+
if not cursor:
|
|
44
|
+
return tools
|
|
45
|
+
raise PaymentError("Capability discovery exceeded the page limit")
|
|
46
|
+
|
|
47
|
+
async def call(self, name: str, arguments: dict) -> CallToolResult:
|
|
48
|
+
if self.config.kind == "mcp":
|
|
49
|
+
async with connect(self.config.upstream, self.timeout) as session:
|
|
50
|
+
return await session.call_tool(name, arguments)
|
|
51
|
+
from .runtime_http import RuntimeHTTP
|
|
52
|
+
|
|
53
|
+
if name != "ask_agent":
|
|
54
|
+
raise PaymentError("Unknown Agent capability")
|
|
55
|
+
return await RuntimeHTTP(self.config, self.timeout).call(arguments["question"])
|
envarpay/chain.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Independent USDC receipt checks, including the authorization nonce."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from eth_utils import keccak
|
|
11
|
+
|
|
12
|
+
from .config import Config, address
|
|
13
|
+
from .storage import PaymentError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Chain:
|
|
17
|
+
def __init__(self, config: Config):
|
|
18
|
+
self.config = config
|
|
19
|
+
|
|
20
|
+
async def rpc(self, method: str, params: list) -> Any:
|
|
21
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
22
|
+
response = await client.post(
|
|
23
|
+
self.config.rpc_url,
|
|
24
|
+
json={
|
|
25
|
+
"jsonrpc": "2.0",
|
|
26
|
+
"id": 1,
|
|
27
|
+
"method": method,
|
|
28
|
+
"params": params,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
data = response.json()
|
|
33
|
+
if "error" in data or "result" not in data:
|
|
34
|
+
raise PaymentError("RPC failed; preserve the original payment and inspect status")
|
|
35
|
+
return data["result"]
|
|
36
|
+
|
|
37
|
+
async def check_network(self) -> None:
|
|
38
|
+
if int(await self.rpc("eth_chainId", []), 16) != self.config.chain_id:
|
|
39
|
+
raise PaymentError("RPC chain does not match the configured network")
|
|
40
|
+
|
|
41
|
+
async def prove(self, tx: str, authorization: dict) -> dict:
|
|
42
|
+
await self.check_network()
|
|
43
|
+
if len(tx) != 66 or not tx.startswith("0x"):
|
|
44
|
+
raise PaymentError("Invalid settlement transaction hash")
|
|
45
|
+
end = time.monotonic() + self.config.timeout_seconds
|
|
46
|
+
while True:
|
|
47
|
+
receipt = await self.rpc("eth_getTransactionReceipt", [tx])
|
|
48
|
+
if receipt:
|
|
49
|
+
latest = int(await self.rpc("eth_blockNumber", []), 16)
|
|
50
|
+
if latest - int(receipt["blockNumber"], 16) + 1 >= self.config.confirmations:
|
|
51
|
+
break
|
|
52
|
+
if time.monotonic() >= end:
|
|
53
|
+
raise PaymentError("Receipt remains unconfirmed; do not sign again")
|
|
54
|
+
await asyncio.sleep(1)
|
|
55
|
+
block = await self.rpc("eth_getBlockByNumber", [receipt["blockNumber"], False])
|
|
56
|
+
log_index = self.validate_receipt(receipt, tx, authorization)
|
|
57
|
+
if not block or block["hash"].lower() != receipt["blockHash"].lower():
|
|
58
|
+
raise PaymentError("Receipt is not on the current canonical chain")
|
|
59
|
+
return {
|
|
60
|
+
"network": self.config.network,
|
|
61
|
+
"transaction": tx,
|
|
62
|
+
"asset": self.config.asset,
|
|
63
|
+
"payer": address(authorization["from"]),
|
|
64
|
+
"recipient": address(authorization["to"]),
|
|
65
|
+
"amount_atomic": str(authorization["value"]),
|
|
66
|
+
"nonce": authorization["nonce"],
|
|
67
|
+
"block_number": int(receipt["blockNumber"], 16),
|
|
68
|
+
"block_hash": receipt["blockHash"],
|
|
69
|
+
"log_index": log_index,
|
|
70
|
+
"confirmations": self.config.confirmations,
|
|
71
|
+
"verified_at": time.time(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async def find_authorization(self, authorization: dict, from_block: int) -> str | None:
|
|
75
|
+
await self.check_network()
|
|
76
|
+
latest = int(await self.rpc("eth_blockNumber", []), 16)
|
|
77
|
+
if from_block < 0 or latest - from_block > 50000:
|
|
78
|
+
raise PaymentError("Original payment needs a bounded chain audit")
|
|
79
|
+
used = "0x" + keccak(text="AuthorizationUsed(address,bytes32)").hex()
|
|
80
|
+
payer = "0x" + address(authorization["from"])[2:].lower().zfill(64)
|
|
81
|
+
logs = await self.rpc(
|
|
82
|
+
"eth_getLogs",
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
"address": self.config.asset,
|
|
86
|
+
"fromBlock": hex(from_block),
|
|
87
|
+
"toBlock": hex(latest),
|
|
88
|
+
"topics": [used, payer, authorization["nonce"]],
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
transactions = {log["transactionHash"] for log in logs if not log.get("removed")}
|
|
93
|
+
if len(transactions) > 1:
|
|
94
|
+
raise PaymentError("Ambiguous authorization history")
|
|
95
|
+
return next(iter(transactions), None)
|
|
96
|
+
|
|
97
|
+
def validate_receipt(self, receipt: dict, tx: str, authorization: dict) -> int:
|
|
98
|
+
return verify_usdc_receipt(receipt, tx, self.config.asset, authorization)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def verify_usdc_receipt(receipt: dict, tx: str, asset: str, authorization: dict) -> int:
|
|
102
|
+
"""Verify transfer identity without constructing a wallet or reading a signing key."""
|
|
103
|
+
if int(receipt["status"], 16) != 1 or receipt["transactionHash"].lower() != tx.lower():
|
|
104
|
+
raise PaymentError("Transaction did not succeed")
|
|
105
|
+
payer = "0x" + address(authorization["from"])[2:].lower().zfill(64)
|
|
106
|
+
recipient = "0x" + address(authorization["to"])[2:].lower().zfill(64)
|
|
107
|
+
transfer = "0x" + keccak(text="Transfer(address,address,uint256)").hex()
|
|
108
|
+
used = "0x" + keccak(text="AuthorizationUsed(address,bytes32)").hex()
|
|
109
|
+
logs = [
|
|
110
|
+
log
|
|
111
|
+
for log in receipt["logs"]
|
|
112
|
+
if log["address"].lower() == asset.lower() and not log.get("removed")
|
|
113
|
+
]
|
|
114
|
+
transfers = [
|
|
115
|
+
log
|
|
116
|
+
for log in logs
|
|
117
|
+
if [t.lower() for t in log["topics"]] == [transfer, payer, recipient]
|
|
118
|
+
and len(log["data"]) == 66
|
|
119
|
+
and int(log["data"], 16) == int(authorization["value"])
|
|
120
|
+
]
|
|
121
|
+
nonces = [
|
|
122
|
+
log
|
|
123
|
+
for log in logs
|
|
124
|
+
if [t.lower() for t in log["topics"]] == [used, payer, authorization["nonce"].lower()]
|
|
125
|
+
]
|
|
126
|
+
if len(transfers) != 1 or len(nonces) != 1:
|
|
127
|
+
raise PaymentError("Exact USDC Transfer and original AuthorizationUsed nonce required")
|
|
128
|
+
return int(transfers[0]["logIndex"], 16)
|
envarpay/cli.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Command-line setup, serving and read-only diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from eth_account import Account
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from pydantic import ValidationError
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
from .chain import Chain
|
|
18
|
+
from .config import load_config
|
|
19
|
+
from .setup import AGENTS, describe_config, initialize, write_new
|
|
20
|
+
from .storage import PaymentError, Store
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parser() -> argparse.ArgumentParser:
|
|
24
|
+
root = argparse.ArgumentParser(prog="envarpay", description="MCP + x402 agent payments")
|
|
25
|
+
root.add_argument("--version", action="version", version=__version__)
|
|
26
|
+
sub = root.add_subparsers(dest="command", required=True)
|
|
27
|
+
from .tasks.cli import add_parser
|
|
28
|
+
|
|
29
|
+
add_parser(sub)
|
|
30
|
+
init = sub.add_parser(
|
|
31
|
+
"init", help="Choose an agent and role; generate configs, wallet entry and setup guide"
|
|
32
|
+
)
|
|
33
|
+
init.add_argument(
|
|
34
|
+
"--directory", type=Path, default=Path("agent-pay"), help="New empty setup directory"
|
|
35
|
+
)
|
|
36
|
+
init.add_argument(
|
|
37
|
+
"--agent", choices=AGENTS, default="mcp", help="Your existing agent/framework"
|
|
38
|
+
)
|
|
39
|
+
init.add_argument(
|
|
40
|
+
"--role", choices=["buyer", "seller", "both"], help="Paid setup role (default: both)"
|
|
41
|
+
)
|
|
42
|
+
init.add_argument(
|
|
43
|
+
"--mode",
|
|
44
|
+
choices=["seller", "private"],
|
|
45
|
+
default="seller",
|
|
46
|
+
help="Private mode connects an authenticated service without payments",
|
|
47
|
+
)
|
|
48
|
+
init.add_argument(
|
|
49
|
+
"--pay-to",
|
|
50
|
+
help="Full receiving address: yours for seller/both, the seller's for buyer",
|
|
51
|
+
)
|
|
52
|
+
init.add_argument("--peer-pay-to", help="Other seller's full address when using --role both")
|
|
53
|
+
init.add_argument(
|
|
54
|
+
"--peer-url", default="http://127.0.0.1:4020/mcp", help="Paid seller MCP endpoint"
|
|
55
|
+
)
|
|
56
|
+
init.add_argument(
|
|
57
|
+
"--upstream", default="http://127.0.0.1:8000/mcp", help="Private MCP capability to sell"
|
|
58
|
+
)
|
|
59
|
+
init.add_argument("--tool", default="ask_agent", help="MCP tool to sell/allow")
|
|
60
|
+
amounts = init.add_argument_group("amounts in USDC (up to six decimals)")
|
|
61
|
+
amounts.add_argument(
|
|
62
|
+
"--price", default="0.01", help="Seller price per tool call (default: 0.01)"
|
|
63
|
+
)
|
|
64
|
+
amounts.add_argument(
|
|
65
|
+
"--max-per-call", default="0.01", help="Buyer per-call limit (default: 0.01)"
|
|
66
|
+
)
|
|
67
|
+
amounts.add_argument(
|
|
68
|
+
"--budget", default="0.01", help="Buyer cumulative limit, not daily (default: 0.01)"
|
|
69
|
+
)
|
|
70
|
+
init.add_argument(
|
|
71
|
+
"--allow-http",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Allow remote plain HTTP for private container networks",
|
|
74
|
+
)
|
|
75
|
+
init.add_argument("--json", action="store_true", help="Print the setup summary as JSON")
|
|
76
|
+
init.add_argument(
|
|
77
|
+
"--backend",
|
|
78
|
+
choices=["mcp", "openclaw", "hermes-http"],
|
|
79
|
+
default="mcp",
|
|
80
|
+
help="Seller adapter; defaults to private MCP. HTTP presets are experimental.",
|
|
81
|
+
)
|
|
82
|
+
keygen = sub.add_parser(
|
|
83
|
+
"keygen", help="Create a new dedicated EVM key, displaying only its address"
|
|
84
|
+
)
|
|
85
|
+
keygen.add_argument("--output", type=Path, required=True)
|
|
86
|
+
for command in [
|
|
87
|
+
"serve",
|
|
88
|
+
"wallet",
|
|
89
|
+
"wallet-serve",
|
|
90
|
+
"sync",
|
|
91
|
+
"recover",
|
|
92
|
+
"call",
|
|
93
|
+
"probe",
|
|
94
|
+
"tools",
|
|
95
|
+
"status",
|
|
96
|
+
"reconcile",
|
|
97
|
+
"host-config",
|
|
98
|
+
"doctor",
|
|
99
|
+
]:
|
|
100
|
+
item = sub.add_parser(command)
|
|
101
|
+
item.add_argument("--config", type=Path, required=True)
|
|
102
|
+
if command in ("call", "probe", "tools"):
|
|
103
|
+
item.add_argument("--peer", required=True)
|
|
104
|
+
if command in ("call", "probe"):
|
|
105
|
+
item.add_argument("--tool", required=True)
|
|
106
|
+
item.add_argument("--arguments", default="{}", help="JSON object")
|
|
107
|
+
if command == "call":
|
|
108
|
+
item.add_argument(
|
|
109
|
+
"--request-id", required=True, help="Stable ID; reuse for the same request"
|
|
110
|
+
)
|
|
111
|
+
if command == "recover":
|
|
112
|
+
item.add_argument("--request-id", required=True)
|
|
113
|
+
if command == "sync":
|
|
114
|
+
item.add_argument("--watch", action="store_true")
|
|
115
|
+
if command in ("status", "reconcile"):
|
|
116
|
+
item.add_argument("--operation-id", required=command == "reconcile")
|
|
117
|
+
if command == "host-config":
|
|
118
|
+
item.add_argument(
|
|
119
|
+
"--host", choices=["hermes", "openclaw", "opencode", "goose"], required=True
|
|
120
|
+
)
|
|
121
|
+
if command == "doctor":
|
|
122
|
+
item.add_argument("--online", action="store_true", help="Also read RPC chain ID")
|
|
123
|
+
return root
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def read_or_call(args: argparse.Namespace) -> dict | list:
|
|
127
|
+
from x402 import x402Client
|
|
128
|
+
from x402.mcp.client import x402MCPSession
|
|
129
|
+
|
|
130
|
+
from .transport import connect
|
|
131
|
+
from .wallet import WalletService
|
|
132
|
+
|
|
133
|
+
config = load_config(args.config)
|
|
134
|
+
if args.command == "sync":
|
|
135
|
+
from .directory import Envar
|
|
136
|
+
|
|
137
|
+
if not config.connection:
|
|
138
|
+
raise PaymentError("Configure [connection] before synchronizing reports")
|
|
139
|
+
envar = Envar(config.connection, Store(config.state_dir))
|
|
140
|
+
while True:
|
|
141
|
+
result = await envar.flush()
|
|
142
|
+
if not args.watch:
|
|
143
|
+
return result
|
|
144
|
+
await asyncio.sleep(5)
|
|
145
|
+
if args.command == "recover":
|
|
146
|
+
return await WalletService(config).recover(args.request_id)
|
|
147
|
+
if args.command == "doctor":
|
|
148
|
+
endpoint = config.seller or config.service
|
|
149
|
+
if endpoint and endpoint.backend.kind == "http":
|
|
150
|
+
from .runtime_http import RuntimeHTTP
|
|
151
|
+
|
|
152
|
+
RuntimeHTTP(endpoint.backend, config.timeout_seconds).credential()
|
|
153
|
+
if config.service:
|
|
154
|
+
from .service import AgentService
|
|
155
|
+
|
|
156
|
+
AgentService(config)
|
|
157
|
+
rpc_checked = bool(args.online and (config.seller or config.wallet))
|
|
158
|
+
if rpc_checked:
|
|
159
|
+
await Chain(config).check_network()
|
|
160
|
+
if args.online and endpoint:
|
|
161
|
+
from .backend import AgentBackend
|
|
162
|
+
|
|
163
|
+
await AgentBackend(endpoint.backend, config.timeout_seconds).list_tools()
|
|
164
|
+
return {
|
|
165
|
+
"valid": True,
|
|
166
|
+
**describe_config(config),
|
|
167
|
+
"rpc_checked": rpc_checked,
|
|
168
|
+
"versions": {name: version(name) for name in ("envarpay", "x402", "mcp")},
|
|
169
|
+
}
|
|
170
|
+
if args.command in ("status", "reconcile"):
|
|
171
|
+
store = Store(config.state_dir)
|
|
172
|
+
if args.command == "status":
|
|
173
|
+
return store.public_status(args.operation_id)
|
|
174
|
+
row = store.get(args.operation_id)
|
|
175
|
+
if not row or not row["data"].get("transaction") or not row["data"].get("payload"):
|
|
176
|
+
raise PaymentError(
|
|
177
|
+
"No recorded transaction to reconcile; this command never resubmits payment"
|
|
178
|
+
)
|
|
179
|
+
return await Chain(config).prove(
|
|
180
|
+
row["data"]["transaction"], row["data"]["payload"]["payload"]["authorization"]
|
|
181
|
+
)
|
|
182
|
+
service = WalletService(config)
|
|
183
|
+
if args.command == "tools":
|
|
184
|
+
return await service.list_tools(args.peer)
|
|
185
|
+
arguments = json.loads(args.arguments)
|
|
186
|
+
if not isinstance(arguments, dict):
|
|
187
|
+
raise PaymentError("--arguments must be a JSON object")
|
|
188
|
+
if args.command == "call":
|
|
189
|
+
return await service.call(args.peer, args.tool, arguments, args.request_id)
|
|
190
|
+
peer = service.peer(args.peer, args.tool)
|
|
191
|
+
async with connect(peer, config.timeout_seconds) as session:
|
|
192
|
+
result = await x402MCPSession(session, x402Client(), auto_payment=False).call_tool(
|
|
193
|
+
args.tool, arguments
|
|
194
|
+
)
|
|
195
|
+
return {
|
|
196
|
+
"payment_made": False,
|
|
197
|
+
"result": result.raw_result.model_dump(mode="json", by_alias=True),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main() -> None:
|
|
202
|
+
logger.remove()
|
|
203
|
+
logger.add(sys.stderr, level="WARNING", format="{level}: {message}")
|
|
204
|
+
args = parser().parse_args()
|
|
205
|
+
try:
|
|
206
|
+
if args.command == "task":
|
|
207
|
+
from .tasks.cli import run
|
|
208
|
+
|
|
209
|
+
run(args)
|
|
210
|
+
return
|
|
211
|
+
if args.command == "init":
|
|
212
|
+
result = initialize(
|
|
213
|
+
args.directory,
|
|
214
|
+
args.pay_to,
|
|
215
|
+
args.backend,
|
|
216
|
+
mode=args.mode,
|
|
217
|
+
agent=args.agent,
|
|
218
|
+
role=args.role,
|
|
219
|
+
upstream=args.upstream,
|
|
220
|
+
peer_url=args.peer_url,
|
|
221
|
+
peer_pay_to=args.peer_pay_to,
|
|
222
|
+
tool=args.tool,
|
|
223
|
+
price=args.price,
|
|
224
|
+
max_per_call=args.max_per_call,
|
|
225
|
+
budget=args.budget,
|
|
226
|
+
allow_http=args.allow_http,
|
|
227
|
+
)
|
|
228
|
+
if not args.json:
|
|
229
|
+
print(f"EnvarPay · {result['agent']} · {result['role']}")
|
|
230
|
+
print(f"Network: {result['network']} | Buyer payments: OFF")
|
|
231
|
+
if result["buyer"]:
|
|
232
|
+
buyer = result["buyer"]
|
|
233
|
+
print(f"\nBuy from: {buyer['peer_url']}")
|
|
234
|
+
print(f" Recipient: {buyer['recipient']}")
|
|
235
|
+
print(f" Tool: {buyer['tool']}")
|
|
236
|
+
print(
|
|
237
|
+
f" Limit: {buyer['max_per_call_usdc']} USDC/call; "
|
|
238
|
+
f"{buyer['budget_usdc']} USDC cumulative"
|
|
239
|
+
)
|
|
240
|
+
if result["seller"]:
|
|
241
|
+
seller = result["seller"]
|
|
242
|
+
print(f"\nSell: {seller['tool']} for {seller['price_usdc']} USDC/call")
|
|
243
|
+
print(f" Receive at: {seller['recipient']}")
|
|
244
|
+
print(f" Private upstream: {seller['upstream']} ({seller['backend']})")
|
|
245
|
+
print("\nCreated in " + result["directory"] + ": " + ", ".join(result["files"]))
|
|
246
|
+
print("\nNext:")
|
|
247
|
+
for index, step in enumerate(result["next_steps"], 1):
|
|
248
|
+
print(f" {index}. {step}")
|
|
249
|
+
print("\nGuide: " + result["guide"])
|
|
250
|
+
return
|
|
251
|
+
elif args.command == "keygen":
|
|
252
|
+
account = Account.create()
|
|
253
|
+
write_new(args.output.expanduser(), account.key.hex() + "\n")
|
|
254
|
+
result = {
|
|
255
|
+
"address": account.address,
|
|
256
|
+
"key_file": str(args.output),
|
|
257
|
+
"private_key_printed": False,
|
|
258
|
+
}
|
|
259
|
+
elif args.command == "host-config":
|
|
260
|
+
from .host_config import host_config
|
|
261
|
+
|
|
262
|
+
result = host_config(args.host, args.config)
|
|
263
|
+
elif args.command == "wallet":
|
|
264
|
+
from .wallet import WalletService, wallet_mcp
|
|
265
|
+
|
|
266
|
+
wallet_mcp(WalletService(load_config(args.config))).run(transport="stdio")
|
|
267
|
+
return
|
|
268
|
+
elif args.command == "wallet-serve":
|
|
269
|
+
import uvicorn
|
|
270
|
+
|
|
271
|
+
from .wallet import WalletService, wallet_app
|
|
272
|
+
|
|
273
|
+
config = load_config(args.config)
|
|
274
|
+
app = wallet_app(WalletService(config))
|
|
275
|
+
uvicorn.run(
|
|
276
|
+
app,
|
|
277
|
+
host=config.wallet_server.host,
|
|
278
|
+
port=config.wallet_server.port,
|
|
279
|
+
log_level="warning",
|
|
280
|
+
)
|
|
281
|
+
return
|
|
282
|
+
elif args.command == "serve":
|
|
283
|
+
import uvicorn
|
|
284
|
+
|
|
285
|
+
from .seller import PaidServer
|
|
286
|
+
from .service import AgentService
|
|
287
|
+
|
|
288
|
+
config = load_config(args.config)
|
|
289
|
+
service = PaidServer(config) if config.seller else AgentService(config)
|
|
290
|
+
uvicorn.run(
|
|
291
|
+
service.app(config_path=args.config) if config.seller else service.app(),
|
|
292
|
+
host=service.policy.host,
|
|
293
|
+
port=service.policy.port,
|
|
294
|
+
log_level="warning",
|
|
295
|
+
)
|
|
296
|
+
return
|
|
297
|
+
else:
|
|
298
|
+
result = asyncio.run(read_or_call(args))
|
|
299
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
300
|
+
except (PaymentError, FileExistsError, FileNotFoundError) as error:
|
|
301
|
+
print(f"envarpay: {error}", file=sys.stderr)
|
|
302
|
+
raise SystemExit(1) from None
|
|
303
|
+
except ValidationError as error:
|
|
304
|
+
print(
|
|
305
|
+
"envarpay: invalid configuration: "
|
|
306
|
+
+ "; ".join(
|
|
307
|
+
f"{'.'.join(map(str, e['loc']))}: {e['msg']}"
|
|
308
|
+
for e in error.errors(include_input=False)
|
|
309
|
+
),
|
|
310
|
+
file=sys.stderr,
|
|
311
|
+
)
|
|
312
|
+
raise SystemExit(1) from None
|
|
313
|
+
except Exception as error:
|
|
314
|
+
print(f"envarpay: {type(error).__name__}; operation did not complete", file=sys.stderr)
|
|
315
|
+
raise SystemExit(1) from None
|