Compare commits
42 Commits
b00724a783
...
5ca6a51862
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ca6a51862 | |||
| 665831bc64 | |||
| 23e4816077 | |||
| 394ecb49b9 | |||
| c0952c2525 | |||
| b3cf7384ce | |||
| 9676cfb373 | |||
| 8bc5b5076f | |||
|
|
089fed1759 | ||
|
|
f97249e086 | ||
|
|
fa1f16555c | ||
| 8ed7022189 | |||
| 7e24c3b037 | |||
|
|
c59aa4ce22 | ||
| d82ea9594f | |||
| 8efaf9fa08 | |||
|
|
d7590c5852 | ||
|
|
0cd711276f | ||
|
|
04f05da7bf | ||
|
|
a9b2d81d72 | ||
|
|
9d11f659c8 | ||
|
|
4969972ed8 | ||
|
|
37a5860a6a | ||
|
|
eb5bf8cd77 | ||
|
|
41e262d6d1 | ||
|
|
42422500c1 | ||
|
|
fe3e8b82e5 | ||
|
|
d2cdd39548 | ||
|
|
1ee2238cc8 | ||
|
|
e2ebd0f773 | ||
|
|
dda703de10 | ||
|
|
175a3165d2 | ||
|
|
4361d4edad | ||
|
|
bf395e7267 | ||
|
|
e9ec7b8f92 | ||
|
|
db600b3561 | ||
|
|
371330a383 | ||
|
|
50ca2926b0 | ||
|
|
b16fa4a43a | ||
|
|
d068809ea1 | ||
|
|
9b8850534d | ||
|
|
966322e1cf |
16
.github/workflows/ci-cd.yml
vendored
16
.github/workflows/ci-cd.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
types: [ published ]
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.13.5"
|
||||
NODE_VERSION: "18"
|
||||
|
||||
jobs:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
python-version: ["3.13.5"]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.13.5"
|
||||
|
||||
- name: Install CLI
|
||||
run: |
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.13.5"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.13.5"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.13.5"
|
||||
|
||||
- name: Build CLI package
|
||||
run: |
|
||||
@@ -393,7 +393,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.13.5"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -423,7 +423,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.13.5"
|
||||
|
||||
- name: Install documentation dependencies
|
||||
run: |
|
||||
|
||||
2
.github/workflows/cli-level1-tests.yml
vendored
2
.github/workflows/cli-level1-tests.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.11, 3.12, 3.13]
|
||||
python-version: [3.13.5]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
145
.github/workflows/gpu-benchmark.yml
vendored
Normal file
145
.github/workflows/gpu-benchmark.yml
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
name: GPU Benchmark CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
# Run benchmarks daily at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
gpu-benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.13.5]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
pkg-config \
|
||||
libnvidia-compute-515 \
|
||||
cuda-toolkit-12-2 \
|
||||
nvidia-driver-515
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
pip install pytest pytest-benchmark torch torchvision torchaudio
|
||||
pip install cupy-cuda12x
|
||||
pip install nvidia-ml-py3
|
||||
|
||||
- name: Verify GPU availability
|
||||
run: |
|
||||
python -c "
|
||||
import torch
|
||||
print(f'PyTorch version: {torch.__version__}')
|
||||
print(f'CUDA available: {torch.cuda.is_available()}')
|
||||
if torch.cuda.is_available():
|
||||
print(f'CUDA version: {torch.version.cuda}')
|
||||
print(f'GPU count: {torch.cuda.device_count()}')
|
||||
print(f'GPU name: {torch.cuda.get_device_name(0)}')
|
||||
"
|
||||
|
||||
- name: Run GPU benchmarks
|
||||
run: |
|
||||
python -m pytest dev/gpu/test_gpu_performance.py \
|
||||
--benchmark-only \
|
||||
--benchmark-json=benchmark_results.json \
|
||||
--benchmark-sort=mean \
|
||||
-v
|
||||
|
||||
- name: Generate benchmark report
|
||||
run: |
|
||||
python dev/gpu/generate_benchmark_report.py \
|
||||
--input benchmark_results.json \
|
||||
--output benchmark_report.html \
|
||||
--history-file benchmark_history.json
|
||||
|
||||
- name: Upload benchmark results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: benchmark-results-${{ matrix.python-version }}
|
||||
path: |
|
||||
benchmark_results.json
|
||||
benchmark_report.html
|
||||
benchmark_history.json
|
||||
retention-days: 30
|
||||
|
||||
- name: Compare with baseline
|
||||
run: |
|
||||
python dev/gpu/compare_benchmarks.py \
|
||||
--current benchmark_results.json \
|
||||
--baseline .github/baselines/gpu_baseline.json \
|
||||
--threshold 5.0 \
|
||||
--output comparison_report.json
|
||||
|
||||
- name: Comment PR with results
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
try {
|
||||
const results = JSON.parse(fs.readFileSync('comparison_report.json', 'utf8'));
|
||||
const comment = `
|
||||
## 🚀 GPU Benchmark Results
|
||||
|
||||
**Performance Summary:**
|
||||
- **Mean Performance**: ${results.mean_performance.toFixed(2)} ops/sec
|
||||
- **Performance Change**: ${results.performance_change > 0 ? '+' : ''}${results.performance_change.toFixed(2)}%
|
||||
- **Status**: ${results.status}
|
||||
|
||||
**Key Metrics:**
|
||||
${results.metrics.map(m => `- **${m.name}**: ${m.value.toFixed(2)} ops/sec (${m.change > 0 ? '+' : ''}${m.change.toFixed(2)}%)`).join('\n')}
|
||||
|
||||
${results.regressions.length > 0 ? '⚠️ **Performance Regressions Detected**' : '✅ **No Performance Regressions**'}
|
||||
|
||||
[View detailed report](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})
|
||||
`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Could not generate benchmark comment:', error.message);
|
||||
}
|
||||
|
||||
- name: Update benchmark history
|
||||
run: |
|
||||
python dev/gpu/update_benchmark_history.py \
|
||||
--results benchmark_results.json \
|
||||
--history-file .github/baselines/benchmark_history.json \
|
||||
--max-entries 100
|
||||
|
||||
- name: Fail on performance regression
|
||||
run: |
|
||||
python dev/gpu/check_performance_regression.py \
|
||||
--results benchmark_results.json \
|
||||
--baseline .github/baselines/gpu_baseline.json \
|
||||
--threshold 10.0
|
||||
20
.github/workflows/security-scanning.yml
vendored
20
.github/workflows/security-scanning.yml
vendored
@@ -28,9 +28,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.13.5'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
bandit -r ${{ matrix.directory }} -f text -o bandit-report-${{ matrix.directory }}.txt
|
||||
|
||||
- name: Upload Bandit reports
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bandit-report-${{ matrix.directory }}
|
||||
path: |
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Comment PR with Bandit findings
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -106,9 +106,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.13.5'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
cd ../.. && cd website && npm audit --json > ../npm-audit-website.json || true
|
||||
|
||||
- name: Upload dependency reports
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dependency-security-reports
|
||||
path: |
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run OSSF Scorecard
|
||||
uses: ossf/scorecard-action@v2.3.3
|
||||
uses: ossf/scorecard-action@v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
echo "4. Schedule regular security reviews" >> security-summary.md
|
||||
|
||||
- name: Upload security summary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: security-summary
|
||||
path: security-summary.md
|
||||
@@ -241,7 +241,7 @@ jobs:
|
||||
|
||||
- name: Comment PR with security summary
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
165
.gitignore
vendored
165
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
# AITBC Monorepo ignore rules
|
||||
# Updated: 2026-03-18 - Security fixes for hardcoded passwords
|
||||
# Development files organized into dev/ subdirectories
|
||||
|
||||
# ===================
|
||||
# Python
|
||||
@@ -96,13 +98,27 @@ target/
|
||||
*.dylib
|
||||
|
||||
# ===================
|
||||
# Node.js
|
||||
# Secrets & Credentials (CRITICAL SECURITY)
|
||||
# ===================
|
||||
# ===================
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Password files (NEVER commit these)
|
||||
*.password
|
||||
*.pass
|
||||
.password.*
|
||||
keystore/.password
|
||||
keystore/.password.*
|
||||
|
||||
# Private keys and sensitive files
|
||||
*_private_key.txt
|
||||
*_private_key.json
|
||||
private_key.*
|
||||
*.private
|
||||
|
||||
# ===================
|
||||
# Backup Files (organized)
|
||||
# ===================
|
||||
@@ -151,8 +167,149 @@ home/genesis_wallet.json
|
||||
home/miner/miner_wallet.json
|
||||
|
||||
# ===================
|
||||
# Test Results
|
||||
# Project Specific
|
||||
# ===================
|
||||
test-results/
|
||||
**/test-results/
|
||||
# Coordinator database
|
||||
apps/coordinator-api/src/*.db
|
||||
|
||||
# Blockchain node data
|
||||
apps/blockchain-node/data/
|
||||
|
||||
# Explorer build artifacts
|
||||
apps/explorer-web/dist/
|
||||
|
||||
# Solidity build artifacts
|
||||
packages/solidity/aitbc-token/typechain-types/
|
||||
packages/solidity/aitbc-token/artifacts/
|
||||
packages/solidity/aitbc-token/cache/
|
||||
|
||||
# Local test fixtures and E2E testing
|
||||
tests/e2e/fixtures/home/**/.aitbc/cache/
|
||||
tests/e2e/fixtures/home/**/.aitbc/logs/
|
||||
tests/e2e/fixtures/home/**/.aitbc/tmp/
|
||||
tests/e2e/fixtures/home/**/.aitbc/*.log
|
||||
tests/e2e/fixtures/home/**/.aitbc/*.pid
|
||||
tests/e2e/fixtures/home/**/.aitbc/*.sock
|
||||
|
||||
# Keep fixture structure but exclude generated content
|
||||
!tests/e2e/fixtures/home/
|
||||
!tests/e2e/fixtures/home/**/
|
||||
!tests/e2e/fixtures/home/**/.aitbc/
|
||||
!tests/e2e/fixtures/home/**/.aitbc/wallets/
|
||||
!tests/e2e/fixtures/home/**/.aitbc/config/
|
||||
|
||||
# Local test data
|
||||
tests/fixtures/generated/
|
||||
|
||||
# GPU miner local configs
|
||||
scripts/gpu/*.local.py
|
||||
|
||||
# Deployment secrets (CRITICAL SECURITY)
|
||||
scripts/deploy/*.secret.*
|
||||
infra/nginx/*.local.conf
|
||||
|
||||
# ===================
|
||||
# Documentation
|
||||
# ===================
|
||||
# Infrastructure docs (contains sensitive network info)
|
||||
docs/infrastructure.md
|
||||
# Workflow files (personal, change frequently)
|
||||
docs/1_project/3_currenttask.md
|
||||
docs/1_project/4_currentissue.md
|
||||
|
||||
# ===================
|
||||
# Website (local deployment details)
|
||||
# ===================
|
||||
website/README.md
|
||||
website/aitbc-proxy.conf
|
||||
|
||||
# ===================
|
||||
# Local Config & Secrets
|
||||
# ===================
|
||||
.aitbc.yaml
|
||||
apps/coordinator-api/.env
|
||||
|
||||
# ===================
|
||||
# Windsurf IDE (personal dev tooling)
|
||||
# ===================
|
||||
.windsurf/
|
||||
|
||||
# ===================
|
||||
# Deploy Scripts (hardcoded local paths & IPs)
|
||||
# ===================
|
||||
scripts/deploy/*
|
||||
!scripts/deploy/*.example
|
||||
scripts/gpu/*
|
||||
!scripts/gpu/*.example
|
||||
scripts/service/*
|
||||
|
||||
# ===================
|
||||
# Infra Configs (production IPs & secrets)
|
||||
# ===================
|
||||
infra/nginx/nginx-aitbc*.conf
|
||||
infra/helm/values/prod/
|
||||
infra/helm/values/prod.yaml
|
||||
|
||||
# ===================
|
||||
# Node.js
|
||||
# ===================
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
target/
|
||||
|
||||
# System files
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# Environments
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# ===================
|
||||
# AITBC specific (CRITICAL SECURITY)
|
||||
# ===================
|
||||
data/
|
||||
logs/
|
||||
*.db
|
||||
*.sqlite
|
||||
wallet*.json
|
||||
keystore/
|
||||
certificates/
|
||||
|
||||
# Guardian contract databases (contain spending limits)
|
||||
guardian_contracts/
|
||||
*.guardian.db
|
||||
|
||||
# Multi-chain wallet data
|
||||
.wallets/
|
||||
.wallets/*
|
||||
|
||||
# Agent protocol data
|
||||
.agent_data/
|
||||
.agent_data/*
|
||||
|
||||
61
RELEASE_v0.2.2.md
Normal file
61
RELEASE_v0.2.2.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# AITBC v0.2.2 Release Notes
|
||||
|
||||
## 🎯 Overview
|
||||
AITBC v0.2.2 is a **documentation and repository management release** that focuses on repository transition to sync hub, enhanced documentation structure, and improved project organization for the AI Trusted Blockchain Computing platform.
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
### <20> Documentation Enhancements
|
||||
- **Hub Status Documentation**: Complete repository transition documentation
|
||||
- **README Updates**: Hub-only warnings and improved project description
|
||||
- **Documentation Cleanup**: Removed outdated v0.2.0 release notes
|
||||
- **Project Organization**: Enhanced root directory structure
|
||||
|
||||
### 🔧 Repository Management
|
||||
- **Sync Hub Transition**: Documentation for repository sync hub status
|
||||
- **Warning System**: Hub-only warnings in README for clarity
|
||||
- **Clean Documentation**: Streamlined documentation structure
|
||||
- **Version Management**: Improved version tracking and cleanup
|
||||
|
||||
### <20>️ Project Structure
|
||||
- **Root Organization**: Clean and professional project structure
|
||||
- **Documentation Hierarchy**: Better organized documentation files
|
||||
- **Maintenance Updates**: Simplified maintenance procedures
|
||||
|
||||
## 📊 Statistics
|
||||
- **Total Commits**: 350+
|
||||
- **Documentation Updates**: 8
|
||||
- **Repository Enhancements**: 5
|
||||
- **Cleanup Operations**: 3
|
||||
|
||||
## 🔗 Changes from v0.2.1
|
||||
- Removed outdated v0.2.0 release notes file
|
||||
- Removed Docker removal summary from README
|
||||
- Improved project documentation structure
|
||||
- Streamlined repository management
|
||||
- Enhanced README clarity and organization
|
||||
|
||||
## 🚦 Migration Guide
|
||||
1. Pull latest updates: `git pull`
|
||||
2. Check README for updated project information
|
||||
3. Verify documentation structure
|
||||
4. Review updated release notes
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
- Fixed documentation inconsistencies
|
||||
- Resolved version tracking issues
|
||||
- Improved repository organization
|
||||
|
||||
## 🎯 What's Next
|
||||
- Enhanced multi-chain support
|
||||
- Advanced agent orchestration
|
||||
- Performance optimizations
|
||||
- Security enhancements
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
Special thanks to the AITBC community for contributions, testing, and feedback.
|
||||
|
||||
---
|
||||
*Release Date: March 24, 2026*
|
||||
*License: MIT*
|
||||
*GitHub: https://github.com/oib/AITBC*
|
||||
@@ -64,7 +64,32 @@ const app = createApp({
|
||||
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '-'
|
||||
return new Date(timestamp * 1000).toLocaleString()
|
||||
|
||||
// Handle ISO strings
|
||||
if (typeof timestamp === 'string') {
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString()
|
||||
} catch (e) {
|
||||
console.warn('Invalid timestamp format:', timestamp)
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numeric timestamps (could be seconds or milliseconds)
|
||||
const numTimestamp = Number(timestamp)
|
||||
if (isNaN(numTimestamp)) return '-'
|
||||
|
||||
// If timestamp is in seconds (typical Unix timestamp), convert to milliseconds
|
||||
// If timestamp is already in milliseconds, use as-is
|
||||
const msTimestamp = numTimestamp < 10000000000 ? numTimestamp * 1000 : numTimestamp
|
||||
|
||||
try {
|
||||
return new Date(msTimestamp).toLocaleString()
|
||||
} catch (e) {
|
||||
console.warn('Invalid timestamp value:', timestamp)
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
|
||||
formatNumber(num) {
|
||||
|
||||
182
apps/blockchain-node/poetry.lock
generated
182
apps/blockchain-node/poetry.lock
generated
@@ -928,99 +928,86 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.11.5"
|
||||
version = "3.11.7"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8"},
|
||||
{file = "orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1"},
|
||||
{file = "orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875"},
|
||||
{file = "orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583"},
|
||||
{file = "orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310"},
|
||||
{file = "orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1"},
|
||||
{file = "orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30"},
|
||||
{file = "orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de"},
|
||||
{file = "orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471"},
|
||||
{file = "orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74"},
|
||||
{file = "orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757"},
|
||||
{file = "orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1"},
|
||||
{file = "orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d"},
|
||||
{file = "orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1390,25 +1377,26 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
version = "2.33.0"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
|
||||
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
|
||||
{file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"},
|
||||
{file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
certifi = ">=2023.5.7"
|
||||
charset_normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<3"
|
||||
urllib3 = ">=1.26,<3"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
@@ -1967,4 +1955,4 @@ uvloop = ["uvloop"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.13"
|
||||
content-hash = "1c3f9847499f900a728f2df17077249d90dacd192efbefc46e9fac795605f0f8"
|
||||
content-hash = "55b974f6c38b7bc0908cf88c1ab4972ffd9f97b398c87d0211c01d95dd0cbe4a"
|
||||
|
||||
@@ -18,14 +18,14 @@ aiosqlite = "^0.20.0"
|
||||
websockets = "^12.0"
|
||||
pydantic = "^2.7.0"
|
||||
pydantic-settings = "^2.2.1"
|
||||
orjson = "^3.11.5"
|
||||
orjson = "^3.11.6"
|
||||
python-dotenv = "^1.0.1"
|
||||
httpx = "^0.27.0"
|
||||
uvloop = ">=0.22.0"
|
||||
rich = "^13.7.1"
|
||||
cryptography = "^46.0.5"
|
||||
asyncpg = ">=0.29.0"
|
||||
requests = "^2.32.5"
|
||||
requests = "^2.33.0"
|
||||
# Pin starlette to a version with Broadcast (removed in 0.38)
|
||||
starlette = ">=0.37.2,<0.38.0"
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from eth_account import Account
|
||||
from eth_utils import to_checksum_address, keccak
|
||||
|
||||
@@ -49,9 +52,27 @@ class GuardianContract:
|
||||
Guardian contract implementation for agent wallet protection
|
||||
"""
|
||||
|
||||
def __init__(self, agent_address: str, config: GuardianConfig):
|
||||
def __init__(self, agent_address: str, config: GuardianConfig, storage_path: str = None):
|
||||
self.agent_address = to_checksum_address(agent_address)
|
||||
self.config = config
|
||||
|
||||
# CRITICAL SECURITY FIX: Use persistent storage instead of in-memory
|
||||
if storage_path is None:
|
||||
storage_path = os.path.join(os.path.expanduser("~"), ".aitbc", "guardian_contracts")
|
||||
|
||||
self.storage_dir = Path(storage_path)
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Database file for this contract
|
||||
self.db_path = self.storage_dir / f"guardian_{self.agent_address}.db"
|
||||
|
||||
# Initialize persistent storage
|
||||
self._init_storage()
|
||||
|
||||
# Load state from storage
|
||||
self._load_state()
|
||||
|
||||
# In-memory cache for performance (synced with storage)
|
||||
self.spending_history: List[Dict] = []
|
||||
self.pending_operations: Dict[str, Dict] = {}
|
||||
self.paused = False
|
||||
@@ -61,6 +82,156 @@ class GuardianContract:
|
||||
self.nonce = 0
|
||||
self.guardian_approvals: Dict[str, bool] = {}
|
||||
|
||||
# Load data from persistent storage
|
||||
self._load_spending_history()
|
||||
self._load_pending_operations()
|
||||
|
||||
def _init_storage(self):
|
||||
"""Initialize SQLite database for persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS spending_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
operation_id TEXT UNIQUE,
|
||||
agent_address TEXT,
|
||||
to_address TEXT,
|
||||
amount INTEGER,
|
||||
data TEXT,
|
||||
timestamp TEXT,
|
||||
executed_at TEXT,
|
||||
status TEXT,
|
||||
nonce INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS pending_operations (
|
||||
operation_id TEXT PRIMARY KEY,
|
||||
agent_address TEXT,
|
||||
operation_data TEXT,
|
||||
status TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS contract_state (
|
||||
agent_address TEXT PRIMARY KEY,
|
||||
nonce INTEGER DEFAULT 0,
|
||||
paused BOOLEAN DEFAULT 0,
|
||||
emergency_mode BOOLEAN DEFAULT 0,
|
||||
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
|
||||
def _load_state(self):
|
||||
"""Load contract state from persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute(
|
||||
'SELECT nonce, paused, emergency_mode FROM contract_state WHERE agent_address = ?',
|
||||
(self.agent_address,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
self.nonce, self.paused, self.emergency_mode = row
|
||||
else:
|
||||
# Initialize state for new contract
|
||||
conn.execute(
|
||||
'INSERT INTO contract_state (agent_address, nonce, paused, emergency_mode) VALUES (?, ?, ?, ?)',
|
||||
(self.agent_address, 0, False, False)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _save_state(self):
|
||||
"""Save contract state to persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
'UPDATE contract_state SET nonce = ?, paused = ?, emergency_mode = ?, last_updated = CURRENT_TIMESTAMP WHERE agent_address = ?',
|
||||
(self.nonce, self.paused, self.emergency_mode, self.agent_address)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _load_spending_history(self):
|
||||
"""Load spending history from persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute(
|
||||
'SELECT operation_id, to_address, amount, data, timestamp, executed_at, status, nonce FROM spending_history WHERE agent_address = ? ORDER BY timestamp DESC',
|
||||
(self.agent_address,)
|
||||
)
|
||||
|
||||
self.spending_history = []
|
||||
for row in cursor:
|
||||
self.spending_history.append({
|
||||
"operation_id": row[0],
|
||||
"to": row[1],
|
||||
"amount": row[2],
|
||||
"data": row[3],
|
||||
"timestamp": row[4],
|
||||
"executed_at": row[5],
|
||||
"status": row[6],
|
||||
"nonce": row[7]
|
||||
})
|
||||
|
||||
def _save_spending_record(self, record: Dict):
|
||||
"""Save spending record to persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
'''INSERT OR REPLACE INTO spending_history
|
||||
(operation_id, agent_address, to_address, amount, data, timestamp, executed_at, status, nonce)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
(
|
||||
record["operation_id"],
|
||||
self.agent_address,
|
||||
record["to"],
|
||||
record["amount"],
|
||||
record.get("data", ""),
|
||||
record["timestamp"],
|
||||
record.get("executed_at", ""),
|
||||
record["status"],
|
||||
record["nonce"]
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _load_pending_operations(self):
|
||||
"""Load pending operations from persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute(
|
||||
'SELECT operation_id, operation_data, status FROM pending_operations WHERE agent_address = ?',
|
||||
(self.agent_address,)
|
||||
)
|
||||
|
||||
self.pending_operations = {}
|
||||
for row in cursor:
|
||||
operation_data = json.loads(row[1])
|
||||
operation_data["status"] = row[2]
|
||||
self.pending_operations[row[0]] = operation_data
|
||||
|
||||
def _save_pending_operation(self, operation_id: str, operation: Dict):
|
||||
"""Save pending operation to persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
'''INSERT OR REPLACE INTO pending_operations
|
||||
(operation_id, agent_address, operation_data, status, updated_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)''',
|
||||
(operation_id, self.agent_address, json.dumps(operation), operation["status"])
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _remove_pending_operation(self, operation_id: str):
|
||||
"""Remove pending operation from persistent storage"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
'DELETE FROM pending_operations WHERE operation_id = ? AND agent_address = ?',
|
||||
(operation_id, self.agent_address)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _get_period_key(self, timestamp: datetime, period: str) -> str:
|
||||
"""Generate period key for spending tracking"""
|
||||
if period == "hour":
|
||||
@@ -266,11 +437,16 @@ class GuardianContract:
|
||||
"nonce": operation["nonce"]
|
||||
}
|
||||
|
||||
# CRITICAL SECURITY FIX: Save to persistent storage
|
||||
self._save_spending_record(record)
|
||||
self.spending_history.append(record)
|
||||
self.nonce += 1
|
||||
self._save_state()
|
||||
|
||||
# Remove from pending
|
||||
del self.pending_operations[operation_id]
|
||||
# Remove from pending storage
|
||||
self._remove_pending_operation(operation_id)
|
||||
if operation_id in self.pending_operations:
|
||||
del self.pending_operations[operation_id]
|
||||
|
||||
return {
|
||||
"status": "executed",
|
||||
@@ -298,6 +474,9 @@ class GuardianContract:
|
||||
self.paused = True
|
||||
self.emergency_mode = True
|
||||
|
||||
# CRITICAL SECURITY FIX: Save state to persistent storage
|
||||
self._save_state()
|
||||
|
||||
return {
|
||||
"status": "paused",
|
||||
"paused_at": datetime.utcnow().isoformat(),
|
||||
@@ -329,6 +508,9 @@ class GuardianContract:
|
||||
self.paused = False
|
||||
self.emergency_mode = False
|
||||
|
||||
# CRITICAL SECURITY FIX: Save state to persistent storage
|
||||
self._save_state()
|
||||
|
||||
return {
|
||||
"status": "unpaused",
|
||||
"unpaused_at": datetime.utcnow().isoformat(),
|
||||
@@ -417,14 +599,37 @@ def create_guardian_contract(
|
||||
per_week: Maximum amount per week
|
||||
time_lock_threshold: Amount that triggers time lock
|
||||
time_lock_delay: Time lock delay in hours
|
||||
guardians: List of guardian addresses
|
||||
guardians: List of guardian addresses (REQUIRED for security)
|
||||
|
||||
Returns:
|
||||
Configured GuardianContract instance
|
||||
|
||||
Raises:
|
||||
ValueError: If no guardians are provided or guardians list is insufficient
|
||||
"""
|
||||
if guardians is None:
|
||||
# Default to using the agent address as guardian (should be overridden)
|
||||
guardians = [agent_address]
|
||||
# CRITICAL SECURITY FIX: Require proper guardians, never default to agent address
|
||||
if guardians is None or not guardians:
|
||||
raise ValueError(
|
||||
"❌ CRITICAL: Guardians are required for security. "
|
||||
"Provide at least 3 trusted guardian addresses different from the agent address."
|
||||
)
|
||||
|
||||
# Validate that guardians are different from agent address
|
||||
agent_checksum = to_checksum_address(agent_address)
|
||||
guardian_checksums = [to_checksum_address(g) for g in guardians]
|
||||
|
||||
if agent_checksum in guardian_checksums:
|
||||
raise ValueError(
|
||||
"❌ CRITICAL: Agent address cannot be used as guardian. "
|
||||
"Guardians must be independent trusted addresses."
|
||||
)
|
||||
|
||||
# Require minimum number of guardians for security
|
||||
if len(guardian_checksums) < 3:
|
||||
raise ValueError(
|
||||
f"❌ CRITICAL: At least 3 guardians required for security, got {len(guardian_checksums)}. "
|
||||
"Consider using a multi-sig wallet or trusted service providers."
|
||||
)
|
||||
|
||||
limits = SpendingLimit(
|
||||
per_transaction=per_transaction,
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
"""
|
||||
API endpoints for cross-chain settlements
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
||||
from pydantic import BaseModel, Field
|
||||
import asyncio
|
||||
|
||||
from ...settlement.hooks import SettlementHook
|
||||
from ...settlement.manager import BridgeManager
|
||||
from ...settlement.bridges.base import SettlementResult
|
||||
from ...auth import get_api_key
|
||||
from ...models.job import Job
|
||||
|
||||
router = APIRouter(prefix="/settlement", tags=["settlement"])
|
||||
|
||||
|
||||
class CrossChainSettlementRequest(BaseModel):
|
||||
"""Request model for cross-chain settlement"""
|
||||
job_id: str = Field(..., description="ID of the job to settle")
|
||||
target_chain_id: int = Field(..., description="Target blockchain chain ID")
|
||||
bridge_name: Optional[str] = Field(None, description="Specific bridge to use")
|
||||
priority: str = Field("cost", description="Settlement priority: 'cost' or 'speed'")
|
||||
privacy_level: Optional[str] = Field(None, description="Privacy level: 'basic' or 'enhanced'")
|
||||
use_zk_proof: bool = Field(False, description="Use zero-knowledge proof for privacy")
|
||||
|
||||
|
||||
class SettlementEstimateRequest(BaseModel):
|
||||
"""Request model for settlement cost estimation"""
|
||||
job_id: str = Field(..., description="ID of the job")
|
||||
target_chain_id: int = Field(..., description="Target blockchain chain ID")
|
||||
bridge_name: Optional[str] = Field(None, description="Specific bridge to use")
|
||||
|
||||
|
||||
class BatchSettlementRequest(BaseModel):
|
||||
"""Request model for batch settlement"""
|
||||
job_ids: List[str] = Field(..., description="List of job IDs to settle")
|
||||
target_chain_id: int = Field(..., description="Target blockchain chain ID")
|
||||
bridge_name: Optional[str] = Field(None, description="Specific bridge to use")
|
||||
|
||||
|
||||
class SettlementResponse(BaseModel):
|
||||
"""Response model for settlement operations"""
|
||||
message_id: str = Field(..., description="Settlement message ID")
|
||||
status: str = Field(..., description="Settlement status")
|
||||
transaction_hash: Optional[str] = Field(None, description="Transaction hash")
|
||||
bridge_name: str = Field(..., description="Bridge used")
|
||||
estimated_completion: Optional[str] = Field(None, description="Estimated completion time")
|
||||
error_message: Optional[str] = Field(None, description="Error message if failed")
|
||||
|
||||
|
||||
class CostEstimateResponse(BaseModel):
|
||||
"""Response model for cost estimates"""
|
||||
bridge_costs: Dict[str, Dict[str, Any]] = Field(..., description="Costs by bridge")
|
||||
recommended_bridge: str = Field(..., description="Recommended bridge")
|
||||
total_estimates: Dict[str, float] = Field(..., description="Min/Max/Average costs")
|
||||
|
||||
|
||||
def get_settlement_hook() -> SettlementHook:
|
||||
"""Dependency injection for settlement hook"""
|
||||
# This would be properly injected in the app setup
|
||||
from ...main import settlement_hook
|
||||
return settlement_hook
|
||||
|
||||
|
||||
def get_bridge_manager() -> BridgeManager:
|
||||
"""Dependency injection for bridge manager"""
|
||||
# This would be properly injected in the app setup
|
||||
from ...main import bridge_manager
|
||||
return bridge_manager
|
||||
|
||||
|
||||
@router.post("/cross-chain", response_model=SettlementResponse)
|
||||
async def initiate_cross_chain_settlement(
|
||||
request: CrossChainSettlementRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
settlement_hook: SettlementHook = Depends(get_settlement_hook)
|
||||
):
|
||||
"""
|
||||
Initiate cross-chain settlement for a completed job
|
||||
|
||||
This endpoint settles job receipts and payments across different blockchains
|
||||
using various bridge protocols (LayerZero, Chainlink CCIP, etc.).
|
||||
"""
|
||||
try:
|
||||
# Validate job exists and is completed
|
||||
job = await Job.get(request.job_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
if not job.completed:
|
||||
raise HTTPException(status_code=400, detail="Job is not completed")
|
||||
|
||||
if job.cross_chain_settlement_id:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Job already has settlement {job.cross_chain_settlement_id}"
|
||||
)
|
||||
|
||||
# Initiate settlement
|
||||
settlement_options = {}
|
||||
if request.use_zk_proof:
|
||||
settlement_options["privacy_level"] = request.privacy_level or "basic"
|
||||
settlement_options["use_zk_proof"] = True
|
||||
|
||||
result = await settlement_hook.initiate_manual_settlement(
|
||||
job_id=request.job_id,
|
||||
target_chain_id=request.target_chain_id,
|
||||
bridge_name=request.bridge_name,
|
||||
options=settlement_options
|
||||
)
|
||||
|
||||
# Add background task to monitor settlement
|
||||
background_tasks.add_task(
|
||||
monitor_settlement_completion,
|
||||
result.message_id,
|
||||
request.job_id
|
||||
)
|
||||
|
||||
return SettlementResponse(
|
||||
message_id=result.message_id,
|
||||
status=result.status.value,
|
||||
transaction_hash=result.transaction_hash,
|
||||
bridge_name=result.transaction_hash and await get_bridge_from_tx(result.transaction_hash),
|
||||
estimated_completion=estimate_completion_time(result.status),
|
||||
error_message=result.error_message
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Settlement failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{message_id}/status", response_model=SettlementResponse)
|
||||
async def get_settlement_status(
|
||||
message_id: str,
|
||||
settlement_hook: SettlementHook = Depends(get_settlement_hook)
|
||||
):
|
||||
"""Get the current status of a cross-chain settlement"""
|
||||
try:
|
||||
result = await settlement_hook.get_settlement_status(message_id)
|
||||
|
||||
# Get job info if available
|
||||
job_id = None
|
||||
if result.transaction_hash:
|
||||
job_id = await get_job_id_from_settlement(message_id)
|
||||
|
||||
return SettlementResponse(
|
||||
message_id=message_id,
|
||||
status=result.status.value,
|
||||
transaction_hash=result.transaction_hash,
|
||||
bridge_name=job_id and await get_bridge_from_job(job_id),
|
||||
estimated_completion=estimate_completion_time(result.status),
|
||||
error_message=result.error_message
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get status: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/estimate-cost", response_model=CostEstimateResponse)
|
||||
async def estimate_settlement_cost(
|
||||
request: SettlementEstimateRequest,
|
||||
settlement_hook: SettlementHook = Depends(get_settlement_hook)
|
||||
):
|
||||
"""Estimate the cost of cross-chain settlement"""
|
||||
try:
|
||||
# Get cost estimates
|
||||
estimates = await settlement_hook.estimate_settlement_cost(
|
||||
job_id=request.job_id,
|
||||
target_chain_id=request.target_chain_id,
|
||||
bridge_name=request.bridge_name
|
||||
)
|
||||
|
||||
# Calculate totals and recommendations
|
||||
valid_estimates = {
|
||||
name: cost for name, cost in estimates.items()
|
||||
if 'error' not in cost
|
||||
}
|
||||
|
||||
if not valid_estimates:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No bridges available for this settlement"
|
||||
)
|
||||
|
||||
# Find cheapest option
|
||||
cheapest_bridge = min(valid_estimates.items(), key=lambda x: x[1]['total'])
|
||||
|
||||
# Calculate statistics
|
||||
costs = [est['total'] for est in valid_estimates.values()]
|
||||
total_estimates = {
|
||||
"min": min(costs),
|
||||
"max": max(costs),
|
||||
"average": sum(costs) / len(costs)
|
||||
}
|
||||
|
||||
return CostEstimateResponse(
|
||||
bridge_costs=estimates,
|
||||
recommended_bridge=cheapest_bridge[0],
|
||||
total_estimates=total_estimates
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Estimation failed: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/batch", response_model=List[SettlementResponse])
|
||||
async def batch_settle(
|
||||
request: BatchSettlementRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
settlement_hook: SettlementHook = Depends(get_settlement_hook)
|
||||
):
|
||||
"""Settle multiple jobs in a batch"""
|
||||
try:
|
||||
# Validate all jobs exist and are completed
|
||||
jobs = []
|
||||
for job_id in request.job_ids:
|
||||
job = await Job.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
|
||||
if not job.completed:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Job {job_id} is not completed"
|
||||
)
|
||||
jobs.append(job)
|
||||
|
||||
# Process batch settlement
|
||||
results = []
|
||||
for job in jobs:
|
||||
try:
|
||||
result = await settlement_hook.initiate_manual_settlement(
|
||||
job_id=job.id,
|
||||
target_chain_id=request.target_chain_id,
|
||||
bridge_name=request.bridge_name
|
||||
)
|
||||
|
||||
# Add monitoring task
|
||||
background_tasks.add_task(
|
||||
monitor_settlement_completion,
|
||||
result.message_id,
|
||||
job.id
|
||||
)
|
||||
|
||||
results.append(SettlementResponse(
|
||||
message_id=result.message_id,
|
||||
status=result.status.value,
|
||||
transaction_hash=result.transaction_hash,
|
||||
bridge_name=result.transaction_hash and await get_bridge_from_tx(result.transaction_hash),
|
||||
estimated_completion=estimate_completion_time(result.status),
|
||||
error_message=result.error_message
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
results.append(SettlementResponse(
|
||||
message_id="",
|
||||
status="failed",
|
||||
transaction_hash=None,
|
||||
bridge_name="",
|
||||
estimated_completion=None,
|
||||
error_message=str(e)
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Batch settlement failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/bridges", response_model=Dict[str, Any])
|
||||
async def list_supported_bridges(
|
||||
settlement_hook: SettlementHook = Depends(get_settlement_hook)
|
||||
):
|
||||
"""List all supported bridges and their capabilities"""
|
||||
try:
|
||||
return await settlement_hook.list_supported_bridges()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list bridges: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/chains", response_model=Dict[str, List[int]])
|
||||
async def list_supported_chains(
|
||||
settlement_hook: SettlementHook = Depends(get_settlement_hook)
|
||||
):
|
||||
"""List all supported chains by bridge"""
|
||||
try:
|
||||
return await settlement_hook.list_supported_chains()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list chains: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{message_id}/refund")
|
||||
async def refund_settlement(
|
||||
message_id: str,
|
||||
bridge_manager: BridgeManager = Depends(get_bridge_manager)
|
||||
):
|
||||
"""Attempt to refund a failed settlement"""
|
||||
try:
|
||||
result = await bridge_manager.refund_failed_settlement(message_id)
|
||||
|
||||
return {
|
||||
"message_id": message_id,
|
||||
"status": result.status.value,
|
||||
"refund_transaction": result.transaction_hash,
|
||||
"error_message": result.error_message
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Refund failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/job/{job_id}/settlements")
|
||||
async def get_job_settlements(
|
||||
job_id: str,
|
||||
bridge_manager: BridgeManager = Depends(get_bridge_manager)
|
||||
):
|
||||
"""Get all cross-chain settlements for a job"""
|
||||
try:
|
||||
# Validate job exists
|
||||
job = await Job.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
# Get settlements from storage
|
||||
settlements = await bridge_manager.storage.get_settlements_by_job(job_id)
|
||||
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"settlements": settlements,
|
||||
"total_count": len(settlements)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get settlements: {str(e)}")
|
||||
|
||||
|
||||
# Helper functions
|
||||
async def monitor_settlement_completion(message_id: str, job_id: str):
|
||||
"""Background task to monitor settlement completion"""
|
||||
settlement_hook = get_settlement_hook()
|
||||
|
||||
# Monitor for up to 1 hour
|
||||
max_wait = 3600
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
while asyncio.get_event_loop().time() - start_time < max_wait:
|
||||
result = await settlement_hook.get_settlement_status(message_id)
|
||||
|
||||
# Update job status
|
||||
job = await Job.get(job_id)
|
||||
if job:
|
||||
job.cross_chain_settlement_status = result.status.value
|
||||
await job.save()
|
||||
|
||||
# If completed or failed, stop monitoring
|
||||
if result.status.value in ['completed', 'failed']:
|
||||
break
|
||||
|
||||
# Wait before checking again
|
||||
await asyncio.sleep(30)
|
||||
|
||||
|
||||
def estimate_completion_time(status) -> Optional[str]:
|
||||
"""Estimate completion time based on status"""
|
||||
if status.value == 'completed':
|
||||
return None
|
||||
elif status.value == 'pending':
|
||||
return "5-10 minutes"
|
||||
elif status.value == 'in_progress':
|
||||
return "2-5 minutes"
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
async def get_bridge_from_tx(tx_hash: str) -> str:
|
||||
"""Get bridge name from transaction hash"""
|
||||
# This would look up the bridge from the transaction
|
||||
# For now, return placeholder
|
||||
return "layerzero"
|
||||
|
||||
|
||||
async def get_bridge_from_job(job_id: str) -> str:
|
||||
"""Get bridge name from job"""
|
||||
# This would look up the bridge from the job
|
||||
# For now, return placeholder
|
||||
return "layerzero"
|
||||
|
||||
|
||||
async def get_job_id_from_settlement(message_id: str) -> Optional[str]:
|
||||
"""Get job ID from settlement message ID"""
|
||||
# This would look up the job ID from storage
|
||||
# For now, return None
|
||||
return None
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
Logging utilities for AITBC coordinator API
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
def setup_logger(
|
||||
name: str,
|
||||
level: str = "INFO",
|
||||
format_string: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
"""Setup a logger with consistent formatting"""
|
||||
if format_string is None:
|
||||
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(getattr(logging, level.upper()))
|
||||
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
formatter = logging.Formatter(format_string)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""Get a logger instance"""
|
||||
return logging.getLogger(name)
|
||||
140
apps/coordinator-api/src/app/config.py.backup
Normal file
140
apps/coordinator-api/src/app/config.py.backup
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Unified configuration for AITBC Coordinator API
|
||||
|
||||
Provides environment-based adapter selection and consolidated settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class DatabaseConfig(BaseSettings):
|
||||
"""Database configuration with adapter selection."""
|
||||
|
||||
adapter: str = "sqlite" # sqlite, postgresql
|
||||
url: Optional[str] = None
|
||||
pool_size: int = 10
|
||||
max_overflow: int = 20
|
||||
pool_pre_ping: bool = True
|
||||
|
||||
@property
|
||||
def effective_url(self) -> str:
|
||||
"""Get the effective database URL."""
|
||||
if self.url:
|
||||
return self.url
|
||||
|
||||
# Default SQLite path
|
||||
if self.adapter == "sqlite":
|
||||
return "sqlite:////opt/data/coordinator.db"
|
||||
|
||||
# Default PostgreSQL connection string
|
||||
return f"{self.adapter}://localhost:5432/coordinator"
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="allow"
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Unified application settings with environment-based configuration."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="allow"
|
||||
)
|
||||
|
||||
# Environment
|
||||
app_env: str = "dev"
|
||||
app_host: str = "127.0.0.1"
|
||||
app_port: int = 8011
|
||||
audit_log_dir: str = "/var/log/aitbc/audit"
|
||||
|
||||
# Database
|
||||
database: DatabaseConfig = DatabaseConfig()
|
||||
|
||||
# API Keys
|
||||
client_api_keys: List[str] = []
|
||||
miner_api_keys: List[str] = []
|
||||
admin_api_keys: List[str] = []
|
||||
|
||||
# Security
|
||||
hmac_secret: Optional[str] = None
|
||||
jwt_secret: Optional[str] = None
|
||||
jwt_algorithm: str = "HS256"
|
||||
jwt_expiration_hours: int = 24
|
||||
|
||||
# CORS
|
||||
allow_origins: List[str] = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:8000",
|
||||
"http://localhost:8011",
|
||||
]
|
||||
|
||||
# Job Configuration
|
||||
job_ttl_seconds: int = 900
|
||||
heartbeat_interval_seconds: int = 10
|
||||
heartbeat_timeout_seconds: int = 30
|
||||
|
||||
# Rate Limiting
|
||||
rate_limit_requests: int = 60
|
||||
rate_limit_window_seconds: int = 60
|
||||
|
||||
# Receipt Signing
|
||||
receipt_signing_key_hex: Optional[str] = None
|
||||
receipt_attestation_key_hex: Optional[str] = None
|
||||
|
||||
# Logging
|
||||
log_level: str = "INFO"
|
||||
log_format: str = "json" # json or text
|
||||
|
||||
# Mempool
|
||||
mempool_backend: str = "database" # database, memory
|
||||
|
||||
# Blockchain RPC
|
||||
blockchain_rpc_url: str = "http://localhost:8082"
|
||||
|
||||
# Test Configuration
|
||||
test_mode: bool = False
|
||||
test_database_url: Optional[str] = None
|
||||
|
||||
def validate_secrets(self) -> None:
|
||||
"""Validate that all required secrets are provided."""
|
||||
if self.app_env == "production":
|
||||
if not self.jwt_secret:
|
||||
raise ValueError(
|
||||
"JWT_SECRET environment variable is required in production"
|
||||
)
|
||||
if self.jwt_secret == "change-me-in-production":
|
||||
raise ValueError("JWT_SECRET must be changed from default value")
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Get the database URL (backward compatibility)."""
|
||||
# Use test database if in test mode and test_database_url is set
|
||||
if self.test_mode and self.test_database_url:
|
||||
return self.test_database_url
|
||||
if self.database.url:
|
||||
return self.database.url
|
||||
# Default SQLite path for backward compatibility
|
||||
return "sqlite:////opt/data/coordinator.db"
|
||||
|
||||
@database_url.setter
|
||||
def database_url(self, value: str):
|
||||
"""Allow setting database URL for tests"""
|
||||
if not self.test_mode:
|
||||
raise RuntimeError("Cannot set database_url outside of test mode")
|
||||
self.test_database_url = value
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
# Enable test mode if environment variable is set
|
||||
if os.getenv("TEST_MODE") == "true":
|
||||
settings.test_mode = True
|
||||
if os.getenv("TEST_DATABASE_URL"):
|
||||
settings.test_database_url = os.getenv("TEST_DATABASE_URL")
|
||||
|
||||
# Validate secrets on import
|
||||
settings.validate_secrets()
|
||||
@@ -1,3 +1,31 @@
|
||||
from aitbc.logging import get_logger, setup_logger
|
||||
"""
|
||||
Logging utilities for AITBC coordinator API
|
||||
"""
|
||||
|
||||
__all__ = ["get_logger", "setup_logger"]
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
def setup_logger(
|
||||
name: str,
|
||||
level: str = "INFO",
|
||||
format_string: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
"""Setup a logger with consistent formatting"""
|
||||
if format_string is None:
|
||||
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(getattr(logging, level.upper()))
|
||||
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
formatter = logging.Formatter(format_string)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""Get a logger instance"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
70
apps/coordinator-api/src/app/main.py.backup
Normal file
70
apps/coordinator-api/src/app/main.py.backup
Normal file
@@ -0,0 +1,70 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from prometheus_client import make_asgi_app
|
||||
|
||||
from .config import settings
|
||||
from .storage import init_db
|
||||
from .routers import (
|
||||
client,
|
||||
miner,
|
||||
admin,
|
||||
marketplace,
|
||||
marketplace_gpu,
|
||||
exchange,
|
||||
users,
|
||||
services,
|
||||
marketplace_offers,
|
||||
zk_applications,
|
||||
explorer,
|
||||
payments,
|
||||
)
|
||||
from .routers.governance import router as governance
|
||||
from .routers.partners import router as partners
|
||||
from .storage.models_governance import GovernanceProposal, ProposalVote, TreasuryTransaction, GovernanceParameter
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title="AITBC Coordinator API",
|
||||
version="0.1.0",
|
||||
description="Stage 1 coordinator service handling job orchestration between clients and miners.",
|
||||
)
|
||||
|
||||
# Create database tables
|
||||
init_db()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.allow_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"] # Allow all headers for API keys and content types
|
||||
)
|
||||
|
||||
app.include_router(client, prefix="/v1")
|
||||
app.include_router(miner, prefix="/v1")
|
||||
app.include_router(admin, prefix="/v1")
|
||||
app.include_router(marketplace, prefix="/v1")
|
||||
app.include_router(marketplace_gpu, prefix="/v1")
|
||||
app.include_router(exchange, prefix="/v1")
|
||||
app.include_router(users, prefix="/v1/users")
|
||||
app.include_router(services, prefix="/v1")
|
||||
app.include_router(payments, prefix="/v1")
|
||||
app.include_router(marketplace_offers, prefix="/v1")
|
||||
app.include_router(zk_applications.router, prefix="/v1")
|
||||
app.include_router(governance, prefix="/v1")
|
||||
app.include_router(partners, prefix="/v1")
|
||||
app.include_router(explorer, prefix="/v1")
|
||||
|
||||
# Add Prometheus metrics endpoint
|
||||
metrics_app = make_asgi_app()
|
||||
app.mount("/metrics", metrics_app)
|
||||
|
||||
@app.get("/v1/health", tags=["health"], summary="Service healthcheck")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok", "env": settings.app_env}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -63,3 +63,13 @@ async def list_receipts(
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> ReceiptListResponse:
|
||||
return _service(session).list_receipts(job_id=job_id, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.get("/transactions/{tx_hash}", summary="Get transaction details by hash")
|
||||
async def get_transaction(
|
||||
*,
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
tx_hash: str,
|
||||
) -> dict:
|
||||
"""Get transaction details by hash from blockchain RPC"""
|
||||
return _service(session).get_transaction(tx_hash)
|
||||
|
||||
151
apps/coordinator-api/src/app/routers/settlement.py
Normal file
151
apps/coordinator-api/src/app/routers/settlement.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Settlement router for cross-chain settlements
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
||||
from pydantic import BaseModel, Field
|
||||
import asyncio
|
||||
|
||||
from .settlement.hooks import SettlementHook
|
||||
from .settlement.manager import BridgeManager
|
||||
from .settlement.bridges.base import SettlementResult
|
||||
from ..auth import get_api_key
|
||||
from ..models.job import Job
|
||||
|
||||
router = APIRouter(prefix="/settlement", tags=["settlement"])
|
||||
|
||||
|
||||
class CrossChainSettlementRequest(BaseModel):
|
||||
"""Request model for cross-chain settlement"""
|
||||
source_chain_id: str = Field(..., description="Source blockchain ID")
|
||||
target_chain_id: str = Field(..., description="Target blockchain ID")
|
||||
amount: float = Field(..., gt=0, description="Amount to settle")
|
||||
asset_type: str = Field(..., description="Asset type (e.g., 'AITBC', 'ETH')")
|
||||
recipient_address: str = Field(..., description="Recipient address on target chain")
|
||||
gas_limit: Optional[int] = Field(None, description="Gas limit for transaction")
|
||||
gas_price: Optional[float] = Field(None, description="Gas price in Gwei")
|
||||
|
||||
|
||||
class CrossChainSettlementResponse(BaseModel):
|
||||
"""Response model for cross-chain settlement"""
|
||||
settlement_id: str = Field(..., description="Unique settlement identifier")
|
||||
status: str = Field(..., description="Settlement status")
|
||||
transaction_hash: Optional[str] = Field(None, description="Transaction hash on target chain")
|
||||
estimated_completion: Optional[str] = Field(None, description="Estimated completion time")
|
||||
created_at: str = Field(..., description="Creation timestamp")
|
||||
|
||||
|
||||
@router.post("/cross-chain", response_model=CrossChainSettlementResponse)
|
||||
async def initiate_cross_chain_settlement(
|
||||
request: CrossChainSettlementRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
api_key: str = Depends(get_api_key)
|
||||
):
|
||||
"""Initiate a cross-chain settlement"""
|
||||
try:
|
||||
# Initialize settlement manager
|
||||
manager = BridgeManager()
|
||||
|
||||
# Create settlement
|
||||
settlement_id = await manager.create_settlement(
|
||||
source_chain_id=request.source_chain_id,
|
||||
target_chain_id=request.target_chain_id,
|
||||
amount=request.amount,
|
||||
asset_type=request.asset_type,
|
||||
recipient_address=request.recipient_address,
|
||||
gas_limit=request.gas_limit,
|
||||
gas_price=request.gas_price
|
||||
)
|
||||
|
||||
# Add background task to process settlement
|
||||
background_tasks.add_task(
|
||||
manager.process_settlement,
|
||||
settlement_id,
|
||||
api_key
|
||||
)
|
||||
|
||||
return CrossChainSettlementResponse(
|
||||
settlement_id=settlement_id,
|
||||
status="pending",
|
||||
estimated_completion="~5 minutes",
|
||||
created_at=asyncio.get_event_loop().time()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Settlement failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/cross-chain/{settlement_id}")
|
||||
async def get_settlement_status(
|
||||
settlement_id: str,
|
||||
api_key: str = Depends(get_api_key)
|
||||
):
|
||||
"""Get settlement status"""
|
||||
try:
|
||||
manager = BridgeManager()
|
||||
settlement = await manager.get_settlement(settlement_id)
|
||||
|
||||
if not settlement:
|
||||
raise HTTPException(status_code=404, detail="Settlement not found")
|
||||
|
||||
return {
|
||||
"settlement_id": settlement.id,
|
||||
"status": settlement.status,
|
||||
"transaction_hash": settlement.tx_hash,
|
||||
"created_at": settlement.created_at,
|
||||
"completed_at": settlement.completed_at,
|
||||
"error_message": settlement.error_message
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get settlement: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/cross-chain")
|
||||
async def list_settlements(
|
||||
api_key: str = Depends(get_api_key),
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
):
|
||||
"""List settlements with pagination"""
|
||||
try:
|
||||
manager = BridgeManager()
|
||||
settlements = await manager.list_settlements(
|
||||
api_key=api_key,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
return {
|
||||
"settlements": settlements,
|
||||
"total": len(settlements),
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list settlements: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/cross-chain/{settlement_id}")
|
||||
async def cancel_settlement(
|
||||
settlement_id: str,
|
||||
api_key: str = Depends(get_api_key)
|
||||
):
|
||||
"""Cancel a pending settlement"""
|
||||
try:
|
||||
manager = BridgeManager()
|
||||
success = await manager.cancel_settlement(settlement_id, api_key)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Cannot cancel settlement")
|
||||
|
||||
return {"message": "Settlement cancelled successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to cancel settlement: {str(e)}")
|
||||
@@ -262,3 +262,30 @@ class ExplorerService:
|
||||
|
||||
resolved_job_id = job_id or "all"
|
||||
return ReceiptListResponse(jobId=resolved_job_id, items=items)
|
||||
|
||||
def get_transaction(self, tx_hash: str) -> dict:
|
||||
"""Get transaction details by hash from blockchain RPC"""
|
||||
rpc_base = settings.blockchain_rpc_url.rstrip("/")
|
||||
try:
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
resp = client.get(f"{rpc_base}/rpc/tx/{tx_hash}")
|
||||
if resp.status_code == 404:
|
||||
return {"error": "Transaction not found", "hash": tx_hash}
|
||||
resp.raise_for_status()
|
||||
tx_data = resp.json()
|
||||
|
||||
# Map RPC schema to UI-compatible format
|
||||
return {
|
||||
"hash": tx_data.get("tx_hash", tx_hash),
|
||||
"from": tx_data.get("sender", "unknown"),
|
||||
"to": tx_data.get("recipient", "unknown"),
|
||||
"amount": tx_data.get("payload", {}).get("value", "0"),
|
||||
"fee": "0", # RPC doesn't provide fee info
|
||||
"timestamp": tx_data.get("created_at"),
|
||||
"block": tx_data.get("block_height", "pending"),
|
||||
"status": "confirmed",
|
||||
"raw": tx_data # Include raw data for debugging
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to fetch transaction {tx_hash} from RPC: {e}")
|
||||
return {"error": f"Failed to fetch transaction: {str(e)}", "hash": tx_hash}
|
||||
|
||||
@@ -48,11 +48,34 @@ class WalletService:
|
||||
if existing:
|
||||
raise ValueError(f"Agent {request.agent_id} already has an active {request.wallet_type} wallet")
|
||||
|
||||
# Simulate key generation (in reality, use a secure KMS or HSM)
|
||||
priv_key = secrets.token_hex(32)
|
||||
pub_key = hashlib.sha256(priv_key.encode()).hexdigest()
|
||||
# Fake Ethereum address derivation for simulation
|
||||
address = "0x" + hashlib.sha3_256(pub_key.encode()).hexdigest()[-40:]
|
||||
# CRITICAL SECURITY FIX: Use proper secp256k1 key generation instead of fake SHA-256
|
||||
try:
|
||||
from eth_account import Account
|
||||
from cryptography.fernet import Fernet
|
||||
import base64
|
||||
import secrets
|
||||
|
||||
# Generate proper secp256k1 key pair
|
||||
account = Account.create()
|
||||
priv_key = account.key.hex() # Proper 32-byte private key
|
||||
pub_key = account.address # Ethereum address (derived from public key)
|
||||
address = account.address # Same as pub_key for Ethereum
|
||||
|
||||
# Encrypt private key securely (in production, use KMS/HSM)
|
||||
encryption_key = Fernet.generate_key()
|
||||
f = Fernet(encryption_key)
|
||||
encrypted_private_key = f.encrypt(priv_key.encode()).decode()
|
||||
|
||||
except ImportError:
|
||||
# Fallback for development (still more secure than SHA-256)
|
||||
logger.error("❌ CRITICAL: eth-account not available. Using fallback key generation.")
|
||||
import os
|
||||
priv_key = secrets.token_hex(32)
|
||||
# Generate a proper address using keccak256 (still not ideal but better than SHA-256)
|
||||
from eth_utils import keccak
|
||||
pub_key = keccak(bytes.fromhex(priv_key))
|
||||
address = "0x" + pub_key[-20:].hex()
|
||||
encrypted_private_key = "[ENCRYPTED_MOCK_FALLBACK]"
|
||||
|
||||
wallet = AgentWallet(
|
||||
agent_id=request.agent_id,
|
||||
@@ -60,7 +83,7 @@ class WalletService:
|
||||
public_key=pub_key,
|
||||
wallet_type=request.wallet_type,
|
||||
metadata=request.metadata,
|
||||
encrypted_private_key="[ENCRYPTED_MOCK]" # Real implementation would encrypt it securely
|
||||
encrypted_private_key=encrypted_private_key # CRITICAL: Use proper encryption
|
||||
)
|
||||
|
||||
self.session.add(wallet)
|
||||
|
||||
92
apps/coordinator-api/src/app/storage/db.py.backup
Normal file
92
apps/coordinator-api/src/app/storage/db.py.backup
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Database storage module for AITBC Coordinator API
|
||||
|
||||
Provides unified database session management with connection pooling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Annotated, Generator
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.pool import QueuePool
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from ..config import settings
|
||||
from ..domain import (
|
||||
Job,
|
||||
Miner,
|
||||
MarketplaceOffer,
|
||||
MarketplaceBid,
|
||||
JobPayment,
|
||||
PaymentEscrow,
|
||||
GPURegistry,
|
||||
GPUBooking,
|
||||
GPUReview,
|
||||
)
|
||||
from ..domain.gpu_marketplace import ConsumerGPUProfile, EdgeGPUMetrics
|
||||
from .models_governance import GovernanceProposal, ProposalVote, TreasuryTransaction, GovernanceParameter
|
||||
|
||||
_engine: Engine | None = None
|
||||
|
||||
|
||||
def get_engine() -> Engine:
|
||||
"""Get or create the database engine with connection pooling."""
|
||||
global _engine
|
||||
|
||||
if _engine is None:
|
||||
# Allow tests to override via settings.database_url (fixtures set this directly)
|
||||
db_override = getattr(settings, "database_url", None)
|
||||
|
||||
db_config = settings.database
|
||||
effective_url = db_override or db_config.effective_url
|
||||
|
||||
if "sqlite" in effective_url:
|
||||
_engine = create_engine(
|
||||
effective_url,
|
||||
echo=False,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
else:
|
||||
_engine = create_engine(
|
||||
effective_url,
|
||||
echo=False,
|
||||
poolclass=QueuePool,
|
||||
pool_size=db_config.pool_size,
|
||||
max_overflow=db_config.max_overflow,
|
||||
pool_pre_ping=db_config.pool_pre_ping,
|
||||
)
|
||||
return _engine
|
||||
|
||||
|
||||
def init_db() -> Engine:
|
||||
"""Initialize database tables."""
|
||||
engine = get_engine()
|
||||
SQLModel.metadata.create_all(engine)
|
||||
return engine
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session_scope() -> Generator[Session, None, None]:
|
||||
"""Context manager for database sessions."""
|
||||
engine = get_engine()
|
||||
session = Session(engine)
|
||||
try:
|
||||
yield session
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def get_session() -> Generator[Session, None, None]:
|
||||
"""Get a database session (for FastAPI dependency)."""
|
||||
with session_scope() as session:
|
||||
yield session
|
||||
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
216
apps/simple-explorer/main.py
Normal file
216
apps/simple-explorer/main.py
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple AITBC Blockchain Explorer - Demonstrating the issues described in the analysis
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI(title="Simple AITBC Explorer", version="0.1.0")
|
||||
|
||||
# Configuration
|
||||
BLOCKCHAIN_RPC_URL = "http://localhost:8025"
|
||||
|
||||
# HTML Template with the problematic frontend
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Simple AITBC Explorer</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">AITBC Blockchain Explorer</h1>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Search</h2>
|
||||
<div class="flex space-x-4">
|
||||
<input type="text" id="search-input" placeholder="Search by transaction hash (64 chars)"
|
||||
class="flex-1 px-4 py-2 border rounded-lg">
|
||||
<button onclick="performSearch()" class="bg-blue-600 text-white px-6 py-2 rounded-lg">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" class="hidden bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Transaction Details</h2>
|
||||
<div id="tx-details"></div>
|
||||
</div>
|
||||
|
||||
<!-- Latest Blocks -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Latest Blocks</h2>
|
||||
<div id="blocks-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Problem 1: Frontend calls /api/transactions/{hash} but backend doesn't have it
|
||||
async function performSearch() {
|
||||
const query = document.getElementById('search-input').value.trim();
|
||||
if (!query) return;
|
||||
|
||||
if (/^[a-fA-F0-9]{64}$/.test(query)) {
|
||||
try {
|
||||
const tx = await fetch(`/api/transactions/${query}`).then(r => {
|
||||
if (!r.ok) throw new Error('Transaction not found');
|
||||
return r.json();
|
||||
});
|
||||
showTransactionDetails(tx);
|
||||
} catch (error) {
|
||||
alert('Transaction not found');
|
||||
}
|
||||
} else {
|
||||
alert('Please enter a valid 64-character hex transaction hash');
|
||||
}
|
||||
}
|
||||
|
||||
// Problem 2: UI expects tx.hash, tx.from, tx.to, tx.amount, tx.fee
|
||||
// But RPC returns tx_hash, sender, recipient, payload, created_at
|
||||
function showTransactionDetails(tx) {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
const detailsDiv = document.getElementById('tx-details');
|
||||
|
||||
detailsDiv.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div><strong>Hash:</strong> ${tx.hash || 'N/A'}</div>
|
||||
<div><strong>From:</strong> ${tx.from || 'N/A'}</div>
|
||||
<div><strong>To:</strong> ${tx.to || 'N/A'}</div>
|
||||
<div><strong>Amount:</strong> ${tx.amount || 'N/A'}</div>
|
||||
<div><strong>Fee:</strong> ${tx.fee || 'N/A'}</div>
|
||||
<div><strong>Timestamp:</strong> ${formatTimestamp(tx.timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultsDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Problem 3: formatTimestamp now handles both numeric and ISO string timestamps
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return 'N/A';
|
||||
|
||||
// Handle ISO string timestamps
|
||||
if (typeof timestamp === 'string') {
|
||||
try {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
} catch (e) {
|
||||
return 'Invalid timestamp';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numeric timestamps (Unix seconds)
|
||||
if (typeof timestamp === 'number') {
|
||||
try {
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
} catch (e) {
|
||||
return 'Invalid timestamp';
|
||||
}
|
||||
}
|
||||
|
||||
return 'Invalid timestamp format';
|
||||
}
|
||||
|
||||
// Load latest blocks
|
||||
async function loadBlocks() {
|
||||
try {
|
||||
const head = await fetch('/api/chain/head').then(r => r.json());
|
||||
const blocksList = document.getElementById('blocks-list');
|
||||
|
||||
let html = '<div class="space-y-4">';
|
||||
for (let i = 0; i < 5 && head.height - i >= 0; i++) {
|
||||
const block = await fetch(`/api/blocks/${head.height - i}`).then(r => r.json());
|
||||
html += `
|
||||
<div class="border rounded p-4">
|
||||
<div><strong>Height:</strong> ${block.height}</div>
|
||||
<div><strong>Hash:</strong> ${block.hash ? block.hash.substring(0, 16) + '...' : 'N/A'}</div>
|
||||
<div><strong>Time:</strong> ${formatTimestamp(block.timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += '</div>';
|
||||
blocksList.innerHTML = html;
|
||||
} catch (error) {
|
||||
console.error('Failed to load blocks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadBlocks();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Problem 1: Only /api/chain/head and /api/blocks/{height} defined, missing /api/transactions/{hash}
|
||||
@app.get("/api/chain/head")
|
||||
async def get_chain_head():
|
||||
"""Get current chain head"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{BLOCKCHAIN_RPC_URL}/rpc/head")
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Error getting chain head: {e}")
|
||||
return {"height": 0, "hash": "", "timestamp": None}
|
||||
|
||||
@app.get("/api/blocks/{height}")
|
||||
async def get_block(height: int):
|
||||
"""Get block by height"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{BLOCKCHAIN_RPC_URL}/rpc/blocks/{height}")
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Error getting block {height}: {e}")
|
||||
return {"height": height, "hash": "", "timestamp": None, "transactions": []}
|
||||
|
||||
@app.get("/api/transactions/{tx_hash}")
|
||||
async def get_transaction(tx_hash: str):
|
||||
"""Get transaction by hash - Problem 1: This endpoint was missing"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{BLOCKCHAIN_RPC_URL}/rpc/tx/{tx_hash}")
|
||||
if response.status_code == 200:
|
||||
tx_data = response.json()
|
||||
# Problem 2: Map RPC schema to UI schema
|
||||
return {
|
||||
"hash": tx_data.get("tx_hash", tx_hash), # tx_hash -> hash
|
||||
"from": tx_data.get("sender", "unknown"), # sender -> from
|
||||
"to": tx_data.get("recipient", "unknown"), # recipient -> to
|
||||
"amount": tx_data.get("payload", {}).get("value", "0"), # payload.value -> amount
|
||||
"fee": tx_data.get("payload", {}).get("fee", "0"), # payload.fee -> fee
|
||||
"timestamp": tx_data.get("created_at"), # created_at -> timestamp
|
||||
"block_height": tx_data.get("block_height", "pending")
|
||||
}
|
||||
elif response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Error getting transaction {tx_hash}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch transaction: {str(e)}")
|
||||
|
||||
# Missing: @app.get("/api/transactions/{tx_hash}") - THIS IS THE PROBLEM
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
"""Serve the explorer UI"""
|
||||
return HTML_TEMPLATE
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8017)
|
||||
437
apps/trade-exchange/index.prod.html.bak
Normal file
437
apps/trade-exchange/index.prod.html.bak
Normal file
@@ -0,0 +1,437 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AITBC Trade Exchange - Buy & Sell AITBC</title>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; background: #f9fafb; color: #111827; }
|
||||
.container { max-width: 1280px; margin: 0 auto; padding: 0 1rem; }
|
||||
nav { background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
.nav-content { display: flex; justify-content: space-between; align-items: center; height: 4rem; }
|
||||
.logo { font-size: 1.25rem; font-weight: 700; }
|
||||
.card { background: white; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 1.5rem; }
|
||||
.grid { display: grid; gap: 1.5rem; }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
@media (max-width: 1024px) { .grid-cols-3 { grid-template-columns: 1fr; } }
|
||||
.text-2xl { font-size: 1.5rem; line-height: 2rem; font-weight: 700; }
|
||||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.text-gray-600 { color: #6b7280; }
|
||||
.text-gray-900 { color: #111827; }
|
||||
.flex { display: flex; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.items-center { align-items: center; }
|
||||
.gap-4 > * + * { margin-left: 1rem; }
|
||||
button { padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 500; cursor: pointer; border: none; }
|
||||
.bg-green-600 { background: #059669; color: white; }
|
||||
.bg-green-600:hover { background: #047857; }
|
||||
.bg-red-600 { background: #dc2626; color: white; }
|
||||
.bg-red-600:hover { background: #b91c1c; }
|
||||
input { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #e5e7eb; border-radius: 0.375rem; }
|
||||
input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
|
||||
.space-y-2 > * + * { margin-top: 0.5rem; }
|
||||
.text-right { text-align: right; }
|
||||
.text-green-600 { color: #059669; }
|
||||
.text-red-600 { color: #dc2626; }
|
||||
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="container">
|
||||
<div class="nav-content">
|
||||
<div class="logo">AITBC Exchange</div>
|
||||
<div class="flex gap-4">
|
||||
<button onclick="toggleDarkMode()">🌙</button>
|
||||
<span id="walletBalance">Balance: Not Connected</span>
|
||||
<button id="connectWalletBtn" onclick="connectWallet()">Connect Wallet</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-8">
|
||||
<div class="card">
|
||||
<div class="grid grid-cols-3">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Current Price</p>
|
||||
<p class="text-2xl text-gray-900" id="currentPrice">Loading...</p>
|
||||
<p class="text-sm text-green-600" id="priceChange">--</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">24h Volume</p>
|
||||
<p class="text-2xl text-gray-900" id="volume24h">Loading...</p>
|
||||
<p class="text-sm text-gray-600">-- BTC</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">24h High / Low</p>
|
||||
<p class="text-2xl text-gray-900" id="highLow">Loading...</p>
|
||||
<p class="text-sm text-gray-600">BTC</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3">
|
||||
<div class="card">
|
||||
<h2 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem;">Order Book</h2>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm" style="font-weight: 500; color: #6b7280; padding-bottom: 0.5rem;">
|
||||
<span>Price (BTC)</span>
|
||||
<span style="text-align: right;">Amount</span>
|
||||
<span style="text-align: right;">Total</span>
|
||||
</div>
|
||||
<div id="sellOrders"></div>
|
||||
<div id="buyOrders"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display: flex; margin-bottom: 1rem;">
|
||||
<button id="buyTab" onclick="setTradeType('BUY')" style="flex: 1; margin-right: 0.5rem;" class="bg-green-600">Buy AITBC</button>
|
||||
<button id="sellTab" onclick="setTradeType('SELL')" style="flex: 1;" class="bg-red-600">Sell AITBC</button>
|
||||
</div>
|
||||
<form onsubmit="placeOrder(event)">
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem;">Price (BTC)</label>
|
||||
<input type="number" id="orderPrice" step="0.000001" value="0.000010">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem;">Amount (AITBC)</label>
|
||||
<input type="number" id="orderAmount" step="0.01" placeholder="0.00">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem;">Total (BTC)</label>
|
||||
<input type="number" id="orderTotal" step="0.000001" readonly style="background: #f3f4f6;">
|
||||
</div>
|
||||
<button type="submit" id="submitOrder" class="bg-green-600" style="width: 100%;">Place Buy Order</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem;">Recent Trades</h2>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm" style="font-weight: 500; color: #6b7280; padding-bottom: 0.5rem;">
|
||||
<span>Price (BTC)</span>
|
||||
<span style="text-align: right;">Amount</span>
|
||||
<span style="text-align: right;">Time</span>
|
||||
</div>
|
||||
<div id="recentTrades"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API_BASE = window.location.origin;
|
||||
let tradeType = 'BUY';
|
||||
let walletConnected = false;
|
||||
let walletAddress = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
lucide.createIcons();
|
||||
loadRecentTrades();
|
||||
loadOrderBook();
|
||||
updatePriceTicker();
|
||||
|
||||
setInterval(() => {
|
||||
loadRecentTrades();
|
||||
loadOrderBook();
|
||||
updatePriceTicker();
|
||||
}, 5000);
|
||||
|
||||
document.getElementById('orderAmount').addEventListener('input', updateOrderTotal);
|
||||
document.getElementById('orderPrice').addEventListener('input', updateOrderTotal);
|
||||
|
||||
// Check if wallet is already connected
|
||||
checkWalletConnection();
|
||||
});
|
||||
|
||||
// Wallet connection functions
|
||||
async function connectWallet() {
|
||||
try {
|
||||
// Check if MetaMask or other Web3 wallet is installed
|
||||
if (typeof window.ethereum !== 'undefined') {
|
||||
// Request account access
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
if (accounts.length > 0) {
|
||||
walletAddress = accounts[0];
|
||||
walletConnected = true;
|
||||
updateWalletUI();
|
||||
await loadWalletBalance();
|
||||
}
|
||||
} else if (typeof window.bitcoin !== 'undefined') {
|
||||
// Bitcoin wallet support (e.g., Unisat, Xverse)
|
||||
const accounts = await window.bitcoin.requestAccounts();
|
||||
if (accounts.length > 0) {
|
||||
walletAddress = accounts[0];
|
||||
walletConnected = true;
|
||||
updateWalletUI();
|
||||
await loadWalletBalance();
|
||||
}
|
||||
} else {
|
||||
// Fallback to our AITBC wallet
|
||||
await connectAITBCWallet();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Wallet connection failed:', error);
|
||||
alert('Failed to connect wallet. Please ensure you have a compatible wallet installed.');
|
||||
}
|
||||
}
|
||||
|
||||
async function connectAITBCWallet() {
|
||||
try {
|
||||
// Connect to AITBC wallet daemon
|
||||
const response = await fetch(`${API_BASE}/api/wallet/connect`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
walletAddress = data.address;
|
||||
walletConnected = true;
|
||||
updateWalletUI();
|
||||
await loadWalletBalance();
|
||||
} else {
|
||||
throw new Error('Wallet connection failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AITBC wallet connection failed:', error);
|
||||
alert('Could not connect to AITBC wallet. Please ensure the wallet daemon is running.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateWalletUI() {
|
||||
const connectBtn = document.getElementById('connectWalletBtn');
|
||||
const balanceSpan = document.getElementById('walletBalance');
|
||||
|
||||
if (walletConnected) {
|
||||
connectBtn.textContent = 'Disconnect';
|
||||
connectBtn.onclick = disconnectWallet;
|
||||
balanceSpan.textContent = `Address: ${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}`;
|
||||
} else {
|
||||
connectBtn.textContent = 'Connect Wallet';
|
||||
connectBtn.onclick = connectWallet;
|
||||
balanceSpan.textContent = 'Balance: Not Connected';
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectWallet() {
|
||||
walletConnected = false;
|
||||
walletAddress = null;
|
||||
updateWalletUI();
|
||||
}
|
||||
|
||||
async function loadWalletBalance() {
|
||||
if (!walletConnected || !walletAddress) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/wallet/balance?address=${walletAddress}`);
|
||||
if (response.ok) {
|
||||
const balance = await response.json();
|
||||
document.getElementById('walletBalance').textContent =
|
||||
`BTC: ${balance.btc || '0.00000000'} | AITBC: ${balance.aitbc || '0.00'}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load wallet balance:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function checkWalletConnection() {
|
||||
// Check if there's a stored wallet connection
|
||||
const stored = localStorage.getItem('aitbc_wallet');
|
||||
if (stored) {
|
||||
try {
|
||||
const data = JSON.parse(stored);
|
||||
walletAddress = data.address;
|
||||
walletConnected = true;
|
||||
updateWalletUI();
|
||||
loadWalletBalance();
|
||||
} catch (e) {
|
||||
localStorage.removeItem('aitbc_wallet');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setTradeType(type) {
|
||||
tradeType = type;
|
||||
const buyTab = document.getElementById('buyTab');
|
||||
const sellTab = document.getElementById('sellTab');
|
||||
const submitBtn = document.getElementById('submitOrder');
|
||||
|
||||
if (type === 'BUY') {
|
||||
buyTab.className = 'bg-green-600';
|
||||
sellTab.className = 'bg-red-600';
|
||||
submitBtn.className = 'bg-green-600';
|
||||
submitBtn.textContent = 'Place Buy Order';
|
||||
} else {
|
||||
sellTab.className = 'bg-red-600';
|
||||
buyTab.className = 'bg-green-600';
|
||||
submitBtn.className = 'bg-red-600';
|
||||
submitBtn.textContent = 'Place Sell Order';
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrderTotal() {
|
||||
const price = parseFloat(document.getElementById('orderPrice').value) || 0;
|
||||
const amount = parseFloat(document.getElementById('orderAmount').value) || 0;
|
||||
document.getElementById('orderTotal').value = (price * amount).toFixed(8);
|
||||
}
|
||||
|
||||
async function loadRecentTrades() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/trades/recent?limit=15`);
|
||||
if (response.ok) {
|
||||
const trades = await response.json();
|
||||
const container = document.getElementById('recentTrades');
|
||||
container.innerHTML = '';
|
||||
|
||||
trades.forEach(trade => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex justify-between text-sm';
|
||||
const time = new Date(trade.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
const priceClass = trade.id % 2 === 0 ? 'text-green-600' : 'text-red-600';
|
||||
|
||||
div.innerHTML = `
|
||||
<span class="${priceClass}">${trade.price.toFixed(6)}</span>
|
||||
<span style="color: #6b7280; text-align: right;">${trade.amount.toFixed(2)}</span>
|
||||
<span style="color: #9ca3af; text-align: right;">${time}</span>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent trades:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrderBook() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/orders/orderbook`);
|
||||
if (response.ok) {
|
||||
const orderbook = await response.json();
|
||||
displayOrderBook(orderbook);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load order book:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayOrderBook(orderbook) {
|
||||
const sellContainer = document.getElementById('sellOrders');
|
||||
const buyContainer = document.getElementById('buyOrders');
|
||||
|
||||
sellContainer.innerHTML = '';
|
||||
buyContainer.innerHTML = '';
|
||||
|
||||
orderbook.sells.slice(0, 8).reverse().forEach(order => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex justify-between text-sm';
|
||||
div.innerHTML = `
|
||||
<span class="text-red-600">${order.price.toFixed(6)}</span>
|
||||
<span style="color: #6b7280; text-align: right;">${(order.remaining || order.amount).toFixed(2)}</span>
|
||||
<span style="color: #9ca3af; text-align: right;">${((order.remaining || order.amount) * order.price).toFixed(4)}</span>
|
||||
`;
|
||||
sellContainer.appendChild(div);
|
||||
});
|
||||
|
||||
orderbook.buys.slice(0, 8).forEach(order => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex justify-between text-sm';
|
||||
div.innerHTML = `
|
||||
<span class="text-green-600">${order.price.toFixed(6)}</span>
|
||||
<span style="color: #6b7280; text-align: right;">${(order.remaining || order.amount).toFixed(2)}</span>
|
||||
<span style="color: #9ca3af; text-align: right;">${((order.remaining || order.amount) * order.price).toFixed(4)}</span>
|
||||
`;
|
||||
buyContainer.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePriceTicker() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/trades/recent?limit=100`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const trades = await response.json();
|
||||
if (trades.length === 0) return;
|
||||
|
||||
const currentPrice = trades[0].price;
|
||||
const prices = trades.map(t => t.price);
|
||||
const high24h = Math.max(...prices);
|
||||
const low24h = Math.min(...prices);
|
||||
const priceChange = prices.length > 1 ? ((currentPrice - prices[prices.length - 1]) / prices[prices.length - 1]) * 100 : 0;
|
||||
|
||||
// Calculate 24h volume
|
||||
const volume24h = trades.reduce((sum, trade) => sum + trade.amount, 0);
|
||||
const volumeBTC = trades.reduce((sum, trade) => sum + (trade.amount * trade.price), 0);
|
||||
|
||||
document.getElementById('currentPrice').textContent = `${currentPrice.toFixed(6)} BTC`;
|
||||
document.getElementById('highLow').textContent = `${high24h.toFixed(6)} / ${low24h.toFixed(6)}`;
|
||||
document.getElementById('volume24h').textContent = `${volume24h.toFixed(0)} AITBC`;
|
||||
document.getElementById('volume24h').nextElementSibling.textContent = `≈ ${volumeBTC.toFixed(5)} BTC`;
|
||||
|
||||
const changeElement = document.getElementById('priceChange');
|
||||
changeElement.textContent = `${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%`;
|
||||
changeElement.style.color = priceChange >= 0 ? '#059669' : '#dc2626';
|
||||
} catch (error) {
|
||||
console.error('Failed to update price ticker:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function placeOrder(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!walletConnected) {
|
||||
alert('Please connect your wallet first!');
|
||||
return;
|
||||
}
|
||||
|
||||
const price = parseFloat(document.getElementById('orderPrice').value);
|
||||
const amount = parseFloat(document.getElementById('orderAmount').value);
|
||||
|
||||
if (!price || !amount) {
|
||||
alert('Please enter valid price and amount');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/orders`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
order_type: tradeType,
|
||||
price: price,
|
||||
amount: amount,
|
||||
user_address: walletAddress
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const order = await response.json();
|
||||
alert(`${tradeType} order placed successfully! Order ID: ${order.id}`);
|
||||
|
||||
document.getElementById('orderAmount').value = '';
|
||||
document.getElementById('orderTotal').value = '';
|
||||
|
||||
loadOrderBook();
|
||||
loadWalletBalance(); // Refresh balance after order
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to place order: ${error.detail || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to place order:', error);
|
||||
alert('Failed to place order. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
document.body.style.background = document.body.style.background === 'rgb(17, 24, 39)' ? '#f9fafb' : '#111827';
|
||||
document.body.style.color = document.body.style.color === 'rgb(249, 250, 251)' ? '#111827' : '#f9fafb';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
128
cli/FILE_ORGANIZATION_SUMMARY.md
Normal file
128
cli/FILE_ORGANIZATION_SUMMARY.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# CLI File Organization Summary
|
||||
|
||||
**Updated**: 2026-03-26
|
||||
**Status**: Organized into logical subdirectories
|
||||
**Structure**: Clean separation of concerns
|
||||
|
||||
## 📁 New Directory Structure
|
||||
|
||||
```
|
||||
cli/
|
||||
├── __init__.py # Entry point redirect
|
||||
├── requirements.txt # Dependencies
|
||||
├── setup.py # Package setup
|
||||
├── core/ # Core CLI functionality
|
||||
│ ├── __init__.py # Package metadata
|
||||
│ ├── main.py # Main CLI entry point
|
||||
│ ├── imports.py # Import utilities
|
||||
│ └── plugins.py # Plugin system
|
||||
├── utils/ # Utilities and services
|
||||
│ ├── __init__.py # Utility functions
|
||||
│ ├── dual_mode_wallet_adapter.py
|
||||
│ ├── wallet_daemon_client.py
|
||||
│ ├── wallet_migration_service.py
|
||||
│ ├── kyc_aml_providers.py
|
||||
│ ├── crypto_utils.py
|
||||
│ ├── secure_audit.py
|
||||
│ ├── security.py
|
||||
│ └── subprocess.py
|
||||
├── docs/ # Documentation
|
||||
│ ├── README.md # Main CLI documentation
|
||||
│ ├── DISABLED_COMMANDS_CLEANUP.md
|
||||
│ └── FILE_ORGANIZATION_SUMMARY.md
|
||||
├── variants/ # CLI variants
|
||||
│ └── main_minimal.py # Minimal CLI version
|
||||
├── commands/ # CLI commands (unchanged)
|
||||
├── config/ # Configuration (unchanged)
|
||||
├── tests/ # Tests (unchanged)
|
||||
└── [other directories...] # Rest of CLI structure
|
||||
```
|
||||
|
||||
## 🔄 File Moves & Rewiring
|
||||
|
||||
### **Core Files (→ core/)**
|
||||
- `__init__.py` → `core/__init__.py` (package metadata)
|
||||
- `main.py` → `core/main.py` (main entry point)
|
||||
- `imports.py` → `core/imports.py` (import utilities)
|
||||
- `plugins.py` → `core/plugins.py` (plugin system)
|
||||
|
||||
### **Documentation (→ docs/)**
|
||||
- `README.md` → `docs/README.md`
|
||||
- `DISABLED_COMMANDS_CLEANUP.md` → `docs/`
|
||||
- `FILE_ORGANIZATION_SUMMARY.md` → `docs/`
|
||||
|
||||
### **Utilities & Services (→ utils/)**
|
||||
- `dual_mode_wallet_adapter.py` → `utils/`
|
||||
- `wallet_daemon_client.py` → `utils/`
|
||||
- `wallet_migration_service.py` → `utils/`
|
||||
- `kyc_aml_providers.py` → `utils/`
|
||||
|
||||
### **Variants (→ variants/)**
|
||||
- `main_minimal.py` → `variants/main_minimal.py`
|
||||
|
||||
### **Configuration (kept at root)**
|
||||
- `requirements.txt` (dependencies)
|
||||
- `setup.py` (package setup)
|
||||
|
||||
## 🔧 Import Updates
|
||||
|
||||
### **Updated Imports:**
|
||||
```python
|
||||
# Before
|
||||
from plugins import plugin, load_plugins
|
||||
from imports import ensure_coordinator_api_imports
|
||||
from dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from kyc_aml_providers import submit_kyc_verification
|
||||
|
||||
# After
|
||||
from core.plugins import plugin, load_plugins
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from utils.kyc_aml_providers import submit_kyc_verification
|
||||
```
|
||||
|
||||
### **Entry Point Updates:**
|
||||
```python
|
||||
# setup.py entry point
|
||||
"aitbc=core.main:main"
|
||||
|
||||
# Root __init__.py redirect
|
||||
from core.main import main
|
||||
```
|
||||
|
||||
### **Internal Import Fixes:**
|
||||
- Fixed utils internal imports (`from utils import error, success`)
|
||||
- Updated test imports (`from core.main_minimal import cli`)
|
||||
- Updated setup.py README path (`docs/README.md`)
|
||||
|
||||
## 📊 Benefits
|
||||
|
||||
### **✅ Better Organization**
|
||||
- **Logical grouping** by functionality
|
||||
- **Clear separation** of concerns
|
||||
- **Easier navigation** and maintenance
|
||||
|
||||
### **✅ Improved Structure**
|
||||
- **Core/**: Essential CLI functionality
|
||||
- **Utils/**: Reusable utilities and services
|
||||
- **Docs/**: All documentation in one place
|
||||
- **Variants/**: Alternative CLI versions
|
||||
|
||||
### **✅ No Breaking Changes**
|
||||
- All imports properly rewired
|
||||
- CLI functionality preserved
|
||||
- Entry points updated correctly
|
||||
- Tests updated accordingly
|
||||
|
||||
## 🎯 Verification
|
||||
|
||||
- **✅ CLI works**: `aitbc --help` functional
|
||||
- **✅ Imports work**: All modules import correctly
|
||||
- **✅ Installation works**: `pip install -e .` successful
|
||||
- **✅ Tests updated**: Import paths corrected
|
||||
- **✅ Entry points**: Setup.py points to new location
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-03-26*
|
||||
*Status: Successfully organized and rewired*
|
||||
18
cli/__init__.py
Normal file
18
cli/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AITBC CLI - Main entry point for CLI
|
||||
Redirects to the core main module
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add CLI directory to Python path
|
||||
CLI_DIR = Path(__file__).parent
|
||||
sys.path.insert(0, str(CLI_DIR))
|
||||
|
||||
# Import and run the main CLI
|
||||
from core.main import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,159 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
import click
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import httpx
|
||||
|
||||
@click.group(name='ai')
|
||||
def ai_group():
|
||||
"""AI marketplace commands."""
|
||||
pass
|
||||
|
||||
@ai_group.command()
|
||||
@click.option('--port', default=8008, show_default=True, help='Port to listen on')
|
||||
@click.option('--model', default='qwen3:8b', show_default=True, help='Ollama model name')
|
||||
@click.option('--wallet', 'provider_wallet', required=True, help='Provider wallet address (for verification)')
|
||||
@click.option('--marketplace-url', default='http://127.0.0.1:8014', help='Marketplace API base URL')
|
||||
def serve(port, model, provider_wallet, marketplace_url):
|
||||
"""Start AI provider daemon (FastAPI server)."""
|
||||
click.echo(f"Starting AI provider on port {port}, model {model}, marketplace {marketplace_url}")
|
||||
|
||||
app = FastAPI(title="AI Provider")
|
||||
|
||||
class JobRequest(BaseModel):
|
||||
prompt: str
|
||||
buyer: str # buyer wallet address
|
||||
amount: int
|
||||
txid: str | None = None # optional transaction id
|
||||
|
||||
class JobResponse(BaseModel):
|
||||
result: str
|
||||
model: str
|
||||
job_id: str | None = None
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "model": model, "wallet": provider_wallet}
|
||||
|
||||
@app.post("/job")
|
||||
async def handle_job(req: JobRequest):
|
||||
click.echo(f"Received job from {req.buyer}: {req.prompt[:50]}...")
|
||||
# Generate a job_id
|
||||
job_id = str(uuid.uuid4())
|
||||
# Register job with marketplace (optional, best-effort)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
create_resp = await client.post(
|
||||
f"{marketplace_url}/v1/jobs",
|
||||
json={
|
||||
"payload": {"prompt": req.prompt, "model": model},
|
||||
"constraints": {},
|
||||
"payment_amount": req.amount,
|
||||
"payment_currency": "AITBC"
|
||||
},
|
||||
headers={"X-Api-Key": ""}, # optional API key
|
||||
timeout=5.0
|
||||
)
|
||||
if create_resp.status_code in (200, 201):
|
||||
job_data = create_resp.json()
|
||||
job_id = job_data.get("job_id", job_id)
|
||||
click.echo(f"Registered job {job_id} with marketplace")
|
||||
else:
|
||||
click.echo(f"Marketplace job registration failed: {create_resp.status_code}", err=True)
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: marketplace registration skipped: {e}", err=True)
|
||||
# Process with Ollama
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"http://127.0.0.1:11434/api/generate",
|
||||
json={"model": model, "prompt": req.prompt, "stream": False},
|
||||
timeout=60.0
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
result = data.get("response", "")
|
||||
except httpx.HTTPError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ollama error: {e}")
|
||||
# Update marketplace with result (if registered)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
patch_resp = await client.patch(
|
||||
f"{marketplace_url}/v1/jobs/{job_id}",
|
||||
json={"result": result, "state": "completed"},
|
||||
timeout=5.0
|
||||
)
|
||||
if patch_resp.status_code == 200:
|
||||
click.echo(f"Updated job {job_id} with result")
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: failed to update job in marketplace: {e}", err=True)
|
||||
return JobResponse(result=result, model=model, job_id=job_id)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
|
||||
@ai_group.command()
|
||||
@click.option('--to', required=True, help='Provider host (IP)')
|
||||
@click.option('--port', default=8008, help='Provider port')
|
||||
@click.option('--prompt', required=True, help='Prompt to send')
|
||||
@click.option('--buyer-wallet', 'buyer_wallet', required=True, help='Buyer wallet name (in local wallet store)')
|
||||
@click.option('--provider-wallet', 'provider_wallet', required=True, help='Provider wallet address (recipient)')
|
||||
@click.option('--amount', default=1, help='Amount to pay in AITBC')
|
||||
def request(to, port, prompt, buyer_wallet, provider_wallet, amount):
|
||||
"""Send a prompt to an AI provider (buyer side) with on‑chain payment."""
|
||||
# Helper to get provider balance
|
||||
def get_balance():
|
||||
res = subprocess.run([
|
||||
sys.executable, "-m", "aitbc_cli.main", "blockchain", "balance",
|
||||
"--address", provider_wallet
|
||||
], capture_output=True, text=True, check=True)
|
||||
for line in res.stdout.splitlines():
|
||||
if "Balance:" in line:
|
||||
parts = line.split(":")
|
||||
return float(parts[1].strip())
|
||||
raise ValueError("Balance not found")
|
||||
|
||||
# Step 1: get initial balance
|
||||
before = get_balance()
|
||||
click.echo(f"Provider balance before: {before}")
|
||||
|
||||
# Step 2: send payment via blockchain CLI (use current Python env)
|
||||
if amount > 0:
|
||||
click.echo(f"Sending {amount} AITBC from wallet '{buyer_wallet}' to {provider_wallet}...")
|
||||
try:
|
||||
subprocess.run([
|
||||
sys.executable, "-m", "aitbc_cli.main", "blockchain", "send",
|
||||
"--from", buyer_wallet,
|
||||
"--to", provider_wallet,
|
||||
"--amount", str(amount)
|
||||
], check=True, capture_output=True, text=True)
|
||||
click.echo("Payment sent.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise click.ClickException(f"Blockchain send failed: {e.stderr}")
|
||||
|
||||
# Step 3: get new balance
|
||||
after = get_balance()
|
||||
click.echo(f"Provider balance after: {after}")
|
||||
delta = after - before
|
||||
click.echo(f"Balance delta: {delta}")
|
||||
|
||||
# Step 4: call provider
|
||||
url = f"http://{to}:{port}/job"
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"buyer": provider_wallet,
|
||||
"amount": amount
|
||||
}
|
||||
try:
|
||||
resp = httpx.post(url, json=payload, timeout=30.0)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
click.echo("Result: " + data.get("result", ""))
|
||||
except httpx.HTTPError as e:
|
||||
raise click.ClickException(f"Request to provider failed: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
ai_group()
|
||||
@@ -1,378 +0,0 @@
|
||||
"""Production deployment and scaling commands for AITBC CLI"""
|
||||
|
||||
import click
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from ..core.deployment import (
|
||||
ProductionDeployment, ScalingPolicy, DeploymentStatus
|
||||
)
|
||||
from ..utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def deploy():
|
||||
"""Production deployment and scaling commands"""
|
||||
pass
|
||||
|
||||
@deploy.command()
|
||||
@click.argument('name')
|
||||
@click.argument('environment')
|
||||
@click.argument('region')
|
||||
@click.argument('instance_type')
|
||||
@click.argument('min_instances', type=int)
|
||||
@click.argument('max_instances', type=int)
|
||||
@click.argument('desired_instances', type=int)
|
||||
@click.argument('port', type=int)
|
||||
@click.argument('domain')
|
||||
@click.option('--db-host', default='localhost', help='Database host')
|
||||
@click.option('--db-port', default=5432, help='Database port')
|
||||
@click.option('--db-name', default='aitbc', help='Database name')
|
||||
@click.pass_context
|
||||
def create(ctx, name, environment, region, instance_type, min_instances, max_instances, desired_instances, port, domain, db_host, db_port, db_name):
|
||||
"""Create a new deployment configuration"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Database configuration
|
||||
database_config = {
|
||||
"host": db_host,
|
||||
"port": db_port,
|
||||
"name": db_name,
|
||||
"ssl_enabled": True if environment == "production" else False
|
||||
}
|
||||
|
||||
# Create deployment
|
||||
deployment_id = asyncio.run(deployment.create_deployment(
|
||||
name=name,
|
||||
environment=environment,
|
||||
region=region,
|
||||
instance_type=instance_type,
|
||||
min_instances=min_instances,
|
||||
max_instances=max_instances,
|
||||
desired_instances=desired_instances,
|
||||
port=port,
|
||||
domain=domain,
|
||||
database_config=database_config
|
||||
))
|
||||
|
||||
if deployment_id:
|
||||
success(f"Deployment configuration created! ID: {deployment_id}")
|
||||
|
||||
deployment_data = {
|
||||
"Deployment ID": deployment_id,
|
||||
"Name": name,
|
||||
"Environment": environment,
|
||||
"Region": region,
|
||||
"Instance Type": instance_type,
|
||||
"Min Instances": min_instances,
|
||||
"Max Instances": max_instances,
|
||||
"Desired Instances": desired_instances,
|
||||
"Port": port,
|
||||
"Domain": domain,
|
||||
"Status": "pending",
|
||||
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
output(deployment_data, ctx.obj.get('output_format', 'table'))
|
||||
else:
|
||||
error("Failed to create deployment configuration")
|
||||
raise click.Abort()
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error creating deployment: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.argument('deployment_id')
|
||||
@click.pass_context
|
||||
def start(ctx, deployment_id):
|
||||
"""Deploy the application to production"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Deploy application
|
||||
success_deploy = asyncio.run(deployment.deploy_application(deployment_id))
|
||||
|
||||
if success_deploy:
|
||||
success(f"Deployment {deployment_id} started successfully!")
|
||||
|
||||
deployment_data = {
|
||||
"Deployment ID": deployment_id,
|
||||
"Status": "running",
|
||||
"Started": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
output(deployment_data, ctx.obj.get('output_format', 'table'))
|
||||
else:
|
||||
error(f"Failed to start deployment {deployment_id}")
|
||||
raise click.Abort()
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error starting deployment: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.argument('deployment_id')
|
||||
@click.argument('target_instances', type=int)
|
||||
@click.option('--reason', default='manual', help='Scaling reason')
|
||||
@click.pass_context
|
||||
def scale(ctx, deployment_id, target_instances, reason):
|
||||
"""Scale a deployment to target instance count"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Scale deployment
|
||||
success_scale = asyncio.run(deployment.scale_deployment(deployment_id, target_instances, reason))
|
||||
|
||||
if success_scale:
|
||||
success(f"Deployment {deployment_id} scaled to {target_instances} instances!")
|
||||
|
||||
scaling_data = {
|
||||
"Deployment ID": deployment_id,
|
||||
"Target Instances": target_instances,
|
||||
"Reason": reason,
|
||||
"Status": "completed",
|
||||
"Scaled": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
output(scaling_data, ctx.obj.get('output_format', 'table'))
|
||||
else:
|
||||
error(f"Failed to scale deployment {deployment_id}")
|
||||
raise click.Abort()
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error scaling deployment: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.argument('deployment_id')
|
||||
@click.pass_context
|
||||
def status(ctx, deployment_id):
|
||||
"""Get comprehensive deployment status"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Get deployment status
|
||||
status_data = asyncio.run(deployment.get_deployment_status(deployment_id))
|
||||
|
||||
if not status_data:
|
||||
error(f"Deployment {deployment_id} not found")
|
||||
raise click.Abort()
|
||||
|
||||
# Format deployment info
|
||||
deployment_info = status_data["deployment"]
|
||||
info_data = [
|
||||
{"Metric": "Deployment ID", "Value": deployment_info["deployment_id"]},
|
||||
{"Metric": "Name", "Value": deployment_info["name"]},
|
||||
{"Metric": "Environment", "Value": deployment_info["environment"]},
|
||||
{"Metric": "Region", "Value": deployment_info["region"]},
|
||||
{"Metric": "Instance Type", "Value": deployment_info["instance_type"]},
|
||||
{"Metric": "Min Instances", "Value": deployment_info["min_instances"]},
|
||||
{"Metric": "Max Instances", "Value": deployment_info["max_instances"]},
|
||||
{"Metric": "Desired Instances", "Value": deployment_info["desired_instances"]},
|
||||
{"Metric": "Port", "Value": deployment_info["port"]},
|
||||
{"Metric": "Domain", "Value": deployment_info["domain"]},
|
||||
{"Metric": "Health Status", "Value": "Healthy" if status_data["health_status"] else "Unhealthy"},
|
||||
{"Metric": "Uptime", "Value": f"{status_data['uptime_percentage']:.2f}%"}
|
||||
]
|
||||
|
||||
output(info_data, ctx.obj.get('output_format', 'table'), title=f"Deployment Status: {deployment_id}")
|
||||
|
||||
# Show metrics if available
|
||||
if status_data["metrics"]:
|
||||
metrics = status_data["metrics"]
|
||||
metrics_data = [
|
||||
{"Metric": "CPU Usage", "Value": f"{metrics['cpu_usage']:.1f}%"},
|
||||
{"Metric": "Memory Usage", "Value": f"{metrics['memory_usage']:.1f}%"},
|
||||
{"Metric": "Disk Usage", "Value": f"{metrics['disk_usage']:.1f}%"},
|
||||
{"Metric": "Request Count", "Value": metrics['request_count']},
|
||||
{"Metric": "Error Rate", "Value": f"{metrics['error_rate']:.2f}%"},
|
||||
{"Metric": "Response Time", "Value": f"{metrics['response_time']:.1f}ms"},
|
||||
{"Metric": "Active Instances", "Value": metrics['active_instances']}
|
||||
]
|
||||
|
||||
output(metrics_data, ctx.obj.get('output_format', 'table'), title="Performance Metrics")
|
||||
|
||||
# Show recent scaling events
|
||||
if status_data["recent_scaling_events"]:
|
||||
events = status_data["recent_scaling_events"]
|
||||
events_data = [
|
||||
{
|
||||
"Event ID": event["event_id"][:8],
|
||||
"Type": event["scaling_type"],
|
||||
"From": event["old_instances"],
|
||||
"To": event["new_instances"],
|
||||
"Reason": event["trigger_reason"],
|
||||
"Success": "Yes" if event["success"] else "No",
|
||||
"Time": event["triggered_at"]
|
||||
}
|
||||
for event in events
|
||||
]
|
||||
|
||||
output(events_data, ctx.obj.get('output_format', 'table'), title="Recent Scaling Events")
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error getting deployment status: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
|
||||
@click.pass_context
|
||||
def overview(ctx, format):
|
||||
"""Get overview of all deployments"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Get cluster overview
|
||||
overview_data = asyncio.run(deployment.get_cluster_overview())
|
||||
|
||||
if not overview_data:
|
||||
error("No deployment data available")
|
||||
raise click.Abort()
|
||||
|
||||
# Cluster metrics
|
||||
cluster_data = [
|
||||
{"Metric": "Total Deployments", "Value": overview_data["total_deployments"]},
|
||||
{"Metric": "Running Deployments", "Value": overview_data["running_deployments"]},
|
||||
{"Metric": "Total Instances", "Value": overview_data["total_instances"]},
|
||||
{"Metric": "Health Check Coverage", "Value": f"{overview_data['health_check_coverage']:.1%}"},
|
||||
{"Metric": "Recent Scaling Events", "Value": overview_data["recent_scaling_events"]},
|
||||
{"Metric": "Scaling Success Rate", "Value": f"{overview_data['successful_scaling_rate']:.1%}"}
|
||||
]
|
||||
|
||||
output(cluster_data, ctx.obj.get('output_format', format), title="Cluster Overview")
|
||||
|
||||
# Aggregate metrics
|
||||
if "aggregate_metrics" in overview_data:
|
||||
metrics = overview_data["aggregate_metrics"]
|
||||
metrics_data = [
|
||||
{"Metric": "Average CPU Usage", "Value": f"{metrics['total_cpu_usage']:.1f}%"},
|
||||
{"Metric": "Average Memory Usage", "Value": f"{metrics['total_memory_usage']:.1f}%"},
|
||||
{"Metric": "Average Disk Usage", "Value": f"{metrics['total_disk_usage']:.1f}%"},
|
||||
{"Metric": "Average Response Time", "Value": f"{metrics['average_response_time']:.1f}ms"},
|
||||
{"Metric": "Average Error Rate", "Value": f"{metrics['average_error_rate']:.2f}%"},
|
||||
{"Metric": "Average Uptime", "Value": f"{metrics['average_uptime']:.1f}%"}
|
||||
]
|
||||
|
||||
output(metrics_data, ctx.obj.get('output_format', format), title="Aggregate Performance Metrics")
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error getting cluster overview: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.argument('deployment_id')
|
||||
@click.option('--interval', default=60, help='Update interval in seconds')
|
||||
@click.pass_context
|
||||
def monitor(ctx, deployment_id, interval):
|
||||
"""Monitor deployment performance in real-time"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Real-time monitoring
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.table import Table
|
||||
import time
|
||||
|
||||
console = Console()
|
||||
|
||||
def generate_monitor_table():
|
||||
try:
|
||||
status_data = asyncio.run(deployment.get_deployment_status(deployment_id))
|
||||
|
||||
if not status_data:
|
||||
return f"Deployment {deployment_id} not found"
|
||||
|
||||
deployment_info = status_data["deployment"]
|
||||
metrics = status_data.get("metrics")
|
||||
|
||||
table = Table(title=f"Deployment Monitor - {deployment_info['name']} ({deployment_id[:8]}) - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
table.add_row("Environment", deployment_info["environment"])
|
||||
table.add_row("Desired Instances", str(deployment_info["desired_instances"]))
|
||||
table.add_row("Health Status", "✅ Healthy" if status_data["health_status"] else "❌ Unhealthy")
|
||||
table.add_row("Uptime", f"{status_data['uptime_percentage']:.2f}%")
|
||||
|
||||
if metrics:
|
||||
table.add_row("CPU Usage", f"{metrics['cpu_usage']:.1f}%")
|
||||
table.add_row("Memory Usage", f"{metrics['memory_usage']:.1f}%")
|
||||
table.add_row("Disk Usage", f"{metrics['disk_usage']:.1f}%")
|
||||
table.add_row("Request Count", str(metrics['request_count']))
|
||||
table.add_row("Error Rate", f"{metrics['error_rate']:.2f}%")
|
||||
table.add_row("Response Time", f"{metrics['response_time']:.1f}ms")
|
||||
table.add_row("Active Instances", str(metrics['active_instances']))
|
||||
|
||||
return table
|
||||
except Exception as e:
|
||||
return f"Error getting deployment data: {e}"
|
||||
|
||||
with Live(generate_monitor_table(), refresh_per_second=1) as live:
|
||||
try:
|
||||
while True:
|
||||
live.update(generate_monitor_table())
|
||||
time.sleep(interval)
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error during monitoring: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.argument('deployment_id')
|
||||
@click.pass_context
|
||||
def auto_scale(ctx, deployment_id):
|
||||
"""Trigger auto-scaling evaluation for a deployment"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Trigger auto-scaling
|
||||
success_auto = asyncio.run(deployment.auto_scale_deployment(deployment_id))
|
||||
|
||||
if success_auto:
|
||||
success(f"Auto-scaling evaluation completed for deployment {deployment_id}")
|
||||
else:
|
||||
error(f"Auto-scaling evaluation failed for deployment {deployment_id}")
|
||||
raise click.Abort()
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error in auto-scaling: {str(e)}")
|
||||
raise click.Abort()
|
||||
|
||||
@deploy.command()
|
||||
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
|
||||
@click.pass_context
|
||||
def list_deployments(ctx, format):
|
||||
"""List all deployments"""
|
||||
try:
|
||||
deployment = ProductionDeployment()
|
||||
|
||||
# Get all deployment statuses
|
||||
deployments = []
|
||||
for deployment_id in deployment.deployments.keys():
|
||||
status_data = asyncio.run(deployment.get_deployment_status(deployment_id))
|
||||
if status_data:
|
||||
deployment_info = status_data["deployment"]
|
||||
deployments.append({
|
||||
"Deployment ID": deployment_info["deployment_id"][:8],
|
||||
"Name": deployment_info["name"],
|
||||
"Environment": deployment_info["environment"],
|
||||
"Instances": f"{deployment_info['desired_instances']}/{deployment_info['max_instances']}",
|
||||
"Status": "Running" if status_data["health_status"] else "Stopped",
|
||||
"Uptime": f"{status_data['uptime_percentage']:.1f}%",
|
||||
"Created": deployment_info["created_at"]
|
||||
})
|
||||
|
||||
if not deployments:
|
||||
output("No deployments found", ctx.obj.get('output_format', 'table'))
|
||||
return
|
||||
|
||||
output(deployments, ctx.obj.get('output_format', format), title="All Deployments")
|
||||
|
||||
except Exception as e:
|
||||
error(f"Error listing deployments: {str(e)}")
|
||||
raise click.Abort()
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Core modules for multi-chain functionality
|
||||
"""
|
||||
@@ -1,652 +0,0 @@
|
||||
"""
|
||||
Production deployment and scaling system
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
import uuid
|
||||
import os
|
||||
import sys
|
||||
|
||||
class DeploymentStatus(Enum):
|
||||
"""Deployment status"""
|
||||
PENDING = "pending"
|
||||
DEPLOYING = "deploying"
|
||||
RUNNING = "running"
|
||||
FAILED = "failed"
|
||||
STOPPED = "stopped"
|
||||
SCALING = "scaling"
|
||||
|
||||
class ScalingPolicy(Enum):
|
||||
"""Scaling policies"""
|
||||
MANUAL = "manual"
|
||||
AUTO = "auto"
|
||||
SCHEDULED = "scheduled"
|
||||
LOAD_BASED = "load_based"
|
||||
|
||||
@dataclass
|
||||
class DeploymentConfig:
|
||||
"""Deployment configuration"""
|
||||
deployment_id: str
|
||||
name: str
|
||||
environment: str
|
||||
region: str
|
||||
instance_type: str
|
||||
min_instances: int
|
||||
max_instances: int
|
||||
desired_instances: int
|
||||
scaling_policy: ScalingPolicy
|
||||
health_check_path: str
|
||||
port: int
|
||||
ssl_enabled: bool
|
||||
domain: str
|
||||
database_config: Dict[str, Any]
|
||||
monitoring_enabled: bool
|
||||
backup_enabled: bool
|
||||
auto_scaling_enabled: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@dataclass
|
||||
class DeploymentMetrics:
|
||||
"""Deployment performance metrics"""
|
||||
deployment_id: str
|
||||
cpu_usage: float
|
||||
memory_usage: float
|
||||
disk_usage: float
|
||||
network_in: float
|
||||
network_out: float
|
||||
request_count: int
|
||||
error_rate: float
|
||||
response_time: float
|
||||
uptime_percentage: float
|
||||
active_instances: int
|
||||
last_updated: datetime
|
||||
|
||||
@dataclass
|
||||
class ScalingEvent:
|
||||
"""Scaling event record"""
|
||||
event_id: str
|
||||
deployment_id: str
|
||||
scaling_type: str
|
||||
old_instances: int
|
||||
new_instances: int
|
||||
trigger_reason: str
|
||||
triggered_at: datetime
|
||||
completed_at: Optional[datetime]
|
||||
success: bool
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
class ProductionDeployment:
|
||||
"""Production deployment and scaling system"""
|
||||
|
||||
def __init__(self, config_path: str = "/home/oib/windsurf/aitbc"):
|
||||
self.config_path = Path(config_path)
|
||||
self.deployments: Dict[str, DeploymentConfig] = {}
|
||||
self.metrics: Dict[str, DeploymentMetrics] = {}
|
||||
self.scaling_events: List[ScalingEvent] = []
|
||||
self.health_checks: Dict[str, bool] = {}
|
||||
|
||||
# Deployment paths
|
||||
self.deployment_dir = self.config_path / "deployments"
|
||||
self.config_dir = self.config_path / "config"
|
||||
self.logs_dir = self.config_path / "logs"
|
||||
self.backups_dir = self.config_path / "backups"
|
||||
|
||||
# Ensure directories exist
|
||||
self.config_path.mkdir(parents=True, exist_ok=True)
|
||||
self.deployment_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.backups_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Scaling thresholds
|
||||
self.scaling_thresholds = {
|
||||
'cpu_high': 80.0,
|
||||
'cpu_low': 20.0,
|
||||
'memory_high': 85.0,
|
||||
'memory_low': 30.0,
|
||||
'error_rate_high': 5.0,
|
||||
'response_time_high': 2000.0, # ms
|
||||
'min_uptime': 99.0
|
||||
}
|
||||
|
||||
async def create_deployment(self, name: str, environment: str, region: str,
|
||||
instance_type: str, min_instances: int, max_instances: int,
|
||||
desired_instances: int, port: int, domain: str,
|
||||
database_config: Dict[str, Any]) -> Optional[str]:
|
||||
"""Create a new deployment configuration"""
|
||||
try:
|
||||
deployment_id = str(uuid.uuid4())
|
||||
|
||||
deployment = DeploymentConfig(
|
||||
deployment_id=deployment_id,
|
||||
name=name,
|
||||
environment=environment,
|
||||
region=region,
|
||||
instance_type=instance_type,
|
||||
min_instances=min_instances,
|
||||
max_instances=max_instances,
|
||||
desired_instances=desired_instances,
|
||||
scaling_policy=ScalingPolicy.AUTO,
|
||||
health_check_path="/health",
|
||||
port=port,
|
||||
ssl_enabled=True,
|
||||
domain=domain,
|
||||
database_config=database_config,
|
||||
monitoring_enabled=True,
|
||||
backup_enabled=True,
|
||||
auto_scaling_enabled=True,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
|
||||
self.deployments[deployment_id] = deployment
|
||||
|
||||
# Create deployment directory structure
|
||||
deployment_path = self.deployment_dir / deployment_id
|
||||
deployment_path.mkdir(exist_ok=True)
|
||||
|
||||
# Generate deployment configuration files
|
||||
await self._generate_deployment_configs(deployment, deployment_path)
|
||||
|
||||
return deployment_id
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating deployment: {e}")
|
||||
return None
|
||||
|
||||
async def deploy_application(self, deployment_id: str) -> bool:
|
||||
"""Deploy the application to production"""
|
||||
try:
|
||||
deployment = self.deployments.get(deployment_id)
|
||||
if not deployment:
|
||||
return False
|
||||
|
||||
print(f"Starting deployment of {deployment.name} ({deployment_id})")
|
||||
|
||||
# 1. Build application
|
||||
build_success = await self._build_application(deployment)
|
||||
if not build_success:
|
||||
return False
|
||||
|
||||
# 2. Deploy infrastructure
|
||||
infra_success = await self._deploy_infrastructure(deployment)
|
||||
if not infra_success:
|
||||
return False
|
||||
|
||||
# 3. Configure monitoring
|
||||
monitoring_success = await self._setup_monitoring(deployment)
|
||||
if not monitoring_success:
|
||||
return False
|
||||
|
||||
# 4. Start health checks
|
||||
await self._start_health_checks(deployment)
|
||||
|
||||
# 5. Initialize metrics collection
|
||||
await self._initialize_metrics(deployment_id)
|
||||
|
||||
print(f"Deployment {deployment_id} completed successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error deploying application: {e}")
|
||||
return False
|
||||
|
||||
async def scale_deployment(self, deployment_id: str, target_instances: int,
|
||||
reason: str = "manual") -> bool:
|
||||
"""Scale a deployment to target instance count"""
|
||||
try:
|
||||
deployment = self.deployments.get(deployment_id)
|
||||
if not deployment:
|
||||
return False
|
||||
|
||||
# Validate scaling limits
|
||||
if target_instances < deployment.min_instances or target_instances > deployment.max_instances:
|
||||
return False
|
||||
|
||||
old_instances = deployment.desired_instances
|
||||
|
||||
# Create scaling event
|
||||
scaling_event = ScalingEvent(
|
||||
event_id=str(uuid.uuid4()),
|
||||
deployment_id=deployment_id,
|
||||
scaling_type="manual" if reason == "manual" else "auto",
|
||||
old_instances=old_instances,
|
||||
new_instances=target_instances,
|
||||
trigger_reason=reason,
|
||||
triggered_at=datetime.now(),
|
||||
completed_at=None,
|
||||
success=False,
|
||||
metadata={"deployment_name": deployment.name}
|
||||
)
|
||||
|
||||
self.scaling_events.append(scaling_event)
|
||||
|
||||
# Update deployment
|
||||
deployment.desired_instances = target_instances
|
||||
deployment.updated_at = datetime.now()
|
||||
|
||||
# Execute scaling
|
||||
scaling_success = await self._execute_scaling(deployment, target_instances)
|
||||
|
||||
# Update scaling event
|
||||
scaling_event.completed_at = datetime.now()
|
||||
scaling_event.success = scaling_success
|
||||
|
||||
if scaling_success:
|
||||
print(f"Scaled deployment {deployment_id} from {old_instances} to {target_instances} instances")
|
||||
else:
|
||||
# Rollback on failure
|
||||
deployment.desired_instances = old_instances
|
||||
print(f"Scaling failed, rolled back to {old_instances} instances")
|
||||
|
||||
return scaling_success
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scaling deployment: {e}")
|
||||
return False
|
||||
|
||||
async def auto_scale_deployment(self, deployment_id: str) -> bool:
|
||||
"""Automatically scale deployment based on metrics"""
|
||||
try:
|
||||
deployment = self.deployments.get(deployment_id)
|
||||
if not deployment or not deployment.auto_scaling_enabled:
|
||||
return False
|
||||
|
||||
metrics = self.metrics.get(deployment_id)
|
||||
if not metrics:
|
||||
return False
|
||||
|
||||
current_instances = deployment.desired_instances
|
||||
new_instances = current_instances
|
||||
|
||||
# Scale up conditions
|
||||
scale_up_triggers = []
|
||||
if metrics.cpu_usage > self.scaling_thresholds['cpu_high']:
|
||||
scale_up_triggers.append(f"CPU usage high: {metrics.cpu_usage:.1f}%")
|
||||
|
||||
if metrics.memory_usage > self.scaling_thresholds['memory_high']:
|
||||
scale_up_triggers.append(f"Memory usage high: {metrics.memory_usage:.1f}%")
|
||||
|
||||
if metrics.error_rate > self.scaling_thresholds['error_rate_high']:
|
||||
scale_up_triggers.append(f"Error rate high: {metrics.error_rate:.1f}%")
|
||||
|
||||
# Scale down conditions
|
||||
scale_down_triggers = []
|
||||
if (metrics.cpu_usage < self.scaling_thresholds['cpu_low'] and
|
||||
metrics.memory_usage < self.scaling_thresholds['memory_low'] and
|
||||
current_instances > deployment.min_instances):
|
||||
scale_down_triggers.append("Low resource usage")
|
||||
|
||||
# Execute scaling
|
||||
if scale_up_triggers and current_instances < deployment.max_instances:
|
||||
new_instances = min(current_instances + 1, deployment.max_instances)
|
||||
reason = f"Auto scale up: {', '.join(scale_up_triggers)}"
|
||||
return await self.scale_deployment(deployment_id, new_instances, reason)
|
||||
|
||||
elif scale_down_triggers and current_instances > deployment.min_instances:
|
||||
new_instances = max(current_instances - 1, deployment.min_instances)
|
||||
reason = f"Auto scale down: {', '.join(scale_down_triggers)}"
|
||||
return await self.scale_deployment(deployment_id, new_instances, reason)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in auto-scaling: {e}")
|
||||
return False
|
||||
|
||||
async def get_deployment_status(self, deployment_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get comprehensive deployment status"""
|
||||
try:
|
||||
deployment = self.deployments.get(deployment_id)
|
||||
if not deployment:
|
||||
return None
|
||||
|
||||
metrics = self.metrics.get(deployment_id)
|
||||
health_status = self.health_checks.get(deployment_id, False)
|
||||
|
||||
# Get recent scaling events
|
||||
recent_events = [
|
||||
event for event in self.scaling_events
|
||||
if event.deployment_id == deployment_id and
|
||||
event.triggered_at >= datetime.now() - timedelta(hours=24)
|
||||
]
|
||||
|
||||
status = {
|
||||
"deployment": asdict(deployment),
|
||||
"metrics": asdict(metrics) if metrics else None,
|
||||
"health_status": health_status,
|
||||
"recent_scaling_events": [asdict(event) for event in recent_events[-5:]],
|
||||
"uptime_percentage": metrics.uptime_percentage if metrics else 0.0,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return status
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting deployment status: {e}")
|
||||
return None
|
||||
|
||||
async def get_cluster_overview(self) -> Dict[str, Any]:
|
||||
"""Get overview of all deployments"""
|
||||
try:
|
||||
total_deployments = len(self.deployments)
|
||||
running_deployments = len([
|
||||
d for d in self.deployments.values()
|
||||
if self.health_checks.get(d.deployment_id, False)
|
||||
])
|
||||
|
||||
total_instances = sum(d.desired_instances for d in self.deployments.values())
|
||||
|
||||
# Calculate aggregate metrics
|
||||
aggregate_metrics = {
|
||||
"total_cpu_usage": 0.0,
|
||||
"total_memory_usage": 0.0,
|
||||
"total_disk_usage": 0.0,
|
||||
"average_response_time": 0.0,
|
||||
"average_error_rate": 0.0,
|
||||
"average_uptime": 0.0
|
||||
}
|
||||
|
||||
active_metrics = [m for m in self.metrics.values()]
|
||||
if active_metrics:
|
||||
aggregate_metrics["total_cpu_usage"] = sum(m.cpu_usage for m in active_metrics) / len(active_metrics)
|
||||
aggregate_metrics["total_memory_usage"] = sum(m.memory_usage for m in active_metrics) / len(active_metrics)
|
||||
aggregate_metrics["total_disk_usage"] = sum(m.disk_usage for m in active_metrics) / len(active_metrics)
|
||||
aggregate_metrics["average_response_time"] = sum(m.response_time for m in active_metrics) / len(active_metrics)
|
||||
aggregate_metrics["average_error_rate"] = sum(m.error_rate for m in active_metrics) / len(active_metrics)
|
||||
aggregate_metrics["average_uptime"] = sum(m.uptime_percentage for m in active_metrics) / len(active_metrics)
|
||||
|
||||
# Recent scaling activity
|
||||
recent_scaling = [
|
||||
event for event in self.scaling_events
|
||||
if event.triggered_at >= datetime.now() - timedelta(hours=24)
|
||||
]
|
||||
|
||||
overview = {
|
||||
"total_deployments": total_deployments,
|
||||
"running_deployments": running_deployments,
|
||||
"total_instances": total_instances,
|
||||
"aggregate_metrics": aggregate_metrics,
|
||||
"recent_scaling_events": len(recent_scaling),
|
||||
"successful_scaling_rate": sum(1 for e in recent_scaling if e.success) / len(recent_scaling) if recent_scaling else 0.0,
|
||||
"health_check_coverage": len(self.health_checks) / total_deployments if total_deployments > 0 else 0.0,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return overview
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting cluster overview: {e}")
|
||||
return {}
|
||||
|
||||
async def _generate_deployment_configs(self, deployment: DeploymentConfig, deployment_path: Path):
|
||||
"""Generate deployment configuration files"""
|
||||
try:
|
||||
# Generate systemd service file
|
||||
service_content = f"""[Unit]
|
||||
Description={deployment.name} Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aitbc
|
||||
WorkingDirectory={self.config_path}
|
||||
ExecStart=/usr/bin/python3 -m aitbc_cli.main --port {deployment.port}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=PYTHONPATH={self.config_path}
|
||||
Environment=DEPLOYMENT_ID={deployment.deployment_id}
|
||||
Environment=ENVIRONMENT={deployment.environment}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
|
||||
service_file = deployment_path / f"{deployment.name}.service"
|
||||
with open(service_file, 'w') as f:
|
||||
f.write(service_content)
|
||||
|
||||
# Generate nginx configuration
|
||||
nginx_content = f"""upstream {deployment.name}_backend {{
|
||||
server 127.0.0.1:{deployment.port};
|
||||
}}
|
||||
|
||||
server {{
|
||||
listen 80;
|
||||
server_name {deployment.domain};
|
||||
|
||||
location / {{
|
||||
proxy_pass http://{deployment.name}_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}}
|
||||
|
||||
location {deployment.health_check_path} {{
|
||||
proxy_pass http://{deployment.name}_backend;
|
||||
access_log off;
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
nginx_file = deployment_path / f"{deployment.name}.nginx.conf"
|
||||
with open(nginx_file, 'w') as f:
|
||||
f.write(nginx_content)
|
||||
|
||||
# Generate monitoring configuration
|
||||
monitoring_content = f"""# Monitoring configuration for {deployment.name}
|
||||
deployment_id: {deployment.deployment_id}
|
||||
name: {deployment.name}
|
||||
environment: {deployment.environment}
|
||||
port: {deployment.port}
|
||||
health_check_path: {deployment.health_check_path}
|
||||
metrics_interval: 30
|
||||
alert_thresholds:
|
||||
cpu_usage: {self.scaling_thresholds['cpu_high']}
|
||||
memory_usage: {self.scaling_thresholds['memory_high']}
|
||||
error_rate: {self.scaling_thresholds['error_rate_high']}
|
||||
response_time: {self.scaling_thresholds['response_time_high']}
|
||||
"""
|
||||
|
||||
monitoring_file = deployment_path / "monitoring.yml"
|
||||
with open(monitoring_file, 'w') as f:
|
||||
f.write(monitoring_content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating deployment configs: {e}")
|
||||
|
||||
async def _build_application(self, deployment: DeploymentConfig) -> bool:
|
||||
"""Build the application for deployment"""
|
||||
try:
|
||||
print(f"Building application for {deployment.name}")
|
||||
|
||||
# Simulate build process
|
||||
build_steps = [
|
||||
"Installing dependencies...",
|
||||
"Compiling application...",
|
||||
"Running tests...",
|
||||
"Creating deployment package...",
|
||||
"Optimizing for production..."
|
||||
]
|
||||
|
||||
for step in build_steps:
|
||||
print(f" {step}")
|
||||
await asyncio.sleep(0.5) # Simulate build time
|
||||
|
||||
print("Build completed successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error building application: {e}")
|
||||
return False
|
||||
|
||||
async def _deploy_infrastructure(self, deployment: DeploymentConfig) -> bool:
|
||||
"""Deploy infrastructure components"""
|
||||
try:
|
||||
print(f"Deploying infrastructure for {deployment.name}")
|
||||
|
||||
# Deploy systemd service
|
||||
service_file = self.deployment_dir / deployment.deployment_id / f"{deployment.name}.service"
|
||||
system_service_path = Path("/etc/systemd/system") / f"{deployment.name}.service"
|
||||
|
||||
if service_file.exists():
|
||||
shutil.copy2(service_file, system_service_path)
|
||||
subprocess.run(["systemctl", "daemon-reload"], check=True)
|
||||
subprocess.run(["systemctl", "enable", deployment.name], check=True)
|
||||
subprocess.run(["systemctl", "start", deployment.name], check=True)
|
||||
print(f" Service {deployment.name} started")
|
||||
|
||||
# Deploy nginx configuration
|
||||
nginx_file = self.deployment_dir / deployment.deployment_id / f"{deployment.name}.nginx.conf"
|
||||
nginx_config_path = Path("/etc/nginx/sites-available") / f"{deployment.name}.conf"
|
||||
|
||||
if nginx_file.exists():
|
||||
shutil.copy2(nginx_file, nginx_config_path)
|
||||
|
||||
# Enable site
|
||||
sites_enabled = Path("/etc/nginx/sites-enabled")
|
||||
site_link = sites_enabled / f"{deployment.name}.conf"
|
||||
if not site_link.exists():
|
||||
site_link.symlink_to(nginx_config_path)
|
||||
|
||||
subprocess.run(["nginx", "-t"], check=True)
|
||||
subprocess.run(["systemctl", "reload", "nginx"], check=True)
|
||||
print(f" Nginx configuration updated")
|
||||
|
||||
print("Infrastructure deployment completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error deploying infrastructure: {e}")
|
||||
return False
|
||||
|
||||
async def _setup_monitoring(self, deployment: DeploymentConfig) -> bool:
|
||||
"""Set up monitoring for the deployment"""
|
||||
try:
|
||||
print(f"Setting up monitoring for {deployment.name}")
|
||||
|
||||
monitoring_file = self.deployment_dir / deployment.deployment_id / "monitoring.yml"
|
||||
if monitoring_file.exists():
|
||||
print(f" Monitoring configuration loaded")
|
||||
print(f" Health checks enabled on {deployment.health_check_path}")
|
||||
print(f" Metrics collection started")
|
||||
|
||||
print("Monitoring setup completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting up monitoring: {e}")
|
||||
return False
|
||||
|
||||
async def _start_health_checks(self, deployment: DeploymentConfig):
|
||||
"""Start health checks for the deployment"""
|
||||
try:
|
||||
print(f"Starting health checks for {deployment.name}")
|
||||
|
||||
# Initialize health status
|
||||
self.health_checks[deployment.deployment_id] = True
|
||||
|
||||
# Start periodic health checks
|
||||
asyncio.create_task(self._periodic_health_check(deployment))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error starting health checks: {e}")
|
||||
|
||||
async def _periodic_health_check(self, deployment: DeploymentConfig):
|
||||
"""Periodic health check for deployment"""
|
||||
while True:
|
||||
try:
|
||||
# Simulate health check
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
|
||||
# Update health status (simulated)
|
||||
self.health_checks[deployment.deployment_id] = True
|
||||
|
||||
# Update metrics
|
||||
await self._update_metrics(deployment.deployment_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in health check for {deployment.name}: {e}")
|
||||
self.health_checks[deployment.deployment_id] = False
|
||||
|
||||
async def _initialize_metrics(self, deployment_id: str):
|
||||
"""Initialize metrics collection for deployment"""
|
||||
try:
|
||||
metrics = DeploymentMetrics(
|
||||
deployment_id=deployment_id,
|
||||
cpu_usage=0.0,
|
||||
memory_usage=0.0,
|
||||
disk_usage=0.0,
|
||||
network_in=0.0,
|
||||
network_out=0.0,
|
||||
request_count=0,
|
||||
error_rate=0.0,
|
||||
response_time=0.0,
|
||||
uptime_percentage=100.0,
|
||||
active_instances=1,
|
||||
last_updated=datetime.now()
|
||||
)
|
||||
|
||||
self.metrics[deployment_id] = metrics
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error initializing metrics: {e}")
|
||||
|
||||
async def _update_metrics(self, deployment_id: str):
|
||||
"""Update deployment metrics"""
|
||||
try:
|
||||
metrics = self.metrics.get(deployment_id)
|
||||
if not metrics:
|
||||
return
|
||||
|
||||
# Simulate metric updates (in production, these would be real metrics)
|
||||
import random
|
||||
|
||||
metrics.cpu_usage = random.uniform(10, 70)
|
||||
metrics.memory_usage = random.uniform(20, 80)
|
||||
metrics.disk_usage = random.uniform(30, 60)
|
||||
metrics.network_in = random.uniform(100, 1000)
|
||||
metrics.network_out = random.uniform(50, 500)
|
||||
metrics.request_count += random.randint(10, 100)
|
||||
metrics.error_rate = random.uniform(0, 2)
|
||||
metrics.response_time = random.uniform(50, 500)
|
||||
metrics.uptime_percentage = random.uniform(99.0, 100.0)
|
||||
metrics.last_updated = datetime.now()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating metrics: {e}")
|
||||
|
||||
async def _execute_scaling(self, deployment: DeploymentConfig, target_instances: int) -> bool:
|
||||
"""Execute scaling operation"""
|
||||
try:
|
||||
print(f"Executing scaling to {target_instances} instances")
|
||||
|
||||
# Simulate scaling process
|
||||
scaling_steps = [
|
||||
f"Provisioning {target_instances - deployment.desired_instances} new instances...",
|
||||
"Configuring new instances...",
|
||||
"Load balancing configuration...",
|
||||
"Health checks on new instances...",
|
||||
"Traffic migration..."
|
||||
]
|
||||
|
||||
for step in scaling_steps:
|
||||
print(f" {step}")
|
||||
await asyncio.sleep(1) # Simulate scaling time
|
||||
|
||||
print("Scaling completed successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error executing scaling: {e}")
|
||||
return False
|
||||
@@ -1,424 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Real KYC/AML Provider Integration
|
||||
Connects with actual KYC/AML service providers for compliance verification
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class KYCProvider(str, Enum):
|
||||
"""KYC service providers"""
|
||||
CHAINALYSIS = "chainalysis"
|
||||
SUMSUB = "sumsub"
|
||||
ONFIDO = "onfido"
|
||||
JUMIO = "jumio"
|
||||
VERIFF = "veriff"
|
||||
|
||||
class KYCStatus(str, Enum):
|
||||
"""KYC verification status"""
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
FAILED = "failed"
|
||||
EXPIRED = "expired"
|
||||
|
||||
class AMLRiskLevel(str, Enum):
|
||||
"""AML risk levels"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
@dataclass
|
||||
class KYCRequest:
|
||||
"""KYC verification request"""
|
||||
user_id: str
|
||||
provider: KYCProvider
|
||||
customer_data: Dict[str, Any]
|
||||
documents: List[Dict[str, Any]] = None
|
||||
verification_level: str = "standard" # standard, enhanced
|
||||
|
||||
@dataclass
|
||||
class KYCResponse:
|
||||
"""KYC verification response"""
|
||||
request_id: str
|
||||
user_id: str
|
||||
provider: KYCProvider
|
||||
status: KYCStatus
|
||||
risk_score: float
|
||||
verification_data: Dict[str, Any]
|
||||
created_at: datetime
|
||||
expires_at: Optional[datetime] = None
|
||||
rejection_reason: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class AMLCheck:
|
||||
"""AML screening check"""
|
||||
check_id: str
|
||||
user_id: str
|
||||
provider: str
|
||||
risk_level: AMLRiskLevel
|
||||
risk_score: float
|
||||
sanctions_hits: List[Dict[str, Any]]
|
||||
pep_hits: List[Dict[str, Any]]
|
||||
adverse_media: List[Dict[str, Any]]
|
||||
checked_at: datetime
|
||||
|
||||
class RealKYCProvider:
|
||||
"""Real KYC provider integration"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_keys: Dict[KYCProvider, str] = {}
|
||||
self.base_urls: Dict[KYCProvider, str] = {
|
||||
KYCProvider.CHAINALYSIS: "https://api.chainalysis.com",
|
||||
KYCProvider.SUMSUB: "https://api.sumsub.com",
|
||||
KYCProvider.ONFIDO: "https://api.onfido.com",
|
||||
KYCProvider.JUMIO: "https://api.jumio.com",
|
||||
KYCProvider.VERIFF: "https://api.veriff.com"
|
||||
}
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry"""
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
def set_api_key(self, provider: KYCProvider, api_key: str):
|
||||
"""Set API key for provider"""
|
||||
self.api_keys[provider] = api_key
|
||||
logger.info(f"✅ API key set for {provider}")
|
||||
|
||||
async def submit_kyc_verification(self, request: KYCRequest) -> KYCResponse:
|
||||
"""Submit KYC verification to provider"""
|
||||
try:
|
||||
if request.provider not in self.api_keys:
|
||||
raise ValueError(f"No API key configured for {request.provider}")
|
||||
|
||||
if request.provider == KYCProvider.CHAINALYSIS:
|
||||
return await self._chainalysis_kyc(request)
|
||||
elif request.provider == KYCProvider.SUMSUB:
|
||||
return await self._sumsub_kyc(request)
|
||||
elif request.provider == KYCProvider.ONFIDO:
|
||||
return await self._onfido_kyc(request)
|
||||
elif request.provider == KYCProvider.JUMIO:
|
||||
return await self._jumio_kyc(request)
|
||||
elif request.provider == KYCProvider.VERIFF:
|
||||
return await self._veriff_kyc(request)
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {request.provider}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ KYC submission failed: {e}")
|
||||
raise
|
||||
|
||||
async def _chainalysis_kyc(self, request: KYCRequest) -> KYCResponse:
|
||||
"""Chainalysis KYC verification"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_keys[KYCProvider.CHAINALYSIS]}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# Mock Chainalysis API call (would be real in production)
|
||||
payload = {
|
||||
"userId": request.user_id,
|
||||
"customerData": request.customer_data,
|
||||
"verificationLevel": request.verification_level
|
||||
}
|
||||
|
||||
# Simulate API response
|
||||
await asyncio.sleep(1) # Simulate network latency
|
||||
|
||||
return KYCResponse(
|
||||
request_id=f"chainalysis_{request.user_id}_{int(datetime.now().timestamp())}",
|
||||
user_id=request.user_id,
|
||||
provider=KYCProvider.CHAINALYSIS,
|
||||
status=KYCStatus.PENDING,
|
||||
risk_score=0.15,
|
||||
verification_data={"provider": "chainalysis", "submitted": True},
|
||||
created_at=datetime.now(),
|
||||
expires_at=datetime.now() + timedelta(days=30)
|
||||
)
|
||||
|
||||
async def _sumsub_kyc(self, request: KYCRequest) -> KYCResponse:
|
||||
"""Sumsub KYC verification"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_keys[KYCProvider.SUMSUB]}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# Mock Sumsub API call
|
||||
payload = {
|
||||
"applicantId": request.user_id,
|
||||
"externalUserId": request.user_id,
|
||||
"info": {
|
||||
"firstName": request.customer_data.get("first_name"),
|
||||
"lastName": request.customer_data.get("last_name"),
|
||||
"email": request.customer_data.get("email")
|
||||
}
|
||||
}
|
||||
|
||||
await asyncio.sleep(1.5) # Simulate network latency
|
||||
|
||||
return KYCResponse(
|
||||
request_id=f"sumsub_{request.user_id}_{int(datetime.now().timestamp())}",
|
||||
user_id=request.user_id,
|
||||
provider=KYCProvider.SUMSUB,
|
||||
status=KYCStatus.PENDING,
|
||||
risk_score=0.12,
|
||||
verification_data={"provider": "sumsub", "submitted": True},
|
||||
created_at=datetime.now(),
|
||||
expires_at=datetime.now() + timedelta(days=90)
|
||||
)
|
||||
|
||||
async def _onfido_kyc(self, request: KYCRequest) -> KYCResponse:
|
||||
"""Onfido KYC verification"""
|
||||
await asyncio.sleep(1.2)
|
||||
|
||||
return KYCResponse(
|
||||
request_id=f"onfido_{request.user_id}_{int(datetime.now().timestamp())}",
|
||||
user_id=request.user_id,
|
||||
provider=KYCProvider.ONFIDO,
|
||||
status=KYCStatus.PENDING,
|
||||
risk_score=0.08,
|
||||
verification_data={"provider": "onfido", "submitted": True},
|
||||
created_at=datetime.now(),
|
||||
expires_at=datetime.now() + timedelta(days=60)
|
||||
)
|
||||
|
||||
async def _jumio_kyc(self, request: KYCRequest) -> KYCResponse:
|
||||
"""Jumio KYC verification"""
|
||||
await asyncio.sleep(1.3)
|
||||
|
||||
return KYCResponse(
|
||||
request_id=f"jumio_{request.user_id}_{int(datetime.now().timestamp())}",
|
||||
user_id=request.user_id,
|
||||
provider=KYCProvider.JUMIO,
|
||||
status=KYCStatus.PENDING,
|
||||
risk_score=0.10,
|
||||
verification_data={"provider": "jumio", "submitted": True},
|
||||
created_at=datetime.now(),
|
||||
expires_at=datetime.now() + timedelta(days=45)
|
||||
)
|
||||
|
||||
async def _veriff_kyc(self, request: KYCRequest) -> KYCResponse:
|
||||
"""Veriff KYC verification"""
|
||||
await asyncio.sleep(1.1)
|
||||
|
||||
return KYCResponse(
|
||||
request_id=f"veriff_{request.user_id}_{int(datetime.now().timestamp())}",
|
||||
user_id=request.user_id,
|
||||
provider=KYCProvider.VERIFF,
|
||||
status=KYCStatus.PENDING,
|
||||
risk_score=0.07,
|
||||
verification_data={"provider": "veriff", "submitted": True},
|
||||
created_at=datetime.now(),
|
||||
expires_at=datetime.now() + timedelta(days=30)
|
||||
)
|
||||
|
||||
async def check_kyc_status(self, request_id: str, provider: KYCProvider) -> KYCResponse:
|
||||
"""Check KYC verification status"""
|
||||
try:
|
||||
# Mock status check - in production would call provider API
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Simulate different statuses based on request_id
|
||||
hash_val = int(hashlib.md5(request_id.encode()).hexdigest()[:8], 16)
|
||||
|
||||
if hash_val % 4 == 0:
|
||||
status = KYCStatus.APPROVED
|
||||
risk_score = 0.05
|
||||
elif hash_val % 4 == 1:
|
||||
status = KYCStatus.PENDING
|
||||
risk_score = 0.15
|
||||
elif hash_val % 4 == 2:
|
||||
status = KYCStatus.REJECTED
|
||||
risk_score = 0.85
|
||||
rejection_reason = "Document verification failed"
|
||||
else:
|
||||
status = KYCStatus.FAILED
|
||||
risk_score = 0.95
|
||||
rejection_reason = "Technical error during verification"
|
||||
|
||||
return KYCResponse(
|
||||
request_id=request_id,
|
||||
user_id=request_id.split("_")[1],
|
||||
provider=provider,
|
||||
status=status,
|
||||
risk_score=risk_score,
|
||||
verification_data={"provider": provider.value, "checked": True},
|
||||
created_at=datetime.now() - timedelta(hours=1),
|
||||
rejection_reason=rejection_reason if status in [KYCStatus.REJECTED, KYCStatus.FAILED] else None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ KYC status check failed: {e}")
|
||||
raise
|
||||
|
||||
class RealAMLProvider:
|
||||
"""Real AML screening provider"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_keys: Dict[str, str] = {}
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry"""
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
def set_api_key(self, provider: str, api_key: str):
|
||||
"""Set API key for AML provider"""
|
||||
self.api_keys[provider] = api_key
|
||||
logger.info(f"✅ AML API key set for {provider}")
|
||||
|
||||
async def screen_user(self, user_id: str, user_data: Dict[str, Any]) -> AMLCheck:
|
||||
"""Screen user for AML compliance"""
|
||||
try:
|
||||
# Mock AML screening - in production would call real provider
|
||||
await asyncio.sleep(2.0) # Simulate comprehensive screening
|
||||
|
||||
# Simulate different risk levels
|
||||
hash_val = int(hashlib.md5(f"{user_id}_{user_data.get('email', '')}".encode()).hexdigest()[:8], 16)
|
||||
|
||||
if hash_val % 5 == 0:
|
||||
risk_level = AMLRiskLevel.CRITICAL
|
||||
risk_score = 0.95
|
||||
sanctions_hits = [{"list": "OFAC", "name": "Test Sanction", "confidence": 0.9}]
|
||||
elif hash_val % 5 == 1:
|
||||
risk_level = AMLRiskLevel.HIGH
|
||||
risk_score = 0.75
|
||||
sanctions_hits = []
|
||||
elif hash_val % 5 == 2:
|
||||
risk_level = AMLRiskLevel.MEDIUM
|
||||
risk_score = 0.45
|
||||
sanctions_hits = []
|
||||
else:
|
||||
risk_level = AMLRiskLevel.LOW
|
||||
risk_score = 0.15
|
||||
sanctions_hits = []
|
||||
|
||||
return AMLCheck(
|
||||
check_id=f"aml_{user_id}_{int(datetime.now().timestamp())}",
|
||||
user_id=user_id,
|
||||
provider="chainalysis_aml",
|
||||
risk_level=risk_level,
|
||||
risk_score=risk_score,
|
||||
sanctions_hits=sanctions_hits,
|
||||
pep_hits=[], # Politically Exposed Persons
|
||||
adverse_media=[],
|
||||
checked_at=datetime.now()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AML screening failed: {e}")
|
||||
raise
|
||||
|
||||
# Global instances
|
||||
kyc_provider = RealKYCProvider()
|
||||
aml_provider = RealAMLProvider()
|
||||
|
||||
# CLI Interface Functions
|
||||
async def submit_kyc_verification(user_id: str, provider: str, customer_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Submit KYC verification"""
|
||||
async with kyc_provider:
|
||||
kyc_provider.set_api_key(KYCProvider(provider), "demo_api_key")
|
||||
|
||||
request = KYCRequest(
|
||||
user_id=user_id,
|
||||
provider=KYCProvider(provider),
|
||||
customer_data=customer_data
|
||||
)
|
||||
|
||||
response = await kyc_provider.submit_kyc_verification(request)
|
||||
|
||||
return {
|
||||
"request_id": response.request_id,
|
||||
"user_id": response.user_id,
|
||||
"provider": response.provider.value,
|
||||
"status": response.status.value,
|
||||
"risk_score": response.risk_score,
|
||||
"created_at": response.created_at.isoformat()
|
||||
}
|
||||
|
||||
async def check_kyc_status(request_id: str, provider: str) -> Dict[str, Any]:
|
||||
"""Check KYC verification status"""
|
||||
async with kyc_provider:
|
||||
response = await kyc_provider.check_kyc_status(request_id, KYCProvider(provider))
|
||||
|
||||
return {
|
||||
"request_id": response.request_id,
|
||||
"user_id": response.user_id,
|
||||
"provider": response.provider.value,
|
||||
"status": response.status.value,
|
||||
"risk_score": response.risk_score,
|
||||
"rejection_reason": response.rejection_reason,
|
||||
"created_at": response.created_at.isoformat()
|
||||
}
|
||||
|
||||
async def perform_aml_screening(user_id: str, user_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Perform AML screening"""
|
||||
async with aml_provider:
|
||||
aml_provider.set_api_key("chainalysis_aml", "demo_api_key")
|
||||
|
||||
check = await aml_provider.screen_user(user_id, user_data)
|
||||
|
||||
return {
|
||||
"check_id": check.check_id,
|
||||
"user_id": check.user_id,
|
||||
"provider": check.provider,
|
||||
"risk_level": check.risk_level.value,
|
||||
"risk_score": check.risk_score,
|
||||
"sanctions_hits": check.sanctions_hits,
|
||||
"checked_at": check.checked_at.isoformat()
|
||||
}
|
||||
|
||||
# Test function
|
||||
async def test_kyc_aml_integration():
|
||||
"""Test KYC/AML integration"""
|
||||
print("🧪 Testing KYC/AML Integration...")
|
||||
|
||||
# Test KYC submission
|
||||
customer_data = {
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email": "john.doe@example.com",
|
||||
"date_of_birth": "1990-01-01"
|
||||
}
|
||||
|
||||
kyc_result = await submit_kyc_verification("user123", "chainalysis", customer_data)
|
||||
print(f"✅ KYC Submitted: {kyc_result}")
|
||||
|
||||
# Test KYC status check
|
||||
kyc_status = await check_kyc_status(kyc_result["request_id"], "chainalysis")
|
||||
print(f"📋 KYC Status: {kyc_status}")
|
||||
|
||||
# Test AML screening
|
||||
aml_result = await perform_aml_screening("user123", customer_data)
|
||||
print(f"🔍 AML Screening: {aml_result}")
|
||||
|
||||
print("🎉 KYC/AML integration test complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_kyc_aml_integration())
|
||||
@@ -3,7 +3,7 @@
|
||||
import keyring
|
||||
import os
|
||||
from typing import Optional, Dict
|
||||
from ..utils import success, error, warning
|
||||
from utils import success, error, warning
|
||||
|
||||
|
||||
class AuthManager:
|
||||
@@ -4,7 +4,7 @@ import click
|
||||
import httpx
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -496,7 +496,7 @@ def backup(ctx):
|
||||
@click.pass_context
|
||||
def audit_log(ctx, limit: int, action_filter: Optional[str]):
|
||||
"""View audit log"""
|
||||
from ..utils import AuditLogger
|
||||
from utils import AuditLogger
|
||||
|
||||
logger = AuditLogger()
|
||||
entries = logger.get_logs(limit=limit, action_filter=action_filter)
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from aitbc_cli.imports import ensure_coordinator_api_imports
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
|
||||
ensure_coordinator_api_imports()
|
||||
|
||||
@@ -7,7 +7,7 @@ import time
|
||||
import uuid
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -166,10 +166,10 @@ def execute(ctx, agent_id: str, inputs, verification: str, priority: str, timeou
|
||||
|
||||
@agent.command()
|
||||
@click.argument("execution_id")
|
||||
@click.option("--watch", is_flag=True, help="Watch execution status in real-time")
|
||||
@click.option("--timeout", default=30, help="Maximum watch time in seconds")
|
||||
@click.option("--interval", default=5, help="Watch interval in seconds")
|
||||
@click.pass_context
|
||||
def status(ctx, execution_id: str, watch: bool, interval: int):
|
||||
def status(ctx, execution_id: str, timeout: int, interval: int):
|
||||
"""Get status of agent execution"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
@@ -180,35 +180,26 @@ def status(ctx, execution_id: str, watch: bool, interval: int):
|
||||
f"{config.coordinator_url}/api/v1/agents/executions/{execution_id}",
|
||||
headers={"X-Api-Key": config.api_key or ""}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
error(f"Failed to get execution status: {response.status_code}")
|
||||
error(f"Failed to get status: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
return None
|
||||
|
||||
if watch:
|
||||
click.echo(f"Watching execution {execution_id} (Ctrl+C to stop)...")
|
||||
while True:
|
||||
status_data = get_status()
|
||||
if status_data:
|
||||
click.clear()
|
||||
click.echo(f"Execution Status: {status_data.get('status', 'Unknown')}")
|
||||
click.echo(f"Progress: {status_data.get('progress', 0)}%")
|
||||
click.echo(f"Current Step: {status_data.get('current_step', 'N/A')}")
|
||||
click.echo(f"Cost: ${status_data.get('total_cost', 0.0):.4f}")
|
||||
|
||||
if status_data.get('status') in ['completed', 'failed']:
|
||||
break
|
||||
|
||||
time.sleep(interval)
|
||||
else:
|
||||
status_data = get_status()
|
||||
if status_data:
|
||||
output(status_data, ctx.obj['output_format'])
|
||||
# Single status check with timeout
|
||||
status_data = get_status()
|
||||
if status_data:
|
||||
output(status_data, ctx.obj['output_format'])
|
||||
|
||||
# If execution is still running, provide guidance
|
||||
if status_data.get('status') not in ['completed', 'failed']:
|
||||
output(f"Execution still in progress. Use 'aitbc agent status {execution_id}' to check again.",
|
||||
ctx.obj['output_format'])
|
||||
output(f"Current status: {status_data.get('status', 'Unknown')}", ctx.obj['output_format'])
|
||||
output(f"Progress: {status_data.get('progress', 0)}%", ctx.obj['output_format'])
|
||||
|
||||
|
||||
@agent.command()
|
||||
@@ -5,12 +5,12 @@ import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from ..core.config import load_multichain_config
|
||||
from ..core.agent_communication import (
|
||||
from core.config import load_multichain_config
|
||||
from core.agent_communication import (
|
||||
CrossChainAgentCommunication, AgentInfo, AgentMessage,
|
||||
MessageType, AgentStatus
|
||||
)
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def agent_comm():
|
||||
124
cli/commands/ai.py
Normal file
124
cli/commands/ai.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
import click
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
@click.group(name='ai')
|
||||
def ai_group():
|
||||
"""AI marketplace commands."""
|
||||
pass
|
||||
|
||||
@ai_group.command()
|
||||
@click.option('--port', default=8008, show_default=True, help='AI provider port')
|
||||
@click.option('--model', default='qwen3:8b', show_default=True, help='Ollama model name')
|
||||
@click.option('--wallet', 'provider_wallet', required=True, help='Provider wallet address (for verification)')
|
||||
@click.option('--marketplace-url', default='http://127.0.0.1:8014', help='Marketplace API base URL')
|
||||
def status(port, model, provider_wallet, marketplace_url):
|
||||
"""Check AI provider service status."""
|
||||
try:
|
||||
resp = httpx.get(f"http://127.0.0.1:{port}/health", timeout=5.0)
|
||||
if resp.status_code == 200:
|
||||
health = resp.json()
|
||||
click.echo(f"✅ AI Provider Status: {health.get('status', 'unknown')}")
|
||||
click.echo(f" Model: {health.get('model', 'unknown')}")
|
||||
click.echo(f" Wallet: {health.get('wallet', 'unknown')}")
|
||||
else:
|
||||
click.echo(f"❌ AI Provider not responding (status: {resp.status_code})")
|
||||
except httpx.ConnectError:
|
||||
click.echo(f"❌ AI Provider not running on port {port}")
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Error checking AI Provider: {e}")
|
||||
|
||||
@ai_group.command()
|
||||
@click.option('--port', default=8008, show_default=True, help='AI provider port')
|
||||
@click.option('--model', default='qwen3:8b', show_default=True, help='Ollama model name')
|
||||
@click.option('--wallet', 'provider_wallet', required=True, help='Provider wallet address (for verification)')
|
||||
@click.option('--marketplace-url', default='http://127.0.0.1:8014', help='Marketplace API base URL')
|
||||
def start(port, model, provider_wallet, marketplace_url):
|
||||
"""Start AI provider service - provides setup instructions"""
|
||||
click.echo(f"AI Provider Service Setup:")
|
||||
click.echo(f" Port: {port}")
|
||||
click.echo(f" Model: {model}")
|
||||
click.echo(f" Wallet: {provider_wallet}")
|
||||
click.echo(f" Marketplace: {marketplace_url}")
|
||||
|
||||
click.echo("\n📋 To start the AI Provider service:")
|
||||
click.echo(f" 1. Create systemd service: /etc/systemd/system/aitbc-ai-provider.service")
|
||||
click.echo(f" 2. Run: sudo systemctl daemon-reload")
|
||||
click.echo(f" 3. Run: sudo systemctl enable aitbc-ai-provider")
|
||||
click.echo(f" 4. Run: sudo systemctl start aitbc-ai-provider")
|
||||
click.echo(f"\n💡 Use 'aitbc ai status --port {port}' to verify service is running")
|
||||
|
||||
@ai_group.command()
|
||||
def stop():
|
||||
"""Stop AI provider service - provides shutdown instructions"""
|
||||
click.echo("📋 To stop the AI Provider service:")
|
||||
click.echo(" 1. Run: sudo systemctl stop aitbc-ai-provider")
|
||||
click.echo(" 2. Run: sudo systemctl status aitbc-ai-provider (to verify)")
|
||||
click.echo("\n💡 Use 'aitbc ai status' to check if service is stopped")
|
||||
|
||||
@ai_group.command()
|
||||
@click.option('--to', required=True, help='Provider host (IP)')
|
||||
@click.option('--port', default=8008, help='Provider port')
|
||||
@click.option('--prompt', required=True, help='Prompt to send')
|
||||
@click.option('--buyer-wallet', 'buyer_wallet', required=True, help='Buyer wallet name (in local wallet store)')
|
||||
@click.option('--provider-wallet', 'provider_wallet', required=True, help='Provider wallet address (recipient)')
|
||||
@click.option('--amount', default=1, help='Amount to pay in AITBC')
|
||||
def request(to, port, prompt, buyer_wallet, provider_wallet, amount):
|
||||
"""Send a prompt to an AI provider (buyer side) with on‑chain payment."""
|
||||
# Helper to get provider balance
|
||||
def get_balance():
|
||||
res = subprocess.run([
|
||||
sys.executable, "-m", "aitbc_cli.main", "blockchain", "balance",
|
||||
"--address", provider_wallet
|
||||
], capture_output=True, text=True, check=True)
|
||||
for line in res.stdout.splitlines():
|
||||
if "Balance:" in line:
|
||||
parts = line.split(":")
|
||||
return float(parts[1].strip())
|
||||
raise ValueError("Balance not found")
|
||||
|
||||
# Step 1: get initial balance
|
||||
before = get_balance()
|
||||
click.echo(f"Provider balance before: {before}")
|
||||
|
||||
# Step 2: send payment via blockchain CLI (use current Python env)
|
||||
if amount > 0:
|
||||
click.echo(f"Sending {amount} AITBC from wallet '{buyer_wallet}' to {provider_wallet}...")
|
||||
try:
|
||||
subprocess.run([
|
||||
sys.executable, "-m", "aitbc_cli.main", "blockchain", "send",
|
||||
"--from", buyer_wallet,
|
||||
"--to", provider_wallet,
|
||||
"--amount", str(amount)
|
||||
], check=True, capture_output=True, text=True)
|
||||
click.echo("Payment sent.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise click.ClickException(f"Blockchain send failed: {e.stderr}")
|
||||
|
||||
# Step 3: get new balance
|
||||
after = get_balance()
|
||||
click.echo(f"Provider balance after: {after}")
|
||||
delta = after - before
|
||||
click.echo(f"Balance delta: {delta}")
|
||||
|
||||
# Step 4: call provider
|
||||
url = f"http://{to}:{port}/job"
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"buyer": provider_wallet,
|
||||
"amount": amount
|
||||
}
|
||||
try:
|
||||
resp = httpx.post(url, json=payload, timeout=30.0)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
click.echo("Result: " + data.get("result", ""))
|
||||
except httpx.HTTPError as e:
|
||||
raise click.ClickException(f"Request to provider failed: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
ai_group()
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from aitbc_cli.imports import ensure_coordinator_api_imports
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
|
||||
ensure_coordinator_api_imports()
|
||||
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from aitbc_cli.imports import ensure_coordinator_api_imports
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
|
||||
ensure_coordinator_api_imports()
|
||||
|
||||
@@ -4,9 +4,9 @@ import click
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from ..core.config import load_multichain_config
|
||||
from ..core.analytics import ChainAnalytics
|
||||
from ..utils import output, error, success
|
||||
from core.config import load_multichain_config
|
||||
from core.analytics import ChainAnalytics
|
||||
from utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def analytics():
|
||||
@@ -3,8 +3,8 @@
|
||||
import click
|
||||
import os
|
||||
from typing import Optional
|
||||
from ..auth import AuthManager
|
||||
from ..utils import output, success, error, warning
|
||||
from auth import AuthManager
|
||||
from utils import output, success, error, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -12,7 +12,7 @@ def _get_node_endpoint(ctx):
|
||||
return "http://127.0.0.1:8006" # Use new blockchain RPC port
|
||||
|
||||
from typing import Optional, List
|
||||
from ..utils import output, error
|
||||
from utils import output, error
|
||||
import os
|
||||
|
||||
|
||||
@@ -1016,7 +1016,7 @@ def verify_genesis(ctx, chain: str, genesis_hash: Optional[str], verify_signatur
|
||||
"""Verify genesis block integrity for a specific chain"""
|
||||
try:
|
||||
import httpx
|
||||
from ..utils import success
|
||||
from utils import success
|
||||
|
||||
with httpx.Client() as client:
|
||||
# Get genesis block for the specified chain
|
||||
@@ -1129,7 +1129,7 @@ def genesis_hash(ctx, chain: str):
|
||||
"""Get the genesis block hash for a specific chain"""
|
||||
try:
|
||||
import httpx
|
||||
from ..utils import success
|
||||
from utils import success
|
||||
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import click
|
||||
from typing import Optional
|
||||
from ..core.chain_manager import ChainManager, ChainNotFoundError, NodeNotAvailableError
|
||||
from ..core.config import MultiChainConfig, load_multichain_config
|
||||
from ..models.chain import ChainType
|
||||
from ..utils import output, error, success
|
||||
from core.chain_manager import ChainManager, ChainNotFoundError, NodeNotAvailableError
|
||||
from core.config import MultiChainConfig, load_multichain_config
|
||||
from models.chain import ChainType
|
||||
from utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def chain():
|
||||
@@ -200,7 +200,7 @@ def create(ctx, config_file, node, dry_run):
|
||||
"""Create a new chain from configuration file"""
|
||||
try:
|
||||
import yaml
|
||||
from ..models.chain import ChainConfig
|
||||
from models.chain import ChainConfig
|
||||
|
||||
config = load_multichain_config()
|
||||
chain_manager = ChainManager(config)
|
||||
@@ -5,7 +5,7 @@ import httpx
|
||||
import json
|
||||
import time
|
||||
from typing import Optional
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -348,7 +348,7 @@ def batch_submit(ctx, file_path: str, file_format: Optional[str], retries: int,
|
||||
"""Submit multiple jobs from a CSV or JSON file"""
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from ..utils import progress_bar
|
||||
from utils import progress_bar
|
||||
|
||||
config = ctx.obj['config']
|
||||
path = Path(file_path)
|
||||
@@ -11,7 +11,7 @@ from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
# Import compliance providers
|
||||
from aitbc_cli.kyc_aml_providers import submit_kyc_verification, check_kyc_status, perform_aml_screening
|
||||
from utils.kyc_aml_providers import submit_kyc_verification, check_kyc_status, perform_aml_screening
|
||||
|
||||
@click.group()
|
||||
def compliance():
|
||||
@@ -8,8 +8,8 @@ import yaml
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from ..config import get_config, Config
|
||||
from ..utils import output, error, success
|
||||
from config import get_config, Config
|
||||
from utils import output, error, success
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -459,7 +459,7 @@ def delete(ctx, name: str):
|
||||
@click.pass_context
|
||||
def set_secret(ctx, key: str, value: str):
|
||||
"""Set an encrypted configuration value"""
|
||||
from ..utils import encrypt_value
|
||||
from utils import encrypt_value
|
||||
|
||||
config_dir = Path.home() / ".config" / "aitbc"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -488,7 +488,7 @@ def set_secret(ctx, key: str, value: str):
|
||||
@click.pass_context
|
||||
def get_secret(ctx, key: str):
|
||||
"""Get a decrypted configuration value"""
|
||||
from ..utils import decrypt_value
|
||||
from utils import decrypt_value
|
||||
|
||||
secrets_file = Path.home() / ".config" / "aitbc" / "secrets.json"
|
||||
|
||||
@@ -5,8 +5,8 @@ import httpx
|
||||
import json
|
||||
from typing import Optional
|
||||
from tabulate import tabulate
|
||||
from ..config import get_config
|
||||
from ..utils import success, error, output
|
||||
from config import get_config
|
||||
from utils import success, error, output
|
||||
|
||||
|
||||
@click.group()
|
||||
316
cli/commands/dao.py
Normal file
316
cli/commands/dao.py
Normal file
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenClaw DAO CLI Commands
|
||||
Provides command-line interface for DAO governance operations
|
||||
"""
|
||||
|
||||
import click
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any
|
||||
from web3 import Web3
|
||||
from utils.blockchain import get_web3_connection, get_contract
|
||||
from utils.config import load_config
|
||||
|
||||
@click.group()
|
||||
def dao():
|
||||
"""OpenClaw DAO governance commands"""
|
||||
pass
|
||||
|
||||
@dao.command()
|
||||
@click.option('--token-address', required=True, help='Governance token contract address')
|
||||
@click.option('--timelock-address', required=True, help='Timelock controller address')
|
||||
@click.option('--network', default='mainnet', help='Blockchain network')
|
||||
def deploy(token_address: str, timelock_address: str, network: str):
|
||||
"""Deploy OpenClaw DAO contract"""
|
||||
try:
|
||||
w3 = get_web3_connection(network)
|
||||
config = load_config()
|
||||
|
||||
# Account for deployment
|
||||
account = w3.eth.account.from_key(config['private_key'])
|
||||
|
||||
# Contract ABI (simplified)
|
||||
abi = [
|
||||
{
|
||||
"inputs": [
|
||||
{"internalType": "address", "name": "_governanceToken", "type": "address"},
|
||||
{"internalType": "contract TimelockController", "name": "_timelock", "type": "address"}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
}
|
||||
]
|
||||
|
||||
# Deploy contract
|
||||
contract = w3.eth.contract(abi=abi, bytecode="0x...") # Actual bytecode needed
|
||||
|
||||
# Build transaction
|
||||
tx = contract.constructor(token_address, timelock_address).build_transaction({
|
||||
'from': account.address,
|
||||
'gas': 2000000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(account.address)
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
# Wait for confirmation
|
||||
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
click.echo(f"✅ OpenClaw DAO deployed at: {receipt.contractAddress}")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Deployment failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
@click.option('--targets', required=True, help='Comma-separated target addresses')
|
||||
@click.option('--values', required=True, help='Comma-separated ETH values')
|
||||
@click.option('--calldatas', required=True, help='Comma-separated hex calldatas')
|
||||
@click.option('--description', required=True, help='Proposal description')
|
||||
@click.option('--type', 'proposal_type', type=click.Choice(['0', '1', '2', '3']),
|
||||
default='0', help='Proposal type (0=parameter, 1=upgrade, 2=treasury, 3=emergency)')
|
||||
def propose(dao_address: str, targets: str, values: str, calldatas: str,
|
||||
description: str, proposal_type: str):
|
||||
"""Create a new governance proposal"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
config = load_config()
|
||||
|
||||
# Parse inputs
|
||||
target_addresses = targets.split(',')
|
||||
value_list = [int(v) for v in values.split(',')]
|
||||
calldata_list = calldatas.split(',')
|
||||
|
||||
# Get contract
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Build transaction
|
||||
tx = dao_contract.functions.propose(
|
||||
target_addresses,
|
||||
value_list,
|
||||
calldata_list,
|
||||
description,
|
||||
int(proposal_type)
|
||||
).build_transaction({
|
||||
'from': config['address'],
|
||||
'gas': 500000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(config['address'])
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
# Get proposal ID
|
||||
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
# Parse proposal ID from events
|
||||
proposal_id = None
|
||||
for log in receipt.logs:
|
||||
try:
|
||||
event = dao_contract.events.ProposalCreated().process_log(log)
|
||||
proposal_id = event.args.proposalId
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
click.echo(f"✅ Proposal created!")
|
||||
click.echo(f"📋 Proposal ID: {proposal_id}")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Proposal creation failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
@click.option('--proposal-id', required=True, type=int, help='Proposal ID')
|
||||
def vote(dao_address: str, proposal_id: int):
|
||||
"""Cast a vote on a proposal"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
config = load_config()
|
||||
|
||||
# Get contract
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Check proposal state
|
||||
state = dao_contract.functions.state(proposal_id).call()
|
||||
if state != 1: # Active
|
||||
click.echo("❌ Proposal is not active for voting")
|
||||
return
|
||||
|
||||
# Get voting power
|
||||
token_address = dao_contract.functions.governanceToken().call()
|
||||
token_contract = get_contract(token_address, "ERC20")
|
||||
voting_power = token_contract.functions.balanceOf(config['address']).call()
|
||||
|
||||
if voting_power == 0:
|
||||
click.echo("❌ No voting power (no governance tokens)")
|
||||
return
|
||||
|
||||
click.echo(f"🗳️ Your voting power: {voting_power}")
|
||||
|
||||
# Get vote choice
|
||||
support = click.prompt(
|
||||
"Vote (0=Against, 1=For, 2=Abstain)",
|
||||
type=click.Choice(['0', '1', '2'])
|
||||
)
|
||||
|
||||
reason = click.prompt("Reason (optional)", default="", show_default=False)
|
||||
|
||||
# Build transaction
|
||||
tx = dao_contract.functions.castVoteWithReason(
|
||||
proposal_id,
|
||||
int(support),
|
||||
reason
|
||||
).build_transaction({
|
||||
'from': config['address'],
|
||||
'gas': 100000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(config['address'])
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
click.echo(f"✅ Vote cast!")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Voting failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
@click.option('--proposal-id', required=True, type=int, help='Proposal ID')
|
||||
def execute(dao_address: str, proposal_id: int):
|
||||
"""Execute a successful proposal"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
config = load_config()
|
||||
|
||||
# Get contract
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Check proposal state
|
||||
state = dao_contract.functions.state(proposal_id).call()
|
||||
if state != 7: # Succeeded
|
||||
click.echo("❌ Proposal has not succeeded")
|
||||
return
|
||||
|
||||
# Build transaction
|
||||
tx = dao_contract.functions.execute(proposal_id).build_transaction({
|
||||
'from': config['address'],
|
||||
'gas': 300000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(config['address'])
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
click.echo(f"✅ Proposal executed!")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Execution failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
def list_proposals(dao_address: str):
|
||||
"""List all proposals"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Get proposal count
|
||||
proposal_count = dao_contract.functions.proposalCount().call()
|
||||
|
||||
click.echo(f"📋 Found {proposal_count} proposals:\n")
|
||||
|
||||
for i in range(1, proposal_count + 1):
|
||||
try:
|
||||
proposal = dao_contract.functions.getProposal(i).call()
|
||||
state = dao_contract.functions.state(i).call()
|
||||
|
||||
state_names = {
|
||||
0: "Pending",
|
||||
1: "Active",
|
||||
2: "Canceled",
|
||||
3: "Defeated",
|
||||
4: "Succeeded",
|
||||
5: "Queued",
|
||||
6: "Expired",
|
||||
7: "Executed"
|
||||
}
|
||||
|
||||
type_names = {
|
||||
0: "Parameter Change",
|
||||
1: "Protocol Upgrade",
|
||||
2: "Treasury Allocation",
|
||||
3: "Emergency Action"
|
||||
}
|
||||
|
||||
click.echo(f"🔹 Proposal #{i}")
|
||||
click.echo(f" Type: {type_names.get(proposal[3], 'Unknown')}")
|
||||
click.echo(f" State: {state_names.get(state, 'Unknown')}")
|
||||
click.echo(f" Description: {proposal[4]}")
|
||||
click.echo(f" For: {proposal[6]}, Against: {proposal[7]}, Abstain: {proposal[8]}")
|
||||
click.echo()
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to list proposals: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
def status(dao_address: str):
|
||||
"""Show DAO status and statistics"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Get DAO info
|
||||
token_address = dao_contract.functions.governanceToken().call()
|
||||
token_contract = get_contract(token_address, "ERC20")
|
||||
|
||||
total_supply = token_contract.functions.totalSupply().call()
|
||||
proposal_count = dao_contract.functions.proposalCount().call()
|
||||
|
||||
# Get active proposals
|
||||
active_proposals = dao_contract.functions.getActiveProposals().call()
|
||||
|
||||
click.echo("🏛️ OpenClaw DAO Status")
|
||||
click.echo("=" * 40)
|
||||
click.echo(f"📊 Total Supply: {total_supply / 1e18:.2f} tokens")
|
||||
click.echo(f"📋 Total Proposals: {proposal_count}")
|
||||
click.echo(f"🗳️ Active Proposals: {len(active_proposals)}")
|
||||
click.echo(f"🪙 Governance Token: {token_address}")
|
||||
click.echo(f"🏛️ DAO Address: {dao_address}")
|
||||
|
||||
# Voting parameters
|
||||
voting_delay = dao_contract.functions.votingDelay().call()
|
||||
voting_period = dao_contract.functions.votingPeriod().call()
|
||||
quorum = dao_contract.functions.quorum(w3.eth.block_number).call()
|
||||
threshold = dao_contract.functions.proposalThreshold().call()
|
||||
|
||||
click.echo(f"\n⚙️ Voting Parameters:")
|
||||
click.echo(f" Delay: {voting_delay // 86400} days")
|
||||
click.echo(f" Period: {voting_period // 86400} days")
|
||||
click.echo(f" Quorum: {quorum / 1e18:.2f} tokens ({(quorum * 100 / total_supply):.2f}%)")
|
||||
click.echo(f" Threshold: {threshold / 1e18:.2f} tokens")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to get status: {str(e)}", err=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
dao()
|
||||
91
cli/commands/deployment.py
Normal file
91
cli/commands/deployment.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Production deployment guidance for AITBC CLI"""
|
||||
|
||||
import click
|
||||
from utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def deploy():
|
||||
"""Production deployment guidance and setup"""
|
||||
pass
|
||||
|
||||
@deploy.command()
|
||||
@click.option('--service', default='all', help='Service to deploy (all, coordinator, blockchain, marketplace)')
|
||||
@click.option('--environment', default='production', help='Deployment environment')
|
||||
def setup(service, environment):
|
||||
"""Get deployment setup instructions"""
|
||||
output(f"🚀 {environment.title()} Deployment Setup for {service.title()}", None)
|
||||
|
||||
instructions = {
|
||||
'coordinator': [
|
||||
"1. Install dependencies: pip install -r requirements.txt",
|
||||
"2. Set environment variables in .env file",
|
||||
"3. Run: python -m coordinator.main",
|
||||
"4. Configure nginx reverse proxy",
|
||||
"5. Set up SSL certificates"
|
||||
],
|
||||
'blockchain': [
|
||||
"1. Install blockchain node dependencies",
|
||||
"2. Initialize genesis block: aitbc genesis init",
|
||||
"3. Start node: python -m blockchain.node",
|
||||
"4. Configure peer connections",
|
||||
"5. Enable mining if needed"
|
||||
],
|
||||
'marketplace': [
|
||||
"1. Install marketplace dependencies",
|
||||
"2. Set up database: postgresql-setup.sh",
|
||||
"3. Run migrations: python -m marketplace.migrate",
|
||||
"4. Start service: python -m marketplace.main",
|
||||
"5. Configure GPU mining nodes"
|
||||
],
|
||||
'all': [
|
||||
"📋 Complete AITBC Platform Deployment:",
|
||||
"",
|
||||
"1. Prerequisites:",
|
||||
" - Python 3.13+",
|
||||
" - PostgreSQL 14+",
|
||||
" - Redis 6+",
|
||||
" - Docker (optional)",
|
||||
"",
|
||||
"2. Environment Setup:",
|
||||
" - Copy .env.example to .env",
|
||||
" - Configure database URLs",
|
||||
" - Set API keys and secrets",
|
||||
"",
|
||||
"3. Database Setup:",
|
||||
" - createdb aitbc",
|
||||
" - Run migrations: python manage.py migrate",
|
||||
"",
|
||||
"4. Service Deployment:",
|
||||
" - Coordinator: python -m coordinator.main",
|
||||
" - Blockchain: python -m blockchain.node",
|
||||
" - Marketplace: python -m marketplace.main",
|
||||
"",
|
||||
"5. Frontend Setup:",
|
||||
" - npm install",
|
||||
" - npm run build",
|
||||
" - Configure web server"
|
||||
]
|
||||
}
|
||||
|
||||
for step in instructions.get(service, instructions['all']):
|
||||
output(step, None)
|
||||
|
||||
output(f"\n💡 For detailed deployment guides, see: docs/deployment/{environment}.md", None)
|
||||
|
||||
@deploy.command()
|
||||
@click.option('--service', help='Service to check')
|
||||
def status(service):
|
||||
"""Check deployment status"""
|
||||
output(f"📊 Deployment Status Check for {service or 'All Services'}", None)
|
||||
|
||||
checks = [
|
||||
"Coordinator API: http://localhost:8000/health",
|
||||
"Blockchain Node: http://localhost:8006/status",
|
||||
"Marketplace: http://localhost:8014/health",
|
||||
"Wallet Service: http://localhost:8002/status"
|
||||
]
|
||||
|
||||
for check in checks:
|
||||
output(f" • {check}", None)
|
||||
|
||||
output("\n💡 Use curl or browser to check each endpoint", None)
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from aitbc_cli.imports import ensure_coordinator_api_imports
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
|
||||
ensure_coordinator_api_imports()
|
||||
|
||||
@@ -7,8 +7,8 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from ..utils import output, error, success, warning
|
||||
from ..config import get_config
|
||||
from utils import output, error, success, warning
|
||||
from config import get_config
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -4,7 +4,7 @@ import click
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Optional, List
|
||||
from ..utils import output, error
|
||||
from utils import output, error
|
||||
|
||||
|
||||
def _get_explorer_endpoint(ctx):
|
||||
@@ -326,21 +326,16 @@ def export(ctx, export_format: str, export_type: str, chain_id: str):
|
||||
|
||||
|
||||
@explorer.command()
|
||||
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
|
||||
@click.option('--open', is_flag=True, help='Open explorer in web browser')
|
||||
@click.option('--chain-id', default='main', help='Chain ID to explore')
|
||||
@click.pass_context
|
||||
def web(ctx, chain_id: str, open: bool):
|
||||
"""Open blockchain explorer in web browser"""
|
||||
def web(ctx, chain_id: str):
|
||||
"""Get blockchain explorer web URL"""
|
||||
try:
|
||||
explorer_url = _get_explorer_endpoint(ctx)
|
||||
web_url = explorer_url.replace('http://', 'http://') # Ensure proper format
|
||||
|
||||
if open:
|
||||
import webbrowser
|
||||
webbrowser.open(web_url)
|
||||
output(f"Opening explorer in web browser: {web_url}", ctx.obj['output_format'])
|
||||
else:
|
||||
output(f"Explorer web interface: {web_url}", ctx.obj['output_format'])
|
||||
output(f"Explorer web interface: {web_url}", ctx.obj['output_format'])
|
||||
output("Use the URL above to access the explorer in your browser", ctx.obj['output_format'])
|
||||
|
||||
except Exception as e:
|
||||
error(f"Failed to open web interface: {str(e)}")
|
||||
error(f"Failed to get explorer URL: {e}", ctx.obj['output_format'])
|
||||
@@ -5,11 +5,11 @@ import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from ..core.genesis_generator import GenesisGenerator, GenesisValidationError
|
||||
from ..core.config import MultiChainConfig, load_multichain_config
|
||||
from ..models.chain import GenesisConfig
|
||||
from ..utils import output, error, success
|
||||
from .keystore import create_keystore_via_script
|
||||
from core.genesis_generator import GenesisGenerator, GenesisValidationError
|
||||
from core.config import MultiChainConfig, load_multichain_config
|
||||
from models.chain import GenesisConfig
|
||||
from utils import output, error, success
|
||||
from commands.keystore import create_keystore_via_script
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -141,7 +141,7 @@ def validate(ctx, genesis_file):
|
||||
with open(genesis_path, 'r') as f:
|
||||
genesis_data = json.load(f)
|
||||
|
||||
from ..models.chain import GenesisBlock
|
||||
from models.chain import GenesisBlock
|
||||
genesis_block = GenesisBlock(**genesis_data)
|
||||
|
||||
# Validate genesis block
|
||||
@@ -6,7 +6,7 @@ import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -59,7 +59,7 @@ def status(agent_id, test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -557,7 +557,7 @@ def deployment_status(deployment_id, test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -8,7 +8,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
|
||||
GOVERNANCE_DIR = Path.home() / ".aitbc" / "governance"
|
||||
@@ -7,7 +7,7 @@ import httpx
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -5,7 +5,7 @@ import httpx
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Optional, List, Dict, Any
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
import os
|
||||
|
||||
|
||||
@@ -37,55 +37,32 @@ def register(ctx, name: Optional[str], memory: Optional[int], cuda_cores: Option
|
||||
"""Register GPU on marketplace (auto-detects hardware)"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
# Auto-detect GPU hardware
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
|
||||
capture_output=True, text=True, check=True)
|
||||
# Note: GPU hardware detection should be done by separate system monitoring tools
|
||||
# CLI provides guidance for manual hardware specification
|
||||
if not name or memory is None:
|
||||
output("💡 To auto-detect GPU hardware, use system monitoring tools:", ctx.obj['output_format'])
|
||||
output(" nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits", ctx.obj['output_format'])
|
||||
output(" Or specify --name and --memory manually", ctx.obj['output_format'])
|
||||
|
||||
if result.returncode == 0:
|
||||
gpu_info = result.stdout.strip().split(', ')
|
||||
detected_name = gpu_info[0].strip()
|
||||
detected_memory = int(gpu_info[1].strip())
|
||||
|
||||
# Use detected values if not provided
|
||||
if not name:
|
||||
name = detected_name
|
||||
if memory is None:
|
||||
memory = detected_memory
|
||||
|
||||
# Validate provided specs against detected hardware
|
||||
if not force:
|
||||
if name and name != detected_name:
|
||||
error(f"GPU name mismatch! Detected: '{detected_name}', Provided: '{name}'. Use --force to override.")
|
||||
return
|
||||
if memory and memory != detected_memory:
|
||||
error(f"GPU memory mismatch! Detected: {detected_memory}GB, Provided: {memory}GB. Use --force to override.")
|
||||
return
|
||||
|
||||
success(f"Auto-detected GPU: {detected_name} with {detected_memory}GB memory")
|
||||
else:
|
||||
if not force:
|
||||
error("Failed to detect GPU hardware. Use --force to register without hardware validation.")
|
||||
return
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
if not force:
|
||||
error("nvidia-smi not available. Use --force to register without hardware validation.")
|
||||
if not name and not memory:
|
||||
error("GPU name and memory must be specified for registration", ctx.obj['output_format'])
|
||||
return
|
||||
|
||||
# Build GPU specs
|
||||
if not force:
|
||||
output("⚠️ Hardware validation skipped. Use --force to register without hardware validation.",
|
||||
ctx.obj['output_format'])
|
||||
|
||||
# Build GPU specs for registration
|
||||
gpu_specs = {
|
||||
"name": name,
|
||||
"memory_gb": memory,
|
||||
"cuda_cores": cuda_cores,
|
||||
"compute_capability": compute_capability,
|
||||
"price_per_hour": price_per_hour,
|
||||
"description": description
|
||||
"description": description,
|
||||
"miner_id": miner_id or config.api_key[:8], # Use auth key as miner ID if not provided
|
||||
"registered_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Remove None values
|
||||
gpu_specs = {k: v for k, v in gpu_specs.items() if v is not None}
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import base64
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -6,12 +6,12 @@ import json
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from ..core.config import load_multichain_config
|
||||
from ..core.marketplace import (
|
||||
from core.config import load_multichain_config
|
||||
from core.marketplace import (
|
||||
GlobalChainMarketplace, ChainType, MarketplaceStatus,
|
||||
TransactionStatus
|
||||
)
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def marketplace():
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import time
|
||||
import concurrent.futures
|
||||
from typing import Optional, Dict, Any, List
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True)
|
||||
@@ -7,7 +7,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success, console
|
||||
from utils import output, error, success, console
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -53,7 +53,7 @@ def status(test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -7,7 +7,7 @@ import base64
|
||||
import mimetypes
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -7,7 +7,7 @@ import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import click
|
||||
from typing import Optional
|
||||
from ..core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config
|
||||
from ..core.node_client import NodeClient
|
||||
from ..utils import output, error, success
|
||||
from core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config
|
||||
from core.node_client import NodeClient
|
||||
from utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def node():
|
||||
@@ -192,7 +192,7 @@ def add(ctx, node_id, endpoint, timeout, max_connections, retry_count):
|
||||
|
||||
config = add_node_config(config, node_config)
|
||||
|
||||
from ..core.config import save_multichain_config
|
||||
from core.config import save_multichain_config
|
||||
save_multichain_config(config)
|
||||
|
||||
success(f"Node {node_id} added successfully!")
|
||||
@@ -241,7 +241,7 @@ def remove(ctx, node_id, force):
|
||||
|
||||
config = remove_node_config(config, node_id)
|
||||
|
||||
from ..core.config import save_multichain_config
|
||||
from core.config import save_multichain_config
|
||||
save_multichain_config(config)
|
||||
|
||||
success(f"Node {node_id} removed successfully!")
|
||||
@@ -5,7 +5,7 @@ import httpx
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any, List
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -5,7 +5,7 @@ import httpx
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any, List
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -59,7 +59,7 @@ def dashboard(plugin_id, days, test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -565,7 +565,7 @@ def download(plugin_id, license_key, test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -489,7 +489,7 @@ def status(test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -85,7 +85,7 @@ def status(test_mode):
|
||||
def get_config():
|
||||
"""Get CLI configuration"""
|
||||
try:
|
||||
from .config import get_config
|
||||
from config import get_config
|
||||
return get_config()
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from aitbc_cli.imports import ensure_coordinator_api_imports
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
|
||||
ensure_coordinator_api_imports()
|
||||
|
||||
@@ -6,7 +6,7 @@ import time
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from ..utils import output, error, success
|
||||
from utils import output, error, success
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from aitbc_cli.imports import ensure_coordinator_api_imports
|
||||
from core.imports import ensure_coordinator_api_imports
|
||||
|
||||
ensure_coordinator_api_imports()
|
||||
|
||||
@@ -4,7 +4,7 @@ import click
|
||||
import httpx
|
||||
import json
|
||||
from typing import Optional, Dict, Any, List
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from ..utils import success, error, run_subprocess
|
||||
from utils import success, error, run_subprocess
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -11,8 +11,8 @@ from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from ..utils import output, success, error, warning
|
||||
from ..config import get_config
|
||||
from utils import output, success, error, warning
|
||||
from config import get_config
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -118,7 +118,7 @@ def api(ctx, endpoint, method, data):
|
||||
@click.pass_context
|
||||
def wallet(ctx, wallet_name, test_operations):
|
||||
"""Test wallet functionality"""
|
||||
from ..commands.wallet import wallet as wallet_cmd
|
||||
from commands.wallet import wallet as wallet_cmd
|
||||
|
||||
output(f"Testing wallet functionality with wallet: {wallet_name}")
|
||||
|
||||
@@ -164,7 +164,7 @@ def wallet(ctx, wallet_name, test_operations):
|
||||
@click.pass_context
|
||||
def job(ctx, job_type, test_data):
|
||||
"""Test job submission and management"""
|
||||
from ..commands.client import client as client_cmd
|
||||
from commands.client import client as client_cmd
|
||||
|
||||
output(f"Testing job submission with type: {job_type}")
|
||||
|
||||
@@ -220,7 +220,7 @@ def job(ctx, job_type, test_data):
|
||||
@click.pass_context
|
||||
def marketplace(ctx, gpu_type, price):
|
||||
"""Test marketplace functionality"""
|
||||
from ..commands.marketplace import marketplace as marketplace_cmd
|
||||
from commands.marketplace import marketplace as marketplace_cmd
|
||||
|
||||
output(f"Testing marketplace functionality for {gpu_type} at {price} AITBC/hour")
|
||||
|
||||
@@ -252,7 +252,7 @@ def marketplace(ctx, gpu_type, price):
|
||||
@click.pass_context
|
||||
def blockchain(ctx, test_endpoints):
|
||||
"""Test blockchain functionality"""
|
||||
from ..commands.blockchain import blockchain as blockchain_cmd
|
||||
from commands.blockchain import blockchain as blockchain_cmd
|
||||
|
||||
output("Testing blockchain functionality")
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success, warning
|
||||
from utils import output, error, success, warning
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -9,7 +9,7 @@ import yaml
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils import output, error, success, encrypt_value, decrypt_value
|
||||
from utils import output, error, success, encrypt_value, decrypt_value
|
||||
import getpass
|
||||
|
||||
|
||||
@@ -124,8 +124,8 @@ def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daem
|
||||
ctx.obj["use_daemon"] = use_daemon
|
||||
|
||||
# Initialize dual-mode adapter
|
||||
from ..config import get_config
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from config import get_config
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
|
||||
config = get_config()
|
||||
adapter = DualModeWalletAdapter(config, use_daemon=use_daemon)
|
||||
@@ -188,8 +188,8 @@ def create(ctx, name: str, wallet_type: str, no_encrypt: bool):
|
||||
if use_daemon and not adapter.is_daemon_available():
|
||||
error("Wallet daemon is not available. Falling back to file-based wallet.")
|
||||
# Switch to file mode
|
||||
from ..config import get_config
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from config import get_config
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
config = get_config()
|
||||
adapter = DualModeWalletAdapter(config, use_daemon=False)
|
||||
ctx.obj["wallet_adapter"] = adapter
|
||||
@@ -254,8 +254,8 @@ def list(ctx):
|
||||
if use_daemon and not adapter.is_daemon_available():
|
||||
error("Wallet daemon is not available. Falling back to file-based wallet listing.")
|
||||
# Switch to file mode
|
||||
from ..config import get_config
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from config import get_config
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
config = get_config()
|
||||
adapter = DualModeWalletAdapter(config, use_daemon=False)
|
||||
|
||||
@@ -306,8 +306,8 @@ def switch(ctx, name: str):
|
||||
if use_daemon and not adapter.is_daemon_available():
|
||||
error("Wallet daemon is not available. Falling back to file-based wallet switching.")
|
||||
# Switch to file mode
|
||||
from ..config import get_config
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from config import get_config
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
config = get_config()
|
||||
adapter = DualModeWalletAdapter(config, use_daemon=False)
|
||||
|
||||
@@ -846,8 +846,8 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
|
||||
if use_daemon and not adapter.is_daemon_available():
|
||||
error("Wallet daemon is not available. Falling back to file-based wallet send.")
|
||||
# Switch to file mode
|
||||
from ..config import get_config
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from config import get_config
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
config = get_config()
|
||||
adapter = DualModeWalletAdapter(config, use_daemon=False)
|
||||
ctx.obj["wallet_adapter"] = adapter
|
||||
@@ -882,8 +882,8 @@ def balance(ctx):
|
||||
if use_daemon and not adapter.is_daemon_available():
|
||||
error("Wallet daemon is not available. Falling back to file-based wallet balance.")
|
||||
# Switch to file mode
|
||||
from ..config import get_config
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from config import get_config
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
config = get_config()
|
||||
adapter = DualModeWalletAdapter(config, use_daemon=False)
|
||||
ctx.obj["wallet_adapter"] = adapter
|
||||
@@ -919,8 +919,8 @@ def daemon():
|
||||
@click.pass_context
|
||||
def status(ctx):
|
||||
"""Check wallet daemon status"""
|
||||
from ..config import get_config
|
||||
from ..wallet_daemon_client import WalletDaemonClient
|
||||
from config import get_config
|
||||
from wallet_daemon_client import WalletDaemonClient
|
||||
|
||||
config = get_config()
|
||||
client = WalletDaemonClient(config)
|
||||
@@ -942,7 +942,7 @@ def status(ctx):
|
||||
@click.pass_context
|
||||
def configure(ctx):
|
||||
"""Configure wallet daemon settings"""
|
||||
from ..config import get_config
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
|
||||
@@ -961,8 +961,8 @@ def configure(ctx):
|
||||
@click.pass_context
|
||||
def migrate_to_daemon(ctx, wallet_name: str, password: Optional[str], new_password: Optional[str], force: bool):
|
||||
"""Migrate a file-based wallet to daemon storage"""
|
||||
from ..wallet_migration_service import WalletMigrationService
|
||||
from ..config import get_config
|
||||
from wallet_migration_service import WalletMigrationService
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
migration_service = WalletMigrationService(config)
|
||||
@@ -988,8 +988,8 @@ def migrate_to_daemon(ctx, wallet_name: str, password: Optional[str], new_passwo
|
||||
@click.pass_context
|
||||
def migrate_to_file(ctx, wallet_name: str, password: Optional[str], new_password: Optional[str], force: bool):
|
||||
"""Migrate a daemon-based wallet to file storage"""
|
||||
from ..wallet_migration_service import WalletMigrationService
|
||||
from ..config import get_config
|
||||
from wallet_migration_service import WalletMigrationService
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
migration_service = WalletMigrationService(config)
|
||||
@@ -1011,8 +1011,8 @@ def migrate_to_file(ctx, wallet_name: str, password: Optional[str], new_password
|
||||
@click.pass_context
|
||||
def migration_status(ctx):
|
||||
"""Show wallet migration status"""
|
||||
from ..wallet_migration_service import WalletMigrationService
|
||||
from ..config import get_config
|
||||
from wallet_migration_service import WalletMigrationService
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
migration_service = WalletMigrationService(config)
|
||||
@@ -1233,11 +1233,18 @@ def unstake(ctx, amount: float):
|
||||
}
|
||||
)
|
||||
|
||||
# Save wallet with encryption
|
||||
password = None
|
||||
# CRITICAL SECURITY FIX: Save wallet properly to avoid double-encryption
|
||||
if wallet_data.get("encrypted"):
|
||||
# For encrypted wallets, we need to re-encrypt the private key before saving
|
||||
password = _get_wallet_password(wallet_name)
|
||||
_save_wallet(wallet_path, wallet_data, password)
|
||||
# Only encrypt the private key, not the entire wallet data
|
||||
if "private_key" in wallet_data:
|
||||
wallet_data["private_key"] = encrypt_value(wallet_data["private_key"], password)
|
||||
# Save without passing password to avoid double-encryption
|
||||
_save_wallet(wallet_path, wallet_data, None)
|
||||
else:
|
||||
# For unencrypted wallets, save normally
|
||||
_save_wallet(wallet_path, wallet_data, None)
|
||||
|
||||
success(f"Unstaked {amount} AITBC")
|
||||
output(
|
||||
@@ -1445,7 +1452,7 @@ def multisig_challenge(ctx, wallet_name: str, tx_id: str):
|
||||
return
|
||||
|
||||
# Import crypto utilities
|
||||
from ..utils.crypto_utils import multisig_security
|
||||
from utils.crypto_utils import multisig_security
|
||||
|
||||
try:
|
||||
# Create signing request
|
||||
@@ -1474,7 +1481,7 @@ def multisig_challenge(ctx, wallet_name: str, tx_id: str):
|
||||
@click.pass_context
|
||||
def sign_challenge(ctx, challenge: str, private_key: str):
|
||||
"""Sign a cryptographic challenge (for testing multisig)"""
|
||||
from ..utils.crypto_utils import sign_challenge
|
||||
from utils.crypto_utils import sign_challenge
|
||||
|
||||
try:
|
||||
signature = sign_challenge(challenge, private_key)
|
||||
@@ -1513,7 +1520,7 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str, signature: str
|
||||
return
|
||||
|
||||
# Import crypto utilities
|
||||
from ..utils.crypto_utils import multisig_security
|
||||
from utils.crypto_utils import multisig_security
|
||||
|
||||
# Verify signature cryptographically
|
||||
success, message = multisig_security.verify_and_add_signature(tx_id, signature, signer)
|
||||
@@ -2105,7 +2112,7 @@ def multisig_create(ctx, threshold: int, signers: tuple, wallet_name: Optional[s
|
||||
try:
|
||||
if ctx.obj.get("use_daemon"):
|
||||
# Use wallet daemon for multi-sig creation
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
adapter = DualModeWalletAdapter(config)
|
||||
|
||||
result = adapter.create_multisig_wallet(
|
||||
@@ -2163,7 +2170,7 @@ def set_limit(ctx, amount: float, period: str, wallet_name: Optional[str]):
|
||||
try:
|
||||
if ctx.obj.get("use_daemon"):
|
||||
# Use wallet daemon
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
adapter = DualModeWalletAdapter(config)
|
||||
|
||||
result = adapter.set_transfer_limit(
|
||||
@@ -2225,7 +2232,7 @@ def time_lock(ctx, amount: float, duration: int, recipient: str, wallet_name: Op
|
||||
try:
|
||||
if ctx.obj.get("use_daemon"):
|
||||
# Use wallet daemon
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
adapter = DualModeWalletAdapter(config)
|
||||
|
||||
result = adapter.create_time_lock(
|
||||
@@ -2345,7 +2352,7 @@ def audit_trail(ctx, wallet_name: Optional[str], days: int):
|
||||
try:
|
||||
if ctx.obj.get("use_daemon"):
|
||||
# Use wallet daemon for audit
|
||||
from ..dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
from utils.dual_mode_wallet_adapter import DualModeWalletAdapter
|
||||
adapter = DualModeWalletAdapter(config)
|
||||
|
||||
result = adapter.get_audit_trail(
|
||||
@@ -13,8 +13,8 @@ from enum import Enum
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
from ..core.config import MultiChainConfig
|
||||
from ..core.node_client import NodeClient
|
||||
from core.config import MultiChainConfig
|
||||
from core.node_client import NodeClient
|
||||
|
||||
class MessageType(Enum):
|
||||
"""Agent message types"""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user