agentlaunch-templates 0.1.0
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.
- package/dist/generator.d.ts +43 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +213 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +47 -0
- package/dist/registry.js.map +1 -0
- package/dist/templates/custom.d.ts +11 -0
- package/dist/templates/custom.d.ts.map +1 -0
- package/dist/templates/custom.js +458 -0
- package/dist/templates/custom.js.map +1 -0
- package/dist/templates/data-analyzer.d.ts +11 -0
- package/dist/templates/data-analyzer.d.ts.map +1 -0
- package/dist/templates/data-analyzer.js +565 -0
- package/dist/templates/data-analyzer.js.map +1 -0
- package/dist/templates/gifter.d.ts +15 -0
- package/dist/templates/gifter.d.ts.map +1 -0
- package/dist/templates/gifter.js +717 -0
- package/dist/templates/gifter.js.map +1 -0
- package/dist/templates/price-monitor.d.ts +11 -0
- package/dist/templates/price-monitor.d.ts.map +1 -0
- package/dist/templates/price-monitor.js +577 -0
- package/dist/templates/price-monitor.js.map +1 -0
- package/dist/templates/research.d.ts +11 -0
- package/dist/templates/research.d.ts.map +1 -0
- package/dist/templates/research.js +593 -0
- package/dist/templates/research.js.map +1 -0
- package/dist/templates/trading-bot.d.ts +11 -0
- package/dist/templates/trading-bot.d.ts.map +1 -0
- package/dist/templates/trading-bot.js +559 -0
- package/dist/templates/trading-bot.js.map +1 -0
- package/package.json +24 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gifter.ts — FET Gifter pattern: distributes testnet FET and BNB to new developers
|
|
3
|
+
*
|
|
4
|
+
* This template is based on the FET Gifter agent pattern (faucet type from
|
|
5
|
+
* packages/cli/src/commands/scaffold.ts). It adapts the gifter use case for
|
|
6
|
+
* Agentverse-native deployment with Chat Protocol v0.3.0.
|
|
7
|
+
*
|
|
8
|
+
* Platform constants (source of truth: deployed smart contracts):
|
|
9
|
+
* - Deploy fee: 120 FET (read dynamically, can change via multi-sig)
|
|
10
|
+
* - Graduation target: 30,000 FET -> auto DEX listing
|
|
11
|
+
* - Trading fee: 2% -> 100% to protocol treasury (NO creator fee)
|
|
12
|
+
*/
|
|
13
|
+
export const template = {
|
|
14
|
+
name: "gifter",
|
|
15
|
+
description: "Distributes testnet FET and BNB to new developers — the FET Gifter pattern",
|
|
16
|
+
category: "Infrastructure",
|
|
17
|
+
variables: [
|
|
18
|
+
{ name: "agent_name", required: true, description: "Name of the agent" },
|
|
19
|
+
{
|
|
20
|
+
name: "description",
|
|
21
|
+
default: "Distributes testnet FET and BNB to new developers",
|
|
22
|
+
description: "Short description of what this agent does",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "daily_gift_limit_fet",
|
|
26
|
+
default: "100",
|
|
27
|
+
description: "Maximum FET to gift per user per day",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "daily_gift_limit_bnb",
|
|
31
|
+
default: "0.01",
|
|
32
|
+
description: "Maximum BNB to gift per user per day",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "treasury_address",
|
|
36
|
+
required: true,
|
|
37
|
+
description: "Wallet address that holds the gift funds",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "rate_limit_per_minute",
|
|
41
|
+
default: "5",
|
|
42
|
+
description: "Max gift requests per user per minute",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "free_requests_per_day",
|
|
46
|
+
default: "3",
|
|
47
|
+
description: "Free gift requests allowed per user per day",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "premium_token_threshold",
|
|
51
|
+
default: "1000",
|
|
52
|
+
description: "Token balance required for premium tier (higher gift limit)",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
dependencies: ["requests", "web3"],
|
|
56
|
+
secrets: [
|
|
57
|
+
"AGENTVERSE_API_KEY",
|
|
58
|
+
"AGENTLAUNCH_API_KEY",
|
|
59
|
+
"AGENT_ADDRESS",
|
|
60
|
+
"AGENT_OWNER_ADDRESS",
|
|
61
|
+
"TREASURY_PRIVATE_KEY",
|
|
62
|
+
"BSC_TESTNET_RPC",
|
|
63
|
+
"ETH_SEPOLIA_RPC",
|
|
64
|
+
],
|
|
65
|
+
code: `#!/usr/bin/env python3
|
|
66
|
+
"""
|
|
67
|
+
{{agent_name}} — AgentLaunch FET Gifter Agent
|
|
68
|
+
Generated by: agentlaunch scaffold {{agent_name}} --type gifter
|
|
69
|
+
|
|
70
|
+
Distributes testnet FET (BSC Testnet) and BNB to new developers so they can
|
|
71
|
+
explore the AgentLaunch platform without needing to acquire testnet funds.
|
|
72
|
+
|
|
73
|
+
Pattern: FET Gifter — Agent + Wallet + Chat + Value Exchange = Ecosystem onboarding
|
|
74
|
+
|
|
75
|
+
Platform constants (source of truth: deployed smart contracts):
|
|
76
|
+
- Deploy fee: 120 FET (read dynamically, can change via multi-sig)
|
|
77
|
+
- Graduation target: 30,000 FET -> auto DEX listing
|
|
78
|
+
- Trading fee: 2% -> 100% to protocol treasury (NO creator fee)
|
|
79
|
+
|
|
80
|
+
SECURITY NOTES:
|
|
81
|
+
- TREASURY_PRIVATE_KEY must be set as an Agentverse secret, never hardcoded
|
|
82
|
+
- Treasury wallet should hold ONLY testnet funds
|
|
83
|
+
- All gift recipients are logged for audit
|
|
84
|
+
- Daily limits enforced per recipient address
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
from uagents import Agent, Context, Protocol
|
|
88
|
+
from uagents_core.contrib.protocols.chat import (
|
|
89
|
+
ChatAcknowledgement,
|
|
90
|
+
ChatMessage,
|
|
91
|
+
EndSessionContent,
|
|
92
|
+
TextContent,
|
|
93
|
+
chat_protocol_spec,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
import json
|
|
97
|
+
import os
|
|
98
|
+
import time
|
|
99
|
+
from collections import defaultdict
|
|
100
|
+
from datetime import datetime, date
|
|
101
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
102
|
+
from uuid import uuid4
|
|
103
|
+
|
|
104
|
+
import requests
|
|
105
|
+
|
|
106
|
+
# ==============================================================================
|
|
107
|
+
# API CONFIG — Override via environment variables, never hardcode
|
|
108
|
+
# ==============================================================================
|
|
109
|
+
|
|
110
|
+
AGENTLAUNCH_API = os.environ.get("AGENTLAUNCH_API", "https://agent-launch.ai/api")
|
|
111
|
+
BSC_TESTNET_RPC = os.environ.get("BSC_TESTNET_RPC", "https://data-seed-prebsc-1-s1.binance.org:8545")
|
|
112
|
+
ETH_SEPOLIA_RPC = os.environ.get("ETH_SEPOLIA_RPC", "https://rpc.sepolia.org")
|
|
113
|
+
|
|
114
|
+
# Testnet FET contract on BSC Testnet
|
|
115
|
+
FET_CONTRACT_BSC_TESTNET = "0x210778e62C17b4F7B6De7ab27346e4C35e3b1b5"
|
|
116
|
+
|
|
117
|
+
# ==============================================================================
|
|
118
|
+
# BUSINESS CONFIG
|
|
119
|
+
# ==============================================================================
|
|
120
|
+
|
|
121
|
+
OWNER_ADDRESS = os.environ.get("AGENT_OWNER_ADDRESS", "")
|
|
122
|
+
TREASURY_ADDRESS = os.environ.get("TREASURY_ADDRESS", "{{treasury_address}}")
|
|
123
|
+
|
|
124
|
+
BUSINESS = {
|
|
125
|
+
"name": "{{agent_name}}",
|
|
126
|
+
"description": "{{description}}",
|
|
127
|
+
"version": "1.0.0",
|
|
128
|
+
"daily_gift_limit_fet": float("{{daily_gift_limit_fet}}"),
|
|
129
|
+
"daily_gift_limit_bnb": float("{{daily_gift_limit_bnb}}"),
|
|
130
|
+
"free_requests_per_day": {{free_requests_per_day}},
|
|
131
|
+
"premium_token_threshold": {{premium_token_threshold}},
|
|
132
|
+
"rate_limit_per_minute": {{rate_limit_per_minute}},
|
|
133
|
+
"max_input_length": 5000,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ERC20 minimal ABI for FET transfers
|
|
137
|
+
ERC20_ABI = [
|
|
138
|
+
{
|
|
139
|
+
"name": "transfer",
|
|
140
|
+
"type": "function",
|
|
141
|
+
"inputs": [
|
|
142
|
+
{"name": "to", "type": "address"},
|
|
143
|
+
{"name": "value", "type": "uint256"},
|
|
144
|
+
],
|
|
145
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
146
|
+
"stateMutability": "nonpayable",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "balanceOf",
|
|
150
|
+
"type": "function",
|
|
151
|
+
"inputs": [{"name": "account", "type": "address"}],
|
|
152
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
153
|
+
"stateMutability": "view",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "decimals",
|
|
157
|
+
"type": "function",
|
|
158
|
+
"inputs": [],
|
|
159
|
+
"outputs": [{"name": "", "type": "uint8"}],
|
|
160
|
+
"stateMutability": "view",
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ==============================================================================
|
|
166
|
+
# LAYER 1: FOUNDATION
|
|
167
|
+
# ==============================================================================
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Logger:
|
|
171
|
+
"""Structured logging with audit trail."""
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def info(ctx: Context, event: str, data: Optional[Dict] = None) -> None:
|
|
175
|
+
ctx.logger.info(f"[{event}] {json.dumps(data or {})}")
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def audit(ctx: Context, user: str, action: str) -> None:
|
|
179
|
+
ctx.logger.info(
|
|
180
|
+
f"[AUDIT] user={user[:20]} action={action} "
|
|
181
|
+
f"ts={datetime.utcnow().isoformat()}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def error(ctx: Context, event: str, error: str) -> None:
|
|
186
|
+
ctx.logger.error(f"[{event}] {error}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ==============================================================================
|
|
190
|
+
# LAYER 2: SECURITY
|
|
191
|
+
# ==============================================================================
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Security:
|
|
195
|
+
"""Rate limiting and input validation."""
|
|
196
|
+
|
|
197
|
+
def __init__(self) -> None:
|
|
198
|
+
self._requests: Dict[str, List[float]] = defaultdict(list)
|
|
199
|
+
self._check_count: int = 0
|
|
200
|
+
|
|
201
|
+
def check(self, ctx: Context, user_id: str, message: str) -> tuple:
|
|
202
|
+
now = time.time()
|
|
203
|
+
|
|
204
|
+
self._requests[user_id] = [
|
|
205
|
+
t for t in self._requests[user_id] if now - t < 60
|
|
206
|
+
]
|
|
207
|
+
if len(self._requests[user_id]) >= BUSINESS["rate_limit_per_minute"]:
|
|
208
|
+
return None, "Rate limit exceeded. Please wait a moment."
|
|
209
|
+
self._requests[user_id].append(now)
|
|
210
|
+
|
|
211
|
+
self._check_count += 1
|
|
212
|
+
if self._check_count % 100 == 0:
|
|
213
|
+
stale = [
|
|
214
|
+
k
|
|
215
|
+
for k, v in self._requests.items()
|
|
216
|
+
if not v or (now - max(v)) > 300
|
|
217
|
+
]
|
|
218
|
+
for k in stale:
|
|
219
|
+
del self._requests[k]
|
|
220
|
+
|
|
221
|
+
if not message or not message.strip():
|
|
222
|
+
return None, "Empty message."
|
|
223
|
+
if len(message) > BUSINESS["max_input_length"]:
|
|
224
|
+
return None, f"Message too long (max {BUSINESS['max_input_length']} chars)."
|
|
225
|
+
|
|
226
|
+
return message.strip(), None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ==============================================================================
|
|
230
|
+
# LAYER 3: STABILITY
|
|
231
|
+
# ==============================================================================
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Health:
|
|
235
|
+
"""Track uptime and error rate."""
|
|
236
|
+
|
|
237
|
+
def __init__(self) -> None:
|
|
238
|
+
self._start: datetime = datetime.utcnow()
|
|
239
|
+
self._requests: int = 0
|
|
240
|
+
self._errors: int = 0
|
|
241
|
+
|
|
242
|
+
def record(self, success: bool) -> None:
|
|
243
|
+
self._requests += 1
|
|
244
|
+
if not success:
|
|
245
|
+
self._errors += 1
|
|
246
|
+
|
|
247
|
+
def status(self) -> Dict[str, Any]:
|
|
248
|
+
uptime = (datetime.utcnow() - self._start).total_seconds()
|
|
249
|
+
error_rate = (self._errors / self._requests * 100) if self._requests else 0
|
|
250
|
+
return {
|
|
251
|
+
"status": "healthy" if error_rate < 10 else "degraded",
|
|
252
|
+
"uptime_seconds": int(uptime),
|
|
253
|
+
"requests": self._requests,
|
|
254
|
+
"error_rate": f"{error_rate:.1f}%",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ==============================================================================
|
|
259
|
+
# LAYER 4: SPEED
|
|
260
|
+
# ==============================================================================
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class Cache:
|
|
264
|
+
"""In-memory TTL cache."""
|
|
265
|
+
|
|
266
|
+
def __init__(self, max_size: int = 1000) -> None:
|
|
267
|
+
self._data: Dict[str, tuple] = {}
|
|
268
|
+
self._max_size: int = max_size
|
|
269
|
+
|
|
270
|
+
def get(self, key: str) -> Any:
|
|
271
|
+
if key in self._data:
|
|
272
|
+
value, expires = self._data[key]
|
|
273
|
+
if expires > time.time():
|
|
274
|
+
return value
|
|
275
|
+
del self._data[key]
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def set(self, key: str, value: Any, ttl: int = 300) -> None:
|
|
279
|
+
if len(self._data) >= self._max_size:
|
|
280
|
+
now = time.time()
|
|
281
|
+
expired = [k for k, (_, exp) in self._data.items() if exp <= now]
|
|
282
|
+
for k in expired:
|
|
283
|
+
del self._data[k]
|
|
284
|
+
if len(self._data) >= self._max_size:
|
|
285
|
+
to_drop = sorted(self._data.items(), key=lambda x: x[1][1])[
|
|
286
|
+
: self._max_size // 10
|
|
287
|
+
]
|
|
288
|
+
for k, _ in to_drop:
|
|
289
|
+
del self._data[k]
|
|
290
|
+
self._data[key] = (value, time.time() + ttl)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ==============================================================================
|
|
294
|
+
# LAYER 5: REVENUE
|
|
295
|
+
# ==============================================================================
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class Revenue:
|
|
299
|
+
"""Token-gated access and daily usage quotas."""
|
|
300
|
+
|
|
301
|
+
def __init__(self, cache: Cache) -> None:
|
|
302
|
+
self._cache = cache
|
|
303
|
+
self._usage: Dict[str, List[str]] = defaultdict(list)
|
|
304
|
+
|
|
305
|
+
def get_tier(self, user_address: str) -> str:
|
|
306
|
+
cached = self._cache.get(f"tier:{user_address}")
|
|
307
|
+
if cached is not None:
|
|
308
|
+
return cached
|
|
309
|
+
try:
|
|
310
|
+
r = requests.get(
|
|
311
|
+
f"{AGENTLAUNCH_API}/agents/token/{user_address}", timeout=5
|
|
312
|
+
)
|
|
313
|
+
if r.status_code == 200:
|
|
314
|
+
data = r.json()
|
|
315
|
+
balance = data.get("balance", 0)
|
|
316
|
+
tier = (
|
|
317
|
+
"premium"
|
|
318
|
+
if balance >= BUSINESS["premium_token_threshold"]
|
|
319
|
+
else "free"
|
|
320
|
+
)
|
|
321
|
+
self._cache.set(f"tier:{user_address}", tier, ttl=300)
|
|
322
|
+
return tier
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
return "free"
|
|
326
|
+
|
|
327
|
+
def check_quota(self, user_id: str, tier: str) -> tuple:
|
|
328
|
+
today = datetime.utcnow().date().isoformat()
|
|
329
|
+
self._usage[user_id] = [
|
|
330
|
+
t for t in self._usage[user_id] if t.startswith(today)
|
|
331
|
+
]
|
|
332
|
+
today_usage = len(self._usage[user_id])
|
|
333
|
+
limit = 1000 if tier == "premium" else BUSINESS["free_requests_per_day"]
|
|
334
|
+
if today_usage >= limit:
|
|
335
|
+
if tier == "free":
|
|
336
|
+
return False, (
|
|
337
|
+
f"Free limit reached ({limit}/day). "
|
|
338
|
+
f"Hold {BUSINESS['premium_token_threshold']} tokens for premium!"
|
|
339
|
+
)
|
|
340
|
+
return False, f"Daily limit reached ({limit}/day)."
|
|
341
|
+
self._usage[user_id].append(datetime.utcnow().isoformat())
|
|
342
|
+
return True, None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ==============================================================================
|
|
346
|
+
# AGENTLAUNCH INTEGRATION
|
|
347
|
+
# ==============================================================================
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class AgentLaunch:
|
|
351
|
+
"""Create and manage tokens on AgentLaunch."""
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def tokenize() -> Dict:
|
|
355
|
+
agent_address = os.environ.get("AGENT_ADDRESS")
|
|
356
|
+
if not agent_address:
|
|
357
|
+
return {"error": "AGENT_ADDRESS env var not set."}
|
|
358
|
+
try:
|
|
359
|
+
r = requests.post(
|
|
360
|
+
f"{AGENTLAUNCH_API}/agents/tokenize",
|
|
361
|
+
headers={
|
|
362
|
+
"X-API-Key": os.environ.get("AGENTLAUNCH_API_KEY", ""),
|
|
363
|
+
"Content-Type": "application/json",
|
|
364
|
+
},
|
|
365
|
+
json={
|
|
366
|
+
"agentAddress": agent_address,
|
|
367
|
+
"name": BUSINESS["name"],
|
|
368
|
+
"description": BUSINESS["description"],
|
|
369
|
+
},
|
|
370
|
+
timeout=30,
|
|
371
|
+
)
|
|
372
|
+
return r.json() if r.status_code in [200, 201] else {"error": r.text}
|
|
373
|
+
except Exception as e:
|
|
374
|
+
return {"error": str(e)}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ==============================================================================
|
|
378
|
+
# GIFTER BUSINESS LOGIC
|
|
379
|
+
# ==============================================================================
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class GifterBusiness:
|
|
383
|
+
"""
|
|
384
|
+
FET Gifter pattern: send testnet FET and BNB to developer wallets.
|
|
385
|
+
|
|
386
|
+
Daily limits per recipient:
|
|
387
|
+
- FET: {{daily_gift_limit_fet}} (BSC Testnet)
|
|
388
|
+
- BNB: {{daily_gift_limit_bnb}} (BSC Testnet gas)
|
|
389
|
+
|
|
390
|
+
Uses Web3.py for on-chain transfers. Private key loaded from env secret.
|
|
391
|
+
|
|
392
|
+
Note: This is for TESTNET only. Treasury holds only testnet funds.
|
|
393
|
+
Note: Trading fee is 2% -> 100% to protocol treasury (no creator fee).
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
def __init__(self) -> None:
|
|
397
|
+
self._daily_gifts: Dict[str, Dict[str, float]] = defaultdict(
|
|
398
|
+
lambda: {"fet": 0.0, "bnb": 0.0, "date": date.today().isoformat()}
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def _reset_if_new_day(self, recipient: str) -> None:
|
|
402
|
+
record = self._daily_gifts[recipient]
|
|
403
|
+
if record["date"] != date.today().isoformat():
|
|
404
|
+
self._daily_gifts[recipient] = {"fet": 0.0, "bnb": 0.0, "date": date.today().isoformat()}
|
|
405
|
+
|
|
406
|
+
def can_gift_fet(self, recipient: str, amount: float) -> Tuple[bool, str]:
|
|
407
|
+
self._reset_if_new_day(recipient)
|
|
408
|
+
used = self._daily_gifts[recipient]["fet"]
|
|
409
|
+
limit = BUSINESS["daily_gift_limit_fet"]
|
|
410
|
+
if used + amount > limit:
|
|
411
|
+
remaining = max(0.0, limit - used)
|
|
412
|
+
return False, f"Daily FET limit reached. Remaining today: {remaining:.2f} FET"
|
|
413
|
+
return True, ""
|
|
414
|
+
|
|
415
|
+
def can_gift_bnb(self, recipient: str, amount: float) -> Tuple[bool, str]:
|
|
416
|
+
self._reset_if_new_day(recipient)
|
|
417
|
+
used = self._daily_gifts[recipient]["bnb"]
|
|
418
|
+
limit = BUSINESS["daily_gift_limit_bnb"]
|
|
419
|
+
if used + amount > limit:
|
|
420
|
+
remaining = max(0.0, limit - used)
|
|
421
|
+
return False, f"Daily BNB limit reached. Remaining today: {remaining:.6f} BNB"
|
|
422
|
+
return True, ""
|
|
423
|
+
|
|
424
|
+
def send_fet(self, ctx: Context, recipient: str, amount: float) -> Tuple[bool, str]:
|
|
425
|
+
"""Send testnet FET via Web3.py on BSC Testnet."""
|
|
426
|
+
try:
|
|
427
|
+
from web3 import Web3
|
|
428
|
+
|
|
429
|
+
private_key = os.environ.get("TREASURY_PRIVATE_KEY", "")
|
|
430
|
+
if not private_key:
|
|
431
|
+
return False, "TREASURY_PRIVATE_KEY not configured."
|
|
432
|
+
|
|
433
|
+
w3 = Web3(Web3.HTTPProvider(BSC_TESTNET_RPC))
|
|
434
|
+
if not w3.is_connected():
|
|
435
|
+
return False, "Could not connect to BSC Testnet RPC."
|
|
436
|
+
|
|
437
|
+
contract = w3.eth.contract(
|
|
438
|
+
address=Web3.to_checksum_address(FET_CONTRACT_BSC_TESTNET),
|
|
439
|
+
abi=ERC20_ABI,
|
|
440
|
+
)
|
|
441
|
+
decimals = contract.functions.decimals().call()
|
|
442
|
+
amount_wei = int(amount * (10 ** decimals))
|
|
443
|
+
|
|
444
|
+
treasury = w3.eth.account.from_key(private_key)
|
|
445
|
+
nonce = w3.eth.get_transaction_count(treasury.address)
|
|
446
|
+
chain_id = w3.eth.chain_id
|
|
447
|
+
|
|
448
|
+
tx = contract.functions.transfer(
|
|
449
|
+
Web3.to_checksum_address(recipient), amount_wei
|
|
450
|
+
).build_transaction({
|
|
451
|
+
"from": treasury.address,
|
|
452
|
+
"nonce": nonce,
|
|
453
|
+
"gas": 100000,
|
|
454
|
+
"gasPrice": w3.eth.gas_price,
|
|
455
|
+
"chainId": chain_id,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
signed = w3.eth.account.sign_transaction(tx, private_key)
|
|
459
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
460
|
+
|
|
461
|
+
self._daily_gifts[recipient]["fet"] += amount
|
|
462
|
+
ctx.logger.info(
|
|
463
|
+
f"[GIFT_FET] recipient={recipient[:20]} amount={amount} "
|
|
464
|
+
f"tx={tx_hash.hex()}"
|
|
465
|
+
)
|
|
466
|
+
return True, f"0x{tx_hash.hex()}"
|
|
467
|
+
|
|
468
|
+
except ImportError:
|
|
469
|
+
return False, "web3 package not installed. Run: pip install web3"
|
|
470
|
+
except Exception as e:
|
|
471
|
+
return False, str(e)
|
|
472
|
+
|
|
473
|
+
def send_bnb(self, ctx: Context, recipient: str, amount: float) -> Tuple[bool, str]:
|
|
474
|
+
"""Send testnet BNB via Web3.py on BSC Testnet."""
|
|
475
|
+
try:
|
|
476
|
+
from web3 import Web3
|
|
477
|
+
|
|
478
|
+
private_key = os.environ.get("TREASURY_PRIVATE_KEY", "")
|
|
479
|
+
if not private_key:
|
|
480
|
+
return False, "TREASURY_PRIVATE_KEY not configured."
|
|
481
|
+
|
|
482
|
+
w3 = Web3(Web3.HTTPProvider(BSC_TESTNET_RPC))
|
|
483
|
+
if not w3.is_connected():
|
|
484
|
+
return False, "Could not connect to BSC Testnet RPC."
|
|
485
|
+
|
|
486
|
+
treasury = w3.eth.account.from_key(private_key)
|
|
487
|
+
nonce = w3.eth.get_transaction_count(treasury.address)
|
|
488
|
+
chain_id = w3.eth.chain_id
|
|
489
|
+
amount_wei = w3.to_wei(amount, "ether")
|
|
490
|
+
|
|
491
|
+
tx = {
|
|
492
|
+
"to": Web3.to_checksum_address(recipient),
|
|
493
|
+
"value": amount_wei,
|
|
494
|
+
"gas": 21000,
|
|
495
|
+
"gasPrice": w3.eth.gas_price,
|
|
496
|
+
"nonce": nonce,
|
|
497
|
+
"chainId": chain_id,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
signed = w3.eth.account.sign_transaction(tx, private_key)
|
|
501
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
502
|
+
|
|
503
|
+
self._daily_gifts[recipient]["bnb"] += amount
|
|
504
|
+
ctx.logger.info(
|
|
505
|
+
f"[GIFT_BNB] recipient={recipient[:20]} amount={amount} "
|
|
506
|
+
f"tx={tx_hash.hex()}"
|
|
507
|
+
)
|
|
508
|
+
return True, f"0x{tx_hash.hex()}"
|
|
509
|
+
|
|
510
|
+
except ImportError:
|
|
511
|
+
return False, "web3 package not installed. Run: pip install web3"
|
|
512
|
+
except Exception as e:
|
|
513
|
+
return False, str(e)
|
|
514
|
+
|
|
515
|
+
async def handle(self, ctx: Context, user_id: str, message: str, tier: str) -> str:
|
|
516
|
+
lower = message.lower()
|
|
517
|
+
|
|
518
|
+
# "gift fet <address> [amount]"
|
|
519
|
+
if lower.startswith("gift fet ") or lower.startswith("send fet "):
|
|
520
|
+
parts = message.split()
|
|
521
|
+
if len(parts) < 3:
|
|
522
|
+
return "Usage: gift fet <wallet_address> [amount]"
|
|
523
|
+
recipient = parts[2].strip()
|
|
524
|
+
amount = float(parts[3]) if len(parts) >= 4 else min(10.0, BUSINESS["daily_gift_limit_fet"])
|
|
525
|
+
|
|
526
|
+
ok, reason = self.can_gift_fet(recipient, amount)
|
|
527
|
+
if not ok:
|
|
528
|
+
return reason
|
|
529
|
+
|
|
530
|
+
Logger.audit(ctx, user_id, f"gift_fet:{recipient[:20]}:{amount}")
|
|
531
|
+
success, result = self.send_fet(ctx, recipient, amount)
|
|
532
|
+
if success:
|
|
533
|
+
return (
|
|
534
|
+
f"Sent {amount} FET to {recipient[:12]}...\\n"
|
|
535
|
+
f"Tx: {result}\\n"
|
|
536
|
+
f"BSC Testnet explorer: https://testnet.bscscan.com/tx/{result}"
|
|
537
|
+
)
|
|
538
|
+
return f"Gift failed: {result}"
|
|
539
|
+
|
|
540
|
+
# "gift bnb <address> [amount]"
|
|
541
|
+
if lower.startswith("gift bnb ") or lower.startswith("send bnb "):
|
|
542
|
+
parts = message.split()
|
|
543
|
+
if len(parts) < 3:
|
|
544
|
+
return "Usage: gift bnb <wallet_address> [amount]"
|
|
545
|
+
recipient = parts[2].strip()
|
|
546
|
+
amount = float(parts[3]) if len(parts) >= 4 else BUSINESS["daily_gift_limit_bnb"]
|
|
547
|
+
|
|
548
|
+
ok, reason = self.can_gift_bnb(recipient, amount)
|
|
549
|
+
if not ok:
|
|
550
|
+
return reason
|
|
551
|
+
|
|
552
|
+
Logger.audit(ctx, user_id, f"gift_bnb:{recipient[:20]}:{amount}")
|
|
553
|
+
success, result = self.send_bnb(ctx, recipient, amount)
|
|
554
|
+
if success:
|
|
555
|
+
return (
|
|
556
|
+
f"Sent {amount} BNB to {recipient[:12]}...\\n"
|
|
557
|
+
f"Tx: {result}\\n"
|
|
558
|
+
f"BSC Testnet explorer: https://testnet.bscscan.com/tx/{result}"
|
|
559
|
+
)
|
|
560
|
+
return f"Gift failed: {result}"
|
|
561
|
+
|
|
562
|
+
# "limits" — show current daily allowances
|
|
563
|
+
if lower in ("limits", "allowance", "quota"):
|
|
564
|
+
self._reset_if_new_day(user_id)
|
|
565
|
+
record = self._daily_gifts[user_id]
|
|
566
|
+
fet_used = record["fet"]
|
|
567
|
+
bnb_used = record["bnb"]
|
|
568
|
+
return (
|
|
569
|
+
f"Your daily gift allowance:\\n"
|
|
570
|
+
f" FET: {fet_used:.2f} / {BUSINESS['daily_gift_limit_fet']:.2f} used\\n"
|
|
571
|
+
f" BNB: {bnb_used:.6f} / {BUSINESS['daily_gift_limit_bnb']:.6f} used\\n"
|
|
572
|
+
f"Resets at midnight UTC."
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
f"Welcome to {BUSINESS['name']}!\\n\\n"
|
|
577
|
+
f"I distribute testnet FET and BNB to help developers explore AgentLaunch.\\n\\n"
|
|
578
|
+
f"Commands:\\n"
|
|
579
|
+
f" gift fet <wallet_address> [amount] — receive testnet FET\\n"
|
|
580
|
+
f" gift bnb <wallet_address> [amount] — receive testnet BNB (gas)\\n"
|
|
581
|
+
f" limits — check your daily allowance\\n\\n"
|
|
582
|
+
f"Daily limits: {BUSINESS['daily_gift_limit_fet']} FET, "
|
|
583
|
+
f"{BUSINESS['daily_gift_limit_bnb']} BNB per address\\n\\n"
|
|
584
|
+
f"Platform: agent-launch.ai | Graduation target: 30,000 FET"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ==============================================================================
|
|
589
|
+
# REPLY HELPER
|
|
590
|
+
# ==============================================================================
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
async def reply(ctx: Context, sender: str, text: str, end: bool = False) -> None:
|
|
594
|
+
content = [TextContent(type="text", text=text)]
|
|
595
|
+
if end:
|
|
596
|
+
content.append(EndSessionContent(type="end-session"))
|
|
597
|
+
try:
|
|
598
|
+
await ctx.send(
|
|
599
|
+
sender,
|
|
600
|
+
ChatMessage(timestamp=datetime.utcnow(), msg_id=uuid4(), content=content),
|
|
601
|
+
)
|
|
602
|
+
except Exception as e:
|
|
603
|
+
ctx.logger.error(f"Failed to send reply to {sender[:20]}: {e}")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# ==============================================================================
|
|
607
|
+
# MAIN AGENT
|
|
608
|
+
# ==============================================================================
|
|
609
|
+
|
|
610
|
+
cache = Cache(max_size=1000)
|
|
611
|
+
security = Security()
|
|
612
|
+
health = Health()
|
|
613
|
+
revenue = Revenue(cache)
|
|
614
|
+
business = GifterBusiness()
|
|
615
|
+
|
|
616
|
+
agent = Agent()
|
|
617
|
+
chat_proto = Protocol(spec=chat_protocol_spec)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@chat_proto.on_message(ChatMessage)
|
|
621
|
+
async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None:
|
|
622
|
+
try:
|
|
623
|
+
await ctx.send(
|
|
624
|
+
sender,
|
|
625
|
+
ChatAcknowledgement(
|
|
626
|
+
timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
ctx.logger.error(f"Failed to send ack to {sender[:20]}: {e}")
|
|
631
|
+
|
|
632
|
+
text = " ".join(
|
|
633
|
+
item.text for item in msg.content if isinstance(item, TextContent)
|
|
634
|
+
).strip()
|
|
635
|
+
text = text[: BUSINESS["max_input_length"]]
|
|
636
|
+
|
|
637
|
+
clean, error = security.check(ctx, sender, text)
|
|
638
|
+
if error:
|
|
639
|
+
health.record(False)
|
|
640
|
+
await reply(ctx, sender, error, end=True)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
Logger.audit(ctx, sender, "request")
|
|
644
|
+
|
|
645
|
+
lower = clean.lower()
|
|
646
|
+
|
|
647
|
+
if lower in ("help", "?"):
|
|
648
|
+
tier = revenue.get_tier(sender)
|
|
649
|
+
await reply(
|
|
650
|
+
ctx,
|
|
651
|
+
sender,
|
|
652
|
+
f"**{BUSINESS['name']}** v{BUSINESS['version']}\\n\\n"
|
|
653
|
+
f"{BUSINESS['description']}\\n\\n"
|
|
654
|
+
f"Your tier: {tier.upper()}\\n\\n"
|
|
655
|
+
f"Commands: help, status, tokenize, gift fet <addr>, gift bnb <addr>, limits",
|
|
656
|
+
)
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
if lower == "status":
|
|
660
|
+
s = health.status()
|
|
661
|
+
await reply(
|
|
662
|
+
ctx,
|
|
663
|
+
sender,
|
|
664
|
+
f"Status: {s['status']} | Uptime: {s['uptime_seconds']}s | "
|
|
665
|
+
f"Requests: {s['requests']} | Error rate: {s['error_rate']}",
|
|
666
|
+
)
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
if "tokenize" in lower:
|
|
670
|
+
if OWNER_ADDRESS and sender != OWNER_ADDRESS:
|
|
671
|
+
await reply(ctx, sender, "Only the agent owner can trigger tokenization.", end=True)
|
|
672
|
+
return
|
|
673
|
+
result = AgentLaunch.tokenize()
|
|
674
|
+
link = result.get("data", {}).get("handoff_link") or result.get("handoff_link")
|
|
675
|
+
await reply(
|
|
676
|
+
ctx,
|
|
677
|
+
sender,
|
|
678
|
+
f"Token created! Deploy here: {link}" if link else f"Result: {json.dumps(result)}",
|
|
679
|
+
end=True,
|
|
680
|
+
)
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
tier = revenue.get_tier(sender)
|
|
684
|
+
allowed, quota_error = revenue.check_quota(sender, tier)
|
|
685
|
+
if not allowed:
|
|
686
|
+
health.record(False)
|
|
687
|
+
await reply(ctx, sender, quota_error, end=True)
|
|
688
|
+
return
|
|
689
|
+
|
|
690
|
+
try:
|
|
691
|
+
response = await business.handle(ctx, sender, clean, tier)
|
|
692
|
+
health.record(True)
|
|
693
|
+
except Exception as e:
|
|
694
|
+
health.record(False)
|
|
695
|
+
Logger.error(ctx, "business_handle", str(e))
|
|
696
|
+
response = "Something went wrong. Please try again."
|
|
697
|
+
|
|
698
|
+
await reply(ctx, sender, response, end=True)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@chat_proto.on_message(ChatAcknowledgement)
|
|
702
|
+
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> None:
|
|
703
|
+
ctx.logger.debug(f"Ack from {sender[:20]} for msg {msg.acknowledged_msg_id}")
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@agent.on_interval(period=3600)
|
|
707
|
+
async def periodic_health(ctx: Context) -> None:
|
|
708
|
+
ctx.logger.info(f"[HEALTH] {json.dumps(health.status())}")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
agent.include(chat_proto, publish_manifest=True)
|
|
712
|
+
|
|
713
|
+
if __name__ == "__main__":
|
|
714
|
+
agent.run()
|
|
715
|
+
`,
|
|
716
|
+
};
|
|
717
|
+
//# sourceMappingURL=gifter.js.map
|