web3-agent-kit 1.0.0__tar.gz → 1.2.0__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.
Files changed (88) hide show
  1. {web3_agent_kit-1.0.0/web3_agent_kit.egg-info → web3_agent_kit-1.2.0}/PKG-INFO +1 -1
  2. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/pyproject.toml +1 -1
  3. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/__init__.py +24 -1
  4. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/__init__.py +39 -0
  5. web3_agent_kit-1.2.0/src/airdrop/dashboard.py +533 -0
  6. web3_agent_kit-1.2.0/src/airdrop/discovery.py +696 -0
  7. web3_agent_kit-1.2.0/src/airdrop/faucet.py +586 -0
  8. web3_agent_kit-1.2.0/src/airdrop/onchain.py +724 -0
  9. web3_agent_kit-1.2.0/src/airdrop/referral.py +523 -0
  10. web3_agent_kit-1.2.0/src/airdrop/scheduler.py +702 -0
  11. web3_agent_kit-1.2.0/src/security/__init__.py +883 -0
  12. web3_agent_kit-1.2.0/tests/test_airdrop_new_modules.py +648 -0
  13. web3_agent_kit-1.2.0/tests/test_security.py +434 -0
  14. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0/web3_agent_kit.egg-info}/PKG-INFO +1 -1
  15. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/web3_agent_kit.egg-info/SOURCES.txt +9 -0
  16. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/LICENSE +0 -0
  17. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/README.md +0 -0
  18. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/setup.cfg +0 -0
  19. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/setup.py +0 -0
  20. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/agent.py +0 -0
  21. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/base.py +0 -0
  22. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/__init__.py +0 -0
  23. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/base_executor.py +0 -0
  24. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/browser.py +0 -0
  25. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/captcha_solver.py +0 -0
  26. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/cli.py +0 -0
  27. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/galxe_exec.py +0 -0
  28. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/gleam_exec.py +0 -0
  29. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/intract_exec.py +0 -0
  30. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/layer3_exec.py +0 -0
  31. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/plugin_registry.py +0 -0
  32. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/port3_exec.py +0 -0
  33. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/questn.py +0 -0
  34. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/social_exec.py +0 -0
  35. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/taskon.py +0 -0
  36. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/executor/zealy_exec.py +0 -0
  37. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/galxe.py +0 -0
  38. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/gleam.py +0 -0
  39. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/multi_wallet.py +0 -0
  40. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/social.py +0 -0
  41. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/tracker.py +0 -0
  42. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/airdrop/zealy.py +0 -0
  43. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/__init__.py +0 -0
  44. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/models.py +0 -0
  45. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/__init__.py +0 -0
  46. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/approval.py +0 -0
  47. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/bridge.py +0 -0
  48. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/dca.py +0 -0
  49. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/gas.py +0 -0
  50. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/portfolio.py +0 -0
  51. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/swap.py +0 -0
  52. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/wallet.py +0 -0
  53. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/watcher.py +0 -0
  54. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/api/routes/yield_opt.py +0 -0
  55. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/approval_manager.py +0 -0
  56. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/bridge.py +0 -0
  57. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/chain.py +0 -0
  58. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/dca_bot.py +0 -0
  59. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/defi/__init__.py +0 -0
  60. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/gas_optimizer.py +0 -0
  61. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/llm.py +0 -0
  62. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/multi_wallet.py +0 -0
  63. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/plugins/__init__.py +0 -0
  64. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/plugins/examples/gas_tracker.py +0 -0
  65. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/portfolio.py +0 -0
  66. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/sniper.py +0 -0
  67. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/utils/__init__.py +0 -0
  68. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/wallet.py +0 -0
  69. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/wallet_watcher.py +0 -0
  70. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/src/yield_optimizer.py +0 -0
  71. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_airdrop.py +0 -0
  72. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_api.py +0 -0
  73. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_bridge.py +0 -0
  74. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_core.py +0 -0
  75. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_dca_bot.py +0 -0
  76. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_defi.py +0 -0
  77. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_executor.py +0 -0
  78. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_llm.py +0 -0
  79. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_multi_wallet.py +0 -0
  80. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_new_features.py +0 -0
  81. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_platforms.py +0 -0
  82. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_plugins.py +0 -0
  83. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_portfolio.py +0 -0
  84. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_sniper.py +0 -0
  85. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/tests/test_yield_optimizer.py +0 -0
  86. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
  87. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/web3_agent_kit.egg-info/requires.txt +0 -0
  88. {web3_agent_kit-1.0.0 → web3_agent_kit-1.2.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: web3-agent-kit
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: Open-source framework for building autonomous AI agents that interact with blockchain networks
5
5
  Author-email: Maulana <khasbim240803@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "web3-agent-kit"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "Open-source framework for building autonomous AI agents that interact with blockchain networks"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  """Web3 Agent Kit — Open-source framework for autonomous Web3 AI agents."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "1.2.0"
4
4
  __author__ = "Maulana"
5
5
 
6
6
  from .agent import Agent, AgentConfig
@@ -28,6 +28,18 @@ from .dca_bot import DCABot, DCAOrder, DCAResult, Interval, DCAStatus
28
28
  from .gas_optimizer import GasOptimizer, GasEstimate, GasRecommendation, GasPriority
29
29
  from .wallet_watcher import WalletWatcher, WatchedWallet, WalletAlert, AlertType, AlertSeverity
30
30
  from .approval_manager import ApprovalManager, TokenApproval, RevokeResult, ApprovalRisk
31
+ from .security import (
32
+ TokenAnalyzer,
33
+ SecurityConfig,
34
+ SecurityReport,
35
+ TokenInfo,
36
+ TaxInfo,
37
+ LiquidityInfo,
38
+ HolderInfo,
39
+ ContractAudit,
40
+ RiskLevel as SecurityRiskLevel,
41
+ ContractPattern,
42
+ )
31
43
 
32
44
  __all__ = [
33
45
  # Core
@@ -86,4 +98,15 @@ __all__ = [
86
98
  "TokenApproval",
87
99
  "RevokeResult",
88
100
  "ApprovalRisk",
101
+ # Security
102
+ "TokenAnalyzer",
103
+ "SecurityConfig",
104
+ "SecurityReport",
105
+ "TokenInfo",
106
+ "TaxInfo",
107
+ "LiquidityInfo",
108
+ "HolderInfo",
109
+ "ContractAudit",
110
+ "SecurityRiskLevel",
111
+ "ContractPattern",
89
112
  ]
@@ -25,6 +25,14 @@ from .social import (
25
25
  from .tracker import AirdropTracker, AirdropReward, AirdropSummary
26
26
  from .multi_wallet import AirdropFarmer, SybilAvoidanceConfig, FarmResult, WalletFarmProgress
27
27
 
28
+ # New modules
29
+ from .discovery import CampaignDiscovery, DiscoveryConfig, DiscoveredCampaign
30
+ from .onchain import OnChainAirdropFarmer, OnChainConfig, TransactionResult, Chain, DeFiProtocol, FARMING_PLANS
31
+ from .scheduler import AirdropScheduler, SchedulerConfig, ScheduledTask, ScheduleFrequency
32
+ from .dashboard import PointsDashboard, DashboardConfig, PlatformPoints, PointsSnapshot
33
+ from .referral import ReferralManager, ReferralLink, ReferralPlatform, ReferralStats
34
+ from .faucet import FaucetClaimer, FaucetConfig, ClaimResult, FAUCETS
35
+
28
36
  __all__ = [
29
37
  # Base
30
38
  "BaseAirdropPlatform",
@@ -62,4 +70,35 @@ __all__ = [
62
70
  "SybilAvoidanceConfig",
63
71
  "FarmResult",
64
72
  "WalletFarmProgress",
73
+ # Discovery
74
+ "CampaignDiscovery",
75
+ "DiscoveryConfig",
76
+ "DiscoveredCampaign",
77
+ # On-chain
78
+ "OnChainAirdropFarmer",
79
+ "OnChainConfig",
80
+ "TransactionResult",
81
+ "Chain",
82
+ "DeFiProtocol",
83
+ "FARMING_PLANS",
84
+ # Scheduler
85
+ "AirdropScheduler",
86
+ "SchedulerConfig",
87
+ "ScheduledTask",
88
+ "ScheduleFrequency",
89
+ # Dashboard
90
+ "PointsDashboard",
91
+ "DashboardConfig",
92
+ "PlatformPoints",
93
+ "PointsSnapshot",
94
+ # Referral
95
+ "ReferralManager",
96
+ "ReferralLink",
97
+ "ReferralPlatform",
98
+ "ReferralStats",
99
+ # Faucet
100
+ "FaucetClaimer",
101
+ "FaucetConfig",
102
+ "ClaimResult",
103
+ "FAUCETS",
65
104
  ]
@@ -0,0 +1,533 @@
1
+ """Points Dashboard — track airdrop points across all platforms.
2
+
3
+ Aggregates points from Galxe, Zealy, Layer3, and other platforms
4
+ into a unified dashboard with historical tracking.
5
+
6
+ Usage::
7
+
8
+ from web3_agent_kit.airdrop.dashboard import PointsDashboard
9
+
10
+ dashboard = PointsDashboard()
11
+ dashboard.sync_all(wallet="0x...")
12
+ dashboard.print_summary()
13
+ dashboard.export_json("points_report.json")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Any, Optional
24
+
25
+ import requests
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ @dataclass
31
+ class PlatformPoints:
32
+ """Points balance on a single platform."""
33
+ platform: str
34
+ points: int = 0
35
+ rank: int = 0
36
+ level: int = 0
37
+ tier: str = ""
38
+ campaigns_completed: int = 0
39
+ campaigns_active: int = 0
40
+ last_activity: Optional[datetime] = None
41
+ streak_days: int = 0
42
+ referrals: int = 0
43
+ referral_points: int = 0
44
+ badges: list[str] = field(default_factory=list)
45
+ metadata: dict = field(default_factory=dict)
46
+
47
+ @property
48
+ def total_with_referrals(self) -> int:
49
+ """Total points including referrals."""
50
+ return self.points + self.referral_points
51
+
52
+ def to_dict(self) -> dict:
53
+ return {
54
+ "platform": self.platform,
55
+ "points": self.points,
56
+ "rank": self.rank,
57
+ "level": self.level,
58
+ "tier": self.tier,
59
+ "campaigns_completed": self.campaigns_completed,
60
+ "campaigns_active": self.campaigns_active,
61
+ "streak_days": self.streak_days,
62
+ "referrals": self.referrals,
63
+ "referral_points": self.referral_points,
64
+ "badges": self.badges,
65
+ "last_activity": self.last_activity.isoformat() if self.last_activity else None,
66
+ }
67
+
68
+
69
+ @dataclass
70
+ class PointsSnapshot:
71
+ """A point-in-time snapshot of all platform points."""
72
+ timestamp: datetime
73
+ platforms: dict[str, PlatformPoints] = field(default_factory=dict)
74
+
75
+ @property
76
+ def total_points(self) -> int:
77
+ """Total points across all platforms."""
78
+ return sum(p.points for p in self.platforms.values())
79
+
80
+ @property
81
+ def total_campaigns(self) -> int:
82
+ """Total campaigns completed across all platforms."""
83
+ return sum(p.campaigns_completed for p in self.platforms.values())
84
+
85
+ def to_dict(self) -> dict:
86
+ return {
87
+ "timestamp": self.timestamp.isoformat(),
88
+ "total_points": self.total_points,
89
+ "total_campaigns": self.total_campaigns,
90
+ "platforms": {k: v.to_dict() for k, v in self.platforms.items()},
91
+ }
92
+
93
+
94
+ @dataclass
95
+ class DashboardConfig:
96
+ """Configuration for the points dashboard."""
97
+ # Wallet address
98
+ wallet_address: str = ""
99
+ # Platforms to track
100
+ platforms: list[str] = field(default_factory=lambda: [
101
+ "galxe", "zealy", "layer3", "questn", "taskon", "intract", "port3"
102
+ ])
103
+ # API keys (optional, for authenticated endpoints)
104
+ galxe_api_key: str = ""
105
+ # History file
106
+ history_path: Optional[str] = None
107
+ # Proxy
108
+ proxy: Optional[str] = None
109
+
110
+
111
+ class PointsDashboard:
112
+ """Track airdrop points across all platforms.
113
+
114
+ Aggregates points from multiple platforms, tracks history,
115
+ and provides reporting/export capabilities.
116
+
117
+ Example::
118
+
119
+ dashboard = PointsDashboard(config)
120
+ dashboard.sync_all(wallet="0x721e885...A522")
121
+ dashboard.print_summary()
122
+ """
123
+
124
+ def __init__(self, config: Optional[DashboardConfig] = None):
125
+ """Initialize the dashboard.
126
+
127
+ Args:
128
+ config: Dashboard configuration.
129
+ """
130
+ self.config = config or DashboardConfig()
131
+ self.session = requests.Session()
132
+ self.session.headers.update({
133
+ "User-Agent": (
134
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
135
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
136
+ "Chrome/120.0.0.0 Safari/537.36"
137
+ ),
138
+ })
139
+ if self.config.proxy:
140
+ self.session.proxies = {
141
+ "http": self.config.proxy,
142
+ "https": self.config.proxy,
143
+ }
144
+ self._current: Optional[PointsSnapshot] = None
145
+ self._history: list[PointsSnapshot] = []
146
+ self._load_history()
147
+ logger.info("PointsDashboard initialized")
148
+
149
+ def sync_all(self, wallet: Optional[str] = None) -> PointsSnapshot:
150
+ """Sync points from all configured platforms.
151
+
152
+ Args:
153
+ wallet: Wallet address (overrides config).
154
+
155
+ Returns:
156
+ Current points snapshot.
157
+ """
158
+ address = wallet or self.config.wallet_address
159
+ if not address:
160
+ raise ValueError("Wallet address required")
161
+
162
+ platforms: dict[str, PlatformPoints] = {}
163
+
164
+ syncers = {
165
+ "galxe": self._sync_galxe,
166
+ "zealy": self._sync_zealy,
167
+ "layer3": self._sync_layer3,
168
+ "questn": self._sync_questn,
169
+ "taskon": self._sync_taskon,
170
+ "intract": self._sync_intract,
171
+ "port3": self._sync_port3,
172
+ }
173
+
174
+ for platform in self.config.platforms:
175
+ syncer = syncers.get(platform)
176
+ if not syncer:
177
+ continue
178
+ try:
179
+ points = syncer(address)
180
+ if points:
181
+ platforms[platform] = points
182
+ logger.info(f"{platform}: {points.points} points")
183
+ except Exception as e:
184
+ logger.error(f"Failed to sync {platform}: {e}")
185
+
186
+ self._current = PointsSnapshot(
187
+ timestamp=datetime.now(timezone.utc),
188
+ platforms=platforms,
189
+ )
190
+ self._history.append(self._current)
191
+ self._save_history()
192
+
193
+ logger.info(f"Sync complete: {self._current.total_points} total points")
194
+ return self._current
195
+
196
+ def get_current(self) -> Optional[PointsSnapshot]:
197
+ """Get the current points snapshot.
198
+
199
+ Returns:
200
+ Current snapshot, or None if not synced.
201
+ """
202
+ return self._current
203
+
204
+ def get_history(self, limit: int = 30) -> list[PointsSnapshot]:
205
+ """Get historical snapshots.
206
+
207
+ Args:
208
+ limit: Max snapshots to return.
209
+
210
+ Returns:
211
+ List of historical snapshots.
212
+ """
213
+ return self._history[-limit:]
214
+
215
+ def get_growth(self, days: int = 7) -> dict:
216
+ """Calculate points growth over time.
217
+
218
+ Args:
219
+ days: Number of days to look back.
220
+
221
+ Returns:
222
+ Growth dict with per-platform and total deltas.
223
+ """
224
+ if len(self._history) < 2:
225
+ return {"total_delta": 0, "platforms": {}}
226
+
227
+ now = self._history[-1]
228
+ cutoff = datetime.now(timezone.utc).timestamp() - (days * 86400)
229
+
230
+ # Find snapshot closest to cutoff
231
+ old = self._history[0]
232
+ for snap in self._history:
233
+ if snap.timestamp.timestamp() >= cutoff:
234
+ old = snap
235
+ break
236
+
237
+ growth: dict[str, Any] = {
238
+ "days": days,
239
+ "total_delta": now.total_points - old.total_points,
240
+ "total_delta_pct": 0.0,
241
+ "platforms": {},
242
+ }
243
+
244
+ if old.total_points > 0:
245
+ growth["total_delta_pct"] = round(
246
+ (growth["total_delta"] / old.total_points) * 100, 1
247
+ )
248
+
249
+ for platform in set(
250
+ list(now.platforms.keys()) + list(old.platforms.keys())
251
+ ):
252
+ new_pts = now.platforms.get(platform, PlatformPoints(platform)).points
253
+ old_pts = old.platforms.get(platform, PlatformPoints(platform)).points
254
+ delta = new_pts - old_pts
255
+ growth["platforms"][platform] = {
256
+ "current": new_pts,
257
+ "previous": old_pts,
258
+ "delta": delta,
259
+ }
260
+
261
+ return growth
262
+
263
+ def get_leaderboard_position(self) -> list[dict]:
264
+ """Get estimated leaderboard positions.
265
+
266
+ Returns:
267
+ List of platform rankings.
268
+ """
269
+ if not self._current:
270
+ return []
271
+
272
+ positions = []
273
+ for platform, points in self._current.platforms.items():
274
+ positions.append({
275
+ "platform": platform,
276
+ "points": points.points,
277
+ "rank": points.rank,
278
+ "tier": points.tier,
279
+ })
280
+
281
+ positions.sort(key=lambda x: x["points"], reverse=True)
282
+ return positions
283
+
284
+ def print_summary(self) -> str:
285
+ """Print a formatted summary.
286
+
287
+ Returns:
288
+ Formatted summary string.
289
+ """
290
+ if not self._current:
291
+ return "Not synced yet. Call sync_all() first."
292
+
293
+ lines = [
294
+ "╔══════════════════════════════════════════════════════════╗",
295
+ "║ 🏆 AIRDROP POINTS DASHBOARD ║",
296
+ "╠══════════════════════════════════════════════════════════╣",
297
+ f"║ Wallet: {self.config.wallet_address[:20]}...{' ' * 20}║",
298
+ f"║ Synced: {self._current.timestamp.strftime('%Y-%m-%d %H:%M UTC')}{' ' * 25}║",
299
+ "╠══════════════════════════════════════════════════════════╣",
300
+ ]
301
+
302
+ for platform, points in sorted(
303
+ self._current.platforms.items(),
304
+ key=lambda x: x[1].points,
305
+ reverse=True,
306
+ ):
307
+ bar_len = min(30, points.points // 100)
308
+ bar = "█" * bar_len + "░" * (30 - bar_len)
309
+ lines.append(
310
+ f"║ {platform:12} │ {points.points:>8,} pts │ {bar} ║"
311
+ )
312
+
313
+ lines.extend([
314
+ "╠══════════════════════════════════════════════════════════╣",
315
+ f"║ TOTAL: {self._current.total_points:>10,} points"
316
+ f" │ {self._current.total_campaigns} campaigns completed"
317
+ f" ║",
318
+ "╚══════════════════════════════════════════════════════════╝",
319
+ ])
320
+
321
+ summary = "\n".join(lines)
322
+ print(summary)
323
+ return summary
324
+
325
+ def export_json(self, path: Optional[str] = None) -> str:
326
+ """Export dashboard data to JSON.
327
+
328
+ Args:
329
+ path: Optional file path to save.
330
+
331
+ Returns:
332
+ JSON string.
333
+ """
334
+ data = {
335
+ "wallet": self.config.wallet_address,
336
+ "exported_at": datetime.now(timezone.utc).isoformat(),
337
+ "current": self._current.to_dict() if self._current else None,
338
+ "history_count": len(self._history),
339
+ "growth_7d": self.get_growth(7),
340
+ "growth_30d": self.get_growth(30),
341
+ }
342
+ json_str = json.dumps(data, indent=2, default=str)
343
+
344
+ if path:
345
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
346
+ Path(path).write_text(json_str)
347
+ logger.info(f"Exported dashboard to {path}")
348
+
349
+ return json_str
350
+
351
+ def export_markdown(self, path: Optional[str] = None) -> str:
352
+ """Export dashboard as markdown report.
353
+
354
+ Args:
355
+ path: Optional file path to save.
356
+
357
+ Returns:
358
+ Markdown string.
359
+ """
360
+ if not self._current:
361
+ return "# No data synced yet"
362
+
363
+ lines = [
364
+ "# 🏆 Airdrop Points Report",
365
+ "",
366
+ f"**Wallet:** `{self.config.wallet_address}`",
367
+ f"**Synced:** {self._current.timestamp.strftime('%Y-%m-%d %H:%M UTC')}",
368
+ "",
369
+ "## Summary",
370
+ "",
371
+ f"- **Total Points:** {self._current.total_points:,}",
372
+ f"- **Campaigns Completed:** {self._current.total_campaigns}",
373
+ "",
374
+ "## Platform Breakdown",
375
+ "",
376
+ "| Platform | Points | Rank | Campaigns | Streak |",
377
+ "|----------|--------|------|-----------|--------|",
378
+ ]
379
+
380
+ for platform, points in sorted(
381
+ self._current.platforms.items(),
382
+ key=lambda x: x[1].points,
383
+ reverse=True,
384
+ ):
385
+ lines.append(
386
+ f"| {platform} | {points.points:,} | "
387
+ f"#{points.rank or '-'} | {points.campaigns_completed} | "
388
+ f"{points.streak_days}d |"
389
+ )
390
+
391
+ # Growth section
392
+ growth = self.get_growth(7)
393
+ if growth["total_delta"] != 0:
394
+ lines.extend([
395
+ "",
396
+ "## 7-Day Growth",
397
+ "",
398
+ f"- **Total Delta:** +{growth['total_delta']:,} points",
399
+ ])
400
+ for platform, data in growth["platforms"].items():
401
+ if data["delta"] != 0:
402
+ lines.append(
403
+ f"- **{platform}:** +{data['delta']:,} "
404
+ f"({data['current']:,} total)"
405
+ )
406
+
407
+ md = "\n".join(lines)
408
+
409
+ if path:
410
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
411
+ Path(path).write_text(md)
412
+ logger.info(f"Exported markdown to {path}")
413
+
414
+ return md
415
+
416
+ # ─── Platform Syncers ─────────────────────────────────────────
417
+
418
+ def _sync_galxe(self, wallet: str) -> Optional[PlatformPoints]:
419
+ """Sync Galxe points."""
420
+ try:
421
+ # Galxe GraphQL query for user points
422
+ query = """
423
+ query UserPoints($address: String!) {
424
+ user(address: $address) {
425
+ galxeScore {
426
+ score
427
+ rank
428
+ }
429
+ participatedCampaignCount
430
+ }
431
+ }
432
+ """
433
+ resp = self.session.post(
434
+ "https://graphigo.prd.galaxy.eco/query",
435
+ json={"query": query, "variables": {"address": wallet}},
436
+ timeout=15,
437
+ )
438
+ if resp.status_code == 200:
439
+ data = resp.json()
440
+ user = data.get("data", {}).get("user", {})
441
+ score = user.get("galxeScore", {})
442
+ return PlatformPoints(
443
+ platform="galxe",
444
+ points=score.get("score", 0),
445
+ rank=score.get("rank", 0),
446
+ campaigns_completed=user.get("participatedCampaignCount", 0),
447
+ )
448
+ except Exception as e:
449
+ logger.error(f"Galxe sync error: {e}")
450
+ return None
451
+
452
+ def _sync_zealy(self, wallet: str) -> Optional[PlatformPoints]:
453
+ """Sync Zealy points."""
454
+ # Zealy doesn't have a public API for user points
455
+ # Would need to scrape or use hidden API
456
+ return PlatformPoints(platform="zealy")
457
+
458
+ def _sync_layer3(self, wallet: str) -> Optional[PlatformPoints]:
459
+ """Sync Layer3 points."""
460
+ try:
461
+ resp = self.session.get(
462
+ f"https://layer3.xyz/api/users/{wallet}/stats",
463
+ timeout=15,
464
+ )
465
+ if resp.status_code == 200:
466
+ data = resp.json()
467
+ return PlatformPoints(
468
+ platform="layer3",
469
+ points=data.get("xp", 0),
470
+ campaigns_completed=data.get("questsCompleted", 0),
471
+ )
472
+ except Exception as e:
473
+ logger.error(f"Layer3 sync error: {e}")
474
+ return None
475
+
476
+ def _sync_questn(self, wallet: str) -> Optional[PlatformPoints]:
477
+ """Sync QuestN points."""
478
+ return PlatformPoints(platform="questn")
479
+
480
+ def _sync_taskon(self, wallet: str) -> Optional[PlatformPoints]:
481
+ """Sync TaskOn points."""
482
+ return PlatformPoints(platform="taskon")
483
+
484
+ def _sync_intract(self, wallet: str) -> Optional[PlatformPoints]:
485
+ """Sync Intract points."""
486
+ return PlatformPoints(platform="intract")
487
+
488
+ def _sync_port3(self, wallet: str) -> Optional[PlatformPoints]:
489
+ """Sync Port3 points."""
490
+ return PlatformPoints(platform="port3")
491
+
492
+ # ─── Persistence ──────────────────────────────────────────────
493
+
494
+ def _load_history(self) -> None:
495
+ """Load history from file."""
496
+ if not self.config.history_path:
497
+ return
498
+ path = Path(self.config.history_path)
499
+ if not path.exists():
500
+ return
501
+ try:
502
+ data = json.loads(path.read_text())
503
+ for snap_data in data.get("snapshots", []):
504
+ platforms = {}
505
+ for k, v in snap_data.get("platforms", {}).items():
506
+ platforms[k] = PlatformPoints(
507
+ platform=k,
508
+ points=v.get("points", 0),
509
+ rank=v.get("rank", 0),
510
+ campaigns_completed=v.get("campaigns_completed", 0),
511
+ )
512
+ self._history.append(PointsSnapshot(
513
+ timestamp=datetime.fromisoformat(snap_data["timestamp"]),
514
+ platforms=platforms,
515
+ ))
516
+ logger.info(f"Loaded {len(self._history)} historical snapshots")
517
+ except Exception as e:
518
+ logger.warning(f"Failed to load history: {e}")
519
+
520
+ def _save_history(self) -> None:
521
+ """Save history to file."""
522
+ if not self.config.history_path:
523
+ return
524
+ path = Path(self.config.history_path)
525
+ path.parent.mkdir(parents=True, exist_ok=True)
526
+ try:
527
+ data = {
528
+ "wallet": self.config.wallet_address,
529
+ "snapshots": [s.to_dict() for s in self._history[-100:]],
530
+ }
531
+ path.write_text(json.dumps(data, indent=2, default=str))
532
+ except Exception as e:
533
+ logger.warning(f"Failed to save history: {e}")