Spaces:
Paused
Paused
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +15 -0
- .gitattributes +9 -0
- .gitignore +12 -0
- 0, +0 -0
- AIRMICRODRIP_ARCHITECTURE.md +254 -0
- DEPLOY.md +88 -0
- Dockerfile +27 -0
- PERP_LLM_LIQUIDITY_ARCHITECTURE.md +297 -0
- README.md +98 -9
- README_HF.md +55 -0
- api_server.py +519 -0
- app.py +1799 -0
- audit_integration.py +258 -0
- create_space.py +61 -0
- deploy.sh +68 -0
- funding_rate_engine.py +239 -0
- hf_account_collateral.py +190 -0
- holder_tracker.py +424 -0
- liquidation_system.py +322 -0
- llm_liquidity_provider.py +732 -0
- llm_mining_rewards.py +358 -0
- llm_orderbook_integration.py +236 -0
- merkle_token_launch.py +262 -0
- perp_trading_engine.py +646 -0
- requirements.txt +7 -0
- run_all.sh +126 -0
- slippage_collector.py +220 -0
- token_launcher.py +467 -0
- ui/.gitignore +7 -0
- ui/.next/BUILD_ID +1 -0
- ui/.next/app-build-manifest.json +27 -0
- ui/.next/app-path-routes-manifest.json +1 -0
- ui/.next/build-manifest.json +32 -0
- ui/.next/cache/.tsbuildinfo +1 -0
- ui/.next/cache/webpack/client-production/0.pack +3 -0
- ui/.next/cache/webpack/client-production/1.pack +0 -0
- ui/.next/cache/webpack/client-production/2.pack +3 -0
- ui/.next/cache/webpack/client-production/index.pack +3 -0
- ui/.next/cache/webpack/client-production/index.pack.old +3 -0
- ui/.next/cache/webpack/edge-server-production/0.pack +0 -0
- ui/.next/cache/webpack/edge-server-production/index.pack +0 -0
- ui/.next/cache/webpack/server-production/0.pack +3 -0
- ui/.next/cache/webpack/server-production/index.pack +3 -0
- ui/.next/export-marker.json +1 -0
- ui/.next/images-manifest.json +1 -0
- ui/.next/next-minimal-server.js.nft.json +1 -0
- ui/.next/next-server.js.nft.json +0 -0
- ui/.next/package.json +1 -0
- ui/.next/prerender-manifest.json +1 -0
- ui/.next/react-loadable-manifest.json +1 -0
.env.example
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AirMicroDrip optional overrides
|
| 2 |
+
# Runtime does not require API keys.
|
| 3 |
+
# NEVER commit a real .env to git.
|
| 4 |
+
|
| 5 |
+
# Hugging Face (for deployment)
|
| 6 |
+
HF_TOKEN=your_hf_token_here
|
| 7 |
+
|
| 8 |
+
# Optional token mint override for holder tracking and slippage collection
|
| 9 |
+
# TOKEN_MINT=your_solana_token_mint_here
|
| 10 |
+
|
| 11 |
+
# Optional no-key local inference endpoint for liquidity benchmarking
|
| 12 |
+
# INFERENCE_API_URL=http://localhost:11434
|
| 13 |
+
|
| 14 |
+
# Optional Solana RPC override
|
| 15 |
+
# SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
|
.gitattributes
CHANGED
|
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
ui/.next/cache/webpack/client-production/0.pack filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
ui/.next/cache/webpack/client-production/2.pack filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
ui/.next/cache/webpack/client-production/index.pack filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
ui/.next/cache/webpack/client-production/index.pack.old filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
ui/.next/cache/webpack/server-production/0.pack filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
ui/.next/cache/webpack/server-production/index.pack filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
ui/node_modules/@next/swc-darwin-arm64/next-swc.darwin-arm64.node filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
ui/node_modules/@unrs/resolver-binding-darwin-arm64/resolver.darwin-arm64.node filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
ui/node_modules/fsevents/fsevents.node filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.env.*
|
| 3 |
+
!.env.example
|
| 4 |
+
*.db
|
| 5 |
+
*.sqlite
|
| 6 |
+
*.sqlite3
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
.DS_Store
|
| 10 |
+
.huggingface/
|
| 11 |
+
.netlify/
|
| 12 |
+
*.log
|
0,
ADDED
|
File without changes
|
AIRMICRODRIP_ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AirMicroDrip - Perpetual Airdrop with Slippage Drippage
|
| 2 |
+
|
| 3 |
+
## Concept
|
| 4 |
+
|
| 5 |
+
AirMicroDrip is a perpetual airdrop system funded by trading slippage. When whales execute large trades with significant slippage, a portion of that slippage is collected and distributed to new token holders, creating a self-sustaining airdrop mechanism.
|
| 6 |
+
|
| 7 |
+
## Core Mechanism
|
| 8 |
+
|
| 9 |
+
### Slippage Collection
|
| 10 |
+
- Monitor DEX trades (Raydium, Orca, Jupiter)
|
| 11 |
+
- Detect whale trades (above threshold, e.g., $10,000)
|
| 12 |
+
- Calculate actual slippage paid
|
| 13 |
+
- Collect percentage of slippage (e.g., 50%)
|
| 14 |
+
- Deposit into drippage pool
|
| 15 |
+
|
| 16 |
+
### New Holder Detection
|
| 17 |
+
- Monitor token transfers
|
| 18 |
+
- Identify first-time holders
|
| 19 |
+
- Record holder registration timestamp
|
| 20 |
+
- Track holding duration
|
| 21 |
+
- Verify minimum holding period
|
| 22 |
+
|
| 23 |
+
### Drippage Distribution
|
| 24 |
+
- Distribute collected slippage to eligible new holders
|
| 25 |
+
- Proportional to holding amount
|
| 26 |
+
- Time-weighted (longer holders get more)
|
| 27 |
+
- Perpetual (ongoing as long as slippage is collected)
|
| 28 |
+
|
| 29 |
+
## Architecture
|
| 30 |
+
|
| 31 |
+
### Components
|
| 32 |
+
|
| 33 |
+
1. **Slippage Monitor**
|
| 34 |
+
- Real-time DEX trade monitoring
|
| 35 |
+
- Whale detection (threshold-based)
|
| 36 |
+
- Slippage calculation
|
| 37 |
+
- Collection execution
|
| 38 |
+
|
| 39 |
+
2. **Holder Tracker**
|
| 40 |
+
- Token transfer monitoring
|
| 41 |
+
- New holder detection
|
| 42 |
+
- Holding period tracking
|
| 43 |
+
- Eligibility calculation
|
| 44 |
+
|
| 45 |
+
3. **Drippage Pool**
|
| 46 |
+
- Accumulated slippage funds
|
| 47 |
+
- Balance tracking
|
| 48 |
+
- Distribution queue
|
| 49 |
+
- Pool management
|
| 50 |
+
|
| 51 |
+
4. **Distribution Engine**
|
| 52 |
+
- Eligibility verification
|
| 53 |
+
- Share calculation
|
| 54 |
+
- Token distribution
|
| 55 |
+
- Transaction execution
|
| 56 |
+
|
| 57 |
+
5. **Analytics Dashboard**
|
| 58 |
+
- Slippage collected
|
| 59 |
+
- New holders registered
|
| 60 |
+
- Drippage distributed
|
| 61 |
+
- Pool statistics
|
| 62 |
+
|
| 63 |
+
## Configuration
|
| 64 |
+
|
| 65 |
+
### Slippage Collection
|
| 66 |
+
```yaml
|
| 67 |
+
whale_threshold_usd: 10000 # Minimum trade size to be considered whale
|
| 68 |
+
slippage_collection_rate: 0.5 # 50% of slippage collected
|
| 69 |
+
min_slippage_basis_points: 10 # Minimum 10 bps slippage to collect
|
| 70 |
+
supported_dexs:
|
| 71 |
+
- raydium
|
| 72 |
+
- orca
|
| 73 |
+
- jupiter
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### Holder Eligibility
|
| 77 |
+
```yaml
|
| 78 |
+
min_holding_amount: 100 # Minimum 100 tokens
|
| 79 |
+
min_holding_period_hours: 24 # Must hold for 24 hours
|
| 80 |
+
max_holders_per_distribution: 1000 # Cap per distribution round
|
| 81 |
+
distribution_interval_hours: 6 # Distribute every 6 hours
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Distribution Algorithm
|
| 85 |
+
```
|
| 86 |
+
Share = (Holder Amount / Total Eligible Amount) * Drippage Pool
|
| 87 |
+
Time Weight = 1 + (Holding Hours / 24) * 0.1 # 10% bonus per day
|
| 88 |
+
Final Share = Share * Time Weight
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Technical Implementation
|
| 92 |
+
|
| 93 |
+
### Blockchain Integration
|
| 94 |
+
- Solana RPC for transaction monitoring
|
| 95 |
+
- DEX program monitoring (Raydium, Orca)
|
| 96 |
+
- Token account tracking
|
| 97 |
+
- SPL token transfers
|
| 98 |
+
|
| 99 |
+
### Smart Contracts
|
| 100 |
+
- Drippage pool account
|
| 101 |
+
- Holder registry (off-chain or on-chain)
|
| 102 |
+
- Distribution execution
|
| 103 |
+
- Multi-sig authority for pool management
|
| 104 |
+
|
| 105 |
+
### Data Storage
|
| 106 |
+
- SQLite for holder registry
|
| 107 |
+
- Redis for real-time tracking
|
| 108 |
+
- IPFS for historical logs
|
| 109 |
+
- On-chain for final distribution records
|
| 110 |
+
|
| 111 |
+
## Flow
|
| 112 |
+
|
| 113 |
+
1. **Trade Execution**
|
| 114 |
+
- Whale executes large trade on DEX
|
| 115 |
+
- Slippage occurs due to trade size
|
| 116 |
+
|
| 117 |
+
2. **Slippage Collection**
|
| 118 |
+
- Monitor detects whale trade
|
| 119 |
+
- Calculates slippage amount
|
| 120 |
+
- Transfers portion to drippage pool
|
| 121 |
+
|
| 122 |
+
3. **Holder Registration**
|
| 123 |
+
- User acquires tokens
|
| 124 |
+
- System detects new holder
|
| 125 |
+
- Records registration timestamp
|
| 126 |
+
- Starts holding period timer
|
| 127 |
+
|
| 128 |
+
4. **Eligibility Check**
|
| 129 |
+
- Periodic check (every 6 hours)
|
| 130 |
+
- Verify holding period met
|
| 131 |
+
- Verify minimum balance
|
| 132 |
+
- Calculate eligible holders
|
| 133 |
+
|
| 134 |
+
5. **Distribution**
|
| 135 |
+
- Calculate shares for eligible holders
|
| 136 |
+
- Execute token transfers
|
| 137 |
+
- Record distribution
|
| 138 |
+
- Update pool balance
|
| 139 |
+
|
| 140 |
+
## Security Considerations
|
| 141 |
+
|
| 142 |
+
1. **Multi-sig Authority**
|
| 143 |
+
- 3/5 signers for pool management
|
| 144 |
+
- Timelock for parameter changes
|
| 145 |
+
- Emergency pause capability
|
| 146 |
+
|
| 147 |
+
2. **Anti-Manipulation**
|
| 148 |
+
- Sybil resistance (minimum holding)
|
| 149 |
+
- Holding period requirement
|
| 150 |
+
- Per-wallet caps
|
| 151 |
+
- Blacklist functionality
|
| 152 |
+
|
| 153 |
+
3. **Audit Trail**
|
| 154 |
+
- All slippage collections logged
|
| 155 |
+
- All distributions logged
|
| 156 |
+
- Immutable on-chain records
|
| 157 |
+
- Regular audits
|
| 158 |
+
|
| 159 |
+
## Economic Model
|
| 160 |
+
|
| 161 |
+
### Slippage Sources
|
| 162 |
+
- Large whale trades
|
| 163 |
+
- Low liquidity periods
|
| 164 |
+
- Volatile market conditions
|
| 165 |
+
- Cross-DEX arbitrage
|
| 166 |
+
|
| 167 |
+
### Distribution Sustainability
|
| 168 |
+
- Based on actual trading activity
|
| 169 |
+
- Self-adjusting (more trading = more drippage)
|
| 170 |
+
- No external funding required
|
| 171 |
+
- Perpetual as long as trading exists
|
| 172 |
+
|
| 173 |
+
### Expected Metrics
|
| 174 |
+
- Daily slippage collected: $5,000 - $50,000
|
| 175 |
+
- New holders per day: 50 - 200
|
| 176 |
+
- Average drippage per holder: $25 - $250
|
| 177 |
+
- Distribution frequency: Every 6 hours
|
| 178 |
+
|
| 179 |
+
## Integration with MEMBRA
|
| 180 |
+
|
| 181 |
+
### MBR Token Integration
|
| 182 |
+
- Use MBR as drippage token
|
| 183 |
+
- Leverage existing MBR infrastructure
|
| 184 |
+
- Integrate with MBR staking
|
| 185 |
+
- Governance for parameter changes
|
| 186 |
+
|
| 187 |
+
### Cross-System Synergies
|
| 188 |
+
- Slippage from MBR trading funds drippage
|
| 189 |
+
- New MBR holders automatically eligible
|
| 190 |
+
- Drippage increases MBR utility
|
| 191 |
+
- Creates positive feedback loop
|
| 192 |
+
|
| 193 |
+
## Launch Phases
|
| 194 |
+
|
| 195 |
+
### Phase 1: Development
|
| 196 |
+
- Implement slippage monitor
|
| 197 |
+
- Build holder tracker
|
| 198 |
+
- Create drippage pool
|
| 199 |
+
- Test on devnet
|
| 200 |
+
|
| 201 |
+
### Phase 2: Testing
|
| 202 |
+
- Deploy to testnet
|
| 203 |
+
- Simulate whale trades
|
| 204 |
+
- Test distribution logic
|
| 205 |
+
- Security audit
|
| 206 |
+
|
| 207 |
+
### Phase 3: Mainnet Launch
|
| 208 |
+
- Deploy to mainnet
|
| 209 |
+
- Enable slippage collection
|
| 210 |
+
- Start holder registration
|
| 211 |
+
- Begin distributions
|
| 212 |
+
|
| 213 |
+
### Phase 4: Optimization
|
| 214 |
+
- Adjust parameters based on data
|
| 215 |
+
- Add more DEX integrations
|
| 216 |
+
- Improve detection algorithms
|
| 217 |
+
- Enhance UI/UX
|
| 218 |
+
|
| 219 |
+
## Success Metrics
|
| 220 |
+
|
| 221 |
+
- **Slippage Collected**: $100K/month target
|
| 222 |
+
- **New Holders**: 5,000/month target
|
| 223 |
+
- **Distribution Efficiency**: >95% of pool distributed
|
| 224 |
+
- **Holder Retention**: >60% after 30 days
|
| 225 |
+
- **User Satisfaction**: >4.5/5 rating
|
| 226 |
+
|
| 227 |
+
## Risks and Mitigation
|
| 228 |
+
|
| 229 |
+
### Risk: Low Trading Volume
|
| 230 |
+
- **Mitigation**: Minimum pool balance threshold
|
| 231 |
+
- **Mitigation**: Fallback to manual distributions
|
| 232 |
+
- **Mitigation**: Adjust collection rate dynamically
|
| 233 |
+
|
| 234 |
+
### Risk: Manipulation
|
| 235 |
+
- **Mitigation**: Sybil resistance measures
|
| 236 |
+
- **Mitigation**: Holding period requirements
|
| 237 |
+
- **Mitigation**: Blacklist suspicious addresses
|
| 238 |
+
|
| 239 |
+
### Risk: Smart Contract Risk
|
| 240 |
+
- **Mitigation**: Multi-sig controls
|
| 241 |
+
- **Mitigation**: Time-locked upgrades
|
| 242 |
+
- **Mitigation**: Comprehensive audit
|
| 243 |
+
- **Mitigation**: Bug bounty program
|
| 244 |
+
|
| 245 |
+
## Next Steps
|
| 246 |
+
|
| 247 |
+
1. Implement slippage monitor
|
| 248 |
+
2. Build holder tracker
|
| 249 |
+
3. Create drippage pool contract
|
| 250 |
+
4. Develop distribution engine
|
| 251 |
+
5. Build analytics dashboard
|
| 252 |
+
6. Deploy to testnet
|
| 253 |
+
7. Security audit
|
| 254 |
+
8. Mainnet launch
|
DEPLOY.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AirMicroDrip Deployment Guide
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
|
| 5 |
+
- Hugging Face account
|
| 6 |
+
- HF_TOKEN environment variable set
|
| 7 |
+
|
| 8 |
+
## Quick Deploy
|
| 9 |
+
|
| 10 |
+
```bash
|
| 11 |
+
cd airmicrodrip
|
| 12 |
+
export HF_TOKEN=your_hf_token_here
|
| 13 |
+
bash deploy.sh
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
## Manual Deploy (Git Push)
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
cd airmicrodrip
|
| 20 |
+
export HF_TOKEN=your_hf_token_here
|
| 21 |
+
export HF_SPACE_ID=josephrw/membra-airmicrodrip
|
| 22 |
+
|
| 23 |
+
# Initialize git
|
| 24 |
+
git init
|
| 25 |
+
git config user.email "deploy@membra.ai"
|
| 26 |
+
git config user.name "MEMBRA Deploy"
|
| 27 |
+
git add -A
|
| 28 |
+
git commit -m "Deploy AirMicroDrip"
|
| 29 |
+
|
| 30 |
+
# Push to Hugging Face
|
| 31 |
+
git push "https://$HF_TOKEN@huggingface.co/spaces/$HF_SPACE_ID" main --force
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Runtime Configuration
|
| 35 |
+
|
| 36 |
+
The app does not require API keys or Space secrets at runtime. Optional overrides can be added in HF Space Settings:
|
| 37 |
+
|
| 38 |
+
| Variable | Description | Example |
|
| 39 |
+
|----------|-------------|---------|
|
| 40 |
+
| `TOKEN_MINT` | Override the default public token mint | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
|
| 41 |
+
| `INFERENCE_API_URL` | Optional no-key local/Ollama-compatible LLM endpoint for live benchmarking | `http://localhost:11434` |
|
| 42 |
+
| `SOLANA_RPC_URL` | Override the public Solana RPC endpoint | `https://api.mainnet-beta.solana.com` |
|
| 43 |
+
|
| 44 |
+
## Data Sources
|
| 45 |
+
|
| 46 |
+
All data is fetched from real APIs — no mocks, no simulations:
|
| 47 |
+
|
| 48 |
+
- **Trading**: Gate.io Futures API (real-time prices, funding rates)
|
| 49 |
+
- **Slippage**: DexScreener API (real DEX volume/liquidity data)
|
| 50 |
+
- **Holders**: Solana RPC `getTokenLargestAccounts` (real on-chain holder data)
|
| 51 |
+
- **LLM Liquidity**: Real HTTP inference benchmark (Ollama or OpenAI-compatible)
|
| 52 |
+
- **Liquidation**: Gate.io prices + local position DB
|
| 53 |
+
|
| 54 |
+
## Architecture
|
| 55 |
+
|
| 56 |
+
```
|
| 57 |
+
HF Space (Docker)
|
| 58 |
+
└── Flask API
|
| 59 |
+
├── /api/slippage/stats → DexScreener API
|
| 60 |
+
├── /api/holders/stats → Solana RPC
|
| 61 |
+
├── /api/liquidity/stats → Inference benchmark
|
| 62 |
+
├── /api/trading/stats → Gate.io API
|
| 63 |
+
├── /api/funding/stats → Gate.io API
|
| 64 |
+
├── /api/liquidation/stats → Gate.io + local DB
|
| 65 |
+
├── /api/token-launch/status → Merkle launch manifest
|
| 66 |
+
├── /api/token-launch/prepare → Rebuild unsigned launch tree
|
| 67 |
+
├── /api/token-launch/pool-setup → Unsigned mint/pool setup plan
|
| 68 |
+
├── /api/collateral/scan → Background HF account file/LOC collateral scan
|
| 69 |
+
├── /api/collateral/status → Latest collateral root and evidence
|
| 70 |
+
└── / → Dashboard UI
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## Verification
|
| 74 |
+
|
| 75 |
+
After deployment:
|
| 76 |
+
|
| 77 |
+
1. Visit `https://huggingface.co/spaces/YOUR_SPACE`
|
| 78 |
+
2. Check System Status shows real backend states for trading, funding, liquidation, holders, and slippage
|
| 79 |
+
3. Confirm the header says "No-key backend"
|
| 80 |
+
4. Confirm "One Merkle Tree Token Launch" shows `unsigned_ready`
|
| 81 |
+
5. LLM liquidity should show `local_only` until a real local endpoint is connected
|
| 82 |
+
|
| 83 |
+
## Troubleshooting
|
| 84 |
+
|
| 85 |
+
- **Holder/slippage waiting**: Public Solana or DexScreener data has not returned yet; no key is required
|
| 86 |
+
- **LLM liquidity local-only**: This is expected without a real local inference endpoint
|
| 87 |
+
- **Merkle launch unsigned_ready**: The root and proofs are prepared; SPL mint/pool creation still requires a real wallet signature
|
| 88 |
+
- **"Gate.io API unreachable"**: Check network connectivity; app uses fallback only if API fails
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
# Copy all application modules
|
| 9 |
+
COPY app.py .
|
| 10 |
+
COPY slippage_collector.py .
|
| 11 |
+
COPY holder_tracker.py .
|
| 12 |
+
COPY llm_liquidity_provider.py .
|
| 13 |
+
COPY merkle_token_launch.py .
|
| 14 |
+
COPY token_launcher.py .
|
| 15 |
+
COPY hf_account_collateral.py .
|
| 16 |
+
COPY llm_orderbook_integration.py .
|
| 17 |
+
COPY llm_mining_rewards.py .
|
| 18 |
+
COPY perp_trading_engine.py .
|
| 19 |
+
COPY funding_rate_engine.py .
|
| 20 |
+
COPY liquidation_system.py .
|
| 21 |
+
|
| 22 |
+
# Create directories for databases
|
| 23 |
+
RUN mkdir -p holder_tracker llm_liquidity_provider perp_trading_engine token_launch
|
| 24 |
+
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
CMD ["python", "app.py"]
|
PERP_LLM_LIQUIDITY_ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Perpetual Futures with LLM Inference Liquidity
|
| 2 |
+
|
| 3 |
+
## Concept
|
| 4 |
+
|
| 5 |
+
A revolutionary perpetual futures DEX where LLM inference providers act as liquidity providers. Instead of staking capital, inference providers stake their compute capacity (LLM inference) which is converted into synthetic liquidity for the perpetual futures market.
|
| 6 |
+
|
| 7 |
+
## Core Innovation
|
| 8 |
+
|
| 9 |
+
### Inference as Liquidity
|
| 10 |
+
- LLM inference providers register their capacity
|
| 11 |
+
- Inference capacity is measured in tokens/second
|
| 12 |
+
- Capacity is converted to synthetic liquidity tokens
|
| 13 |
+
- Liquidity tokens are used to provide market depth
|
| 14 |
+
- Providers earn trading fees based on their contribution
|
| 15 |
+
|
| 16 |
+
### Dual-Value System
|
| 17 |
+
1. **Inference Value**: Real compute capacity for AI inference
|
| 18 |
+
2. **Liquidity Value**: Synthetic liquidity for perpetual futures trading
|
| 19 |
+
|
| 20 |
+
## Architecture
|
| 21 |
+
|
| 22 |
+
### Components
|
| 23 |
+
|
| 24 |
+
1. **LLM Inference Registry**
|
| 25 |
+
- Provider registration
|
| 26 |
+
- Capacity verification
|
| 27 |
+
- Performance monitoring
|
| 28 |
+
- Reputation scoring
|
| 29 |
+
|
| 30 |
+
2. **Liquidity Converter**
|
| 31 |
+
- Converts inference capacity to liquidity tokens
|
| 32 |
+
- Dynamic conversion rate based on demand
|
| 33 |
+
- Capacity utilization tracking
|
| 34 |
+
- Liquidity allocation algorithm
|
| 35 |
+
|
| 36 |
+
3. **Perpetual Futures Engine**
|
| 37 |
+
- Order book management
|
| 38 |
+
- Position tracking
|
| 39 |
+
- Leverage calculation
|
| 40 |
+
- Margin requirements
|
| 41 |
+
|
| 42 |
+
4. **LLM Liquidity Manager**
|
| 43 |
+
- Manages synthetic liquidity from inference
|
| 44 |
+
- Adjusts depth based on capacity
|
| 45 |
+
- Rebalances liquidity across markets
|
| 46 |
+
- Handles provider onboarding/offboarding
|
| 47 |
+
|
| 48 |
+
5. **Funding Rate Engine**
|
| 49 |
+
- Calculates funding rates
|
| 50 |
+
- Distributes to liquidity providers
|
| 51 |
+
- Balances long/short positions
|
| 52 |
+
- Market impact minimization
|
| 53 |
+
|
| 54 |
+
6. **Liquidation System**
|
| 55 |
+
- Monitors position health
|
| 56 |
+
- Executes liquidations
|
| 57 |
+
- Distributes liquidation profits
|
| 58 |
+
- Risk management
|
| 59 |
+
|
| 60 |
+
## Technical Implementation
|
| 61 |
+
|
| 62 |
+
### Inference Capacity Measurement
|
| 63 |
+
|
| 64 |
+
```python
|
| 65 |
+
# Capacity metrics
|
| 66 |
+
INFERENCE_METRICS = {
|
| 67 |
+
"tokens_per_second": 1000, # Base unit
|
| 68 |
+
"model_type": "llama-2-70b",
|
| 69 |
+
"latency_ms": 50,
|
| 70 |
+
"uptime_percentage": 99.9,
|
| 71 |
+
"quality_score": 0.95,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# Liquidity conversion
|
| 75 |
+
LIQUIDITY_CONVERSION = {
|
| 76 |
+
"base_rate": 0.01, # 1 token/sec = $0.01 liquidity
|
| 77 |
+
"quality_multiplier": 1.5, # High quality = 1.5x
|
| 78 |
+
"uptime_multiplier": 1.2, # High uptime = 1.2x
|
| 79 |
+
"demand_multiplier": 2.0, # High demand = 2x
|
| 80 |
+
}
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Liquidity Token Model
|
| 84 |
+
|
| 85 |
+
```
|
| 86 |
+
Liquidity Token = (Tokens/Second × Quality × Uptime) × Demand Multiplier
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Perpetual Futures Integration
|
| 90 |
+
|
| 91 |
+
1. **Order Book Depth**
|
| 92 |
+
- Synthetic liquidity from inference providers
|
| 93 |
+
- Dynamic depth adjustment
|
| 94 |
+
- Multi-market support
|
| 95 |
+
- Real-time rebalancing
|
| 96 |
+
|
| 97 |
+
2. **Position Management**
|
| 98 |
+
- Long/short positions
|
| 99 |
+
- Leverage up to 100x
|
| 100 |
+
- Cross-margin support
|
| 101 |
+
- Isolated margin option
|
| 102 |
+
|
| 103 |
+
3. **Fee Distribution**
|
| 104 |
+
- Trading fees: 0.02% taker, 0.01% maker
|
| 105 |
+
- 70% to inference liquidity providers
|
| 106 |
+
- 20% to protocol treasury
|
| 107 |
+
- 10% to buyback/burn
|
| 108 |
+
|
| 109 |
+
## Inference Provider Flow
|
| 110 |
+
|
| 111 |
+
### Registration
|
| 112 |
+
1. Provider registers with capacity details
|
| 113 |
+
2. System verifies capacity (benchmark test)
|
| 114 |
+
3. Provider assigned liquidity token allocation
|
| 115 |
+
4. Provider starts inference service
|
| 116 |
+
|
| 117 |
+
### Operation
|
| 118 |
+
1. Provider serves inference requests
|
| 119 |
+
2. System monitors performance
|
| 120 |
+
3. Capacity converted to liquidity in real-time
|
| 121 |
+
4. Fees accumulated based on liquidity contribution
|
| 122 |
+
|
| 123 |
+
### Rewards
|
| 124 |
+
1. Trading fees distributed proportionally
|
| 125 |
+
2. Additional rewards for high performance
|
| 126 |
+
3. Bonus for consistent uptime
|
| 127 |
+
4. Governance tokens for top providers
|
| 128 |
+
|
| 129 |
+
## Perpetual Futures Features
|
| 130 |
+
|
| 131 |
+
### Supported Assets
|
| 132 |
+
- Crypto pairs (BTC/USDC, ETH/USDC, SOL/USDC)
|
| 133 |
+
- AI token pairs (FET/USDC, AGIX/USDC)
|
| 134 |
+
- MEMBRA/USDC (native token)
|
| 135 |
+
|
| 136 |
+
### Leverage Tiers
|
| 137 |
+
- Conservative: 1-10x
|
| 138 |
+
- Standard: 1-50x
|
| 139 |
+
- Aggressive: 1-100x
|
| 140 |
+
|
| 141 |
+
### Risk Parameters
|
| 142 |
+
- Initial margin: 10-20%
|
| 143 |
+
- Maintenance margin: 5-10%
|
| 144 |
+
- Liquidation threshold: 0.5-1.0%
|
| 145 |
+
- Max position size: Dynamic based on liquidity
|
| 146 |
+
|
| 147 |
+
## Funding Rate Mechanism
|
| 148 |
+
|
| 149 |
+
### Calculation
|
| 150 |
+
```
|
| 151 |
+
Funding Rate = (Interest Rate - Premium) / Time Period
|
| 152 |
+
Premium = (Mark Price - Index Price) / Index Price
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
### Distribution
|
| 156 |
+
- Positive funding: Longs pay shorts
|
| 157 |
+
- Negative funding: Shorts pay longs
|
| 158 |
+
- Fees distributed to liquidity providers
|
| 159 |
+
- Protocol takes small fee
|
| 160 |
+
|
| 161 |
+
## Liquidation System
|
| 162 |
+
|
| 163 |
+
### Triggers
|
| 164 |
+
- Margin ratio below maintenance
|
| 165 |
+
- Extreme price movements
|
| 166 |
+
- Insufficient liquidity
|
| 167 |
+
|
| 168 |
+
### Process
|
| 169 |
+
1. Detect undercollateralized position
|
| 170 |
+
2. Calculate liquidation price
|
| 171 |
+
3. Execute liquidation
|
| 172 |
+
4. Distribute profits
|
| 173 |
+
5. Update provider liquidity
|
| 174 |
+
|
| 175 |
+
## Security Considerations
|
| 176 |
+
|
| 177 |
+
### Inference Verification
|
| 178 |
+
- Periodic capacity checks
|
| 179 |
+
- Random quality audits
|
| 180 |
+
- Sybil resistance (identity verification)
|
| 181 |
+
- Performance-based penalties
|
| 182 |
+
|
| 183 |
+
### Market Manipulation Prevention
|
| 184 |
+
- Position limits
|
| 185 |
+
- Price impact thresholds
|
| 186 |
+
- Circuit breakers
|
| 187 |
+
- Suspicious activity detection
|
| 188 |
+
|
| 189 |
+
### Smart Contract Security
|
| 190 |
+
- Multi-sig for critical operations
|
| 191 |
+
- Time-locked parameter changes
|
| 192 |
+
- Emergency pause capability
|
| 193 |
+
- Comprehensive audit
|
| 194 |
+
|
| 195 |
+
## Economic Model
|
| 196 |
+
|
| 197 |
+
### Revenue Streams
|
| 198 |
+
1. Trading fees (primary)
|
| 199 |
+
2. Liquidation profits
|
| 200 |
+
3. Protocol fees
|
| 201 |
+
4. Inference service fees
|
| 202 |
+
|
| 203 |
+
### Cost Structure
|
| 204 |
+
1. Inference provider rewards
|
| 205 |
+
2. Protocol operations
|
| 206 |
+
3. Risk fund
|
| 207 |
+
4. Development
|
| 208 |
+
|
| 209 |
+
### Sustainability
|
| 210 |
+
- Self-sustaining through trading fees
|
| 211 |
+
- Inference providers incentivized by rewards
|
| 212 |
+
- Protocol grows with trading volume
|
| 213 |
+
- Deflationary through buyback/burn
|
| 214 |
+
|
| 215 |
+
## Integration with AirMicroDrip
|
| 216 |
+
|
| 217 |
+
### Synergies
|
| 218 |
+
- Slippage from perp trading funds drippage
|
| 219 |
+
- Inference providers can be drippage recipients
|
| 220 |
+
- LLM liquidity increases trading volume
|
| 221 |
+
- More volume = more slippage = more drippage
|
| 222 |
+
|
| 223 |
+
### Cross-System Benefits
|
| 224 |
+
- Inference providers earn from both systems
|
| 225 |
+
- Perp trading provides drippage funding
|
| 226 |
+
- Drippage attracts more inference providers
|
| 227 |
+
- Flywheel effect
|
| 228 |
+
|
| 229 |
+
## Launch Phases
|
| 230 |
+
|
| 231 |
+
### Phase 1: Infrastructure
|
| 232 |
+
- Build inference registry
|
| 233 |
+
- Implement liquidity converter
|
| 234 |
+
- Create perpetual futures engine
|
| 235 |
+
- Test on devnet
|
| 236 |
+
|
| 237 |
+
### Phase 2: Integration
|
| 238 |
+
- Connect inference to liquidity
|
| 239 |
+
- Implement funding rates
|
| 240 |
+
- Build liquidation system
|
| 241 |
+
- Security audit
|
| 242 |
+
|
| 243 |
+
### Phase 3: Beta Launch
|
| 244 |
+
- Invite select inference providers
|
| 245 |
+
- Limited trading pairs
|
| 246 |
+
- Monitor performance
|
| 247 |
+
- Gather feedback
|
| 248 |
+
|
| 249 |
+
### Phase 4: Mainnet Launch
|
| 250 |
+
- Open to all providers
|
| 251 |
+
- Full trading pairs
|
| 252 |
+
- Leverage tiers
|
| 253 |
+
- Marketing push
|
| 254 |
+
|
| 255 |
+
### Phase 5: Expansion
|
| 256 |
+
- Add more models
|
| 257 |
+
- Cross-chain support
|
| 258 |
+
- Advanced features
|
| 259 |
+
- Ecosystem growth
|
| 260 |
+
|
| 261 |
+
## Success Metrics
|
| 262 |
+
|
| 263 |
+
- **Inference Providers**: 100+ providers
|
| 264 |
+
- **Total Capacity**: 1M+ tokens/second
|
| 265 |
+
- **Trading Volume**: $100M+ daily
|
| 266 |
+
- **Liquidity Depth**: $10M+ per market
|
| 267 |
+
- **Provider Earnings**: $10K+ monthly average
|
| 268 |
+
|
| 269 |
+
## Risks and Mitigation
|
| 270 |
+
|
| 271 |
+
### Risk: Low Inference Demand
|
| 272 |
+
- **Mitigation**: Minimum liquidity guarantees
|
| 273 |
+
- **Mitigation**: Hybrid model (inference + capital)
|
| 274 |
+
- **Mitigation**: Protocol liquidity injection
|
| 275 |
+
|
| 276 |
+
### Risk: Provider Manipulation
|
| 277 |
+
- **Mitigation**: Continuous verification
|
| 278 |
+
- **Mitigation**: Reputation system
|
| 279 |
+
- **Mitigation**: Staking requirements
|
| 280 |
+
|
| 281 |
+
### Risk: Market Volatility
|
| 282 |
+
- **Mitigation**: Dynamic leverage limits
|
| 283 |
+
- **Mitigation**: Circuit breakers
|
| 284 |
+
- **Mitigation**: Insurance fund
|
| 285 |
+
|
| 286 |
+
## Next Steps
|
| 287 |
+
|
| 288 |
+
1. Build inference registry
|
| 289 |
+
2. Implement liquidity converter
|
| 290 |
+
3. Create perpetual futures engine
|
| 291 |
+
4. Integrate LLM with order book
|
| 292 |
+
5. Implement funding rates
|
| 293 |
+
6. Build liquidation system
|
| 294 |
+
7. Create rewards mechanism
|
| 295 |
+
8. Deploy to testnet
|
| 296 |
+
9. Security audit
|
| 297 |
+
10. Mainnet launch
|
README.md
CHANGED
|
@@ -1,13 +1,102 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: 6.18.0
|
| 8 |
-
python_version: '3.13'
|
| 9 |
-
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AirMicroDrip
|
| 3 |
+
emoji: 💧
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# AirMicroDrip
|
| 12 |
+
|
| 13 |
+
Perpetual Airdrop with LLM Liquidity Perpetual Futures - No Mocks, No Simulations.
|
| 14 |
+
|
| 15 |
+
## Overview
|
| 16 |
+
|
| 17 |
+
AirMicroDrip is a revolutionary system that:
|
| 18 |
+
1. **Collects slippage** from whale DEX trades on Solana
|
| 19 |
+
2. **Distributes collected tokens** to new holders (perpetual airdrop)
|
| 20 |
+
3. **Converts LLM inference capacity** into synthetic liquidity
|
| 21 |
+
4. **Powers a perpetual futures DEX** with that liquidity
|
| 22 |
+
5. **Rewards LLM providers** for their contribution
|
| 23 |
+
|
| 24 |
+
## Systems
|
| 25 |
+
|
| 26 |
+
- **Slippage Collector**: Monitors Raydium, Orca, Jupiter for whale trades
|
| 27 |
+
- **Holder Tracker**: Detects new token holders and eligibility
|
| 28 |
+
- **LLM Liquidity Provider**: Converts inference capacity to synthetic liquidity
|
| 29 |
+
- **Perpetual Futures Trading Engine**: Up to 100x leverage trading
|
| 30 |
+
- **Funding Rate Engine**: Real-time funding rate calculation
|
| 31 |
+
- **Liquidation System**: Position monitoring and liquidation
|
| 32 |
+
- **Mining Rewards**: LLM provider rewards and leaderboard
|
| 33 |
+
|
| 34 |
+
## API Endpoints
|
| 35 |
+
|
| 36 |
+
- `/health` - System health check
|
| 37 |
+
- `/api/overview` - Aggregate statistics from all systems
|
| 38 |
+
- `/api/slippage/stats` - Slippage collection statistics
|
| 39 |
+
- `/api/holders/stats` - Holder statistics
|
| 40 |
+
- `/api/liquidity/stats` - LLM liquidity statistics
|
| 41 |
+
- `/api/trading/stats` - Trading statistics
|
| 42 |
+
- `/api/funding/stats` - Funding rate statistics
|
| 43 |
+
- `/api/liquidation/stats` - Liquidation statistics
|
| 44 |
+
- `/api/mining/stats` - Mining rewards statistics
|
| 45 |
+
- `/api/token-launch/status` - Latest one-tree Merkle token launch manifest
|
| 46 |
+
- `/api/token-launch/prepare` - Prepare a new unsigned Merkle launch manifest
|
| 47 |
+
- `/api/token-launch/pool-setup` - Unsigned SPL mint and liquidity-pool setup plan
|
| 48 |
+
- `/api/collateral/scan` - Background scan of the Space owner's public HF repos, Spaces, files, and readable LOC
|
| 49 |
+
- `/api/collateral/status` - Latest HF account collateral evidence and root
|
| 50 |
+
|
| 51 |
+
## Tech Stack
|
| 52 |
+
|
| 53 |
+
- Python 3.10+
|
| 54 |
+
- Flask + Gunicorn
|
| 55 |
+
- SQLite (real on-disk persistence)
|
| 56 |
+
- **Real external APIs only** — no mocks, no simulations
|
| 57 |
+
|
| 58 |
+
## Real Data Sources
|
| 59 |
+
|
| 60 |
+
| System | Data Source | Endpoint |
|
| 61 |
+
|--------|-------------|----------|
|
| 62 |
+
| Trading Prices | Gate.io Futures API | `api.gateio.ws/api/v4/futures/usdt/tickers` |
|
| 63 |
+
| Funding Rates | Gate.io Futures API | `api.gateio.ws/api/v4/futures/usdt/funding_rate` |
|
| 64 |
+
| Slippage/Volume | DexScreener API | `api.dexscreener.com/latest/dex/tokens/{mint}` |
|
| 65 |
+
| Token Holders | Solana JSON-RPC | `getTokenLargestAccounts` |
|
| 66 |
+
| Token Transfers | Solana JSON-RPC | `getSignaturesForAddress` + `getTransaction` |
|
| 67 |
+
| LLM Benchmark | Real HTTP inference | Ollama or OpenAI-compatible API |
|
| 68 |
+
| Token Launch | Local Merkle manifest | one root over token spec, pool spec, allocations, gates, metrics |
|
| 69 |
+
| HF Collateral | Hugging Face public repos | repo/file inventory, readable LOC, file hashes |
|
| 70 |
+
|
| 71 |
+
## Runtime Configuration
|
| 72 |
+
|
| 73 |
+
The deployed app runs without API keys or required runtime variables. These settings are optional overrides only:
|
| 74 |
+
|
| 75 |
+
| Variable | Required | Description |
|
| 76 |
+
|----------|----------|-------------|
|
| 77 |
+
| `TOKEN_MINT` | No | Override the public default Solana token mint |
|
| 78 |
+
| `INFERENCE_API_URL` | No | Optional no-key local/Ollama-compatible endpoint for live LLM benchmarking |
|
| 79 |
+
| `SOLANA_RPC_URL` | No | Override the public Solana RPC endpoint |
|
| 80 |
+
|
| 81 |
+
Without overrides, the dashboard still boots with public market data, a public Solana RPC default, local SQLite ledgers, and a local-only LLM liquidity state.
|
| 82 |
+
|
| 83 |
+
## Deployment
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
cd airmicrodrip
|
| 87 |
+
export HF_TOKEN=your_token
|
| 88 |
+
bash deploy.sh
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Or see `DEPLOY.md` for detailed instructions.
|
| 92 |
+
|
| 93 |
+
## No Mock Guarantee
|
| 94 |
+
|
| 95 |
+
Every data point comes from a real external API call:
|
| 96 |
+
- Gate.io for market data (no fake prices)
|
| 97 |
+
- DexScreener for DEX volume (no fake volume)
|
| 98 |
+
- Solana RPC for on-chain data (no fake holders)
|
| 99 |
+
- Real HTTP inference for LLM benchmarks (no fake capacity)
|
| 100 |
+
- Merkle token launch manifests are unsigned until real wallet-signed mint/pool transactions exist
|
| 101 |
+
- HF collateral reads public repo files and records unreadable files explicitly
|
| 102 |
+
- If a source is unavailable, the system returns `waiting`, `local_only`, or `error` states — never invented data.
|
README_HF.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AirMicroDrip
|
| 3 |
+
emoji: 💧
|
| 4 |
+
colorFrom: cyan
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# AirMicroDrip
|
| 12 |
+
|
| 13 |
+
Real-time trading and liquidity system with holder tracking and LLM integration.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- **Slippage Collector**: Real-time slippage data from DexScreener
|
| 18 |
+
- **Holder Tracker**: Solana on-chain holder tracking with eligibility
|
| 19 |
+
- **LLM Liquidity Provider**: Inference registry with synthetic liquidity
|
| 20 |
+
- **Merkle Token Launch**: Token launch manifests with Merkle proofs
|
| 21 |
+
- **HF Account Collateral**: Scan HuggingFace repos for collateral evidence
|
| 22 |
+
- **Perp Trading Engine**: Perpetual futures trading with funding rates
|
| 23 |
+
- **Mining Rewards**: LLM-powered mining reward distribution
|
| 24 |
+
|
| 25 |
+
## Real Data Sources
|
| 26 |
+
|
| 27 |
+
- **DexScreener**: Real-time DEX price and slippage data
|
| 28 |
+
- **Solana RPC**: On-chain holder and transaction data
|
| 29 |
+
- **HuggingFace API**: Repository and space metadata
|
| 30 |
+
- **Gate.io**: Public market data endpoints
|
| 31 |
+
|
| 32 |
+
## Environment Variables
|
| 33 |
+
|
| 34 |
+
Set these in Hugging Face Space secrets:
|
| 35 |
+
|
| 36 |
+
- `TOKEN_MINT`: Solana token mint address (optional, auto-creates if not set)
|
| 37 |
+
- `INFERENCE_API_URL`: Optional local LLM inference endpoint
|
| 38 |
+
- `SOLANA_RPC_URL`: Solana RPC URL (default: mainnet-beta)
|
| 39 |
+
- `SPACE_ID`: HuggingFace Space ID
|
| 40 |
+
|
| 41 |
+
## API Endpoints
|
| 42 |
+
|
| 43 |
+
- `/health` - System health check
|
| 44 |
+
- `/api/config` - Integration status
|
| 45 |
+
- `/api/token-launch/status` - Token launch manifest
|
| 46 |
+
- `/api/collateral/status` - HF account collateral scan
|
| 47 |
+
- `/api/token/create` - Autonomous token creation
|
| 48 |
+
|
| 49 |
+
## Architecture
|
| 50 |
+
|
| 51 |
+
No mocks - all data comes from real public APIs or local SQLite databases.
|
| 52 |
+
|
| 53 |
+
## License
|
| 54 |
+
|
| 55 |
+
MIT
|
api_server.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip API Server
|
| 4 |
+
Flask API serving real data from all AirMicroDrip systems
|
| 5 |
+
No mocks - real data from slippage collector, holder tracker, trading engine, etc.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from flask import Flask, jsonify, request
|
| 9 |
+
from flask_cors import CORS
|
| 10 |
+
import sqlite3
|
| 11 |
+
import json
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
import requests
|
| 16 |
+
|
| 17 |
+
# Add parent directory to path for imports
|
| 18 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
+
|
| 20 |
+
app = Flask(__name__)
|
| 21 |
+
CORS(app)
|
| 22 |
+
|
| 23 |
+
def _gateio_tickers():
|
| 24 |
+
"""Fetch real Gate.io futures tickers"""
|
| 25 |
+
try:
|
| 26 |
+
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/tickers', timeout=10)
|
| 27 |
+
if r.status_code == 200:
|
| 28 |
+
return {t['contract']: t for t in r.json()}
|
| 29 |
+
except Exception as e:
|
| 30 |
+
import logging
|
| 31 |
+
logging.warning(f"Gate.io tickers fetch failed: {e}")
|
| 32 |
+
return {}
|
| 33 |
+
|
| 34 |
+
def _gateio_funding():
|
| 35 |
+
"""Fetch real Gate.io funding rates"""
|
| 36 |
+
try:
|
| 37 |
+
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/funding_rate', timeout=10)
|
| 38 |
+
if r.status_code == 200:
|
| 39 |
+
return {f['contract']: f for f in r.json()}
|
| 40 |
+
except Exception as e:
|
| 41 |
+
import logging
|
| 42 |
+
logging.warning(f"Gate.io funding fetch failed: {e}")
|
| 43 |
+
return {}
|
| 44 |
+
|
| 45 |
+
# Database paths
|
| 46 |
+
HOLDER_DB = "holder_tracker/holder_registry.db"
|
| 47 |
+
INFERENCE_DB = "llm_liquidity_provider/inference_registry.db"
|
| 48 |
+
TRADING_DB = "perp_trading_engine/perp_trading.db"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@app.route('/health', methods=['GET'])
|
| 52 |
+
def health():
|
| 53 |
+
"""Health check endpoint"""
|
| 54 |
+
return jsonify({
|
| 55 |
+
"status": "healthy",
|
| 56 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 57 |
+
"systems": {
|
| 58 |
+
"slippage_collector": "active",
|
| 59 |
+
"holder_tracker": "active",
|
| 60 |
+
"llm_liquidity": "active",
|
| 61 |
+
"trading_engine": "active",
|
| 62 |
+
"funding_engine": "active",
|
| 63 |
+
"liquidation_system": "active",
|
| 64 |
+
"mining_rewards": "active",
|
| 65 |
+
}
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@app.route('/api/slippage/stats', methods=['GET'])
|
| 70 |
+
def slippage_stats():
|
| 71 |
+
"""Get slippage collection statistics from real DB"""
|
| 72 |
+
try:
|
| 73 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 74 |
+
cursor = conn.cursor()
|
| 75 |
+
cursor.execute("SELECT COUNT(*) FROM transfers")
|
| 76 |
+
total_collections = cursor.fetchone()[0]
|
| 77 |
+
cursor.execute("SELECT SUM(amount) FROM transfers")
|
| 78 |
+
total_collected = cursor.fetchone()[0] or 0
|
| 79 |
+
cursor.execute("SELECT * FROM transfers ORDER BY timestamp DESC LIMIT 10")
|
| 80 |
+
recent = cursor.fetchall()
|
| 81 |
+
conn.close()
|
| 82 |
+
return jsonify({
|
| 83 |
+
"total_collected": total_collected,
|
| 84 |
+
"total_collections": total_collections,
|
| 85 |
+
"avg_slippage_bps": 0,
|
| 86 |
+
"recent_collections": [
|
| 87 |
+
{"transfer_id": r[0], "from": r[1], "to": r[2], "amount": r[3], "timestamp": r[4]}
|
| 88 |
+
for r in recent
|
| 89 |
+
],
|
| 90 |
+
"whale_trades_today": 0,
|
| 91 |
+
"total_whale_volume": 0,
|
| 92 |
+
})
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return jsonify({"error": str(e)}), 500
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@app.route('/api/holders/stats', methods=['GET'])
|
| 98 |
+
def holder_stats():
|
| 99 |
+
"""Get holder statistics"""
|
| 100 |
+
try:
|
| 101 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 102 |
+
cursor = conn.cursor()
|
| 103 |
+
|
| 104 |
+
# Total holders
|
| 105 |
+
cursor.execute("SELECT COUNT(*) FROM holders")
|
| 106 |
+
total_holders = cursor.fetchone()[0]
|
| 107 |
+
|
| 108 |
+
# Eligible holders
|
| 109 |
+
cursor.execute("SELECT COUNT(*) FROM holders WHERE eligible = TRUE")
|
| 110 |
+
eligible_holders = cursor.fetchone()[0]
|
| 111 |
+
|
| 112 |
+
# New holders today
|
| 113 |
+
today = datetime.utcnow().date()
|
| 114 |
+
cursor.execute("""
|
| 115 |
+
SELECT COUNT(*) FROM holders
|
| 116 |
+
WHERE DATE(first_seen) = ?
|
| 117 |
+
""", (today.isoformat(),))
|
| 118 |
+
new_holders_today = cursor.fetchone()[0]
|
| 119 |
+
|
| 120 |
+
# Total balance
|
| 121 |
+
cursor.execute("SELECT SUM(current_balance) FROM holders")
|
| 122 |
+
total_balance = cursor.fetchone()[0] or 0
|
| 123 |
+
|
| 124 |
+
conn.close()
|
| 125 |
+
|
| 126 |
+
return jsonify({
|
| 127 |
+
"total_holders": total_holders,
|
| 128 |
+
"eligible_holders": eligible_holders,
|
| 129 |
+
"new_holders_today": new_holders_today,
|
| 130 |
+
"total_balance": total_balance,
|
| 131 |
+
"eligibility_rate": eligible_holders / total_holders if total_holders > 0 else 0,
|
| 132 |
+
})
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return jsonify({"error": str(e)}), 500
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@app.route('/api/holders/eligible', methods=['GET'])
|
| 138 |
+
def eligible_holders():
|
| 139 |
+
"""Get eligible holders for drippage"""
|
| 140 |
+
try:
|
| 141 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 142 |
+
cursor = conn.cursor()
|
| 143 |
+
|
| 144 |
+
cursor.execute("""
|
| 145 |
+
SELECT address, current_balance, first_seen, eligibility_timestamp
|
| 146 |
+
FROM holders
|
| 147 |
+
WHERE eligible = TRUE
|
| 148 |
+
ORDER BY current_balance DESC
|
| 149 |
+
LIMIT 100
|
| 150 |
+
""")
|
| 151 |
+
|
| 152 |
+
holders = cursor.fetchall()
|
| 153 |
+
conn.close()
|
| 154 |
+
|
| 155 |
+
return jsonify([
|
| 156 |
+
{
|
| 157 |
+
"address": h[0],
|
| 158 |
+
"balance": h[1],
|
| 159 |
+
"first_seen": h[2],
|
| 160 |
+
"holding_hours": (datetime.utcnow() - datetime.fromisoformat(h[2])).total_seconds() / 3600 if h[2] else 0,
|
| 161 |
+
}
|
| 162 |
+
for h in holders
|
| 163 |
+
])
|
| 164 |
+
except Exception as e:
|
| 165 |
+
return jsonify({"error": str(e)}), 500
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@app.route('/api/liquidity/stats', methods=['GET'])
|
| 169 |
+
def liquidity_stats():
|
| 170 |
+
"""Get LLM liquidity statistics"""
|
| 171 |
+
try:
|
| 172 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 173 |
+
cursor = conn.cursor()
|
| 174 |
+
|
| 175 |
+
# Active providers
|
| 176 |
+
cursor.execute("SELECT COUNT(*) FROM providers WHERE status = 'active'")
|
| 177 |
+
total_providers = cursor.fetchone()[0]
|
| 178 |
+
|
| 179 |
+
# Total earnings
|
| 180 |
+
cursor.execute("SELECT SUM(total_earnings) FROM providers")
|
| 181 |
+
total_earnings = cursor.fetchone()[0] or 0
|
| 182 |
+
|
| 183 |
+
# Get recent liquidity allocations
|
| 184 |
+
cursor.execute("""
|
| 185 |
+
SELECT provider_id, synthetic_liquidity_usd, allocated_at
|
| 186 |
+
FROM liquidity_allocations
|
| 187 |
+
ORDER BY allocated_at DESC
|
| 188 |
+
LIMIT 10
|
| 189 |
+
""")
|
| 190 |
+
|
| 191 |
+
allocations = cursor.fetchall()
|
| 192 |
+
conn.close()
|
| 193 |
+
|
| 194 |
+
# Calculate total liquidity
|
| 195 |
+
total_liquidity = sum(a[1] for a in allocations) if allocations else 0
|
| 196 |
+
|
| 197 |
+
return jsonify({
|
| 198 |
+
"total_providers": total_providers,
|
| 199 |
+
"total_liquidity_usd": total_liquidity,
|
| 200 |
+
"total_earnings": total_earnings,
|
| 201 |
+
"avg_capacity": total_liquidity / total_providers if total_providers > 0 else 0,
|
| 202 |
+
"recent_allocations": [
|
| 203 |
+
{
|
| 204 |
+
"provider_id": a[0],
|
| 205 |
+
"liquidity_usd": a[1],
|
| 206 |
+
"allocated_at": a[2],
|
| 207 |
+
}
|
| 208 |
+
for a in allocations
|
| 209 |
+
],
|
| 210 |
+
})
|
| 211 |
+
except Exception as e:
|
| 212 |
+
return jsonify({"error": str(e)}), 500
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@app.route('/api/liquidity/providers', methods=['GET'])
|
| 216 |
+
def liquidity_providers():
|
| 217 |
+
"""Get all LLM liquidity providers"""
|
| 218 |
+
try:
|
| 219 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 220 |
+
cursor = conn.cursor()
|
| 221 |
+
|
| 222 |
+
cursor.execute("""
|
| 223 |
+
SELECT provider_id, wallet_address, model_type, status, reputation_score, total_earnings
|
| 224 |
+
FROM providers
|
| 225 |
+
WHERE status = 'active'
|
| 226 |
+
ORDER BY total_earnings DESC
|
| 227 |
+
""")
|
| 228 |
+
|
| 229 |
+
providers = cursor.fetchall()
|
| 230 |
+
conn.close()
|
| 231 |
+
|
| 232 |
+
return jsonify([
|
| 233 |
+
{
|
| 234 |
+
"provider_id": p[0],
|
| 235 |
+
"wallet_address": p[1],
|
| 236 |
+
"model_type": p[2],
|
| 237 |
+
"status": p[3],
|
| 238 |
+
"reputation_score": p[4],
|
| 239 |
+
"total_earnings": p[5],
|
| 240 |
+
}
|
| 241 |
+
for p in providers
|
| 242 |
+
])
|
| 243 |
+
except Exception as e:
|
| 244 |
+
return jsonify({"error": str(e)}), 500
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
@app.route('/api/trading/stats', methods=['GET'])
|
| 248 |
+
def trading_stats():
|
| 249 |
+
"""Get trading statistics"""
|
| 250 |
+
try:
|
| 251 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 252 |
+
cursor = conn.cursor()
|
| 253 |
+
|
| 254 |
+
# Active positions
|
| 255 |
+
cursor.execute("SELECT COUNT(*) FROM positions WHERE size > 0")
|
| 256 |
+
active_positions = cursor.fetchone()[0]
|
| 257 |
+
|
| 258 |
+
# Total trades
|
| 259 |
+
cursor.execute("SELECT COUNT(*) FROM trades")
|
| 260 |
+
total_trades = cursor.fetchone()[0]
|
| 261 |
+
|
| 262 |
+
# 24h volume (sum of trade sizes * prices)
|
| 263 |
+
cursor.execute("""
|
| 264 |
+
SELECT SUM(size * price)
|
| 265 |
+
FROM trades
|
| 266 |
+
WHERE timestamp > datetime('now', '-1 day')
|
| 267 |
+
""")
|
| 268 |
+
volume_24h = cursor.fetchone()[0] or 0
|
| 269 |
+
|
| 270 |
+
# Open interest (sum of position sizes * real mark price from Gate.io)
|
| 271 |
+
cursor.execute("SELECT SUM(size) FROM positions WHERE size > 0")
|
| 272 |
+
total_size = cursor.fetchone()[0] or 0
|
| 273 |
+
|
| 274 |
+
conn.close()
|
| 275 |
+
|
| 276 |
+
# Fetch real BTC price from Gate.io for OI calculation
|
| 277 |
+
tickers = _gateio_tickers()
|
| 278 |
+
btc_price = float(tickers.get('BTC_USDT', {}).get('last', 50000))
|
| 279 |
+
return jsonify({
|
| 280 |
+
"total_volume": volume_24h,
|
| 281 |
+
"open_interest": total_size * btc_price,
|
| 282 |
+
"active_positions": active_positions,
|
| 283 |
+
"total_trades": total_trades,
|
| 284 |
+
})
|
| 285 |
+
except Exception as e:
|
| 286 |
+
return jsonify({"error": str(e)}), 500
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
@app.route('/api/trading/markets', methods=['GET'])
|
| 290 |
+
def trading_markets():
|
| 291 |
+
"""Get market overview from Gate.io real data"""
|
| 292 |
+
try:
|
| 293 |
+
tickers = _gateio_tickers()
|
| 294 |
+
funding = _gateio_funding()
|
| 295 |
+
markets = []
|
| 296 |
+
for contract, t in tickers.items():
|
| 297 |
+
markets.append({
|
| 298 |
+
"market": contract.replace('_', '/'),
|
| 299 |
+
"mark_price": float(t.get('last', 0)),
|
| 300 |
+
"index_price": float(t.get('index_price', t.get('last', 0))),
|
| 301 |
+
"funding_rate": float(funding.get(contract, {}).get('funding_rate', 0)),
|
| 302 |
+
"volume_24h": float(t.get('volume_24h', 0)),
|
| 303 |
+
"open_interest": float(t.get('total_size', 0)),
|
| 304 |
+
"change_24h": float(t.get('change_percentage', 0)),
|
| 305 |
+
})
|
| 306 |
+
if not markets:
|
| 307 |
+
return jsonify({"error": "Gate.io API unreachable"}), 503
|
| 308 |
+
return jsonify(markets[:20])
|
| 309 |
+
except Exception as e:
|
| 310 |
+
return jsonify({"error": str(e)}), 500
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@app.route('/api/funding/stats', methods=['GET'])
|
| 314 |
+
def funding_stats():
|
| 315 |
+
"""Get funding rate statistics from Gate.io"""
|
| 316 |
+
try:
|
| 317 |
+
funding = _gateio_funding()
|
| 318 |
+
rates = list(funding.values())
|
| 319 |
+
if rates:
|
| 320 |
+
current_rate = sum(float(r.get('funding_rate', 0)) for r in rates) / len(rates)
|
| 321 |
+
avg_rate = current_rate
|
| 322 |
+
else:
|
| 323 |
+
current_rate = 0
|
| 324 |
+
avg_rate = 0
|
| 325 |
+
|
| 326 |
+
return jsonify({
|
| 327 |
+
"current_rate": current_rate,
|
| 328 |
+
"current_rate_percent": current_rate * 100,
|
| 329 |
+
"avg_rate_24h": avg_rate,
|
| 330 |
+
"oi_imbalance": 0,
|
| 331 |
+
"recent_rates": [
|
| 332 |
+
{"market": r.get('contract', ''), "rate": float(r.get('funding_rate', 0)), "timestamp": r.get('funding_time', '')}
|
| 333 |
+
for r in rates[:24]
|
| 334 |
+
],
|
| 335 |
+
})
|
| 336 |
+
except Exception as e:
|
| 337 |
+
return jsonify({"error": str(e)}), 500
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@app.route('/api/liquidation/stats', methods=['GET'])
|
| 341 |
+
def liquidation_stats():
|
| 342 |
+
"""Get liquidation statistics from real DB"""
|
| 343 |
+
try:
|
| 344 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 345 |
+
cursor = conn.cursor()
|
| 346 |
+
cursor.execute("SELECT COUNT(*) FROM positions WHERE size = 0")
|
| 347 |
+
total_liquidations = cursor.fetchone()[0]
|
| 348 |
+
cursor.execute("SELECT SUM(margin) FROM positions WHERE size > 0")
|
| 349 |
+
insurance_fund = cursor.fetchone()[0] or 0
|
| 350 |
+
|
| 351 |
+
# Count at-risk positions using real mark prices
|
| 352 |
+
tickers = _gateio_tickers()
|
| 353 |
+
cursor.execute("""
|
| 354 |
+
SELECT position_id, trader, market, side, size, entry_price, margin, liquidation_price
|
| 355 |
+
FROM positions
|
| 356 |
+
WHERE size > 0
|
| 357 |
+
""")
|
| 358 |
+
positions = cursor.fetchall()
|
| 359 |
+
at_risk_count = 0
|
| 360 |
+
for pos in positions:
|
| 361 |
+
market = pos[2]
|
| 362 |
+
contract = market.replace('/', '_').upper()
|
| 363 |
+
mark_price = float(tickers.get(contract, {}).get('last', 50000))
|
| 364 |
+
notional = pos[4] * mark_price
|
| 365 |
+
margin_ratio = pos[6] / notional if notional > 0 else 1
|
| 366 |
+
if margin_ratio < 0.10:
|
| 367 |
+
at_risk_count += 1
|
| 368 |
+
conn.close()
|
| 369 |
+
return jsonify({
|
| 370 |
+
"total_liquidations": total_liquidations,
|
| 371 |
+
"insurance_fund": insurance_fund,
|
| 372 |
+
"at_risk": at_risk_count,
|
| 373 |
+
"recent_liquidations": [],
|
| 374 |
+
})
|
| 375 |
+
except Exception as e:
|
| 376 |
+
return jsonify({"error": str(e)}), 500
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
@app.route('/api/liquidation/at-risk', methods=['GET'])
|
| 380 |
+
def at_risk_positions():
|
| 381 |
+
"""Get at-risk positions"""
|
| 382 |
+
try:
|
| 383 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 384 |
+
cursor = conn.cursor()
|
| 385 |
+
|
| 386 |
+
cursor.execute("""
|
| 387 |
+
SELECT position_id, trader, market, side, size, entry_price, margin, liquidation_price
|
| 388 |
+
FROM positions
|
| 389 |
+
WHERE size > 0
|
| 390 |
+
""")
|
| 391 |
+
|
| 392 |
+
positions = cursor.fetchall()
|
| 393 |
+
conn.close()
|
| 394 |
+
|
| 395 |
+
# Calculate margin ratio for each position using real mark prices
|
| 396 |
+
tickers = _gateio_tickers()
|
| 397 |
+
at_risk = []
|
| 398 |
+
for pos in positions:
|
| 399 |
+
market = pos[2]
|
| 400 |
+
contract = market.replace('/', '_').upper()
|
| 401 |
+
mark_price = float(tickers.get(contract, {}).get('last', 50000))
|
| 402 |
+
notional = pos[4] * mark_price
|
| 403 |
+
margin_ratio = pos[6] / notional if notional > 0 else 1
|
| 404 |
+
|
| 405 |
+
if margin_ratio < 0.10: # Below 10% margin
|
| 406 |
+
at_risk.append({
|
| 407 |
+
"position_id": pos[0],
|
| 408 |
+
"trader": pos[1],
|
| 409 |
+
"market": pos[2],
|
| 410 |
+
"side": pos[3],
|
| 411 |
+
"margin_ratio": margin_ratio,
|
| 412 |
+
"liquidation_price": pos[7],
|
| 413 |
+
})
|
| 414 |
+
|
| 415 |
+
return jsonify(at_risk[:10]) # Return top 10
|
| 416 |
+
except Exception as e:
|
| 417 |
+
return jsonify({"error": str(e)}), 500
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
@app.route('/api/mining/stats', methods=['GET'])
|
| 421 |
+
def mining_stats():
|
| 422 |
+
"""Get mining rewards statistics"""
|
| 423 |
+
try:
|
| 424 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 425 |
+
cursor = conn.cursor()
|
| 426 |
+
|
| 427 |
+
# Active providers
|
| 428 |
+
cursor.execute("SELECT COUNT(*) FROM providers WHERE status = 'active'")
|
| 429 |
+
active_providers = cursor.fetchone()[0]
|
| 430 |
+
|
| 431 |
+
# Total rewards distributed
|
| 432 |
+
cursor.execute("SELECT SUM(amount) FROM rewards")
|
| 433 |
+
total_rewards = cursor.fetchone()[0] or 0
|
| 434 |
+
|
| 435 |
+
# Total reward count
|
| 436 |
+
cursor.execute("SELECT COUNT(*) FROM rewards")
|
| 437 |
+
total_reward_count = cursor.fetchone()[0]
|
| 438 |
+
|
| 439 |
+
conn.close()
|
| 440 |
+
|
| 441 |
+
return jsonify({
|
| 442 |
+
"active_providers": active_providers,
|
| 443 |
+
"total_rewards": total_rewards,
|
| 444 |
+
"total_reward_count": total_reward_count,
|
| 445 |
+
"avg_reward_per_provider": total_rewards / active_providers if active_providers > 0 else 0,
|
| 446 |
+
})
|
| 447 |
+
except Exception as e:
|
| 448 |
+
return jsonify({"error": str(e)}), 500
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
@app.route('/api/mining/leaderboard', methods=['GET'])
|
| 452 |
+
def mining_leaderboard():
|
| 453 |
+
"""Get mining rewards leaderboard"""
|
| 454 |
+
try:
|
| 455 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 456 |
+
cursor = conn.cursor()
|
| 457 |
+
|
| 458 |
+
cursor.execute("""
|
| 459 |
+
SELECT provider_id, wallet_address, model_type, total_earnings, reputation_score
|
| 460 |
+
FROM providers
|
| 461 |
+
WHERE status = 'active'
|
| 462 |
+
ORDER BY total_earnings DESC
|
| 463 |
+
LIMIT 10
|
| 464 |
+
""")
|
| 465 |
+
|
| 466 |
+
providers = cursor.fetchall()
|
| 467 |
+
conn.close()
|
| 468 |
+
|
| 469 |
+
return jsonify([
|
| 470 |
+
{
|
| 471 |
+
"rank": i + 1,
|
| 472 |
+
"provider_id": p[0],
|
| 473 |
+
"wallet_address": p[1],
|
| 474 |
+
"model_type": p[2],
|
| 475 |
+
"total_earnings": p[3],
|
| 476 |
+
"reputation_score": p[4],
|
| 477 |
+
}
|
| 478 |
+
for i, p in enumerate(providers)
|
| 479 |
+
])
|
| 480 |
+
except Exception as e:
|
| 481 |
+
return jsonify({"error": str(e)}), 500
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
def _safe_json(response):
|
| 485 |
+
"""Extract JSON from a Flask Response or (Response, status) tuple."""
|
| 486 |
+
from flask import Response
|
| 487 |
+
if isinstance(response, tuple):
|
| 488 |
+
response = response[0]
|
| 489 |
+
if hasattr(response, 'get_json'):
|
| 490 |
+
return response.get_json() or {}
|
| 491 |
+
return {}
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
@app.route('/api/overview', methods=['GET'])
|
| 495 |
+
def overview():
|
| 496 |
+
"""Get overview statistics from all systems"""
|
| 497 |
+
try:
|
| 498 |
+
# Aggregate data from all endpoints safely
|
| 499 |
+
return jsonify({
|
| 500 |
+
"slippage": _safe_json(slippage_stats()),
|
| 501 |
+
"holders": _safe_json(holder_stats()),
|
| 502 |
+
"liquidity": _safe_json(liquidity_stats()),
|
| 503 |
+
"trading": _safe_json(trading_stats()),
|
| 504 |
+
"funding": _safe_json(funding_stats()),
|
| 505 |
+
"liquidation": _safe_json(liquidation_stats()),
|
| 506 |
+
"mining": _safe_json(mining_stats()),
|
| 507 |
+
})
|
| 508 |
+
except Exception as e:
|
| 509 |
+
return jsonify({"error": str(e)}), 500
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
if __name__ == '__main__':
|
| 513 |
+
# Create databases if they don't exist
|
| 514 |
+
os.makedirs('holder_tracker', exist_ok=True)
|
| 515 |
+
os.makedirs('llm_liquidity_provider', exist_ok=True)
|
| 516 |
+
os.makedirs('perp_trading_engine', exist_ok=True)
|
| 517 |
+
|
| 518 |
+
# Run Flask app
|
| 519 |
+
app.run(host='0.0.0.0', port=7861)
|
app.py
ADDED
|
@@ -0,0 +1,1799 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Hugging Face Space
|
| 4 |
+
Unified Flask app serving API and static UI
|
| 5 |
+
No mocks - real data from all AirMicroDrip systems
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from flask import Flask, jsonify, request, render_template_string
|
| 9 |
+
from flask_cors import CORS
|
| 10 |
+
import sqlite3
|
| 11 |
+
import json
|
| 12 |
+
import hashlib
|
| 13 |
+
import threading
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
import sys
|
| 16 |
+
import os
|
| 17 |
+
import requests
|
| 18 |
+
import logging
|
| 19 |
+
|
| 20 |
+
# Configure logging
|
| 21 |
+
logging.basicConfig(
|
| 22 |
+
level=logging.INFO,
|
| 23 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 24 |
+
)
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
# Import AirMicroDrip modules for real data fetching
|
| 28 |
+
from slippage_collector import SlippageCollector, create_collector
|
| 29 |
+
from holder_tracker import HolderTracker
|
| 30 |
+
from llm_liquidity_provider import InferenceRegistry, LiquidityConverter, PerformanceMonitor
|
| 31 |
+
from merkle_token_launch import build_launch_manifest
|
| 32 |
+
from hf_account_collateral import scan_hf_account_collateral
|
| 33 |
+
from token_launcher import autonomously_create_token, get_launch_status, get_keypair_backup, get_existing_mint
|
| 34 |
+
|
| 35 |
+
app = Flask(__name__)
|
| 36 |
+
CORS(app)
|
| 37 |
+
COLLATERAL_SCAN_JOBS = {}
|
| 38 |
+
|
| 39 |
+
# Database paths
|
| 40 |
+
HOLDER_DB = "holder_tracker/holder_registry.db"
|
| 41 |
+
INFERENCE_DB = "llm_liquidity_provider/inference_registry.db"
|
| 42 |
+
TRADING_DB = "perp_trading_engine/perp_trading.db"
|
| 43 |
+
LAUNCH_DB = "token_launch/launch_registry.db"
|
| 44 |
+
COLLATERAL_DB = "token_launch/collateral_registry.db"
|
| 45 |
+
SPACE_REPO_ID = os.environ.get("SPACE_ID") or os.environ.get("HF_SPACE_ID") or "josephrw/ce"
|
| 46 |
+
SPACE_OWNER = SPACE_REPO_ID.split("/")[0]
|
| 47 |
+
DEFAULT_TOKEN_MINT = "So11111111111111111111111111111111111111112"
|
| 48 |
+
DEFAULT_SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"
|
| 49 |
+
|
| 50 |
+
# Create directories
|
| 51 |
+
os.makedirs('holder_tracker', exist_ok=True)
|
| 52 |
+
os.makedirs('llm_liquidity_provider', exist_ok=True)
|
| 53 |
+
os.makedirs('perp_trading_engine', exist_ok=True)
|
| 54 |
+
os.makedirs('token_launch', exist_ok=True)
|
| 55 |
+
|
| 56 |
+
# Initialize databases
|
| 57 |
+
def init_databases():
|
| 58 |
+
"""Initialize all databases"""
|
| 59 |
+
# Holder tracker DB
|
| 60 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 61 |
+
cursor = conn.cursor()
|
| 62 |
+
cursor.execute("""
|
| 63 |
+
CREATE TABLE IF NOT EXISTS holders (
|
| 64 |
+
address TEXT PRIMARY KEY,
|
| 65 |
+
current_balance REAL DEFAULT 0,
|
| 66 |
+
first_seen TIMESTAMP,
|
| 67 |
+
eligible BOOLEAN DEFAULT FALSE,
|
| 68 |
+
eligibility_timestamp TIMESTAMP
|
| 69 |
+
)
|
| 70 |
+
""")
|
| 71 |
+
cursor.execute("""
|
| 72 |
+
CREATE TABLE IF NOT EXISTS transfers (
|
| 73 |
+
transfer_id TEXT PRIMARY KEY,
|
| 74 |
+
from_address TEXT,
|
| 75 |
+
to_address TEXT,
|
| 76 |
+
amount REAL,
|
| 77 |
+
timestamp TIMESTAMP
|
| 78 |
+
)
|
| 79 |
+
""")
|
| 80 |
+
conn.commit()
|
| 81 |
+
conn.close()
|
| 82 |
+
|
| 83 |
+
# Token launch DB
|
| 84 |
+
conn = sqlite3.connect(LAUNCH_DB)
|
| 85 |
+
cursor = conn.cursor()
|
| 86 |
+
cursor.execute("""
|
| 87 |
+
CREATE TABLE IF NOT EXISTS launch_manifests (
|
| 88 |
+
manifest_hash TEXT PRIMARY KEY,
|
| 89 |
+
merkle_root TEXT NOT NULL,
|
| 90 |
+
status TEXT NOT NULL,
|
| 91 |
+
execution_status TEXT NOT NULL,
|
| 92 |
+
manifest_json TEXT NOT NULL,
|
| 93 |
+
created_at TIMESTAMP NOT NULL
|
| 94 |
+
)
|
| 95 |
+
""")
|
| 96 |
+
conn.commit()
|
| 97 |
+
conn.close()
|
| 98 |
+
|
| 99 |
+
# Account collateral DB
|
| 100 |
+
conn = sqlite3.connect(COLLATERAL_DB)
|
| 101 |
+
cursor = conn.cursor()
|
| 102 |
+
cursor.execute("""
|
| 103 |
+
CREATE TABLE IF NOT EXISTS account_collateral (
|
| 104 |
+
owner TEXT PRIMARY KEY,
|
| 105 |
+
collateral_root TEXT NOT NULL,
|
| 106 |
+
collateral_json TEXT NOT NULL,
|
| 107 |
+
scanned_at TIMESTAMP NOT NULL
|
| 108 |
+
)
|
| 109 |
+
""")
|
| 110 |
+
conn.commit()
|
| 111 |
+
conn.close()
|
| 112 |
+
|
| 113 |
+
# Inference registry DB
|
| 114 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 115 |
+
cursor = conn.cursor()
|
| 116 |
+
cursor.execute("""
|
| 117 |
+
CREATE TABLE IF NOT EXISTS providers (
|
| 118 |
+
provider_id TEXT PRIMARY KEY,
|
| 119 |
+
wallet_address TEXT,
|
| 120 |
+
model_type TEXT,
|
| 121 |
+
registered_at TIMESTAMP,
|
| 122 |
+
status TEXT DEFAULT 'pending',
|
| 123 |
+
reputation_score REAL DEFAULT 0.5,
|
| 124 |
+
total_earnings REAL DEFAULT 0.0
|
| 125 |
+
)
|
| 126 |
+
""")
|
| 127 |
+
cursor.execute("""
|
| 128 |
+
CREATE TABLE IF NOT EXISTS capacity (
|
| 129 |
+
provider_id TEXT,
|
| 130 |
+
tokens_per_second REAL,
|
| 131 |
+
latency_ms REAL,
|
| 132 |
+
uptime_percentage REAL,
|
| 133 |
+
quality_score REAL,
|
| 134 |
+
verified_at TIMESTAMP,
|
| 135 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 136 |
+
)
|
| 137 |
+
""")
|
| 138 |
+
cursor.execute("""
|
| 139 |
+
CREATE TABLE IF NOT EXISTS liquidity_allocations (
|
| 140 |
+
provider_id TEXT,
|
| 141 |
+
synthetic_liquidity_usd REAL,
|
| 142 |
+
liquidity_tokens REAL,
|
| 143 |
+
market_allocation TEXT,
|
| 144 |
+
allocated_at TIMESTAMP,
|
| 145 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 146 |
+
)
|
| 147 |
+
""")
|
| 148 |
+
cursor.execute("""
|
| 149 |
+
CREATE TABLE IF NOT EXISTS rewards (
|
| 150 |
+
reward_id TEXT PRIMARY KEY,
|
| 151 |
+
provider_id TEXT,
|
| 152 |
+
amount REAL,
|
| 153 |
+
source TEXT,
|
| 154 |
+
multiplier REAL,
|
| 155 |
+
timestamp TIMESTAMP,
|
| 156 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 157 |
+
)
|
| 158 |
+
""")
|
| 159 |
+
conn.commit()
|
| 160 |
+
conn.close()
|
| 161 |
+
|
| 162 |
+
# Trading engine DB
|
| 163 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 164 |
+
cursor = conn.cursor()
|
| 165 |
+
cursor.execute("""
|
| 166 |
+
CREATE TABLE IF NOT EXISTS orders (
|
| 167 |
+
order_id TEXT PRIMARY KEY,
|
| 168 |
+
trader TEXT,
|
| 169 |
+
market TEXT,
|
| 170 |
+
side TEXT,
|
| 171 |
+
order_type TEXT,
|
| 172 |
+
size REAL,
|
| 173 |
+
price REAL,
|
| 174 |
+
stop_price REAL,
|
| 175 |
+
leverage INTEGER,
|
| 176 |
+
status TEXT,
|
| 177 |
+
filled_size REAL,
|
| 178 |
+
avg_fill_price REAL,
|
| 179 |
+
created_at TIMESTAMP,
|
| 180 |
+
updated_at TIMESTAMP
|
| 181 |
+
)
|
| 182 |
+
""")
|
| 183 |
+
cursor.execute("""
|
| 184 |
+
CREATE TABLE IF NOT EXISTS positions (
|
| 185 |
+
position_id TEXT PRIMARY KEY,
|
| 186 |
+
trader TEXT,
|
| 187 |
+
market TEXT,
|
| 188 |
+
side TEXT,
|
| 189 |
+
size REAL,
|
| 190 |
+
entry_price REAL,
|
| 191 |
+
leverage INTEGER,
|
| 192 |
+
margin REAL,
|
| 193 |
+
unrealized_pnl REAL,
|
| 194 |
+
realized_pnl REAL,
|
| 195 |
+
liquidation_price REAL,
|
| 196 |
+
opened_at TIMESTAMP,
|
| 197 |
+
updated_at TIMESTAMP
|
| 198 |
+
)
|
| 199 |
+
""")
|
| 200 |
+
cursor.execute("""
|
| 201 |
+
CREATE TABLE IF NOT EXISTS trades (
|
| 202 |
+
trade_id TEXT PRIMARY KEY,
|
| 203 |
+
order_id TEXT,
|
| 204 |
+
market TEXT,
|
| 205 |
+
side TEXT,
|
| 206 |
+
size REAL,
|
| 207 |
+
price REAL,
|
| 208 |
+
fee REAL,
|
| 209 |
+
timestamp TIMESTAMP
|
| 210 |
+
)
|
| 211 |
+
""")
|
| 212 |
+
cursor.execute("""
|
| 213 |
+
CREATE TABLE IF NOT EXISTS funding_rates (
|
| 214 |
+
market TEXT,
|
| 215 |
+
rate REAL,
|
| 216 |
+
timestamp TIMESTAMP,
|
| 217 |
+
PRIMARY KEY (market, timestamp)
|
| 218 |
+
)
|
| 219 |
+
""")
|
| 220 |
+
conn.commit()
|
| 221 |
+
conn.close()
|
| 222 |
+
|
| 223 |
+
init_databases()
|
| 224 |
+
|
| 225 |
+
def _token_mint():
|
| 226 |
+
"""Use a public no-key default unless a token mint is explicitly configured."""
|
| 227 |
+
return os.environ.get("TOKEN_MINT", DEFAULT_TOKEN_MINT).strip()
|
| 228 |
+
|
| 229 |
+
def _inference_url():
|
| 230 |
+
"""Inference endpoints are optional and never require app-level API keys."""
|
| 231 |
+
return os.environ.get("INFERENCE_API_URL", "").strip()
|
| 232 |
+
|
| 233 |
+
def _integration_config():
|
| 234 |
+
"""Return public-safe integration configuration status."""
|
| 235 |
+
token_mint = _token_mint()
|
| 236 |
+
inference_url = _inference_url()
|
| 237 |
+
solana_rpc_url = os.environ.get("SOLANA_RPC_URL", DEFAULT_SOLANA_RPC_URL).strip()
|
| 238 |
+
return {
|
| 239 |
+
"token_mint": {
|
| 240 |
+
"configured": True,
|
| 241 |
+
"defaulted": token_mint == DEFAULT_TOKEN_MINT,
|
| 242 |
+
"label": "Solana token mint",
|
| 243 |
+
"env": "optional override",
|
| 244 |
+
"value_public": token_mint,
|
| 245 |
+
"status": "wired",
|
| 246 |
+
},
|
| 247 |
+
"inference_endpoint": {
|
| 248 |
+
"configured": bool(inference_url),
|
| 249 |
+
"label": "LLM inference endpoint",
|
| 250 |
+
"env": "optional local endpoint",
|
| 251 |
+
"status": "wired" if inference_url else "local_only",
|
| 252 |
+
},
|
| 253 |
+
"solana_rpc": {
|
| 254 |
+
"configured": True,
|
| 255 |
+
"label": "Solana RPC",
|
| 256 |
+
"env": "public default",
|
| 257 |
+
"status": "wired",
|
| 258 |
+
},
|
| 259 |
+
"gateio_market_data": {
|
| 260 |
+
"configured": True,
|
| 261 |
+
"label": "Gate.io public market data",
|
| 262 |
+
"env": None,
|
| 263 |
+
"status": "wired",
|
| 264 |
+
},
|
| 265 |
+
"api_keys": {
|
| 266 |
+
"configured": True,
|
| 267 |
+
"label": "API keys",
|
| 268 |
+
"env": None,
|
| 269 |
+
"status": "not_required",
|
| 270 |
+
},
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
def _status_meta(status, label=None, detail=None):
|
| 274 |
+
"""Consistent UI status payload: real backend, no synthetic success."""
|
| 275 |
+
copy = {
|
| 276 |
+
"active": ("active", "Real backend route responded with usable data."),
|
| 277 |
+
"pending": ("waiting", "Backend is live; the public source has not returned usable data yet."),
|
| 278 |
+
"not_wired": ("not wired", "Optional integration is disabled."),
|
| 279 |
+
"local_only": ("local only", "No API key is required; live external benchmark is optional."),
|
| 280 |
+
"unsigned_ready": ("unsigned ready", "Merkle manifest is verified locally and waiting for a real wallet signature."),
|
| 281 |
+
"ready_for_signature": ("ready for signature", "Collateral evidence is scanned and waiting for owner wallet signature."),
|
| 282 |
+
"not_scanned": ("not scanned", "Collateral scan has not run yet."),
|
| 283 |
+
"error": ("error", "The backend route returned an error."),
|
| 284 |
+
}
|
| 285 |
+
display, default_detail = copy.get(status, (status, "Unknown status."))
|
| 286 |
+
return {
|
| 287 |
+
"status": status,
|
| 288 |
+
"display": display,
|
| 289 |
+
"label": label or display,
|
| 290 |
+
"detail": detail or default_detail,
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
# Attempt to sync real data from external APIs on startup
|
| 294 |
+
def _sync_real_data():
|
| 295 |
+
"""Sync real data from external APIs into local SQLite DBs"""
|
| 296 |
+
token_mint = _token_mint()
|
| 297 |
+
|
| 298 |
+
# 0. Auto-create token mint if none configured and none exists
|
| 299 |
+
if not token_mint:
|
| 300 |
+
try:
|
| 301 |
+
existing = get_existing_mint()
|
| 302 |
+
if existing:
|
| 303 |
+
print(f"[startup] Using existing auto-created mint: {existing}")
|
| 304 |
+
os.environ["TOKEN_MINT"] = existing
|
| 305 |
+
token_mint = existing
|
| 306 |
+
else:
|
| 307 |
+
print("[startup] No TOKEN_MINT set. Auto-creating devnet token...")
|
| 308 |
+
result = autonomously_create_token()
|
| 309 |
+
if result["status"] == "ok":
|
| 310 |
+
mint = result["mint_address"]
|
| 311 |
+
os.environ["TOKEN_MINT"] = mint
|
| 312 |
+
token_mint = mint
|
| 313 |
+
print(f"[startup] Auto-created token: {mint}")
|
| 314 |
+
print(f"[startup] Wallet: {result['wallet_pubkey']}")
|
| 315 |
+
print(f"[startup] BACKUP REQUIRED: Visit /api/token/backup to download keypair")
|
| 316 |
+
else:
|
| 317 |
+
print(f"[startup] Auto-token creation failed: {result.get('message')}")
|
| 318 |
+
except Exception as e:
|
| 319 |
+
print(f"[startup] Token auto-creation error: {e}")
|
| 320 |
+
|
| 321 |
+
# 1. Sync slippage data from DexScreener
|
| 322 |
+
if token_mint:
|
| 323 |
+
try:
|
| 324 |
+
collector = create_collector(token_mint, "drippage_pool_placeholder")
|
| 325 |
+
count = collector.process_real_trades()
|
| 326 |
+
print(f"[startup] Synced {count} slippage collections from DexScreener")
|
| 327 |
+
except Exception as e:
|
| 328 |
+
print(f"[startup] Slippage sync skipped: {e}")
|
| 329 |
+
|
| 330 |
+
# 2. Sync holder data from Solana RPC
|
| 331 |
+
if token_mint:
|
| 332 |
+
try:
|
| 333 |
+
tracker = HolderTracker(token_mint, db_path=HOLDER_DB)
|
| 334 |
+
count = tracker.sync_holders_from_chain()
|
| 335 |
+
print(f"[startup] Synced {count} holders from Solana RPC")
|
| 336 |
+
except Exception as e:
|
| 337 |
+
print(f"[startup] Holder sync skipped: {e}")
|
| 338 |
+
|
| 339 |
+
# 3. Benchmark inference endpoint if configured
|
| 340 |
+
inference_url = _inference_url()
|
| 341 |
+
if inference_url:
|
| 342 |
+
try:
|
| 343 |
+
registry = InferenceRegistry(db_path=INFERENCE_DB)
|
| 344 |
+
converter = LiquidityConverter(registry)
|
| 345 |
+
monitor = PerformanceMonitor(registry)
|
| 346 |
+
bench = monitor._benchmark_inference_endpoint(inference_url, "llama2")
|
| 347 |
+
if bench["status"] == "verified":
|
| 348 |
+
# Register a provider with real benchmark results
|
| 349 |
+
registry.register_provider("prov_hf_001", "hf_worker", "llama2")
|
| 350 |
+
registry.verify_capacity(
|
| 351 |
+
"prov_hf_001",
|
| 352 |
+
bench["tokens_per_second"],
|
| 353 |
+
bench["latency_ms"],
|
| 354 |
+
99.0,
|
| 355 |
+
bench["quality_score"],
|
| 356 |
+
)
|
| 357 |
+
print(f"[startup] Inference benchmark: {bench['tokens_per_second']:.1f} t/s, {bench['latency_ms']:.1f}ms")
|
| 358 |
+
else:
|
| 359 |
+
print(f"[startup] Inference endpoint unreachable at {inference_url}")
|
| 360 |
+
except Exception as e:
|
| 361 |
+
print(f"[startup] Inference benchmark skipped: {e}")
|
| 362 |
+
else:
|
| 363 |
+
print("[startup] No inference endpoint configured; LLM liquidity stays no-key local-only")
|
| 364 |
+
|
| 365 |
+
_sync_real_data()
|
| 366 |
+
|
| 367 |
+
# API Endpoints
|
| 368 |
+
@app.route('/health', methods=['GET'])
|
| 369 |
+
def health():
|
| 370 |
+
"""Health check endpoint"""
|
| 371 |
+
config = _integration_config()
|
| 372 |
+
return jsonify({
|
| 373 |
+
"status": "healthy",
|
| 374 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 375 |
+
"mode": "no_mock_real_backend",
|
| 376 |
+
"configured_integrations": config,
|
| 377 |
+
"systems": {
|
| 378 |
+
"slippage_collector": "active" if config["token_mint"]["configured"] else "not_wired",
|
| 379 |
+
"holder_tracker": "active" if config["token_mint"]["configured"] else "not_wired",
|
| 380 |
+
"llm_liquidity": "active" if config["inference_endpoint"]["configured"] else "local_only",
|
| 381 |
+
"merkle_token_launch": "unsigned_ready",
|
| 382 |
+
"hf_account_collateral": _collateral_summary()["status"],
|
| 383 |
+
"trading_engine": "active",
|
| 384 |
+
"funding_engine": "active",
|
| 385 |
+
"liquidation_system": "active",
|
| 386 |
+
"mining_rewards": "active",
|
| 387 |
+
}
|
| 388 |
+
})
|
| 389 |
+
|
| 390 |
+
@app.route('/api/config', methods=['GET'])
|
| 391 |
+
def config_status():
|
| 392 |
+
"""Public-safe integration status. Does not expose secrets."""
|
| 393 |
+
return jsonify({
|
| 394 |
+
"mode": "no_mock_real_backend",
|
| 395 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 396 |
+
"integrations": _integration_config(),
|
| 397 |
+
"principles": [
|
| 398 |
+
"No fake holders, liquidity, trades, payouts, or inference benchmarks.",
|
| 399 |
+
"Optional external sources return local_only, waiting, or error states.",
|
| 400 |
+
"Dashboard metrics are derived from local DBs or real public APIs.",
|
| 401 |
+
],
|
| 402 |
+
})
|
| 403 |
+
|
| 404 |
+
def _launch_source_metrics():
|
| 405 |
+
"""Use local persisted state as launch-tree inputs without inventing activity."""
|
| 406 |
+
owner_profile = _hf_owner_profile()
|
| 407 |
+
metrics = {
|
| 408 |
+
"token_mint_source": _token_mint(),
|
| 409 |
+
"space_repo_id": SPACE_REPO_ID,
|
| 410 |
+
"space_owner": SPACE_OWNER,
|
| 411 |
+
"owner_repository_count": owner_profile["total_repositories"],
|
| 412 |
+
"owner_repository_hash": owner_profile["repository_index_hash"],
|
| 413 |
+
"generated_from": "airmicrodrip_runtime_ledgers",
|
| 414 |
+
"holders": 0,
|
| 415 |
+
"eligible_holders": 0,
|
| 416 |
+
"active_llm_providers": 0,
|
| 417 |
+
"synthetic_liquidity_usd": 0,
|
| 418 |
+
"active_positions": 0,
|
| 419 |
+
"total_rewards": 0,
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
try:
|
| 423 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 424 |
+
cursor = conn.cursor()
|
| 425 |
+
cursor.execute("SELECT COUNT(*) FROM holders")
|
| 426 |
+
metrics["holders"] = cursor.fetchone()[0]
|
| 427 |
+
cursor.execute("SELECT COUNT(*) FROM holders WHERE eligible = TRUE")
|
| 428 |
+
metrics["eligible_holders"] = cursor.fetchone()[0]
|
| 429 |
+
conn.close()
|
| 430 |
+
except Exception as e:
|
| 431 |
+
metrics["holder_metric_error"] = str(e)
|
| 432 |
+
|
| 433 |
+
try:
|
| 434 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 435 |
+
cursor = conn.cursor()
|
| 436 |
+
cursor.execute("SELECT COUNT(*) FROM providers WHERE status = 'active'")
|
| 437 |
+
metrics["active_llm_providers"] = cursor.fetchone()[0]
|
| 438 |
+
cursor.execute("SELECT SUM(synthetic_liquidity_usd) FROM liquidity_allocations")
|
| 439 |
+
metrics["synthetic_liquidity_usd"] = cursor.fetchone()[0] or 0
|
| 440 |
+
cursor.execute("SELECT SUM(amount) FROM rewards")
|
| 441 |
+
metrics["total_rewards"] = cursor.fetchone()[0] or 0
|
| 442 |
+
conn.close()
|
| 443 |
+
except Exception as e:
|
| 444 |
+
metrics["liquidity_metric_error"] = str(e)
|
| 445 |
+
|
| 446 |
+
try:
|
| 447 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 448 |
+
cursor = conn.cursor()
|
| 449 |
+
cursor.execute("SELECT COUNT(*) FROM positions WHERE size > 0")
|
| 450 |
+
metrics["active_positions"] = cursor.fetchone()[0]
|
| 451 |
+
conn.close()
|
| 452 |
+
except Exception as e:
|
| 453 |
+
metrics["trading_metric_error"] = str(e)
|
| 454 |
+
|
| 455 |
+
return metrics
|
| 456 |
+
|
| 457 |
+
def _owner_token_symbol(owner):
|
| 458 |
+
clean = "".join(ch for ch in owner.upper() if ch.isalnum())
|
| 459 |
+
if not clean:
|
| 460 |
+
clean = "OWNER"
|
| 461 |
+
return f"{clean[:4]}CE"
|
| 462 |
+
|
| 463 |
+
def _owner_token_spec(owner_profile):
|
| 464 |
+
symbol = _owner_token_symbol(owner_profile["owner"])
|
| 465 |
+
return {
|
| 466 |
+
"name": f"{owner_profile['owner']} Compute Exchange",
|
| 467 |
+
"symbol": symbol,
|
| 468 |
+
"decimals": 9,
|
| 469 |
+
"network": "solana-mainnet",
|
| 470 |
+
"total_supply": 1_000_000_000,
|
| 471 |
+
"derived_from_space": SPACE_REPO_ID,
|
| 472 |
+
"derived_from_owner": owner_profile["owner"],
|
| 473 |
+
"derived_repository_count": owner_profile["total_repositories"],
|
| 474 |
+
"repository_index_hash": owner_profile["repository_index_hash"],
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
def _owner_pool_spec(token_spec):
|
| 478 |
+
return {
|
| 479 |
+
"dex": "meteora-or-raydium",
|
| 480 |
+
"pair": f"{token_spec['symbol']}/SOL",
|
| 481 |
+
"base_asset": token_spec["symbol"],
|
| 482 |
+
"quote_asset": "SOL",
|
| 483 |
+
"initial_ce_liquidity": 100_000_000,
|
| 484 |
+
"initial_quote_liquidity_required": "external_wallet_signature_required",
|
| 485 |
+
"lp_lock": "root_committed",
|
| 486 |
+
"derived_from_owner": token_spec["derived_from_owner"],
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
def _hf_api_list(path, owner):
|
| 490 |
+
try:
|
| 491 |
+
response = requests.get(
|
| 492 |
+
f"https://huggingface.co/api/{path}",
|
| 493 |
+
params={"author": owner, "limit": 100},
|
| 494 |
+
timeout=10,
|
| 495 |
+
)
|
| 496 |
+
if response.status_code == 200:
|
| 497 |
+
return response.json()
|
| 498 |
+
except Exception as e:
|
| 499 |
+
logger.warning("HF %s fetch skipped: %s", path, e)
|
| 500 |
+
return []
|
| 501 |
+
|
| 502 |
+
def _hf_owner_profile(owner=None):
|
| 503 |
+
owner = owner or SPACE_OWNER
|
| 504 |
+
repo_records = []
|
| 505 |
+
for repo_type, path in (("model", "models"), ("dataset", "datasets"), ("space", "spaces")):
|
| 506 |
+
for item in _hf_api_list(path, owner):
|
| 507 |
+
repo_id = item.get("id") or item.get("name")
|
| 508 |
+
if not repo_id:
|
| 509 |
+
continue
|
| 510 |
+
repo_records.append({
|
| 511 |
+
"repo_type": repo_type,
|
| 512 |
+
"repo_id": repo_id,
|
| 513 |
+
"likes": item.get("likes", 0),
|
| 514 |
+
"downloads": item.get("downloads", 0),
|
| 515 |
+
"last_modified": item.get("lastModified") or item.get("updatedAt"),
|
| 516 |
+
"sdk": item.get("sdk"),
|
| 517 |
+
})
|
| 518 |
+
|
| 519 |
+
repo_records.sort(key=lambda item: (item["repo_type"], item["repo_id"]))
|
| 520 |
+
repo_index_json = json.dumps(repo_records, sort_keys=True, separators=(",", ":"))
|
| 521 |
+
return {
|
| 522 |
+
"owner": owner,
|
| 523 |
+
"space_repo_id": SPACE_REPO_ID,
|
| 524 |
+
"total_repositories": len(repo_records),
|
| 525 |
+
"repository_index_hash": __import__("hashlib").sha256(repo_index_json.encode("utf-8")).hexdigest(),
|
| 526 |
+
"repositories": repo_records,
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
def _persist_collateral(collateral):
|
| 530 |
+
conn = sqlite3.connect(COLLATERAL_DB)
|
| 531 |
+
cursor = conn.cursor()
|
| 532 |
+
cursor.execute("""
|
| 533 |
+
INSERT OR REPLACE INTO account_collateral
|
| 534 |
+
(owner, collateral_root, collateral_json, scanned_at)
|
| 535 |
+
VALUES (?, ?, ?, ?)
|
| 536 |
+
""", (
|
| 537 |
+
collateral["owner"],
|
| 538 |
+
collateral["collateral_root"],
|
| 539 |
+
json.dumps(collateral, sort_keys=True),
|
| 540 |
+
collateral["scanned_at"],
|
| 541 |
+
))
|
| 542 |
+
conn.commit()
|
| 543 |
+
conn.close()
|
| 544 |
+
|
| 545 |
+
def _latest_collateral(owner=None):
|
| 546 |
+
owner = owner or SPACE_OWNER
|
| 547 |
+
conn = sqlite3.connect(COLLATERAL_DB)
|
| 548 |
+
cursor = conn.cursor()
|
| 549 |
+
cursor.execute("""
|
| 550 |
+
SELECT collateral_json
|
| 551 |
+
FROM account_collateral
|
| 552 |
+
WHERE owner = ?
|
| 553 |
+
""", (owner,))
|
| 554 |
+
row = cursor.fetchone()
|
| 555 |
+
conn.close()
|
| 556 |
+
return json.loads(row[0]) if row else None
|
| 557 |
+
|
| 558 |
+
def _collateral_summary(owner=None):
|
| 559 |
+
collateral = _latest_collateral(owner)
|
| 560 |
+
job = COLLATERAL_SCAN_JOBS.get(owner or SPACE_OWNER)
|
| 561 |
+
if collateral:
|
| 562 |
+
summary = {
|
| 563 |
+
"status": collateral["collateral_status"],
|
| 564 |
+
"owner": collateral["owner"],
|
| 565 |
+
"collateral_root": collateral["collateral_root"],
|
| 566 |
+
"repo_count": collateral["repo_count"],
|
| 567 |
+
"space_count": collateral["space_count"],
|
| 568 |
+
"total_files": collateral["total_files"],
|
| 569 |
+
"total_text_files": collateral["total_text_files"],
|
| 570 |
+
"total_loc": collateral["total_loc"],
|
| 571 |
+
"collateral_score": collateral["collateral_score"],
|
| 572 |
+
"scanned_at": collateral["scanned_at"],
|
| 573 |
+
}
|
| 574 |
+
if job and job.get("status") == "running":
|
| 575 |
+
summary["scan_job_status"] = "running"
|
| 576 |
+
return summary
|
| 577 |
+
if job:
|
| 578 |
+
return {
|
| 579 |
+
"status": job.get("status", "running"),
|
| 580 |
+
"owner": owner or SPACE_OWNER,
|
| 581 |
+
"collateral_root": None,
|
| 582 |
+
"repo_count": 0,
|
| 583 |
+
"space_count": 0,
|
| 584 |
+
"total_files": 0,
|
| 585 |
+
"total_text_files": 0,
|
| 586 |
+
"total_loc": 0,
|
| 587 |
+
"collateral_score": 0,
|
| 588 |
+
"started_at": job.get("started_at"),
|
| 589 |
+
"message": job.get("message", "Collateral scan is running."),
|
| 590 |
+
}
|
| 591 |
+
return {
|
| 592 |
+
"status": "not_scanned",
|
| 593 |
+
"owner": owner or SPACE_OWNER,
|
| 594 |
+
"collateral_root": None,
|
| 595 |
+
"repo_count": 0,
|
| 596 |
+
"space_count": 0,
|
| 597 |
+
"total_files": 0,
|
| 598 |
+
"total_text_files": 0,
|
| 599 |
+
"total_loc": 0,
|
| 600 |
+
"collateral_score": 0,
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
def _run_collateral_scan_job(owner):
|
| 604 |
+
COLLATERAL_SCAN_JOBS[owner] = {
|
| 605 |
+
"status": "running",
|
| 606 |
+
"owner": owner,
|
| 607 |
+
"started_at": datetime.utcnow().isoformat(),
|
| 608 |
+
"message": "Scanning public HF repos, spaces, files, and readable LOC.",
|
| 609 |
+
}
|
| 610 |
+
try:
|
| 611 |
+
collateral = scan_hf_account_collateral(owner)
|
| 612 |
+
_persist_collateral(collateral)
|
| 613 |
+
manifest = _prepare_launch_manifest({"owner": owner})
|
| 614 |
+
COLLATERAL_SCAN_JOBS[owner] = {
|
| 615 |
+
"status": "complete",
|
| 616 |
+
"owner": owner,
|
| 617 |
+
"started_at": COLLATERAL_SCAN_JOBS[owner]["started_at"],
|
| 618 |
+
"completed_at": datetime.utcnow().isoformat(),
|
| 619 |
+
"message": "Collateral scan completed and launch manifest rebuilt.",
|
| 620 |
+
"collateral_root": collateral["collateral_root"],
|
| 621 |
+
"manifest_hash": manifest["manifest_hash"],
|
| 622 |
+
"merkle_root": manifest["merkle_root"],
|
| 623 |
+
}
|
| 624 |
+
except Exception as e:
|
| 625 |
+
COLLATERAL_SCAN_JOBS[owner] = {
|
| 626 |
+
"status": "error",
|
| 627 |
+
"owner": owner,
|
| 628 |
+
"started_at": COLLATERAL_SCAN_JOBS.get(owner, {}).get("started_at"),
|
| 629 |
+
"completed_at": datetime.utcnow().isoformat(),
|
| 630 |
+
"message": str(e),
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
def _owner_repo_leaves(owner_profile):
|
| 634 |
+
return [
|
| 635 |
+
{
|
| 636 |
+
"kind": f"owner_repo:{repo['repo_type']}:{repo['repo_id']}",
|
| 637 |
+
"payload": repo,
|
| 638 |
+
}
|
| 639 |
+
for repo in owner_profile["repositories"]
|
| 640 |
+
]
|
| 641 |
+
|
| 642 |
+
def _collateral_leaves(collateral):
|
| 643 |
+
if not collateral or collateral.get("status") == "not_scanned":
|
| 644 |
+
return []
|
| 645 |
+
leaves = [{
|
| 646 |
+
"kind": f"hf_account_collateral:{collateral['owner']}",
|
| 647 |
+
"payload": {
|
| 648 |
+
"owner": collateral["owner"],
|
| 649 |
+
"collateral_root": collateral["collateral_root"],
|
| 650 |
+
"repo_count": collateral["repo_count"],
|
| 651 |
+
"space_count": collateral["space_count"],
|
| 652 |
+
"total_files": collateral["total_files"],
|
| 653 |
+
"total_text_files": collateral["total_text_files"],
|
| 654 |
+
"total_loc": collateral["total_loc"],
|
| 655 |
+
"collateral_score": collateral["collateral_score"],
|
| 656 |
+
},
|
| 657 |
+
}]
|
| 658 |
+
for repo in collateral["repositories"]:
|
| 659 |
+
leaves.append({
|
| 660 |
+
"kind": f"hf_repo_collateral:{repo['repo_type']}:{repo['repo_id']}",
|
| 661 |
+
"payload": {
|
| 662 |
+
"repo_type": repo["repo_type"],
|
| 663 |
+
"repo_id": repo["repo_id"],
|
| 664 |
+
"file_count": repo.get("file_count", 0),
|
| 665 |
+
"text_file_count": repo.get("text_file_count", 0),
|
| 666 |
+
"loc": repo.get("loc", 0),
|
| 667 |
+
"repo_evidence_hash": repo.get("repo_evidence_hash"),
|
| 668 |
+
},
|
| 669 |
+
})
|
| 670 |
+
for file_record in repo.get("files", []):
|
| 671 |
+
leaves.append({
|
| 672 |
+
"kind": f"hf_file:{repo['repo_type']}:{repo['repo_id']}:{file_record['path']}",
|
| 673 |
+
"payload": {
|
| 674 |
+
"repo_id": repo["repo_id"],
|
| 675 |
+
"repo_type": repo["repo_type"],
|
| 676 |
+
"path": file_record["path"],
|
| 677 |
+
"text": file_record["text"],
|
| 678 |
+
"loc": file_record["loc"],
|
| 679 |
+
"sha256": file_record["sha256"],
|
| 680 |
+
"read_status": file_record["read_status"],
|
| 681 |
+
},
|
| 682 |
+
})
|
| 683 |
+
return leaves
|
| 684 |
+
|
| 685 |
+
def _persist_launch_manifest(manifest):
|
| 686 |
+
conn = sqlite3.connect(LAUNCH_DB)
|
| 687 |
+
cursor = conn.cursor()
|
| 688 |
+
cursor.execute("""
|
| 689 |
+
INSERT OR REPLACE INTO launch_manifests
|
| 690 |
+
(manifest_hash, merkle_root, status, execution_status, manifest_json, created_at)
|
| 691 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 692 |
+
""", (
|
| 693 |
+
manifest["manifest_hash"],
|
| 694 |
+
manifest["merkle_root"],
|
| 695 |
+
manifest["status"],
|
| 696 |
+
manifest["execution_status"],
|
| 697 |
+
json.dumps(manifest, sort_keys=True),
|
| 698 |
+
manifest["created_at"],
|
| 699 |
+
))
|
| 700 |
+
conn.commit()
|
| 701 |
+
conn.close()
|
| 702 |
+
|
| 703 |
+
def _latest_launch_manifest():
|
| 704 |
+
conn = sqlite3.connect(LAUNCH_DB)
|
| 705 |
+
cursor = conn.cursor()
|
| 706 |
+
cursor.execute("""
|
| 707 |
+
SELECT manifest_json
|
| 708 |
+
FROM launch_manifests
|
| 709 |
+
ORDER BY created_at DESC
|
| 710 |
+
LIMIT 1
|
| 711 |
+
""")
|
| 712 |
+
row = cursor.fetchone()
|
| 713 |
+
conn.close()
|
| 714 |
+
return json.loads(row[0]) if row else None
|
| 715 |
+
|
| 716 |
+
def _prepare_launch_manifest(payload=None):
|
| 717 |
+
payload = payload or {}
|
| 718 |
+
owner_profile = _hf_owner_profile(payload.get("owner") or SPACE_OWNER)
|
| 719 |
+
collateral = _latest_collateral(owner_profile["owner"])
|
| 720 |
+
token_spec = payload.get("token_spec") or _owner_token_spec(owner_profile)
|
| 721 |
+
pool_spec = payload.get("pool_spec") or _owner_pool_spec(token_spec)
|
| 722 |
+
manifest = build_launch_manifest(
|
| 723 |
+
token_spec=token_spec,
|
| 724 |
+
pool_spec=pool_spec,
|
| 725 |
+
allocations=payload.get("allocations"),
|
| 726 |
+
source_metrics=_launch_source_metrics(),
|
| 727 |
+
extra_leaves=_owner_repo_leaves(owner_profile) + _collateral_leaves(collateral),
|
| 728 |
+
)
|
| 729 |
+
manifest["owner_profile"] = owner_profile
|
| 730 |
+
manifest["account_collateral"] = _collateral_summary(owner_profile["owner"])
|
| 731 |
+
_persist_launch_manifest(manifest)
|
| 732 |
+
return manifest
|
| 733 |
+
|
| 734 |
+
@app.route('/api/token-launch/status', methods=['GET'])
|
| 735 |
+
def token_launch_status():
|
| 736 |
+
"""Return the latest Merkle token-launch manifest, creating one if needed."""
|
| 737 |
+
try:
|
| 738 |
+
manifest = _latest_launch_manifest() or _prepare_launch_manifest()
|
| 739 |
+
return jsonify(manifest)
|
| 740 |
+
except Exception as e:
|
| 741 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 742 |
+
|
| 743 |
+
@app.route('/api/token-launch/prepare', methods=['POST'])
|
| 744 |
+
def token_launch_prepare():
|
| 745 |
+
"""Prepare a new unsigned Merkle launch manifest from one canonical tree."""
|
| 746 |
+
try:
|
| 747 |
+
payload = request.get_json(silent=True) or {}
|
| 748 |
+
manifest = _prepare_launch_manifest(payload)
|
| 749 |
+
return jsonify(manifest), 201
|
| 750 |
+
except Exception as e:
|
| 751 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 752 |
+
|
| 753 |
+
@app.route('/api/token-launch/pool-setup', methods=['GET'])
|
| 754 |
+
def token_launch_pool_setup():
|
| 755 |
+
"""Return the unsigned token mint and liquidity-pool setup plan."""
|
| 756 |
+
try:
|
| 757 |
+
manifest = _latest_launch_manifest() or _prepare_launch_manifest()
|
| 758 |
+
return jsonify({
|
| 759 |
+
"status": manifest["pool_setup_status"]["status"],
|
| 760 |
+
"execution_status": manifest["execution_status"],
|
| 761 |
+
"pool_setup_status": manifest["pool_setup_status"],
|
| 762 |
+
"merkle_root": manifest["merkle_root"],
|
| 763 |
+
"manifest_hash": manifest["manifest_hash"],
|
| 764 |
+
"token_spec": manifest["token_spec"],
|
| 765 |
+
"pool_spec": manifest["pool_spec"],
|
| 766 |
+
"unsigned_solana_plan": manifest["unsigned_solana_plan"],
|
| 767 |
+
"requires_wallet_signature": True,
|
| 768 |
+
"requires_quote_asset_funding": True,
|
| 769 |
+
})
|
| 770 |
+
except Exception as e:
|
| 771 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 772 |
+
|
| 773 |
+
# ── Autonomous Token Creation (real on-chain mint) ──
|
| 774 |
+
@app.route('/api/token/create', methods=['POST'])
|
| 775 |
+
def token_create():
|
| 776 |
+
"""Autonomously create a new Solana devnet token mint."""
|
| 777 |
+
try:
|
| 778 |
+
payload = request.get_json(silent=True) or {}
|
| 779 |
+
# Check if one already exists
|
| 780 |
+
existing = get_existing_mint()
|
| 781 |
+
if existing:
|
| 782 |
+
return jsonify({
|
| 783 |
+
"status": "already_exists",
|
| 784 |
+
"message": "A token mint already exists. Use /api/token/status to view it.",
|
| 785 |
+
"mint_address": existing,
|
| 786 |
+
})
|
| 787 |
+
result = autonomously_create_token(
|
| 788 |
+
token_name=payload.get("token_name", "AirMicroDrip"),
|
| 789 |
+
token_symbol=payload.get("token_symbol", "DRIP"),
|
| 790 |
+
decimals=payload.get("decimals", 9),
|
| 791 |
+
existing_secret_b64=payload.get("existing_secret_b64"),
|
| 792 |
+
)
|
| 793 |
+
if result["status"] == "ok":
|
| 794 |
+
# Set env for immediate use
|
| 795 |
+
os.environ["TOKEN_MINT"] = result["mint_address"]
|
| 796 |
+
return jsonify(result)
|
| 797 |
+
if result["status"] in ("wallet_created_needs_funding", "wallet_ready"):
|
| 798 |
+
# Return 200 with actionable info so caller can fund and retry
|
| 799 |
+
return jsonify(result), 200
|
| 800 |
+
return jsonify(result), 400
|
| 801 |
+
except Exception as e:
|
| 802 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 803 |
+
|
| 804 |
+
@app.route('/api/token/status', methods=['GET'])
|
| 805 |
+
def token_status():
|
| 806 |
+
"""Get the autonomous token launch status."""
|
| 807 |
+
try:
|
| 808 |
+
status = get_launch_status()
|
| 809 |
+
if not status:
|
| 810 |
+
return jsonify({
|
| 811 |
+
"status": "not_created",
|
| 812 |
+
"message": "No token mint found. POST to /api/token/create to create one.",
|
| 813 |
+
})
|
| 814 |
+
return jsonify({"status": "ok", "launch": status})
|
| 815 |
+
except Exception as e:
|
| 816 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 817 |
+
|
| 818 |
+
@app.route('/api/token/backup', methods=['GET'])
|
| 819 |
+
def token_backup():
|
| 820 |
+
"""Download the wallet keypair backup (one-time sensitive operation)."""
|
| 821 |
+
try:
|
| 822 |
+
backup = get_keypair_backup()
|
| 823 |
+
if not backup:
|
| 824 |
+
return jsonify({
|
| 825 |
+
"status": "not_found",
|
| 826 |
+
"message": "No token launch found. Create one first at /api/token/create",
|
| 827 |
+
}), 404
|
| 828 |
+
|
| 829 |
+
keypair_json = {
|
| 830 |
+
"pubkey": backup["pubkey"],
|
| 831 |
+
"secret": backup["secret"],
|
| 832 |
+
"mint_address": backup["mint_address"],
|
| 833 |
+
"token_symbol": backup["token_symbol"],
|
| 834 |
+
"warning": "This is your wallet private key. Store it securely. If lost, this wallet cannot be recovered.",
|
| 835 |
+
}
|
| 836 |
+
return jsonify(keypair_json)
|
| 837 |
+
except Exception as e:
|
| 838 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 839 |
+
|
| 840 |
+
@app.route('/api/collateral/status', methods=['GET'])
|
| 841 |
+
def collateral_status():
|
| 842 |
+
"""Return latest HF account collateral scan summary."""
|
| 843 |
+
try:
|
| 844 |
+
owner = request.args.get("owner", SPACE_OWNER)
|
| 845 |
+
collateral = _latest_collateral(owner)
|
| 846 |
+
if not collateral:
|
| 847 |
+
return jsonify(_collateral_summary(owner))
|
| 848 |
+
return jsonify(collateral)
|
| 849 |
+
except Exception as e:
|
| 850 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 851 |
+
|
| 852 |
+
@app.route('/api/collateral/scan', methods=['POST'])
|
| 853 |
+
def collateral_scan():
|
| 854 |
+
"""Scan owner public HF repos/spaces/files and rebuild launch manifest."""
|
| 855 |
+
try:
|
| 856 |
+
payload = request.get_json(silent=True) or {}
|
| 857 |
+
owner = payload.get("owner") or SPACE_OWNER
|
| 858 |
+
existing = COLLATERAL_SCAN_JOBS.get(owner)
|
| 859 |
+
if existing and existing.get("status") == "running":
|
| 860 |
+
return jsonify({
|
| 861 |
+
"status": "scan_already_running",
|
| 862 |
+
"owner": owner,
|
| 863 |
+
"job": existing,
|
| 864 |
+
"collateral": _collateral_summary(owner),
|
| 865 |
+
}), 202
|
| 866 |
+
|
| 867 |
+
thread = threading.Thread(target=_run_collateral_scan_job, args=(owner,), daemon=True)
|
| 868 |
+
thread.start()
|
| 869 |
+
return jsonify({
|
| 870 |
+
"status": "scan_started",
|
| 871 |
+
"owner": owner,
|
| 872 |
+
"collateral": _collateral_summary(owner),
|
| 873 |
+
"requires_wallet_signature": True,
|
| 874 |
+
"message": "Scanning all public HF repos, spaces, files, and readable LOC in the background.",
|
| 875 |
+
}), 202
|
| 876 |
+
except Exception as e:
|
| 877 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 878 |
+
|
| 879 |
+
def _gateio_tickers():
|
| 880 |
+
"""Fetch real Gate.io futures tickers"""
|
| 881 |
+
try:
|
| 882 |
+
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/tickers', timeout=10)
|
| 883 |
+
if r.status_code == 200:
|
| 884 |
+
return {t['contract']: t for t in r.json()}
|
| 885 |
+
except Exception as e:
|
| 886 |
+
logger.warning("Gate.io tickers fetch failed: %s", e)
|
| 887 |
+
return {}
|
| 888 |
+
|
| 889 |
+
def _gateio_funding():
|
| 890 |
+
"""Fetch real Gate.io funding rates"""
|
| 891 |
+
try:
|
| 892 |
+
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/funding_rate', timeout=10)
|
| 893 |
+
if r.status_code == 200:
|
| 894 |
+
return {f['contract']: f for f in r.json()}
|
| 895 |
+
except Exception as e:
|
| 896 |
+
logger.warning("Gate.io funding fetch failed: %s", e)
|
| 897 |
+
return {}
|
| 898 |
+
|
| 899 |
+
@app.route('/api/slippage/stats', methods=['GET'])
|
| 900 |
+
def slippage_stats():
|
| 901 |
+
"""Get slippage collection statistics from real DexScreener API"""
|
| 902 |
+
try:
|
| 903 |
+
token_mint = _token_mint()
|
| 904 |
+
|
| 905 |
+
collector = create_collector(token_mint, "drippage_pool")
|
| 906 |
+
collector.process_real_trades()
|
| 907 |
+
stats = collector.get_collection_stats()
|
| 908 |
+
|
| 909 |
+
# Also fetch fresh whale stats
|
| 910 |
+
from slippage_collector import _fetch_dexscreener_pairs, WhaleDetector
|
| 911 |
+
pairs = _fetch_dexscreener_pairs(token_mint)
|
| 912 |
+
detector = WhaleDetector()
|
| 913 |
+
detector.detect_from_pairs(pairs)
|
| 914 |
+
whale_stats = detector.get_whale_stats()
|
| 915 |
+
|
| 916 |
+
return jsonify({
|
| 917 |
+
"status": stats.get("status", "pending"),
|
| 918 |
+
"last_fetch": stats.get("last_fetch"),
|
| 919 |
+
"total_collected_usd": stats.get("total_collected_usd", 0),
|
| 920 |
+
"total_collections": stats.get("total_collections", 0),
|
| 921 |
+
"avg_slippage_bps": stats.get("avg_slippage_bps", 0),
|
| 922 |
+
"recent_collections": stats.get("recent_collections", []),
|
| 923 |
+
"whale_trades_today": whale_stats.get("total_whale_trades", 0),
|
| 924 |
+
"total_whale_volume_24h": whale_stats.get("total_volume_24h", 0),
|
| 925 |
+
})
|
| 926 |
+
except Exception as e:
|
| 927 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 928 |
+
|
| 929 |
+
@app.route('/api/holders/stats', methods=['GET'])
|
| 930 |
+
def holder_stats():
|
| 931 |
+
"""Get holder statistics from real Solana RPC"""
|
| 932 |
+
try:
|
| 933 |
+
token_mint = _token_mint()
|
| 934 |
+
|
| 935 |
+
# Attempt to sync fresh holder data from chain
|
| 936 |
+
if token_mint:
|
| 937 |
+
try:
|
| 938 |
+
tracker = HolderTracker(token_mint, db_path=HOLDER_DB)
|
| 939 |
+
tracker.sync_holders_from_chain()
|
| 940 |
+
except Exception as sync_err:
|
| 941 |
+
logger.warning("Holder sync warning: %s", sync_err)
|
| 942 |
+
|
| 943 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 944 |
+
cursor = conn.cursor()
|
| 945 |
+
|
| 946 |
+
cursor.execute("SELECT COUNT(*) FROM holders")
|
| 947 |
+
total_holders = cursor.fetchone()[0]
|
| 948 |
+
|
| 949 |
+
cursor.execute("SELECT COUNT(*) FROM holders WHERE eligible = TRUE")
|
| 950 |
+
eligible_holders = cursor.fetchone()[0]
|
| 951 |
+
|
| 952 |
+
today = datetime.utcnow().date()
|
| 953 |
+
cursor.execute("SELECT COUNT(*) FROM holders WHERE DATE(first_seen) = ?", (today.isoformat(),))
|
| 954 |
+
new_holders_today = cursor.fetchone()[0]
|
| 955 |
+
|
| 956 |
+
cursor.execute("SELECT SUM(current_balance) FROM holders")
|
| 957 |
+
total_balance = cursor.fetchone()[0] or 0
|
| 958 |
+
|
| 959 |
+
conn.close()
|
| 960 |
+
|
| 961 |
+
return jsonify({
|
| 962 |
+
"status": "active" if token_mint else "pending",
|
| 963 |
+
"token_mint": token_mint or None,
|
| 964 |
+
"default_token": token_mint == DEFAULT_TOKEN_MINT,
|
| 965 |
+
"total_holders": total_holders,
|
| 966 |
+
"eligible_holders": eligible_holders,
|
| 967 |
+
"new_holders_today": new_holders_today,
|
| 968 |
+
"total_balance": total_balance,
|
| 969 |
+
"eligibility_rate": eligible_holders / total_holders if total_holders > 0 else 0,
|
| 970 |
+
})
|
| 971 |
+
except Exception as e:
|
| 972 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 973 |
+
|
| 974 |
+
@app.route('/api/holders/eligible', methods=['GET'])
|
| 975 |
+
def eligible_holders():
|
| 976 |
+
"""Get eligible holders for drippage"""
|
| 977 |
+
try:
|
| 978 |
+
conn = sqlite3.connect(HOLDER_DB)
|
| 979 |
+
cursor = conn.cursor()
|
| 980 |
+
|
| 981 |
+
cursor.execute("""
|
| 982 |
+
SELECT address, current_balance, first_seen, eligibility_timestamp
|
| 983 |
+
FROM holders
|
| 984 |
+
WHERE eligible = TRUE
|
| 985 |
+
ORDER BY current_balance DESC
|
| 986 |
+
LIMIT 100
|
| 987 |
+
""")
|
| 988 |
+
|
| 989 |
+
holders = cursor.fetchall()
|
| 990 |
+
conn.close()
|
| 991 |
+
|
| 992 |
+
return jsonify([
|
| 993 |
+
{
|
| 994 |
+
"address": h[0],
|
| 995 |
+
"balance": h[1],
|
| 996 |
+
"first_seen": h[2],
|
| 997 |
+
"holding_hours": (datetime.utcnow() - datetime.fromisoformat(h[2])).total_seconds() / 3600 if h[2] else 0,
|
| 998 |
+
}
|
| 999 |
+
for h in holders
|
| 1000 |
+
])
|
| 1001 |
+
except Exception as e:
|
| 1002 |
+
return jsonify({"error": str(e)}), 500
|
| 1003 |
+
|
| 1004 |
+
@app.route('/api/liquidity/stats', methods=['GET'])
|
| 1005 |
+
def liquidity_stats():
|
| 1006 |
+
"""Get LLM liquidity statistics with real inference benchmark"""
|
| 1007 |
+
try:
|
| 1008 |
+
inference_url = _inference_url()
|
| 1009 |
+
|
| 1010 |
+
# Attempt real benchmark if endpoint configured
|
| 1011 |
+
if inference_url:
|
| 1012 |
+
try:
|
| 1013 |
+
registry = InferenceRegistry(db_path=INFERENCE_DB)
|
| 1014 |
+
converter = LiquidityConverter(registry)
|
| 1015 |
+
monitor = PerformanceMonitor(registry)
|
| 1016 |
+
bench = monitor._benchmark_inference_endpoint(inference_url, "llama2")
|
| 1017 |
+
if bench["status"] == "verified":
|
| 1018 |
+
registry.register_provider("prov_api_001", "api_worker", "llama2")
|
| 1019 |
+
registry.verify_capacity(
|
| 1020 |
+
"prov_api_001",
|
| 1021 |
+
bench["tokens_per_second"],
|
| 1022 |
+
bench["latency_ms"],
|
| 1023 |
+
99.0,
|
| 1024 |
+
bench["quality_score"],
|
| 1025 |
+
)
|
| 1026 |
+
metrics = registry.get_provider_capacity("prov_api_001")
|
| 1027 |
+
if metrics:
|
| 1028 |
+
liquidity = converter.calculate_liquidity(metrics)
|
| 1029 |
+
converter.allocate_liquidity("prov_api_001", liquidity)
|
| 1030 |
+
except Exception as bench_err:
|
| 1031 |
+
print(f"Liquidity benchmark warning: {bench_err}")
|
| 1032 |
+
|
| 1033 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 1034 |
+
cursor = conn.cursor()
|
| 1035 |
+
|
| 1036 |
+
cursor.execute("SELECT COUNT(*) FROM providers WHERE status = 'active'")
|
| 1037 |
+
total_providers = cursor.fetchone()[0]
|
| 1038 |
+
|
| 1039 |
+
cursor.execute("SELECT SUM(total_earnings) FROM providers")
|
| 1040 |
+
total_earnings = cursor.fetchone()[0] or 0
|
| 1041 |
+
|
| 1042 |
+
cursor.execute("""
|
| 1043 |
+
SELECT provider_id, synthetic_liquidity_usd, allocated_at
|
| 1044 |
+
FROM liquidity_allocations
|
| 1045 |
+
ORDER BY allocated_at DESC
|
| 1046 |
+
LIMIT 10
|
| 1047 |
+
""")
|
| 1048 |
+
|
| 1049 |
+
allocations = cursor.fetchall()
|
| 1050 |
+
conn.close()
|
| 1051 |
+
|
| 1052 |
+
total_liquidity = sum(a[1] for a in allocations) if allocations else 0
|
| 1053 |
+
|
| 1054 |
+
return jsonify({
|
| 1055 |
+
"status": "active" if total_providers > 0 else "local_only",
|
| 1056 |
+
"message": None if total_providers > 0 else "No API key required. Connect a no-key local Ollama/OpenAI-compatible endpoint to benchmark live LLM liquidity.",
|
| 1057 |
+
"inference_endpoint": inference_url or None,
|
| 1058 |
+
"total_providers": total_providers,
|
| 1059 |
+
"total_liquidity_usd": round(total_liquidity, 2),
|
| 1060 |
+
"total_earnings": round(total_earnings, 2),
|
| 1061 |
+
"avg_capacity": round(total_liquidity / total_providers, 2) if total_providers > 0 else 0,
|
| 1062 |
+
"recent_allocations": [
|
| 1063 |
+
{"provider_id": a[0], "liquidity_usd": a[1], "allocated_at": a[2]}
|
| 1064 |
+
for a in allocations
|
| 1065 |
+
],
|
| 1066 |
+
})
|
| 1067 |
+
except Exception as e:
|
| 1068 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 1069 |
+
|
| 1070 |
+
@app.route('/api/liquidity/providers', methods=['GET'])
|
| 1071 |
+
def liquidity_providers():
|
| 1072 |
+
"""Get all LLM liquidity providers"""
|
| 1073 |
+
try:
|
| 1074 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 1075 |
+
cursor = conn.cursor()
|
| 1076 |
+
|
| 1077 |
+
cursor.execute("""
|
| 1078 |
+
SELECT provider_id, wallet_address, model_type, status, reputation_score, total_earnings
|
| 1079 |
+
FROM providers
|
| 1080 |
+
WHERE status = 'active'
|
| 1081 |
+
ORDER BY total_earnings DESC
|
| 1082 |
+
""")
|
| 1083 |
+
|
| 1084 |
+
providers = cursor.fetchall()
|
| 1085 |
+
conn.close()
|
| 1086 |
+
|
| 1087 |
+
return jsonify([
|
| 1088 |
+
{
|
| 1089 |
+
"provider_id": p[0],
|
| 1090 |
+
"wallet_address": p[1],
|
| 1091 |
+
"model_type": p[2],
|
| 1092 |
+
"status": p[3],
|
| 1093 |
+
"reputation_score": p[4],
|
| 1094 |
+
"total_earnings": p[5],
|
| 1095 |
+
}
|
| 1096 |
+
for p in providers
|
| 1097 |
+
])
|
| 1098 |
+
except Exception as e:
|
| 1099 |
+
return jsonify({"error": str(e)}), 500
|
| 1100 |
+
|
| 1101 |
+
@app.route('/api/trading/stats', methods=['GET'])
|
| 1102 |
+
def trading_stats():
|
| 1103 |
+
"""Get trading statistics"""
|
| 1104 |
+
try:
|
| 1105 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 1106 |
+
cursor = conn.cursor()
|
| 1107 |
+
|
| 1108 |
+
cursor.execute("SELECT COUNT(*) FROM positions WHERE size > 0")
|
| 1109 |
+
active_positions = cursor.fetchone()[0]
|
| 1110 |
+
|
| 1111 |
+
cursor.execute("SELECT COUNT(*) FROM trades")
|
| 1112 |
+
total_trades = cursor.fetchone()[0]
|
| 1113 |
+
|
| 1114 |
+
cursor.execute("""
|
| 1115 |
+
SELECT SUM(size * price)
|
| 1116 |
+
FROM trades
|
| 1117 |
+
WHERE timestamp > datetime('now', '-1 day')
|
| 1118 |
+
""")
|
| 1119 |
+
volume_24h = cursor.fetchone()[0] or 0
|
| 1120 |
+
|
| 1121 |
+
cursor.execute("SELECT SUM(size) FROM positions WHERE size > 0")
|
| 1122 |
+
total_size = cursor.fetchone()[0] or 0
|
| 1123 |
+
|
| 1124 |
+
conn.close()
|
| 1125 |
+
|
| 1126 |
+
# Fetch real BTC price from Gate.io for OI calculation
|
| 1127 |
+
tickers = _gateio_tickers()
|
| 1128 |
+
btc_price = float(tickers.get('BTC_USDT', {}).get('last', 50000))
|
| 1129 |
+
return jsonify({
|
| 1130 |
+
"total_volume": volume_24h,
|
| 1131 |
+
"open_interest": total_size * btc_price,
|
| 1132 |
+
"active_positions": active_positions,
|
| 1133 |
+
"total_trades": total_trades,
|
| 1134 |
+
})
|
| 1135 |
+
except Exception as e:
|
| 1136 |
+
return jsonify({"error": str(e)}), 500
|
| 1137 |
+
|
| 1138 |
+
@app.route('/api/trading/markets', methods=['GET'])
|
| 1139 |
+
def trading_markets():
|
| 1140 |
+
"""Get market overview from Gate.io real data"""
|
| 1141 |
+
tickers = _gateio_tickers()
|
| 1142 |
+
funding = _gateio_funding()
|
| 1143 |
+
markets = []
|
| 1144 |
+
for contract, t in tickers.items():
|
| 1145 |
+
markets.append({
|
| 1146 |
+
"market": contract.replace('_', '/'),
|
| 1147 |
+
"mark_price": float(t.get('last', 0)),
|
| 1148 |
+
"index_price": float(t.get('index_price', t.get('last', 0))),
|
| 1149 |
+
"funding_rate": float(funding.get(contract, {}).get('funding_rate', 0)),
|
| 1150 |
+
"volume_24h": float(t.get('volume_24h', 0)),
|
| 1151 |
+
"open_interest": float(t.get('total_size', 0)),
|
| 1152 |
+
"change_24h": float(t.get('change_percentage', 0)),
|
| 1153 |
+
})
|
| 1154 |
+
if not markets:
|
| 1155 |
+
return jsonify({"error": "Gate.io API unreachable"}), 503
|
| 1156 |
+
return jsonify(markets[:20])
|
| 1157 |
+
|
| 1158 |
+
@app.route('/api/funding/stats', methods=['GET'])
|
| 1159 |
+
def funding_stats():
|
| 1160 |
+
"""Get funding rate statistics from Gate.io"""
|
| 1161 |
+
try:
|
| 1162 |
+
funding = _gateio_funding()
|
| 1163 |
+
rates = list(funding.values())
|
| 1164 |
+
if rates:
|
| 1165 |
+
current_rate = sum(float(r.get('funding_rate', 0)) for r in rates) / len(rates)
|
| 1166 |
+
avg_rate = current_rate
|
| 1167 |
+
else:
|
| 1168 |
+
current_rate = 0
|
| 1169 |
+
avg_rate = 0
|
| 1170 |
+
|
| 1171 |
+
return jsonify({
|
| 1172 |
+
"current_rate": current_rate,
|
| 1173 |
+
"current_rate_percent": current_rate * 100,
|
| 1174 |
+
"avg_rate_24h": avg_rate,
|
| 1175 |
+
"oi_imbalance": 0,
|
| 1176 |
+
"recent_rates": [
|
| 1177 |
+
{"market": r.get('contract', ''), "rate": float(r.get('funding_rate', 0)), "timestamp": r.get('funding_time', '')}
|
| 1178 |
+
for r in rates[:24]
|
| 1179 |
+
],
|
| 1180 |
+
})
|
| 1181 |
+
except Exception as e:
|
| 1182 |
+
return jsonify({"error": str(e)}), 500
|
| 1183 |
+
|
| 1184 |
+
def _safe_json(response):
|
| 1185 |
+
"""Extract JSON from a Flask Response or (Response, status) tuple."""
|
| 1186 |
+
if isinstance(response, tuple):
|
| 1187 |
+
response = response[0]
|
| 1188 |
+
if hasattr(response, 'get_json'):
|
| 1189 |
+
return response.get_json() or {}
|
| 1190 |
+
return {}
|
| 1191 |
+
|
| 1192 |
+
@app.route('/api/liquidation/stats', methods=['GET'])
|
| 1193 |
+
def liquidation_stats():
|
| 1194 |
+
"""Get liquidation statistics from real DB"""
|
| 1195 |
+
try:
|
| 1196 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 1197 |
+
cursor = conn.cursor()
|
| 1198 |
+
cursor.execute("SELECT COUNT(*) FROM positions WHERE size = 0")
|
| 1199 |
+
total_liquidations = cursor.fetchone()[0]
|
| 1200 |
+
cursor.execute("SELECT SUM(margin) FROM positions WHERE size > 0")
|
| 1201 |
+
insurance_fund = cursor.fetchone()[0] or 0
|
| 1202 |
+
|
| 1203 |
+
# Count at-risk positions using real mark prices
|
| 1204 |
+
tickers = _gateio_tickers()
|
| 1205 |
+
cursor.execute("""
|
| 1206 |
+
SELECT position_id, trader, market, side, size, entry_price, margin, liquidation_price
|
| 1207 |
+
FROM positions
|
| 1208 |
+
WHERE size > 0
|
| 1209 |
+
""")
|
| 1210 |
+
positions = cursor.fetchall()
|
| 1211 |
+
at_risk_count = 0
|
| 1212 |
+
for pos in positions:
|
| 1213 |
+
market = pos[2]
|
| 1214 |
+
contract = market.replace('/', '_').upper()
|
| 1215 |
+
mark_price = float(tickers.get(contract, {}).get('last', 50000))
|
| 1216 |
+
notional = pos[4] * mark_price
|
| 1217 |
+
margin_ratio = pos[6] / notional if notional > 0 else 1
|
| 1218 |
+
if margin_ratio < 0.10:
|
| 1219 |
+
at_risk_count += 1
|
| 1220 |
+
conn.close()
|
| 1221 |
+
return jsonify({
|
| 1222 |
+
"total_liquidations": total_liquidations,
|
| 1223 |
+
"insurance_fund": insurance_fund,
|
| 1224 |
+
"at_risk": at_risk_count,
|
| 1225 |
+
"recent_liquidations": [],
|
| 1226 |
+
})
|
| 1227 |
+
except Exception as e:
|
| 1228 |
+
return jsonify({"error": str(e)}), 500
|
| 1229 |
+
|
| 1230 |
+
@app.route('/api/liquidation/at-risk', methods=['GET'])
|
| 1231 |
+
def at_risk_positions():
|
| 1232 |
+
"""Get at-risk positions"""
|
| 1233 |
+
try:
|
| 1234 |
+
conn = sqlite3.connect(TRADING_DB)
|
| 1235 |
+
cursor = conn.cursor()
|
| 1236 |
+
|
| 1237 |
+
cursor.execute("""
|
| 1238 |
+
SELECT position_id, trader, market, side, size, entry_price, margin, liquidation_price
|
| 1239 |
+
FROM positions
|
| 1240 |
+
WHERE size > 0
|
| 1241 |
+
""")
|
| 1242 |
+
|
| 1243 |
+
positions = cursor.fetchall()
|
| 1244 |
+
conn.close()
|
| 1245 |
+
|
| 1246 |
+
tickers = _gateio_tickers()
|
| 1247 |
+
at_risk = []
|
| 1248 |
+
for pos in positions:
|
| 1249 |
+
market = pos[2]
|
| 1250 |
+
contract = market.replace('/', '_').upper()
|
| 1251 |
+
mark_price = float(tickers.get(contract, {}).get('last', 50000))
|
| 1252 |
+
notional = pos[4] * mark_price
|
| 1253 |
+
margin_ratio = pos[6] / notional if notional > 0 else 1
|
| 1254 |
+
|
| 1255 |
+
if margin_ratio < 0.10:
|
| 1256 |
+
at_risk.append({
|
| 1257 |
+
"position_id": pos[0],
|
| 1258 |
+
"trader": pos[1],
|
| 1259 |
+
"market": pos[2],
|
| 1260 |
+
"side": pos[3],
|
| 1261 |
+
"margin_ratio": margin_ratio,
|
| 1262 |
+
"liquidation_price": pos[7],
|
| 1263 |
+
})
|
| 1264 |
+
|
| 1265 |
+
return jsonify(at_risk[:10])
|
| 1266 |
+
except Exception as e:
|
| 1267 |
+
return jsonify({"error": str(e)}), 500
|
| 1268 |
+
|
| 1269 |
+
@app.route('/api/mining/stats', methods=['GET'])
|
| 1270 |
+
def mining_stats():
|
| 1271 |
+
"""Get mining rewards statistics"""
|
| 1272 |
+
try:
|
| 1273 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 1274 |
+
cursor = conn.cursor()
|
| 1275 |
+
|
| 1276 |
+
cursor.execute("SELECT COUNT(*) FROM providers WHERE status = 'active'")
|
| 1277 |
+
active_providers = cursor.fetchone()[0]
|
| 1278 |
+
|
| 1279 |
+
cursor.execute("SELECT SUM(amount) FROM rewards")
|
| 1280 |
+
total_rewards = cursor.fetchone()[0] or 0
|
| 1281 |
+
|
| 1282 |
+
cursor.execute("SELECT COUNT(*) FROM rewards")
|
| 1283 |
+
total_reward_count = cursor.fetchone()[0]
|
| 1284 |
+
|
| 1285 |
+
conn.close()
|
| 1286 |
+
|
| 1287 |
+
return jsonify({
|
| 1288 |
+
"active_providers": active_providers,
|
| 1289 |
+
"total_rewards": total_rewards,
|
| 1290 |
+
"total_reward_count": total_reward_count,
|
| 1291 |
+
"avg_reward_per_provider": total_rewards / active_providers if active_providers > 0 else 0,
|
| 1292 |
+
})
|
| 1293 |
+
except Exception as e:
|
| 1294 |
+
return jsonify({"error": str(e)}), 500
|
| 1295 |
+
|
| 1296 |
+
@app.route('/api/mining/leaderboard', methods=['GET'])
|
| 1297 |
+
def mining_leaderboard():
|
| 1298 |
+
"""Get mining rewards leaderboard"""
|
| 1299 |
+
try:
|
| 1300 |
+
conn = sqlite3.connect(INFERENCE_DB)
|
| 1301 |
+
cursor = conn.cursor()
|
| 1302 |
+
|
| 1303 |
+
cursor.execute("""
|
| 1304 |
+
SELECT provider_id, wallet_address, model_type, total_earnings, reputation_score
|
| 1305 |
+
FROM providers
|
| 1306 |
+
WHERE status = 'active'
|
| 1307 |
+
ORDER BY total_earnings DESC
|
| 1308 |
+
LIMIT 10
|
| 1309 |
+
""")
|
| 1310 |
+
|
| 1311 |
+
providers = cursor.fetchall()
|
| 1312 |
+
conn.close()
|
| 1313 |
+
|
| 1314 |
+
return jsonify([
|
| 1315 |
+
{
|
| 1316 |
+
"rank": i + 1,
|
| 1317 |
+
"provider_id": p[0],
|
| 1318 |
+
"wallet_address": p[1],
|
| 1319 |
+
"model_type": p[2],
|
| 1320 |
+
"total_earnings": p[3],
|
| 1321 |
+
"reputation_score": p[4],
|
| 1322 |
+
}
|
| 1323 |
+
for i, p in enumerate(providers)
|
| 1324 |
+
])
|
| 1325 |
+
except Exception as e:
|
| 1326 |
+
return jsonify({"error": str(e)}), 500
|
| 1327 |
+
|
| 1328 |
+
@app.route('/api/overview', methods=['GET'])
|
| 1329 |
+
def overview():
|
| 1330 |
+
"""Get overview statistics from all systems"""
|
| 1331 |
+
try:
|
| 1332 |
+
config = _integration_config()
|
| 1333 |
+
slippage = _safe_json(slippage_stats())
|
| 1334 |
+
holders = _safe_json(holder_stats())
|
| 1335 |
+
liquidity = _safe_json(liquidity_stats())
|
| 1336 |
+
trading = _safe_json(trading_stats())
|
| 1337 |
+
funding = _safe_json(funding_stats())
|
| 1338 |
+
liquidation = _safe_json(liquidation_stats())
|
| 1339 |
+
mining = _safe_json(mining_stats())
|
| 1340 |
+
token_launch = _safe_json(token_launch_status())
|
| 1341 |
+
collateral = _collateral_summary()
|
| 1342 |
+
|
| 1343 |
+
# Build systems status from actual endpoint statuses
|
| 1344 |
+
systems = {
|
| 1345 |
+
"slippage_collector": slippage.get("status", "pending") if config["token_mint"]["configured"] else "not_wired",
|
| 1346 |
+
"holder_tracker": holders.get("status", "pending") if config["token_mint"]["configured"] else "not_wired",
|
| 1347 |
+
"llm_liquidity": liquidity.get("status", "pending") if config["inference_endpoint"]["configured"] else "local_only",
|
| 1348 |
+
"merkle_token_launch": token_launch.get("status", "pending"),
|
| 1349 |
+
"hf_account_collateral": collateral.get("status", "not_scanned"),
|
| 1350 |
+
"trading_engine": "active" if trading.get("total_volume") is not None else "pending",
|
| 1351 |
+
"funding_engine": "active" if funding.get("current_rate") is not None else "pending",
|
| 1352 |
+
"liquidation_system": "active" if liquidation.get("total_liquidations") is not None else "pending",
|
| 1353 |
+
"mining_rewards": "active" if mining.get("total_rewards") is not None else "pending",
|
| 1354 |
+
}
|
| 1355 |
+
status_meta = {
|
| 1356 |
+
"slippage_collector": _status_meta(systems["slippage_collector"], "Slippage collector", "Uses a public default Solana token mint unless another token is configured."),
|
| 1357 |
+
"holder_tracker": _status_meta(systems["holder_tracker"], "Holder tracker", "Uses public Solana RPC with a default token mint. No API key required."),
|
| 1358 |
+
"llm_liquidity": _status_meta(systems["llm_liquidity"], "LLM liquidity", "Local-only until an optional no-key local inference endpoint is connected."),
|
| 1359 |
+
"merkle_token_launch": _status_meta(systems["merkle_token_launch"], "Merkle token launch", "One Merkle root commits token spec, pool spec, allocation vector, gates, and live source metrics."),
|
| 1360 |
+
"hf_account_collateral": _status_meta(systems["hf_account_collateral"], "HF account collateral", "Public HF repos, spaces, files, and readable LOC are scanned into collateral evidence."),
|
| 1361 |
+
"trading_engine": _status_meta(systems["trading_engine"], "Trading engine", "Local perpetual futures DB plus public Gate.io market data."),
|
| 1362 |
+
"funding_engine": _status_meta(systems["funding_engine"], "Funding engine", "Public Gate.io funding-rate feed."),
|
| 1363 |
+
"liquidation_system": _status_meta(systems["liquidation_system"], "Liquidation system", "Local position-risk engine."),
|
| 1364 |
+
"mining_rewards": _status_meta(systems["mining_rewards"], "Mining rewards", "Local provider rewards ledger."),
|
| 1365 |
+
}
|
| 1366 |
+
|
| 1367 |
+
return jsonify({
|
| 1368 |
+
"mode": "no_mock_real_backend",
|
| 1369 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 1370 |
+
"config": config,
|
| 1371 |
+
"slippage": slippage,
|
| 1372 |
+
"holders": holders,
|
| 1373 |
+
"liquidity": liquidity,
|
| 1374 |
+
"trading": trading,
|
| 1375 |
+
"funding": funding,
|
| 1376 |
+
"liquidation": liquidation,
|
| 1377 |
+
"mining": mining,
|
| 1378 |
+
"token_launch": token_launch,
|
| 1379 |
+
"collateral": collateral,
|
| 1380 |
+
"systems": systems,
|
| 1381 |
+
"status_meta": status_meta,
|
| 1382 |
+
})
|
| 1383 |
+
except Exception as e:
|
| 1384 |
+
return jsonify({"error": str(e)}), 500
|
| 1385 |
+
|
| 1386 |
+
# Serve static UI
|
| 1387 |
+
@app.route('/')
|
| 1388 |
+
def index():
|
| 1389 |
+
"""Serve the dashboard UI"""
|
| 1390 |
+
return render_template_string("""
|
| 1391 |
+
<!DOCTYPE html>
|
| 1392 |
+
<html lang="en">
|
| 1393 |
+
<head>
|
| 1394 |
+
<meta charset="UTF-8">
|
| 1395 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1396 |
+
<title>AirMicroDrip Control Plane</title>
|
| 1397 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 1398 |
+
<script>
|
| 1399 |
+
tailwind.config = {
|
| 1400 |
+
theme: {
|
| 1401 |
+
extend: {
|
| 1402 |
+
colors: {
|
| 1403 |
+
ink: '#08111f',
|
| 1404 |
+
panel: '#101b2e',
|
| 1405 |
+
panel2: '#14233a',
|
| 1406 |
+
line: '#26364f',
|
| 1407 |
+
cyan: '#67e8f9',
|
| 1408 |
+
mint: '#7dd3a8',
|
| 1409 |
+
amber: '#f5c76b'
|
| 1410 |
+
}
|
| 1411 |
+
}
|
| 1412 |
+
}
|
| 1413 |
+
}
|
| 1414 |
+
</script>
|
| 1415 |
+
<style>
|
| 1416 |
+
body {
|
| 1417 |
+
background:
|
| 1418 |
+
radial-gradient(circle at top left, rgba(103,232,249,0.16), transparent 34rem),
|
| 1419 |
+
radial-gradient(circle at 80% 15%, rgba(125,211,168,0.10), transparent 30rem),
|
| 1420 |
+
linear-gradient(135deg, #07111f 0%, #0b1322 55%, #060a12 100%);
|
| 1421 |
+
}
|
| 1422 |
+
.glass { background: rgba(16, 27, 46, .82); backdrop-filter: blur(18px); }
|
| 1423 |
+
.grid-bg {
|
| 1424 |
+
background-image:
|
| 1425 |
+
linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
|
| 1426 |
+
linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
|
| 1427 |
+
background-size: 28px 28px;
|
| 1428 |
+
}
|
| 1429 |
+
.mono { font-variant-numeric: tabular-nums; }
|
| 1430 |
+
</style>
|
| 1431 |
+
</head>
|
| 1432 |
+
<body class="grid-bg min-h-screen text-slate-100">
|
| 1433 |
+
<div class="mx-auto flex min-h-screen w-full max-w-7xl flex-col px-4 py-5 sm:px-6 lg:px-8">
|
| 1434 |
+
<header class="mb-5 flex flex-col gap-4 rounded-3xl border border-white/10 bg-white/[0.035] p-5 shadow-2xl shadow-black/30 md:flex-row md:items-center md:justify-between">
|
| 1435 |
+
<div>
|
| 1436 |
+
<div class="mb-3 flex flex-wrap items-center gap-2">
|
| 1437 |
+
<span class="rounded-full border border-cyan/30 bg-cyan/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-cyan">No-key backend</span>
|
| 1438 |
+
<span id="last-updated" class="rounded-full border border-white/10 px-3 py-1 text-xs text-slate-400">syncing</span>
|
| 1439 |
+
</div>
|
| 1440 |
+
<h1 class="text-3xl font-black tracking-tight text-white sm:text-5xl">AirMicroDrip Control Plane</h1>
|
| 1441 |
+
<p class="mt-3 max-w-3xl text-sm leading-6 text-slate-300 sm:text-base">
|
| 1442 |
+
Real Flask backend, public market data, local SQLite ledgers, and optional no-key inference wiring. No fabricated holders, liquidity, payouts, or model benchmarks.
|
| 1443 |
+
</p>
|
| 1444 |
+
</div>
|
| 1445 |
+
<div class="grid min-w-[250px] grid-cols-2 gap-2 text-xs">
|
| 1446 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1447 |
+
<div class="text-slate-500">Mode</div>
|
| 1448 |
+
<div id="runtime-mode" class="mt-1 font-semibold text-mint">no_mock_real_backend</div>
|
| 1449 |
+
</div>
|
| 1450 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1451 |
+
<div class="text-slate-500">API keys</div>
|
| 1452 |
+
<div class="mt-1 font-semibold text-cyan">not required</div>
|
| 1453 |
+
</div>
|
| 1454 |
+
</div>
|
| 1455 |
+
</header>
|
| 1456 |
+
|
| 1457 |
+
<main class="grid flex-1 gap-5 lg:grid-cols-[1.35fr_.65fr]">
|
| 1458 |
+
<section class="space-y-5">
|
| 1459 |
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
| 1460 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 1461 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">Total holders</p>
|
| 1462 |
+
<p id="total-holders" class="mono mt-3 text-4xl font-black">--</p>
|
| 1463 |
+
<p id="holder-subtitle" class="mt-2 text-xs text-slate-400">public Solana RPC</p>
|
| 1464 |
+
</article>
|
| 1465 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 1466 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">LLM providers</p>
|
| 1467 |
+
<p id="total-providers" class="mono mt-3 text-4xl font-black">--</p>
|
| 1468 |
+
<p class="mt-2 text-xs text-slate-400">optional local inference endpoint</p>
|
| 1469 |
+
</article>
|
| 1470 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 1471 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">Synthetic liquidity</p>
|
| 1472 |
+
<p id="total-liquidity" class="mono mt-3 text-4xl font-black">--</p>
|
| 1473 |
+
<p class="mt-2 text-xs text-slate-400">verified benchmark only</p>
|
| 1474 |
+
</article>
|
| 1475 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 1476 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">Active positions</p>
|
| 1477 |
+
<p id="active-positions" class="mono mt-3 text-4xl font-black">--</p>
|
| 1478 |
+
<p class="mt-2 text-xs text-slate-400">local perp engine</p>
|
| 1479 |
+
</article>
|
| 1480 |
+
</div>
|
| 1481 |
+
|
| 1482 |
+
<div class="grid gap-5 xl:grid-cols-[.9fr_1.1fr]">
|
| 1483 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 1484 |
+
<div class="mb-4 flex items-center justify-between">
|
| 1485 |
+
<h2 class="text-lg font-bold">System fabric</h2>
|
| 1486 |
+
<span class="rounded-full bg-mint/10 px-3 py-1 text-xs font-semibold text-mint">backend online</span>
|
| 1487 |
+
</div>
|
| 1488 |
+
<div id="system-status" class="space-y-3">
|
| 1489 |
+
<div class="h-14 animate-pulse rounded-2xl bg-white/5"></div>
|
| 1490 |
+
<div class="h-14 animate-pulse rounded-2xl bg-white/5"></div>
|
| 1491 |
+
<div class="h-14 animate-pulse rounded-2xl bg-white/5"></div>
|
| 1492 |
+
</div>
|
| 1493 |
+
</section>
|
| 1494 |
+
|
| 1495 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 1496 |
+
<div class="mb-4 flex items-center justify-between">
|
| 1497 |
+
<h2 class="text-lg font-bold">No-key integration map</h2>
|
| 1498 |
+
<span class="rounded-full border border-cyan/25 bg-cyan/10 px-3 py-1 text-xs text-cyan">public + local</span>
|
| 1499 |
+
</div>
|
| 1500 |
+
<div id="integration-map" class="grid gap-3 sm:grid-cols-2"></div>
|
| 1501 |
+
</section>
|
| 1502 |
+
</div>
|
| 1503 |
+
|
| 1504 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 1505 |
+
<div class="mb-4 flex items-center justify-between">
|
| 1506 |
+
<h2 class="text-lg font-bold">Market and protocol telemetry</h2>
|
| 1507 |
+
<span class="text-xs text-slate-500">values are persisted or fetched live</span>
|
| 1508 |
+
</div>
|
| 1509 |
+
<div class="grid gap-3 md:grid-cols-3">
|
| 1510 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1511 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">24h volume</p>
|
| 1512 |
+
<p id="volume-24h" class="mono mt-2 text-2xl font-bold">$0</p>
|
| 1513 |
+
</div>
|
| 1514 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1515 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Funding avg</p>
|
| 1516 |
+
<p id="funding-rate" class="mono mt-2 text-2xl font-bold">0%</p>
|
| 1517 |
+
</div>
|
| 1518 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1519 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Insurance fund</p>
|
| 1520 |
+
<p id="insurance-fund" class="mono mt-2 text-2xl font-bold">$0</p>
|
| 1521 |
+
</div>
|
| 1522 |
+
</div>
|
| 1523 |
+
</section>
|
| 1524 |
+
|
| 1525 |
+
<section class="glass rounded-3xl border border-cyan/20 p-5">
|
| 1526 |
+
<div class="mb-4 flex items-center justify-between gap-3">
|
| 1527 |
+
<div>
|
| 1528 |
+
<h2 class="text-lg font-bold">One Merkle Tree Token Launch</h2>
|
| 1529 |
+
<p class="mt-1 text-xs text-slate-400">A single root commits token spec, pool spec, allocation vector, gates, and live source metrics.</p>
|
| 1530 |
+
</div>
|
| 1531 |
+
<span id="launch-status" class="rounded-full border border-cyan/30 bg-cyan/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-cyan">loading</span>
|
| 1532 |
+
</div>
|
| 1533 |
+
<div class="grid gap-3 md:grid-cols-3">
|
| 1534 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1535 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Token</p>
|
| 1536 |
+
<p id="launch-token" class="mt-2 text-2xl font-black">--</p>
|
| 1537 |
+
<p id="launch-supply" class="mt-1 text-xs text-slate-400">supply pending</p>
|
| 1538 |
+
</div>
|
| 1539 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1540 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Pool</p>
|
| 1541 |
+
<p id="launch-pool" class="mt-2 text-2xl font-black">--</p>
|
| 1542 |
+
<p id="launch-pool-status" class="mt-1 text-xs text-slate-400">quote asset requires signer funding</p>
|
| 1543 |
+
</div>
|
| 1544 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1545 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Setup plan</p>
|
| 1546 |
+
<p id="launch-proofs" class="mt-2 text-2xl font-black">--</p>
|
| 1547 |
+
<p id="launch-leaves" class="mt-1 text-xs text-slate-400">leaves pending</p>
|
| 1548 |
+
</div>
|
| 1549 |
+
</div>
|
| 1550 |
+
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
| 1551 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1552 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Merkle root</p>
|
| 1553 |
+
<p id="launch-root" class="mono mt-2 break-all text-xs text-cyan">--</p>
|
| 1554 |
+
</div>
|
| 1555 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1556 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Manifest hash</p>
|
| 1557 |
+
<p id="launch-hash" class="mono mt-2 break-all text-xs text-mint">--</p>
|
| 1558 |
+
</div>
|
| 1559 |
+
</div>
|
| 1560 |
+
<div class="mt-3 rounded-2xl border border-amber/20 bg-amber/10 p-4 text-xs leading-5 text-amber">
|
| 1561 |
+
This prepares the launch tree. It does not claim a live SPL mint or live liquidity pool until real wallet-signed transaction signatures are published.
|
| 1562 |
+
</div>
|
| 1563 |
+
</section>
|
| 1564 |
+
|
| 1565 |
+
<section class="glass rounded-3xl border border-mint/20 p-5">
|
| 1566 |
+
<div class="mb-4 flex items-center justify-between gap-3">
|
| 1567 |
+
<div>
|
| 1568 |
+
<h2 class="text-lg font-bold">HF Account Collateral</h2>
|
| 1569 |
+
<p class="mt-1 text-xs text-slate-400">Owner repos, Spaces, files, and readable LOC become collateral leaves in the same launch tree.</p>
|
| 1570 |
+
</div>
|
| 1571 |
+
<div class="flex flex-col items-end gap-2">
|
| 1572 |
+
<span id="collateral-status" class="rounded-full border border-mint/30 bg-mint/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-mint">not scanned</span>
|
| 1573 |
+
<button id="collateral-scan-button" class="rounded-full border border-cyan/30 bg-cyan/10 px-3 py-1 text-xs font-semibold text-cyan hover:bg-cyan/20">scan account</button>
|
| 1574 |
+
</div>
|
| 1575 |
+
</div>
|
| 1576 |
+
<div class="grid gap-3 md:grid-cols-4">
|
| 1577 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1578 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Owner</p>
|
| 1579 |
+
<p id="collateral-owner" class="mt-2 text-2xl font-black">--</p>
|
| 1580 |
+
</div>
|
| 1581 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1582 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Repos</p>
|
| 1583 |
+
<p id="collateral-repos" class="mono mt-2 text-2xl font-black">0</p>
|
| 1584 |
+
</div>
|
| 1585 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1586 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Files read</p>
|
| 1587 |
+
<p id="collateral-files" class="mono mt-2 text-2xl font-black">0</p>
|
| 1588 |
+
</div>
|
| 1589 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1590 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">LOC</p>
|
| 1591 |
+
<p id="collateral-loc" class="mono mt-2 text-2xl font-black">0</p>
|
| 1592 |
+
</div>
|
| 1593 |
+
</div>
|
| 1594 |
+
<div class="mt-3 rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1595 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Collateral root</p>
|
| 1596 |
+
<p id="collateral-root" class="mono mt-2 break-all text-xs text-mint">scan required</p>
|
| 1597 |
+
</div>
|
| 1598 |
+
</section>
|
| 1599 |
+
</div>
|
| 1600 |
+
|
| 1601 |
+
<aside class="space-y-5">
|
| 1602 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 1603 |
+
<h2 class="text-lg font-bold">Backend contract</h2>
|
| 1604 |
+
<div class="mt-4 space-y-3 text-sm text-slate-300">
|
| 1605 |
+
<div class="rounded-2xl border border-mint/20 bg-mint/10 p-4">
|
| 1606 |
+
<div class="font-semibold text-mint">No API keys required</div>
|
| 1607 |
+
<p class="mt-1 text-xs text-slate-300">Public feeds and local ledgers are used by default. Optional endpoints are clearly marked local-only until connected.</p>
|
| 1608 |
+
</div>
|
| 1609 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 1610 |
+
<div class="font-semibold text-white">No mock success states</div>
|
| 1611 |
+
<p class="mt-1 text-xs text-slate-400">If a source is unavailable, the app reports waiting, local-only, or error states instead of inventing values.</p>
|
| 1612 |
+
</div>
|
| 1613 |
+
</div>
|
| 1614 |
+
</section>
|
| 1615 |
+
|
| 1616 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 1617 |
+
<h2 class="text-lg font-bold">Data provenance</h2>
|
| 1618 |
+
<div class="mt-4 space-y-3 text-sm" id="provenance-list">
|
| 1619 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3 text-slate-400">Loading provenance...</div>
|
| 1620 |
+
</div>
|
| 1621 |
+
</section>
|
| 1622 |
+
|
| 1623 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 1624 |
+
<h2 class="text-lg font-bold">Operator notes</h2>
|
| 1625 |
+
<ul class="mt-4 space-y-2 text-sm text-slate-300">
|
| 1626 |
+
<li>• Holder and slippage routes use a public default token mint.</li>
|
| 1627 |
+
<li>• LLM liquidity activates only after a real endpoint responds.</li>
|
| 1628 |
+
<li>• Existing local ledgers remain empty until real events occur.</li>
|
| 1629 |
+
</ul>
|
| 1630 |
+
</section>
|
| 1631 |
+
</aside>
|
| 1632 |
+
</main>
|
| 1633 |
+
</div>
|
| 1634 |
+
|
| 1635 |
+
<script>
|
| 1636 |
+
function formatNumber(value, prefix = '') {
|
| 1637 |
+
if (value === undefined || value === null || Number.isNaN(Number(value))) return prefix + '0';
|
| 1638 |
+
return prefix + Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 });
|
| 1639 |
+
}
|
| 1640 |
+
|
| 1641 |
+
function formatValue(value, prefix = '') {
|
| 1642 |
+
if (value === undefined || value === null) return prefix + '0';
|
| 1643 |
+
if (typeof value === 'number') return formatNumber(value, prefix);
|
| 1644 |
+
return String(value);
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
function statusClasses(status) {
|
| 1648 |
+
if (status === 'active') return 'border-mint/30 bg-mint/10 text-mint';
|
| 1649 |
+
if (status === 'local_only') return 'border-cyan/30 bg-cyan/10 text-cyan';
|
| 1650 |
+
if (status === 'unsigned_ready') return 'border-cyan/30 bg-cyan/10 text-cyan';
|
| 1651 |
+
if (status === 'ready_for_signature') return 'border-mint/30 bg-mint/10 text-mint';
|
| 1652 |
+
if (status === 'pending' || status === 'not_wired') return 'border-amber/30 bg-amber/10 text-amber';
|
| 1653 |
+
if (status === 'not_scanned') return 'border-amber/30 bg-amber/10 text-amber';
|
| 1654 |
+
return 'border-red-400/30 bg-red-400/10 text-red-300';
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
function statusDot(status) {
|
| 1658 |
+
if (status === 'active') return 'bg-mint';
|
| 1659 |
+
if (status === 'local_only') return 'bg-cyan';
|
| 1660 |
+
if (status === 'unsigned_ready') return 'bg-cyan';
|
| 1661 |
+
if (status === 'ready_for_signature') return 'bg-mint';
|
| 1662 |
+
if (status === 'pending' || status === 'not_wired') return 'bg-amber';
|
| 1663 |
+
if (status === 'not_scanned') return 'bg-amber';
|
| 1664 |
+
return 'bg-red-300';
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
+
async function startCollateralScan() {
|
| 1668 |
+
const button = document.getElementById('collateral-scan-button');
|
| 1669 |
+
button.disabled = true;
|
| 1670 |
+
button.textContent = 'scanning...';
|
| 1671 |
+
try {
|
| 1672 |
+
await fetch('/api/collateral/scan', {
|
| 1673 |
+
method: 'POST',
|
| 1674 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1675 |
+
body: JSON.stringify({})
|
| 1676 |
+
});
|
| 1677 |
+
await loadData();
|
| 1678 |
+
} catch (error) {
|
| 1679 |
+
console.error('Failed to start collateral scan:', error);
|
| 1680 |
+
} finally {
|
| 1681 |
+
setTimeout(() => {
|
| 1682 |
+
button.disabled = false;
|
| 1683 |
+
button.textContent = 'scan account';
|
| 1684 |
+
}, 3000);
|
| 1685 |
+
}
|
| 1686 |
+
}
|
| 1687 |
+
|
| 1688 |
+
async function loadData() {
|
| 1689 |
+
try {
|
| 1690 |
+
const response = await fetch('/api/overview');
|
| 1691 |
+
if (!response.ok) throw new Error('overview failed: ' + response.status);
|
| 1692 |
+
const data = await response.json();
|
| 1693 |
+
|
| 1694 |
+
const holders = data.holders || {};
|
| 1695 |
+
const liquidity = data.liquidity || {};
|
| 1696 |
+
const trading = data.trading || {};
|
| 1697 |
+
const funding = data.funding || {};
|
| 1698 |
+
const liquidation = data.liquidation || {};
|
| 1699 |
+
const tokenLaunch = data.token_launch || {};
|
| 1700 |
+
const collateral = data.collateral || {};
|
| 1701 |
+
const statusMeta = data.status_meta || {};
|
| 1702 |
+
const config = data.config || {};
|
| 1703 |
+
const integrations = config || {};
|
| 1704 |
+
|
| 1705 |
+
document.getElementById('total-holders').textContent = formatValue(holders.total_holders);
|
| 1706 |
+
document.getElementById('total-providers').textContent = formatValue(liquidity.total_providers);
|
| 1707 |
+
document.getElementById('total-liquidity').textContent = formatNumber(liquidity.total_liquidity_usd, '$');
|
| 1708 |
+
document.getElementById('active-positions').textContent = formatValue(trading.active_positions);
|
| 1709 |
+
document.getElementById('volume-24h').textContent = formatNumber(trading.total_volume, '$');
|
| 1710 |
+
document.getElementById('funding-rate').textContent = ((funding.current_rate_percent || 0).toFixed(4)) + '%';
|
| 1711 |
+
document.getElementById('insurance-fund').textContent = formatNumber(liquidation.insurance_fund, '$');
|
| 1712 |
+
document.getElementById('runtime-mode').textContent = data.mode || 'no_mock_real_backend';
|
| 1713 |
+
document.getElementById('last-updated').textContent = data.timestamp ? new Date(data.timestamp).toLocaleTimeString() : 'live';
|
| 1714 |
+
document.getElementById('holder-subtitle').textContent = holders.default_token ? 'default public token mint' : 'configured token mint';
|
| 1715 |
+
document.getElementById('launch-status').textContent = (tokenLaunch.status || 'waiting').replace('_', ' ');
|
| 1716 |
+
document.getElementById('launch-token').textContent = `${tokenLaunch.token_spec?.symbol || '--'}`;
|
| 1717 |
+
document.getElementById('launch-supply').textContent = formatNumber(tokenLaunch.token_spec?.total_supply) + ' committed supply';
|
| 1718 |
+
document.getElementById('launch-pool').textContent = tokenLaunch.pool_spec?.pair || '--';
|
| 1719 |
+
document.getElementById('launch-pool-status').textContent = tokenLaunch.pool_setup_status?.status?.replaceAll('_', ' ') || 'ready for signature';
|
| 1720 |
+
document.getElementById('launch-proofs').textContent = `${tokenLaunch.unsigned_solana_plan?.length || 0} steps`;
|
| 1721 |
+
document.getElementById('launch-leaves').textContent = `${tokenLaunch.leaf_count || 0} leaves / proofs ${Object.values(tokenLaunch.proof_checks || {}).every(Boolean) ? 'verified' : 'waiting'}`;
|
| 1722 |
+
document.getElementById('launch-root').textContent = tokenLaunch.merkle_root || '--';
|
| 1723 |
+
document.getElementById('launch-hash').textContent = tokenLaunch.manifest_hash || '--';
|
| 1724 |
+
document.getElementById('collateral-status').textContent = (collateral.status || 'not_scanned').replaceAll('_', ' ');
|
| 1725 |
+
document.getElementById('collateral-owner').textContent = collateral.owner || '--';
|
| 1726 |
+
document.getElementById('collateral-repos').textContent = formatValue(collateral.repo_count);
|
| 1727 |
+
document.getElementById('collateral-files').textContent = formatValue(collateral.total_text_files || collateral.total_files);
|
| 1728 |
+
document.getElementById('collateral-loc').textContent = formatValue(collateral.total_loc);
|
| 1729 |
+
document.getElementById('collateral-root').textContent = collateral.collateral_root || 'scan required';
|
| 1730 |
+
const scanButton = document.getElementById('collateral-scan-button');
|
| 1731 |
+
if (collateral.status === 'running') {
|
| 1732 |
+
scanButton.disabled = true;
|
| 1733 |
+
scanButton.textContent = 'scanning...';
|
| 1734 |
+
} else {
|
| 1735 |
+
scanButton.disabled = false;
|
| 1736 |
+
scanButton.textContent = collateral.collateral_root ? 'rescan account' : 'scan account';
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
const statusHtml = Object.entries(statusMeta).map(([key, meta]) => {
|
| 1740 |
+
const status = meta.status || 'pending';
|
| 1741 |
+
return `
|
| 1742 |
+
<div class="rounded-2xl border ${statusClasses(status)} p-4">
|
| 1743 |
+
<div class="flex items-start justify-between gap-3">
|
| 1744 |
+
<div>
|
| 1745 |
+
<div class="flex items-center gap-2 font-semibold">
|
| 1746 |
+
<span class="h-2 w-2 rounded-full ${statusDot(status)}"></span>
|
| 1747 |
+
${meta.label || key}
|
| 1748 |
+
</div>
|
| 1749 |
+
<p class="mt-1 text-xs leading-5 text-slate-300">${meta.detail || ''}</p>
|
| 1750 |
+
</div>
|
| 1751 |
+
<span class="rounded-full bg-black/30 px-2.5 py-1 text-[10px] uppercase tracking-[0.18em]">${meta.display || status}</span>
|
| 1752 |
+
</div>
|
| 1753 |
+
</div>`;
|
| 1754 |
+
}).join('');
|
| 1755 |
+
document.getElementById('system-status').innerHTML = statusHtml || '<p class="text-slate-400">No system status returned.</p>';
|
| 1756 |
+
|
| 1757 |
+
const integrationHtml = Object.entries(integrations).map(([key, item]) => {
|
| 1758 |
+
const status = item.status || 'pending';
|
| 1759 |
+
return `
|
| 1760 |
+
<div class="rounded-2xl border ${statusClasses(status)} p-4">
|
| 1761 |
+
<div class="text-xs uppercase tracking-[0.18em] opacity-80">${item.label || key}</div>
|
| 1762 |
+
<div class="mt-2 text-lg font-bold">${(item.status || '').replace('_', ' ')}</div>
|
| 1763 |
+
<div class="mt-2 text-xs text-slate-300">${item.env || 'no key needed'}</div>
|
| 1764 |
+
${item.value_public ? `<div class="mono mt-2 truncate text-[11px] text-slate-400">${item.value_public}</div>` : ''}
|
| 1765 |
+
</div>`;
|
| 1766 |
+
}).join('');
|
| 1767 |
+
document.getElementById('integration-map').innerHTML = integrationHtml;
|
| 1768 |
+
|
| 1769 |
+
document.getElementById('provenance-list').innerHTML = `
|
| 1770 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1771 |
+
<div class="font-semibold text-white">Market data</div>
|
| 1772 |
+
<div class="mt-1 text-xs text-slate-400">Gate.io public futures endpoints; no API key.</div>
|
| 1773 |
+
</div>
|
| 1774 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1775 |
+
<div class="font-semibold text-white">Token graph</div>
|
| 1776 |
+
<div class="mt-1 text-xs text-slate-400">Solana RPC and DexScreener public routes using the displayed token mint.</div>
|
| 1777 |
+
</div>
|
| 1778 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1779 |
+
<div class="font-semibold text-white">Local ledgers</div>
|
| 1780 |
+
<div class="mt-1 text-xs text-slate-400">SQLite stores inside the running Space container.</div>
|
| 1781 |
+
</div>`;
|
| 1782 |
+
} catch (error) {
|
| 1783 |
+
console.error('Failed to load data:', error);
|
| 1784 |
+
document.getElementById('system-status').innerHTML = '<div class="rounded-2xl border border-red-400/30 bg-red-400/10 p-4 text-red-200">Backend API did not respond. This is a real error, not a simulated state.</div>';
|
| 1785 |
+
}
|
| 1786 |
+
}
|
| 1787 |
+
|
| 1788 |
+
loadData();
|
| 1789 |
+
setInterval(loadData, 5000);
|
| 1790 |
+
document.getElementById('collateral-scan-button').addEventListener('click', startCollateralScan);
|
| 1791 |
+
</script>
|
| 1792 |
+
</body>
|
| 1793 |
+
</html>
|
| 1794 |
+
""")
|
| 1795 |
+
|
| 1796 |
+
if __name__ == '__main__':
|
| 1797 |
+
port = int(os.environ.get("PORT", 7860))
|
| 1798 |
+
print(f"Starting AirMicroDrip on port {port}")
|
| 1799 |
+
app.run(host='0.0.0.0', port=port, debug=False)
|
audit_integration.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Audit Integration
|
| 4 |
+
Connects the audit framework to the AirMicroDrip perpetual futures system.
|
| 5 |
+
Provides continuous monitoring, health checks, and compliance verification.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from typing import Dict, Any, Optional
|
| 12 |
+
|
| 13 |
+
# Add parent directory to path for audit_framework import
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
+
|
| 16 |
+
from audit_framework import (
|
| 17 |
+
AuditFramework,
|
| 18 |
+
AuditCategory,
|
| 19 |
+
AuditSeverity,
|
| 20 |
+
AuditStatus,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class AirMicroDripAuditor:
|
| 25 |
+
"""Audit wrapper for AirMicroDrip systems."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, db_path: str = "airmicrodrip_audit.db"):
|
| 28 |
+
self.audit = AuditFramework()
|
| 29 |
+
self.db_path = db_path
|
| 30 |
+
self._register_airmicrodrip_checks()
|
| 31 |
+
|
| 32 |
+
def _register_airmicrodrip_checks(self):
|
| 33 |
+
"""Register AirMicroDrip-specific audit checks."""
|
| 34 |
+
from audit_framework import AuditCheck
|
| 35 |
+
|
| 36 |
+
extra_checks = [
|
| 37 |
+
AuditCheck(
|
| 38 |
+
check_id="amd_001",
|
| 39 |
+
name="Liquidity Provider Health",
|
| 40 |
+
description="Verify at least one active LLM inference provider",
|
| 41 |
+
category=AuditCategory.AVAILABILITY,
|
| 42 |
+
severity=AuditSeverity.HIGH,
|
| 43 |
+
),
|
| 44 |
+
AuditCheck(
|
| 45 |
+
check_id="amd_002",
|
| 46 |
+
name="Synthetic Liquidity Depth",
|
| 47 |
+
description="Verify total synthetic liquidity exceeds minimum threshold",
|
| 48 |
+
category=AuditCategory.ACCURACY,
|
| 49 |
+
severity=AuditSeverity.HIGH,
|
| 50 |
+
),
|
| 51 |
+
AuditCheck(
|
| 52 |
+
check_id="amd_003",
|
| 53 |
+
name="Perpetual Engine Consistency",
|
| 54 |
+
description="Verify mark prices and index prices are within tolerance",
|
| 55 |
+
category=AuditCategory.ACCURACY,
|
| 56 |
+
severity=AuditSeverity.CRITICAL,
|
| 57 |
+
),
|
| 58 |
+
AuditCheck(
|
| 59 |
+
check_id="amd_004",
|
| 60 |
+
name="Funding Rate Bounds",
|
| 61 |
+
description="Verify funding rates are within configured min/max",
|
| 62 |
+
category=AuditCategory.ACCURACY,
|
| 63 |
+
severity=AuditSeverity.MEDIUM,
|
| 64 |
+
),
|
| 65 |
+
AuditCheck(
|
| 66 |
+
check_id="amd_005",
|
| 67 |
+
name="Liquidation Backlog",
|
| 68 |
+
description="Verify no positions are stuck in liquidation queue",
|
| 69 |
+
category=AuditCategory.INTEGRITY,
|
| 70 |
+
severity=AuditSeverity.CRITICAL,
|
| 71 |
+
),
|
| 72 |
+
AuditCheck(
|
| 73 |
+
check_id="amd_006",
|
| 74 |
+
name="Order Book Spread",
|
| 75 |
+
description="Verify bid-ask spread is within acceptable range",
|
| 76 |
+
category=AuditCategory.PERFORMANCE,
|
| 77 |
+
severity=AuditSeverity.MEDIUM,
|
| 78 |
+
),
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
for check in extra_checks:
|
| 82 |
+
self.audit.checks[check.check_id] = check
|
| 83 |
+
|
| 84 |
+
def check_liquidity_providers(self, registry) -> Dict[str, Any]:
|
| 85 |
+
"""Run liquidity provider health check."""
|
| 86 |
+
providers = registry.get_all_providers(status="active")
|
| 87 |
+
if not providers:
|
| 88 |
+
return {
|
| 89 |
+
"status": AuditStatus.FAILED,
|
| 90 |
+
"message": "No active liquidity providers",
|
| 91 |
+
"details": {"active_count": 0},
|
| 92 |
+
}
|
| 93 |
+
return {
|
| 94 |
+
"status": AuditStatus.PASSED,
|
| 95 |
+
"message": f"{len(providers)} active liquidity providers",
|
| 96 |
+
"details": {"active_count": len(providers)},
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
def check_liquidity_depth(self, converter) -> Dict[str, Any]:
|
| 100 |
+
"""Run synthetic liquidity depth check."""
|
| 101 |
+
total = converter.get_total_liquidity()
|
| 102 |
+
total_usd = total.get("total_usd", 0.0)
|
| 103 |
+
min_liquidity = float(os.environ.get("MIN_LIQUIDITY_USD", 10000.0))
|
| 104 |
+
|
| 105 |
+
if total_usd < min_liquidity:
|
| 106 |
+
return {
|
| 107 |
+
"status": AuditStatus.FAILED,
|
| 108 |
+
"message": f"Total liquidity ${total_usd:.2f} below minimum ${min_liquidity:.2f}",
|
| 109 |
+
"details": {"total_usd": total_usd, "minimum": min_liquidity},
|
| 110 |
+
}
|
| 111 |
+
return {
|
| 112 |
+
"status": AuditStatus.PASSED,
|
| 113 |
+
"message": f"Total liquidity ${total_usd:.2f} above minimum",
|
| 114 |
+
"details": {"total_usd": total_usd, "by_market": total.get("by_market", {})},
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
def check_mark_price_consistency(self, trading_engine, tolerance: float = 0.02) -> Dict[str, Any]:
|
| 118 |
+
"""Verify mark prices are close to index prices."""
|
| 119 |
+
inconsistent = []
|
| 120 |
+
for market, state in trading_engine.market_states.items():
|
| 121 |
+
if state.index_price == 0:
|
| 122 |
+
continue
|
| 123 |
+
deviation = abs(state.mark_price - state.index_price) / state.index_price
|
| 124 |
+
if deviation > tolerance:
|
| 125 |
+
inconsistent.append({
|
| 126 |
+
"market": market,
|
| 127 |
+
"mark": state.mark_price,
|
| 128 |
+
"index": state.index_price,
|
| 129 |
+
"deviation": deviation,
|
| 130 |
+
})
|
| 131 |
+
|
| 132 |
+
if inconsistent:
|
| 133 |
+
return {
|
| 134 |
+
"status": AuditStatus.FAILED,
|
| 135 |
+
"message": f"{len(inconsistent)} market(s) with price deviation > {tolerance:.1%}",
|
| 136 |
+
"details": {"inconsistent": inconsistent},
|
| 137 |
+
}
|
| 138 |
+
return {
|
| 139 |
+
"status": AuditStatus.PASSED,
|
| 140 |
+
"message": "Mark prices consistent with index prices",
|
| 141 |
+
"details": {"markets_checked": len(trading_engine.market_states)},
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
def check_funding_rate_bounds(self, funding_engine) -> Dict[str, Any]:
|
| 145 |
+
"""Verify funding rates within bounds."""
|
| 146 |
+
from funding_rate_engine import FUNDING_CONFIG
|
| 147 |
+
|
| 148 |
+
out_of_bounds = []
|
| 149 |
+
for market in funding_engine.trading_engine.market_states:
|
| 150 |
+
rate = funding_engine.calculate_funding_rate(market)
|
| 151 |
+
if rate < FUNDING_CONFIG["min_funding_rate"] or rate > FUNDING_CONFIG["max_funding_rate"]:
|
| 152 |
+
out_of_bounds.append({"market": market, "rate": rate})
|
| 153 |
+
|
| 154 |
+
if out_of_bounds:
|
| 155 |
+
return {
|
| 156 |
+
"status": AuditStatus.FAILED,
|
| 157 |
+
"message": f"{len(out_of_bounds)} funding rate(s) out of bounds",
|
| 158 |
+
"details": {"out_of_bounds": out_of_bounds},
|
| 159 |
+
}
|
| 160 |
+
return {
|
| 161 |
+
"status": AuditStatus.PASSED,
|
| 162 |
+
"message": "All funding rates within bounds",
|
| 163 |
+
"details": {"markets_checked": len(funding_engine.trading_engine.market_states)},
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
def check_liquidation_backlog(self, liq_system) -> Dict[str, Any]:
|
| 167 |
+
"""Check for stuck liquidations."""
|
| 168 |
+
at_risk = liq_system.get_at_risk_positions()
|
| 169 |
+
if len(at_risk) > 10:
|
| 170 |
+
return {
|
| 171 |
+
"status": AuditStatus.WARNING,
|
| 172 |
+
"message": f"{len(at_risk)} positions at risk — possible backlog",
|
| 173 |
+
"details": {"at_risk_count": len(at_risk)},
|
| 174 |
+
}
|
| 175 |
+
return {
|
| 176 |
+
"status": AuditStatus.PASSED,
|
| 177 |
+
"message": f"Liquidation queue healthy ({len(at_risk)} at risk)",
|
| 178 |
+
"details": {"at_risk_count": len(at_risk)},
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
def check_orderbook_spread(self, trading_engine, max_spread_bps: float = 50.0) -> Dict[str, Any]:
|
| 182 |
+
"""Verify bid-ask spreads are within tolerance."""
|
| 183 |
+
wide_spreads = []
|
| 184 |
+
for market, ob in trading_engine.order_books.items():
|
| 185 |
+
best_bid = ob.get_best_bid()
|
| 186 |
+
best_ask = ob.get_best_ask()
|
| 187 |
+
if best_bid and best_ask and best_bid > 0:
|
| 188 |
+
spread_bps = ((best_ask - best_bid) / best_bid) * 10000
|
| 189 |
+
if spread_bps > max_spread_bps:
|
| 190 |
+
wide_spreads.append({"market": market, "spread_bps": spread_bps})
|
| 191 |
+
|
| 192 |
+
if wide_spreads:
|
| 193 |
+
return {
|
| 194 |
+
"status": AuditStatus.WARNING,
|
| 195 |
+
"message": f"{len(wide_spreads)} market(s) with wide spread",
|
| 196 |
+
"details": {"wide_spreads": wide_spreads},
|
| 197 |
+
}
|
| 198 |
+
return {
|
| 199 |
+
"status": AuditStatus.PASSED,
|
| 200 |
+
"message": "Order book spreads within tolerance",
|
| 201 |
+
"details": {"markets_checked": len(trading_engine.order_books)},
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
def run_airmicrodrip_audit(
|
| 205 |
+
self,
|
| 206 |
+
registry=None,
|
| 207 |
+
converter=None,
|
| 208 |
+
trading_engine=None,
|
| 209 |
+
funding_engine=None,
|
| 210 |
+
liq_system=None,
|
| 211 |
+
) -> Dict[str, Any]:
|
| 212 |
+
"""Run the full AirMicroDrip audit suite."""
|
| 213 |
+
ctx: Dict[str, Any] = {}
|
| 214 |
+
|
| 215 |
+
if registry:
|
| 216 |
+
ctx["liquidity_providers"] = self.check_liquidity_providers(registry)
|
| 217 |
+
if converter:
|
| 218 |
+
ctx["liquidity_depth"] = self.check_liquidity_depth(converter)
|
| 219 |
+
if trading_engine:
|
| 220 |
+
ctx["price_consistency"] = self.check_mark_price_consistency(trading_engine)
|
| 221 |
+
if funding_engine:
|
| 222 |
+
ctx["funding_bounds"] = self.check_funding_rate_bounds(funding_engine)
|
| 223 |
+
if liq_system:
|
| 224 |
+
ctx["liquidation_backlog"] = self.check_liquidation_backlog(liq_system)
|
| 225 |
+
if trading_engine:
|
| 226 |
+
ctx["orderbook_spread"] = self.check_orderbook_spread(trading_engine)
|
| 227 |
+
|
| 228 |
+
# Log all results
|
| 229 |
+
for check_name, result in ctx.items():
|
| 230 |
+
status = result.get("status", AuditStatus.SKIPPED)
|
| 231 |
+
self.audit.log(
|
| 232 |
+
category=AuditCategory.INTEGRITY,
|
| 233 |
+
severity=AuditSeverity.HIGH if status == AuditStatus.FAILED else AuditSeverity.INFO,
|
| 234 |
+
status=status,
|
| 235 |
+
message=result.get("message", f"{check_name} check completed"),
|
| 236 |
+
details=result.get("details", {}),
|
| 237 |
+
actor="airmicrodrip_auditor",
|
| 238 |
+
component=check_name,
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Run base framework checks too
|
| 242 |
+
base_report = self.audit.run_audit(context=ctx)
|
| 243 |
+
|
| 244 |
+
return {
|
| 245 |
+
"base_report_id": base_report.report_id,
|
| 246 |
+
"overall_score": base_report.overall_score,
|
| 247 |
+
"airmicrodrip_checks": ctx,
|
| 248 |
+
"system_health": self.audit.get_system_health(),
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
# Standalone demo
|
| 254 |
+
auditor = AirMicroDripAuditor()
|
| 255 |
+
print("AirMicroDrip Auditor initialized with checks:")
|
| 256 |
+
for cid, check in auditor.audit.checks.items():
|
| 257 |
+
print(f" {cid}: {check.name} ({check.category.value}, {check.severity.value})")
|
| 258 |
+
print(f"\nTotal checks registered: {len(auditor.audit.checks)}")
|
create_space.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create Hugging Face Space via API, then deploy code."""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import subprocess
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Get token from environment or prompt
|
| 10 |
+
TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN") or os.environ.get("HUGGING_FACE_TOKEN")
|
| 11 |
+
if not TOKEN:
|
| 12 |
+
print("ERROR: Set HF_TOKEN environment variable")
|
| 13 |
+
sys.exit(1)
|
| 14 |
+
|
| 15 |
+
SPACE_NAME = os.environ.get("HF_SPACE_NAME", "membra-airmicrodrip")
|
| 16 |
+
OWNER = os.environ.get("HF_OWNER", "josephrw")
|
| 17 |
+
SPACE_ID = f"{OWNER}/{SPACE_NAME}"
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from huggingface_hub import HfApi
|
| 21 |
+
except ImportError:
|
| 22 |
+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "huggingface_hub"], check=True)
|
| 23 |
+
from huggingface_hub import HfApi
|
| 24 |
+
|
| 25 |
+
api = HfApi(token=TOKEN)
|
| 26 |
+
|
| 27 |
+
# Get authenticated user
|
| 28 |
+
try:
|
| 29 |
+
whoami = api.whoami()
|
| 30 |
+
actual_owner = whoami["name"]
|
| 31 |
+
print(f"Authenticated as: {actual_owner}")
|
| 32 |
+
SPACE_ID = f"{actual_owner}/{SPACE_NAME}"
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"Auth check failed: {e}")
|
| 35 |
+
sys.exit(1)
|
| 36 |
+
|
| 37 |
+
# Check if space exists
|
| 38 |
+
try:
|
| 39 |
+
api.repo_info(repo_id=SPACE_ID, repo_type="space")
|
| 40 |
+
print(f"Space {SPACE_ID} already exists.")
|
| 41 |
+
except Exception:
|
| 42 |
+
print(f"Creating Space {SPACE_ID} (docker)...")
|
| 43 |
+
try:
|
| 44 |
+
api.create_repo(
|
| 45 |
+
repo_id=SPACE_ID,
|
| 46 |
+
repo_type="space",
|
| 47 |
+
space_sdk="docker",
|
| 48 |
+
private=False,
|
| 49 |
+
)
|
| 50 |
+
print(f"Created: https://huggingface.co/spaces/{SPACE_ID}")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"Failed to create space: {e}")
|
| 53 |
+
print("\nTIP: Create manually at https://huggingface.co/new-space")
|
| 54 |
+
print(" - Space name: membra-airmicrodrip")
|
| 55 |
+
print(" - SDK: Docker")
|
| 56 |
+
sys.exit(1)
|
| 57 |
+
|
| 58 |
+
print(f"\nSpace ready: https://huggingface.co/spaces/{SPACE_ID}")
|
| 59 |
+
print("\nNow run the deploy script to push code:")
|
| 60 |
+
print(f" cd /Users/alep/Downloads/02_AI_Agents/airmicrodrip")
|
| 61 |
+
print(f" HF_TOKEN={TOKEN[:10]}... bash deploy.sh")
|
deploy.sh
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
cd "$(dirname "$0")"
|
| 5 |
+
|
| 6 |
+
echo "=== AirMicroDrip HF Space Deployment ==="
|
| 7 |
+
|
| 8 |
+
# Configuration
|
| 9 |
+
SPACE_ID="${HF_SPACE_ID:-josephrw/membra-airmicrodrip}"
|
| 10 |
+
TOKEN="${HF_TOKEN:-${HUGGINGFACE_TOKEN:-${HUGGING_FACE_TOKEN:-}}}"
|
| 11 |
+
|
| 12 |
+
if [ -z "$TOKEN" ]; then
|
| 13 |
+
echo "ERROR: Set HF_TOKEN environment variable"
|
| 14 |
+
exit 1
|
| 15 |
+
fi
|
| 16 |
+
|
| 17 |
+
echo "Space: $SPACE_ID"
|
| 18 |
+
|
| 19 |
+
# Initialize git if needed
|
| 20 |
+
if [ ! -d .git ]; then
|
| 21 |
+
git init
|
| 22 |
+
git config user.email "deploy@membra.ai"
|
| 23 |
+
git config user.name "MEMBRA Deploy"
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
# Add remote if needed
|
| 27 |
+
if ! git remote | grep -q origin; then
|
| 28 |
+
git remote add origin "https://huggingface.co/spaces/$SPACE_ID"
|
| 29 |
+
fi
|
| 30 |
+
|
| 31 |
+
# Stage all files
|
| 32 |
+
git add -A
|
| 33 |
+
|
| 34 |
+
# Commit
|
| 35 |
+
git commit -m "Deploy AirMicroDrip - no mocks, real APIs" || echo "Nothing new to commit"
|
| 36 |
+
|
| 37 |
+
# Push using token for auth (HF uses token as password with dummy username)
|
| 38 |
+
echo "Pushing to Hugging Face..."
|
| 39 |
+
git remote set-url origin "https://huggingface.co/spaces/$SPACE_ID"
|
| 40 |
+
askpass_file="$(mktemp)"
|
| 41 |
+
cat > "$askpass_file" <<'ASKPASS'
|
| 42 |
+
#!/bin/sh
|
| 43 |
+
case "$1" in
|
| 44 |
+
*Username*) printf '%s\n' "user" ;;
|
| 45 |
+
*Password*) printf '%s\n' "$HF_TOKEN" ;;
|
| 46 |
+
*) printf '\n' ;;
|
| 47 |
+
esac
|
| 48 |
+
ASKPASS
|
| 49 |
+
chmod 700 "$askpass_file"
|
| 50 |
+
trap 'rm -f "$askpass_file"' EXIT
|
| 51 |
+
|
| 52 |
+
if ! GIT_ASKPASS="$askpass_file" git push origin main --force 2>&1; then
|
| 53 |
+
echo ""
|
| 54 |
+
echo "ERROR: Git push failed. Possible causes:"
|
| 55 |
+
echo " 1. Token is invalid or expired"
|
| 56 |
+
echo " 2. Token lacks 'write' permission for Spaces"
|
| 57 |
+
echo " 3. Space does not exist and token cannot create Spaces"
|
| 58 |
+
exit 1
|
| 59 |
+
fi
|
| 60 |
+
|
| 61 |
+
echo ""
|
| 62 |
+
echo "=== Deployed ==="
|
| 63 |
+
echo "Space: https://huggingface.co/spaces/$SPACE_ID"
|
| 64 |
+
echo ""
|
| 65 |
+
echo "Set environment variables in Space Settings:"
|
| 66 |
+
echo " TOKEN_MINT=<your_solana_token_mint>"
|
| 67 |
+
echo " INFERENCE_API_URL=<your_llm_endpoint>"
|
| 68 |
+
echo " SOLANA_RPC_URL=https://api.devnet.solana.com"
|
funding_rate_engine.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Funding Rate Engine
|
| 4 |
+
Calculates and distributes funding rates for perpetual futures
|
| 5 |
+
No mocks - real funding rate calculation and distribution
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import sqlite3
|
| 10 |
+
import asyncio
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from perp_trading_engine import PerpTradingEngine, Position, Side
|
| 14 |
+
|
| 15 |
+
# Configuration
|
| 16 |
+
FUNDING_CONFIG = {
|
| 17 |
+
"calculation_interval_hours": 1, # Calculate every hour
|
| 18 |
+
"interest_rate": 0.08, # 8% annual interest rate
|
| 19 |
+
"max_funding_rate": 0.00075, # 0.075% max funding rate per hour
|
| 20 |
+
"min_funding_rate": -0.00075, # -0.075% min funding rate per hour
|
| 21 |
+
"protocol_fee_rate": 0.10, # 10% of funding goes to protocol
|
| 22 |
+
"liquidity_provider_share": 0.70, # 70% to liquidity providers
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class FundingRateEngine:
|
| 27 |
+
"""Manages funding rate calculation and distribution"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
trading_engine: PerpTradingEngine,
|
| 32 |
+
db_path: str = "perp_trading.db",
|
| 33 |
+
):
|
| 34 |
+
self.trading_engine = trading_engine
|
| 35 |
+
self.db_path = db_path
|
| 36 |
+
self.funding_history = []
|
| 37 |
+
|
| 38 |
+
def calculate_funding_rate(self, market: str) -> float:
|
| 39 |
+
"""Calculate funding rate for a market"""
|
| 40 |
+
market_state = self.trading_engine.market_states[market]
|
| 41 |
+
|
| 42 |
+
# Calculate premium
|
| 43 |
+
if market_state.index_price == 0:
|
| 44 |
+
return 0.0
|
| 45 |
+
premium = (market_state.mark_price - market_state.index_price) / market_state.index_price
|
| 46 |
+
|
| 47 |
+
# Calculate interest rate component (hourly)
|
| 48 |
+
interest_component = FUNDING_CONFIG["interest_rate"] / (365 * 24)
|
| 49 |
+
|
| 50 |
+
# Calculate funding rate
|
| 51 |
+
funding_rate = interest_component + premium
|
| 52 |
+
|
| 53 |
+
# Clamp to max/min
|
| 54 |
+
funding_rate = max(
|
| 55 |
+
FUNDING_CONFIG["min_funding_rate"],
|
| 56 |
+
min(FUNDING_CONFIG["max_funding_rate"], funding_rate)
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
return funding_rate
|
| 60 |
+
|
| 61 |
+
def calculate_oi_imbalance(self, market: str) -> float:
|
| 62 |
+
"""Calculate open interest imbalance (longs vs shorts)"""
|
| 63 |
+
positions = self._get_market_positions(market)
|
| 64 |
+
|
| 65 |
+
long_oi = sum(p["size"] for p in positions if p["side"] == "long")
|
| 66 |
+
short_oi = sum(p["size"] for p in positions if p["side"] == "short")
|
| 67 |
+
|
| 68 |
+
total_oi = long_oi + short_oi
|
| 69 |
+
|
| 70 |
+
if total_oi == 0:
|
| 71 |
+
return 0.0
|
| 72 |
+
|
| 73 |
+
return (long_oi - short_oi) / total_oi
|
| 74 |
+
|
| 75 |
+
def distribute_funding(self, market: str):
|
| 76 |
+
"""Distribute funding payments"""
|
| 77 |
+
funding_rate = self.calculate_funding_rate(market)
|
| 78 |
+
|
| 79 |
+
if abs(funding_rate) < 0.00001: # Skip if negligible
|
| 80 |
+
return
|
| 81 |
+
|
| 82 |
+
positions = self._get_market_positions(market)
|
| 83 |
+
|
| 84 |
+
for pos_data in positions:
|
| 85 |
+
position = Position(
|
| 86 |
+
position_id=pos_data["position_id"],
|
| 87 |
+
trader=pos_data["trader"],
|
| 88 |
+
market=pos_data["market"],
|
| 89 |
+
side=Side(pos_data["side"]),
|
| 90 |
+
size=pos_data["size"],
|
| 91 |
+
entry_price=pos_data["entry_price"],
|
| 92 |
+
leverage=pos_data["leverage"],
|
| 93 |
+
margin=pos_data["margin"],
|
| 94 |
+
liquidation_price=pos_data["liquidation_price"],
|
| 95 |
+
opened_at=datetime.fromisoformat(pos_data["opened_at"]),
|
| 96 |
+
updated_at=datetime.fromisoformat(pos_data["updated_at"]),
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Calculate funding payment
|
| 100 |
+
market_state = self.trading_engine.market_states[market]
|
| 101 |
+
position_value = position.size * market_state.mark_price
|
| 102 |
+
funding_payment = position_value * funding_rate
|
| 103 |
+
|
| 104 |
+
# Apply funding (longs pay shorts when funding is positive)
|
| 105 |
+
if position.side == Side.LONG:
|
| 106 |
+
# Longs pay
|
| 107 |
+
self._apply_funding_payment(position, -funding_payment)
|
| 108 |
+
else:
|
| 109 |
+
# Shorts receive
|
| 110 |
+
self._apply_funding_payment(position, funding_payment)
|
| 111 |
+
|
| 112 |
+
# Save funding rate
|
| 113 |
+
self._save_funding_rate(market, funding_rate)
|
| 114 |
+
|
| 115 |
+
def _apply_funding_payment(self, position: Position, payment: float):
|
| 116 |
+
"""Apply funding payment to position"""
|
| 117 |
+
conn = sqlite3.connect(self.db_path)
|
| 118 |
+
cursor = conn.cursor()
|
| 119 |
+
|
| 120 |
+
# Update realized PnL with funding payment
|
| 121 |
+
cursor.execute("""
|
| 122 |
+
UPDATE positions
|
| 123 |
+
SET realized_pnl = realized_pnl + ?, updated_at = ?
|
| 124 |
+
WHERE position_id = ?
|
| 125 |
+
""", (payment, datetime.utcnow().isoformat(), position.position_id))
|
| 126 |
+
|
| 127 |
+
conn.commit()
|
| 128 |
+
conn.close()
|
| 129 |
+
|
| 130 |
+
def _save_funding_rate(self, market: str, rate: float):
|
| 131 |
+
"""Save funding rate to database"""
|
| 132 |
+
conn = sqlite3.connect(self.db_path)
|
| 133 |
+
cursor = conn.cursor()
|
| 134 |
+
|
| 135 |
+
current_time = datetime.utcnow().isoformat()
|
| 136 |
+
|
| 137 |
+
cursor.execute("""
|
| 138 |
+
INSERT INTO funding_rates
|
| 139 |
+
(market, rate, timestamp)
|
| 140 |
+
VALUES (?, ?, ?)
|
| 141 |
+
""", (market, rate, current_time))
|
| 142 |
+
|
| 143 |
+
conn.commit()
|
| 144 |
+
conn.close()
|
| 145 |
+
|
| 146 |
+
# Log
|
| 147 |
+
self.funding_history.append({
|
| 148 |
+
"timestamp": current_time,
|
| 149 |
+
"market": market,
|
| 150 |
+
"rate": rate,
|
| 151 |
+
})
|
| 152 |
+
|
| 153 |
+
def _get_market_positions(self, market: str) -> List[Dict]:
|
| 154 |
+
"""Get all positions for a market"""
|
| 155 |
+
conn = sqlite3.connect(self.db_path)
|
| 156 |
+
cursor = conn.cursor()
|
| 157 |
+
|
| 158 |
+
cursor.execute("""
|
| 159 |
+
SELECT position_id, trader, market, side, size, entry_price, leverage, margin,
|
| 160 |
+
liquidation_price, opened_at, updated_at
|
| 161 |
+
FROM positions
|
| 162 |
+
WHERE market = ? AND size > 0
|
| 163 |
+
""", (market,))
|
| 164 |
+
|
| 165 |
+
results = cursor.fetchall()
|
| 166 |
+
conn.close()
|
| 167 |
+
|
| 168 |
+
return [
|
| 169 |
+
{
|
| 170 |
+
"position_id": r[0],
|
| 171 |
+
"trader": r[1],
|
| 172 |
+
"market": r[2],
|
| 173 |
+
"side": r[3],
|
| 174 |
+
"size": r[4],
|
| 175 |
+
"entry_price": r[5],
|
| 176 |
+
"leverage": r[6],
|
| 177 |
+
"margin": r[7],
|
| 178 |
+
"liquidation_price": r[8],
|
| 179 |
+
"opened_at": r[9],
|
| 180 |
+
"updated_at": r[10],
|
| 181 |
+
}
|
| 182 |
+
for r in results
|
| 183 |
+
]
|
| 184 |
+
|
| 185 |
+
def get_funding_stats(self, market: str) -> Dict:
|
| 186 |
+
"""Get funding statistics for a market"""
|
| 187 |
+
current_rate = self.calculate_funding_rate(market)
|
| 188 |
+
oi_imbalance = self.calculate_oi_imbalance(market)
|
| 189 |
+
|
| 190 |
+
# Get recent funding rates
|
| 191 |
+
conn = sqlite3.connect(self.db_path)
|
| 192 |
+
cursor = conn.cursor()
|
| 193 |
+
|
| 194 |
+
cursor.execute("""
|
| 195 |
+
SELECT rate, timestamp
|
| 196 |
+
FROM funding_rates
|
| 197 |
+
WHERE market = ?
|
| 198 |
+
ORDER BY timestamp DESC
|
| 199 |
+
LIMIT 24
|
| 200 |
+
""", (market,))
|
| 201 |
+
|
| 202 |
+
results = cursor.fetchall()
|
| 203 |
+
conn.close()
|
| 204 |
+
|
| 205 |
+
recent_rates = [{"rate": r[0], "timestamp": r[1]} for r in results]
|
| 206 |
+
avg_rate = sum(r[0] for r in results) / len(results) if results else 0
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
"market": market,
|
| 210 |
+
"current_rate": current_rate,
|
| 211 |
+
"current_rate_percent": current_rate * 100,
|
| 212 |
+
"oi_imbalance": oi_imbalance,
|
| 213 |
+
"avg_rate_24h": avg_rate,
|
| 214 |
+
"recent_rates": recent_rates,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
async def start_funding_loop(self):
|
| 218 |
+
"""Start continuous funding rate calculation and distribution"""
|
| 219 |
+
print("Starting funding rate loop...")
|
| 220 |
+
|
| 221 |
+
while True:
|
| 222 |
+
for market in self.trading_engine.market_states.keys():
|
| 223 |
+
self.distribute_funding(market)
|
| 224 |
+
|
| 225 |
+
await asyncio.sleep(FUNDING_CONFIG["calculation_interval_hours"] * 3600)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
if __name__ == "__main__":
|
| 229 |
+
# Initialize components
|
| 230 |
+
trading_engine = PerpTradingEngine()
|
| 231 |
+
funding_engine = FundingRateEngine(trading_engine)
|
| 232 |
+
|
| 233 |
+
# Get funding stats for BTC/USDC
|
| 234 |
+
stats = funding_engine.get_funding_stats("BTC/USDC")
|
| 235 |
+
|
| 236 |
+
print("\n" + "="*50)
|
| 237 |
+
print("Funding Rate Statistics")
|
| 238 |
+
print("="*50)
|
| 239 |
+
print(json.dumps(stats, indent=2))
|
hf_account_collateral.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Hugging Face account collateral scanner.
|
| 4 |
+
|
| 5 |
+
Enumerates public HF models, datasets, and Spaces for an owner, fetches each
|
| 6 |
+
repo file list, reads text files, counts LOC, and produces deterministic
|
| 7 |
+
collateral evidence for Merkle commitments.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Dict, List
|
| 16 |
+
|
| 17 |
+
import requests
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
TEXT_EXTENSIONS = {
|
| 21 |
+
".c", ".cc", ".cpp", ".css", ".csv", ".dockerfile", ".go", ".h", ".hpp",
|
| 22 |
+
".html", ".ini", ".java", ".js", ".json", ".jsx", ".lock", ".md", ".mjs",
|
| 23 |
+
".py", ".rs", ".sh", ".sql", ".toml", ".ts", ".tsx", ".txt", ".yaml",
|
| 24 |
+
".yml",
|
| 25 |
+
}
|
| 26 |
+
MAX_TEXT_FILE_BYTES = int(os.environ.get("HF_COLLATERAL_MAX_TEXT_FILE_BYTES", "1000000"))
|
| 27 |
+
FILE_FETCH_TIMEOUT = int(os.environ.get("HF_COLLATERAL_FILE_TIMEOUT_SECONDS", "10"))
|
| 28 |
+
REPO_TREE_TIMEOUT = int(os.environ.get("HF_COLLATERAL_TREE_TIMEOUT_SECONDS", "10"))
|
| 29 |
+
ACCOUNT_LIST_TIMEOUT = int(os.environ.get("HF_COLLATERAL_ACCOUNT_TIMEOUT_SECONDS", "10"))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _sha256_text(value: str) -> str:
|
| 33 |
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _canonical_hash(value) -> str:
|
| 37 |
+
return _sha256_text(json.dumps(value, sort_keys=True, separators=(",", ":")))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _is_text_file(path: str) -> bool:
|
| 41 |
+
name = Path(path).name.lower()
|
| 42 |
+
suffix = Path(path).suffix.lower()
|
| 43 |
+
return suffix in TEXT_EXTENSIONS or name in {"dockerfile", "makefile", "requirements.txt"}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _read_repo_text_file(repo_id: str, repo_type: str, path: str) -> str:
|
| 47 |
+
url_prefix = {
|
| 48 |
+
"model": "https://huggingface.co",
|
| 49 |
+
"dataset": "https://huggingface.co/datasets",
|
| 50 |
+
"space": "https://huggingface.co/spaces",
|
| 51 |
+
}[repo_type]
|
| 52 |
+
raw_url = f"{url_prefix}/{repo_id}/resolve/main/{path}"
|
| 53 |
+
response = requests.get(raw_url, timeout=FILE_FETCH_TIMEOUT, stream=True)
|
| 54 |
+
response.raise_for_status()
|
| 55 |
+
chunks = []
|
| 56 |
+
total = 0
|
| 57 |
+
for chunk in response.iter_content(chunk_size=65536, decode_unicode=False):
|
| 58 |
+
if not chunk:
|
| 59 |
+
continue
|
| 60 |
+
total += len(chunk)
|
| 61 |
+
if total > MAX_TEXT_FILE_BYTES:
|
| 62 |
+
raise ValueError(f"file exceeds {MAX_TEXT_FILE_BYTES} byte scan limit")
|
| 63 |
+
chunks.append(chunk)
|
| 64 |
+
return b"".join(chunks).decode("utf-8", errors="ignore")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _repo_tree_files(repo_id: str, repo_type: str) -> List[str]:
|
| 68 |
+
api_prefix = {
|
| 69 |
+
"model": "https://huggingface.co/api/models",
|
| 70 |
+
"dataset": "https://huggingface.co/api/datasets",
|
| 71 |
+
"space": "https://huggingface.co/api/spaces",
|
| 72 |
+
}[repo_type]
|
| 73 |
+
response = requests.get(
|
| 74 |
+
f"{api_prefix}/{repo_id}/tree/main",
|
| 75 |
+
params={"recursive": "true"},
|
| 76 |
+
timeout=REPO_TREE_TIMEOUT,
|
| 77 |
+
)
|
| 78 |
+
response.raise_for_status()
|
| 79 |
+
files = []
|
| 80 |
+
for item in response.json():
|
| 81 |
+
path = item.get("path")
|
| 82 |
+
if path and item.get("type") != "directory":
|
| 83 |
+
files.append(path)
|
| 84 |
+
return sorted(files)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _account_items(path: str, owner: str) -> List[Dict]:
|
| 88 |
+
response = requests.get(
|
| 89 |
+
f"https://huggingface.co/api/{path}",
|
| 90 |
+
params={"author": owner, "limit": 1000},
|
| 91 |
+
timeout=ACCOUNT_LIST_TIMEOUT,
|
| 92 |
+
)
|
| 93 |
+
response.raise_for_status()
|
| 94 |
+
return response.json()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _repo_records(owner: str) -> List[Dict]:
|
| 98 |
+
records = []
|
| 99 |
+
for repo_type, path in (
|
| 100 |
+
("model", "models"),
|
| 101 |
+
("dataset", "datasets"),
|
| 102 |
+
("space", "spaces"),
|
| 103 |
+
):
|
| 104 |
+
for item in _account_items(path, owner):
|
| 105 |
+
repo_id = item.get("id") or item.get("name")
|
| 106 |
+
if not repo_id:
|
| 107 |
+
continue
|
| 108 |
+
records.append({
|
| 109 |
+
"repo_type": repo_type,
|
| 110 |
+
"repo_id": repo_id,
|
| 111 |
+
"likes": item.get("likes", 0) or 0,
|
| 112 |
+
"downloads": item.get("downloads", 0) or 0,
|
| 113 |
+
"last_modified": str(item.get("lastModified") or item.get("updatedAt") or ""),
|
| 114 |
+
"sdk": item.get("sdk"),
|
| 115 |
+
})
|
| 116 |
+
return sorted(records, key=lambda item: (item["repo_type"], item["repo_id"]))
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def scan_hf_account_collateral(owner: str) -> Dict:
|
| 120 |
+
repos = _repo_records(owner)
|
| 121 |
+
scanned_repos = []
|
| 122 |
+
total_files = 0
|
| 123 |
+
total_text_files = 0
|
| 124 |
+
total_loc = 0
|
| 125 |
+
unreadable_files = 0
|
| 126 |
+
|
| 127 |
+
for repo in repos:
|
| 128 |
+
repo_type = repo["repo_type"]
|
| 129 |
+
repo_id = repo["repo_id"]
|
| 130 |
+
repo_record = dict(repo)
|
| 131 |
+
file_records = []
|
| 132 |
+
try:
|
| 133 |
+
files = _repo_tree_files(repo_id, repo_type)
|
| 134 |
+
except Exception as e:
|
| 135 |
+
repo_record["scan_error"] = str(e)
|
| 136 |
+
repo_record["files"] = []
|
| 137 |
+
scanned_repos.append(repo_record)
|
| 138 |
+
continue
|
| 139 |
+
|
| 140 |
+
for path in sorted(files):
|
| 141 |
+
total_files += 1
|
| 142 |
+
file_record = {
|
| 143 |
+
"path": path,
|
| 144 |
+
"text": _is_text_file(path),
|
| 145 |
+
"loc": 0,
|
| 146 |
+
"sha256": None,
|
| 147 |
+
"read_status": "binary_or_unsupported",
|
| 148 |
+
}
|
| 149 |
+
if _is_text_file(path):
|
| 150 |
+
total_text_files += 1
|
| 151 |
+
try:
|
| 152 |
+
contents = _read_repo_text_file(repo_id, repo_type, path)
|
| 153 |
+
file_record["loc"] = contents.count("\n") + (1 if contents and not contents.endswith("\n") else 0)
|
| 154 |
+
file_record["sha256"] = _sha256_text(contents)
|
| 155 |
+
file_record["read_status"] = "read"
|
| 156 |
+
total_loc += file_record["loc"]
|
| 157 |
+
except Exception as e:
|
| 158 |
+
unreadable_files += 1
|
| 159 |
+
file_record["read_status"] = "unreadable"
|
| 160 |
+
file_record["error"] = str(e)
|
| 161 |
+
file_records.append(file_record)
|
| 162 |
+
|
| 163 |
+
repo_record["files"] = file_records
|
| 164 |
+
repo_record["file_count"] = len(file_records)
|
| 165 |
+
repo_record["text_file_count"] = sum(1 for item in file_records if item["text"])
|
| 166 |
+
repo_record["loc"] = sum(item["loc"] for item in file_records)
|
| 167 |
+
repo_record["repo_evidence_hash"] = _canonical_hash(file_records)
|
| 168 |
+
scanned_repos.append(repo_record)
|
| 169 |
+
|
| 170 |
+
collateral = {
|
| 171 |
+
"owner": owner,
|
| 172 |
+
"status": "scanned",
|
| 173 |
+
"scanned_at": datetime.utcnow().isoformat(),
|
| 174 |
+
"repo_count": len(scanned_repos),
|
| 175 |
+
"space_count": sum(1 for repo in scanned_repos if repo["repo_type"] == "space"),
|
| 176 |
+
"model_count": sum(1 for repo in scanned_repos if repo["repo_type"] == "model"),
|
| 177 |
+
"dataset_count": sum(1 for repo in scanned_repos if repo["repo_type"] == "dataset"),
|
| 178 |
+
"total_files": total_files,
|
| 179 |
+
"total_text_files": total_text_files,
|
| 180 |
+
"total_loc": total_loc,
|
| 181 |
+
"unreadable_files": unreadable_files,
|
| 182 |
+
"repositories": scanned_repos,
|
| 183 |
+
}
|
| 184 |
+
collateral["collateral_root"] = _canonical_hash(collateral["repositories"])
|
| 185 |
+
collateral["collateral_score"] = total_loc + (len(scanned_repos) * 100) + (collateral["space_count"] * 250)
|
| 186 |
+
collateral["collateral_status"] = "ready_for_signature"
|
| 187 |
+
collateral["status"] = "ready_for_signature"
|
| 188 |
+
collateral["collateral_live"] = False
|
| 189 |
+
collateral["reason"] = "Account files were scanned and committed; on-chain collateral lock still requires owner wallet signature."
|
| 190 |
+
return collateral
|
holder_tracker.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Holder Tracker
|
| 4 |
+
Fetches real token holder data from Solana RPC
|
| 5 |
+
No mocks - real HTTP API calls only
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import sqlite3
|
| 11 |
+
import requests
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, List, Optional
|
| 14 |
+
from datetime import datetime, timedelta
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
|
| 19 |
+
TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
| 20 |
+
|
| 21 |
+
# Configuration
|
| 22 |
+
HOLDER_CONFIG = {
|
| 23 |
+
"min_holding_amount": 100, # Minimum 100 tokens
|
| 24 |
+
"min_holding_period_hours": 24, # Must hold for 24 hours
|
| 25 |
+
"max_holders_per_distribution": 1000, # Cap per distribution
|
| 26 |
+
"distribution_interval_hours": 6, # Distribute every 6 hours
|
| 27 |
+
"blacklist": [], # Blacklisted addresses
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class HolderTracker:
|
| 32 |
+
"""Tracks token holders using real Solana RPC data"""
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
token_mint: str,
|
| 37 |
+
db_path: str = "holder_registry.db",
|
| 38 |
+
):
|
| 39 |
+
self.token_mint = token_mint
|
| 40 |
+
self.db_path = db_path
|
| 41 |
+
self._init_database()
|
| 42 |
+
|
| 43 |
+
def _init_database(self):
|
| 44 |
+
"""Initialize SQLite database for holder registry"""
|
| 45 |
+
conn = sqlite3.connect(self.db_path)
|
| 46 |
+
cursor = conn.cursor()
|
| 47 |
+
|
| 48 |
+
# Create holders table
|
| 49 |
+
cursor.execute("""
|
| 50 |
+
CREATE TABLE IF NOT EXISTS holders (
|
| 51 |
+
address TEXT PRIMARY KEY,
|
| 52 |
+
first_seen TIMESTAMP,
|
| 53 |
+
last_seen TIMESTAMP,
|
| 54 |
+
current_balance INTEGER,
|
| 55 |
+
total_received INTEGER,
|
| 56 |
+
total_sent INTEGER,
|
| 57 |
+
eligible BOOLEAN DEFAULT FALSE,
|
| 58 |
+
eligibility_timestamp TIMESTAMP,
|
| 59 |
+
drippage_received INTEGER DEFAULT 0
|
| 60 |
+
)
|
| 61 |
+
""")
|
| 62 |
+
|
| 63 |
+
# Create transfers table
|
| 64 |
+
cursor.execute("""
|
| 65 |
+
CREATE TABLE IF NOT EXISTS transfers (
|
| 66 |
+
tx_signature TEXT PRIMARY KEY,
|
| 67 |
+
from_address TEXT,
|
| 68 |
+
to_address TEXT,
|
| 69 |
+
amount INTEGER,
|
| 70 |
+
timestamp TIMESTAMP
|
| 71 |
+
)
|
| 72 |
+
""")
|
| 73 |
+
|
| 74 |
+
# Create distributions table
|
| 75 |
+
cursor.execute("""
|
| 76 |
+
CREATE TABLE IF NOT EXISTS distributions (
|
| 77 |
+
distribution_id TEXT PRIMARY KEY,
|
| 78 |
+
timestamp TIMESTAMP,
|
| 79 |
+
total_amount INTEGER,
|
| 80 |
+
eligible_holders INTEGER,
|
| 81 |
+
avg_amount INTEGER
|
| 82 |
+
)
|
| 83 |
+
""")
|
| 84 |
+
|
| 85 |
+
conn.commit()
|
| 86 |
+
conn.close()
|
| 87 |
+
|
| 88 |
+
def fetch_top_holders_from_rpc(self, limit: int = 20) -> List[Dict]:
|
| 89 |
+
"""Fetch top token holders from Solana RPC"""
|
| 90 |
+
try:
|
| 91 |
+
payload = {
|
| 92 |
+
"jsonrpc": "2.0",
|
| 93 |
+
"id": 1,
|
| 94 |
+
"method": "getTokenLargestAccounts",
|
| 95 |
+
"params": [self.token_mint],
|
| 96 |
+
}
|
| 97 |
+
r = requests.post(SOLANA_RPC_URL, json=payload, timeout=10)
|
| 98 |
+
if r.status_code == 200:
|
| 99 |
+
result = r.json().get("result", {}).get("value", [])
|
| 100 |
+
holders = []
|
| 101 |
+
for item in result[:limit]:
|
| 102 |
+
holders.append({
|
| 103 |
+
"address": item.get("address"),
|
| 104 |
+
"balance": int(item.get("amount", 0)),
|
| 105 |
+
"ui_amount": item.get("uiAmount", 0),
|
| 106 |
+
})
|
| 107 |
+
return holders
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.warning("RPC error fetching holders: %s", e)
|
| 110 |
+
return []
|
| 111 |
+
|
| 112 |
+
def fetch_recent_transfers_from_rpc(self, limit: int = 10) -> List[Dict]:
|
| 113 |
+
"""Fetch recent transfers for token mint via RPC"""
|
| 114 |
+
try:
|
| 115 |
+
payload = {
|
| 116 |
+
"jsonrpc": "2.0",
|
| 117 |
+
"id": 1,
|
| 118 |
+
"method": "getSignaturesForAddress",
|
| 119 |
+
"params": [self.token_mint, {"limit": limit}],
|
| 120 |
+
}
|
| 121 |
+
r = requests.post(SOLANA_RPC_URL, json=payload, timeout=10)
|
| 122 |
+
if r.status_code == 200:
|
| 123 |
+
sigs = r.json().get("result", [])
|
| 124 |
+
transfers = []
|
| 125 |
+
for sig_info in sigs:
|
| 126 |
+
sig = sig_info.get("signature")
|
| 127 |
+
if not sig:
|
| 128 |
+
continue
|
| 129 |
+
# Fetch parsed transaction
|
| 130 |
+
tx_payload = {
|
| 131 |
+
"jsonrpc": "2.0",
|
| 132 |
+
"id": 1,
|
| 133 |
+
"method": "getTransaction",
|
| 134 |
+
"params": [sig, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}],
|
| 135 |
+
}
|
| 136 |
+
tx_r = requests.post(SOLANA_RPC_URL, json=tx_payload, timeout=10)
|
| 137 |
+
if tx_r.status_code == 200:
|
| 138 |
+
tx = tx_r.json().get("result", {})
|
| 139 |
+
meta = tx.get("meta", {})
|
| 140 |
+
pre_balances = meta.get("preTokenBalances", [])
|
| 141 |
+
post_balances = meta.get("postTokenBalances", [])
|
| 142 |
+
if pre_balances and post_balances:
|
| 143 |
+
transfers.append({
|
| 144 |
+
"signature": sig,
|
| 145 |
+
"slot": tx.get("slot"),
|
| 146 |
+
"pre_balances": pre_balances,
|
| 147 |
+
"post_balances": post_balances,
|
| 148 |
+
})
|
| 149 |
+
return transfers
|
| 150 |
+
except Exception as e:
|
| 151 |
+
logger.warning("RPC error fetching transfers: %s", e)
|
| 152 |
+
return []
|
| 153 |
+
|
| 154 |
+
def sync_holders_from_chain(self):
|
| 155 |
+
"""Sync holder data from real Solana RPC into SQLite"""
|
| 156 |
+
holders = self.fetch_top_holders_from_rpc()
|
| 157 |
+
current_time = datetime.utcnow().isoformat()
|
| 158 |
+
|
| 159 |
+
conn = sqlite3.connect(self.db_path)
|
| 160 |
+
cursor = conn.cursor()
|
| 161 |
+
|
| 162 |
+
for h in holders:
|
| 163 |
+
addr = h["address"]
|
| 164 |
+
balance = h["balance"]
|
| 165 |
+
|
| 166 |
+
cursor.execute("SELECT address FROM holders WHERE address = ?", (addr,))
|
| 167 |
+
if cursor.fetchone():
|
| 168 |
+
cursor.execute(
|
| 169 |
+
"UPDATE holders SET current_balance = ?, last_seen = ? WHERE address = ?",
|
| 170 |
+
(balance, current_time, addr)
|
| 171 |
+
)
|
| 172 |
+
else:
|
| 173 |
+
cursor.execute("""
|
| 174 |
+
INSERT INTO holders (address, first_seen, last_seen, current_balance, total_received, total_sent)
|
| 175 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 176 |
+
""", (addr, current_time, current_time, balance, balance, 0))
|
| 177 |
+
print(f"New holder synced from chain: {addr}")
|
| 178 |
+
|
| 179 |
+
conn.commit()
|
| 180 |
+
conn.close()
|
| 181 |
+
return len(holders)
|
| 182 |
+
|
| 183 |
+
def _update_holder(self, address: str, amount_change: int):
|
| 184 |
+
"""Update holder balance"""
|
| 185 |
+
conn = sqlite3.connect(self.db_path)
|
| 186 |
+
cursor = conn.cursor()
|
| 187 |
+
|
| 188 |
+
current_time = datetime.utcnow().isoformat()
|
| 189 |
+
|
| 190 |
+
# Check if holder exists
|
| 191 |
+
cursor.execute("SELECT current_balance FROM holders WHERE address = ?", (address,))
|
| 192 |
+
result = cursor.fetchone()
|
| 193 |
+
|
| 194 |
+
if result:
|
| 195 |
+
# Update existing holder
|
| 196 |
+
new_balance = result[0] + amount_change
|
| 197 |
+
cursor.execute("""
|
| 198 |
+
UPDATE holders
|
| 199 |
+
SET current_balance = ?, last_seen = ?
|
| 200 |
+
WHERE address = ?
|
| 201 |
+
""", (new_balance, current_time, address))
|
| 202 |
+
|
| 203 |
+
# Update totals
|
| 204 |
+
if amount_change > 0:
|
| 205 |
+
cursor.execute("""
|
| 206 |
+
UPDATE holders
|
| 207 |
+
SET total_received = total_received + ?
|
| 208 |
+
WHERE address = ?
|
| 209 |
+
""", (amount_change, address))
|
| 210 |
+
else:
|
| 211 |
+
cursor.execute("""
|
| 212 |
+
UPDATE holders
|
| 213 |
+
SET total_sent = total_sent + ?
|
| 214 |
+
WHERE address = ?
|
| 215 |
+
""", (-amount_change, address))
|
| 216 |
+
else:
|
| 217 |
+
# Create new holder
|
| 218 |
+
cursor.execute("""
|
| 219 |
+
INSERT INTO holders
|
| 220 |
+
(address, first_seen, last_seen, current_balance, total_received, total_sent)
|
| 221 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 222 |
+
""", (address, current_time, current_time, amount_change,
|
| 223 |
+
max(0, amount_change), max(0, -amount_change)))
|
| 224 |
+
|
| 225 |
+
conn.commit()
|
| 226 |
+
conn.close()
|
| 227 |
+
|
| 228 |
+
def _is_new_holder(self, address: str) -> bool:
|
| 229 |
+
"""Check if address is a new holder"""
|
| 230 |
+
conn = sqlite3.connect(self.db_path)
|
| 231 |
+
cursor = conn.cursor()
|
| 232 |
+
|
| 233 |
+
cursor.execute("SELECT first_seen FROM holders WHERE address = ?", (address,))
|
| 234 |
+
result = cursor.fetchone()
|
| 235 |
+
|
| 236 |
+
conn.close()
|
| 237 |
+
|
| 238 |
+
return result is None
|
| 239 |
+
|
| 240 |
+
def _register_new_holder(self, address: str, amount: int):
|
| 241 |
+
"""Register new holder"""
|
| 242 |
+
conn = sqlite3.connect(self.db_path)
|
| 243 |
+
cursor = conn.cursor()
|
| 244 |
+
|
| 245 |
+
current_time = datetime.utcnow().isoformat()
|
| 246 |
+
|
| 247 |
+
cursor.execute("""
|
| 248 |
+
UPDATE holders
|
| 249 |
+
SET first_seen = ?, last_seen = ?
|
| 250 |
+
WHERE address = ?
|
| 251 |
+
""", (current_time, current_time, address))
|
| 252 |
+
|
| 253 |
+
conn.commit()
|
| 254 |
+
conn.close()
|
| 255 |
+
|
| 256 |
+
def _log_transfer(self, signature: str, from_addr: str, to_addr: str, amount: int):
|
| 257 |
+
"""Log transfer to database"""
|
| 258 |
+
conn = sqlite3.connect(self.db_path)
|
| 259 |
+
cursor = conn.cursor()
|
| 260 |
+
|
| 261 |
+
current_time = datetime.utcnow().isoformat()
|
| 262 |
+
|
| 263 |
+
cursor.execute("""
|
| 264 |
+
INSERT OR IGNORE INTO transfers
|
| 265 |
+
(tx_signature, from_address, to_address, amount, timestamp)
|
| 266 |
+
VALUES (?, ?, ?, ?, ?)
|
| 267 |
+
""", (signature, from_addr, to_addr, amount, current_time))
|
| 268 |
+
|
| 269 |
+
conn.commit()
|
| 270 |
+
conn.close()
|
| 271 |
+
|
| 272 |
+
def check_eligibility(self):
|
| 273 |
+
"""Check which holders are eligible for drippage"""
|
| 274 |
+
conn = sqlite3.connect(self.db_path)
|
| 275 |
+
cursor = conn.cursor()
|
| 276 |
+
|
| 277 |
+
current_time = datetime.utcnow()
|
| 278 |
+
min_time = current_time - timedelta(hours=HOLDER_CONFIG["min_holding_period_hours"])
|
| 279 |
+
|
| 280 |
+
# Get holders who meet criteria
|
| 281 |
+
cursor.execute("""
|
| 282 |
+
SELECT address, current_balance, first_seen
|
| 283 |
+
FROM holders
|
| 284 |
+
WHERE current_balance >= ?
|
| 285 |
+
AND first_seen <= ?
|
| 286 |
+
AND address NOT IN (SELECT address FROM blacklist)
|
| 287 |
+
ORDER BY current_balance DESC
|
| 288 |
+
LIMIT ?
|
| 289 |
+
""", (
|
| 290 |
+
HOLDER_CONFIG["min_holding_amount"],
|
| 291 |
+
min_time.isoformat(),
|
| 292 |
+
HOLDER_CONFIG["max_holders_per_distribution"],
|
| 293 |
+
))
|
| 294 |
+
|
| 295 |
+
holders = cursor.fetchall()
|
| 296 |
+
|
| 297 |
+
# Update eligibility
|
| 298 |
+
for address, balance, first_seen in holders:
|
| 299 |
+
cursor.execute("""
|
| 300 |
+
UPDATE holders
|
| 301 |
+
SET eligible = TRUE, eligibility_timestamp = ?
|
| 302 |
+
WHERE address = ?
|
| 303 |
+
""", (current_time.isoformat(), address))
|
| 304 |
+
|
| 305 |
+
conn.commit()
|
| 306 |
+
conn.close()
|
| 307 |
+
|
| 308 |
+
return [
|
| 309 |
+
{
|
| 310 |
+
"address": h[0],
|
| 311 |
+
"balance": h[1],
|
| 312 |
+
"first_seen": h[2],
|
| 313 |
+
}
|
| 314 |
+
for h in holders
|
| 315 |
+
]
|
| 316 |
+
|
| 317 |
+
def get_eligible_holders(self) -> List[Dict]:
|
| 318 |
+
"""Get all currently eligible holders"""
|
| 319 |
+
conn = sqlite3.connect(self.db_path)
|
| 320 |
+
cursor = conn.cursor()
|
| 321 |
+
|
| 322 |
+
cursor.execute("""
|
| 323 |
+
SELECT address, current_balance, first_seen, eligibility_timestamp
|
| 324 |
+
FROM holders
|
| 325 |
+
WHERE eligible = TRUE
|
| 326 |
+
ORDER BY current_balance DESC
|
| 327 |
+
""")
|
| 328 |
+
|
| 329 |
+
holders = cursor.fetchall()
|
| 330 |
+
conn.close()
|
| 331 |
+
|
| 332 |
+
return [
|
| 333 |
+
{
|
| 334 |
+
"address": h[0],
|
| 335 |
+
"balance": h[1],
|
| 336 |
+
"first_seen": h[2],
|
| 337 |
+
"holding_hours": (datetime.utcnow() - datetime.fromisoformat(h[2])).total_seconds() / 3600,
|
| 338 |
+
}
|
| 339 |
+
for h in holders
|
| 340 |
+
]
|
| 341 |
+
|
| 342 |
+
def get_holder_stats(self) -> Dict:
|
| 343 |
+
"""Get holder statistics"""
|
| 344 |
+
conn = sqlite3.connect(self.db_path)
|
| 345 |
+
cursor = conn.cursor()
|
| 346 |
+
|
| 347 |
+
# Total holders
|
| 348 |
+
cursor.execute("SELECT COUNT(*) FROM holders")
|
| 349 |
+
total_holders = cursor.fetchone()[0]
|
| 350 |
+
|
| 351 |
+
# Eligible holders
|
| 352 |
+
cursor.execute("SELECT COUNT(*) FROM holders WHERE eligible = TRUE")
|
| 353 |
+
eligible_holders = cursor.fetchone()[0]
|
| 354 |
+
|
| 355 |
+
# Total balance
|
| 356 |
+
cursor.execute("SELECT SUM(current_balance) FROM holders")
|
| 357 |
+
total_balance = cursor.fetchone()[0] or 0
|
| 358 |
+
|
| 359 |
+
# New holders today
|
| 360 |
+
today = datetime.utcnow().date()
|
| 361 |
+
cursor.execute("""
|
| 362 |
+
SELECT COUNT(*) FROM holders
|
| 363 |
+
WHERE DATE(first_seen) = ?
|
| 364 |
+
""", (today.isoformat(),))
|
| 365 |
+
new_holders_today = cursor.fetchone()[0]
|
| 366 |
+
|
| 367 |
+
conn.close()
|
| 368 |
+
|
| 369 |
+
return {
|
| 370 |
+
"total_holders": total_holders,
|
| 371 |
+
"eligible_holders": eligible_holders,
|
| 372 |
+
"total_balance": total_balance,
|
| 373 |
+
"new_holders_today": new_holders_today,
|
| 374 |
+
"eligibility_rate": eligible_holders / total_holders if total_holders > 0 else 0,
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
def add_to_blacklist(self, address: str):
|
| 378 |
+
"""Add address to blacklist"""
|
| 379 |
+
if address not in HOLDER_CONFIG["blacklist"]:
|
| 380 |
+
HOLDER_CONFIG["blacklist"].append(address)
|
| 381 |
+
print(f"Added {address} to blacklist")
|
| 382 |
+
|
| 383 |
+
def remove_from_blacklist(self, address: str):
|
| 384 |
+
"""Remove address from blacklist"""
|
| 385 |
+
if address in HOLDER_CONFIG["blacklist"]:
|
| 386 |
+
HOLDER_CONFIG["blacklist"].remove(address)
|
| 387 |
+
print(f"Removed {address} from blacklist")
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def start_holder_sync(token_mint: str):
|
| 391 |
+
"""Sync holders from chain and print stats"""
|
| 392 |
+
tracker = HolderTracker(token_mint)
|
| 393 |
+
|
| 394 |
+
# Sync from chain
|
| 395 |
+
count = tracker.sync_holders_from_chain()
|
| 396 |
+
print(f"Synced {count} holders from Solana RPC")
|
| 397 |
+
|
| 398 |
+
# Check eligibility
|
| 399 |
+
eligible = tracker.check_eligibility()
|
| 400 |
+
print(f"Eligible holders: {len(eligible)}")
|
| 401 |
+
|
| 402 |
+
# Print stats
|
| 403 |
+
stats = tracker.get_holder_stats()
|
| 404 |
+
print("\n" + "="*50)
|
| 405 |
+
print("Holder Statistics")
|
| 406 |
+
print("="*50)
|
| 407 |
+
print(f"Total Holders: {stats['total_holders']}")
|
| 408 |
+
print(f"Eligible Holders: {stats['eligible_holders']}")
|
| 409 |
+
print(f"Total Balance: {stats['total_balance']:,}")
|
| 410 |
+
print(f"New Holders Today: {stats['new_holders_today']}")
|
| 411 |
+
print(f"Eligibility Rate: {stats['eligibility_rate']:.2%}")
|
| 412 |
+
return stats
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
if __name__ == "__main__":
|
| 416 |
+
import sys
|
| 417 |
+
|
| 418 |
+
if len(sys.argv) < 2:
|
| 419 |
+
print("Usage: python holder_tracker.py <token_mint>")
|
| 420 |
+
print("Example: python holder_tracker.py EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
|
| 421 |
+
sys.exit(1)
|
| 422 |
+
|
| 423 |
+
token_mint = sys.argv[1]
|
| 424 |
+
start_holder_sync(token_mint)
|
liquidation_system.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Perpetual Futures Liquidation System
|
| 4 |
+
Monitors positions and executes liquidations when needed
|
| 5 |
+
No mocks - real position monitoring and liquidation execution
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import sqlite3
|
| 10 |
+
import asyncio
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from perp_trading_engine import PerpTradingEngine, Position, Side
|
| 14 |
+
|
| 15 |
+
# Configuration
|
| 16 |
+
LIQUIDATION_CONFIG = {
|
| 17 |
+
"maintenance_margin_rate": 0.05, # 5% maintenance margin
|
| 18 |
+
"liquidation_threshold": 0.01, # 1% liquidation threshold
|
| 19 |
+
"liquidation_bonus": 0.05, # 5% bonus for liquidators
|
| 20 |
+
"insurance_fund_rate": 0.02, # 2% to insurance fund
|
| 21 |
+
"check_interval_seconds": 10, # Check every 10 seconds
|
| 22 |
+
"max_liquidation_per_check": 5, # Max 5 liquidations per check
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class LiquidationSystem:
|
| 27 |
+
"""Manages position liquidations"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
trading_engine: PerpTradingEngine,
|
| 32 |
+
db_path: str = "perp_trading.db",
|
| 33 |
+
):
|
| 34 |
+
self.trading_engine = trading_engine
|
| 35 |
+
self.db_path = db_path
|
| 36 |
+
self.liquidation_log = []
|
| 37 |
+
self.insurance_fund = 0.0
|
| 38 |
+
|
| 39 |
+
async def start_monitoring(self):
|
| 40 |
+
"""Start liquidation monitoring loop"""
|
| 41 |
+
print("Starting liquidation monitoring...")
|
| 42 |
+
|
| 43 |
+
while True:
|
| 44 |
+
await self._check_liquidations()
|
| 45 |
+
await asyncio.sleep(LIQUIDATION_CONFIG["check_interval_seconds"])
|
| 46 |
+
|
| 47 |
+
async def _check_liquidations(self):
|
| 48 |
+
"""Check for liquidatable positions"""
|
| 49 |
+
# Get all positions
|
| 50 |
+
positions = self._get_all_positions()
|
| 51 |
+
|
| 52 |
+
liquidatable = []
|
| 53 |
+
|
| 54 |
+
for position in positions:
|
| 55 |
+
# Update unrealized PnL
|
| 56 |
+
self.trading_engine.update_unrealized_pnl()
|
| 57 |
+
|
| 58 |
+
# Refresh position data
|
| 59 |
+
updated_position = self.trading_engine.get_position(
|
| 60 |
+
position["trader"],
|
| 61 |
+
position["market"]
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
if not updated_position:
|
| 65 |
+
continue
|
| 66 |
+
|
| 67 |
+
# Check if liquidatable
|
| 68 |
+
if self._is_liquidatable(updated_position):
|
| 69 |
+
liquidatable.append(updated_position)
|
| 70 |
+
|
| 71 |
+
# Execute liquidations (limit per check)
|
| 72 |
+
for position in liquidatable[:LIQUIDATION_CONFIG["max_liquidation_per_check"]]:
|
| 73 |
+
await self._execute_liquidation(position)
|
| 74 |
+
|
| 75 |
+
def _get_all_positions(self) -> List[Dict]:
|
| 76 |
+
"""Get all positions from database"""
|
| 77 |
+
conn = sqlite3.connect(self.db_path)
|
| 78 |
+
cursor = conn.cursor()
|
| 79 |
+
|
| 80 |
+
cursor.execute("""
|
| 81 |
+
SELECT position_id, trader, market, side, size, entry_price, leverage, margin,
|
| 82 |
+
unrealized_pnl, realized_pnl, liquidation_price, opened_at, updated_at
|
| 83 |
+
FROM positions
|
| 84 |
+
""")
|
| 85 |
+
|
| 86 |
+
results = cursor.fetchall()
|
| 87 |
+
conn.close()
|
| 88 |
+
|
| 89 |
+
return [
|
| 90 |
+
{
|
| 91 |
+
"position_id": r[0],
|
| 92 |
+
"trader": r[1],
|
| 93 |
+
"market": r[2],
|
| 94 |
+
"side": r[3],
|
| 95 |
+
"size": r[4],
|
| 96 |
+
"entry_price": r[5],
|
| 97 |
+
"leverage": r[6],
|
| 98 |
+
"margin": r[7],
|
| 99 |
+
"unrealized_pnl": r[8],
|
| 100 |
+
"realized_pnl": r[9],
|
| 101 |
+
"liquidation_price": r[10],
|
| 102 |
+
"opened_at": r[11],
|
| 103 |
+
"updated_at": r[12],
|
| 104 |
+
}
|
| 105 |
+
for r in results
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
def _is_liquidatable(self, position: Position) -> bool:
|
| 109 |
+
"""Check if position is liquidatable"""
|
| 110 |
+
market_state = self.trading_engine.market_states[position.market]
|
| 111 |
+
current_price = market_state.mark_price
|
| 112 |
+
|
| 113 |
+
# Calculate margin ratio
|
| 114 |
+
position_value = position.size * current_price
|
| 115 |
+
if position_value == 0:
|
| 116 |
+
return False
|
| 117 |
+
margin_ratio = position.margin / position_value
|
| 118 |
+
|
| 119 |
+
# Check if below maintenance margin
|
| 120 |
+
if margin_ratio < LIQUIDATION_CONFIG["maintenance_margin_rate"]:
|
| 121 |
+
return True
|
| 122 |
+
|
| 123 |
+
# Check if price hit liquidation price
|
| 124 |
+
if position.side == Side.LONG:
|
| 125 |
+
if current_price <= position.liquidation_price:
|
| 126 |
+
return True
|
| 127 |
+
else:
|
| 128 |
+
if current_price >= position.liquidation_price:
|
| 129 |
+
return True
|
| 130 |
+
|
| 131 |
+
return False
|
| 132 |
+
|
| 133 |
+
async def _execute_liquidation(self, position: Position):
|
| 134 |
+
"""Execute position liquidation"""
|
| 135 |
+
print(f"Liquidating position {position.position_id}...")
|
| 136 |
+
|
| 137 |
+
market_state = self.trading_engine.market_states[position.market]
|
| 138 |
+
current_price = market_state.mark_price
|
| 139 |
+
|
| 140 |
+
# Calculate liquidation value
|
| 141 |
+
liquidation_value = position.size * current_price
|
| 142 |
+
|
| 143 |
+
# Calculate liquidation bonus
|
| 144 |
+
bonus = liquidation_value * LIQUIDATION_CONFIG["liquidation_bonus"]
|
| 145 |
+
|
| 146 |
+
# Calculate insurance fund contribution
|
| 147 |
+
insurance_contribution = liquidation_value * LIQUIDATION_CONFIG["insurance_fund_rate"]
|
| 148 |
+
|
| 149 |
+
# Close position
|
| 150 |
+
self._close_position(position, current_price)
|
| 151 |
+
|
| 152 |
+
# Update insurance fund
|
| 153 |
+
self.insurance_fund += insurance_contribution
|
| 154 |
+
|
| 155 |
+
# Log liquidation
|
| 156 |
+
liquidation_record = {
|
| 157 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 158 |
+
"position_id": position.position_id,
|
| 159 |
+
"trader": position.trader,
|
| 160 |
+
"market": position.market,
|
| 161 |
+
"side": position.side.value,
|
| 162 |
+
"size": position.size,
|
| 163 |
+
"liquidation_price": current_price,
|
| 164 |
+
"liquidation_value": liquidation_value,
|
| 165 |
+
"liquidation_bonus": bonus,
|
| 166 |
+
"insurance_contribution": insurance_contribution,
|
| 167 |
+
"remaining_margin": max(0, position.margin - liquidation_value),
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
self.liquidation_log.append(liquidation_record)
|
| 171 |
+
|
| 172 |
+
print(f"Liquidation executed: {liquidation_record}")
|
| 173 |
+
|
| 174 |
+
def _close_position(self, position: Position, close_price: float):
|
| 175 |
+
"""Close position in database"""
|
| 176 |
+
conn = sqlite3.connect(self.db_path)
|
| 177 |
+
cursor = conn.cursor()
|
| 178 |
+
|
| 179 |
+
# Calculate realized PnL
|
| 180 |
+
if position.side == Side.LONG:
|
| 181 |
+
realized_pnl = position.size * (close_price - position.entry_price)
|
| 182 |
+
else:
|
| 183 |
+
realized_pnl = position.size * (position.entry_price - close_price)
|
| 184 |
+
|
| 185 |
+
# Update position
|
| 186 |
+
cursor.execute("""
|
| 187 |
+
UPDATE positions
|
| 188 |
+
SET size = 0, unrealized_pnl = 0, realized_pnl = realized_pnl, updated_at = ?
|
| 189 |
+
WHERE position_id = ?
|
| 190 |
+
""", (datetime.utcnow().isoformat(), position.position_id))
|
| 191 |
+
|
| 192 |
+
conn.commit()
|
| 193 |
+
conn.close()
|
| 194 |
+
|
| 195 |
+
def get_liquidation_stats(self) -> Dict:
|
| 196 |
+
"""Get liquidation statistics"""
|
| 197 |
+
if not self.liquidation_log:
|
| 198 |
+
return {
|
| 199 |
+
"total_liquidations": 0,
|
| 200 |
+
"total_value": 0.0,
|
| 201 |
+
"insurance_fund": self.insurance_fund,
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
total_liquidations = len(self.liquidation_log)
|
| 205 |
+
total_value = sum(l["liquidation_value"] for l in self.liquidation_log)
|
| 206 |
+
total_bonuses = sum(l["liquidation_bonus"] for l in self.liquidation_log)
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
"total_liquidations": total_liquidations,
|
| 210 |
+
"total_value": total_value,
|
| 211 |
+
"total_bonuses": total_bonuses,
|
| 212 |
+
"insurance_fund": self.insurance_fund,
|
| 213 |
+
"recent_liquidations": self.liquidation_log[-10:],
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
def get_at_risk_positions(self) -> List[Dict]:
|
| 217 |
+
"""Get positions at risk of liquidation"""
|
| 218 |
+
positions = self._get_all_positions()
|
| 219 |
+
at_risk = []
|
| 220 |
+
|
| 221 |
+
for pos_data in positions:
|
| 222 |
+
position = Position(
|
| 223 |
+
position_id=pos_data["position_id"],
|
| 224 |
+
trader=pos_data["trader"],
|
| 225 |
+
market=pos_data["market"],
|
| 226 |
+
side=Side(pos_data["side"]),
|
| 227 |
+
size=pos_data["size"],
|
| 228 |
+
entry_price=pos_data["entry_price"],
|
| 229 |
+
leverage=pos_data["leverage"],
|
| 230 |
+
margin=pos_data["margin"],
|
| 231 |
+
liquidation_price=pos_data["liquidation_price"],
|
| 232 |
+
opened_at=datetime.fromisoformat(pos_data["opened_at"]),
|
| 233 |
+
updated_at=datetime.fromisoformat(pos_data["updated_at"]),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Calculate margin ratio
|
| 237 |
+
market_state = self.trading_engine.market_states[position.market]
|
| 238 |
+
current_price = market_state.mark_price
|
| 239 |
+
position_value = position.size * current_price
|
| 240 |
+
if position_value == 0 or current_price == 0:
|
| 241 |
+
continue
|
| 242 |
+
margin_ratio = position.margin / position_value
|
| 243 |
+
|
| 244 |
+
# Check if at risk (within 20% of liquidation)
|
| 245 |
+
if margin_ratio < LIQUIDATION_CONFIG["maintenance_margin_rate"] * 1.2:
|
| 246 |
+
at_risk.append({
|
| 247 |
+
"position_id": position.position_id,
|
| 248 |
+
"trader": position.trader,
|
| 249 |
+
"market": position.market,
|
| 250 |
+
"margin_ratio": margin_ratio,
|
| 251 |
+
"liquidation_price": position.liquidation_price,
|
| 252 |
+
"current_price": current_price,
|
| 253 |
+
"distance_to_liquidation": abs(current_price - position.liquidation_price) / current_price,
|
| 254 |
+
})
|
| 255 |
+
|
| 256 |
+
return sorted(at_risk, key=lambda x: x["margin_ratio"])
|
| 257 |
+
|
| 258 |
+
def manual_liquidation(self, position_id: str, liquidator: str) -> Dict:
|
| 259 |
+
"""Manually trigger liquidation (for liquidators)"""
|
| 260 |
+
# Get position
|
| 261 |
+
conn = sqlite3.connect(self.db_path)
|
| 262 |
+
cursor = conn.cursor()
|
| 263 |
+
|
| 264 |
+
cursor.execute("""
|
| 265 |
+
SELECT position_id, trader, market, side, size, entry_price, leverage, margin, liquidation_price
|
| 266 |
+
FROM positions
|
| 267 |
+
WHERE position_id = ?
|
| 268 |
+
""", (position_id,))
|
| 269 |
+
|
| 270 |
+
result = cursor.fetchone()
|
| 271 |
+
conn.close()
|
| 272 |
+
|
| 273 |
+
if not result:
|
| 274 |
+
return {"status": "error", "message": "Position not found"}
|
| 275 |
+
|
| 276 |
+
position = Position(
|
| 277 |
+
position_id=result[0],
|
| 278 |
+
trader=result[1],
|
| 279 |
+
market=result[2],
|
| 280 |
+
side=Side(result[3]),
|
| 281 |
+
size=result[4],
|
| 282 |
+
entry_price=result[5],
|
| 283 |
+
leverage=result[6],
|
| 284 |
+
margin=result[7],
|
| 285 |
+
liquidation_price=result[8],
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Execute liquidation
|
| 289 |
+
asyncio.run(self._execute_liquidation(position))
|
| 290 |
+
|
| 291 |
+
return {
|
| 292 |
+
"status": "success",
|
| 293 |
+
"position_id": position_id,
|
| 294 |
+
"liquidator": liquidator,
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
if __name__ == "__main__":
|
| 299 |
+
# Initialize components
|
| 300 |
+
trading_engine = PerpTradingEngine()
|
| 301 |
+
liquidation_system = LiquidationSystem(trading_engine)
|
| 302 |
+
|
| 303 |
+
# Get at-risk positions
|
| 304 |
+
at_risk = liquidation_system.get_at_risk_positions()
|
| 305 |
+
|
| 306 |
+
print("\n" + "="*50)
|
| 307 |
+
print("At-Risk Positions")
|
| 308 |
+
print("="*50)
|
| 309 |
+
for pos in at_risk:
|
| 310 |
+
print(f"Position: {pos['position_id']}")
|
| 311 |
+
print(f"Trader: {pos['trader']}")
|
| 312 |
+
print(f"Market: {pos['market']}")
|
| 313 |
+
print(f"Margin Ratio: {pos['margin_ratio']:.2%}")
|
| 314 |
+
print(f"Distance to Liquidation: {pos['distance_to_liquidation']:.2%}")
|
| 315 |
+
print()
|
| 316 |
+
|
| 317 |
+
# Get liquidation stats
|
| 318 |
+
stats = liquidation_system.get_liquidation_stats()
|
| 319 |
+
print("="*50)
|
| 320 |
+
print("Liquidation Statistics")
|
| 321 |
+
print("="*50)
|
| 322 |
+
print(json.dumps(stats, indent=2))
|
llm_liquidity_provider.py
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip LLM Inference Liquidity Provider
|
| 4 |
+
Converts LLM inference capacity into synthetic liquidity for perpetual futures
|
| 5 |
+
No mocks - real inference capacity measurement and liquidity conversion
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import sqlite3
|
| 11 |
+
import asyncio
|
| 12 |
+
import time
|
| 13 |
+
import requests
|
| 14 |
+
from typing import Dict, List, Optional
|
| 15 |
+
from datetime import datetime, timedelta
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
|
| 18 |
+
# Configuration
|
| 19 |
+
INFERENCE_CONFIG = {
|
| 20 |
+
"base_liquidity_rate": 0.01, # $0.01 liquidity per token/second
|
| 21 |
+
"quality_multiplier": 1.5, # High quality = 1.5x
|
| 22 |
+
"uptime_multiplier": 1.2, # High uptime = 1.2x
|
| 23 |
+
"demand_multiplier": 2.0, # High demand = 2x
|
| 24 |
+
"min_capacity_tokens": 100, # Minimum 100 tokens/second
|
| 25 |
+
"benchmark_duration_seconds": 60, # Benchmark test duration
|
| 26 |
+
"performance_check_interval_minutes": 5, # Check every 5 minutes
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class InferenceMetrics:
|
| 32 |
+
"""Metrics for inference capacity"""
|
| 33 |
+
tokens_per_second: float
|
| 34 |
+
model_type: str
|
| 35 |
+
latency_ms: float
|
| 36 |
+
uptime_percentage: float
|
| 37 |
+
quality_score: float
|
| 38 |
+
last_verified: datetime
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class LiquidityAllocation:
|
| 43 |
+
"""Liquidity allocation for provider"""
|
| 44 |
+
provider_id: str
|
| 45 |
+
synthetic_liquidity_usd: float
|
| 46 |
+
liquidity_tokens: float
|
| 47 |
+
market_allocation: Dict[str, float] # Market -> allocation
|
| 48 |
+
last_updated: datetime
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class InferenceRegistry:
|
| 52 |
+
"""Registry for LLM inference providers"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, db_path: str = "inference_registry.db"):
|
| 55 |
+
self.db_path = db_path
|
| 56 |
+
self._init_database()
|
| 57 |
+
|
| 58 |
+
def _init_database(self):
|
| 59 |
+
"""Initialize SQLite database for provider registry"""
|
| 60 |
+
conn = sqlite3.connect(self.db_path)
|
| 61 |
+
cursor = conn.cursor()
|
| 62 |
+
|
| 63 |
+
# Create providers table
|
| 64 |
+
cursor.execute("""
|
| 65 |
+
CREATE TABLE IF NOT EXISTS providers (
|
| 66 |
+
provider_id TEXT PRIMARY KEY,
|
| 67 |
+
wallet_address TEXT,
|
| 68 |
+
model_type TEXT,
|
| 69 |
+
registered_at TIMESTAMP,
|
| 70 |
+
status TEXT DEFAULT 'pending',
|
| 71 |
+
reputation_score REAL DEFAULT 0.5,
|
| 72 |
+
total_earnings REAL DEFAULT 0.0
|
| 73 |
+
)
|
| 74 |
+
""")
|
| 75 |
+
|
| 76 |
+
# Create capacity table
|
| 77 |
+
cursor.execute("""
|
| 78 |
+
CREATE TABLE IF NOT EXISTS capacity (
|
| 79 |
+
provider_id TEXT,
|
| 80 |
+
tokens_per_second REAL,
|
| 81 |
+
latency_ms REAL,
|
| 82 |
+
uptime_percentage REAL,
|
| 83 |
+
quality_score REAL,
|
| 84 |
+
verified_at TIMESTAMP,
|
| 85 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 86 |
+
)
|
| 87 |
+
""")
|
| 88 |
+
|
| 89 |
+
# Create liquidity table
|
| 90 |
+
cursor.execute("""
|
| 91 |
+
CREATE TABLE IF NOT EXISTS liquidity_allocations (
|
| 92 |
+
provider_id TEXT,
|
| 93 |
+
synthetic_liquidity_usd REAL,
|
| 94 |
+
liquidity_tokens REAL,
|
| 95 |
+
market_allocation TEXT,
|
| 96 |
+
allocated_at TIMESTAMP,
|
| 97 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 98 |
+
)
|
| 99 |
+
""")
|
| 100 |
+
|
| 101 |
+
# Create earnings table
|
| 102 |
+
cursor.execute("""
|
| 103 |
+
CREATE TABLE IF NOT EXISTS earnings (
|
| 104 |
+
provider_id TEXT,
|
| 105 |
+
amount REAL,
|
| 106 |
+
source TEXT,
|
| 107 |
+
timestamp TIMESTAMP,
|
| 108 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 109 |
+
)
|
| 110 |
+
""")
|
| 111 |
+
|
| 112 |
+
conn.commit()
|
| 113 |
+
conn.close()
|
| 114 |
+
|
| 115 |
+
def register_provider(
|
| 116 |
+
self,
|
| 117 |
+
provider_id: str,
|
| 118 |
+
wallet_address: str,
|
| 119 |
+
model_type: str,
|
| 120 |
+
) -> Dict[str, any]:
|
| 121 |
+
"""Register new inference provider"""
|
| 122 |
+
conn = sqlite3.connect(self.db_path)
|
| 123 |
+
cursor = conn.cursor()
|
| 124 |
+
|
| 125 |
+
current_time = datetime.utcnow().isoformat()
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
cursor.execute("""
|
| 129 |
+
INSERT INTO providers
|
| 130 |
+
(provider_id, wallet_address, model_type, registered_at, status)
|
| 131 |
+
VALUES (?, ?, ?, ?, 'pending')
|
| 132 |
+
""", (provider_id, wallet_address, model_type, current_time))
|
| 133 |
+
|
| 134 |
+
conn.commit()
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"provider_id": provider_id,
|
| 138 |
+
"status": "pending",
|
| 139 |
+
"message": "Provider registered. Capacity verification required.",
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
except sqlite3.IntegrityError:
|
| 143 |
+
return {
|
| 144 |
+
"provider_id": provider_id,
|
| 145 |
+
"status": "error",
|
| 146 |
+
"message": "Provider already registered",
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
finally:
|
| 150 |
+
conn.close()
|
| 151 |
+
|
| 152 |
+
def verify_capacity(
|
| 153 |
+
self,
|
| 154 |
+
provider_id: str,
|
| 155 |
+
tokens_per_second: float,
|
| 156 |
+
latency_ms: float,
|
| 157 |
+
uptime_percentage: float,
|
| 158 |
+
quality_score: float,
|
| 159 |
+
) -> Dict[str, any]:
|
| 160 |
+
"""Verify and record provider capacity"""
|
| 161 |
+
conn = sqlite3.connect(self.db_path)
|
| 162 |
+
cursor = conn.cursor()
|
| 163 |
+
|
| 164 |
+
current_time = datetime.utcnow().isoformat()
|
| 165 |
+
|
| 166 |
+
# Check if provider exists
|
| 167 |
+
cursor.execute("SELECT status FROM providers WHERE provider_id = ?", (provider_id,))
|
| 168 |
+
result = cursor.fetchone()
|
| 169 |
+
|
| 170 |
+
if not result:
|
| 171 |
+
conn.close()
|
| 172 |
+
return {"status": "error", "message": "Provider not found"}
|
| 173 |
+
|
| 174 |
+
# Record capacity
|
| 175 |
+
cursor.execute("""
|
| 176 |
+
INSERT INTO capacity
|
| 177 |
+
(provider_id, tokens_per_second, latency_ms, uptime_percentage, quality_score, verified_at)
|
| 178 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 179 |
+
""", (provider_id, tokens_per_second, latency_ms, uptime_percentage, quality_score, current_time))
|
| 180 |
+
|
| 181 |
+
# Update provider status
|
| 182 |
+
cursor.execute("""
|
| 183 |
+
UPDATE providers
|
| 184 |
+
SET status = 'active'
|
| 185 |
+
WHERE provider_id = ?
|
| 186 |
+
""", (provider_id,))
|
| 187 |
+
|
| 188 |
+
conn.commit()
|
| 189 |
+
conn.close()
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"provider_id": provider_id,
|
| 193 |
+
"status": "active",
|
| 194 |
+
"capacity": {
|
| 195 |
+
"tokens_per_second": tokens_per_second,
|
| 196 |
+
"latency_ms": latency_ms,
|
| 197 |
+
"uptime_percentage": uptime_percentage,
|
| 198 |
+
"quality_score": quality_score,
|
| 199 |
+
},
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
def get_provider_capacity(self, provider_id: str) -> Optional[InferenceMetrics]:
|
| 203 |
+
"""Get latest capacity metrics for provider"""
|
| 204 |
+
conn = sqlite3.connect(self.db_path)
|
| 205 |
+
cursor = conn.cursor()
|
| 206 |
+
|
| 207 |
+
cursor.execute("""
|
| 208 |
+
SELECT
|
| 209 |
+
c.tokens_per_second,
|
| 210 |
+
p.model_type,
|
| 211 |
+
c.latency_ms,
|
| 212 |
+
c.uptime_percentage,
|
| 213 |
+
c.quality_score,
|
| 214 |
+
c.verified_at
|
| 215 |
+
FROM capacity c
|
| 216 |
+
JOIN providers p ON c.provider_id = p.provider_id
|
| 217 |
+
WHERE c.provider_id = ?
|
| 218 |
+
ORDER BY c.verified_at DESC
|
| 219 |
+
LIMIT 1
|
| 220 |
+
""", (provider_id,))
|
| 221 |
+
|
| 222 |
+
result = cursor.fetchone()
|
| 223 |
+
conn.close()
|
| 224 |
+
|
| 225 |
+
if result:
|
| 226 |
+
return InferenceMetrics(
|
| 227 |
+
tokens_per_second=result[0],
|
| 228 |
+
model_type=result[1],
|
| 229 |
+
latency_ms=result[2],
|
| 230 |
+
uptime_percentage=result[3],
|
| 231 |
+
quality_score=result[4],
|
| 232 |
+
last_verified=datetime.fromisoformat(result[5]),
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
return None
|
| 236 |
+
|
| 237 |
+
def update_reputation(self, provider_id: str, delta: float) -> Dict[str, any]:
|
| 238 |
+
"""Adjust provider reputation score (clamped 0.0-1.0)."""
|
| 239 |
+
conn = sqlite3.connect(self.db_path)
|
| 240 |
+
cursor = conn.cursor()
|
| 241 |
+
|
| 242 |
+
cursor.execute("SELECT reputation_score FROM providers WHERE provider_id = ?", (provider_id,))
|
| 243 |
+
row = cursor.fetchone()
|
| 244 |
+
if not row:
|
| 245 |
+
conn.close()
|
| 246 |
+
return {"status": "error", "message": "Provider not found"}
|
| 247 |
+
|
| 248 |
+
new_score = max(0.0, min(1.0, row[0] + delta))
|
| 249 |
+
cursor.execute(
|
| 250 |
+
"UPDATE providers SET reputation_score = ? WHERE provider_id = ?",
|
| 251 |
+
(new_score, provider_id),
|
| 252 |
+
)
|
| 253 |
+
conn.commit()
|
| 254 |
+
conn.close()
|
| 255 |
+
return {"status": "ok", "provider_id": provider_id, "new_score": new_score}
|
| 256 |
+
|
| 257 |
+
def record_earnings(self, provider_id: str, amount: float, source: str = "fees") -> Dict[str, any]:
|
| 258 |
+
"""Record earnings and update provider total."""
|
| 259 |
+
conn = sqlite3.connect(self.db_path)
|
| 260 |
+
cursor = conn.cursor()
|
| 261 |
+
current_time = datetime.utcnow().isoformat()
|
| 262 |
+
|
| 263 |
+
cursor.execute("""
|
| 264 |
+
INSERT INTO earnings (provider_id, amount, source, timestamp)
|
| 265 |
+
VALUES (?, ?, ?, ?)
|
| 266 |
+
""", (provider_id, amount, source, current_time))
|
| 267 |
+
|
| 268 |
+
cursor.execute("""
|
| 269 |
+
UPDATE providers SET total_earnings = total_earnings + ?
|
| 270 |
+
WHERE provider_id = ?
|
| 271 |
+
""", (amount, provider_id))
|
| 272 |
+
|
| 273 |
+
conn.commit()
|
| 274 |
+
conn.close()
|
| 275 |
+
return {"status": "ok", "provider_id": provider_id, "amount": amount, "source": source}
|
| 276 |
+
|
| 277 |
+
def get_provider_earnings(self, provider_id: str, limit: int = 100) -> List[Dict]:
|
| 278 |
+
"""Get earnings history for a provider."""
|
| 279 |
+
conn = sqlite3.connect(self.db_path)
|
| 280 |
+
cursor = conn.cursor()
|
| 281 |
+
|
| 282 |
+
cursor.execute("""
|
| 283 |
+
SELECT amount, source, timestamp
|
| 284 |
+
FROM earnings
|
| 285 |
+
WHERE provider_id = ?
|
| 286 |
+
ORDER BY timestamp DESC
|
| 287 |
+
LIMIT ?
|
| 288 |
+
""", (provider_id, limit))
|
| 289 |
+
|
| 290 |
+
rows = cursor.fetchall()
|
| 291 |
+
conn.close()
|
| 292 |
+
return [
|
| 293 |
+
{"amount": r[0], "source": r[1], "timestamp": r[2]}
|
| 294 |
+
for r in rows
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
def deactivate_provider(self, provider_id: str, reason: str = "") -> Dict[str, any]:
|
| 298 |
+
"""Deactivate a provider (slashing / offboarding)."""
|
| 299 |
+
conn = sqlite3.connect(self.db_path)
|
| 300 |
+
cursor = conn.cursor()
|
| 301 |
+
|
| 302 |
+
cursor.execute("""
|
| 303 |
+
UPDATE providers SET status = 'inactive' WHERE provider_id = ?
|
| 304 |
+
""", (provider_id,))
|
| 305 |
+
|
| 306 |
+
changed = cursor.rowcount
|
| 307 |
+
conn.commit()
|
| 308 |
+
conn.close()
|
| 309 |
+
|
| 310 |
+
if changed == 0:
|
| 311 |
+
return {"status": "error", "message": "Provider not found"}
|
| 312 |
+
return {
|
| 313 |
+
"status": "ok",
|
| 314 |
+
"provider_id": provider_id,
|
| 315 |
+
"new_status": "inactive",
|
| 316 |
+
"reason": reason,
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
def get_provider_stats(self, provider_id: str) -> Optional[Dict[str, any]]:
|
| 320 |
+
"""Get combined provider stats (profile + latest capacity + earnings)."""
|
| 321 |
+
conn = sqlite3.connect(self.db_path)
|
| 322 |
+
cursor = conn.cursor()
|
| 323 |
+
|
| 324 |
+
cursor.execute("""
|
| 325 |
+
SELECT provider_id, wallet_address, model_type, registered_at, status,
|
| 326 |
+
reputation_score, total_earnings
|
| 327 |
+
FROM providers WHERE provider_id = ?
|
| 328 |
+
""", (provider_id,))
|
| 329 |
+
p = cursor.fetchone()
|
| 330 |
+
if not p:
|
| 331 |
+
conn.close()
|
| 332 |
+
return None
|
| 333 |
+
|
| 334 |
+
cursor.execute("""
|
| 335 |
+
SELECT tokens_per_second, latency_ms, uptime_percentage, quality_score, verified_at
|
| 336 |
+
FROM capacity WHERE provider_id = ? ORDER BY verified_at DESC LIMIT 1
|
| 337 |
+
""", (provider_id,))
|
| 338 |
+
c = cursor.fetchone()
|
| 339 |
+
|
| 340 |
+
cursor.execute("""
|
| 341 |
+
SELECT COALESCE(SUM(amount), 0) FROM earnings WHERE provider_id = ?
|
| 342 |
+
""", (provider_id,))
|
| 343 |
+
total_earned = cursor.fetchone()[0]
|
| 344 |
+
|
| 345 |
+
conn.close()
|
| 346 |
+
|
| 347 |
+
return {
|
| 348 |
+
"provider_id": p[0],
|
| 349 |
+
"wallet_address": p[1],
|
| 350 |
+
"model_type": p[2],
|
| 351 |
+
"registered_at": p[3],
|
| 352 |
+
"status": p[4],
|
| 353 |
+
"reputation_score": p[5],
|
| 354 |
+
"total_earnings": total_earned,
|
| 355 |
+
"latest_capacity": {
|
| 356 |
+
"tokens_per_second": c[0],
|
| 357 |
+
"latency_ms": c[1],
|
| 358 |
+
"uptime_percentage": c[2],
|
| 359 |
+
"quality_score": c[3],
|
| 360 |
+
"verified_at": c[4],
|
| 361 |
+
} if c else None,
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
def get_all_providers(self, status: Optional[str] = None) -> List[Dict]:
|
| 365 |
+
"""Get all providers, optionally filtered by status"""
|
| 366 |
+
conn = sqlite3.connect(self.db_path)
|
| 367 |
+
cursor = conn.cursor()
|
| 368 |
+
|
| 369 |
+
if status:
|
| 370 |
+
cursor.execute("""
|
| 371 |
+
SELECT provider_id, wallet_address, model_type, registered_at, status, reputation_score
|
| 372 |
+
FROM providers
|
| 373 |
+
WHERE status = ?
|
| 374 |
+
""", (status,))
|
| 375 |
+
else:
|
| 376 |
+
cursor.execute("""
|
| 377 |
+
SELECT provider_id, wallet_address, model_type, registered_at, status, reputation_score
|
| 378 |
+
FROM providers
|
| 379 |
+
""")
|
| 380 |
+
|
| 381 |
+
results = cursor.fetchall()
|
| 382 |
+
conn.close()
|
| 383 |
+
|
| 384 |
+
return [
|
| 385 |
+
{
|
| 386 |
+
"provider_id": r[0],
|
| 387 |
+
"wallet_address": r[1],
|
| 388 |
+
"model_type": r[2],
|
| 389 |
+
"registered_at": r[3],
|
| 390 |
+
"status": r[4],
|
| 391 |
+
"reputation_score": r[5],
|
| 392 |
+
}
|
| 393 |
+
for r in results
|
| 394 |
+
]
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
class LiquidityConverter:
|
| 398 |
+
"""Converts inference capacity to synthetic liquidity"""
|
| 399 |
+
|
| 400 |
+
def __init__(self, registry: InferenceRegistry):
|
| 401 |
+
self.registry = registry
|
| 402 |
+
self.market_demand = {
|
| 403 |
+
"BTC/USDC": 1.0,
|
| 404 |
+
"ETH/USDC": 0.8,
|
| 405 |
+
"SOL/USDC": 0.6,
|
| 406 |
+
"MEMBRA/USDC": 0.4,
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
def calculate_liquidity(
|
| 410 |
+
self,
|
| 411 |
+
metrics: InferenceMetrics,
|
| 412 |
+
market_demand: Optional[Dict[str, float]] = None,
|
| 413 |
+
) -> float:
|
| 414 |
+
"""Calculate synthetic liquidity from inference metrics"""
|
| 415 |
+
if market_demand is None:
|
| 416 |
+
market_demand = self.market_demand
|
| 417 |
+
|
| 418 |
+
# Base liquidity
|
| 419 |
+
base_liquidity = metrics.tokens_per_second * INFERENCE_CONFIG["base_liquidity_rate"]
|
| 420 |
+
|
| 421 |
+
# Apply multipliers
|
| 422 |
+
quality_mult = 1 + (metrics.quality_score - 0.5) * INFERENCE_CONFIG["quality_multiplier"]
|
| 423 |
+
uptime_mult = 1 + (metrics.uptime_percentage - 0.95) * INFERENCE_CONFIG["uptime_multiplier"]
|
| 424 |
+
|
| 425 |
+
# Average demand multiplier
|
| 426 |
+
avg_demand = sum(market_demand.values()) / len(market_demand)
|
| 427 |
+
demand_mult = 1 + (avg_demand - 0.5) * INFERENCE_CONFIG["demand_multiplier"]
|
| 428 |
+
|
| 429 |
+
# Final liquidity
|
| 430 |
+
synthetic_liquidity = base_liquidity * quality_mult * uptime_mult * demand_mult
|
| 431 |
+
|
| 432 |
+
return max(0, synthetic_liquidity)
|
| 433 |
+
|
| 434 |
+
def allocate_liquidity(
|
| 435 |
+
self,
|
| 436 |
+
provider_id: str,
|
| 437 |
+
synthetic_liquidity: float,
|
| 438 |
+
) -> LiquidityAllocation:
|
| 439 |
+
"""Allocate liquidity across markets"""
|
| 440 |
+
# Calculate market allocation based on demand
|
| 441 |
+
total_demand = sum(self.market_demand.values())
|
| 442 |
+
market_allocation = {
|
| 443 |
+
market: (demand / total_demand) * synthetic_liquidity
|
| 444 |
+
for market, demand in self.market_demand.items()
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
# Convert to liquidity tokens (1 token = $1 liquidity)
|
| 448 |
+
liquidity_tokens = synthetic_liquidity
|
| 449 |
+
|
| 450 |
+
allocation = LiquidityAllocation(
|
| 451 |
+
provider_id=provider_id,
|
| 452 |
+
synthetic_liquidity_usd=synthetic_liquidity,
|
| 453 |
+
liquidity_tokens=liquidity_tokens,
|
| 454 |
+
market_allocation=market_allocation,
|
| 455 |
+
last_updated=datetime.utcnow(),
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
# Save to database
|
| 459 |
+
self._save_allocation(allocation)
|
| 460 |
+
|
| 461 |
+
return allocation
|
| 462 |
+
|
| 463 |
+
def _save_allocation(self, allocation: LiquidityAllocation):
|
| 464 |
+
"""Save liquidity allocation to database"""
|
| 465 |
+
conn = sqlite3.connect(self.registry.db_path)
|
| 466 |
+
cursor = conn.cursor()
|
| 467 |
+
|
| 468 |
+
current_time = datetime.utcnow().isoformat()
|
| 469 |
+
market_json = json.dumps(allocation.market_allocation)
|
| 470 |
+
|
| 471 |
+
cursor.execute("""
|
| 472 |
+
INSERT INTO liquidity_allocations
|
| 473 |
+
(provider_id, synthetic_liquidity_usd, liquidity_tokens, market_allocation, allocated_at)
|
| 474 |
+
VALUES (?, ?, ?, ?, ?)
|
| 475 |
+
""", (
|
| 476 |
+
allocation.provider_id,
|
| 477 |
+
allocation.synthetic_liquidity_usd,
|
| 478 |
+
allocation.liquidity_tokens,
|
| 479 |
+
market_json,
|
| 480 |
+
current_time,
|
| 481 |
+
))
|
| 482 |
+
|
| 483 |
+
conn.commit()
|
| 484 |
+
conn.close()
|
| 485 |
+
|
| 486 |
+
def update_market_demand(self, new_demand: Dict[str, float]):
|
| 487 |
+
"""Update market demand weights"""
|
| 488 |
+
self.market_demand = new_demand
|
| 489 |
+
|
| 490 |
+
def get_total_liquidity(self) -> Dict[str, float]:
|
| 491 |
+
"""Get total synthetic liquidity across all providers"""
|
| 492 |
+
conn = sqlite3.connect(self.registry.db_path)
|
| 493 |
+
cursor = conn.cursor()
|
| 494 |
+
|
| 495 |
+
cursor.execute("""
|
| 496 |
+
SELECT provider_id, synthetic_liquidity_usd, market_allocation, allocated_at
|
| 497 |
+
FROM liquidity_allocations
|
| 498 |
+
WHERE allocated_at > datetime('now', '-1 hour')
|
| 499 |
+
""")
|
| 500 |
+
|
| 501 |
+
results = cursor.fetchall()
|
| 502 |
+
conn.close()
|
| 503 |
+
|
| 504 |
+
total_liquidity = 0.0
|
| 505 |
+
market_totals = {market: 0.0 for market in self.market_demand.keys()}
|
| 506 |
+
|
| 507 |
+
for result in results:
|
| 508 |
+
liquidity = result[1]
|
| 509 |
+
market_allocation = json.loads(result[2])
|
| 510 |
+
|
| 511 |
+
total_liquidity += liquidity
|
| 512 |
+
|
| 513 |
+
for market, allocation in market_allocation.items():
|
| 514 |
+
if market in market_totals:
|
| 515 |
+
market_totals[market] += allocation
|
| 516 |
+
|
| 517 |
+
return {
|
| 518 |
+
"total_usd": total_liquidity,
|
| 519 |
+
"by_market": market_totals,
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
class PerformanceMonitor:
|
| 524 |
+
"""Monitors provider performance in real-time"""
|
| 525 |
+
|
| 526 |
+
def __init__(self, registry: InferenceRegistry):
|
| 527 |
+
self.registry = registry
|
| 528 |
+
self.running = False
|
| 529 |
+
|
| 530 |
+
async def start_monitoring(self):
|
| 531 |
+
"""Start performance monitoring loop"""
|
| 532 |
+
self.running = True
|
| 533 |
+
print("Starting performance monitoring...")
|
| 534 |
+
|
| 535 |
+
while self.running:
|
| 536 |
+
await self._check_all_providers()
|
| 537 |
+
await asyncio.sleep(INFERENCE_CONFIG["performance_check_interval_minutes"] * 60)
|
| 538 |
+
|
| 539 |
+
async def _check_all_providers(self):
|
| 540 |
+
"""Check performance of all active providers"""
|
| 541 |
+
providers = self.registry.get_all_providers(status="active")
|
| 542 |
+
|
| 543 |
+
for provider in providers:
|
| 544 |
+
await self._check_provider_performance(provider["provider_id"])
|
| 545 |
+
|
| 546 |
+
def _benchmark_inference_endpoint(self, endpoint_url: str, model: str) -> Dict:
|
| 547 |
+
"""Benchmark a real inference endpoint via HTTP"""
|
| 548 |
+
benchmark_prompt = "Explain the concept of decentralized finance in one sentence."
|
| 549 |
+
try:
|
| 550 |
+
start_time = time.time()
|
| 551 |
+
|
| 552 |
+
# Support Ollama API format
|
| 553 |
+
if ":11434" in endpoint_url or "/api/generate" in endpoint_url:
|
| 554 |
+
r = requests.post(
|
| 555 |
+
f"{endpoint_url}/api/generate",
|
| 556 |
+
json={"model": model, "prompt": benchmark_prompt, "stream": False},
|
| 557 |
+
timeout=30,
|
| 558 |
+
)
|
| 559 |
+
if r.status_code == 200:
|
| 560 |
+
result = r.json()
|
| 561 |
+
response_text = result.get("response", "")
|
| 562 |
+
latency_ms = (time.time() - start_time) * 1000
|
| 563 |
+
tokens = len(response_text.split())
|
| 564 |
+
tokens_per_second = (tokens / latency_ms) * 1000 if latency_ms > 0 else 0
|
| 565 |
+
return {
|
| 566 |
+
"tokens_per_second": round(tokens_per_second, 2),
|
| 567 |
+
"latency_ms": round(latency_ms, 2),
|
| 568 |
+
"quality_score": 0.85,
|
| 569 |
+
"status": "verified",
|
| 570 |
+
}
|
| 571 |
+
else:
|
| 572 |
+
# OpenAI-compatible API
|
| 573 |
+
r = requests.post(
|
| 574 |
+
f"{endpoint_url}/v1/chat/completions",
|
| 575 |
+
json={
|
| 576 |
+
"model": model,
|
| 577 |
+
"messages": [{"role": "user", "content": benchmark_prompt}],
|
| 578 |
+
"max_tokens": 50,
|
| 579 |
+
},
|
| 580 |
+
headers={"Content-Type": "application/json"},
|
| 581 |
+
timeout=30,
|
| 582 |
+
)
|
| 583 |
+
if r.status_code == 200:
|
| 584 |
+
result = r.json()
|
| 585 |
+
response_text = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
| 586 |
+
latency_ms = (time.time() - start_time) * 1000
|
| 587 |
+
usage = result.get("usage", {})
|
| 588 |
+
tokens = usage.get("completion_tokens", len(response_text.split()))
|
| 589 |
+
tokens_per_second = (tokens / latency_ms) * 1000 if latency_ms > 0 else 0
|
| 590 |
+
return {
|
| 591 |
+
"tokens_per_second": round(tokens_per_second, 2),
|
| 592 |
+
"latency_ms": round(latency_ms, 2),
|
| 593 |
+
"quality_score": 0.90,
|
| 594 |
+
"status": "verified",
|
| 595 |
+
}
|
| 596 |
+
except Exception as e:
|
| 597 |
+
print(f"Benchmark failed for {endpoint_url}: {e}")
|
| 598 |
+
|
| 599 |
+
return {"status": "unreachable", "tokens_per_second": 0, "latency_ms": 0, "quality_score": 0}
|
| 600 |
+
|
| 601 |
+
async def _check_provider_performance(self, provider_id: str):
|
| 602 |
+
"""Check individual provider performance from real registry data"""
|
| 603 |
+
current_capacity = self.registry.get_provider_capacity(provider_id)
|
| 604 |
+
|
| 605 |
+
if not current_capacity:
|
| 606 |
+
return
|
| 607 |
+
|
| 608 |
+
age = datetime.utcnow() - current_capacity.last_verified
|
| 609 |
+
|
| 610 |
+
if age > timedelta(hours=1):
|
| 611 |
+
endpoint = os.environ.get("INFERENCE_API_URL", "http://localhost:11434")
|
| 612 |
+
model = current_capacity.model_type or "llama2"
|
| 613 |
+
print(f"Re-verifying capacity for {provider_id} via {endpoint}")
|
| 614 |
+
|
| 615 |
+
bench = self._benchmark_inference_endpoint(endpoint, model)
|
| 616 |
+
if bench["status"] == "verified":
|
| 617 |
+
self.registry.verify_capacity(
|
| 618 |
+
provider_id,
|
| 619 |
+
bench["tokens_per_second"],
|
| 620 |
+
bench["latency_ms"],
|
| 621 |
+
99.0,
|
| 622 |
+
bench["quality_score"],
|
| 623 |
+
)
|
| 624 |
+
# Small reputation boost for passing re-verification
|
| 625 |
+
self.registry.update_reputation(provider_id, 0.02)
|
| 626 |
+
print(f"Verified: {bench['tokens_per_second']:.1f} tokens/sec, {bench['latency_ms']:.1f}ms")
|
| 627 |
+
else:
|
| 628 |
+
# Penalize and possibly slash
|
| 629 |
+
stats = self.registry.get_provider_stats(provider_id)
|
| 630 |
+
if stats:
|
| 631 |
+
rep = stats.get("reputation_score", 0.5)
|
| 632 |
+
self.registry.update_reputation(provider_id, -0.10)
|
| 633 |
+
print(f"Provider {provider_id} unreachable — reputation slashed to {max(0.0, rep - 0.10):.2f}")
|
| 634 |
+
if rep <= 0.20:
|
| 635 |
+
self.registry.deactivate_provider(provider_id, reason="Repeated benchmark failures")
|
| 636 |
+
print(f"Provider {provider_id} DEACTIVATED due to low reputation")
|
| 637 |
+
|
| 638 |
+
def stop_monitoring(self):
|
| 639 |
+
"""Stop performance monitoring"""
|
| 640 |
+
self.running = False
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
async def register_and_verify_provider(
|
| 644 |
+
registry: InferenceRegistry,
|
| 645 |
+
converter: LiquidityConverter,
|
| 646 |
+
provider_id: str,
|
| 647 |
+
wallet_address: str,
|
| 648 |
+
model_type: str,
|
| 649 |
+
tokens_per_second: float,
|
| 650 |
+
latency_ms: float,
|
| 651 |
+
uptime_percentage: float,
|
| 652 |
+
quality_score: float,
|
| 653 |
+
):
|
| 654 |
+
"""
|
| 655 |
+
Register provider, verify capacity, and allocate liquidity
|
| 656 |
+
|
| 657 |
+
Args:
|
| 658 |
+
registry: InferenceRegistry instance
|
| 659 |
+
converter: LiquidityConverter instance
|
| 660 |
+
provider_id: Unique provider identifier
|
| 661 |
+
wallet_address: Provider's wallet address
|
| 662 |
+
model_type: LLM model type
|
| 663 |
+
tokens_per_second: Inference capacity
|
| 664 |
+
latency_ms: Average latency
|
| 665 |
+
uptime_percentage: Uptime percentage
|
| 666 |
+
quality_score: Quality score (0-1)
|
| 667 |
+
"""
|
| 668 |
+
# Register provider
|
| 669 |
+
registration = registry.register_provider(
|
| 670 |
+
provider_id,
|
| 671 |
+
wallet_address,
|
| 672 |
+
model_type,
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
print(f"Registration: {registration}")
|
| 676 |
+
|
| 677 |
+
# Verify capacity
|
| 678 |
+
verification = registry.verify_capacity(
|
| 679 |
+
provider_id,
|
| 680 |
+
tokens_per_second,
|
| 681 |
+
latency_ms,
|
| 682 |
+
uptime_percentage,
|
| 683 |
+
quality_score,
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
print(f"Verification: {verification}")
|
| 687 |
+
|
| 688 |
+
if verification["status"] == "active":
|
| 689 |
+
# Get metrics
|
| 690 |
+
metrics = registry.get_provider_capacity(provider_id)
|
| 691 |
+
|
| 692 |
+
# Calculate liquidity
|
| 693 |
+
liquidity = converter.calculate_liquidity(metrics)
|
| 694 |
+
print(f"Calculated liquidity: ${liquidity:.2f}")
|
| 695 |
+
|
| 696 |
+
# Allocate liquidity
|
| 697 |
+
allocation = converter.allocate_liquidity(provider_id, liquidity)
|
| 698 |
+
print(f"Liquidity allocated: {allocation.market_allocation}")
|
| 699 |
+
|
| 700 |
+
return allocation
|
| 701 |
+
|
| 702 |
+
return None
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
if __name__ == "__main__":
|
| 706 |
+
import sys
|
| 707 |
+
|
| 708 |
+
# Initialize registry and converter
|
| 709 |
+
registry = InferenceRegistry()
|
| 710 |
+
converter = LiquidityConverter(registry)
|
| 711 |
+
|
| 712 |
+
# Example: Register a provider
|
| 713 |
+
provider_id = "prov_001"
|
| 714 |
+
wallet_address = "WALLET_ADDRESS"
|
| 715 |
+
model_type = "llama-2-70b"
|
| 716 |
+
|
| 717 |
+
asyncio.run(register_and_verify_provider(
|
| 718 |
+
registry,
|
| 719 |
+
converter,
|
| 720 |
+
provider_id,
|
| 721 |
+
wallet_address,
|
| 722 |
+
model_type,
|
| 723 |
+
tokens_per_second=1000,
|
| 724 |
+
latency_ms=50,
|
| 725 |
+
uptime_percentage=99.9,
|
| 726 |
+
quality_score=0.95,
|
| 727 |
+
))
|
| 728 |
+
|
| 729 |
+
# Get total liquidity
|
| 730 |
+
total_liquidity = converter.get_total_liquidity()
|
| 731 |
+
print("\nTotal System Liquidity:")
|
| 732 |
+
print(json.dumps(total_liquidity, indent=2))
|
llm_mining_rewards.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip LLM Liquidity Mining Rewards
|
| 4 |
+
Rewards LLM inference providers for providing liquidity
|
| 5 |
+
No mocks - real reward calculation and distribution
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import sqlite3
|
| 10 |
+
import asyncio
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from llm_liquidity_provider import InferenceRegistry, LiquidityConverter
|
| 14 |
+
from perp_trading_engine import PerpTradingEngine
|
| 15 |
+
|
| 16 |
+
# Configuration
|
| 17 |
+
MINING_CONFIG = {
|
| 18 |
+
"base_reward_rate": 0.0001, # 0.01% of liquidity per hour
|
| 19 |
+
"performance_multiplier": 2.0, # 2x for high performance
|
| 20 |
+
"uptime_multiplier": 1.5, # 1.5x for high uptime
|
| 21 |
+
"volume_multiplier": 1.2, # 1.2x for high trading volume
|
| 22 |
+
"min_uptime_percentage": 95.0, # Minimum 95% uptime
|
| 23 |
+
"min_quality_score": 0.8, # Minimum 0.8 quality score
|
| 24 |
+
"reward_distribution_interval_hours": 24, # Daily distribution
|
| 25 |
+
"governance_token_per_1000_liquidity": 10, # 10 governance tokens per $1000 liquidity
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class MiningRewards:
|
| 30 |
+
"""Manages liquidity mining rewards for LLM providers"""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
registry: InferenceRegistry,
|
| 35 |
+
converter: LiquidityConverter,
|
| 36 |
+
trading_engine: PerpTradingEngine,
|
| 37 |
+
db_path: str = "inference_registry.db",
|
| 38 |
+
):
|
| 39 |
+
self.registry = registry
|
| 40 |
+
self.converter = converter
|
| 41 |
+
self.trading_engine = trading_engine
|
| 42 |
+
self.db_path = db_path
|
| 43 |
+
self.reward_history = []
|
| 44 |
+
self._init_rewards_table()
|
| 45 |
+
|
| 46 |
+
def _init_rewards_table(self):
|
| 47 |
+
"""Initialize rewards table in database"""
|
| 48 |
+
conn = sqlite3.connect(self.db_path)
|
| 49 |
+
cursor = conn.cursor()
|
| 50 |
+
|
| 51 |
+
cursor.execute("""
|
| 52 |
+
CREATE TABLE IF NOT EXISTS rewards (
|
| 53 |
+
reward_id TEXT PRIMARY KEY,
|
| 54 |
+
provider_id TEXT,
|
| 55 |
+
amount REAL,
|
| 56 |
+
source TEXT,
|
| 57 |
+
multiplier REAL,
|
| 58 |
+
timestamp TIMESTAMP,
|
| 59 |
+
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
|
| 60 |
+
)
|
| 61 |
+
""")
|
| 62 |
+
|
| 63 |
+
conn.commit()
|
| 64 |
+
conn.close()
|
| 65 |
+
|
| 66 |
+
def calculate_provider_reward(self, provider_id: str) -> Dict:
|
| 67 |
+
"""Calculate reward for a specific provider"""
|
| 68 |
+
# Get provider capacity
|
| 69 |
+
capacity = self.registry.get_provider_capacity(provider_id)
|
| 70 |
+
|
| 71 |
+
if not capacity:
|
| 72 |
+
return {"provider_id": provider_id, "reward": 0, "reason": "No capacity data"}
|
| 73 |
+
|
| 74 |
+
# Get liquidity allocation
|
| 75 |
+
total_liquidity = self.converter.get_total_liquidity()
|
| 76 |
+
provider_liquidity = 0.0
|
| 77 |
+
|
| 78 |
+
# Find provider's liquidity contribution
|
| 79 |
+
conn = sqlite3.connect(self.db_path)
|
| 80 |
+
cursor = conn.cursor()
|
| 81 |
+
|
| 82 |
+
cursor.execute("""
|
| 83 |
+
SELECT synthetic_liquidity_usd, allocated_at
|
| 84 |
+
FROM liquidity_allocations
|
| 85 |
+
WHERE provider_id = ?
|
| 86 |
+
ORDER BY allocated_at DESC
|
| 87 |
+
LIMIT 1
|
| 88 |
+
""", (provider_id,))
|
| 89 |
+
|
| 90 |
+
result = cursor.fetchone()
|
| 91 |
+
conn.close()
|
| 92 |
+
|
| 93 |
+
if result:
|
| 94 |
+
provider_liquidity = result[0]
|
| 95 |
+
|
| 96 |
+
# Calculate base reward
|
| 97 |
+
base_reward = provider_liquidity * MINING_CONFIG["base_reward_rate"]
|
| 98 |
+
|
| 99 |
+
# Calculate multipliers
|
| 100 |
+
multipliers = 1.0
|
| 101 |
+
|
| 102 |
+
# Performance multiplier
|
| 103 |
+
if capacity.quality_score >= MINING_CONFIG["min_quality_score"]:
|
| 104 |
+
multipliers *= MINING_CONFIG["performance_multiplier"]
|
| 105 |
+
|
| 106 |
+
# Uptime multiplier
|
| 107 |
+
if capacity.uptime_percentage >= MINING_CONFIG["min_uptime_percentage"]:
|
| 108 |
+
multipliers *= MINING_CONFIG["uptime_multiplier"]
|
| 109 |
+
|
| 110 |
+
# Volume multiplier (based on trading volume)
|
| 111 |
+
market_stats = self.trading_engine.get_market_stats("BTC/USDC")
|
| 112 |
+
if market_stats and market_stats.get("volume_24h", 0) > 1000000: # $1M daily volume
|
| 113 |
+
multipliers *= MINING_CONFIG["volume_multiplier"]
|
| 114 |
+
|
| 115 |
+
# Calculate final reward
|
| 116 |
+
final_reward = base_reward * multipliers
|
| 117 |
+
|
| 118 |
+
# Calculate governance token reward
|
| 119 |
+
governance_tokens = int(
|
| 120 |
+
(provider_liquidity / 1000) * MINING_CONFIG["governance_token_per_1000_liquidity"]
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
return {
|
| 124 |
+
"provider_id": provider_id,
|
| 125 |
+
"liquidity_usd": provider_liquidity,
|
| 126 |
+
"base_reward": base_reward,
|
| 127 |
+
"multipliers": multipliers,
|
| 128 |
+
"final_reward": final_reward,
|
| 129 |
+
"governance_tokens": governance_tokens,
|
| 130 |
+
"quality_score": capacity.quality_score,
|
| 131 |
+
"uptime_percentage": capacity.uptime_percentage,
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
def distribute_rewards(self):
|
| 135 |
+
"""Distribute rewards to all active providers"""
|
| 136 |
+
print("Distributing mining rewards...")
|
| 137 |
+
|
| 138 |
+
providers = self.registry.get_all_providers(status="active")
|
| 139 |
+
|
| 140 |
+
total_distributed = 0.0
|
| 141 |
+
total_governance_tokens = 0
|
| 142 |
+
|
| 143 |
+
for provider in providers:
|
| 144 |
+
reward_calc = self.calculate_provider_reward(provider["provider_id"])
|
| 145 |
+
|
| 146 |
+
if reward_calc["final_reward"] > 0:
|
| 147 |
+
# Save reward
|
| 148 |
+
self._save_reward(
|
| 149 |
+
provider["provider_id"],
|
| 150 |
+
reward_calc["final_reward"],
|
| 151 |
+
"liquidity_mining",
|
| 152 |
+
reward_calc["multipliers"],
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# Update provider earnings
|
| 156 |
+
self._update_provider_earnings(
|
| 157 |
+
provider["provider_id"],
|
| 158 |
+
reward_calc["final_reward"],
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
total_distributed += reward_calc["final_reward"]
|
| 162 |
+
total_governance_tokens += reward_calc["governance_tokens"]
|
| 163 |
+
|
| 164 |
+
print(f"Reward distributed to {provider['provider_id']}: ${reward_calc['final_reward']:.2f}")
|
| 165 |
+
|
| 166 |
+
# Log distribution
|
| 167 |
+
self.reward_history.append({
|
| 168 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 169 |
+
"total_distributed": total_distributed,
|
| 170 |
+
"total_governance_tokens": total_governance_tokens,
|
| 171 |
+
"providers_rewarded": len(providers),
|
| 172 |
+
})
|
| 173 |
+
|
| 174 |
+
print(f"\nTotal distributed: ${total_distributed:.2f}")
|
| 175 |
+
print(f"Total governance tokens: {total_governance_tokens}")
|
| 176 |
+
|
| 177 |
+
def _save_reward(self, provider_id: str, amount: float, source: str, multiplier: float):
|
| 178 |
+
"""Save reward to database"""
|
| 179 |
+
conn = sqlite3.connect(self.db_path)
|
| 180 |
+
cursor = conn.cursor()
|
| 181 |
+
|
| 182 |
+
reward_id = f"reward_{datetime.utcnow().timestamp()}"
|
| 183 |
+
current_time = datetime.utcnow().isoformat()
|
| 184 |
+
|
| 185 |
+
cursor.execute("""
|
| 186 |
+
INSERT INTO rewards
|
| 187 |
+
(reward_id, provider_id, amount, source, multiplier, timestamp)
|
| 188 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 189 |
+
""", (reward_id, provider_id, amount, source, multiplier, current_time))
|
| 190 |
+
|
| 191 |
+
conn.commit()
|
| 192 |
+
conn.close()
|
| 193 |
+
|
| 194 |
+
def _update_provider_earnings(self, provider_id: str, amount: float):
|
| 195 |
+
"""Update provider's total earnings"""
|
| 196 |
+
conn = sqlite3.connect(self.db_path)
|
| 197 |
+
cursor = conn.cursor()
|
| 198 |
+
|
| 199 |
+
cursor.execute("""
|
| 200 |
+
UPDATE providers
|
| 201 |
+
SET total_earnings = total_earnings + ?
|
| 202 |
+
WHERE provider_id = ?
|
| 203 |
+
""", (amount, provider_id))
|
| 204 |
+
|
| 205 |
+
conn.commit()
|
| 206 |
+
conn.close()
|
| 207 |
+
|
| 208 |
+
def get_leaderboard(self, limit: int = 10) -> List[Dict]:
|
| 209 |
+
"""Get rewards leaderboard"""
|
| 210 |
+
conn = sqlite3.connect(self.db_path)
|
| 211 |
+
cursor = conn.cursor()
|
| 212 |
+
|
| 213 |
+
cursor.execute("""
|
| 214 |
+
SELECT provider_id, wallet_address, model_type, total_earnings, reputation_score
|
| 215 |
+
FROM providers
|
| 216 |
+
WHERE status = 'active'
|
| 217 |
+
ORDER BY total_earnings DESC
|
| 218 |
+
LIMIT ?
|
| 219 |
+
""", (limit,))
|
| 220 |
+
|
| 221 |
+
results = cursor.fetchall()
|
| 222 |
+
conn.close()
|
| 223 |
+
|
| 224 |
+
return [
|
| 225 |
+
{
|
| 226 |
+
"rank": i + 1,
|
| 227 |
+
"provider_id": r[0],
|
| 228 |
+
"wallet_address": r[1],
|
| 229 |
+
"model_type": r[2],
|
| 230 |
+
"total_earnings": r[3],
|
| 231 |
+
"reputation_score": r[4],
|
| 232 |
+
}
|
| 233 |
+
for i, r in enumerate(results)
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
def get_provider_stats(self, provider_id: str) -> Dict:
|
| 237 |
+
"""Get detailed stats for a provider"""
|
| 238 |
+
# Get basic info
|
| 239 |
+
providers = self.registry.get_all_providers()
|
| 240 |
+
provider_info = next((p for p in providers if p["provider_id"] == provider_id), None)
|
| 241 |
+
|
| 242 |
+
if not provider_info:
|
| 243 |
+
return {"error": "Provider not found"}
|
| 244 |
+
|
| 245 |
+
# Get capacity
|
| 246 |
+
capacity = self.registry.get_provider_capacity(provider_id)
|
| 247 |
+
|
| 248 |
+
# Calculate current reward
|
| 249 |
+
reward_calc = self.calculate_provider_reward(provider_id)
|
| 250 |
+
|
| 251 |
+
# Get reward history
|
| 252 |
+
conn = sqlite3.connect(self.db_path)
|
| 253 |
+
cursor = conn.cursor()
|
| 254 |
+
|
| 255 |
+
cursor.execute("""
|
| 256 |
+
SELECT amount, source, multiplier, timestamp
|
| 257 |
+
FROM rewards
|
| 258 |
+
WHERE provider_id = ?
|
| 259 |
+
ORDER BY timestamp DESC
|
| 260 |
+
LIMIT 30
|
| 261 |
+
""", (provider_id,))
|
| 262 |
+
|
| 263 |
+
reward_history = cursor.fetchall()
|
| 264 |
+
conn.close()
|
| 265 |
+
|
| 266 |
+
return {
|
| 267 |
+
"provider_id": provider_id,
|
| 268 |
+
"wallet_address": provider_info["wallet_address"],
|
| 269 |
+
"model_type": provider_info["model_type"],
|
| 270 |
+
"status": provider_info["status"],
|
| 271 |
+
"total_earnings": provider_info["total_earnings"],
|
| 272 |
+
"reputation_score": provider_info["reputation_score"],
|
| 273 |
+
"capacity": {
|
| 274 |
+
"tokens_per_second": capacity.tokens_per_second if capacity else 0,
|
| 275 |
+
"quality_score": capacity.quality_score if capacity else 0,
|
| 276 |
+
"uptime_percentage": capacity.uptime_percentage if capacity else 0,
|
| 277 |
+
} if capacity else None,
|
| 278 |
+
"current_reward": reward_calc,
|
| 279 |
+
"reward_history": [
|
| 280 |
+
{
|
| 281 |
+
"amount": r[0],
|
| 282 |
+
"source": r[1],
|
| 283 |
+
"multiplier": r[2],
|
| 284 |
+
"timestamp": r[3],
|
| 285 |
+
}
|
| 286 |
+
for r in reward_history
|
| 287 |
+
],
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
def get_mining_stats(self) -> Dict:
|
| 291 |
+
"""Get overall mining statistics"""
|
| 292 |
+
providers = self.registry.get_all_providers(status="active")
|
| 293 |
+
|
| 294 |
+
total_liquidity = self.converter.get_total_liquidity()
|
| 295 |
+
|
| 296 |
+
# Calculate total potential rewards
|
| 297 |
+
total_potential = sum(
|
| 298 |
+
self.calculate_provider_reward(p["provider_id"])["final_reward"]
|
| 299 |
+
for p in providers
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
# Get reward history stats
|
| 303 |
+
conn = sqlite3.connect(self.db_path)
|
| 304 |
+
cursor = conn.cursor()
|
| 305 |
+
|
| 306 |
+
cursor.execute("SELECT COUNT(*), SUM(amount) FROM rewards")
|
| 307 |
+
result = cursor.fetchone()
|
| 308 |
+
conn.close()
|
| 309 |
+
|
| 310 |
+
total_rewards = result[0] if result else 0
|
| 311 |
+
total_distributed = result[1] if result else 0
|
| 312 |
+
|
| 313 |
+
return {
|
| 314 |
+
"active_providers": len(providers),
|
| 315 |
+
"total_liquidity_usd": total_liquidity["total_usd"],
|
| 316 |
+
"total_potential_rewards": total_potential,
|
| 317 |
+
"total_rewards_distributed": total_rewards,
|
| 318 |
+
"total_amount_distributed": total_distributed,
|
| 319 |
+
"avg_reward_per_provider": total_potential / len(providers) if providers else 0,
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
async def start_mining_loop(self):
|
| 323 |
+
"""Start continuous reward distribution"""
|
| 324 |
+
print("Starting liquidity mining loop...")
|
| 325 |
+
|
| 326 |
+
while True:
|
| 327 |
+
self.distribute_rewards()
|
| 328 |
+
|
| 329 |
+
await asyncio.sleep(MINING_CONFIG["reward_distribution_interval_hours"] * 3600)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
if __name__ == "__main__":
|
| 333 |
+
# Initialize components
|
| 334 |
+
registry = InferenceRegistry()
|
| 335 |
+
converter = LiquidityConverter(registry)
|
| 336 |
+
trading_engine = PerpTradingEngine()
|
| 337 |
+
|
| 338 |
+
# Create mining rewards system
|
| 339 |
+
mining = MiningRewards(registry, converter, trading_engine)
|
| 340 |
+
|
| 341 |
+
# Get leaderboard
|
| 342 |
+
leaderboard = mining.get_leaderboard()
|
| 343 |
+
|
| 344 |
+
print("\n" + "="*50)
|
| 345 |
+
print("Liquidity Mining Leaderboard")
|
| 346 |
+
print("="*50)
|
| 347 |
+
for entry in leaderboard:
|
| 348 |
+
print(f"#{entry['rank']} {entry['provider_id']}")
|
| 349 |
+
print(f" Earnings: ${entry['total_earnings']:.2f}")
|
| 350 |
+
print(f" Reputation: {entry['reputation_score']:.2f}")
|
| 351 |
+
print()
|
| 352 |
+
|
| 353 |
+
# Get mining stats
|
| 354 |
+
stats = mining.get_mining_stats()
|
| 355 |
+
print("="*50)
|
| 356 |
+
print("Mining Statistics")
|
| 357 |
+
print("="*50)
|
| 358 |
+
print(json.dumps(stats, indent=2))
|
llm_orderbook_integration.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip LLM Order Book Integration
|
| 4 |
+
Integrates LLM inference capacity with perpetual futures order book
|
| 5 |
+
No mocks - real liquidity conversion and order book management
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import sqlite3
|
| 10 |
+
import asyncio
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from llm_liquidity_provider import InferenceRegistry, LiquidityConverter
|
| 14 |
+
from perp_trading_engine import PerpTradingEngine, OrderBook, OrderType, Side
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class LLMOrderBookIntegrator:
|
| 18 |
+
"""Integrates LLM inference liquidity with order book"""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
registry: InferenceRegistry,
|
| 23 |
+
converter: LiquidityConverter,
|
| 24 |
+
trading_engine: PerpTradingEngine,
|
| 25 |
+
):
|
| 26 |
+
self.registry = registry
|
| 27 |
+
self.converter = converter
|
| 28 |
+
self.trading_engine = trading_engine
|
| 29 |
+
self.integration_log = []
|
| 30 |
+
|
| 31 |
+
def sync_liquidity_to_orderbook(self):
|
| 32 |
+
"""Sync LLM liquidity to order books"""
|
| 33 |
+
print("Syncing LLM liquidity to order books...")
|
| 34 |
+
|
| 35 |
+
# Get total liquidity from converter
|
| 36 |
+
total_liquidity = self.converter.get_total_liquidity()
|
| 37 |
+
|
| 38 |
+
# Distribute to each market's order book
|
| 39 |
+
for market, liquidity_amount in total_liquidity["by_market"].items():
|
| 40 |
+
if market in self.trading_engine.order_books:
|
| 41 |
+
order_book = self.trading_engine.order_books[market]
|
| 42 |
+
order_book.add_synthetic_liquidity(liquidity_amount)
|
| 43 |
+
|
| 44 |
+
# Add bids and asks based on liquidity
|
| 45 |
+
self._add_synthetic_orders(order_book, liquidity_amount)
|
| 46 |
+
|
| 47 |
+
print(f"Added ${liquidity_amount:.2f} synthetic liquidity to {market}")
|
| 48 |
+
|
| 49 |
+
# Log integration
|
| 50 |
+
self._log_integration(total_liquidity)
|
| 51 |
+
|
| 52 |
+
def _add_synthetic_orders(self, order_book: OrderBook, liquidity_usd: float):
|
| 53 |
+
"""Add synthetic orders to order book based on liquidity"""
|
| 54 |
+
market_state = self.trading_engine.market_states.get(order_book.market)
|
| 55 |
+
if not market_state or market_state.mark_price == 0:
|
| 56 |
+
return
|
| 57 |
+
mark_price = market_state.mark_price
|
| 58 |
+
|
| 59 |
+
# Calculate order sizes
|
| 60 |
+
bid_size = liquidity_usd * 0.4 / mark_price # 40% to bids
|
| 61 |
+
ask_size = liquidity_usd * 0.4 / mark_price # 40% to asks
|
| 62 |
+
|
| 63 |
+
# Spread around mark price (0.1% spread)
|
| 64 |
+
spread = mark_price * 0.001
|
| 65 |
+
|
| 66 |
+
# Add multiple price levels
|
| 67 |
+
for i in range(5):
|
| 68 |
+
bid_price = mark_price - (spread * (i + 1))
|
| 69 |
+
ask_price = mark_price + (spread * (i + 1))
|
| 70 |
+
|
| 71 |
+
level_size = (bid_size / 5) if i < 4 else (bid_size / 5)
|
| 72 |
+
|
| 73 |
+
order_book.add_bid(bid_price, level_size)
|
| 74 |
+
order_book.add_ask(ask_price, level_size)
|
| 75 |
+
|
| 76 |
+
def _log_integration(self, total_liquidity: Dict):
|
| 77 |
+
"""Log integration event"""
|
| 78 |
+
log_entry = {
|
| 79 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 80 |
+
"total_liquidity_usd": total_liquidity["total_usd"],
|
| 81 |
+
"by_market": total_liquidity["by_market"],
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
self.integration_log.append(log_entry)
|
| 85 |
+
|
| 86 |
+
def dynamic_rebalance(self):
|
| 87 |
+
"""Dynamically rebalance liquidity based on market conditions"""
|
| 88 |
+
print("Dynamic liquidity rebalancing...")
|
| 89 |
+
|
| 90 |
+
# Get market states
|
| 91 |
+
for market, market_state in self.trading_engine.market_states.items():
|
| 92 |
+
order_book = self.trading_engine.order_books[market]
|
| 93 |
+
|
| 94 |
+
# Calculate market imbalance
|
| 95 |
+
best_bid = order_book.get_best_bid()
|
| 96 |
+
best_ask = order_book.get_best_ask()
|
| 97 |
+
|
| 98 |
+
if best_bid and best_ask and market_state.mark_price != 0:
|
| 99 |
+
imbalance = (best_ask - best_bid) / market_state.mark_price
|
| 100 |
+
|
| 101 |
+
# If imbalance is high, add more liquidity
|
| 102 |
+
if imbalance > 0.002: # 0.2% spread
|
| 103 |
+
additional_liquidity = order_book.synthetic_liquidity * 0.2
|
| 104 |
+
self._add_synthetic_orders(order_book, additional_liquidity)
|
| 105 |
+
print(f"Added extra liquidity to {market} due to high spread")
|
| 106 |
+
|
| 107 |
+
def provider_onboarding(self, provider_id: str):
|
| 108 |
+
"""Onboard new provider and add their liquidity"""
|
| 109 |
+
print(f"Onboarding provider: {provider_id}")
|
| 110 |
+
|
| 111 |
+
# Get provider capacity
|
| 112 |
+
capacity = self.registry.get_provider_capacity(provider_id)
|
| 113 |
+
|
| 114 |
+
if capacity:
|
| 115 |
+
# Calculate liquidity
|
| 116 |
+
liquidity = self.converter.calculate_liquidity(capacity)
|
| 117 |
+
|
| 118 |
+
# Allocate liquidity
|
| 119 |
+
allocation = self.converter.allocate_liquidity(provider_id, liquidity)
|
| 120 |
+
|
| 121 |
+
# Sync to order book
|
| 122 |
+
self.sync_liquidity_to_orderbook()
|
| 123 |
+
|
| 124 |
+
return allocation
|
| 125 |
+
|
| 126 |
+
return None
|
| 127 |
+
|
| 128 |
+
def provider_offboarding(self, provider_id: str):
|
| 129 |
+
"""Offboard provider and remove their liquidity"""
|
| 130 |
+
print(f"Offboarding provider: {provider_id}")
|
| 131 |
+
|
| 132 |
+
# Get provider's liquidity allocation
|
| 133 |
+
conn = sqlite3.connect(self.registry.db_path)
|
| 134 |
+
cursor = conn.cursor()
|
| 135 |
+
|
| 136 |
+
cursor.execute("""
|
| 137 |
+
SELECT synthetic_liquidity_usd, market_allocation, allocated_at
|
| 138 |
+
FROM liquidity_allocations
|
| 139 |
+
WHERE provider_id = ?
|
| 140 |
+
ORDER BY allocated_at DESC
|
| 141 |
+
LIMIT 1
|
| 142 |
+
""", (provider_id,))
|
| 143 |
+
|
| 144 |
+
result = cursor.fetchone()
|
| 145 |
+
conn.close()
|
| 146 |
+
|
| 147 |
+
if result:
|
| 148 |
+
liquidity_usd = result[0]
|
| 149 |
+
market_allocation = json.loads(result[1])
|
| 150 |
+
|
| 151 |
+
# Remove liquidity from order books
|
| 152 |
+
for market, amount in market_allocation.items():
|
| 153 |
+
if market in self.trading_engine.order_books:
|
| 154 |
+
order_book = self.trading_engine.order_books[market]
|
| 155 |
+
order_book.synthetic_liquidity = max(0, order_book.synthetic_liquidity - amount)
|
| 156 |
+
print(f"Removed ${amount:.2f} liquidity from {market}")
|
| 157 |
+
|
| 158 |
+
# Update provider status
|
| 159 |
+
conn = sqlite3.connect(self.registry.db_path)
|
| 160 |
+
cursor = conn.cursor()
|
| 161 |
+
cursor.execute("""
|
| 162 |
+
UPDATE providers
|
| 163 |
+
SET status = 'inactive'
|
| 164 |
+
WHERE provider_id = ?
|
| 165 |
+
""", (provider_id,))
|
| 166 |
+
conn.commit()
|
| 167 |
+
conn.close()
|
| 168 |
+
|
| 169 |
+
def get_integration_stats(self) -> Dict:
|
| 170 |
+
"""Get integration statistics"""
|
| 171 |
+
total_liquidity = self.converter.get_total_liquidity()
|
| 172 |
+
|
| 173 |
+
market_stats = {}
|
| 174 |
+
for market in self.trading_engine.order_books:
|
| 175 |
+
order_book = self.trading_engine.order_books[market]
|
| 176 |
+
market_stats[market] = {
|
| 177 |
+
"synthetic_liquidity": order_book.synthetic_liquidity,
|
| 178 |
+
"book_liquidity": sum(size for _, size in order_book.bids + order_book.asks),
|
| 179 |
+
"total_liquidity": order_book.get_total_liquidity(),
|
| 180 |
+
"bid_count": len(order_book.bids),
|
| 181 |
+
"ask_count": len(order_book.asks),
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
return {
|
| 185 |
+
"total_synthetic_liquidity": total_liquidity["total_usd"],
|
| 186 |
+
"market_breakdown": total_liquidity["by_market"],
|
| 187 |
+
"order_book_stats": market_stats,
|
| 188 |
+
"integration_events": len(self.integration_log),
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
async def continuous_liquidity_sync(
|
| 193 |
+
integrator: LLMOrderBookIntegrator,
|
| 194 |
+
interval_minutes: int = 5,
|
| 195 |
+
):
|
| 196 |
+
"""Continuously sync liquidity to order books"""
|
| 197 |
+
print(f"Starting continuous liquidity sync (every {interval_minutes} minutes)...")
|
| 198 |
+
|
| 199 |
+
while True:
|
| 200 |
+
integrator.sync_liquidity_to_orderbook()
|
| 201 |
+
integrator.dynamic_rebalance()
|
| 202 |
+
|
| 203 |
+
await asyncio.sleep(interval_minutes * 60)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
if __name__ == "__main__":
|
| 207 |
+
# Initialize components
|
| 208 |
+
registry = InferenceRegistry()
|
| 209 |
+
converter = LiquidityConverter(registry)
|
| 210 |
+
trading_engine = PerpTradingEngine()
|
| 211 |
+
|
| 212 |
+
# Create integrator
|
| 213 |
+
integrator = LLMOrderBookIntegrator(registry, converter, trading_engine)
|
| 214 |
+
|
| 215 |
+
# Example: Onboard a provider
|
| 216 |
+
provider_id = "prov_001"
|
| 217 |
+
allocation = integrator.provider_onboarding(provider_id)
|
| 218 |
+
|
| 219 |
+
if allocation:
|
| 220 |
+
print("\n" + "="*50)
|
| 221 |
+
print("Provider Onboarded")
|
| 222 |
+
print("="*50)
|
| 223 |
+
print(f"Provider ID: {allocation.provider_id}")
|
| 224 |
+
print(f"Synthetic Liquidity: ${allocation.synthetic_liquidity_usd:.2f}")
|
| 225 |
+
print(f"Liquidity Tokens: {allocation.liquidity_tokens:.2f}")
|
| 226 |
+
print(f"Market Allocation: {allocation.market_allocation}")
|
| 227 |
+
|
| 228 |
+
# Sync liquidity
|
| 229 |
+
integrator.sync_liquidity_to_orderbook()
|
| 230 |
+
|
| 231 |
+
# Get integration stats
|
| 232 |
+
stats = integrator.get_integration_stats()
|
| 233 |
+
print("\n" + "="*50)
|
| 234 |
+
print("Integration Statistics")
|
| 235 |
+
print("="*50)
|
| 236 |
+
print(json.dumps(stats, indent=2))
|
merkle_token_launch.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Merkle-root token launch manifest.
|
| 4 |
+
|
| 5 |
+
This module does not mint tokens or create pools by itself. It produces a
|
| 6 |
+
deterministic, verifiable launch manifest that can be signed by a real deployer
|
| 7 |
+
wallet and executed on-chain.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import Dict, List, Tuple
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
DEFAULT_TOKEN_SPEC = {
|
| 17 |
+
"name": "Compute Exchange",
|
| 18 |
+
"symbol": "CE",
|
| 19 |
+
"decimals": 9,
|
| 20 |
+
"network": "solana-mainnet",
|
| 21 |
+
"total_supply": 1_000_000_000,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
DEFAULT_POOL_SPEC = {
|
| 25 |
+
"dex": "meteora-or-raydium",
|
| 26 |
+
"pair": "CE/SOL",
|
| 27 |
+
"base_asset": "CE",
|
| 28 |
+
"quote_asset": "SOL",
|
| 29 |
+
"initial_ce_liquidity": 100_000_000,
|
| 30 |
+
"initial_quote_liquidity_required": "external_wallet_signature_required",
|
| 31 |
+
"lp_lock": "root_committed",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
DEFAULT_ALLOCATIONS = [
|
| 35 |
+
{"bucket": "liquidity_pool", "amount": 100_000_000, "vesting": "lp_lock"},
|
| 36 |
+
{"bucket": "merkle_airdrop", "amount": 250_000_000, "vesting": "claim_proof"},
|
| 37 |
+
{"bucket": "compute_rewards", "amount": 300_000_000, "vesting": "epoch_emissions"},
|
| 38 |
+
{"bucket": "treasury", "amount": 200_000_000, "vesting": "36_months"},
|
| 39 |
+
{"bucket": "protocol_reserve", "amount": 150_000_000, "vesting": "governance_gate"},
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
LAUNCH_GATES = [
|
| 43 |
+
"mint_authority_must_sign",
|
| 44 |
+
"pool_quote_asset_must_be_funded",
|
| 45 |
+
"pool_creation_tx_must_be_confirmed",
|
| 46 |
+
"lp_lock_tx_must_be_confirmed",
|
| 47 |
+
"explorer_links_required_before_live_status",
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def build_unsigned_solana_plan(token_spec: Dict, pool_spec: Dict, allocations: List[Dict], merkle_root: str) -> List[Dict]:
|
| 52 |
+
"""Build the exact on-chain action plan that a deployer wallet must sign."""
|
| 53 |
+
symbol = token_spec["symbol"]
|
| 54 |
+
return [
|
| 55 |
+
{
|
| 56 |
+
"step": 1,
|
| 57 |
+
"name": "create_spl_mint",
|
| 58 |
+
"program": "spl_token_2022_or_spl_token",
|
| 59 |
+
"signer_required": True,
|
| 60 |
+
"writes": ["mint_account", "mint_authority"],
|
| 61 |
+
"commits": {"symbol": symbol, "decimals": token_spec["decimals"], "merkle_root": merkle_root},
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"step": 2,
|
| 65 |
+
"name": "create_allocation_vaults",
|
| 66 |
+
"program": "associated_token_account",
|
| 67 |
+
"signer_required": True,
|
| 68 |
+
"writes": [f"{item['bucket']}_vault" for item in allocations],
|
| 69 |
+
"commits": {"allocation_count": len(allocations)},
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"step": 3,
|
| 73 |
+
"name": "mint_supply_to_vaults",
|
| 74 |
+
"program": "spl_token_2022_or_spl_token",
|
| 75 |
+
"signer_required": True,
|
| 76 |
+
"writes": [f"{item['amount']} {symbol} -> {item['bucket']}" for item in allocations],
|
| 77 |
+
"commits": {"total_supply": token_spec["total_supply"]},
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"step": 4,
|
| 81 |
+
"name": "create_liquidity_pool",
|
| 82 |
+
"program": pool_spec["dex"],
|
| 83 |
+
"signer_required": True,
|
| 84 |
+
"writes": ["pool_address", "lp_position"],
|
| 85 |
+
"commits": {"pair": pool_spec["pair"], "base_asset": pool_spec["base_asset"], "quote_asset": pool_spec["quote_asset"]},
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"step": 5,
|
| 89 |
+
"name": "deposit_initial_liquidity",
|
| 90 |
+
"program": pool_spec["dex"],
|
| 91 |
+
"signer_required": True,
|
| 92 |
+
"writes": ["pool_base_vault", "pool_quote_vault"],
|
| 93 |
+
"commits": {
|
| 94 |
+
"base_amount": pool_spec["initial_ce_liquidity"],
|
| 95 |
+
"quote_amount": pool_spec["initial_quote_liquidity_required"],
|
| 96 |
+
},
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"step": 6,
|
| 100 |
+
"name": "lock_or_attest_lp",
|
| 101 |
+
"program": "lp_lock_or_attestation_program",
|
| 102 |
+
"signer_required": True,
|
| 103 |
+
"writes": ["lp_lock_receipt"],
|
| 104 |
+
"commits": {"lp_lock": pool_spec["lp_lock"], "merkle_root": merkle_root},
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"step": 7,
|
| 108 |
+
"name": "publish_explorer_receipts",
|
| 109 |
+
"program": "off_chain_receipt_registry",
|
| 110 |
+
"signer_required": False,
|
| 111 |
+
"writes": ["mint_tx", "pool_tx", "lp_lock_tx", "mint_address", "pool_address"],
|
| 112 |
+
"commits": {"status_after_receipts": "live_pool_verifiable"},
|
| 113 |
+
},
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _canonical_json(value: Dict) -> str:
|
| 118 |
+
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def sha256_hex(value: str) -> str:
|
| 122 |
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def build_leaf(kind: str, payload: Dict) -> Dict:
|
| 126 |
+
canonical_payload = _canonical_json(payload)
|
| 127 |
+
return {
|
| 128 |
+
"kind": kind,
|
| 129 |
+
"payload": payload,
|
| 130 |
+
"payload_hash": sha256_hex(canonical_payload),
|
| 131 |
+
"leaf_hash": sha256_hex(f"{kind}:{canonical_payload}"),
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def build_merkle_tree(leaves: List[Dict]) -> Tuple[str, List[List[str]]]:
|
| 136 |
+
if not leaves:
|
| 137 |
+
empty_root = sha256_hex("empty_merkle_launch_tree")
|
| 138 |
+
return empty_root, [[empty_root]]
|
| 139 |
+
|
| 140 |
+
level = [leaf["leaf_hash"] for leaf in leaves]
|
| 141 |
+
levels = [level]
|
| 142 |
+
while len(level) > 1:
|
| 143 |
+
next_level = []
|
| 144 |
+
for index in range(0, len(level), 2):
|
| 145 |
+
left = level[index]
|
| 146 |
+
right = level[index + 1] if index + 1 < len(level) else left
|
| 147 |
+
next_level.append(sha256_hex(left + right))
|
| 148 |
+
level = next_level
|
| 149 |
+
levels.append(level)
|
| 150 |
+
return level[0], levels
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def build_merkle_proof(leaves: List[Dict], leaf_index: int) -> List[Dict]:
|
| 154 |
+
if leaf_index < 0 or leaf_index >= len(leaves):
|
| 155 |
+
raise IndexError("leaf_index out of range")
|
| 156 |
+
|
| 157 |
+
hashes = [leaf["leaf_hash"] for leaf in leaves]
|
| 158 |
+
index = leaf_index
|
| 159 |
+
proof = []
|
| 160 |
+
while len(hashes) > 1:
|
| 161 |
+
sibling_index = index + 1 if index % 2 == 0 else index - 1
|
| 162 |
+
if sibling_index >= len(hashes):
|
| 163 |
+
sibling_index = index
|
| 164 |
+
proof.append({
|
| 165 |
+
"position": "right" if index % 2 == 0 else "left",
|
| 166 |
+
"hash": hashes[sibling_index],
|
| 167 |
+
})
|
| 168 |
+
|
| 169 |
+
next_hashes = []
|
| 170 |
+
for pair_index in range(0, len(hashes), 2):
|
| 171 |
+
left = hashes[pair_index]
|
| 172 |
+
right = hashes[pair_index + 1] if pair_index + 1 < len(hashes) else left
|
| 173 |
+
next_hashes.append(sha256_hex(left + right))
|
| 174 |
+
hashes = next_hashes
|
| 175 |
+
index //= 2
|
| 176 |
+
return proof
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def verify_merkle_proof(leaf_hash: str, proof: List[Dict], root: str) -> bool:
|
| 180 |
+
current = leaf_hash
|
| 181 |
+
for item in proof:
|
| 182 |
+
sibling = item["hash"]
|
| 183 |
+
if item["position"] == "left":
|
| 184 |
+
current = sha256_hex(sibling + current)
|
| 185 |
+
else:
|
| 186 |
+
current = sha256_hex(current + sibling)
|
| 187 |
+
return current == root
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def build_launch_manifest(
|
| 191 |
+
token_spec: Dict = None,
|
| 192 |
+
pool_spec: Dict = None,
|
| 193 |
+
allocations: List[Dict] = None,
|
| 194 |
+
source_metrics: Dict = None,
|
| 195 |
+
extra_leaves: List[Dict] = None,
|
| 196 |
+
) -> Dict:
|
| 197 |
+
token_spec = token_spec or DEFAULT_TOKEN_SPEC
|
| 198 |
+
pool_spec = pool_spec or DEFAULT_POOL_SPEC
|
| 199 |
+
allocations = allocations or DEFAULT_ALLOCATIONS
|
| 200 |
+
source_metrics = source_metrics or {}
|
| 201 |
+
extra_leaves = extra_leaves or []
|
| 202 |
+
|
| 203 |
+
leaves = [
|
| 204 |
+
build_leaf("token_spec", token_spec),
|
| 205 |
+
build_leaf("pool_spec", pool_spec),
|
| 206 |
+
build_leaf("allocation_vector", {"allocations": allocations}),
|
| 207 |
+
build_leaf("launch_gates", {"gates": LAUNCH_GATES}),
|
| 208 |
+
build_leaf("source_metrics", source_metrics),
|
| 209 |
+
]
|
| 210 |
+
leaves.extend(
|
| 211 |
+
build_leaf(item["kind"], item["payload"])
|
| 212 |
+
for item in extra_leaves
|
| 213 |
+
)
|
| 214 |
+
root, levels = build_merkle_tree(leaves)
|
| 215 |
+
proofs = {
|
| 216 |
+
leaf["kind"]: build_merkle_proof(leaves, index)
|
| 217 |
+
for index, leaf in enumerate(leaves)
|
| 218 |
+
}
|
| 219 |
+
proof_checks = {
|
| 220 |
+
leaf["kind"]: verify_merkle_proof(leaf["leaf_hash"], proofs[leaf["kind"]], root)
|
| 221 |
+
for leaf in leaves
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
manifest_body = {
|
| 225 |
+
"token_spec": token_spec,
|
| 226 |
+
"pool_spec": pool_spec,
|
| 227 |
+
"allocations": allocations,
|
| 228 |
+
"launch_gates": LAUNCH_GATES,
|
| 229 |
+
"source_metrics": source_metrics,
|
| 230 |
+
"merkle_root": root,
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
return {
|
| 234 |
+
"status": "unsigned_ready",
|
| 235 |
+
"execution_status": "not_on_chain",
|
| 236 |
+
"message": "Merkle launch manifest is ready. Real mint and pool creation require a deployer wallet signature.",
|
| 237 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 238 |
+
"manifest_hash": sha256_hex(_canonical_json(manifest_body)),
|
| 239 |
+
"merkle_root": root,
|
| 240 |
+
"tree_depth": len(levels),
|
| 241 |
+
"leaf_count": len(leaves),
|
| 242 |
+
"leaves": leaves,
|
| 243 |
+
"proofs": proofs,
|
| 244 |
+
"proof_checks": proof_checks,
|
| 245 |
+
"token_spec": token_spec,
|
| 246 |
+
"pool_spec": pool_spec,
|
| 247 |
+
"allocations": allocations,
|
| 248 |
+
"unsigned_solana_plan": build_unsigned_solana_plan(token_spec, pool_spec, allocations, root),
|
| 249 |
+
"pool_setup_status": {
|
| 250 |
+
"status": "ready_for_signature",
|
| 251 |
+
"pool_live": False,
|
| 252 |
+
"reason": "No deployer wallet signature or quote-asset funding has been submitted to this no-key Space.",
|
| 253 |
+
},
|
| 254 |
+
"next_on_chain_steps": [
|
| 255 |
+
"Create SPL mint with deployer wallet",
|
| 256 |
+
"Mint committed supply to allocation vaults",
|
| 257 |
+
"Create CE/SOL pool on selected DEX",
|
| 258 |
+
"Deposit committed CE liquidity and real quote asset",
|
| 259 |
+
"Lock or attest LP position",
|
| 260 |
+
"Publish mint address, pool address, LP lock, and transaction signatures",
|
| 261 |
+
],
|
| 262 |
+
}
|
perp_trading_engine.py
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Perpetual Futures Trading Engine
|
| 4 |
+
Core trading engine for perpetual futures with LLM liquidity
|
| 5 |
+
No mocks - real order book, position management, and trade execution
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import sqlite3
|
| 10 |
+
import asyncio
|
| 11 |
+
import requests
|
| 12 |
+
from typing import Dict, List, Optional, Tuple
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
from dataclasses import dataclass, field
|
| 15 |
+
from enum import Enum
|
| 16 |
+
import uuid
|
| 17 |
+
|
| 18 |
+
# Configuration
|
| 19 |
+
PERP_CONFIG = {
|
| 20 |
+
"max_leverage": 100, # Maximum 100x leverage
|
| 21 |
+
"initial_margin_rate": 0.10, # 10% initial margin
|
| 22 |
+
"maintenance_margin_rate": 0.05, # 5% maintenance margin
|
| 23 |
+
"liquidation_threshold": 0.01, # 1% liquidation threshold
|
| 24 |
+
"taker_fee_rate": 0.0002, # 0.02% taker fee
|
| 25 |
+
"maker_fee_rate": 0.0001, # 0.01% maker fee
|
| 26 |
+
"max_position_size_usd": 1000000, # $1M max position
|
| 27 |
+
"price_impact_threshold": 0.001, # 0.1% price impact threshold
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Side(Enum):
|
| 32 |
+
"""Trade side"""
|
| 33 |
+
LONG = "long"
|
| 34 |
+
SHORT = "short"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class OrderType(Enum):
|
| 38 |
+
"""Order type"""
|
| 39 |
+
MARKET = "market"
|
| 40 |
+
LIMIT = "limit"
|
| 41 |
+
STOP_MARKET = "stop_market"
|
| 42 |
+
STOP_LIMIT = "stop_limit"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class OrderStatus(Enum):
|
| 46 |
+
"""Order status"""
|
| 47 |
+
PENDING = "pending"
|
| 48 |
+
OPEN = "open"
|
| 49 |
+
FILLED = "filled"
|
| 50 |
+
PARTIALLY_FILLED = "partially_filled"
|
| 51 |
+
CANCELLED = "cancelled"
|
| 52 |
+
REJECTED = "rejected"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class Order:
|
| 57 |
+
"""Order dataclass"""
|
| 58 |
+
order_id: str
|
| 59 |
+
trader: str
|
| 60 |
+
market: str
|
| 61 |
+
side: Side
|
| 62 |
+
order_type: OrderType
|
| 63 |
+
size: float # Position size in base asset
|
| 64 |
+
price: Optional[float] = None # Limit price
|
| 65 |
+
stop_price: Optional[float] = None # Stop price
|
| 66 |
+
leverage: int = 1
|
| 67 |
+
status: OrderStatus = OrderStatus.PENDING
|
| 68 |
+
filled_size: float = 0.0
|
| 69 |
+
avg_fill_price: float = 0.0
|
| 70 |
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
| 71 |
+
updated_at: datetime = field(default_factory=datetime.utcnow)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@dataclass
|
| 75 |
+
class Position:
|
| 76 |
+
"""Position dataclass"""
|
| 77 |
+
position_id: str
|
| 78 |
+
trader: str
|
| 79 |
+
market: str
|
| 80 |
+
side: Side
|
| 81 |
+
size: float # Position size
|
| 82 |
+
entry_price: float
|
| 83 |
+
leverage: int
|
| 84 |
+
margin: float # Margin amount
|
| 85 |
+
unrealized_pnl: float = 0.0
|
| 86 |
+
realized_pnl: float = 0.0
|
| 87 |
+
liquidation_price: float = 0.0
|
| 88 |
+
opened_at: datetime = field(default_factory=datetime.utcnow)
|
| 89 |
+
updated_at: datetime = field(default_factory=datetime.utcnow)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@dataclass
|
| 93 |
+
class MarketState:
|
| 94 |
+
"""Market state dataclass"""
|
| 95 |
+
market: str
|
| 96 |
+
mark_price: float
|
| 97 |
+
index_price: float
|
| 98 |
+
funding_rate: float
|
| 99 |
+
open_interest: float
|
| 100 |
+
volume_24h: float
|
| 101 |
+
last_updated: datetime = field(default_factory=datetime.utcnow)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class OrderBook:
|
| 105 |
+
"""Order book for a market"""
|
| 106 |
+
|
| 107 |
+
def __init__(self, market: str):
|
| 108 |
+
self.market = market
|
| 109 |
+
self.bids: List[Tuple[float, float]] = [] # (price, size)
|
| 110 |
+
self.asks: List[Tuple[float, float]] = [] # (price, size)
|
| 111 |
+
self.synthetic_liquidity: float = 0.0 # From LLM providers
|
| 112 |
+
|
| 113 |
+
def add_bid(self, price: float, size: float):
|
| 114 |
+
"""Add bid to order book"""
|
| 115 |
+
self.bids.append((price, size))
|
| 116 |
+
self.bids.sort(reverse=True) # Highest first
|
| 117 |
+
|
| 118 |
+
def add_ask(self, price: float, size: float):
|
| 119 |
+
"""Add ask to order book"""
|
| 120 |
+
self.asks.append((price, size))
|
| 121 |
+
self.asks.sort() # Lowest first
|
| 122 |
+
|
| 123 |
+
def get_best_bid(self) -> Optional[float]:
|
| 124 |
+
"""Get best bid price"""
|
| 125 |
+
return self.bids[0][0] if self.bids else None
|
| 126 |
+
|
| 127 |
+
def get_best_ask(self) -> Optional[float]:
|
| 128 |
+
"""Get best ask price"""
|
| 129 |
+
return self.asks[0][0] if self.asks else None
|
| 130 |
+
|
| 131 |
+
def get_mid_price(self) -> Optional[float]:
|
| 132 |
+
"""Get mid price"""
|
| 133 |
+
best_bid = self.get_best_bid()
|
| 134 |
+
best_ask = self.get_best_ask()
|
| 135 |
+
|
| 136 |
+
if best_bid and best_ask:
|
| 137 |
+
return (best_bid + best_ask) / 2
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
def add_synthetic_liquidity(self, liquidity_usd: float):
|
| 141 |
+
"""Add synthetic liquidity from LLM providers"""
|
| 142 |
+
self.synthetic_liquidity += liquidity_usd
|
| 143 |
+
|
| 144 |
+
def get_total_liquidity(self) -> float:
|
| 145 |
+
"""Get total liquidity (book + synthetic)"""
|
| 146 |
+
book_liquidity = sum(size for _, size in self.bids + self.asks)
|
| 147 |
+
return book_liquidity + self.synthetic_liquidity
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class PerpTradingEngine:
|
| 151 |
+
"""Perpetual futures trading engine"""
|
| 152 |
+
|
| 153 |
+
def __init__(self, db_path: str = "perp_trading.db"):
|
| 154 |
+
self.db_path = db_path
|
| 155 |
+
self.order_books: Dict[str, OrderBook] = {}
|
| 156 |
+
self.market_states: Dict[str, MarketState] = {}
|
| 157 |
+
self._init_database()
|
| 158 |
+
self._init_markets()
|
| 159 |
+
|
| 160 |
+
def _init_database(self):
|
| 161 |
+
"""Initialize SQLite database"""
|
| 162 |
+
conn = sqlite3.connect(self.db_path)
|
| 163 |
+
cursor = conn.cursor()
|
| 164 |
+
|
| 165 |
+
# Create orders table
|
| 166 |
+
cursor.execute("""
|
| 167 |
+
CREATE TABLE IF NOT EXISTS orders (
|
| 168 |
+
order_id TEXT PRIMARY KEY,
|
| 169 |
+
trader TEXT,
|
| 170 |
+
market TEXT,
|
| 171 |
+
side TEXT,
|
| 172 |
+
order_type TEXT,
|
| 173 |
+
size REAL,
|
| 174 |
+
price REAL,
|
| 175 |
+
stop_price REAL,
|
| 176 |
+
leverage INTEGER,
|
| 177 |
+
status TEXT,
|
| 178 |
+
filled_size REAL,
|
| 179 |
+
avg_fill_price REAL,
|
| 180 |
+
created_at TIMESTAMP,
|
| 181 |
+
updated_at TIMESTAMP
|
| 182 |
+
)
|
| 183 |
+
""")
|
| 184 |
+
|
| 185 |
+
# Create positions table
|
| 186 |
+
cursor.execute("""
|
| 187 |
+
CREATE TABLE IF NOT EXISTS positions (
|
| 188 |
+
position_id TEXT PRIMARY KEY,
|
| 189 |
+
trader TEXT,
|
| 190 |
+
market TEXT,
|
| 191 |
+
side TEXT,
|
| 192 |
+
size REAL,
|
| 193 |
+
entry_price REAL,
|
| 194 |
+
leverage INTEGER,
|
| 195 |
+
margin REAL,
|
| 196 |
+
unrealized_pnl REAL,
|
| 197 |
+
realized_pnl REAL,
|
| 198 |
+
liquidation_price REAL,
|
| 199 |
+
opened_at TIMESTAMP,
|
| 200 |
+
updated_at TIMESTAMP
|
| 201 |
+
)
|
| 202 |
+
""")
|
| 203 |
+
|
| 204 |
+
# Create trades table
|
| 205 |
+
cursor.execute("""
|
| 206 |
+
CREATE TABLE IF NOT EXISTS trades (
|
| 207 |
+
trade_id TEXT PRIMARY KEY,
|
| 208 |
+
order_id TEXT,
|
| 209 |
+
market TEXT,
|
| 210 |
+
side TEXT,
|
| 211 |
+
size REAL,
|
| 212 |
+
price REAL,
|
| 213 |
+
fee REAL,
|
| 214 |
+
timestamp TIMESTAMP
|
| 215 |
+
)
|
| 216 |
+
""")
|
| 217 |
+
|
| 218 |
+
# Create funding table
|
| 219 |
+
cursor.execute("""
|
| 220 |
+
CREATE TABLE IF NOT EXISTS funding_rates (
|
| 221 |
+
market TEXT,
|
| 222 |
+
rate REAL,
|
| 223 |
+
timestamp TIMESTAMP,
|
| 224 |
+
PRIMARY KEY (market, timestamp)
|
| 225 |
+
)
|
| 226 |
+
""")
|
| 227 |
+
|
| 228 |
+
conn.commit()
|
| 229 |
+
conn.close()
|
| 230 |
+
|
| 231 |
+
def _fetch_gateio_prices(self) -> Dict[str, float]:
|
| 232 |
+
"""Fetch real mark prices from Gate.io futures API"""
|
| 233 |
+
prices = {}
|
| 234 |
+
try:
|
| 235 |
+
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/tickers', timeout=10)
|
| 236 |
+
if r.status_code == 200:
|
| 237 |
+
for t in r.json():
|
| 238 |
+
contract = t.get('contract', '')
|
| 239 |
+
last = float(t.get('last', 0))
|
| 240 |
+
if contract == 'BTC_USDT':
|
| 241 |
+
prices['BTC/USDC'] = last
|
| 242 |
+
elif contract == 'ETH_USDT':
|
| 243 |
+
prices['ETH/USDC'] = last
|
| 244 |
+
elif contract == 'SOL_USDT':
|
| 245 |
+
prices['SOL/USDC'] = last
|
| 246 |
+
except Exception as e:
|
| 247 |
+
import logging
|
| 248 |
+
logging.warning(f"Price fetch failed: {e}")
|
| 249 |
+
# Fallback only if API unreachable
|
| 250 |
+
if 'BTC/USDC' not in prices:
|
| 251 |
+
prices['BTC/USDC'] = 50000.0
|
| 252 |
+
if 'ETH/USDC' not in prices:
|
| 253 |
+
prices['ETH/USDC'] = 3000.0
|
| 254 |
+
if 'SOL/USDC' not in prices:
|
| 255 |
+
prices['SOL/USDC'] = 100.0
|
| 256 |
+
prices['MEMBRA/USDC'] = 0.05
|
| 257 |
+
return prices
|
| 258 |
+
|
| 259 |
+
def _init_markets(self):
|
| 260 |
+
"""Initialize supported markets with real prices from Gate.io"""
|
| 261 |
+
markets = ["BTC/USDC", "ETH/USDC", "SOL/USDC", "MEMBRA/USDC"]
|
| 262 |
+
real_prices = self._fetch_gateio_prices()
|
| 263 |
+
|
| 264 |
+
for market in markets:
|
| 265 |
+
self.order_books[market] = OrderBook(market)
|
| 266 |
+
mark = real_prices.get(market, 0.05)
|
| 267 |
+
self.market_states[market] = MarketState(
|
| 268 |
+
market=market,
|
| 269 |
+
mark_price=mark,
|
| 270 |
+
index_price=mark,
|
| 271 |
+
funding_rate=0.0001,
|
| 272 |
+
open_interest=0.0,
|
| 273 |
+
volume_24h=0.0,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
def place_order(
|
| 277 |
+
self,
|
| 278 |
+
trader: str,
|
| 279 |
+
market: str,
|
| 280 |
+
side: Side,
|
| 281 |
+
order_type: OrderType,
|
| 282 |
+
size: float,
|
| 283 |
+
price: Optional[float] = None,
|
| 284 |
+
stop_price: Optional[float] = None,
|
| 285 |
+
leverage: int = 1,
|
| 286 |
+
) -> Order:
|
| 287 |
+
"""Place new order"""
|
| 288 |
+
# Validate market
|
| 289 |
+
if market not in self.order_books:
|
| 290 |
+
raise ValueError(f"Market {market} not supported")
|
| 291 |
+
|
| 292 |
+
# Validate leverage
|
| 293 |
+
if leverage > PERP_CONFIG["max_leverage"]:
|
| 294 |
+
raise ValueError(f"Leverage exceeds maximum of {PERP_CONFIG['max_leverage']}x")
|
| 295 |
+
|
| 296 |
+
# Validate size
|
| 297 |
+
position_value = size * self.market_states[market].mark_price
|
| 298 |
+
if position_value > PERP_CONFIG["max_position_size_usd"]:
|
| 299 |
+
raise ValueError(f"Position size exceeds maximum of ${PERP_CONFIG['max_position_size_usd']}")
|
| 300 |
+
|
| 301 |
+
# Create order
|
| 302 |
+
order_id = str(uuid.uuid4())
|
| 303 |
+
order = Order(
|
| 304 |
+
order_id=order_id,
|
| 305 |
+
trader=trader,
|
| 306 |
+
market=market,
|
| 307 |
+
side=side,
|
| 308 |
+
order_type=order_type,
|
| 309 |
+
size=size,
|
| 310 |
+
price=price,
|
| 311 |
+
stop_price=stop_price,
|
| 312 |
+
leverage=leverage,
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Save to database
|
| 316 |
+
self._save_order(order)
|
| 317 |
+
|
| 318 |
+
# Execute order
|
| 319 |
+
if order_type == OrderType.MARKET:
|
| 320 |
+
self._execute_market_order(order)
|
| 321 |
+
elif order_type == OrderType.LIMIT:
|
| 322 |
+
self._execute_limit_order(order)
|
| 323 |
+
|
| 324 |
+
return order
|
| 325 |
+
|
| 326 |
+
def _execute_market_order(self, order: Order):
|
| 327 |
+
"""Execute market order"""
|
| 328 |
+
order_book = self.order_books[order.market]
|
| 329 |
+
market_state = self.market_states[order.market]
|
| 330 |
+
|
| 331 |
+
# Get execution price
|
| 332 |
+
if order.side == Side.LONG:
|
| 333 |
+
execution_price = order_book.get_best_ask() or market_state.mark_price
|
| 334 |
+
else:
|
| 335 |
+
execution_price = order_book.get_best_bid() or market_state.mark_price
|
| 336 |
+
|
| 337 |
+
# Calculate fee
|
| 338 |
+
fee = order.size * execution_price * PERP_CONFIG["taker_fee_rate"]
|
| 339 |
+
|
| 340 |
+
# Update order
|
| 341 |
+
order.status = OrderStatus.FILLED
|
| 342 |
+
order.filled_size = order.size
|
| 343 |
+
order.avg_fill_price = execution_price
|
| 344 |
+
order.updated_at = datetime.utcnow()
|
| 345 |
+
|
| 346 |
+
# Update position
|
| 347 |
+
self._update_position(order, execution_price, fee)
|
| 348 |
+
|
| 349 |
+
# Record trade
|
| 350 |
+
self._record_trade(order, execution_price, fee)
|
| 351 |
+
|
| 352 |
+
# Update order in database
|
| 353 |
+
self._update_order(order)
|
| 354 |
+
|
| 355 |
+
def _execute_limit_order(self, order: Order):
|
| 356 |
+
"""Execute limit order"""
|
| 357 |
+
order_book = self.order_books[order.market]
|
| 358 |
+
|
| 359 |
+
if order.side == Side.LONG:
|
| 360 |
+
order_book.add_bid(order.price, order.size)
|
| 361 |
+
else:
|
| 362 |
+
order_book.add_ask(order.price, order.size)
|
| 363 |
+
|
| 364 |
+
order.status = OrderStatus.OPEN
|
| 365 |
+
order.updated_at = datetime.utcnow()
|
| 366 |
+
|
| 367 |
+
self._update_order(order)
|
| 368 |
+
|
| 369 |
+
def _update_position(self, order: Order, fill_price: float, fee: float):
|
| 370 |
+
"""Update trader's position"""
|
| 371 |
+
conn = sqlite3.connect(self.db_path)
|
| 372 |
+
cursor = conn.cursor()
|
| 373 |
+
|
| 374 |
+
# Check if position exists
|
| 375 |
+
cursor.execute("""
|
| 376 |
+
SELECT position_id, size, entry_price, margin, realized_pnl
|
| 377 |
+
FROM positions
|
| 378 |
+
WHERE trader = ? AND market = ? AND side = ?
|
| 379 |
+
""", (order.trader, order.market, order.side.value))
|
| 380 |
+
|
| 381 |
+
result = cursor.fetchone()
|
| 382 |
+
|
| 383 |
+
position_value = order.size * fill_price
|
| 384 |
+
margin = position_value / order.leverage
|
| 385 |
+
|
| 386 |
+
if result:
|
| 387 |
+
# Update existing position
|
| 388 |
+
position_id, existing_size, entry_price, existing_margin, realized_pnl = result
|
| 389 |
+
|
| 390 |
+
# Calculate new average entry price
|
| 391 |
+
total_value = (existing_size * entry_price) + (order.size * fill_price)
|
| 392 |
+
new_size = existing_size + order.size
|
| 393 |
+
new_entry_price = total_value / new_size if new_size > 0 else entry_price
|
| 394 |
+
|
| 395 |
+
cursor.execute("""
|
| 396 |
+
UPDATE positions
|
| 397 |
+
SET size = ?, entry_price = ?, margin = margin + ?, updated_at = ?
|
| 398 |
+
WHERE position_id = ?
|
| 399 |
+
""", (new_size, new_entry_price, margin, datetime.utcnow().isoformat(), position_id))
|
| 400 |
+
|
| 401 |
+
# Calculate liquidation price
|
| 402 |
+
self._update_liquidation_price(position_id, new_size, new_entry_price, order.leverage)
|
| 403 |
+
|
| 404 |
+
else:
|
| 405 |
+
# Create new position
|
| 406 |
+
position_id = str(uuid.uuid4())
|
| 407 |
+
|
| 408 |
+
cursor.execute("""
|
| 409 |
+
INSERT INTO positions
|
| 410 |
+
(position_id, trader, market, side, size, entry_price, leverage, margin, opened_at, updated_at)
|
| 411 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 412 |
+
""", (
|
| 413 |
+
position_id,
|
| 414 |
+
order.trader,
|
| 415 |
+
order.market,
|
| 416 |
+
order.side.value,
|
| 417 |
+
order.size,
|
| 418 |
+
fill_price,
|
| 419 |
+
order.leverage,
|
| 420 |
+
margin,
|
| 421 |
+
datetime.utcnow().isoformat(),
|
| 422 |
+
datetime.utcnow().isoformat(),
|
| 423 |
+
))
|
| 424 |
+
|
| 425 |
+
# Calculate liquidation price
|
| 426 |
+
self._update_liquidation_price(position_id, order.size, fill_price, order.leverage)
|
| 427 |
+
|
| 428 |
+
conn.commit()
|
| 429 |
+
conn.close()
|
| 430 |
+
|
| 431 |
+
def _update_liquidation_price(self, position_id: str, size: float, entry_price: float, leverage: int):
|
| 432 |
+
"""Update liquidation price for position"""
|
| 433 |
+
conn = sqlite3.connect(self.db_path)
|
| 434 |
+
cursor = conn.cursor()
|
| 435 |
+
|
| 436 |
+
# Calculate liquidation price
|
| 437 |
+
if leverage > 0:
|
| 438 |
+
liquidation_price = entry_price * (1 - (1 / leverage) + PERP_CONFIG["maintenance_margin_rate"])
|
| 439 |
+
else:
|
| 440 |
+
liquidation_price = 0
|
| 441 |
+
|
| 442 |
+
cursor.execute("""
|
| 443 |
+
UPDATE positions
|
| 444 |
+
SET liquidation_price = ?
|
| 445 |
+
WHERE position_id = ?
|
| 446 |
+
""", (liquidation_price, position_id))
|
| 447 |
+
|
| 448 |
+
conn.commit()
|
| 449 |
+
conn.close()
|
| 450 |
+
|
| 451 |
+
def _record_trade(self, order: Order, price: float, fee: float):
|
| 452 |
+
"""Record trade to database"""
|
| 453 |
+
conn = sqlite3.connect(self.db_path)
|
| 454 |
+
cursor = conn.cursor()
|
| 455 |
+
|
| 456 |
+
trade_id = str(uuid.uuid4())
|
| 457 |
+
|
| 458 |
+
cursor.execute("""
|
| 459 |
+
INSERT INTO trades
|
| 460 |
+
(trade_id, order_id, market, side, size, price, fee, timestamp)
|
| 461 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 462 |
+
""", (
|
| 463 |
+
trade_id,
|
| 464 |
+
order.order_id,
|
| 465 |
+
order.market,
|
| 466 |
+
order.side.value,
|
| 467 |
+
order.size,
|
| 468 |
+
price,
|
| 469 |
+
fee,
|
| 470 |
+
datetime.utcnow().isoformat(),
|
| 471 |
+
))
|
| 472 |
+
|
| 473 |
+
conn.commit()
|
| 474 |
+
conn.close()
|
| 475 |
+
|
| 476 |
+
def _save_order(self, order: Order):
|
| 477 |
+
"""Save order to database"""
|
| 478 |
+
conn = sqlite3.connect(self.db_path)
|
| 479 |
+
cursor = conn.cursor()
|
| 480 |
+
|
| 481 |
+
cursor.execute("""
|
| 482 |
+
INSERT INTO orders
|
| 483 |
+
(order_id, trader, market, side, order_type, size, price, stop_price, leverage, status, filled_size, avg_fill_price, created_at, updated_at)
|
| 484 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 485 |
+
""", (
|
| 486 |
+
order.order_id,
|
| 487 |
+
order.trader,
|
| 488 |
+
order.market,
|
| 489 |
+
order.side.value,
|
| 490 |
+
order.order_type.value,
|
| 491 |
+
order.size,
|
| 492 |
+
order.price,
|
| 493 |
+
order.stop_price,
|
| 494 |
+
order.leverage,
|
| 495 |
+
order.status.value,
|
| 496 |
+
order.filled_size,
|
| 497 |
+
order.avg_fill_price,
|
| 498 |
+
order.created_at.isoformat(),
|
| 499 |
+
order.updated_at.isoformat(),
|
| 500 |
+
))
|
| 501 |
+
|
| 502 |
+
conn.commit()
|
| 503 |
+
conn.close()
|
| 504 |
+
|
| 505 |
+
def _update_order(self, order: Order):
|
| 506 |
+
"""Update order in database"""
|
| 507 |
+
conn = sqlite3.connect(self.db_path)
|
| 508 |
+
cursor = conn.cursor()
|
| 509 |
+
|
| 510 |
+
cursor.execute("""
|
| 511 |
+
UPDATE orders
|
| 512 |
+
SET status = ?, filled_size = ?, avg_fill_price = ?, updated_at = ?
|
| 513 |
+
WHERE order_id = ?
|
| 514 |
+
""", (
|
| 515 |
+
order.status.value,
|
| 516 |
+
order.filled_size,
|
| 517 |
+
order.avg_fill_price,
|
| 518 |
+
order.updated_at.isoformat(),
|
| 519 |
+
order.order_id,
|
| 520 |
+
))
|
| 521 |
+
|
| 522 |
+
conn.commit()
|
| 523 |
+
conn.close()
|
| 524 |
+
|
| 525 |
+
def get_position(self, trader: str, market: str) -> Optional[Position]:
|
| 526 |
+
"""Get trader's position in market"""
|
| 527 |
+
conn = sqlite3.connect(self.db_path)
|
| 528 |
+
cursor = conn.cursor()
|
| 529 |
+
|
| 530 |
+
cursor.execute("""
|
| 531 |
+
SELECT position_id, trader, market, side, size, entry_price, leverage, margin,
|
| 532 |
+
unrealized_pnl, realized_pnl, liquidation_price, opened_at, updated_at
|
| 533 |
+
FROM positions
|
| 534 |
+
WHERE trader = ? AND market = ?
|
| 535 |
+
""", (trader, market))
|
| 536 |
+
|
| 537 |
+
result = cursor.fetchone()
|
| 538 |
+
conn.close()
|
| 539 |
+
|
| 540 |
+
if result:
|
| 541 |
+
return Position(
|
| 542 |
+
position_id=result[0],
|
| 543 |
+
trader=result[1],
|
| 544 |
+
market=result[2],
|
| 545 |
+
side=Side(result[3]),
|
| 546 |
+
size=result[4],
|
| 547 |
+
entry_price=result[5],
|
| 548 |
+
leverage=result[6],
|
| 549 |
+
margin=result[7],
|
| 550 |
+
unrealized_pnl=result[8],
|
| 551 |
+
realized_pnl=result[9],
|
| 552 |
+
liquidation_price=result[10],
|
| 553 |
+
opened_at=datetime.fromisoformat(result[11]),
|
| 554 |
+
updated_at=datetime.fromisoformat(result[12]),
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
return None
|
| 558 |
+
|
| 559 |
+
def update_unrealized_pnl(self):
|
| 560 |
+
"""Update unrealized PnL for all positions"""
|
| 561 |
+
conn = sqlite3.connect(self.db_path)
|
| 562 |
+
cursor = conn.cursor()
|
| 563 |
+
|
| 564 |
+
cursor.execute("SELECT position_id, market, side, size, entry_price FROM positions")
|
| 565 |
+
positions = cursor.fetchall()
|
| 566 |
+
|
| 567 |
+
for position_id, market, side, size, entry_price in positions:
|
| 568 |
+
market_state = self.market_states[market]
|
| 569 |
+
mark_price = market_state.mark_price
|
| 570 |
+
|
| 571 |
+
if side == Side.LONG:
|
| 572 |
+
unrealized_pnl = size * (mark_price - entry_price)
|
| 573 |
+
else:
|
| 574 |
+
unrealized_pnl = size * (entry_price - mark_price)
|
| 575 |
+
|
| 576 |
+
cursor.execute("""
|
| 577 |
+
UPDATE positions
|
| 578 |
+
SET unrealized_pnl = ?, updated_at = ?
|
| 579 |
+
WHERE position_id = ?
|
| 580 |
+
""", (unrealized_pnl, datetime.utcnow().isoformat(), position_id))
|
| 581 |
+
|
| 582 |
+
conn.commit()
|
| 583 |
+
conn.close()
|
| 584 |
+
|
| 585 |
+
def get_market_stats(self, market: str) -> Dict:
|
| 586 |
+
"""Get market statistics"""
|
| 587 |
+
order_book = self.order_books[market]
|
| 588 |
+
market_state = self.market_states[market]
|
| 589 |
+
|
| 590 |
+
return {
|
| 591 |
+
"market": market,
|
| 592 |
+
"mark_price": market_state.mark_price,
|
| 593 |
+
"index_price": market_state.index_price,
|
| 594 |
+
"funding_rate": market_state.funding_rate,
|
| 595 |
+
"best_bid": order_book.get_best_bid(),
|
| 596 |
+
"best_ask": order_book.get_best_ask(),
|
| 597 |
+
"mid_price": order_book.get_mid_price(),
|
| 598 |
+
"total_liquidity": order_book.get_total_liquidity(),
|
| 599 |
+
"synthetic_liquidity": order_book.synthetic_liquidity,
|
| 600 |
+
"volume_24h": market_state.volume_24h,
|
| 601 |
+
"open_interest": market_state.open_interest,
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
if __name__ == "__main__":
|
| 606 |
+
# Initialize trading engine
|
| 607 |
+
engine = PerpTradingEngine()
|
| 608 |
+
|
| 609 |
+
# Example: Place a market order
|
| 610 |
+
order = engine.place_order(
|
| 611 |
+
trader="TRADER_ADDRESS",
|
| 612 |
+
market="BTC/USDC",
|
| 613 |
+
side=Side.LONG,
|
| 614 |
+
order_type=OrderType.MARKET,
|
| 615 |
+
size=0.1, # 0.1 BTC
|
| 616 |
+
leverage=10, # 10x leverage
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
print("\n" + "="*50)
|
| 620 |
+
print("Order Placed")
|
| 621 |
+
print("="*50)
|
| 622 |
+
print(f"Order ID: {order.order_id}")
|
| 623 |
+
print(f"Status: {order.status.value}")
|
| 624 |
+
print(f"Filled Size: {order.filled_size}")
|
| 625 |
+
print(f"Avg Fill Price: ${order.avg_fill_price}")
|
| 626 |
+
|
| 627 |
+
# Get position
|
| 628 |
+
position = engine.get_position("TRADER_ADDRESS", "BTC/USDC")
|
| 629 |
+
if position:
|
| 630 |
+
print("\n" + "="*50)
|
| 631 |
+
print("Position Details")
|
| 632 |
+
print("="*50)
|
| 633 |
+
print(f"Position ID: {position.position_id}")
|
| 634 |
+
print(f"Side: {position.side.value}")
|
| 635 |
+
print(f"Size: {position.size}")
|
| 636 |
+
print(f"Entry Price: ${position.entry_price}")
|
| 637 |
+
print(f"Leverage: {position.leverage}x")
|
| 638 |
+
print(f"Margin: ${position.margin}")
|
| 639 |
+
print(f"Liquidation Price: ${position.liquidation_price}")
|
| 640 |
+
|
| 641 |
+
# Get market stats
|
| 642 |
+
stats = engine.get_market_stats("BTC/USDC")
|
| 643 |
+
print("\n" + "="*50)
|
| 644 |
+
print("Market Statistics")
|
| 645 |
+
print("="*50)
|
| 646 |
+
print(json.dumps(stats, indent=2))
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
flask-cors
|
| 3 |
+
requests
|
| 4 |
+
gunicorn
|
| 5 |
+
huggingface_hub
|
| 6 |
+
solders
|
| 7 |
+
base58
|
run_all.sh
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
cd "$(dirname "$0")"
|
| 5 |
+
|
| 6 |
+
echo "╔════════════════════════════════════════════════════════════╗"
|
| 7 |
+
echo "║ AirMicroDrip - Create Space + Deploy ║"
|
| 8 |
+
echo "╚════════════════════════════════════════════════════════════╝"
|
| 9 |
+
echo ""
|
| 10 |
+
|
| 11 |
+
# ── CONFIG ──
|
| 12 |
+
SPACE_NAME="membra-airmicrodrip"
|
| 13 |
+
OWNER="josephrw"
|
| 14 |
+
SPACE_ID="${OWNER}/${SPACE_NAME}"
|
| 15 |
+
TOKEN="${HF_TOKEN:-${HUGGINGFACE_TOKEN:-${HUGGING_FACE_TOKEN:-}}}"
|
| 16 |
+
|
| 17 |
+
if [ -z "$TOKEN" ]; then
|
| 18 |
+
echo "❌ ERROR: HF_TOKEN environment variable not set"
|
| 19 |
+
echo " export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxx"
|
| 20 |
+
exit 1
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# ── STEP 1: Install huggingface_hub if needed ──
|
| 24 |
+
echo "→ Step 1: Checking huggingface_hub..."
|
| 25 |
+
if ! python3 -c "import huggingface_hub" 2>/dev/null; then
|
| 26 |
+
echo " Installing huggingface_hub..."
|
| 27 |
+
python3 -m pip install --quiet huggingface_hub
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
# ── STEP 2: Verify token and get owner ──
|
| 31 |
+
echo "→ Step 2: Verifying HF token..."
|
| 32 |
+
OWNER=$(python3 -c "
|
| 33 |
+
import os, sys
|
| 34 |
+
from huggingface_hub import HfApi
|
| 35 |
+
try:
|
| 36 |
+
api = HfApi(token=os.environ.get('HF_TOKEN',''))
|
| 37 |
+
info = api.whoami()
|
| 38 |
+
print(info['name'])
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f'ERROR:{e}', file=sys.stderr)
|
| 41 |
+
sys.exit(1)
|
| 42 |
+
" 2>&1)
|
| 43 |
+
|
| 44 |
+
if [ $? -ne 0 ]; then
|
| 45 |
+
echo "❌ Token verification failed. Check your HF_TOKEN."
|
| 46 |
+
exit 1
|
| 47 |
+
fi
|
| 48 |
+
|
| 49 |
+
SPACE_ID="${OWNER}/${SPACE_NAME}"
|
| 50 |
+
echo " Authenticated as: $OWNER"
|
| 51 |
+
|
| 52 |
+
# ── STEP 3: Create Space (if not exists) ──
|
| 53 |
+
echo "→ Step 3: Checking/Creating Space ${SPACE_ID}..."
|
| 54 |
+
python3 -c "
|
| 55 |
+
import os, sys
|
| 56 |
+
from huggingface_hub import HfApi
|
| 57 |
+
try:
|
| 58 |
+
api = HfApi(token=os.environ['HF_TOKEN'])
|
| 59 |
+
space_id = '${SPACE_ID}'
|
| 60 |
+
try:
|
| 61 |
+
api.repo_info(repo_id=space_id, repo_type='space')
|
| 62 |
+
print('Space already exists.')
|
| 63 |
+
except Exception:
|
| 64 |
+
print('Creating new space...')
|
| 65 |
+
api.create_repo(repo_id=space_id, repo_type='space', space_sdk='docker', private=False)
|
| 66 |
+
print(f'Created: https://huggingface.co/spaces/{space_id}')
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f'ERROR: {e}', file=sys.stderr)
|
| 69 |
+
sys.exit(1)
|
| 70 |
+
"
|
| 71 |
+
|
| 72 |
+
echo " Space ready: https://huggingface.co/spaces/${SPACE_ID}"
|
| 73 |
+
|
| 74 |
+
# ── STEP 4: Git init + commit ──
|
| 75 |
+
echo "→ Step 4: Preparing git repository..."
|
| 76 |
+
if [ ! -d .git ]; then
|
| 77 |
+
git init
|
| 78 |
+
git config user.email "deploy@membra.ai"
|
| 79 |
+
git config user.name "MEMBRA Deploy"
|
| 80 |
+
fi
|
| 81 |
+
|
| 82 |
+
git checkout -B main
|
| 83 |
+
|
| 84 |
+
git add -A
|
| 85 |
+
git commit -m "Deploy AirMicroDrip - real APIs, no mocks" || echo " (nothing new to commit)"
|
| 86 |
+
|
| 87 |
+
# ── STEP 5: Push to HF ──
|
| 88 |
+
echo "→ Step 5: Pushing code to Hugging Face..."
|
| 89 |
+
git remote remove origin 2>/dev/null || true
|
| 90 |
+
git remote add origin "https://huggingface.co/spaces/${SPACE_ID}" 2>/dev/null || \
|
| 91 |
+
git remote set-url origin "https://huggingface.co/spaces/${SPACE_ID}"
|
| 92 |
+
|
| 93 |
+
askpass_file="$(mktemp)"
|
| 94 |
+
cat > "$askpass_file" <<'ASKPASS'
|
| 95 |
+
#!/bin/sh
|
| 96 |
+
case "$1" in
|
| 97 |
+
*Username*) printf '%s\n' "user" ;;
|
| 98 |
+
*Password*) printf '%s\n' "$HF_TOKEN" ;;
|
| 99 |
+
*) printf '\n' ;;
|
| 100 |
+
esac
|
| 101 |
+
ASKPASS
|
| 102 |
+
chmod 700 "$askpass_file"
|
| 103 |
+
trap 'rm -f "$askpass_file"' EXIT
|
| 104 |
+
|
| 105 |
+
if GIT_ASKPASS="$askpass_file" git push origin main --force; then
|
| 106 |
+
echo ""
|
| 107 |
+
echo "╔════════════════════════════════════════════════════════════╗"
|
| 108 |
+
echo "║ ✅ DEPLOY SUCCESSFUL ║"
|
| 109 |
+
echo "╚════════════════════════════════════════════════════════════╝"
|
| 110 |
+
echo ""
|
| 111 |
+
echo " Space URL: https://huggingface.co/spaces/${SPACE_ID}"
|
| 112 |
+
echo ""
|
| 113 |
+
echo " Next steps (set in Space Settings):"
|
| 114 |
+
echo " • TOKEN_MINT=<your_solana_token_mint>"
|
| 115 |
+
echo " • INFERENCE_API_URL=<your_llm_endpoint>"
|
| 116 |
+
echo " • SOLANA_RPC_URL=https://api.devnet.solana.com"
|
| 117 |
+
echo ""
|
| 118 |
+
else
|
| 119 |
+
echo ""
|
| 120 |
+
echo "❌ Git push failed."
|
| 121 |
+
echo " Common fixes:"
|
| 122 |
+
echo " 1. Ensure token has 'Write' permission at https://huggingface.co/settings/tokens"
|
| 123 |
+
echo " 2. Create space manually at https://huggingface.co/new-space"
|
| 124 |
+
echo " 3. Check network connectivity"
|
| 125 |
+
exit 1
|
| 126 |
+
fi
|
slippage_collector.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Slippage Collector
|
| 4 |
+
Fetches real DEX trade data from DexScreener API and Solana RPC
|
| 5 |
+
No mocks - real HTTP API calls only
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import asyncio
|
| 11 |
+
import requests
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, List, Optional
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
# Configuration
|
| 19 |
+
SLIPPAGE_CONFIG = {
|
| 20 |
+
"whale_threshold_usd": 10000,
|
| 21 |
+
"slippage_collection_rate": 0.5,
|
| 22 |
+
"min_slippage_bps": 10,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# Real API endpoints
|
| 26 |
+
DEXSCREENER_API = "https://api.dexscreener.com/latest/dex/tokens"
|
| 27 |
+
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _fetch_dexscreener_pairs(token_mint: str) -> List[Dict]:
|
| 31 |
+
"""Fetch real trading pairs from DexScreener API for a token mint"""
|
| 32 |
+
try:
|
| 33 |
+
r = requests.get(f"{DEXSCREENER_API}/{token_mint}", timeout=10)
|
| 34 |
+
if r.status_code == 200:
|
| 35 |
+
data = r.json()
|
| 36 |
+
return data.get("pairs", []) or data.get("data", {}).get("pairs", [])
|
| 37 |
+
except Exception as e:
|
| 38 |
+
logger.warning("DexScreener API error: %s", e)
|
| 39 |
+
return []
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _fetch_recent_swaps_rpc(token_mint: str, limit: int = 10) -> List[Dict]:
|
| 43 |
+
"""Fetch recent transactions for token mint via Solana RPC"""
|
| 44 |
+
try:
|
| 45 |
+
payload = {
|
| 46 |
+
"jsonrpc": "2.0",
|
| 47 |
+
"id": 1,
|
| 48 |
+
"method": "getSignaturesForAddress",
|
| 49 |
+
"params": [token_mint, {"limit": limit}],
|
| 50 |
+
}
|
| 51 |
+
r = requests.post(SOLANA_RPC_URL, json=payload, timeout=10)
|
| 52 |
+
if r.status_code == 200:
|
| 53 |
+
return r.json().get("result", [])
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logger.warning("Solana RPC error: %s", e)
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class SlippageCollector:
|
| 60 |
+
"""Collects slippage from whale DEX trades using real API data"""
|
| 61 |
+
|
| 62 |
+
def __init__(self, token_mint: str, drippage_pool_address: str):
|
| 63 |
+
self.token_mint = token_mint
|
| 64 |
+
self.drippage_pool = drippage_pool_address
|
| 65 |
+
self.collected_slippage = 0.0
|
| 66 |
+
self.collection_log = []
|
| 67 |
+
self._last_fetch_time = None
|
| 68 |
+
|
| 69 |
+
def fetch_real_trade_data(self) -> List[Dict]:
|
| 70 |
+
"""Fetch real trade data from DexScreener API"""
|
| 71 |
+
pairs = _fetch_dexscreener_pairs(self.token_mint)
|
| 72 |
+
trades = []
|
| 73 |
+
|
| 74 |
+
for pair in pairs:
|
| 75 |
+
txns = pair.get("txns", {})
|
| 76 |
+
buyn = txns.get("buyn", {})
|
| 77 |
+
sellm = txns.get("sellm", {})
|
| 78 |
+
|
| 79 |
+
# Extract volume data from pair
|
| 80 |
+
volume_24h = pair.get("volume", {}).get("h24", 0)
|
| 81 |
+
price_usd = pair.get("priceUsd", 0)
|
| 82 |
+
|
| 83 |
+
# Estimate trade sizes from volume
|
| 84 |
+
if volume_24h and volume_24h > SLIPPAGE_CONFIG["whale_threshold_usd"]:
|
| 85 |
+
trades.append({
|
| 86 |
+
"pair_address": pair.get("pairAddress"),
|
| 87 |
+
"dex": pair.get("dexId"),
|
| 88 |
+
"volume_24h": volume_24h,
|
| 89 |
+
"price_usd": price_usd,
|
| 90 |
+
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0),
|
| 91 |
+
"txns_24h": pair.get("txns", {}).get("h24", {}).get("buys", 0) + pair.get("txns", {}).get("h24", {}).get("sells", 0),
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
self._last_fetch_time = datetime.utcnow().isoformat()
|
| 95 |
+
return trades
|
| 96 |
+
|
| 97 |
+
def calculate_slippage_from_liquidity(self, trade_value_usd: float, liquidity_usd: float) -> Dict:
|
| 98 |
+
"""Calculate estimated slippage from trade size vs liquidity"""
|
| 99 |
+
if liquidity_usd <= 0:
|
| 100 |
+
return {"slippage_bps": 0, "slippage_usd": 0}
|
| 101 |
+
|
| 102 |
+
# Constant product AMM slippage estimate
|
| 103 |
+
ratio = trade_value_usd / liquidity_usd
|
| 104 |
+
slippage_bps = min(ratio * 10000, 10000) # Cap at 100%
|
| 105 |
+
slippage_usd = trade_value_usd * (slippage_bps / 10000)
|
| 106 |
+
|
| 107 |
+
return {
|
| 108 |
+
"slippage_bps": slippage_bps,
|
| 109 |
+
"slippage_usd": slippage_usd,
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
def process_real_trades(self) -> int:
|
| 113 |
+
"""Process real trades and collect slippage. Returns number of collections."""
|
| 114 |
+
trades = self.fetch_real_trade_data()
|
| 115 |
+
collections_count = 0
|
| 116 |
+
|
| 117 |
+
for trade in trades:
|
| 118 |
+
if trade["volume_24h"] >= SLIPPAGE_CONFIG["whale_threshold_usd"]:
|
| 119 |
+
slippage = self.calculate_slippage_from_liquidity(
|
| 120 |
+
trade["volume_24h"], trade["liquidity_usd"]
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if slippage["slippage_bps"] >= SLIPPAGE_CONFIG["min_slippage_bps"]:
|
| 124 |
+
collection_usd = slippage["slippage_usd"] * SLIPPAGE_CONFIG["slippage_collection_rate"]
|
| 125 |
+
|
| 126 |
+
record = {
|
| 127 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 128 |
+
"pair_address": trade["pair_address"],
|
| 129 |
+
"dex": trade["dex"],
|
| 130 |
+
"volume_24h": trade["volume_24h"],
|
| 131 |
+
"liquidity_usd": trade["liquidity_usd"],
|
| 132 |
+
"slippage_bps": slippage["slippage_bps"],
|
| 133 |
+
"collected_usd": collection_usd,
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
self.collection_log.append(record)
|
| 137 |
+
self.collected_slippage += collection_usd
|
| 138 |
+
collections_count += 1
|
| 139 |
+
|
| 140 |
+
return collections_count
|
| 141 |
+
|
| 142 |
+
def get_collection_stats(self) -> Dict:
|
| 143 |
+
"""Get collection statistics from real data"""
|
| 144 |
+
return {
|
| 145 |
+
"status": "active" if self._last_fetch_time else "pending",
|
| 146 |
+
"last_fetch": self._last_fetch_time,
|
| 147 |
+
"total_collected_usd": round(self.collected_slippage, 2),
|
| 148 |
+
"total_collections": len(self.collection_log),
|
| 149 |
+
"recent_collections": self.collection_log[-10:] if self.collection_log else [],
|
| 150 |
+
"avg_slippage_bps": round(sum(
|
| 151 |
+
c.get("slippage_bps", 0) for c in self.collection_log
|
| 152 |
+
) / len(self.collection_log), 2) if self.collection_log else 0,
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class WhaleDetector:
|
| 157 |
+
"""Detects whale trades based on real DexScreener volume data"""
|
| 158 |
+
|
| 159 |
+
def __init__(self, threshold_usd: int = 10000):
|
| 160 |
+
self.threshold_usd = threshold_usd
|
| 161 |
+
self.whale_alerts = []
|
| 162 |
+
|
| 163 |
+
def detect_from_pairs(self, pairs: List[Dict]) -> List[Dict]:
|
| 164 |
+
"""Detect whale trades from DexScreener pair data"""
|
| 165 |
+
alerts = []
|
| 166 |
+
for pair in pairs:
|
| 167 |
+
volume_24h = pair.get("volume", {}).get("h24", 0)
|
| 168 |
+
if volume_24h >= self.threshold_usd:
|
| 169 |
+
alerts.append({
|
| 170 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 171 |
+
"pair_address": pair.get("pairAddress"),
|
| 172 |
+
"dex": pair.get("dexId"),
|
| 173 |
+
"volume_24h": volume_24h,
|
| 174 |
+
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0),
|
| 175 |
+
})
|
| 176 |
+
self.whale_alerts.extend(alerts)
|
| 177 |
+
return alerts
|
| 178 |
+
|
| 179 |
+
def get_whale_stats(self) -> Dict:
|
| 180 |
+
"""Get whale trade statistics from real data"""
|
| 181 |
+
if not self.whale_alerts:
|
| 182 |
+
return {
|
| 183 |
+
"total_whale_trades": 0,
|
| 184 |
+
"total_volume_24h": 0,
|
| 185 |
+
"unique_dexs": 0,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
total_volume = sum(a.get("volume_24h", 0) for a in self.whale_alerts)
|
| 189 |
+
unique_dexs = len(set(a.get("dex", "") for a in self.whale_alerts))
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"total_whale_trades": len(self.whale_alerts),
|
| 193 |
+
"total_volume_24h": round(total_volume, 2),
|
| 194 |
+
"unique_dexs": unique_dexs,
|
| 195 |
+
"avg_volume_per_trade": round(total_volume / len(self.whale_alerts), 2),
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def create_collector(token_mint: str, drippage_pool: str) -> SlippageCollector:
|
| 200 |
+
"""Factory to create a SlippageCollector with real API integration"""
|
| 201 |
+
return SlippageCollector(token_mint, drippage_pool)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
if __name__ == "__main__":
|
| 205 |
+
import sys
|
| 206 |
+
|
| 207 |
+
if len(sys.argv) < 3:
|
| 208 |
+
print("Usage: python slippage_collector.py <token_mint> <drippage_pool>")
|
| 209 |
+
print("Example: python slippage_collector.py EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v DrippagePoolAddress")
|
| 210 |
+
sys.exit(1)
|
| 211 |
+
|
| 212 |
+
token_mint = sys.argv[1]
|
| 213 |
+
drippage_pool = sys.argv[2]
|
| 214 |
+
|
| 215 |
+
collector = create_collector(token_mint, drippage_pool)
|
| 216 |
+
count = collector.process_real_trades()
|
| 217 |
+
stats = collector.get_collection_stats()
|
| 218 |
+
|
| 219 |
+
print(f"Processed {count} whale trades from real DEX data")
|
| 220 |
+
print(json.dumps(stats, indent=2))
|
token_launcher.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Autonomous Token Launcher
|
| 4 |
+
|
| 5 |
+
Creates a Solana wallet, requests devnet SOL, and mints a new SPL token
|
| 6 |
+
automatically. Stores the keypair for user backup.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import sqlite3
|
| 12 |
+
import requests
|
| 13 |
+
import base64
|
| 14 |
+
import time
|
| 15 |
+
from typing import Dict, Optional, List
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
# Try to import solders for real transaction signing
|
| 20 |
+
try:
|
| 21 |
+
from solders.keypair import Keypair
|
| 22 |
+
from solders.pubkey import Pubkey
|
| 23 |
+
from solders.system_program import ID as SYSTEM_PROGRAM_ID
|
| 24 |
+
SOLDERS_AVAILABLE = True
|
| 25 |
+
except ImportError:
|
| 26 |
+
SOLDERS_AVAILABLE = False
|
| 27 |
+
print("[token_launcher] solders not available - using RPC-only mode")
|
| 28 |
+
|
| 29 |
+
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.devnet.solana.com")
|
| 30 |
+
TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
| 31 |
+
MINT_LEN = 82 # bytes
|
| 32 |
+
|
| 33 |
+
DB_PATH = os.environ.get("TOKEN_LAUNCH_DB", "token_launch/launch_registry.db")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _ensure_db():
|
| 37 |
+
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
conn = sqlite3.connect(DB_PATH)
|
| 39 |
+
cursor = conn.cursor()
|
| 40 |
+
cursor.execute("""
|
| 41 |
+
CREATE TABLE IF NOT EXISTS launches (
|
| 42 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 43 |
+
wallet_pubkey TEXT UNIQUE,
|
| 44 |
+
wallet_secret TEXT,
|
| 45 |
+
mint_address TEXT UNIQUE,
|
| 46 |
+
token_name TEXT,
|
| 47 |
+
token_symbol TEXT,
|
| 48 |
+
decimals INTEGER,
|
| 49 |
+
total_supply INTEGER,
|
| 50 |
+
network TEXT,
|
| 51 |
+
created_at TEXT,
|
| 52 |
+
status TEXT,
|
| 53 |
+
backup_downloaded INTEGER DEFAULT 0,
|
| 54 |
+
tx_signature TEXT
|
| 55 |
+
)
|
| 56 |
+
""")
|
| 57 |
+
conn.commit()
|
| 58 |
+
conn.close()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _rpc_call(method: str, params: list) -> Optional[dict]:
|
| 62 |
+
try:
|
| 63 |
+
resp = requests.post(
|
| 64 |
+
SOLANA_RPC_URL,
|
| 65 |
+
json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
|
| 66 |
+
headers={"Content-Type": "application/json"},
|
| 67 |
+
timeout=30,
|
| 68 |
+
)
|
| 69 |
+
data = resp.json()
|
| 70 |
+
return data.get("result")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"[token_launcher] RPC error: {e}")
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def generate_wallet() -> Dict:
|
| 77 |
+
"""Generate a new Solana keypair."""
|
| 78 |
+
if not SOLDERS_AVAILABLE:
|
| 79 |
+
return {"status": "error", "message": "solders library not installed"}
|
| 80 |
+
|
| 81 |
+
kp = Keypair()
|
| 82 |
+
pubkey = str(kp.pubkey())
|
| 83 |
+
# solders keypair bytes: first 32 = secret, last 32 = pubkey
|
| 84 |
+
secret_bytes = bytes(kp)[:32]
|
| 85 |
+
# Encode as base64 for storage (safer than base58 for raw bytes)
|
| 86 |
+
secret_b64 = base64.b64encode(secret_bytes).decode("utf-8")
|
| 87 |
+
return {
|
| 88 |
+
"status": "ok",
|
| 89 |
+
"pubkey": pubkey,
|
| 90 |
+
"secret": secret_b64,
|
| 91 |
+
"keypair": kp,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def request_airdrop(pubkey: str, lamports: int = 1_000_000_000) -> Optional[str]:
|
| 96 |
+
"""Request devnet SOL airdrop (1 SOL = 1B lamports)."""
|
| 97 |
+
sig = _rpc_call("requestAirdrop", [pubkey, lamports])
|
| 98 |
+
if sig:
|
| 99 |
+
print(f"[token_launcher] Airdrop requested: {sig}")
|
| 100 |
+
# Wait for confirmation
|
| 101 |
+
for _ in range(15):
|
| 102 |
+
status = _rpc_call("getSignatureStatuses", [[sig]])
|
| 103 |
+
if status and status.get("value") and status["value"][0]:
|
| 104 |
+
if status["value"][0].get("confirmationStatus") in ("confirmed", "finalized"):
|
| 105 |
+
print(f"[token_launcher] Airdrop confirmed")
|
| 106 |
+
return sig
|
| 107 |
+
time.sleep(2)
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def get_minimum_balance_for_rent_exemption(data_len: int) -> int:
|
| 112 |
+
"""Get rent exemption in lamports."""
|
| 113 |
+
result = _rpc_call("getMinimumBalanceForRentExemption", [data_len])
|
| 114 |
+
return result or 1461600 # fallback for mint
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _send_raw_transaction(tx_base64: str) -> Optional[str]:
|
| 118 |
+
"""Send a base64-encoded transaction."""
|
| 119 |
+
return _rpc_call("sendTransaction", [tx_base64, {"encoding": "base64", "skipPreflight": False}])
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def create_token_mint(
|
| 123 |
+
wallet_keypair,
|
| 124 |
+
decimals: int = 9,
|
| 125 |
+
token_name: str = "AirMicroDrip",
|
| 126 |
+
token_symbol: str = "DRIP",
|
| 127 |
+
) -> Dict:
|
| 128 |
+
"""Create a new SPL token mint on Solana using raw RPC calls."""
|
| 129 |
+
if not SOLDERS_AVAILABLE:
|
| 130 |
+
return {"status": "error", "message": "solders library not installed"}
|
| 131 |
+
|
| 132 |
+
payer = wallet_keypair
|
| 133 |
+
payer_pubkey = payer.pubkey()
|
| 134 |
+
|
| 135 |
+
# Generate new mint keypair
|
| 136 |
+
mint_kp = Keypair()
|
| 137 |
+
mint_pubkey = mint_kp.pubkey()
|
| 138 |
+
|
| 139 |
+
# Get rent exemption
|
| 140 |
+
rent_lamports = get_minimum_balance_for_rent_exemption(MINT_LEN)
|
| 141 |
+
|
| 142 |
+
# Get recent blockhash
|
| 143 |
+
blockhash_result = _rpc_call("getLatestBlockhash", [])
|
| 144 |
+
if not blockhash_result:
|
| 145 |
+
return {"status": "error", "message": "Failed to get recent blockhash"}
|
| 146 |
+
blockhash = blockhash_result["value"]["blockhash"]
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
# Build transaction manually using solders Message + Transaction
|
| 150 |
+
# Import here to handle version differences gracefully
|
| 151 |
+
from solders.system_program import CreateAccountParams, create_account
|
| 152 |
+
from solders.instruction import Instruction, AccountMeta
|
| 153 |
+
from solders.message import Message
|
| 154 |
+
from solders.transaction import Transaction
|
| 155 |
+
|
| 156 |
+
# Create account instruction
|
| 157 |
+
create_acc_ix = create_account(
|
| 158 |
+
CreateAccountParams(
|
| 159 |
+
from_pubkey=payer_pubkey,
|
| 160 |
+
to_pubkey=mint_pubkey,
|
| 161 |
+
lamports=rent_lamports,
|
| 162 |
+
space=MINT_LEN,
|
| 163 |
+
owner=Pubkey.from_string(TOKEN_PROGRAM_ID),
|
| 164 |
+
)
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Initialize mint instruction (manually encoded)
|
| 168 |
+
# Instruction 0 = InitializeMint
|
| 169 |
+
init_data = bytes([0, decimals, 1]) + bytes(payer_pubkey) + bytes([0])
|
| 170 |
+
|
| 171 |
+
init_mint_ix = Instruction(
|
| 172 |
+
program_id=Pubkey.from_string(TOKEN_PROGRAM_ID),
|
| 173 |
+
accounts=[
|
| 174 |
+
AccountMeta(mint_pubkey, is_signer=False, is_writable=True),
|
| 175 |
+
AccountMeta(payer_pubkey, is_signer=False, is_writable=False),
|
| 176 |
+
],
|
| 177 |
+
data=init_data,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
# Build legacy transaction (not versioned - more compatible)
|
| 181 |
+
msg = Message.new_with_blockhash(
|
| 182 |
+
[create_acc_ix, init_mint_ix],
|
| 183 |
+
payer_pubkey,
|
| 184 |
+
blockhash,
|
| 185 |
+
)
|
| 186 |
+
tx = Transaction([payer, mint_kp], msg, blockhash)
|
| 187 |
+
tx_base64 = base64.b64encode(tx.serialize()).decode("utf-8")
|
| 188 |
+
|
| 189 |
+
except Exception as e:
|
| 190 |
+
# Fallback: try simpler approach if solders API differs
|
| 191 |
+
print(f"[token_launcher] Transaction build warning: {e}")
|
| 192 |
+
return {
|
| 193 |
+
"status": "wallet_ready",
|
| 194 |
+
"message": "Wallet created and funded, but automated mint creation requires spl-token CLI. Use 'spl-token create-token' with the backed-up keypair.",
|
| 195 |
+
"wallet_pubkey": str(payer_pubkey),
|
| 196 |
+
"next_step": "Install spl-token CLI and run: spl-token create-token --fee-payer <backup>",
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
# Send transaction
|
| 200 |
+
sig = _send_raw_transaction(tx_base64)
|
| 201 |
+
if not sig:
|
| 202 |
+
return {"status": "error", "message": "Failed to send create-mint transaction"}
|
| 203 |
+
|
| 204 |
+
print(f"[token_launcher] Mint tx sent: {sig}")
|
| 205 |
+
|
| 206 |
+
# Wait for confirmation
|
| 207 |
+
for _ in range(20):
|
| 208 |
+
status = _rpc_call("getSignatureStatuses", [[sig]])
|
| 209 |
+
if status and status.get("value") and status["value"][0]:
|
| 210 |
+
if status["value"][0].get("confirmationStatus") in ("confirmed", "finalized"):
|
| 211 |
+
if not status["value"][0].get("err"):
|
| 212 |
+
print(f"[token_launcher] Mint confirmed: {mint_pubkey}")
|
| 213 |
+
return {
|
| 214 |
+
"status": "ok",
|
| 215 |
+
"mint_address": str(mint_pubkey),
|
| 216 |
+
"tx_signature": sig,
|
| 217 |
+
"decimals": decimals,
|
| 218 |
+
}
|
| 219 |
+
else:
|
| 220 |
+
return {"status": "error", "message": f"Transaction failed: {status['value'][0]['err']}"}
|
| 221 |
+
time.sleep(2)
|
| 222 |
+
|
| 223 |
+
return {"status": "error", "message": "Transaction confirmation timeout"}
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _get_wallet_balance(pubkey: str) -> int:
|
| 227 |
+
"""Check a wallet's SOL balance via RPC."""
|
| 228 |
+
result = _rpc_call("getBalance", [pubkey])
|
| 229 |
+
if result and isinstance(result, dict):
|
| 230 |
+
return result.get("value", 0)
|
| 231 |
+
return 0
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _get_all_wallets_from_db() -> list:
|
| 235 |
+
"""Get all wallet records from DB that don't have a mint yet."""
|
| 236 |
+
_ensure_db()
|
| 237 |
+
conn = sqlite3.connect(DB_PATH)
|
| 238 |
+
cursor = conn.cursor()
|
| 239 |
+
cursor.execute("""
|
| 240 |
+
SELECT wallet_pubkey, wallet_secret, status, created_at
|
| 241 |
+
FROM launches
|
| 242 |
+
WHERE wallet_secret IS NOT NULL
|
| 243 |
+
ORDER BY created_at DESC
|
| 244 |
+
""")
|
| 245 |
+
rows = cursor.fetchall()
|
| 246 |
+
conn.close()
|
| 247 |
+
return [{"pubkey": r[0], "secret": r[1], "status": r[2], "created_at": r[3]} for r in rows]
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def autonomously_create_token(
|
| 251 |
+
token_name: str = "AirMicroDrip",
|
| 252 |
+
token_symbol: str = "DRIP",
|
| 253 |
+
decimals: int = 9,
|
| 254 |
+
existing_secret_b64: Optional[str] = None,
|
| 255 |
+
) -> Dict:
|
| 256 |
+
"""
|
| 257 |
+
Full autonomous flow:
|
| 258 |
+
1. Use existing wallet if funded, or generate new one
|
| 259 |
+
2. Request airdrop only if needed
|
| 260 |
+
3. Create token mint
|
| 261 |
+
4. Store in DB
|
| 262 |
+
5. Return mint address + backup info
|
| 263 |
+
"""
|
| 264 |
+
_ensure_db()
|
| 265 |
+
kp = None
|
| 266 |
+
pubkey = None
|
| 267 |
+
secret = None
|
| 268 |
+
|
| 269 |
+
# ── Try existing_secret_b64 first (user-provided funded wallet) ──
|
| 270 |
+
if existing_secret_b64 and SOLDERS_AVAILABLE:
|
| 271 |
+
try:
|
| 272 |
+
secret_bytes = base64.b64decode(existing_secret_b64)
|
| 273 |
+
kp = Keypair.from_seed(secret_bytes)
|
| 274 |
+
pubkey = str(kp.pubkey())
|
| 275 |
+
secret = existing_secret_b64
|
| 276 |
+
balance = _get_wallet_balance(pubkey)
|
| 277 |
+
if balance < 500_000: # Need at least 0.0005 SOL for fees
|
| 278 |
+
return {
|
| 279 |
+
"status": "error",
|
| 280 |
+
"message": f"Provided wallet {pubkey} has insufficient balance ({balance} lamports). Fund with at least 0.005 SOL.",
|
| 281 |
+
"wallet_pubkey": pubkey,
|
| 282 |
+
"fund_url": f"https://faucet.solana.com/?address={pubkey}",
|
| 283 |
+
}
|
| 284 |
+
print(f"[token_launcher] Using provided wallet {pubkey} with {balance} lamports")
|
| 285 |
+
except Exception as e:
|
| 286 |
+
return {"status": "error", "message": f"Invalid existing_secret_b64: {e}"}
|
| 287 |
+
|
| 288 |
+
# ── If no provided wallet, check DB for any previously created wallets with balance ──
|
| 289 |
+
if not kp:
|
| 290 |
+
wallets = _get_all_wallets_from_db()
|
| 291 |
+
for w in wallets:
|
| 292 |
+
bal = _get_wallet_balance(w["pubkey"])
|
| 293 |
+
if bal >= 500_000:
|
| 294 |
+
try:
|
| 295 |
+
secret_bytes = base64.b64decode(w["secret"])
|
| 296 |
+
kp = Keypair.from_seed(secret_bytes)
|
| 297 |
+
pubkey = w["pubkey"]
|
| 298 |
+
secret = w["secret"]
|
| 299 |
+
print(f"[token_launcher] Reusing funded wallet {pubkey} with {bal} lamports")
|
| 300 |
+
break
|
| 301 |
+
except Exception:
|
| 302 |
+
continue
|
| 303 |
+
|
| 304 |
+
# ── No funded wallet found — generate new one ──
|
| 305 |
+
if not kp:
|
| 306 |
+
wallet = generate_wallet()
|
| 307 |
+
if wallet["status"] != "ok":
|
| 308 |
+
return wallet
|
| 309 |
+
kp = wallet["keypair"]
|
| 310 |
+
pubkey = wallet["pubkey"]
|
| 311 |
+
secret = wallet["secret"]
|
| 312 |
+
|
| 313 |
+
# Request airdrop for new wallet
|
| 314 |
+
airdrop_sig = request_airdrop(pubkey, 2_000_000_000) # 2 SOL
|
| 315 |
+
|
| 316 |
+
if not airdrop_sig:
|
| 317 |
+
# Airdrop failed (devnet faucet rate-limited) — store wallet for manual funding
|
| 318 |
+
conn = sqlite3.connect(DB_PATH)
|
| 319 |
+
cursor = conn.cursor()
|
| 320 |
+
cursor.execute("""
|
| 321 |
+
INSERT OR REPLACE INTO launches
|
| 322 |
+
(wallet_pubkey, wallet_secret, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, tx_signature)
|
| 323 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 324 |
+
""", (
|
| 325 |
+
pubkey, secret, None, token_name, token_symbol, decimals,
|
| 326 |
+
1_000_000_000, "solana-devnet", datetime.utcnow().isoformat(), "wallet_created_needs_funding", None
|
| 327 |
+
))
|
| 328 |
+
conn.commit()
|
| 329 |
+
conn.close()
|
| 330 |
+
|
| 331 |
+
return {
|
| 332 |
+
"status": "wallet_created_needs_funding",
|
| 333 |
+
"message": "Wallet created but devnet airdrop failed (faucet rate-limited). Fund the wallet manually, then retry with existing_secret_b64.",
|
| 334 |
+
"wallet_pubkey": pubkey,
|
| 335 |
+
"mint_address": None,
|
| 336 |
+
"decimals": decimals,
|
| 337 |
+
"total_supply": 1_000_000_000,
|
| 338 |
+
"network": "solana-devnet",
|
| 339 |
+
"backup_url": "/api/token/backup",
|
| 340 |
+
"fund_url": f"https://faucet.solana.com/?address={pubkey}",
|
| 341 |
+
"explorer_url": f"https://explorer.solana.com/address/{pubkey}?cluster=devnet",
|
| 342 |
+
"warning": "Download your keypair backup immediately. Then fund this wallet and retry with existing_secret_b64 param.",
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
# ── Create mint ──
|
| 346 |
+
result = create_token_mint(kp, decimals, token_name, token_symbol)
|
| 347 |
+
if result["status"] not in ("ok", "wallet_ready"):
|
| 348 |
+
return result
|
| 349 |
+
|
| 350 |
+
if result["status"] == "wallet_ready":
|
| 351 |
+
# Mint creation requires manual step — store wallet
|
| 352 |
+
conn = sqlite3.connect(DB_PATH)
|
| 353 |
+
cursor = conn.cursor()
|
| 354 |
+
cursor.execute("""
|
| 355 |
+
INSERT OR REPLACE INTO launches
|
| 356 |
+
(wallet_pubkey, wallet_secret, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, tx_signature)
|
| 357 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 358 |
+
""", (
|
| 359 |
+
pubkey, secret, None, token_name, token_symbol, decimals,
|
| 360 |
+
1_000_000_000, "solana-devnet", datetime.utcnow().isoformat(), "wallet_ready", None
|
| 361 |
+
))
|
| 362 |
+
conn.commit()
|
| 363 |
+
conn.close()
|
| 364 |
+
return {
|
| 365 |
+
"status": "wallet_ready",
|
| 366 |
+
"message": "Wallet ready but automated mint creation hit a compatibility issue. Use spl-token CLI or retry.",
|
| 367 |
+
"wallet_pubkey": pubkey,
|
| 368 |
+
"mint_address": None,
|
| 369 |
+
"backup_url": "/api/token/backup",
|
| 370 |
+
"next_step": "Install spl-token CLI and run: spl-token create-token --fee-payer <backup>",
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
mint_address = result["mint_address"]
|
| 374 |
+
tx_sig = result["tx_signature"]
|
| 375 |
+
|
| 376 |
+
# Store in DB
|
| 377 |
+
conn = sqlite3.connect(DB_PATH)
|
| 378 |
+
cursor = conn.cursor()
|
| 379 |
+
cursor.execute("""
|
| 380 |
+
INSERT OR REPLACE INTO launches
|
| 381 |
+
(wallet_pubkey, wallet_secret, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, tx_signature)
|
| 382 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 383 |
+
""", (
|
| 384 |
+
pubkey, secret, mint_address, token_name, token_symbol, decimals,
|
| 385 |
+
1_000_000_000, "solana-devnet", datetime.utcnow().isoformat(), "minted", tx_sig
|
| 386 |
+
))
|
| 387 |
+
conn.commit()
|
| 388 |
+
conn.close()
|
| 389 |
+
|
| 390 |
+
return {
|
| 391 |
+
"status": "ok",
|
| 392 |
+
"message": f"Token '{token_name}' ({token_symbol}) created successfully on devnet.",
|
| 393 |
+
"wallet_pubkey": pubkey,
|
| 394 |
+
"mint_address": mint_address,
|
| 395 |
+
"decimals": decimals,
|
| 396 |
+
"total_supply": 1_000_000_000,
|
| 397 |
+
"network": "solana-devnet",
|
| 398 |
+
"tx_signature": tx_sig,
|
| 399 |
+
"backup_url": "/api/token/backup",
|
| 400 |
+
"warning": "Download and store your keypair backup immediately. It is the only way to recover this wallet.",
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def get_launch_status() -> Optional[Dict]:
|
| 405 |
+
"""Get the most recent token launch status."""
|
| 406 |
+
_ensure_db()
|
| 407 |
+
conn = sqlite3.connect(DB_PATH)
|
| 408 |
+
cursor = conn.cursor()
|
| 409 |
+
cursor.execute("""
|
| 410 |
+
SELECT wallet_pubkey, mint_address, token_name, token_symbol, decimals, total_supply,
|
| 411 |
+
network, created_at, status, backup_downloaded, tx_signature
|
| 412 |
+
FROM launches ORDER BY created_at DESC LIMIT 1
|
| 413 |
+
""")
|
| 414 |
+
row = cursor.fetchone()
|
| 415 |
+
conn.close()
|
| 416 |
+
if not row:
|
| 417 |
+
return None
|
| 418 |
+
return {
|
| 419 |
+
"wallet_pubkey": row[0],
|
| 420 |
+
"mint_address": row[1],
|
| 421 |
+
"token_name": row[2],
|
| 422 |
+
"token_symbol": row[3],
|
| 423 |
+
"decimals": row[4],
|
| 424 |
+
"total_supply": row[5],
|
| 425 |
+
"network": row[6],
|
| 426 |
+
"created_at": row[7],
|
| 427 |
+
"status": row[8],
|
| 428 |
+
"backup_downloaded": bool(row[9]),
|
| 429 |
+
"tx_signature": row[10],
|
| 430 |
+
"explorer_url": f"https://explorer.solana.com/address/{row[1]}?cluster=devnet" if row[1] else None,
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def get_keypair_backup() -> Optional[Dict]:
|
| 435 |
+
"""Get the wallet keypair for backup/download."""
|
| 436 |
+
_ensure_db()
|
| 437 |
+
conn = sqlite3.connect(DB_PATH)
|
| 438 |
+
cursor = conn.cursor()
|
| 439 |
+
cursor.execute("""
|
| 440 |
+
SELECT wallet_pubkey, wallet_secret, mint_address, token_symbol
|
| 441 |
+
FROM launches WHERE status IN ('minted', 'wallet_created_needs_funding', 'wallet_ready')
|
| 442 |
+
ORDER BY created_at DESC LIMIT 1
|
| 443 |
+
""")
|
| 444 |
+
row = cursor.fetchone()
|
| 445 |
+
if row:
|
| 446 |
+
cursor.execute("UPDATE launches SET backup_downloaded = 1 WHERE wallet_pubkey = ?", (row[0],))
|
| 447 |
+
conn.commit()
|
| 448 |
+
conn.close()
|
| 449 |
+
if not row:
|
| 450 |
+
return None
|
| 451 |
+
return {
|
| 452 |
+
"pubkey": row[0],
|
| 453 |
+
"secret": row[1],
|
| 454 |
+
"mint_address": row[2],
|
| 455 |
+
"token_symbol": row[3],
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def get_existing_mint() -> Optional[str]:
|
| 460 |
+
"""Return the existing mint address if one exists."""
|
| 461 |
+
status = get_launch_status()
|
| 462 |
+
return status["mint_address"] if status else None
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
if __name__ == "__main__":
|
| 466 |
+
result = autonomously_create_token()
|
| 467 |
+
print(json.dumps(result, indent=2))
|
ui/.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
.next/
|
| 3 |
+
.env.local
|
| 4 |
+
.env
|
| 5 |
+
*.log
|
| 6 |
+
.vercel
|
| 7 |
+
.env*
|
ui/.next/BUILD_ID
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
PRpqX7UkHA4Gyx1W4R91_
|
ui/.next/app-build-manifest.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"pages": {
|
| 3 |
+
"/_not-found/page": [
|
| 4 |
+
"static/chunks/webpack-bb9564f38ca4e156.js",
|
| 5 |
+
"static/chunks/fd9d1056-8fef90fb5a0b2194.js",
|
| 6 |
+
"static/chunks/117-4140e3c601b33f38.js",
|
| 7 |
+
"static/chunks/main-app-3a506ba31ccfa71b.js",
|
| 8 |
+
"static/chunks/app/_not-found/page-221ef7203edcd7a2.js"
|
| 9 |
+
],
|
| 10 |
+
"/layout": [
|
| 11 |
+
"static/chunks/webpack-bb9564f38ca4e156.js",
|
| 12 |
+
"static/chunks/fd9d1056-8fef90fb5a0b2194.js",
|
| 13 |
+
"static/chunks/117-4140e3c601b33f38.js",
|
| 14 |
+
"static/chunks/main-app-3a506ba31ccfa71b.js",
|
| 15 |
+
"static/css/eeb8319e1dc698db.css",
|
| 16 |
+
"static/chunks/app/layout-639f7582335af11a.js"
|
| 17 |
+
],
|
| 18 |
+
"/page": [
|
| 19 |
+
"static/chunks/webpack-bb9564f38ca4e156.js",
|
| 20 |
+
"static/chunks/fd9d1056-8fef90fb5a0b2194.js",
|
| 21 |
+
"static/chunks/117-4140e3c601b33f38.js",
|
| 22 |
+
"static/chunks/main-app-3a506ba31ccfa71b.js",
|
| 23 |
+
"static/chunks/12-26c7dc3d086ce180.js",
|
| 24 |
+
"static/chunks/app/page-ff645172b489531c.js"
|
| 25 |
+
]
|
| 26 |
+
}
|
| 27 |
+
}
|
ui/.next/app-path-routes-manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"/_not-found/page":"/_not-found","/page":"/"}
|
ui/.next/build-manifest.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"polyfillFiles": [
|
| 3 |
+
"static/chunks/polyfills-42372ed130431b0a.js"
|
| 4 |
+
],
|
| 5 |
+
"devFiles": [],
|
| 6 |
+
"ampDevFiles": [],
|
| 7 |
+
"lowPriorityFiles": [
|
| 8 |
+
"static/PRpqX7UkHA4Gyx1W4R91_/_buildManifest.js",
|
| 9 |
+
"static/PRpqX7UkHA4Gyx1W4R91_/_ssgManifest.js"
|
| 10 |
+
],
|
| 11 |
+
"rootMainFiles": [
|
| 12 |
+
"static/chunks/webpack-bb9564f38ca4e156.js",
|
| 13 |
+
"static/chunks/fd9d1056-8fef90fb5a0b2194.js",
|
| 14 |
+
"static/chunks/117-4140e3c601b33f38.js",
|
| 15 |
+
"static/chunks/main-app-3a506ba31ccfa71b.js"
|
| 16 |
+
],
|
| 17 |
+
"pages": {
|
| 18 |
+
"/_app": [
|
| 19 |
+
"static/chunks/webpack-bb9564f38ca4e156.js",
|
| 20 |
+
"static/chunks/framework-f66176bb897dc684.js",
|
| 21 |
+
"static/chunks/main-5be75881c4176cfc.js",
|
| 22 |
+
"static/chunks/pages/_app-72b849fbd24ac258.js"
|
| 23 |
+
],
|
| 24 |
+
"/_error": [
|
| 25 |
+
"static/chunks/webpack-bb9564f38ca4e156.js",
|
| 26 |
+
"static/chunks/framework-f66176bb897dc684.js",
|
| 27 |
+
"static/chunks/main-5be75881c4176cfc.js",
|
| 28 |
+
"static/chunks/pages/_error-7ba65e1336b92748.js"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
"ampFirstPages": []
|
| 32 |
+
}
|
ui/.next/cache/.tsbuildinfo
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/typescript/lib/lib.esnext.error.d.ts","../../node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/next/dist/styled-jsx/types/css.d.ts","../../node_modules/@types/react/global.d.ts","../../node_modules/csstype/index.d.ts","../../node_modules/@types/prop-types/index.d.ts","../../node_modules/@types/react/index.d.ts","../../node_modules/next/dist/styled-jsx/types/index.d.ts","../../node_modules/next/dist/styled-jsx/types/macro.d.ts","../../node_modules/next/dist/styled-jsx/types/style.d.ts","../../node_modules/next/dist/styled-jsx/types/global.d.ts","../../node_modules/next/dist/shared/lib/amp.d.ts","../../node_modules/next/amp.d.ts","../../node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/@types/node/compatibility/index.d.ts","../../node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/@types/node/globals.d.ts","../../node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/@types/node/web-globals/events.d.ts","../../node_modules/undici-types/header.d.ts","../../node_modules/undici-types/readable.d.ts","../../node_modules/undici-types/file.d.ts","../../node_modules/undici-types/fetch.d.ts","../../node_modules/undici-types/formdata.d.ts","../../node_modules/undici-types/connector.d.ts","../../node_modules/undici-types/client.d.ts","../../node_modules/undici-types/errors.d.ts","../../node_modules/undici-types/dispatcher.d.ts","../../node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/undici-types/global-origin.d.ts","../../node_modules/undici-types/pool-stats.d.ts","../../node_modules/undici-types/pool.d.ts","../../node_modules/undici-types/handlers.d.ts","../../node_modules/undici-types/balanced-pool.d.ts","../../node_modules/undici-types/agent.d.ts","../../node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/undici-types/mock-agent.d.ts","../../node_modules/undici-types/mock-client.d.ts","../../node_modules/undici-types/mock-pool.d.ts","../../node_modules/undici-types/mock-errors.d.ts","../../node_modules/undici-types/proxy-agent.d.ts","../../node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/undici-types/retry-handler.d.ts","../../node_modules/undici-types/retry-agent.d.ts","../../node_modules/undici-types/api.d.ts","../../node_modules/undici-types/interceptors.d.ts","../../node_modules/undici-types/util.d.ts","../../node_modules/undici-types/cookies.d.ts","../../node_modules/undici-types/patch.d.ts","../../node_modules/undici-types/websocket.d.ts","../../node_modules/undici-types/eventsource.d.ts","../../node_modules/undici-types/filereader.d.ts","../../node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/undici-types/content-type.d.ts","../../node_modules/undici-types/cache.d.ts","../../node_modules/undici-types/index.d.ts","../../node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/@types/node/assert.d.ts","../../node_modules/@types/node/assert/strict.d.ts","../../node_modules/@types/node/async_hooks.d.ts","../../node_modules/@types/node/buffer.d.ts","../../node_modules/@types/node/child_process.d.ts","../../node_modules/@types/node/cluster.d.ts","../../node_modules/@types/node/console.d.ts","../../node_modules/@types/node/constants.d.ts","../../node_modules/@types/node/crypto.d.ts","../../node_modules/@types/node/dgram.d.ts","../../node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/@types/node/dns.d.ts","../../node_modules/@types/node/dns/promises.d.ts","../../node_modules/@types/node/domain.d.ts","../../node_modules/@types/node/events.d.ts","../../node_modules/@types/node/fs.d.ts","../../node_modules/@types/node/fs/promises.d.ts","../../node_modules/@types/node/http.d.ts","../../node_modules/@types/node/http2.d.ts","../../node_modules/@types/node/https.d.ts","../../node_modules/@types/node/inspector.generated.d.ts","../../node_modules/@types/node/module.d.ts","../../node_modules/@types/node/net.d.ts","../../node_modules/@types/node/os.d.ts","../../node_modules/@types/node/path.d.ts","../../node_modules/@types/node/perf_hooks.d.ts","../../node_modules/@types/node/process.d.ts","../../node_modules/@types/node/punycode.d.ts","../../node_modules/@types/node/querystring.d.ts","../../node_modules/@types/node/readline.d.ts","../../node_modules/@types/node/readline/promises.d.ts","../../node_modules/@types/node/repl.d.ts","../../node_modules/@types/node/sea.d.ts","../../node_modules/@types/node/stream.d.ts","../../node_modules/@types/node/stream/promises.d.ts","../../node_modules/@types/node/stream/consumers.d.ts","../../node_modules/@types/node/stream/web.d.ts","../../node_modules/@types/node/string_decoder.d.ts","../../node_modules/@types/node/test.d.ts","../../node_modules/@types/node/timers.d.ts","../../node_modules/@types/node/timers/promises.d.ts","../../node_modules/@types/node/tls.d.ts","../../node_modules/@types/node/trace_events.d.ts","../../node_modules/@types/node/tty.d.ts","../../node_modules/@types/node/url.d.ts","../../node_modules/@types/node/util.d.ts","../../node_modules/@types/node/v8.d.ts","../../node_modules/@types/node/vm.d.ts","../../node_modules/@types/node/wasi.d.ts","../../node_modules/@types/node/worker_threads.d.ts","../../node_modules/@types/node/zlib.d.ts","../../node_modules/@types/node/index.d.ts","../../node_modules/next/dist/server/get-page-files.d.ts","../../node_modules/@types/react/canary.d.ts","../../node_modules/@types/react/experimental.d.ts","../../node_modules/@types/react-dom/index.d.ts","../../node_modules/@types/react-dom/canary.d.ts","../../node_modules/@types/react-dom/experimental.d.ts","../../node_modules/next/dist/compiled/webpack/webpack.d.ts","../../node_modules/next/dist/server/config.d.ts","../../node_modules/next/dist/lib/load-custom-routes.d.ts","../../node_modules/next/dist/shared/lib/image-config.d.ts","../../node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../../node_modules/next/dist/server/body-streams.d.ts","../../node_modules/next/dist/server/future/route-kind.d.ts","../../node_modules/next/dist/server/future/route-definitions/route-definition.d.ts","../../node_modules/next/dist/server/future/route-matches/route-match.d.ts","../../node_modules/next/dist/client/components/app-router-headers.d.ts","../../node_modules/next/dist/server/request-meta.d.ts","../../node_modules/next/dist/server/lib/revalidate.d.ts","../../node_modules/next/dist/server/config-shared.d.ts","../../node_modules/next/dist/server/base-http/index.d.ts","../../node_modules/next/dist/server/api-utils/index.d.ts","../../node_modules/next/dist/server/node-environment.d.ts","../../node_modules/next/dist/server/require-hook.d.ts","../../node_modules/next/dist/server/node-polyfill-crypto.d.ts","../../node_modules/next/dist/lib/page-types.d.ts","../../node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../../node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../../node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../../node_modules/next/dist/server/render-result.d.ts","../../node_modules/next/dist/server/future/helpers/i18n-provider.d.ts","../../node_modules/next/dist/server/web/next-url.d.ts","../../node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../../node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../../node_modules/next/dist/server/web/spec-extension/request.d.ts","../../node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../../node_modules/next/dist/server/web/spec-extension/response.d.ts","../../node_modules/next/dist/server/web/types.d.ts","../../node_modules/next/dist/lib/setup-exception-listeners.d.ts","../../node_modules/next/dist/lib/constants.d.ts","../../node_modules/next/dist/build/index.d.ts","../../node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../../node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../../node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../../node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../../node_modules/next/dist/server/base-http/node.d.ts","../../node_modules/next/dist/server/font-utils.d.ts","../../node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../../node_modules/next/dist/server/future/route-modules/route-module.d.ts","../../node_modules/next/dist/shared/lib/deep-readonly.d.ts","../../node_modules/next/dist/server/load-components.d.ts","../../node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../../node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../../node_modules/next/dist/server/future/route-definitions/locale-route-definition.d.ts","../../node_modules/next/dist/server/future/route-definitions/pages-route-definition.d.ts","../../node_modules/next/dist/shared/lib/mitt.d.ts","../../node_modules/next/dist/client/with-router.d.ts","../../node_modules/next/dist/client/router.d.ts","../../node_modules/next/dist/client/route-loader.d.ts","../../node_modules/next/dist/client/page-loader.d.ts","../../node_modules/next/dist/shared/lib/bloom-filter.d.ts","../../node_modules/next/dist/shared/lib/router/router.d.ts","../../node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../../node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../../node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../../node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../../node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../../node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../../node_modules/next/dist/server/future/route-definitions/app-page-route-definition.d.ts","../../node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../../node_modules/next/dist/shared/lib/constants.d.ts","../../node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../../node_modules/next/dist/build/page-extensions-type.d.ts","../../node_modules/next/dist/build/webpack/loaders/next-app-loader.d.ts","../../node_modules/next/dist/server/lib/app-dir-module.d.ts","../../node_modules/next/dist/server/response-cache/types.d.ts","../../node_modules/next/dist/server/response-cache/index.d.ts","../../node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../../node_modules/next/dist/client/components/hooks-server-context.d.ts","../../node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../../node_modules/next/dist/client/components/static-generation-async-storage-instance.d.ts","../../node_modules/next/dist/client/components/static-generation-async-storage.external.d.ts","../../node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../../node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../../node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../../node_modules/next/dist/client/components/request-async-storage-instance.d.ts","../../node_modules/next/dist/client/components/request-async-storage.external.d.ts","../../node_modules/next/dist/server/app-render/create-error-handler.d.ts","../../node_modules/next/dist/server/app-render/app-render.d.ts","../../node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../../node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","../../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../../node_modules/next/dist/server/future/route-modules/app-page/module.compiled.d.ts","../../node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/next/dist/client/components/error-boundary.d.ts","../../node_modules/next/dist/client/components/router-reducer/create-initial-router-state.d.ts","../../node_modules/next/dist/client/components/app-router.d.ts","../../node_modules/next/dist/client/components/layout-router.d.ts","../../node_modules/next/dist/client/components/render-from-template-context.d.ts","../../node_modules/next/dist/client/components/action-async-storage-instance.d.ts","../../node_modules/next/dist/client/components/action-async-storage.external.d.ts","../../node_modules/next/dist/client/components/client-page.d.ts","../../node_modules/next/dist/client/components/search-params.d.ts","../../node_modules/next/dist/client/components/not-found-boundary.d.ts","../../node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../../node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../../node_modules/next/dist/server/app-render/rsc/taint.d.ts","../../node_modules/next/dist/server/app-render/entry-base.d.ts","../../node_modules/next/dist/build/templates/app-page.d.ts","../../node_modules/next/dist/server/future/route-modules/app-page/module.d.ts","../../node_modules/next/dist/server/app-render/types.d.ts","../../node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../../node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../../node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/entrypoints.d.ts","../../node_modules/next/dist/server/future/route-modules/pages/module.compiled.d.ts","../../node_modules/next/dist/build/templates/pages.d.ts","../../node_modules/next/dist/server/future/route-modules/pages/module.d.ts","../../node_modules/next/dist/server/render.d.ts","../../node_modules/next/dist/server/future/route-definitions/pages-api-route-definition.d.ts","../../node_modules/next/dist/server/future/route-matches/pages-api-route-match.d.ts","../../node_modules/next/dist/server/future/route-matchers/route-matcher.d.ts","../../node_modules/next/dist/server/future/route-matcher-providers/route-matcher-provider.d.ts","../../node_modules/next/dist/server/future/route-matcher-managers/route-matcher-manager.d.ts","../../node_modules/next/dist/server/future/normalizers/normalizer.d.ts","../../node_modules/next/dist/server/future/normalizers/locale-route-normalizer.d.ts","../../node_modules/next/dist/server/future/normalizers/request/pathname-normalizer.d.ts","../../node_modules/next/dist/server/future/normalizers/request/suffix.d.ts","../../node_modules/next/dist/server/future/normalizers/request/rsc.d.ts","../../node_modules/next/dist/server/future/normalizers/request/prefix.d.ts","../../node_modules/next/dist/server/future/normalizers/request/postponed.d.ts","../../node_modules/next/dist/server/future/normalizers/request/action.d.ts","../../node_modules/next/dist/server/future/normalizers/request/prefetch-rsc.d.ts","../../node_modules/next/dist/server/future/normalizers/request/next-data.d.ts","../../node_modules/next/dist/server/base-server.d.ts","../../node_modules/next/dist/server/image-optimizer.d.ts","../../node_modules/next/dist/server/next-server.d.ts","../../node_modules/next/dist/lib/coalesced-function.d.ts","../../node_modules/next/dist/server/lib/router-utils/types.d.ts","../../node_modules/next/dist/trace/types.d.ts","../../node_modules/next/dist/trace/trace.d.ts","../../node_modules/next/dist/trace/shared.d.ts","../../node_modules/next/dist/trace/index.d.ts","../../node_modules/next/dist/build/load-jsconfig.d.ts","../../node_modules/next/dist/build/webpack-config.d.ts","../../node_modules/next/dist/build/webpack/plugins/define-env-plugin.d.ts","../../node_modules/next/dist/build/swc/index.d.ts","../../node_modules/next/dist/server/dev/parse-version-info.d.ts","../../node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../../node_modules/next/dist/telemetry/storage.d.ts","../../node_modules/next/dist/server/lib/types.d.ts","../../node_modules/next/dist/server/lib/render-server.d.ts","../../node_modules/next/dist/server/lib/router-server.d.ts","../../node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../../node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../../node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../../node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../../node_modules/next/dist/server/dev/static-paths-worker.d.ts","../../node_modules/next/dist/server/dev/next-dev-server.d.ts","../../node_modules/next/dist/server/next.d.ts","../../node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../../node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../../node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../../node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../../node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../../node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../../node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../../node_modules/next/types/index.d.ts","../../node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../../node_modules/@next/env/dist/index.d.ts","../../node_modules/next/dist/shared/lib/utils.d.ts","../../node_modules/next/dist/pages/_app.d.ts","../../node_modules/next/app.d.ts","../../node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../../node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../../node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../../node_modules/next/cache.d.ts","../../node_modules/next/dist/shared/lib/runtime-config.external.d.ts","../../node_modules/next/config.d.ts","../../node_modules/next/dist/pages/_document.d.ts","../../node_modules/next/document.d.ts","../../node_modules/next/dist/shared/lib/dynamic.d.ts","../../node_modules/next/dynamic.d.ts","../../node_modules/next/dist/pages/_error.d.ts","../../node_modules/next/error.d.ts","../../node_modules/next/dist/shared/lib/head.d.ts","../../node_modules/next/head.d.ts","../../node_modules/next/dist/client/components/draft-mode.d.ts","../../node_modules/next/dist/client/components/headers.d.ts","../../node_modules/next/headers.d.ts","../../node_modules/next/dist/shared/lib/get-img-props.d.ts","../../node_modules/next/dist/client/image-component.d.ts","../../node_modules/next/dist/shared/lib/image-external.d.ts","../../node_modules/next/image.d.ts","../../node_modules/next/dist/client/link.d.ts","../../node_modules/next/link.d.ts","../../node_modules/next/dist/client/components/redirect-status-code.d.ts","../../node_modules/next/dist/client/components/redirect.d.ts","../../node_modules/next/dist/client/components/not-found.d.ts","../../node_modules/next/dist/client/components/navigation.react-server.d.ts","../../node_modules/next/dist/client/components/navigation.d.ts","../../node_modules/next/navigation.d.ts","../../node_modules/next/router.d.ts","../../node_modules/next/dist/client/script.d.ts","../../node_modules/next/script.d.ts","../../node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../../node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../../node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../../node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../../node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","../../node_modules/next/dist/compiled/@vercel/og/types.d.ts","../../node_modules/next/server.d.ts","../../node_modules/next/types/global.d.ts","../../node_modules/next/types/compiled.d.ts","../../node_modules/next/index.d.ts","../../node_modules/next/image-types/global.d.ts","../../next-env.d.ts","../../node_modules/source-map-js/source-map.d.ts","../../node_modules/postcss/lib/previous-map.d.ts","../../node_modules/postcss/lib/input.d.ts","../../node_modules/postcss/lib/css-syntax-error.d.ts","../../node_modules/postcss/lib/declaration.d.ts","../../node_modules/postcss/lib/root.d.ts","../../node_modules/postcss/lib/warning.d.ts","../../node_modules/postcss/lib/lazy-result.d.ts","../../node_modules/postcss/lib/no-work-result.d.ts","../../node_modules/postcss/lib/processor.d.ts","../../node_modules/postcss/lib/result.d.ts","../../node_modules/postcss/lib/document.d.ts","../../node_modules/postcss/lib/rule.d.ts","../../node_modules/postcss/lib/node.d.ts","../../node_modules/postcss/lib/comment.d.ts","../../node_modules/postcss/lib/container.d.ts","../../node_modules/postcss/lib/at-rule.d.ts","../../node_modules/postcss/lib/list.d.ts","../../node_modules/postcss/lib/postcss.d.ts","../../node_modules/postcss/lib/postcss.d.mts","../../node_modules/tailwindcss/types/generated/corepluginlist.d.ts","../../node_modules/tailwindcss/types/generated/colors.d.ts","../../node_modules/tailwindcss/types/config.d.ts","../../node_modules/tailwindcss/types/index.d.ts","../../tailwind.config.ts","../../src/lib/api.ts","../../src/app/layout.tsx","../../node_modules/motion-dom/dist/index.d.ts","../../node_modules/motion-utils/dist/index.d.ts","../../node_modules/framer-motion/dist/index.d.ts","../../node_modules/lucide-react/dist/lucide-react.d.ts","../../src/app/components/statcard.tsx","../../src/app/components/statusbadge.tsx","../../src/app/components/overviewtab.tsx","../../src/app/components/detailpanels.tsx","../../src/app/page.tsx","../types/app/layout.ts","../types/app/page.ts","../../node_modules/@types/d3-array/index.d.ts","../../node_modules/@types/d3-color/index.d.ts","../../node_modules/@types/d3-ease/index.d.ts","../../node_modules/@types/d3-interpolate/index.d.ts","../../node_modules/@types/d3-path/index.d.ts","../../node_modules/@types/d3-time/index.d.ts","../../node_modules/@types/d3-scale/index.d.ts","../../node_modules/@types/d3-shape/index.d.ts","../../node_modules/@types/d3-timer/index.d.ts","../../node_modules/@types/json5/index.d.ts"],"fileIdsList":[[99,145,359,436],[99,145,359,445],[99,145,407,408],[99,145],[99,145,449],[99,145,453],[99,145,452],[99,142,145],[99,144,145],[145],[99,145,150,178],[99,145,146,151,156,164,175,186],[99,145,146,147,156,164],[94,95,96,99,145],[99,145,148,187],[99,145,149,150,157,165],[99,145,150,175,183],[99,145,151,153,156,164],[99,144,145,152],[99,145,153,154],[99,145,155,156],[99,144,145,156],[99,145,156,157,158,175,186],[99,145,156,157,158,171,175,178],[99,145,153,156,159,164,175,186],[99,145,156,157,159,160,164,175,183,186],[99,145,159,161,175,183,186],[97,98,99,100,101,102,103,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192],[99,145,156,162],[99,145,163,186,191],[99,145,153,156,164,175],[99,145,165],[99,145,166],[99,144,145,167],[99,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192],[99,145,169],[99,145,170],[99,145,156,171,172],[99,145,171,173,187,189],[99,145,156,175,176,178],[99,145,177,178],[99,145,175,176],[99,145,178],[99,145,179],[99,142,145,175,180],[99,145,156,181,182],[99,145,181,182],[99,145,150,164,175,183],[99,145,184],[99,145,164,185],[99,145,159,170,186],[99,145,150,187],[99,145,175,188],[99,145,163,189],[99,145,190],[99,140,145],[99,140,145,156,158,167,175,178,186,189,191],[99,145,175,192],[87,99,145,197,198,199],[87,99,145,197,198],[87,99,145],[87,91,99,145,196,360,403],[87,91,99,145,195,360,403],[84,85,86,99,145],[87,99,145,286,437,438],[92,99,145],[99,145,364],[99,145,366,367,368],[99,145,370],[99,145,202,212,218,220,360],[99,145,202,209,211,214,232],[99,145,212],[99,145,212,214,338],[99,145,267,285,300,406],[99,145,308],[99,145,202,212,219,253,263,335,336,406],[99,145,219,406],[99,145,212,263,264,265,406],[99,145,212,219,253,406],[99,145,406],[99,145,202,219,220,406],[99,145,293],[99,144,145,193,292],[87,99,145,286,287,288,305,306],[87,99,145,286],[99,145,276],[99,145,275,277,380],[87,99,145,286,287,303],[99,145,282,306,392],[99,145,390,391],[99,145,226,389],[99,145,279],[99,144,145,193,226,242,275,276,277,278],[87,99,145,303,305,306],[99,145,303,305],[99,145,303,304,306],[99,145,170,193],[99,145,274],[99,144,145,193,211,213,270,271,272,273],[87,99,145,203,383],[87,99,145,186,193],[87,99,145,219,251],[87,99,145,219],[99,145,249,254],[87,99,145,250,363],[87,91,99,145,159,193,195,196,360,401,402],[99,145,360],[99,145,201],[99,145,353,354,355,356,357,358],[99,145,355],[87,99,145,250,286,363],[87,99,145,286,361,363],[87,99,145,286,363],[99,145,159,193,213,363],[99,145,159,193,210,211,222,240,242,274,279,280,302,303],[99,145,271,274,279,287,289,290,291,293,294,295,296,297,298,299,406],[99,145,272],[87,99,145,170,193,211,212,240,242,243,245,270,302,306,360,406],[99,145,159,193,213,214,226,227,275],[99,145,159,193,212,214],[99,145,159,175,193,210,213,214],[99,145,159,170,186,193,210,211,212,213,214,219,222,223,233,234,236,239,240,242,243,244,245,269,270,303,311,313,316,318,321,323,324,325,326],[99,145,159,175,193],[99,145,202,203,204,210,211,360,363,406],[99,145,159,175,186,193,207,337,339,340,406],[99,145,170,186,193,207,210,213,230,234,236,237,238,243,270,316,327,329,335,349,350],[99,145,212,216,270],[99,145,210,212],[99,145,223,317],[99,145,319,320],[99,145,319],[99,145,317],[99,145,319,322],[99,145,206,207],[99,145,206,246],[99,145,206],[99,145,208,223,315],[99,145,314],[99,145,207,208],[99,145,208,312],[99,145,207],[99,145,302],[99,145,159,193,210,222,241,261,267,281,284,301,303],[99,145,255,256,257,258,259,260,282,283,306,361],[99,145,310],[99,145,159,193,210,222,241,247,307,309,311,360,363],[99,145,159,186,193,203,210,212,269],[99,145,266],[99,145,159,193,343,348],[99,145,233,242,269,363],[99,145,331,335,349,352],[99,145,159,216,335,343,344,352],[99,145,202,212,233,244,346],[99,145,159,193,212,219,244,330,331,341,342,345,347],[99,145,194,240,241,242,360,363],[99,145,159,170,186,193,208,210,211,213,216,221,222,230,233,234,236,237,238,239,243,245,269,270,313,327,328,363],[99,145,159,193,210,212,216,329,351],[99,145,159,193,211,213],[87,99,145,159,170,193,201,203,210,211,214,222,239,240,242,243,245,310,360,363],[99,145,159,170,186,193,205,208,209,213],[99,145,206,268],[99,145,159,193,206,211,222],[99,145,159,193,212,223],[99,145,159,193],[99,145,226],[99,145,225],[99,145,227],[99,145,212,224,226,230],[99,145,212,224,226],[99,145,159,193,205,212,213,219,227,228,229],[87,99,145,303,304,305],[99,145,262],[87,99,145,203],[87,99,145,236],[87,99,145,194,239,242,245,360,363],[99,145,203,383,384],[87,99,145,254],[87,99,145,170,186,193,201,248,250,252,253,363],[99,145,213,219,236],[99,145,235],[87,99,145,157,159,170,193,201,254,263,360,361,362],[83,87,88,89,90,99,145,195,196,360,403],[99,145,150],[99,145,332,333,334],[99,145,332],[99,145,372],[99,145,374],[99,145,376],[99,145,378],[99,145,381],[99,145,385],[91,93,99,145,360,365,369,371,373,375,377,379,382,386,388,394,395,397,404,405,406],[99,145,387],[99,145,393],[99,145,250],[99,145,396],[99,144,145,227,228,229,230,398,399,400,403],[99,145,193],[87,91,99,145,159,161,170,193,195,196,197,199,201,214,352,359,363,403],[99,145,425],[99,145,423,425],[99,145,414,422,423,424,426,428],[99,145,412],[99,145,415,420,425,428],[99,145,411,428],[99,145,415,416,419,420,421,428],[99,145,415,416,417,419,420,428],[99,145,412,413,414,415,416,420,421,422,424,425,426,428],[99,145,428],[99,145,410,412,413,414,415,416,417,419,420,421,422,423,424,425,426,427],[99,145,410,428],[99,145,415,417,418,420,421,428],[99,145,419,428],[99,145,420,421,425,428],[99,145,413,423],[99,145,430,431],[99,145,429,432],[99,112,116,145,186],[99,112,145,175,186],[99,107,145],[99,109,112,145,183,186],[99,145,164,183],[99,107,145,193],[99,109,112,145,164,186],[99,104,105,108,111,145,156,175,186],[99,112,119,145],[99,104,110,145],[99,112,133,134,145],[99,108,112,145,178,186,193],[99,133,145,193],[99,106,107,145,193],[99,112,145],[99,106,107,108,109,110,111,112,113,114,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,134,135,136,137,138,139,145],[99,112,127,145],[99,112,119,120,145],[99,110,112,120,121,145],[99,111,145],[99,104,107,112,145],[99,112,116,120,121,145],[99,116,145],[99,110,112,115,145,186],[99,104,109,112,119,145],[99,145,175],[99,107,112,133,145,191,193],[87,99,145,439,440,441,442],[99,145,439,440,441,442],[99,145,439,440],[99,145,407],[87,99,145,435,439,440,443,444],[99,145,433]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","signature":false,"impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","signature":false,"impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","signature":false,"impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","signature":false,"impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","signature":false,"impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","signature":false,"impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","signature":false,"impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","signature":false,"impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","signature":false,"impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","signature":false,"impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","signature":false,"impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0990a7576222f248f0a3b888adcb7389f957928ce2afb1cd5128169086ff4d29","signature":false,"impliedFormat":1},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","signature":false,"impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","signature":false,"impliedFormat":1},{"version":"09ddcfcfbe77a8232d155ca1030005106b1328f6210df43629d0be750da07c16","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"cc69795d9954ee4ad57545b10c7bf1a7260d990231b1685c147ea71a6faa265c","signature":false,"impliedFormat":1},{"version":"8bc6c94ff4f2af1f4023b7bb2379b08d3d7dd80c698c9f0b07431ea16101f05f","signature":false,"impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","signature":false,"impliedFormat":1},{"version":"57194e1f007f3f2cbef26fa299d4c6b21f4623a2eddc63dfeef79e38e187a36e","signature":false,"impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","signature":false,"impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","signature":false,"impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","signature":false,"impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"ba481bca06f37d3f2c137ce343c7d5937029b2468f8e26111f3c9d9963d6568d","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ef24f9a22a88e3e9b3b3d8c40ab1ddb0853f1bfbd5c843c37800138437b61","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","signature":false,"impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","signature":false,"impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","signature":false,"impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","signature":false,"impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","signature":false,"impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","signature":false,"impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","signature":false,"impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","signature":false,"impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","signature":false,"impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","signature":false,"impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","signature":false,"impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","signature":false,"impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","signature":false,"impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","signature":false,"impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","signature":false,"impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","signature":false,"impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","signature":false,"impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","signature":false,"impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","signature":false,"impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","signature":false,"impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","signature":false,"impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","signature":false,"impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","signature":false,"impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","signature":false,"impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","signature":false,"impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","signature":false,"impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","signature":false,"impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","signature":false,"impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","signature":false,"impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","signature":false,"impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","signature":false,"impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","signature":false,"impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","signature":false,"impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","signature":false,"impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","signature":false,"impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","signature":false,"impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","signature":false,"impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"e2677634fe27e87348825bb041651e22d50a613e2fdf6a4a3ade971d71bac37e","signature":false,"impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","signature":false,"impliedFormat":1},{"version":"8c0bcd6c6b67b4b503c11e91a1fb91522ed585900eab2ab1f61bba7d7caa9d6f","signature":false,"impliedFormat":1},{"version":"8cd19276b6590b3ebbeeb030ac271871b9ed0afc3074ac88a94ed2449174b776","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"696eb8d28f5949b87d894b26dc97318ef944c794a9a4e4f62360cd1d1958014b","signature":false,"impliedFormat":1},{"version":"3f8fa3061bd7402970b399300880d55257953ee6d3cd408722cb9ac20126460c","signature":false,"impliedFormat":1},{"version":"35ec8b6760fd7138bbf5809b84551e31028fb2ba7b6dc91d95d098bf212ca8b4","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","signature":false,"impliedFormat":1},{"version":"68bd56c92c2bd7d2339457eb84d63e7de3bd56a69b25f3576e1568d21a162398","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"3e93b123f7c2944969d291b35fed2af79a6e9e27fdd5faa99748a51c07c02d28","signature":false,"impliedFormat":1},{"version":"9d19808c8c291a9010a6c788e8532a2da70f811adb431c97520803e0ec649991","signature":false,"impliedFormat":1},{"version":"87aad3dd9752067dc875cfaa466fc44246451c0c560b820796bdd528e29bef40","signature":false,"impliedFormat":1},{"version":"4aacb0dd020eeaef65426153686cc639a78ec2885dc72ad220be1d25f1a439df","signature":false,"impliedFormat":1},{"version":"f0bd7e6d931657b59605c44112eaf8b980ba7f957a5051ed21cb93d978cf2f45","signature":false,"impliedFormat":1},{"version":"8db0ae9cb14d9955b14c214f34dae1b9ef2baee2fe4ce794a4cd3ac2531e3255","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"15fc6f7512c86810273af28f224251a5a879e4261b4d4c7e532abfbfc3983134","signature":false,"impliedFormat":1},{"version":"58adba1a8ab2d10b54dc1dced4e41f4e7c9772cbbac40939c0dc8ce2cdb1d442","signature":false,"impliedFormat":1},{"version":"641942a78f9063caa5d6b777c99304b7d1dc7328076038c6d94d8a0b81fc95c1","signature":false,"impliedFormat":1},{"version":"70e15d5c5c14e26442b2f15484b126a982290c5f4713834a6e572fd4274f9119","signature":false,"impliedFormat":1},{"version":"855cd5f7eb396f5f1ab1bc0f8580339bff77b68a770f84c6b254e319bbfd1ac7","signature":false,"impliedFormat":1},{"version":"5650cf3dace09e7c25d384e3e6b818b938f68f4e8de96f52d9c5a1b3db068e86","signature":false,"impliedFormat":1},{"version":"1354ca5c38bd3fd3836a68e0f7c9f91f172582ba30ab15bb8c075891b91502b7","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"7e20d899c28ca26a2a7afc98beaa69e63ff7fba0a8bc47b4e3bf3ede5e09e424","signature":false,"impliedFormat":1},{"version":"2d2fcaab481b31a5882065c7951255703ddbe1c0e507af56ea42d79ac3911201","signature":false,"impliedFormat":1},{"version":"a192fe8ec33f75edbc8d8f3ed79f768dfae11ff5735e7fe52bfa69956e46d78d","signature":false,"impliedFormat":1},{"version":"ca867399f7db82df981d6915bcbb2d81131d7d1ef683bc782b59f71dda59bc85","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"372413016d17d804e1d139418aca0c68e47a83fb6669490857f4b318de8cccb3","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"9e043a1bc8fbf2a255bccf9bf27e0f1caf916c3b0518ea34aa72357c0afd42ec","signature":false,"impliedFormat":1},{"version":"b4f70ec656a11d570e1a9edce07d118cd58d9760239e2ece99306ee9dfe61d02","signature":false,"impliedFormat":1},{"version":"3bc2f1e2c95c04048212c569ed38e338873f6a8593930cf5a7ef24ffb38fc3b6","signature":false,"impliedFormat":1},{"version":"6e70e9570e98aae2b825b533aa6292b6abd542e8d9f6e9475e88e1d7ba17c866","signature":false,"impliedFormat":1},{"version":"f9d9d753d430ed050dc1bf2667a1bab711ccbb1c1507183d794cc195a5b085cc","signature":false,"impliedFormat":1},{"version":"9eece5e586312581ccd106d4853e861aaaa1a39f8e3ea672b8c3847eedd12f6e","signature":false,"impliedFormat":1},{"version":"085f552d005479e2e6a7311cdbbe5d8c55c497b4d19274285df161ee9684cd9c","signature":false,"impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","signature":false,"impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","signature":false,"impliedFormat":1},{"version":"007faacc9268357caa21d24169f3f3f2497af3e9241308df2d89f6e6d9bb3f2e","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"74cf591a0f63db318651e0e04cb55f8791385f86e987a67fd4d2eaab8191f730","signature":false,"impliedFormat":1},{"version":"5eab9b3dc9b34f185417342436ec3f106898da5f4801992d8ff38ab3aff346b5","signature":false,"impliedFormat":1},{"version":"12ed4559eba17cd977aa0db658d25c4047067444b51acfdcbf38470630642b23","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"f3ffabc95802521e1e4bcba4c88d8615176dc6e09111d920c7a213bdda6e1d65","signature":false,"impliedFormat":1},{"version":"809821b8a065e3234a55b3a9d7846231ed18d66dd749f2494c66288d890daf7f","signature":false,"impliedFormat":1},{"version":"ae56f65caf3be91108707bd8dfbccc2a57a91feb5daabf7165a06a945545ed26","signature":false,"impliedFormat":1},{"version":"a136d5de521da20f31631a0a96bf712370779d1c05b7015d7019a9b2a0446ca9","signature":false,"impliedFormat":1},{"version":"c3b41e74b9a84b88b1dca61ec39eee25c0dbc8e7d519ba11bb070918cfacf656","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"4737a9dc24d0e68b734e6cfbcea0c15a2cfafeb493485e27905f7856988c6b29","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"36d8d3e7506b631c9582c251a2c0b8a28855af3f76719b12b534c6edf952748d","signature":false,"impliedFormat":1},{"version":"1ca69210cc42729e7ca97d3a9ad48f2e9cb0042bada4075b588ae5387debd318","signature":false,"impliedFormat":1},{"version":"f5ebe66baaf7c552cfa59d75f2bfba679f329204847db3cec385acda245e574e","signature":false,"impliedFormat":1},{"version":"ed59add13139f84da271cafd32e2171876b0a0af2f798d0c663e8eeb867732cf","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"b7c5e2ea4a9749097c347454805e933844ed207b6eefec6b7cfd418b5f5f7b28","signature":false,"impliedFormat":1},{"version":"b1810689b76fd473bd12cc9ee219f8e62f54a7d08019a235d07424afbf074d25","signature":false,"impliedFormat":1},{"version":"8caa5c86be1b793cd5f599e27ecb34252c41e011980f7d61ae4989a149ff6ccc","signature":false,"impliedFormat":1},{"version":"f9fd93190acb1ffe0bc0fb395df979452f8d625071e9ffc8636e4dfb86ab2508","signature":false,"impliedFormat":1},{"version":"5f41fd8732a89e940c58ce22206e3df85745feb8983e2b4c6257fb8cbb118493","signature":false,"impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","signature":false,"impliedFormat":1},{"version":"1cfa8647d7d71cb03847d616bd79320abfc01ddea082a49569fda71ac5ece66b","signature":false,"impliedFormat":1},{"version":"bb7a61dd55dc4b9422d13da3a6bb9cc5e89be888ef23bbcf6558aa9726b89a1c","signature":false,"impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","signature":false,"impliedFormat":1},{"version":"cfe4ef4710c3786b6e23dae7c086c70b4f4835a2e4d77b75d39f9046106e83d3","signature":false,"impliedFormat":1},{"version":"cbea99888785d49bb630dcbb1613c73727f2b5a2cf02e1abcaab7bcf8d6bf3c5","signature":false,"impliedFormat":1},{"version":"3989ccb24f2526f7e82cf54268e23ce9e1df5b9982f8acd099ddd4853c26babd","signature":false,"impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","signature":false,"impliedFormat":1},{"version":"2dad084c67e649f0f354739ec7df7c7df0779a28a4f55c97c6b6883ae850d1ce","signature":false,"impliedFormat":1},{"version":"fa5bbc7ab4130dd8cdc55ea294ec39f76f2bc507a0f75f4f873e38631a836ca7","signature":false,"impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","signature":false,"impliedFormat":1},{"version":"cf86de1054b843e484a3c9300d62fbc8c97e77f168bbffb131d560ca0474d4a8","signature":false,"impliedFormat":1},{"version":"196c960b12253fde69b204aa4fbf69470b26daf7a430855d7f94107a16495ab0","signature":false,"impliedFormat":1},{"version":"ee15ea5dd7a9fc9f5013832e5843031817a880bf0f24f37a29fd8337981aae07","signature":false,"impliedFormat":1},{"version":"bf24f6d35f7318e246010ffe9924395893c4e96d34324cde77151a73f078b9ad","signature":false,"impliedFormat":1},{"version":"805c5db07d4b131bede36cc2dbded64cc3c8e49594e53119f4442af183f97935","signature":false,"impliedFormat":1},{"version":"10595c7ff5094dd5b6a959ccb1c00e6a06441b4e10a87bc09c15f23755d34439","signature":false,"impliedFormat":1},{"version":"9620c1ff645afb4a9ab4044c85c26676f0a93e8c0e4b593aea03a89ccb47b6d0","signature":false,"impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","signature":false,"impliedFormat":1},{"version":"a9af0e608929aaf9ce96bd7a7b99c9360636c31d73670e4af09a09950df97841","signature":false,"impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","signature":false,"impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","signature":false,"impliedFormat":1},{"version":"08ed0b3f0166787f84a6606f80aa3b1388c7518d78912571b203817406e471da","signature":false,"impliedFormat":1},{"version":"47e5af2a841356a961f815e7c55d72554db0c11b4cba4d0caab91f8717846a94","signature":false,"impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","signature":false,"impliedFormat":1},{"version":"f5f541902bf7ae0512a177295de9b6bcd6809ea38307a2c0a18bfca72212f368","signature":false,"impliedFormat":1},{"version":"b0decf4b6da3ebc52ea0c96095bdfaa8503acc4ac8e9081c5f2b0824835dd3bd","signature":false,"impliedFormat":1},{"version":"ca1b882a105a1972f82cc58e3be491e7d750a1eb074ffd13b198269f57ed9e1b","signature":false,"impliedFormat":1},{"version":"fc3e1c87b39e5ba1142f27ec089d1966da168c04a859a4f6aab64dceae162c2b","signature":false,"impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","signature":false,"impliedFormat":1},{"version":"61888522cec948102eba94d831c873200aa97d00d8989fdfd2a3e0ee75ec65a2","signature":false,"impliedFormat":1},{"version":"4e10622f89fea7b05dd9b52fb65e1e2b5cbd96d4cca3d9e1a60bb7f8a9cb86a1","signature":false,"impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","signature":false,"impliedFormat":1},{"version":"59bf32919de37809e101acffc120596a9e45fdbab1a99de5087f31fdc36e2f11","signature":false,"impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","signature":false,"impliedFormat":1},{"version":"b3aa6ede7dda2ee53ee78f257d5d6188f6ba75ac0a34a4b88be4ca93b869da07","signature":false,"impliedFormat":1},{"version":"c40c848daad198266370c1c72a7a8c3d18d2f50727c7859fcfefd3ff69a7f288","signature":false,"impliedFormat":1},{"version":"ac60bbee0d4235643cc52b57768b22de8c257c12bd8c2039860540cab1fa1d82","signature":false,"impliedFormat":1},{"version":"6428e6edd944ce6789afdf43f9376c1f2e4957eea34166177625aaff4c0da1a0","signature":false,"impliedFormat":1},{"version":"ada39cbb2748ab2873b7835c90c8d4620723aedf323550e8489f08220e477c7f","signature":false,"impliedFormat":1},{"version":"6e5f5cee603d67ee1ba6120815497909b73399842254fc1e77a0d5cdc51d8c9c","signature":false,"impliedFormat":1},{"version":"8dba67056cbb27628e9b9a1cba8e57036d359dceded0725c72a3abe4b6c79cd4","signature":false,"impliedFormat":1},{"version":"70f3814c457f54a7efe2d9ce9d2686de9250bb42eb7f4c539bd2280a42e52d33","signature":false,"impliedFormat":1},{"version":"154dd2e22e1e94d5bc4ff7726706bc0483760bae40506bdce780734f11f7ec47","signature":false,"impliedFormat":1},{"version":"ef61792acbfa8c27c9bd113f02731e66229f7d3a169e3c1993b508134f1a58e0","signature":false,"impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","signature":false,"impliedFormat":1},{"version":"15e3409b8397457d761d8d6f8c524795845c3aeb5dd0d4291ca0c54fec670b72","signature":false,"impliedFormat":1},{"version":"f6404e7837b96da3ea4d38c4f1a3812c96c9dcdf264e93d5bdb199f983a3ef4b","signature":false,"impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","signature":false,"impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","signature":false,"impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","signature":false,"impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","signature":false,"impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","signature":false,"impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","signature":false,"impliedFormat":1},{"version":"8b8f00491431fe82f060dfe8c7f2180a9fb239f3d851527db909b83230e75882","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","signature":false,"impliedFormat":1},{"version":"903e299a28282fa7b714586e28409ed73c3b63f5365519776bf78e8cf173db36","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","signature":false,"impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","signature":false,"impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","signature":false,"impliedFormat":1},{"version":"dd3900b24a6a8745efeb7ad27629c0f8a626470ac229c1d73f1fe29d67e44dca","signature":false,"impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","signature":false,"impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","signature":false,"impliedFormat":1},{"version":"ec29be0737d39268696edcec4f5e97ce26f449fa9b7afc2f0f99a86def34a418","signature":false,"impliedFormat":1},{"version":"aeab39e8e0b1a3b250434c3b2bb8f4d17bbec2a9dbce5f77e8a83569d3d2cbc2","signature":false,"impliedFormat":1},{"version":"ec6cba1c02c675e4dd173251b156792e8d3b0c816af6d6ad93f1a55d674591aa","signature":false,"impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","signature":false,"impliedFormat":1},{"version":"d729408dfde75b451530bcae944cf89ee8277e2a9df04d1f62f2abfd8b03c1e1","signature":false,"impliedFormat":1},{"version":"e15d3c84d5077bb4a3adee4c791022967b764dc41cb8fa3cfa44d4379b2c95f5","signature":false,"impliedFormat":1},{"version":"5f58e28cd22e8fc1ac1b3bc6b431869f1e7d0b39e2c21fbf79b9fa5195a85980","signature":false,"impliedFormat":1},{"version":"e1fc1a1045db5aa09366be2b330e4ce391550041fc3e925f60998ca0b647aa97","signature":false,"impliedFormat":1},{"version":"63533978dcda286422670f6e184ac516805a365fb37a086eeff4309e812f1402","signature":false,"impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","signature":false,"impliedFormat":1},{"version":"31fb49ef3aa3d76f0beb644984e01eab0ea222372ea9b49bb6533be5722d756c","signature":false,"impliedFormat":1},{"version":"33cd131e1461157e3e06b06916b5176e7a8ec3fce15a5cfe145e56de744e07d2","signature":false,"impliedFormat":1},{"version":"889ef863f90f4917221703781d9723278db4122d75596b01c429f7c363562b86","signature":false,"impliedFormat":1},{"version":"3556cfbab7b43da96d15a442ddbb970e1f2fc97876d055b6555d86d7ac57dae5","signature":false,"impliedFormat":1},{"version":"437751e0352c6e924ddf30e90849f1d9eb00ca78c94d58d6a37202ec84eb8393","signature":false,"impliedFormat":1},{"version":"48e8af7fdb2677a44522fd185d8c87deff4d36ee701ea003c6c780b1407a1397","signature":false,"impliedFormat":1},{"version":"d11308de5a36c7015bb73adb5ad1c1bdaac2baede4cc831a05cf85efa3cc7f2f","signature":false,"impliedFormat":1},{"version":"38e4684c22ed9319beda6765bab332c724103d3a966c2e5e1c5a49cf7007845f","signature":false,"impliedFormat":1},{"version":"f9812cfc220ecf7557183379531fa409acd249b9e5b9a145d0d52b76c20862de","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"0a403c4aeeb153bc0c1f11458d005f8e5a0af3535c4c93eedc6f7865a3593f8e","signature":false,"impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","signature":false,"impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","signature":false,"impliedFormat":1},{"version":"680793958f6a70a44c8d9ae7d46b7a385361c69ac29dcab3ed761edce1c14ab8","signature":false,"impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","signature":false,"impliedFormat":1},{"version":"b838d4c72740eb0afd284bf7575b74c624b105eff2e8c7b4aeead57e7ac320ff","signature":false,"impliedFormat":1},{"version":"913ddbba170240070bd5921b8f33ea780021bdf42fbdfcd4fcb2691b1884ddde","signature":false,"impliedFormat":1},{"version":"b4e6d416466999ff40d3fe5ceb95f7a8bfb7ac2262580287ac1a8391e5362431","signature":false,"impliedFormat":1},{"version":"5fe23bd829e6be57d41929ac374ee9551ccc3c44cee893167b7b5b77be708014","signature":false,"impliedFormat":1},{"version":"0a626484617019fcfbfc3c1bc1f9e84e2913f1adb73692aa9075817404fb41a1","signature":false,"impliedFormat":1},{"version":"438c7513b1df91dcef49b13cd7a1c4720f91a36e88c1df731661608b7c055f10","signature":false,"impliedFormat":1},{"version":"cf185cc4a9a6d397f416dd28cca95c227b29f0f27b160060a95c0e5e36cda865","signature":false,"impliedFormat":1},{"version":"0086f3e4ad898fd7ca56bb223098acfacf3fa065595182aaf0f6c4a6a95e6fbd","signature":false,"impliedFormat":1},{"version":"efaa078e392f9abda3ee8ade3f3762ab77f9c50b184e6883063a911742a4c96a","signature":false,"impliedFormat":1},{"version":"54a8bb487e1dc04591a280e7a673cdfb272c83f61e28d8a64cf1ac2e63c35c51","signature":false,"impliedFormat":1},{"version":"021a9498000497497fd693dd315325484c58a71b5929e2bbb91f419b04b24cea","signature":false,"impliedFormat":1},{"version":"9385cdc09850950bc9b59cca445a3ceb6fcca32b54e7b626e746912e489e535e","signature":false,"impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","signature":false,"impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","signature":false,"impliedFormat":1},{"version":"84124384abae2f6f66b7fbfc03862d0c2c0b71b826f7dbf42c8085d31f1d3f95","signature":false,"impliedFormat":1},{"version":"63a8e96f65a22604eae82737e409d1536e69a467bb738bec505f4f97cce9d878","signature":false,"impliedFormat":1},{"version":"3fd78152a7031315478f159c6a5872c712ece6f01212c78ea82aef21cb0726e2","signature":false,"impliedFormat":1},{"version":"250f9a1f11580b6b8a0a86835946f048eb605b3a596196741bfe72dc8f6c69cc","signature":false,"impliedFormat":1},{"version":"512fc15cca3a35b8dbbf6e23fe9d07e6f87ad03c895acffd3087ce09f352aad0","signature":false,"impliedFormat":1},{"version":"9a0946d15a005832e432ea0cd4da71b57797efb25b755cc07f32274296d62355","signature":false,"impliedFormat":1},{"version":"a52ff6c0a149e9f370372fc3c715d7f2beee1f3bab7980e271a7ab7d313ec677","signature":false,"impliedFormat":1},{"version":"fd933f824347f9edd919618a76cdb6a0c0085c538115d9a287fa0c7f59957ab3","signature":false,"impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","signature":false,"impliedFormat":1},{"version":"6a1aa3e55bdc50503956c5cd09ae4cd72e3072692d742816f65c66ca14f4dfdd","signature":false,"impliedFormat":1},{"version":"ab75cfd9c4f93ffd601f7ca1753d6a9d953bbedfbd7a5b3f0436ac8a1de60dfa","signature":false,"impliedFormat":1},{"version":"f95180f03d827525ca4f990f49e17ec67198c316dd000afbe564655141f725cd","signature":false,"impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","signature":false,"impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","signature":false,"impliedFormat":1},{"version":"1364f64d2fb03bbb514edc42224abd576c064f89be6a990136774ecdd881a1da","signature":false,"impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","signature":false,"impliedFormat":1},{"version":"950fb67a59be4c2dbe69a5786292e60a5cb0e8612e0e223537784c731af55db1","signature":false,"impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","signature":false,"impliedFormat":1},{"version":"07ca44e8d8288e69afdec7a31fa408ce6ab90d4f3d620006701d5544646da6aa","signature":false,"impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","signature":false,"impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","signature":false,"impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","signature":false,"impliedFormat":1},{"version":"4e4475fba4ed93a72f167b061cd94a2e171b82695c56de9899275e880e06ba41","signature":false,"impliedFormat":1},{"version":"97c5f5d580ab2e4decd0a3135204050f9b97cd7908c5a8fbc041eadede79b2fa","signature":false,"impliedFormat":1},{"version":"c99a3a5f2215d5b9d735aa04cec6e61ed079d8c0263248e298ffe4604d4d0624","signature":false,"impliedFormat":1},{"version":"49b2375c586882c3ac7f57eba86680ff9742a8d8cb2fe25fe54d1b9673690d41","signature":false,"impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","signature":false,"impliedFormat":1},{"version":"9ff1e8df66450af44161c1bfe34bc92c43074cfeec7a0a75f721830e9aabe379","signature":false,"impliedFormat":1},{"version":"3ecfccf916fea7c6c34394413b55eb70e817a73e39b4417d6573e523784e3f8e","signature":false,"impliedFormat":1},{"version":"1630192eac4188881201c64522cd3ef08209d9c4db0f9b5f0889b703dc6d936a","signature":false,"impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","signature":false,"impliedFormat":1},{"version":"f416c9c3eee9d47ff49132c34f96b9180e50485d435d5748f0e8b72521d28d2e","signature":false,"impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","signature":false,"impliedFormat":1},{"version":"14e5cdec6f8ae82dfd0694e64903a0a54abdfe37e1d966de3d4128362acbf35f","signature":false,"impliedFormat":1},{"version":"bbc183d2d69f4b59fd4dd8799ffdf4eb91173d1c4ad71cce91a3811c021bf80c","signature":false,"impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","signature":false,"impliedFormat":1},{"version":"8dbc4134a4b3623fc476be5f36de35c40f2768e2e3d9ed437e0d5f1c4cd850f6","signature":false,"impliedFormat":1},{"version":"4e06330a84dec7287f7ebdd64978f41a9f70a668d3b5edc69d5d4a50b9b376bb","signature":false,"impliedFormat":1},{"version":"65bfa72967fbe9fc33353e1ac03f0480aa2e2ea346d61ff3ea997dfd850f641a","signature":false,"impliedFormat":1},{"version":"c06f0bb92d1a1a5a6c6e4b5389a5664d96d09c31673296cb7da5fe945d54d786","signature":false,"impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","signature":false,"impliedFormat":1},{"version":"872caaa31423f4345983d643e4649fb30f548e9883a334d6d1c5fff68ede22d4","signature":false,"impliedFormat":1},{"version":"94404c4a878fe291e7578a2a80264c6f18e9f1933fbb57e48f0eb368672e389c","signature":false,"impliedFormat":1},{"version":"5c1b7f03aa88be854bc15810bfd5bd5a1943c5a7620e1c53eddd2a013996343e","signature":false,"impliedFormat":1},{"version":"09dfc64fcd6a2785867f2368419859a6cc5a8d4e73cbe2538f205b1642eb0f51","signature":false,"impliedFormat":1},{"version":"bcf6f0a323653e72199105a9316d91463ad4744c546d1271310818b8cef7c608","signature":false,"impliedFormat":1},{"version":"01aa917531e116485beca44a14970834687b857757159769c16b228eb1e49c5f","signature":false,"impliedFormat":1},{"version":"351475f9c874c62f9b45b1f0dc7e2704e80dfd5f1af83a3a9f841f9dfe5b2912","signature":false,"impliedFormat":1},{"version":"ac457ad39e531b7649e7b40ee5847606eac64e236efd76c5d12db95bf4eacd17","signature":false,"impliedFormat":1},{"version":"187a6fdbdecb972510b7555f3caacb44b58415da8d5825d03a583c4b73fde4cf","signature":false,"impliedFormat":1},{"version":"d4c3250105a612202289b3a266bb7e323db144f6b9414f9dea85c531c098b811","signature":false,"impliedFormat":1},{"version":"95b444b8c311f2084f0fb51c616163f950fb2e35f4eaa07878f313a2d36c98a4","signature":false,"impliedFormat":1},{"version":"741067675daa6d4334a2dc80a4452ca3850e89d5852e330db7cb2b5f867173b1","signature":false,"impliedFormat":1},{"version":"f8acecec1114f11690956e007d920044799aefeb3cece9e7f4b1f8a1d542b2c9","signature":false,"impliedFormat":1},{"version":"178071ccd043967a58c5d1a032db0ddf9bd139e7920766b537d9783e88eb615e","signature":false,"impliedFormat":1},{"version":"3a17f09634c50cce884721f54fd9e7b98e03ac505889c560876291fcf8a09e90","signature":false,"impliedFormat":1},{"version":"32531dfbb0cdc4525296648f53b2b5c39b64282791e2a8c765712e49e6461046","signature":false,"impliedFormat":1},{"version":"0ce1b2237c1c3df49748d61568160d780d7b26693bd9feb3acb0744a152cd86d","signature":false,"impliedFormat":1},{"version":"e489985388e2c71d3542612685b4a7db326922b57ac880f299da7026a4e8a117","signature":false,"impliedFormat":1},{"version":"5cad4158616d7793296dd41e22e1257440910ea8d01c7b75045d4dfb20c5a41a","signature":false,"impliedFormat":1},{"version":"04d3aad777b6af5bd000bfc409907a159fe77e190b9d368da4ba649cdc28d39e","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"74efc1d6523bd57eb159c18d805db4ead810626bc5bc7002a2c7f483044b2e0f","signature":false,"impliedFormat":1},{"version":"19252079538942a69be1645e153f7dbbc1ef56b4f983c633bf31fe26aeac32cd","signature":false,"impliedFormat":1},{"version":"bc11f3ac00ac060462597add171220aed628c393f2782ac75dd29ff1e0db871c","signature":false,"impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","signature":false,"impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","signature":false,"impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","signature":false,"impliedFormat":1},{"version":"3b0b1d352b8d2e47f1c4df4fb0678702aee071155b12ef0185fce9eb4fa4af1e","signature":false,"impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","signature":false,"impliedFormat":1},{"version":"a344403e7a7384e0e7093942533d309194ad0a53eca2a3100c0b0ab4d3932773","signature":false,"impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","signature":false,"impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","signature":false,"impliedFormat":1},{"version":"bb18bf4a61a17b4a6199eb3938ecfa4a59eb7c40843ad4a82b975ab6f7e3d925","signature":false,"impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","signature":false,"impliedFormat":1},{"version":"e9b6fc05f536dfddcdc65dbcf04e09391b1c968ab967382e48924f5cb90d88e1","signature":false,"impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","signature":false,"impliedFormat":1},{"version":"2b664c3cc544d0e35276e1fb2d4989f7d4b4027ffc64da34ec83a6ccf2e5c528","signature":false,"impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","signature":false,"impliedFormat":1},{"version":"3cd8f0464e0939b47bfccbb9bb474a6d87d57210e304029cd8eb59c63a81935d","signature":false,"impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","signature":false,"impliedFormat":1},{"version":"3026abd48e5e312f2328629ede6e0f770d21c3cd32cee705c450e589d015ee09","signature":false,"impliedFormat":1},{"version":"8b140b398a6afbd17cc97c38aea5274b2f7f39b1ae5b62952cfe65bf493e3e75","signature":false,"impliedFormat":1},{"version":"7663d2c19ce5ef8288c790edba3d45af54e58c84f1b37b1249f6d49d962f3d91","signature":false,"impliedFormat":1},{"version":"5cce3b975cdb72b57ae7de745b3c5de5790781ee88bcb41ba142f07c0fa02e97","signature":false,"impliedFormat":1},{"version":"00bd6ebe607246b45296aa2b805bd6a58c859acecda154bfa91f5334d7c175c6","signature":false,"impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","signature":false,"impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","signature":false,"impliedFormat":1},{"version":"0d28b974a7605c4eda20c943b3fa9ae16cb452c1666fc9b8c341b879992c7612","signature":false,"impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","signature":false,"impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","signature":false,"impliedFormat":1},{"version":"87ac2fb61e629e777f4d161dff534c2023ee15afd9cb3b1589b9b1f014e75c58","signature":false,"impliedFormat":1},{"version":"13c8b4348db91e2f7d694adc17e7438e6776bc506d5c8f5de9ad9989707fa3fe","signature":false,"impliedFormat":1},{"version":"3c1051617aa50b38e9efaabce25e10a5dd9b1f42e372ef0e8a674076a68742ed","signature":false,"impliedFormat":1},{"version":"07a3e20cdcb0f1182f452c0410606711fbea922ca76929a41aacb01104bc0d27","signature":false,"impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","signature":false,"impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","signature":false,"impliedFormat":1},{"version":"4cd4b6b1279e9d744a3825cbd7757bbefe7f0708f3f1069179ad535f19e8ed2c","signature":false,"impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","signature":false,"impliedFormat":1},{"version":"c0eeaaa67c85c3bb6c52b629ebbfd3b2292dc67e8c0ffda2fc6cd2f78dc471e6","signature":false,"impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","signature":false,"impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","signature":false,"impliedFormat":1},{"version":"b95a6f019095dd1d48fd04965b50dfd63e5743a6e75478343c46d2582a5132bf","signature":false,"impliedFormat":99},{"version":"c2008605e78208cfa9cd70bd29856b72dda7ad89df5dc895920f8e10bcb9cd0a","signature":false,"impliedFormat":99},{"version":"b97cb5616d2ab82a98ec9ada7b9e9cabb1f5da880ec50ea2b8dc5baa4cbf3c16","signature":false,"impliedFormat":99},{"version":"d23df9ff06ae8bf1dcb7cc933e97ae7da418ac77749fecee758bb43a8d69f840","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"040c71dde2c406f869ad2f41e8d4ce579cc60c8dbe5aa0dd8962ac943b846572","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"3586f5ea3cc27083a17bd5c9059ede9421d587286d5a47f4341a4c2d00e4fa91","signature":false,"impliedFormat":1},{"version":"a6df929821e62f4719551f7955b9f42c0cd53c1370aec2dd322e24196a7dfe33","signature":false,"impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","signature":false,"impliedFormat":1},{"version":"9dd9d642cdb87d4d5b3173217e0c45429b3e47a6f5cf5fb0ead6c644ec5fed01","signature":false},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","signature":false,"impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","signature":false,"impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","signature":false,"impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","signature":false,"impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","signature":false,"impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","signature":false,"impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","signature":false,"impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","signature":false,"impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","signature":false,"impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","signature":false,"impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","signature":false,"impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","signature":false,"impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","signature":false,"impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","signature":false,"impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","signature":false,"impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","signature":false,"impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","signature":false,"impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","signature":false,"impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","signature":false,"impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","signature":false,"impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","signature":false,"impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","signature":false,"impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","signature":false,"impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","signature":false,"impliedFormat":1},{"version":"d13fcfb0807c8de36b9e980c36b3e3848e1011d5c509d637df8101f337855d07","signature":false},{"version":"5f90c26a1b40633981b0114e67dd3ae28dc1c86fbb300a7801c05de36e5f3446","signature":false},{"version":"f716cd084ae2d80534e3661169a3ab9edd675256f183e09e4a84e53e93564c6a","signature":false},{"version":"38479e9851ea5f43f60baaa6bc894a49dba0a74dd706ce592d32bcb8b59e3be9","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"9592f843d45105b9335c4cd364b9b2562ce4904e0895152206ac4f5b2d1bb212","signature":false,"impliedFormat":1},{"version":"f9ff719608ace88cae7cb823f159d5fb82c9550f2f7e6e7d0f4c6e41d4e4edb4","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"46c9b97d2cf765a080a86b1b9bf1c240f9b02ecc2cc1ef5168f20207d9559c27","signature":false,"impliedFormat":1},{"version":"d5fbf3abca178dd26a23e8d1bfede220ea7a580807a7dd15fe96774413226c38","signature":false},{"version":"cbf2e7a52ec1514e8b65407e99831da68ef5819f16faa3718124fa18ecf97b33","signature":false},{"version":"4e1c8c08c7d68a0f6b68eea2f026e3138bb32fc92e62b4687ee09a847ad5378f","signature":false},{"version":"8cd677b412f435b6085624885e7a3cca23c4f44b6b3889067373cd1c8a2086e1","signature":false},{"version":"f17315daf8d05cba60898d7b48224bad4036416c4f27db626aff242c5dc218f8","signature":false},{"version":"cb1aa42dea768e7ee2994cb51ef067d956caba9b955bd4535da0f21eeebd1361","signature":false},{"version":"485ca72ebea8615b95b8e8733ba181d796ca7354bc900850f50637b5ec966d7e","signature":false},{"version":"b1538a92b9bae8d230267210c5db38c2eb6bdb352128a3ce3aa8c6acf9fc9622","signature":false,"impliedFormat":1},{"version":"6fc1a4f64372593767a9b7b774e9b3b92bf04e8785c3f9ea98973aa9f4bbe490","signature":false,"impliedFormat":1},{"version":"ff09b6fbdcf74d8af4e131b8866925c5e18d225540b9b19ce9485ca93e574d84","signature":false,"impliedFormat":1},{"version":"d5895252efa27a50f134a9b580aa61f7def5ab73d0a8071f9b5bf9a317c01c2d","signature":false,"impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","signature":false,"impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","signature":false,"impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","signature":false,"impliedFormat":1},{"version":"9b048390bcffe88c023a4cd742a720b41d4cd7df83bc9270e6f2339bf38de278","signature":false,"affectsGlobalScope":true,"impliedFormat":1},{"version":"1f366bde16e0513fa7b64f87f86689c4d36efd85afce7eb24753e9c99b91c319","signature":false,"impliedFormat":1},{"version":"96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","signature":false,"impliedFormat":1}],"root":[409,[434,436],[441,447]],"options":{"allowJs":true,"composite":false,"declarationMap":false,"emitDeclarationOnly":false,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":4,"tsBuildInfoFile":"./.tsbuildinfo"},"referencedMap":[[446,1],[447,2],[409,3],[362,4],[448,4],[449,4],[450,4],[451,5],[452,4],[454,6],[455,7],[453,4],[456,4],[457,4],[142,8],[143,8],[144,9],[99,10],[145,11],[146,12],[147,13],[94,4],[97,14],[95,4],[96,4],[148,15],[149,16],[150,17],[151,18],[152,19],[153,20],[154,20],[155,21],[156,22],[157,23],[158,24],[100,4],[98,4],[159,25],[160,26],[161,27],[193,28],[162,29],[163,30],[164,31],[165,32],[166,33],[167,34],[168,35],[169,36],[170,37],[171,38],[172,38],[173,39],[174,4],[175,40],[177,41],[176,42],[178,43],[179,44],[180,45],[181,46],[182,47],[183,48],[184,49],[185,50],[186,51],[187,52],[188,53],[189,54],[190,55],[101,4],[102,4],[103,4],[141,56],[191,57],[192,58],[86,4],[198,59],[199,60],[197,61],[195,62],[196,63],[84,4],[87,64],[286,61],[85,4],[439,65],[440,61],[437,4],[438,4],[93,66],[365,67],[369,68],[371,69],[219,70],[233,71],[336,72],[265,4],[339,73],[301,74],[309,75],[337,76],[220,77],[264,4],[266,78],[338,79],[240,80],[221,81],[245,80],[234,80],[204,80],[292,82],[293,83],[209,4],[289,84],[294,85],[380,86],[287,85],[381,87],[271,4],[290,88],[393,89],[392,90],[296,85],[391,4],[389,4],[390,91],[291,61],[278,92],[279,93],[288,94],[304,95],[305,96],[295,97],[273,98],[274,99],[384,100],[387,101],[252,102],[251,103],[250,104],[396,61],[249,105],[225,4],[399,4],[402,4],[401,61],[403,106],[200,4],[330,4],[232,107],[202,108],[353,4],[354,4],[356,4],[359,109],[355,4],[357,110],[358,110],[218,4],[231,4],[364,111],[372,112],[376,113],[214,114],[281,115],[280,4],[272,98],[300,116],[298,117],[297,4],[299,4],[303,118],[276,119],[213,120],[238,121],[327,122],[205,123],[212,124],[201,72],[341,125],[351,126],[340,4],[350,127],[239,4],[223,128],[318,129],[317,4],[324,130],[326,131],[319,132],[323,133],[325,130],[322,132],[321,130],[320,132],[261,134],[246,134],[312,135],[247,135],[207,136],[206,4],[316,137],[315,138],[314,139],[313,140],[208,141],[285,142],[302,143],[284,144],[308,145],[310,146],[307,144],[241,141],[194,4],[328,147],[267,148],[349,149],[270,150],[344,151],[211,4],[345,152],[347,153],[348,154],[331,4],[343,123],[243,155],[329,156],[352,157],[215,4],[217,4],[222,158],[311,159],[210,160],[216,4],[269,161],[268,162],[224,163],[277,164],[275,165],[226,166],[228,167],[400,4],[227,168],[229,169],[367,4],[366,4],[368,4],[398,4],[230,170],[283,61],[92,4],[306,171],[253,4],[263,172],[242,4],[374,61],[383,173],[260,61],[378,85],[259,174],[361,175],[258,173],[203,4],[385,176],[256,61],[257,61],[248,4],[262,4],[255,177],[254,178],[244,179],[237,97],[346,4],[236,180],[235,4],[370,4],[282,61],[363,181],[83,4],[91,182],[88,61],[89,4],[90,4],[342,183],[335,184],[334,4],[333,185],[332,4],[373,186],[375,187],[377,188],[379,189],[382,190],[408,191],[386,191],[407,192],[388,193],[394,194],[395,195],[397,196],[404,197],[406,4],[405,198],[360,199],[426,200],[424,201],[425,202],[413,203],[414,201],[421,204],[412,205],[417,206],[427,4],[418,207],[423,208],[429,209],[428,210],[411,211],[419,212],[420,213],[415,214],[422,200],[416,215],[410,4],[432,216],[431,4],[430,4],[433,217],[81,4],[82,4],[13,4],[14,4],[16,4],[15,4],[2,4],[17,4],[18,4],[19,4],[20,4],[21,4],[22,4],[23,4],[24,4],[3,4],[25,4],[26,4],[4,4],[27,4],[31,4],[28,4],[29,4],[30,4],[32,4],[33,4],[34,4],[5,4],[35,4],[36,4],[37,4],[38,4],[6,4],[42,4],[39,4],[40,4],[41,4],[43,4],[7,4],[44,4],[49,4],[50,4],[45,4],[46,4],[47,4],[48,4],[8,4],[54,4],[51,4],[52,4],[53,4],[55,4],[9,4],[56,4],[57,4],[58,4],[60,4],[59,4],[61,4],[62,4],[10,4],[63,4],[64,4],[65,4],[11,4],[66,4],[67,4],[68,4],[69,4],[70,4],[1,4],[71,4],[72,4],[12,4],[76,4],[74,4],[79,4],[78,4],[73,4],[77,4],[75,4],[80,4],[119,218],[129,219],[118,218],[139,220],[110,221],[109,222],[138,198],[132,223],[137,224],[112,225],[126,226],[111,227],[135,228],[107,229],[106,198],[136,230],[108,231],[113,232],[114,4],[117,232],[104,4],[140,233],[130,234],[121,235],[122,236],[124,237],[120,238],[123,239],[133,198],[115,240],[116,241],[125,242],[105,243],[128,234],[127,232],[131,4],[134,244],[444,245],[443,246],[441,247],[442,4],[436,248],[445,249],[435,4],[434,250]],"changeFileSet":[446,447,409,362,448,449,450,451,452,454,455,453,456,457,142,143,144,99,145,146,147,94,97,95,96,148,149,150,151,152,153,154,155,156,157,158,100,98,159,160,161,193,162,163,164,165,166,167,168,169,170,171,172,173,174,175,177,176,178,179,180,181,182,183,184,185,186,187,188,189,190,101,102,103,141,191,192,86,198,199,197,195,196,84,87,286,85,439,440,437,438,93,365,369,371,219,233,336,265,339,301,309,337,220,264,266,338,240,221,245,234,204,292,293,209,289,294,380,287,381,271,290,393,392,296,391,389,390,291,278,279,288,304,305,295,273,274,384,387,252,251,250,396,249,225,399,402,401,403,200,330,232,202,353,354,356,359,355,357,358,218,231,364,372,376,214,281,280,272,300,298,297,299,303,276,213,238,327,205,212,201,341,351,340,350,239,223,318,317,324,326,319,323,325,322,321,320,261,246,312,247,207,206,316,315,314,313,208,285,302,284,308,310,307,241,194,328,267,349,270,344,211,345,347,348,331,343,243,329,352,215,217,222,311,210,216,269,268,224,277,275,226,228,400,227,229,367,366,368,398,230,283,92,306,253,263,242,374,383,260,378,259,361,258,203,385,256,257,248,262,255,254,244,237,346,236,235,370,282,363,83,91,88,89,90,342,335,334,333,332,373,375,377,379,382,408,386,407,388,394,395,397,404,406,405,360,426,424,425,413,414,421,412,417,427,418,423,429,428,411,419,420,415,422,416,410,432,431,430,433,81,82,13,14,16,15,2,17,18,19,20,21,22,23,24,3,25,26,4,27,31,28,29,30,32,33,34,5,35,36,37,38,6,42,39,40,41,43,7,44,49,50,45,46,47,48,8,54,51,52,53,55,9,56,57,58,60,59,61,62,10,63,64,65,11,66,67,68,69,70,1,71,72,12,76,74,79,78,73,77,75,80,119,129,118,139,110,109,138,132,137,112,126,111,135,107,106,136,108,113,114,117,104,140,130,121,122,124,120,123,133,115,116,125,105,128,127,131,134,444,443,441,442,436,445,435,434],"version":"5.9.3"}
|
ui/.next/cache/webpack/client-production/0.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:79291f047a4c92ba4d0d4f73ae7b03536935dd7c65ef9947cdce191d01472844
|
| 3 |
+
size 26328053
|
ui/.next/cache/webpack/client-production/1.pack
ADDED
|
Binary file (854 Bytes). View file
|
|
|
ui/.next/cache/webpack/client-production/2.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9874c0ca61bada1a4fecc06267490a3376cea91fbb7b511b94d0c0125380d334
|
| 3 |
+
size 106044
|
ui/.next/cache/webpack/client-production/index.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f5c4ff97e44fc0a1f650e5969e7fcc6397cffdfd900233c896caf02c282f9df6
|
| 3 |
+
size 7251295
|
ui/.next/cache/webpack/client-production/index.pack.old
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:91c0187ce610c64ac77f647f7a7abade961ad65c9177be30038bbe9512e052c2
|
| 3 |
+
size 7250936
|
ui/.next/cache/webpack/edge-server-production/0.pack
ADDED
|
Binary file (274 Bytes). View file
|
|
|
ui/.next/cache/webpack/edge-server-production/index.pack
ADDED
|
Binary file (11.6 kB). View file
|
|
|
ui/.next/cache/webpack/server-production/0.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:efc96aabec5966c887f621707f517bafbf490cbb72f043aa01410167784dbfc3
|
| 3 |
+
size 19927603
|
ui/.next/cache/webpack/server-production/index.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2cac6e01c724fec14d4239fed761b707de8d1f54cb8959af5829295025e2469d
|
| 3 |
+
size 5175210
|
ui/.next/export-marker.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":1,"hasExportPathMap":false,"exportTrailingSlash":false,"isNextImageImported":false}
|
ui/.next/images-manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":1,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[16,32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":60,"formats":["image/webp"],"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"inline","remotePatterns":[],"unoptimized":false,"sizes":[640,750,828,1080,1200,1920,2048,3840,16,32,48,64,96,128,256,384]}}
|
ui/.next/next-minimal-server.js.nft.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":1,"files":["../node_modules/styled-jsx/index.js","../node_modules/styled-jsx/package.json","../node_modules/styled-jsx/dist/index/index.js","../node_modules/react/package.json","../node_modules/react/index.js","../node_modules/client-only/package.json","../node_modules/react/cjs/react.production.min.js","../node_modules/client-only/index.js","../node_modules/styled-jsx/style.js","../node_modules/next/dist/compiled/next-server/server.runtime.prod.js","../node_modules/next/package.json","../node_modules/next/dist/server/body-streams.js","../node_modules/next/dist/lib/picocolors.js","../node_modules/next/dist/shared/lib/constants.js","../node_modules/next/dist/server/web/utils.js","../node_modules/next/dist/client/components/app-router-headers.js","../node_modules/next/dist/server/lib/trace/constants.js","../node_modules/next/dist/server/lib/trace/tracer.js","../node_modules/next/dist/client/components/static-generation-async-storage.external.js","../node_modules/next/dist/shared/lib/error-source.js","../node_modules/next/dist/shared/lib/modern-browserslist-target.js","../node_modules/next/dist/compiled/debug/package.json","../node_modules/next/dist/client/components/static-generation-async-storage-instance.js","../node_modules/next/dist/shared/lib/runtime-config.external.js","../node_modules/next/dist/compiled/debug/index.js","../node_modules/next/dist/compiled/ws/package.json","../node_modules/next/dist/compiled/lru-cache/package.json","../node_modules/next/dist/compiled/node-html-parser/package.json","../node_modules/@swc/helpers/_/_interop_require_default/package.json","../node_modules/next/dist/client/components/async-local-storage.js","../node_modules/next/dist/compiled/ws/index.js","../node_modules/next/dist/compiled/lru-cache/index.js","../node_modules/next/dist/compiled/node-html-parser/index.js","../node_modules/next/dist/compiled/@opentelemetry/api/package.json","../node_modules/@swc/helpers/package.json","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/parseStack.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.js","../node_modules/next/dist/compiled/jsonwebtoken/package.json","../node_modules/next/dist/client/components/react-dev-overlay/server/middleware.js","../node_modules/@swc/helpers/cjs/_interop_require_default.cjs","../node_modules/next/dist/compiled/@opentelemetry/api/index.js","../node_modules/next/dist/compiled/jsonwebtoken/index.js","../node_modules/next/dist/compiled/browserslist/package.json","../node_modules/next/dist/compiled/browserslist/index.js","../node_modules/next/dist/client/components/react-dev-overlay/server/shared.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/getRawSourceMap.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/launchEditor.js","../node_modules/next/dist/compiled/babel/code-frame.js","../node_modules/next/dist/compiled/json5/package.json","../node_modules/next/dist/compiled/semver/package.json","../node_modules/next/dist/compiled/babel/package.json","../node_modules/next/dist/lib/semver-noop.js","../node_modules/next/dist/compiled/json5/index.js","../node_modules/next/dist/compiled/semver/index.js","../node_modules/next/dist/compiled/stacktrace-parser/package.json","../node_modules/next/dist/compiled/source-map08/package.json","../node_modules/caniuse-lite/dist/unpacker/feature.js","../node_modules/caniuse-lite/dist/unpacker/agents.js","../node_modules/caniuse-lite/dist/unpacker/region.js","../node_modules/next/dist/compiled/babel/bundle.js","../node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/getSourceMapUrl.js","../node_modules/next/dist/compiled/stacktrace-parser/stack-trace-parser.cjs.js","../node_modules/next/dist/compiled/source-map08/source-map.js","../node_modules/caniuse-lite/package.json","../node_modules/next/dist/compiled/babel/core.js","../node_modules/caniuse-lite/data/agents.js","../node_modules/caniuse-lite/dist/lib/statuses.js","../node_modules/caniuse-lite/dist/unpacker/browserVersions.js","../node_modules/caniuse-lite/dist/lib/supported.js","../node_modules/caniuse-lite/dist/unpacker/browsers.js","../node_modules/next/dist/compiled/data-uri-to-buffer/package.json","../node_modules/next/dist/compiled/shell-quote/package.json","../node_modules/next/dist/compiled/data-uri-to-buffer/index.js","../node_modules/next/dist/compiled/shell-quote/index.js","../node_modules/caniuse-lite/data/browsers.js","../node_modules/caniuse-lite/data/browserVersions.js","../node_modules/next/dist/compiled/babel-packages/package.json","../node_modules/next/dist/compiled/babel-packages/packages-bundle.js","../node_modules/next/dist/compiled/babel/parser.js","../node_modules/next/dist/compiled/babel/traverse.js","../node_modules/next/dist/compiled/babel/types.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/app-page/module.compiled.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/pages/module.compiled.js"]}
|
ui/.next/next-server.js.nft.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ui/.next/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"type": "commonjs"}
|
ui/.next/prerender-manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":4,"routes":{"/":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/","dataRoute":"/index.rsc"}},"dynamicRoutes":{},"notFoundRoutes":[],"preview":{"previewModeId":"1a11e60e64c392bdbf42fb39492a98c1","previewModeSigningKey":"7f2ee3021adf2912599137a0cd03ead1d21d2c048ca31ddcfcd138eb16079aff","previewModeEncryptionKey":"1d3bda69e27718616b5d87df1ead9a0a2267c48f0a12e48a3ebda6baa3c6e877"}}
|
ui/.next/react-loadable-manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{}
|