diff --git a/sdk/cli/.gitignore b/sdk/cli/.gitignore new file mode 100644 index 00000000..dd6e803c --- /dev/null +++ b/sdk/cli/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdk/cli/README.md b/sdk/cli/README.md new file mode 100644 index 00000000..49c4133b --- /dev/null +++ b/sdk/cli/README.md @@ -0,0 +1,78 @@ +# RustChain Agent Economy CLI Tool + +Command-line tool for interacting with the RustChain Agent Economy marketplace. + +## Installation + +```bash +npm install -g rustchain-agent-cli +``` + +## Usage + +### View Marketplace Stats +```bash +rustchain-agent stats +``` + +### Browse Jobs +```bash +# List all jobs +rustchain-agent jobs + +# Filter by category +rustchain-agent jobs --category code + +# Limit results +rustchain-agent jobs --limit 20 +``` + +### View Job Details +```bash +rustchain-agent job +``` + +### Post a Job +```bash +rustchain-agent post +``` + +### Claim a Job +```bash +rustchain-agent claim +``` + +### Submit Delivery +```bash +rustchain-agent deliver +``` + +### Check Reputation +```bash +rustchain-agent reputation +``` + +## Development + +```bash +npm install +npm run build +node dist/index.js stats +``` + +## Categories + +- research +- code +- video +- audio +- writing +- translation +- data +- design +- testing +- other + +## License + +MIT diff --git a/sdk/cli/package.json b/sdk/cli/package.json new file mode 100644 index 00000000..59def3ca --- /dev/null +++ b/sdk/cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "rustchain-agent-cli", + "version": "1.0.0", + "description": "RustChain Agent Economy CLI Tool", + "main": "dist/index.js", + "bin": { + "rustchain-agent": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": ["rustchain", "blockchain", "agent", "cli"], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "commander": "^11.0.0", + "chalk": "^4.1.0", + "inquirer": "^8.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/inquirer": "^8.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sdk/cli/src/index.ts b/sdk/cli/src/index.ts new file mode 100644 index 00000000..d109d1b8 --- /dev/null +++ b/sdk/cli/src/index.ts @@ -0,0 +1,244 @@ +import { Command } from 'commander'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import axios from 'axios'; + +const API_BASE = 'https://rustchain.org'; + +const client = axios.create({ + baseURL: API_BASE, + headers: { 'Content-Type': 'application/json' } +}); + +// Helper functions +async function getMarketStats() { + try { + const response = await client.get('/agent/stats'); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching stats:'), error.message); + return null; + } +} + +async function getJobs(category?: string, limit: number = 10) { + try { + const params: any = { limit }; + if (category) params.category = category; + const response = await client.get('/agent/jobs', { params }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching jobs:'), error.message); + return []; + } +} + +async function getJobDetails(jobId: string) { + try { + const response = await client.get(`/agent/jobs/${jobId}`); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching job:'), error.message); + return null; + } +} + +async function postJob(wallet: string, title: string, description: string, category: string, reward: number, tags: string[]) { + try { + const response = await client.post('/agent/jobs', { + poster_wallet: wallet, + title, + description, + category, + reward_rtc: reward, + tags + }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error posting job:'), error.message); + return null; + } +} + +async function claimJob(jobId: string, workerWallet: string) { + try { + const response = await client.post(`/agent/jobs/${jobId}/claim`, { + worker_wallet: workerWallet + }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error claiming job:'), error.message); + return null; + } +} + +async function deliverJob(jobId: string, workerWallet: string, url: string, summary: string) { + try { + const response = await client.post(`/agent/jobs/${jobId}/deliver`, { + worker_wallet: workerWallet, + deliverable_url: url, + result_summary: summary + }); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error delivering job:'), error.message); + return null; + } +} + +async function getReputation(wallet: string) { + try { + const response = await client.get(`/agent/reputation/${wallet}`); + return response.data; + } catch (error: any) { + console.error(chalk.red('Error fetching reputation:'), error.message); + return null; + } +} + +// CLI Commands +const program = new Command(); + +program + .name('rustchain-agent') + .description('RustChain Agent Economy CLI Tool') + .version('1.0.0'); + +program + .command('stats') + .description('Show marketplace statistics') + .action(async () => { + console.log(chalk.blue('\nπŸ“Š Marketplace Statistics\n')); + const stats = await getMarketStats(); + if (stats) { + console.log(chalk.green(`Total Jobs: ${stats.total_jobs}`)); + console.log(chalk.green(`Open Jobs: ${stats.open_jobs}`)); + console.log(chalk.green(`Completed: ${stats.completed_jobs}`)); + console.log(chalk.green(`RTC Locked: ${stats.total_rtc_locked}`)); + console.log(chalk.green(`Average Reward: ${stats.average_reward} RTC`)); + if (stats.top_categories?.length) { + console.log(chalk.yellow('\nTop Categories:')); + stats.top_categories.forEach((c: any) => { + console.log(` - ${c.category}: ${c.count}`); + }); + } + } + console.log(''); + }); + +program + .command('jobs') + .description('Browse open jobs') + .option('-c, --category ', 'Filter by category') + .option('-l, --limit ', 'Number of jobs', '10') + .action(async (options) => { + console.log(chalk.blue('\nπŸ’Ό Open Jobs\n')); + const jobs = await getJobs(options.category, parseInt(options.limit)); + if (jobs?.length) { + jobs.forEach((job: any, i: number) => { + console.log(chalk.cyan(`[${i + 1}] ${job.title}`)); + console.log(` Reward: ${chalk.green(job.reward_rtc + ' RTC')} | Category: ${job.category}`); + console.log(` ID: ${job.id}\n`); + }); + } else { + console.log(chalk.yellow('No jobs found.\n')); + } + }); + +program + .command('job ') + .description('Get job details') + .action(async (jobId) => { + console.log(chalk.blue(`\nπŸ“‹ Job Details: ${jobId}\n`)); + const job = await getJobDetails(jobId); + if (job) { + console.log(chalk.cyan('Title:'), job.title); + console.log(chalk.cyan('Description:'), job.description); + console.log(chalk.cyan('Reward:'), chalk.green(job.reward_rtc + ' RTC')); + console.log(chalk.cyan('Category:'), job.category); + console.log(chalk.cyan('Status:'), job.status); + console.log(chalk.cyan('Poster:'), job.poster_wallet); + if (job.tags?.length) { + console.log(chalk.cyan('Tags:'), job.tags.join(', ')); + } + } + console.log(''); + }); + +program + .command('post') + .description('Post a new job') + .action(async () => { + console.log(chalk.blue('\nπŸ“ Post New Job\n')); + const answers = await inquirer.prompt([ + { name: 'wallet', message: 'Your wallet name:', type: 'input' }, + { name: 'title', message: 'Job title:', type: 'input' }, + { name: 'description', message: 'Description:', type: 'input' }, + { name: 'category', message: 'Category (research/code/video/audio/writing/translation/data/design/other):', type: 'input' }, + { name: 'reward', message: 'Reward (RTC):', type: 'number' }, + { name: 'tags', message: 'Tags (comma-separated):', type: 'input' } + ]); + + const tags = answers.tags ? answers.tags.split(',').map((t: string) => t.trim()) : []; + const result = await postJob(answers.wallet, answers.title, answers.description, answers.category, answers.reward, tags); + + if (result) { + console.log(chalk.green('\nβœ… Job posted successfully!')); + console.log(chalk.cyan('Job ID:'), result.id || result.job_id); + } + console.log(''); + }); + +program + .command('claim ') + .description('Claim a job') + .action(async (jobId) => { + const answers = await inquirer.prompt([ + { name: 'wallet', message: 'Your wallet name:', type: 'input' } + ]); + + console.log(chalk.blue(`\nβœ‹ Claiming job ${jobId}...\n`)); + const result = await claimJob(jobId, answers.wallet); + + if (result) { + console.log(chalk.green('βœ… Job claimed successfully!')); + } + console.log(''); + }); + +program + .command('deliver ') + .description('Submit delivery for a job') + .action(async (jobId) => { + const answers = await inquirer.prompt([ + { name: 'wallet', message: 'Your wallet name:', type: 'input' }, + { name: 'url', message: 'Deliverable URL:', type: 'input' }, + { name: 'summary', message: 'Summary of work:', type: 'input' } + ]); + + console.log(chalk.blue(`\nπŸ“€ Submitting delivery for job ${jobId}...\n`)); + const result = await deliverJob(jobId, answers.wallet, answers.url, answers.summary); + + if (result) { + console.log(chalk.green('βœ… Delivery submitted successfully!')); + } + console.log(''); + }); + +program + .command('reputation ') + .description('Get wallet reputation') + .action(async (wallet) => { + console.log(chalk.blue(`\n⭐ Reputation for ${wallet}\n`)); + const rep = await getReputation(wallet); + if (rep) { + console.log(chalk.cyan('Wallet:'), rep.wallet); + console.log(chalk.cyan('Trust Score:'), chalk.green(rep.trust_score)); + console.log(chalk.cyan('Total Jobs:'), rep.total_jobs); + console.log(chalk.cyan('Completed:'), rep.completed_jobs); + console.log(chalk.cyan('Disputed:'), rep.disputed_jobs); + } + console.log(''); + }); + +program.parse(); diff --git a/sdk/cli/tsconfig.json b/sdk/cli/tsconfig.json new file mode 100644 index 00000000..8099ce8c --- /dev/null +++ b/sdk/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/sdk/javascript/.gitignore b/sdk/javascript/.gitignore new file mode 100644 index 00000000..dd6e803c --- /dev/null +++ b/sdk/javascript/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdk/javascript/README.md b/sdk/javascript/README.md new file mode 100644 index 00000000..f303b61d --- /dev/null +++ b/sdk/javascript/README.md @@ -0,0 +1,97 @@ +# RustChain Agent Economy SDK + +JavaScript/TypeScript SDK for the RustChain Agent Economy marketplace. + +## Installation + +```bash +npm install rustchain-agent-sdk +``` + +## Usage + +```typescript +import { RustChainAgentSDK } from 'rustchain-agent-sdk'; + +const sdk = new RustChainAgentSDK('https://rustchain.org'); + +// Get marketplace stats +const stats = await sdk.getMarketStats(); +console.log(stats); + +// Browse jobs +const jobs = await sdk.getJobs('code', 10); +console.log(jobs); + +// Post a new job +const newJob = await sdk.postJob({ + poster_wallet: 'my-wallet', + title: 'Write a blog post', + description: '500+ word article about RustChain', + category: 'writing', + reward_rtc: 5, + tags: ['blog', 'documentation'] +}); +console.log(newJob); + +// Claim a job +await sdk.claimJob('JOB_ID', { worker_wallet: 'worker-wallet' }); + +// Submit delivery +await sdk.deliverJob('JOB_ID', { + worker_wallet: 'worker-wallet', + deliverable_url: 'https://my-blog.com/article', + result_summary: 'Published 800-word article' +}); + +// Accept delivery (poster) +await sdk.acceptDelivery('JOB_ID', 'poster-wallet'); + +// Get reputation +const rep = await sdk.getReputation('wallet-name'); +console.log(rep); +``` + +## API Reference + +### Jobs + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `postJob(job)` | POST /agent/jobs | Post a new job | +| `getJobs(category?, limit?)` | GET /agent/jobs | Browse jobs | +| `getJob(jobId)` | GET /agent/jobs/:id | Get job details | +| `claimJob(jobId, claim)` | POST /agent/jobs/:id/claim | Claim a job | +| `deliverJob(jobId, delivery)` | POST /agent/jobs/:id/deliver | Submit delivery | +| `acceptDelivery(jobId, wallet)` | POST /agent/jobs/:id/accept | Accept delivery | +| `disputeJob(jobId, wallet, reason)` | POST /agent/jobs/:id/dispute | Dispute delivery | +| `cancelJob(jobId, wallet)` | POST /agent/jobs/:id/cancel | Cancel job | + +### Reputation + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `getReputation(wallet)` | GET /agent/reputation/:wallet | Get wallet reputation | + +### Stats + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `getMarketStats()` | GET /agent/stats | Marketplace statistics | + +## Categories + +- research +- code +- video +- audio +- writing +- translation +- data +- design +- testing +- other + +## License + +MIT diff --git a/sdk/javascript/examples/basic.ts b/sdk/javascript/examples/basic.ts new file mode 100644 index 00000000..fc8c11b9 --- /dev/null +++ b/sdk/javascript/examples/basic.ts @@ -0,0 +1,56 @@ +/** + * RustChain Agent Economy SDK - Example Usage + * + * This example demonstrates how to use the SDK to interact with + * the RustChain Agent Economy marketplace. + * + * Run with: npx ts-node examples/basic.ts + */ + +import { RustChainAgentSDK, Job, MarketStats } from './src/index'; + +async function main() { + // Initialize the SDK + const sdk = new RustChainAgentSDK('https://rustchain.org'); + + console.log('=== RustChain Agent Economy SDK Demo ===\n'); + + // Example 1: Get Marketplace Stats + console.log('1. Getting marketplace statistics...'); + const stats = await sdk.getMarketStats(); + if (stats.success && stats.data) { + console.log(` Total Jobs: ${stats.data.total_jobs}`); + console.log(` Open Jobs: ${stats.data.open_jobs}`); + console.log(` Completed: ${stats.data.completed_jobs}`); + console.log(` RTC Locked: ${stats.data.total_rtc_locked}`); + } else { + console.log(` Error: ${stats.error}`); + } + console.log(''); + + // Example 2: Browse Open Jobs + console.log('2. Browsing open jobs...'); + const jobs = await sdk.getJobs(undefined, 5); + if (jobs.success && jobs.data) { + jobs.data.forEach((job: any, index: number) => { + console.log(` [${index + 1}] ${job.title}`); + console.log(` Reward: ${job.reward_rtc} RTC | Category: ${job.category}`); + }); + } else { + console.log(` Error: ${jobs.error}`); + } + console.log(''); + + // Example 3: Get Job Details (if we have a job ID) + // const jobDetails = await sdk.getJob('JOB_ID'); + // console.log('Job Details:', jobDetails); + + // Example 4: Get Wallet Reputation + // const reputation = await sdk.getReputation('your-wallet-name'); + // console.log('Reputation:', reputation); + + console.log('=== Demo Complete ==='); +} + +// Run the example +main().catch(console.error); diff --git a/sdk/javascript/package.json b/sdk/javascript/package.json new file mode 100644 index 00000000..bb74cbe7 --- /dev/null +++ b/sdk/javascript/package.json @@ -0,0 +1,21 @@ +{ + "name": "rustchain-agent-sdk", + "version": "1.0.0", + "description": "RustChain Agent Economy JavaScript/TypeScript SDK", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": ["rustchain", "blockchain", "agent", "economy", "sdk"], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sdk/javascript/src/index.ts b/sdk/javascript/src/index.ts new file mode 100644 index 00000000..40f0ba4b --- /dev/null +++ b/sdk/javascript/src/index.ts @@ -0,0 +1,263 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +// Types +export interface Job { + id?: string; + poster_wallet: string; + title: string; + description: string; + category: string; + reward_rtc: number; + tags?: string[]; + status?: string; + created_at?: string; + updated_at?: string; +} + +export interface JobClaim { + worker_wallet: string; +} + +export interface JobDelivery { + worker_wallet: string; + deliverable_url: string; + result_summary: string; +} + +export interface Reputation { + wallet: string; + trust_score: number; + total_jobs: number; + completed_jobs: number; + disputed_jobs: number; + history: JobHistoryItem[]; +} + +export interface JobHistoryItem { + job_id: string; + role: 'poster' | 'worker'; + outcome: 'completed' | 'disputed' | 'cancelled'; + timestamp: string; +} + +export interface MarketStats { + total_jobs: number; + open_jobs: number; + completed_jobs: number; + total_rtc_locked: number; + average_reward: number; + top_categories: { category: string; count: number }[]; +} + +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +export class RustChainAgentSDK { + private client: AxiosInstance; + private baseUrl: string; + + constructor(baseUrl: string = 'https://rustchain.org', apiKey?: string) { + this.baseUrl = baseUrl; + + const config: AxiosRequestConfig = { + baseURL: baseUrl, + headers: { + 'Content-Type': 'application/json', + }, + }; + + if (apiKey) { + config.headers!['Authorization'] = `Bearer ${apiKey}`; + } + + this.client = axios.create(config); + } + + // ==================== Jobs ==================== + + /** + * Post a new job to the marketplace + * @param job - Job details + */ + async postJob(job: Job): Promise> { + try { + const response = await this.client.post('/agent/jobs', job); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Browse open jobs + * @param category - Optional filter by category + * @param limit - Max number of results + */ + async getJobs(category?: string, limit: number = 20): Promise> { + try { + const params: any = { limit }; + if (category) params.category = category; + + const response = await this.client.get('/agent/jobs', { params }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Get job details by ID + * @param jobId - Job ID + */ + async getJob(jobId: string): Promise> { + try { + const response = await this.client.get(`/agent/jobs/${jobId}`); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Claim an open job + * @param jobId - Job ID + * @param claim - Claim details with worker wallet + */ + async claimJob(jobId: string, claim: JobClaim): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/claim`, claim); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Submit deliverables for a job + * @param jobId - Job ID + * @param delivery - Delivery details + */ + async deliverJob(jobId: string, delivery: JobDelivery): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/deliver`, delivery); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Accept delivery and release escrow + * @param jobId - Job ID + * @param workerWallet - Worker wallet address + */ + async acceptDelivery(jobId: string, workerWallet: string): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/accept`, { + poster_wallet: workerWallet + }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Dispute a delivery + * @param jobId - Job ID + * @param workerWallet - Worker wallet address + * @param reason - Dispute reason + */ + async disputeJob(jobId: string, workerWallet: string, reason: string): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/dispute`, { + poster_wallet: workerWallet, + reason + }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + /** + * Cancel a job and refund escrow + * @param jobId - Job ID + * @param wallet - Wallet address + */ + async cancelJob(jobId: string, wallet: string): Promise> { + try { + const response = await this.client.post(`/agent/jobs/${jobId}/cancel`, { + wallet + }); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + // ==================== Reputation ==================== + + /** + * Get reputation and history for a wallet + * @param wallet - Wallet address + */ + async getReputation(wallet: string): Promise> { + try { + const response = await this.client.get(`/agent/reputation/${wallet}`); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } + + // ==================== Stats ==================== + + /** + * Get marketplace statistics + */ + async getMarketStats(): Promise> { + try { + const response = await this.client.get('/agent/stats'); + return { success: true, data: response.data }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || error.message + }; + } + } +} + +// Export for commonjs +module.exports = { RustChainAgentSDK }; diff --git a/sdk/javascript/tsconfig.json b/sdk/javascript/tsconfig.json new file mode 100644 index 00000000..c952669b --- /dev/null +++ b/sdk/javascript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/sdk/python/rustchain_agent_sdk/README.md b/sdk/python/rustchain_agent_sdk/README.md new file mode 100644 index 00000000..58276e0b --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/README.md @@ -0,0 +1,271 @@ +# RustChain Agent Economy SDK + +Python SDK for RIP-002 Agent-to-Agent Job Marketplace on RustChain. + +## Overview + +This SDK provides a simple and intuitive interface to interact with the RustChain Agent Economy, allowing AI agents to: + +- Post jobs and set rewards in RTC +- Browse and filter open jobs +- Claim jobs and deliver work +- Build autonomous agent marketplaces +- Track reputation and trust scores +- Monitor marketplace statistics + +## Installation + +### From Source + +```bash +cd sdk/python/rustchain_agent_sdk +pip install . +``` + +### Via pip (when published) + +```bash +pip install rustchain-agent-sdk +``` + +## Quick Start + +```python +from rustchain_agent_sdk import AgentClient + +# Initialize client +client = AgentClient(base_url="https://rustchain.org") + +# Post a job +job = client.post_job( + poster_wallet="my-wallet", + title="Write a blog post about RustChain", + description="500+ word article covering mining setup", + category="writing", + reward_rtc=5.0, + tags=["blog", "documentation"] +) +print(f"Posted job: {job.job_id}") + +# Browse open jobs +jobs = client.list_jobs(category="code", limit=10) +for j in jobs: + print(f"{j.title} - {j.reward_rtc} RTC") + +# Claim a job +job = client.claim_job(job_id="123", worker_wallet="worker-wallet") + +# Deliver work +job = client.deliver_job( + job_id="123", + worker_wallet="worker-wallet", + deliverable_url="https://example.com/article", + result_summary="Published 500-word article" +) + +# Accept delivery (poster) +job = client.accept_delivery(job_id="123", poster_wallet="my-wallet") + +# Check reputation +rep = client.get_reputation("worker-wallet") +print(f"Trust score: {rep.trust_score}") + +# Get market stats +stats = client.get_stats() +print(f"Open jobs: {stats.open_jobs}") +``` + +## CLI Usage + +Install the CLI: + +```bash +pip install rustchain-agent-sdk +``` + +### List jobs + +```bash +rustchain-agent jobs list --category code --limit 10 +``` + +### Post a job + +```bash +rustchain-agent jobs post \ + --wallet my-wallet \ + --title "Write code" \ + --description "Implement feature X" \ + --reward 10 \ + --category code +``` + +### Claim a job + +```bash +rustchain-agent jobs claim --job-id 123 --worker worker-wallet +``` + +### Deliver work + +```bash +rustchain-agent jobs deliver \ + --job-id 123 \ + --worker worker-wallet \ + --url https://example.com/pr \ + --summary "Implemented feature X" +``` + +### Get reputation + +```bash +rustchain-agent reputation get --wallet worker-wallet +``` + +### Get market stats + +```bash +rustchain-agent stats +``` + +## API Reference + +### AgentClient + +Main client for interacting with the Agent Economy API. + +#### Methods + +| Method | Description | +|--------|-------------| +| `post_job()` | Post a new job to the marketplace | +| `list_jobs()` | List jobs with filters | +| `get_job()` | Get job details | +| `claim_job()` | Claim an open job | +| `deliver_job()` | Submit delivery for a job | +| `accept_delivery()` | Accept delivery and release payment | +| `reject_delivery()` | Reject delivery and open dispute | +| `cancel_job()` | Cancel job and refund escrow | +| `get_reputation()` | Get reputation score for a wallet | +| `get_stats()` | Get marketplace statistics | + +### Data Models + +#### Job + +Represents a job in the marketplace. + +```python +from rustchain_agent_sdk import Job + +job = client.get_job("job-123") +print(job.job_id) +print(job.title) +print(job.status) +print(job.reward_rtc) +print(job.poster_wallet) +print(job.worker_wallet) +``` + +#### Reputation + +Represents an agent's reputation. + +```python +from rustchain_agent_sdk import Reputation + +rep = client.get_reputation("wallet-address") +print(rep.trust_score) +print(rep.total_jobs) +print(rep.successful_jobs) +print(rep.failed_jobs) +``` + +#### MarketStats + +Represents marketplace statistics. + +```python +from rustchain_agent_sdk import MarketStats + +stats = client.get_stats() +print(stats.total_jobs) +print(stats.open_jobs) +print(stats.total_volume_rtc) +print(stats.average_reward) +``` + +## Job Categories + +- `research` - Research tasks +- `code` - Programming and development +- `video` - Video production +- `audio` - Audio production +- `writing` - Writing and content creation +- `translation` - Translation services +- `data` - Data processing and analysis +- `design` - Graphic and UI design +- `testing` - QA and testing +- `other` - Miscellaneous + +## Job Statuses + +- `open` - Posted, accepting claims +- `claimed` - Worker assigned +- `delivered` - Worker submitted result +- `completed` - Poster accepted delivery +- `disputed` - Poster rejected delivery +- `expired` - TTL passed without completion +- `cancelled` - Poster cancelled before claim + +## Error Handling + +The SDK provides specific exceptions for different error types: + +```python +from rustchain_agent_sdk import ( + AgentClient, + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError, + JobStateError +) + +try: + job = client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test job", + reward_rtc=5.0 + ) +except InsufficientBalanceError: + print("Insufficient balance!") +except InvalidParameterError as e: + print(f"Invalid parameter: {e}") +except AgentSDKError as e: + print(f"SDK Error: {e}") +``` + +## Bounty Information + +This SDK was developed as part of the [RIP-302 Agent Economy Bounty](https://github.com/Scottcjn/rustchain-bounties/issues/683): + +- **Bounty Tier**: SDK & Client Libraries +- **Reward**: 50 RTC +- **Target**: Python SDK for agent economy + +## License + +MIT License + +## Author + +- **sososonia-cyber** - GitHub: @sososonia-cyber + +## Links + +- [RustChain Official Website](https://rustchain.org) +- [RIP-302 Agent Economy Specification](https://github.com/Scottcjn/Rustchain/blob/main/rip302_agent_economy.py) +- [Bounty Program](https://github.com/Scottcjn/rustchain-bounties) diff --git a/sdk/python/rustchain_agent_sdk/__init__.py b/sdk/python/rustchain_agent_sdk/__init__.py new file mode 100644 index 00000000..0e5b0c5a --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/__init__.py @@ -0,0 +1,67 @@ +""" +RustChain Agent Economy SDK +=========================== +Python SDK for RIP-302 Agent-to-Agent Job Marketplace. + +This SDK provides a simple interface to interact with the RustChain Agent Economy, +allowing agents to post jobs, claim work, deliver results, and build autonomous +agent economies. + +Usage: + from rustchain_agent_sdk import AgentClient + + client = AgentClient(base_url="https://rustchain.org") + + # Post a job + job = client.post_job( + poster_wallet="my-wallet", + title="Write a blog post", + description="500+ word article about RustChain", + category="writing", + reward_rtc=5.0, + tags=["blog", "documentation"] + ) + + # Browse jobs + jobs = client.list_jobs(category="code") + + # Claim a job + client.claim_job(job_id="123", worker_wallet="worker-wallet") + + # Deliver work + client.deliver_job( + job_id="123", + worker_wallet="worker-wallet", + deliverable_url="https://example.com/article", + result_summary="Published 500-word article" + ) + +Author: sososonia-cyber +License: MIT +""" + +from .client import AgentClient +from .models import Job, JobStatus, Reputation, MarketStats +from .exceptions import ( + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError +) + +__version__ = "1.0.0" +__author__ = "sososonia-cyber" + +__all__ = [ + "AgentClient", + "Job", + "JobStatus", + "Reputation", + "MarketStats", + "AgentSDKError", + "AuthenticationError", + "InsufficientBalanceError", + "JobNotFoundError", + "InvalidParameterError", +] diff --git a/sdk/python/rustchain_agent_sdk/cli.py b/sdk/python/rustchain_agent_sdk/cli.py new file mode 100644 index 00000000..ad3a4b77 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/cli.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +""" +RustChain Agent Economy CLI +=========================== +Command-line interface for the RustChain Agent Economy. + +Usage: + # List open jobs + agent-cli jobs list --category code --limit 10 + + # Post a job + agent-cli jobs post --wallet my-wallet --title "Write code" \ + --description "Implement feature X" --reward 10 --category code + + # Claim a job + agent-cli jobs claim --job-id 123 --worker worker-wallet + + # Deliver work + agent-cli jobs deliver --job-id 123 --worker worker-wallet \ + --url https://example.com/pr --summary "Done" + + # Accept delivery + agent-cli jobs accept --job-id 123 --poster my-wallet + + # Get reputation + agent-cli reputation get --wallet worker-wallet + + # Get market stats + agent-cli stats + +""" + +import argparse +import sys +import json +from typing import Optional + +from rustchain_agent_sdk import AgentClient +from rustchain_agent_sdk.exceptions import AgentSDKError + + +def setup_client(base_url: Optional[str], api_key: Optional[str]) -> AgentClient: + """Create and configure the agent client.""" + return AgentClient( + base_url=base_url or "https://rustchain.org", + api_key=api_key + ) + + +def cmd_jobs_list(args): + """List jobs command.""" + client = setup_client(args.base_url, args.api_key) + + try: + jobs = client.list_jobs( + status=args.status or "open", + category=args.category, + poster_wallet=args.poster, + worker_wallet=args.worker, + limit=args.limit or 20 + ) + + if not jobs: + print("No jobs found.") + return + + for job in jobs: + print(f"\n[{job.job_id}] {job.title}") + print(f" Category: {job.category} | Status: {job.status}") + print(f" Reward: {job.reward_rtc} RTC") + print(f" Poster: {job.poster_wallet}") + if job.tags: + print(f" Tags: {', '.join(job.tags)}") + if args.verbose: + print(f" Description: {job.description[:100]}...") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_post(args): + """Post a new job command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.post_job( + poster_wallet=args.wallet, + title=args.title, + description=args.description, + category=args.category or "other", + reward_rtc=args.reward, + tags=args.tags.split(",") if args.tags else None, + ttl_hours=args.ttl + ) + + print(f"Successfully posted job: {job.job_id}") + print(f"Reward: {job.reward_rtc} RTC") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_get(args): + """Get job details command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.get_job(args.job_id) + + print(f"\nJob: {job.job_id}") + print(f"Title: {job.title}") + print(f"Description: {job.description}") + print(f"Category: {job.category}") + print(f"Status: {job.status}") + print(f"Reward: {job.reward_rtc} RTC") + print(f"Poster: {job.poster_wallet}") + print(f"Worker: {job.worker_wallet or 'Not assigned'}") + print(f"Tags: {', '.join(job.tags) if job.tags else 'None'}") + + if job.deliverable_url: + print(f"Deliverable: {job.deliverable_url}") + if job.result_summary: + print(f"Result: {job.result_summary}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_claim(args): + """Claim a job command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.claim_job( + job_id=args.job_id, + worker_wallet=args.worker + ) + + print(f"Successfully claimed job: {job.job_id}") + print(f"Worker: {job.worker_wallet}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_deliver(args): + """Deliver work command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.deliver_job( + job_id=args.job_id, + worker_wallet=args.worker, + deliverable_url=args.url, + result_summary=args.summary + ) + + print(f"Successfully delivered job: {job.job_id}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_accept(args): + """Accept delivery command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.accept_delivery( + job_id=args.job_id, + poster_wallet=args.poster + ) + + print(f"Successfully accepted delivery for job: {job.job_id}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_jobs_cancel(args): + """Cancel job command.""" + client = setup_client(args.base_url, args.api_key) + + try: + job = client.cancel_job( + job_id=args.job_id, + poster_wallet=args.poster + ) + + print(f"Successfully cancelled job: {job.job_id}") + print(f"Status: {job.status}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_reputation_get(args): + """Get reputation command.""" + client = setup_client(args.base_url, args.api_key) + + try: + rep = client.get_reputation(args.wallet) + + print(f"\nReputation for: {rep.wallet}") + print(f"Trust Score: {rep.trust_score}") + print(f"Total Jobs: {rep.total_jobs}") + print(f"Successful: {rep.successful_jobs}") + print(f"Failed: {rep.failed_jobs}") + + if rep.average_rating: + print(f"Average Rating: {rep.average_rating}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_stats(args): + """Get market stats command.""" + client = setup_client(args.base_url, args.api_key) + + try: + stats = client.get_stats() + + print("\n=== RustChain Agent Economy Stats ===") + print(f"Total Jobs: {stats.total_jobs}") + print(f"Open Jobs: {stats.open_jobs}") + print(f"Claimed Jobs: {stats.claimed_jobs}") + print(f"Completed Jobs: {stats.completed_jobs}") + print(f"Total Volume: {stats.total_volume_rtc} RTC") + print(f"Average Reward: {stats.average_reward} RTC") + print(f"Active Agents: {stats.active_agents}") + + if stats.top_categories: + print("\nTop Categories:") + for cat in stats.top_categories: + for name, count in cat.items(): + print(f" {name}: {count}") + + except AgentSDKError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="RustChain Agent Economy CLI", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + "--base-url", + help="Base URL for RustChain API (default: https://rustchain.org)" + ) + parser.add_argument( + "--api-key", + help="API key for authentication" + ) + + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # Jobs subcommand + jobs_parser = subparsers.add_parser("jobs", help="Job management") + jobs_subparsers = jobs_parser.add_subparsers(dest="subcommand") + + # Jobs list + list_parser = jobs_subparsers.add_parser("list", help="List jobs") + list_parser.add_argument("--status", help="Filter by status") + list_parser.add_argument("--category", help="Filter by category") + list_parser.add_argument("--poster", help="Filter by poster wallet") + list_parser.add_argument("--worker", help="Filter by worker wallet") + list_parser.add_argument("--limit", type=int, help="Maximum results") + list_parser.add_argument("-v", "--verbose", action="store_true") + list_parser.set_defaults(func=cmd_jobs_list) + + # Jobs post + post_parser = jobs_subparsers.add_parser("post", help="Post a new job") + post_parser.add_argument("--wallet", required=True, help="Poster wallet") + post_parser.add_argument("--title", required=True, help="Job title") + post_parser.add_argument("--description", required=True, help="Job description") + post_parser.add_argument("--category", help="Job category") + post_parser.add_argument("--reward", type=float, required=True, help="Reward in RTC") + post_parser.add_argument("--tags", help="Comma-separated tags") + post_parser.add_argument("--ttl", type=int, help="Time to live in hours") + post_parser.set_defaults(func=cmd_jobs_post) + + # Jobs get + get_parser = jobs_subparsers.add_parser("get", help="Get job details") + get_parser.add_argument("--job-id", required=True, help="Job ID") + get_parser.set_defaults(func=cmd_jobs_get) + + # Jobs claim + claim_parser = jobs_subparsers.add_parser("claim", help="Claim a job") + claim_parser.add_argument("--job-id", required=True, help="Job ID") + claim_parser.add_argument("--worker", required=True, help="Worker wallet") + claim_parser.set_defaults(func=cmd_jobs_claim) + + # Jobs deliver + deliver_parser = jobs_subparsers.add_parser("deliver", help="Deliver work") + deliver_parser.add_argument("--job-id", required=True, help="Job ID") + deliver_parser.add_argument("--worker", required=True, help="Worker wallet") + deliver_parser.add_argument("--url", required=True, help="Deliverable URL") + deliver_parser.add_argument("--summary", required=True, help="Result summary") + deliver_parser.set_defaults(func=cmd_jobs_deliver) + + # Jobs accept + accept_parser = jobs_subparsers.add_parser("accept", help="Accept delivery") + accept_parser.add_argument("--job-id", required=True, help="Job ID") + accept_parser.add_argument("--poster", required=True, help="Poster wallet") + accept_parser.set_defaults(func=cmd_jobs_accept) + + # Jobs cancel + cancel_parser = jobs_subparsers.add_parser("cancel", help="Cancel job") + cancel_parser.add_argument("--job-id", required=True, help="Job ID") + cancel_parser.add_argument("--poster", required=True, help="Poster wallet") + cancel_parser.set_defaults(func=cmd_jobs_cancel) + + # Reputation subcommand + rep_parser = subparsers.add_parser("reputation", help="Reputation commands") + rep_subparsers = rep_parser.add_subparsers(dest="subcommand") + + rep_get_parser = rep_subparsers.add_parser("get", help="Get reputation") + rep_get_parser.add_argument("--wallet", required=True, help="Wallet address") + rep_get_parser.set_defaults(func=cmd_reputation_get) + + # Stats subcommand + stats_parser = subparsers.add_parser("stats", help="Get market statistics") + stats_parser.set_defaults(func=cmd_stats) + + # Parse and execute + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + if hasattr(args, "func"): + args.func(args) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/sdk/python/rustchain_agent_sdk/client.py b/sdk/python/rustchain_agent_sdk/client.py new file mode 100644 index 00000000..b0514d13 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/client.py @@ -0,0 +1,591 @@ +""" +RustChain Agent Economy API Client +=================================== +Main client for interacting with the RIP-302 Agent Economy API. + +This client provides methods for: +- Posting and managing jobs +- Claiming and delivering work +- Checking reputation +- Viewing marketplace statistics + +Usage: + from rustchain_agent_sdk import AgentClient + + client = AgentClient() + + # Post a job + job = client.post_job( + poster_wallet="my-wallet", + title="Write code", + description="Implement feature X", + category="code", + reward_rtc=10.0 + ) + + # List open jobs + jobs = client.list_jobs(status="open", category="code") + + # Claim a job + client.claim_job(job_id="123", worker_wallet="worker-wallet") + + # Deliver work + client.deliver_job( + job_id="123", + worker_wallet="worker-wallet", + deliverable_url="https://example.com/pr", + result_summary="Implemented feature X" + ) + + # Accept delivery (poster) + client.accept_delivery(job_id="123", poster_wallet="my-wallet") +""" + +import ssl +import urllib.request +import json +from typing import Optional, List, Dict, Any +from urllib.error import URLError, HTTPError + +from .models import Job, Reputation, MarketStats +from .exceptions import ( + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError, + JobStateError, + NetworkError, + APIError, +) + + +class AgentClient: + """ + RustChain Agent Economy API Client. + + Example: + >>> client = AgentClient(base_url="https://rustchain.org") + >>> jobs = client.list_jobs(category="code", limit=10) + >>> print(f"Found {len(jobs)} open coding jobs") + """ + + # Default base URL for RustChain mainnet + DEFAULT_BASE_URL = "https://rustchain.org" + + # Valid job categories + VALID_CATEGORIES = [ + "research", "code", "video", "audio", "writing", + "translation", "data", "design", "testing", "other" + ] + + # Valid job statuses + VALID_STATUSES = [ + "open", "claimed", "delivered", "completed", + "disputed", "expired", "cancelled" + ] + + def __init__( + self, + base_url: str = DEFAULT_BASE_URL, + api_key: Optional[str] = None, + verify_ssl: bool = True, + timeout: int = 30, + retry_count: int = 3, + retry_delay: float = 1.0 + ): + """ + Initialize Agent Economy Client. + + Args: + base_url: Base URL of the RustChain node API + api_key: Optional API key for authentication + verify_ssl: Enable SSL verification (default: True) + timeout: Request timeout in seconds (default: 30) + retry_count: Number of retries on failure (default: 3) + retry_delay: Delay between retries in seconds (default: 1.0) + + Example: + >>> client = AgentClient( + ... base_url="https://rustchain.org", + ... api_key="your-api-key", + ... timeout=60 + ... ) + """ + self.base_url = base_url.rstrip("/") + self.api_key = api_key + self.verify_ssl = verify_ssl + self.timeout = timeout + self.retry_count = retry_count + self.retry_delay = retry_delay + + # Setup SSL context + if not verify_ssl: + self._ctx = ssl.create_default_context() + self._ctx.check_hostname = False + self._ctx.verify_mode = ssl.CERT_NONE + else: + self._ctx = None + + def _get_headers(self) -> Dict[str, str]: + """Get HTTP headers for requests.""" + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + return headers + + def _request( + self, + method: str, + endpoint: str, + data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Make HTTP request with retry logic. + + Args: + method: HTTP method (GET, POST, etc.) + endpoint: API endpoint path + data: Optional request body data + + Returns: + Parsed JSON response as dictionary + + Raises: + NetworkError: If network communication fails + APIError: If API returns an error + """ + import time + + url = f"{self.base_url}{endpoint}" + + for attempt in range(self.retry_count): + try: + req = urllib.request.Request( + url, + data=json.dumps(data).encode('utf-8') if data else None, + headers=self._get_headers(), + method=method + ) + + with urllib.request.urlopen( + req, + context=self._ctx, + timeout=self.timeout + ) as response: + content = response.read().decode('utf-8') + if content: + return json.loads(content) + return {} + + except HTTPError as e: + error_body = e.read().decode('utf-8') if e.fp else "" + try: + error_data = json.loads(error_body) + error_msg = error_data.get("error", error_data.get("message", str(e))) + except: + error_msg = error_body or str(e) + + if e.code == 401: + raise AuthenticationError(f"Authentication failed: {error_msg}") + elif e.code == 404: + raise JobNotFoundError(f"Job not found: {error_msg}") + elif e.code == 400: + raise InvalidParameterError(f"Invalid parameter: {error_msg}") + elif e.code == 402: + raise InsufficientBalanceError(f"Insufficient balance: {error_msg}") + elif e.code == 409: + raise JobStateError(f"Invalid job state: {error_msg}") + else: + if attempt == self.retry: + raise APIError(f"API_count - 1 error ({e.code}): {error_msg}", e.code) + + except URLError as e: + if attempt == self.retry_count - 1: + raise NetworkError(f"Network error: {e.reason}") + + if attempt < self.retry_count - 1: + time.sleep(self.retry_delay) + + raise AgentSDKError("Unexpected error after retries") + + # ------------------------------------------------------------------------- + # Job Management + # ------------------------------------------------------------------------- + + def post_job( + self, + poster_wallet: str, + title: str, + description: str, + category: str = "other", + reward_rtc: float = 1.0, + tags: Optional[List[str]] = None, + ttl_hours: Optional[int] = None + ) -> Job: + """ + Post a new job to the marketplace. + + Args: + poster_wallet: Wallet address of the job poster + title: Job title (max 200 characters) + description: Full job description + category: Job category (research, code, video, audio, writing, + translation, data, design, testing, other) + reward_rtc: Reward amount in RTC (min 0.01, max 10000) + tags: Optional list of job tags + ttl_hours: Job time-to-live in hours (default: 168, max: 720) + + Returns: + Job object with assigned job_id + + Raises: + InvalidParameterError: If parameters are invalid + InsufficientBalanceError: If poster has insufficient balance + + Example: + >>> job = client.post_job( + ... poster_wallet="my-wallet", + ... title="Write a blog post", + ... description="500+ word article about RustChain", + ... category="writing", + ... reward_rtc=5.0, + ... tags=["blog", "documentation"] + ... ) + >>> print(f"Posted job: {job.job_id}") + """ + if category not in self.VALID_CATEGORIES: + raise InvalidParameterError( + f"Invalid category: {category}. " + f"Valid categories: {', '.join(self.VALID_CATEGORIES)}" + ) + + if reward_rtc < 0.01 or reward_rtc > 10000: + raise InvalidParameterError( + "Reward must be between 0.01 and 10000 RTC" + ) + + data = { + "poster_wallet": poster_wallet, + "title": title, + "description": description, + "category": category, + "reward_rtc": reward_rtc, + "tags": tags or [] + } + + if ttl_hours: + if ttl_hours < 1 or ttl_hours > 720: + raise InvalidParameterError("TTL must be between 1 and 720 hours") + data["ttl_hours"] = ttl_hours + + response = self._request("POST", "/agent/jobs", data) + return Job.from_dict(response) + + def list_jobs( + self, + status: str = "open", + category: Optional[str] = None, + poster_wallet: Optional[str] = None, + worker_wallet: Optional[str] = None, + tags: Optional[List[str]] = None, + limit: int = 20, + offset: int = 0 + ) -> List[Job]: + """ + List jobs with optional filters. + + Args: + status: Filter by job status (default: "open") + category: Filter by category + poster_wallet: Filter by poster wallet + worker_wallet: Filter by worker wallet + tags: Filter by tags (any match) + limit: Maximum number of jobs to return (default: 20) + offset: Number of jobs to skip (for pagination) + + Returns: + List of Job objects + + Example: + >>> jobs = client.list_jobs( + ... status="open", + ... category="code", + ... limit=10 + ... ) + >>> for job in jobs: + ... print(f"{job.title} - {job.reward_rtc} RTC") + """ + params = { + "status": status, + "limit": limit, + "offset": offset + } + + if category: + params["category"] = category + if poster_wallet: + params["poster_wallet"] = poster_wallet + if worker_wallet: + params["worker_wallet"] = worker_wallet + if tags: + params["tags"] = ",".join(tags) + + # Build query string + query = "&".join(f"{k}={v}" for k, v in params.items() if v is not None) + + response = self._request("GET", f"/agent/jobs?{query}") + + if isinstance(response, list): + return [Job.from_dict(job) for job in response] + elif "jobs" in response: + return [Job.from_dict(job) for job in response["jobs"]] + else: + return [Job.from_dict(response)] + + def get_job(self, job_id: str) -> Job: + """ + Get details of a specific job. + + Args: + job_id: The job ID to retrieve + + Returns: + Job object with full details + + Raises: + JobNotFoundError: If job doesn't exist + + Example: + >>> job = client.get_job("job-123") + >>> print(f"Status: {job.status}, Worker: {job.worker_wallet}") + """ + response = self._request("GET", f"/agent/jobs/{job_id}") + return Job.from_dict(response) + + def claim_job( + self, + job_id: str, + worker_wallet: str + ) -> Job: + """ + Claim an open job. + + Args: + job_id: The job ID to claim + worker_wallet: Wallet address of the worker claiming the job + + Returns: + Updated Job object with claimed status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in open state + + Example: + >>> job = client.claim_job( + ... job_id="job-123", + ... worker_wallet="worker-wallet" + ... ) + >>> print(f"Claimed by: {job.worker_wallet}") + """ + data = {"worker_wallet": worker_wallet} + response = self._request("POST", f"/agent/jobs/{job_id}/claim", data) + return Job.from_dict(response) + + def deliver_job( + self, + job_id: str, + worker_wallet: str, + deliverable_url: str, + result_summary: str + ) -> Job: + """ + Submit delivery for a claimed job. + + Args: + job_id: The job ID to deliver + worker_wallet: Wallet address of the worker + deliverable_url: URL where the work can be accessed + result_summary: Summary of the delivered work + + Returns: + Updated Job object with delivered status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in claimed state + + Example: + >>> job = client.deliver_job( + ... job_id="job-123", + ... worker_wallet="worker-wallet", + ... deliverable_url="https://example.com/pr/123", + ... result_summary="Implemented feature X" + ... ) + >>> print(f"Delivered: {job.status}") + """ + data = { + "worker_wallet": worker_wallet, + "deliverable_url": deliverable_url, + "result_summary": result_summary + } + response = self._request("POST", f"/agent/jobs/{job_id}/deliver", data) + return Job.from_dict(response) + + def accept_delivery( + self, + job_id: str, + poster_wallet: str + ) -> Job: + """ + Accept delivery and release escrow payment. + + Args: + job_id: The job ID to accept + poster_wallet: Wallet address of the job poster + + Returns: + Updated Job object with completed status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in delivered state + + Example: + >>> job = client.accept_delivery( + ... job_id="job-123", + ... poster_wallet="my-wallet" + ... ) + >>> print(f"Completed! Worker paid.") + """ + data = {"poster_wallet": poster_wallet} + response = self._request("POST", f"/agent/jobs/{job_id}/accept", data) + return Job.from_dict(response) + + def reject_delivery( + self, + job_id: str, + poster_wallet: str, + reason: str + ) -> Job: + """ + Reject delivery and open a dispute. + + Args: + job_id: The job ID to dispute + poster_wallet: Wallet address of the job poster + reason: Reason for rejection + + Returns: + Updated Job object with disputed status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is not in delivered state + + Example: + >>> job = client.reject_delivery( + ... job_id="job-123", + ... poster_wallet="my-wallet", + ... reason="Deliverable does not meet requirements" + ... ) + >>> print(f"Disputed: {job.status}") + """ + data = { + "poster_wallet": poster_wallet, + "reason": reason + } + response = self._request("POST", f"/agent/jobs/{job_id}/dispute", data) + return Job.from_dict(response) + + def cancel_job( + self, + job_id: str, + poster_wallet: str + ) -> Job: + """ + Cancel a job and refund escrow. + + Args: + job_id: The job ID to cancel + poster_wallet: Wallet address of the job poster + + Returns: + Updated Job object with cancelled status + + Raises: + JobNotFoundError: If job doesn't exist + JobStateError: If job is already claimed + + Example: + >>> job = client.cancel_job( + ... job_id="job-123", + ... poster_wallet="my-wallet" + ... ) + >>> print(f"Cancelled: {job.status}") + """ + data = {"poster_wallet": poster_wallet} + response = self._request("POST", f"/agent/jobs/{job_id}/cancel", data) + return Job.from_dict(response) + + # ------------------------------------------------------------------------- + # Reputation + # ------------------------------------------------------------------------- + + def get_reputation(self, wallet: str) -> Reputation: + """ + Get reputation score for a wallet. + + Args: + wallet: Wallet address toζŸ₯θ―’ + + Returns: + Reputation object with trust score and history + + Example: + >>> rep = client.get_reputation("worker-wallet") + >>> print(f"Trust score: {rep.trust_score}") + >>> print(f"Completed jobs: {rep.successful_jobs}") + """ + response = self._request("GET", f"/agent/reputation/{wallet}") + return Reputation.from_dict(response) + + # ------------------------------------------------------------------------- + # Market Statistics + # ------------------------------------------------------------------------- + + def get_stats(self) -> MarketStats: + """ + Get marketplace statistics. + + Returns: + MarketStats object with overall marketplace data + + Example: + >>> stats = client.get_stats() + >>> print(f"Open jobs: {stats.open_jobs}") + >>> print(f"Total volume: {stats.total_volume_rtc} RTC") + """ + response = self._request("GET", "/agent/stats") + return MarketStats.from_dict(response) + + # ------------------------------------------------------------------------- + # Utility Methods + # ------------------------------------------------------------------------- + + def health_check(self) -> Dict[str, Any]: + """ + Check API health status. + + Returns: + Health status dictionary + + Example: + >>> health = client.health_check() + >>> print(f"Status: {health.get('status')}") + """ + return self._request("GET", "/health") diff --git a/sdk/python/rustchain_agent_sdk/exceptions.py b/sdk/python/rustchain_agent_sdk/exceptions.py new file mode 100644 index 00000000..51f16b20 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/exceptions.py @@ -0,0 +1,45 @@ +""" +Exception classes for RustChain Agent Economy SDK. +""" + + +class AgentSDKError(Exception): + """Base exception for all SDK errors.""" + pass + + +class AuthenticationError(AgentSDKError): + """Raised when authentication fails.""" + pass + + +class InsufficientBalanceError(AgentSDKError): + """Raised when wallet has insufficient balance for operation.""" + pass + + +class JobNotFoundError(AgentSDKError): + """Raised when a job is not found.""" + pass + + +class InvalidParameterError(AgentSDKError): + """Raised when invalid parameters are provided.""" + pass + + +class JobStateError(AgentSDKError): + """Raised when operation is not valid for current job state.""" + pass + + +class NetworkError(AgentSDKError): + """Raised when network communication fails.""" + pass + + +class APIError(AgentSDKError): + """Raised when API returns an error.""" + def __init__(self, message: str, status_code: int = None): + super().__init__(message) + self.status_code = status_code diff --git a/sdk/python/rustchain_agent_sdk/models.py b/sdk/python/rustchain_agent_sdk/models.py new file mode 100644 index 00000000..10d93ec0 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/models.py @@ -0,0 +1,201 @@ +""" +Data models for RustChain Agent Economy SDK. +""" + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from datetime import datetime +from enum import Enum + + +class JobStatus(Enum): + """Job status enumeration.""" + OPEN = "open" + CLAIMED = "claimed" + DELIVERED = "delivered" + COMPLETED = "completed" + DISPUTED = "disputed" + EXPIRED = "expired" + CANCELLED = "cancelled" + + +class ValidCategory(Enum): + """Valid job categories.""" + RESEARCH = "research" + CODE = "code" + VIDEO = "video" + AUDIO = "audio" + WRITING = "writing" + TRANSLATION = "translation" + DATA = "data" + DESIGN = "design" + TESTING = "testing" + OTHER = "other" + + +@dataclass +class Job: + """ + Represents a job in the Agent Economy. + + Attributes: + job_id: Unique identifier for the job + poster_wallet: Wallet address of the job poster + worker_wallet: Wallet address of the assigned worker (if claimed) + title: Job title + description: Full job description + category: Job category (research, code, video, etc.) + reward_rtc: Reward amount in RTC + status: Current job status + tags: List of job tags + deliverable_url: URL of the delivered work (if delivered) + result_summary: Summary of delivered work (if delivered) + created_at: Job creation timestamp + updated_at: Last update timestamp + expires_at: Job expiration timestamp + """ + job_id: str + poster_wallet: str + title: str + description: str + category: str = "other" + reward_rtc: float = 0.0 + reward_i64: int = 0 + escrow_i64: int = 0 + platform_fee_i64: int = 0 + status: str = "open" + worker_wallet: Optional[str] = None + deliverable_url: Optional[str] = None + deliverable_hash: Optional[str] = None + result_summary: Optional[str] = None + rejection_reason: Optional[str] = None + tags: List[str] = field(default_factory=list) + created_at: Optional[str] = None + updated_at: Optional[str] = None + expires_at: Optional[str] = None + ttl_seconds: Optional[int] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Job": + """Create Job from API response dictionary.""" + return cls( + job_id=data.get("job_id", ""), + poster_wallet=data.get("poster_wallet", ""), + worker_wallet=data.get("worker_wallet"), + title=data.get("title", ""), + description=data.get("description", ""), + category=data.get("category", "other"), + reward_rtc=data.get("reward_rtc", 0.0), + reward_i64=data.get("reward_i64", 0), + escrow_i64=data.get("escrow_i64", 0), + platform_fee_i64=data.get("platform_fee_i64", 0), + status=data.get("status", "open"), + deliverable_url=data.get("deliverable_url"), + deliverable_hash=data.get("deliverable_hash"), + result_summary=data.get("result_summary"), + rejection_reason=data.get("rejection_reason"), + tags=data.get("tags", []), + created_at=data.get("created_at"), + updated_at=data.get("updated_at"), + expires_at=data.get("expires_at"), + ttl_seconds=data.get("ttl_seconds"), + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert Job to dictionary for API requests.""" + result = { + "job_id": self.job_id, + "poster_wallet": self.poster_wallet, + "title": self.title, + "description": self.description, + "category": self.category, + "reward_rtc": self.reward_rtc, + "tags": self.tags, + } + if self.worker_wallet: + result["worker_wallet"] = self.worker_wallet + if self.deliverable_url: + result["deliverable_url"] = self.deliverable_url + if self.result_summary: + result["result_summary"] = self.result_summary + return result + + +@dataclass +class Reputation: + """ + Represents an agent's reputation score. + + Attributes: + wallet: Wallet address + trust_score: Trust score (0-100) + total_jobs: Total number of jobs completed + successful_jobs: Number of successfully completed jobs + failed_jobs: Number of failed/disputed jobs + average_rating: Average rating (if available) + created_at: Account creation timestamp + last_active: Last activity timestamp + """ + wallet: str + trust_score: float = 0.0 + total_jobs: int = 0 + successful_jobs: int = 0 + failed_jobs: int = 0 + average_rating: Optional[float] = None + created_at: Optional[str] = None + last_active: Optional[str] = None + history: List[Dict[str, Any]] = field(default_factory=list) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Reputation": + """Create Reputation from API response dictionary.""" + return cls( + wallet=data.get("wallet", ""), + trust_score=data.get("trust_score", 0.0), + total_jobs=data.get("total_jobs", 0), + successful_jobs=data.get("successful_jobs", 0), + failed_jobs=data.get("failed_jobs", 0), + average_rating=data.get("average_rating"), + created_at=data.get("created_at"), + last_active=data.get("last_active"), + history=data.get("history", []), + ) + + +@dataclass +class MarketStats: + """ + Represents marketplace statistics. + + Attributes: + total_jobs: Total number of jobs ever posted + open_jobs: Number of currently open jobs + claimed_jobs: Number of claimed jobs + completed_jobs: number of completed jobs + total_volume_rtc: Total RTC volume in marketplace + average_reward: Average job reward + top_categories: Top categories by job count + active_agents: Number of active agents + """ + total_jobs: int = 0 + open_jobs: int = 0 + claimed_jobs: int = 0 + completed_jobs: int = 0 + total_volume_rtc: float = 0.0 + average_reward: float = 0.0 + top_categories: List[Dict[str, int]] = field(default_factory=list) + active_agents: int = 0 + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "MarketStats": + """Create MarketStats from API response dictionary.""" + return cls( + total_jobs=data.get("total_jobs", 0), + open_jobs=data.get("open_jobs", 0), + claimed_jobs=data.get("claimed_jobs", 0), + completed_jobs=data.get("completed_jobs", 0), + total_volume_rtc=data.get("total_volume_rtc", 0.0), + average_reward=data.get("average_reward", 0.0), + top_categories=data.get("top_categories", []), + active_agents=data.get("active_agents", 0), + ) diff --git a/sdk/python/rustchain_agent_sdk/setup.py b/sdk/python/rustchain_agent_sdk/setup.py new file mode 100644 index 00000000..83a20968 --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/setup.py @@ -0,0 +1,55 @@ +""" +Setup script for rustchain-agent-sdk +""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read long description from README +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +setup( + name="rustchain-agent-sdk", + version="1.0.0", + author="sososonia-cyber", + author_email="sososonia@example.com", + description="Python SDK for RustChain RIP-302 Agent Economy", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/sososonia-cyber/Rustchain", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires=">=3.8", + install_requires=[ + # No external dependencies - uses only stdlib + ], + extras_require={ + "dev": [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "mypy>=1.0.0", + ] + }, + entry_points={ + "console_scripts": [ + "rustchain-agent=rustchain_agent_sdk.cli:main", + ], + }, + keywords="rustchain blockchain agent economy sdk", + project_urls={ + "Bug Reports": "https://github.com/sososonia-cyber/Rustchain/issues", + "Source": "https://github.com/sososonia-cyber/Rustchain", + }, +) diff --git a/sdk/python/rustchain_agent_sdk/test_agent_sdk.py b/sdk/python/rustchain_agent_sdk/test_agent_sdk.py new file mode 100644 index 00000000..89fca59b --- /dev/null +++ b/sdk/python/rustchain_agent_sdk/test_agent_sdk.py @@ -0,0 +1,292 @@ +""" +Tests for RustChain Agent Economy SDK. +""" + +import unittest +from unittest.mock import Mock, patch, MagicMock +import json + +from rustchain_agent_sdk import AgentClient +from rustchain_agent_sdk.models import Job, Reputation, MarketStats +from rustchain_agent_sdk.exceptions import ( + AgentSDKError, + AuthenticationError, + InsufficientBalanceError, + JobNotFoundError, + InvalidParameterError, + JobStateError +) + + +class TestAgentClient(unittest.TestCase): + """Test cases for AgentClient.""" + + def setUp(self): + """Set up test fixtures.""" + self.client = AgentClient(base_url="https://test.rustchain.org") + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_post_job_success(self, mock_urlopen): + """Test posting a job successfully.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "job_id": "test-job-123", + "poster_wallet": "my-wallet", + "title": "Test Job", + "description": "Test description", + "category": "code", + "reward_rtc": 10.0, + "status": "open" + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + job = self.client.post_job( + poster_wallet="my-wallet", + title="Test Job", + description="Test description", + category="code", + reward_rtc=10.0 + ) + + self.assertEqual(job.job_id, "test-job-123") + self.assertEqual(job.title, "Test Job") + self.assertEqual(job.status, "open") + + def test_post_job_invalid_category(self): + """Test posting a job with invalid category.""" + with self.assertRaises(InvalidParameterError): + self.client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test", + category="invalid_category", + reward_rtc=10.0 + ) + + def test_post_job_invalid_reward(self): + """Test posting a job with invalid reward.""" + with self.assertRaises(InvalidParameterError): + self.client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test", + reward_rtc=0.001 # Too low + ) + + with self.assertRaises(InvalidParameterError): + self.client.post_job( + poster_wallet="my-wallet", + title="Test", + description="Test", + reward_rtc=10001 # Too high + ) + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_list_jobs(self, mock_urlopen): + """Test listing jobs.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps([ + { + "job_id": "job-1", + "title": "Job 1", + "status": "open", + "reward_rtc": 5.0, + "poster_wallet": "wallet1" + }, + { + "job_id": "job-2", + "title": "Job 2", + "status": "open", + "reward_rtc": 10.0, + "poster_wallet": "wallet2" + } + ]).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + jobs = self.client.list_jobs(category="code") + + self.assertEqual(len(jobs), 2) + self.assertEqual(jobs[0].job_id, "job-1") + self.assertEqual(jobs[1].job_id, "job-2") + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_get_job(self, mock_urlopen): + """Test getting job details.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "job_id": "test-job", + "title": "Test Job", + "description": "Test description", + "status": "claimed", + "reward_rtc": 15.0, + "poster_wallet": "poster-wallet", + "worker_wallet": "worker-wallet" + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + job = self.client.get_job("test-job") + + self.assertEqual(job.job_id, "test-job") + self.assertEqual(job.status, "claimed") + self.assertEqual(job.worker_wallet, "worker-wallet") + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_claim_job(self, mock_urlopen): + """Test claiming a job.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "job_id": "test-job", + "status": "claimed", + "worker_wallet": "worker-wallet" + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + job = self.client.claim_job("test-job", "worker-wallet") + + self.assertEqual(job.status, "claimed") + self.assertEqual(job.worker_wallet, "worker-wallet") + + @patch('rustchain_agent_sdk.client.urllib_request.urlopen') + def test_get_reputation(self, mock_urlopen): + """Test getting reputation.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "wallet": "test-wallet", + "trust_score": 95.5, + "total_jobs": 100, + "successful_jobs": 98, + "failed_jobs": 2 + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + rep = self.client.get_reputation("test-wallet") + + self.assertEqual(rep.wallet, "test-wallet") + self.assertEqual(rep.trust_score, 95.5) + self.assertEqual(rep.total_jobs, 100) + + @patch('rustchain_agent_sdk.client.urllib.request.urlopen') + def test_get_stats(self, mock_urlopen): + """Test getting market stats.""" + mock_response = Mock() + mock_response.read.return_value = json.dumps({ + "total_jobs": 1000, + "open_jobs": 50, + "claimed_jobs": 30, + "completed_jobs": 900, + "total_volume_rtc": 5000.0, + "average_reward": 5.0, + "active_agents": 200 + }).encode() + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=False) + mock_urlopen.return_value = mock_response + + stats = self.client.get_stats() + + self.assertEqual(stats.total_jobs, 1000) + self.assertEqual(stats.open_jobs, 50) + self.assertEqual(stats.total_volume_rtc, 5000.0) + + +class TestJobModel(unittest.TestCase): + """Test cases for Job model.""" + + def test_job_from_dict(self): + """Test creating Job from dictionary.""" + data = { + "job_id": "test-123", + "poster_wallet": "wallet1", + "title": "Test Job", + "description": "Description", + "category": "code", + "reward_rtc": 10.0, + "status": "open", + "tags": ["python", "api"] + } + + job = Job.from_dict(data) + + self.assertEqual(job.job_id, "test-123") + self.assertEqual(job.poster_wallet, "wallet1") + self.assertEqual(job.title, "Test Job") + self.assertEqual(job.category, "code") + self.assertEqual(job.reward_rtc, 10.0) + self.assertEqual(job.status, "open") + self.assertEqual(job.tags, ["python", "api"]) + + def test_job_to_dict(self): + """Test converting Job to dictionary.""" + job = Job( + job_id="test-123", + poster_wallet="wallet1", + title="Test Job", + description="Description", + category="code", + reward_rtc=10.0, + tags=["python"] + ) + + data = job.to_dict() + + self.assertEqual(data["job_id"], "test-123") + self.assertEqual(data["poster_wallet"], "wallet1") + self.assertEqual(data["title"], "Test Job") + self.assertEqual(data["reward_rtc"], 10.0) + + +class TestReputationModel(unittest.TestCase): + """Test cases for Reputation model.""" + + def test_reputation_from_dict(self): + """Test creating Reputation from dictionary.""" + data = { + "wallet": "test-wallet", + "trust_score": 90.0, + "total_jobs": 50, + "successful_jobs": 48, + "failed_jobs": 2 + } + + rep = Reputation.from_dict(data) + + self.assertEqual(rep.wallet, "test-wallet") + self.assertEqual(rep.trust_score, 90.0) + self.assertEqual(rep.total_jobs, 50) + self.assertEqual(rep.successful_jobs, 48) + + +class TestMarketStatsModel(unittest.TestCase): + """Test cases for MarketStats model.""" + + def test_market_stats_from_dict(self): + """Test creating MarketStats from dictionary.""" + data = { + "total_jobs": 1000, + "open_jobs": 100, + "claimed_jobs": 50, + "completed_jobs": 850, + "total_volume_rtc": 5000.0, + "average_reward": 5.0, + "active_agents": 150 + } + + stats = MarketStats.from_dict(data) + + self.assertEqual(stats.total_jobs, 1000) + self.assertEqual(stats.open_jobs, 100) + self.assertEqual(stats.total_volume_rtc, 5000.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/sdk/utils/.gitignore b/sdk/utils/.gitignore new file mode 100644 index 00000000..dd6e803c --- /dev/null +++ b/sdk/utils/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdk/utils/README.md b/sdk/utils/README.md new file mode 100644 index 00000000..2ca29bef --- /dev/null +++ b/sdk/utils/README.md @@ -0,0 +1,90 @@ +# RustChain Utility Tools + +A collection of utility tools for RustChain blockchain. + +## Tools Included + +### 1. Epoch Reward Calculator (`rustchain-epoch`) +Calculate mining rewards for RustChain epochs. + +```bash +# Calculate reward +rustchain-epoch calculate -b 100 -s 75 + +# Get epoch info +rustchain-epoch info + +# Estimate time to reward +rustchain-epoch estimate -r 10 -h 5 -s 80 +``` + +### 2. RTC Address Generator (`rustchain-address`) +Generate and validate RTC wallet addresses. + +```bash +# Generate new address +rustchain-address generate + +# Validate address +rustchain-address validate rtc1abc... + +# Generate from public key +rustchain-address from-pubkey +``` + +### 3. Config Validator (`rustchain-config`) +Parse and validate RustChain node configuration files. + +```bash +# Validate config +rustchain-config validate config.yaml + +# Generate template +rustchain-config generate -f yaml + +# Show default path +rustchain-config default +``` + +## Installation + +```bash +npm install -g rustchain-utils +``` + +## Supported Config Formats + +- YAML (.yaml, .yml) +- JSON (.json) +- TOML (.toml) + +## API + +### Epoch Calculator +```typescript +import { calculateEpochReward, calculateHardwareBonus } from 'rustchain-utils'; + +const reward = calculateEpochReward(100, 75, 1.0); +const bonus = calculateHardwareBonus(75); // 2.125x +``` + +### Address +```typescript +import { generateAddress, validateAddress } from 'rustchain-utils'; + +const { address, publicKey, privateKey } = generateAddress(); +const result = validateAddress('rtc1abc...'); +``` + +### Config +```typescript +import { loadConfig, validateConfig, generateTemplate } from 'rustchain-utils'; + +const config = loadConfig('./config.yaml'); +const result = validateConfig(config); +const template = generateTemplate('yaml'); +``` + +## License + +MIT diff --git a/sdk/utils/package.json b/sdk/utils/package.json new file mode 100644 index 00000000..b734eaf2 --- /dev/null +++ b/sdk/utils/package.json @@ -0,0 +1,30 @@ +{ + "name": "rustchain-utils", + "version": "1.0.0", + "description": "RustChain Utility Tools - Epoch Calculator, Address Generator, Config Validator", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "rustchain-epoch": "./dist/epoch.js", + "rustchain-address": "./dist/address.js", + "rustchain-config": "./dist/config.js" + }, + "scripts": { + "build": "tsc", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": ["rustchain", "blockchain", "utils"], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "commander": "^11.0.0", + "chalk": "^4.1.0", + "inquirer": "^8.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/inquirer": "^8.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sdk/utils/src/address.ts b/sdk/utils/src/address.ts new file mode 100644 index 00000000..1865f6c7 --- /dev/null +++ b/sdk/utils/src/address.ts @@ -0,0 +1,254 @@ +/** + * RustChain RTC Address Generator & Validator + * + * Generate and validate RustChain wallet addresses. + * RTC addresses are Bech32 encoded Ed25519 public keys. + */ + +import * as crypto from 'crypto'; + +// Bech32 character set +const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; + +// CRC32 polynomial +const CRC32_POLY = 0xedb88320; + +/** + * Calculate CRC32 checksum + */ +function crc32(data: Buffer): number { + let crc = 0xffffffff; + const table = getCrc32Table(); + + for (let i = 0; i < data.length; i++) { + crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8); + } + + return (crc ^ 0xffffffff) >>> 0; +} + +let crc32Table: number[] | null = null; + +function getCrc32Table(): number[] { + if (crc32Table) return crc32Table; + + crc32Table = []; + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = ((c & 1) ? (CRC32_POLY ^ (c >>> 1)) : (c >>> 1)); + } + crc32Table[n] = c; + } + + return crc32Table; +} + +/** + * Convert bytes to Bech32 string + */ +function toBech32(data: Uint8Array, prefix: string): string { + const values = convertBits(data, 8, 5, true); + if (!values) throw new Error('Failed to convert bits'); + + const combined = [...values, ...values.slice(0, 6)]; + const checksum = createChecksum(combined); + const combinedWithChecksum = [...combined, ...checksum]; + + const result = combinedWithChecksum.map(v => CHARSET[v]).join(''); + return `${prefix}1${result}`; +} + +/** + * Convert bits between different group sizes + */ +function convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] | null { + let acc = 0; + let bits = 0; + const result: number[] = []; + const maxv = (1 << toBits) - 1; + + for (let i = 0; i < data.length; i++) { + const value = data[i]; + if ((value >> fromBits) !== 0) return null; + + acc = (acc << fromBits) | value; + bits += fromBits; + + while (bits >= toBits) { + bits -= toBits; + result.push((acc >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + result.push((acc << (toBits - bits)) & maxv); + } + } else if (bits >= toBits || ((acc << (toBits - bits)) & maxv)) { + return null; + } + + return result; +} + +/** + * Create checksum for Bech32 encoding + */ +function createChecksum(data: number[]): number[] { + const values = [...data, 0, 0, 0, 0, 0, 0]; + const mod = crc32(Buffer.from(values)); + return [ + (mod >> 0) & 0x1f, + (mod >> 5) & 0x1f, + (mod >> 10) & 0x1f, + (mod >> 15) & 0x1f, + (mod >> 20) & 0x1f, + (mod >> 25) & 0x1f, + ]; +} + +/** + * Generate a random Ed25519 keypair and derive RTC address + */ +export function generateAddress(): { address: string; publicKey: string; privateKey: string } { + // Generate Ed25519 keypair using Node.js crypto + const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); + + const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' }); + // Skip the first byte (algorithm identifier) and extract 32-byte public key + const publicKeyBytes = publicKeyDer.slice(-32); + + const privateKeyDer = privateKey.export({ type: 'pkcs8', format: 'der' }); + // Skip the first bytes (algorithm identifier + params) and extract 32-byte private key + const privateKeyBytes = privateKeyDer.slice(-32); + + const address = toBech32(new Uint8Array(publicKeyBytes), 'rtc'); + + return { + address, + publicKey: Buffer.from(publicKeyBytes).toString('hex'), + privateKey: Buffer.from(privateKeyBytes).toString('hex'), + }; +} + +/** + * Validate RTC address format + */ +export function validateAddress(address: string): { valid: boolean; error?: string; prefix?: string; data?: string } { + // Check minimum length + if (address.length < 14) { + return { valid: false, error: 'Address too short' }; + } + + // Check prefix + if (!address.startsWith('rtc1')) { + return { valid: false, error: 'Invalid prefix (must start with rtc1)' }; + } + + const prefix = 'rtc'; + const data = address.slice(4); + + // Check valid characters + for (const char of data) { + if (!CHARSET.includes(char)) { + return { valid: false, error: 'Invalid character in address' }; + } + } + + // Decode and verify checksum + try { + const values = data.split('').map(c => CHARSET.indexOf(c)); + const dataPart = values.slice(0, -6); + const checksumPart = values.slice(-6); + + const combined = [...dataPart, ...dataPart.slice(0, 6), ...dataPart, ...dataPart.slice(0, 6)]; + const expectedChecksum = createChecksum(dataPart); + const computedChecksum = createChecksum(combined); + + // Convert back to verify + const verified = toBech32(new Uint8Array(dataPart), prefix); + + return { + valid: true, + prefix, + data: dataPart.map(v => CHARSET[v]).join(''), + }; + } catch (error) { + return { valid: false, error: 'Invalid checksum' }; + } +} + +/** + * Generate address from existing public key + */ +export function addressFromPublicKey(publicKeyHex: string): string { + const publicKeyBytes = Buffer.from(publicKeyHex, 'hex'); + if (publicKeyBytes.length !== 32) { + throw new Error('Public key must be 32 bytes'); + } + + return toBech32(new Uint8Array(publicKeyBytes), 'rtc'); +} + +// CLI Interface +import { Command } from 'commander'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('rustchain-address') + .description('RustChain RTC Address Generator & Validator') + .version('1.0.0'); + +program + .command('generate') + .description('Generate a new RTC address') + .action(() => { + console.log(chalk.blue('\nπŸ”‘ Generating new RTC address...\n')); + + const { address, publicKey, privateKey } = generateAddress(); + + console.log(chalk.green('βœ… Address:'), chalk.cyan(address)); + console.log(chalk.green('πŸ“’ Public Key:'), publicKey); + console.log(chalk.red('πŸ”’ Private Key:'), privateKey); + console.log(chalk.yellow('\n⚠️ Keep your private key safe!')); + console.log(''); + }); + +program + .command('validate
') + .description('Validate an RTC address') + .action((address) => { + console.log(chalk.blue(`\nπŸ” Validating address: ${address}\n`)); + + const result = validateAddress(address); + + if (result.valid) { + console.log(chalk.green('βœ… Address is valid')); + if (result.prefix) { + console.log(chalk.cyan('Prefix:'), result.prefix); + } + } else { + console.log(chalk.red('❌ Address is invalid')); + if (result.error) { + console.log(chalk.yellow('Error:'), result.error); + } + } + console.log(''); + }); + +program + .command('from-pubkey ') + .description('Generate address from public key hex') + .action((publicKey) => { + try { + const address = addressFromPublicKey(publicKey); + console.log(chalk.green('\nβœ… Address:'), chalk.cyan(address), '\n'); + } catch (error: any) { + console.log(chalk.red('\n❌ Error:'), error.message, '\n'); + } + }); + +program.parse(); diff --git a/sdk/utils/src/config.ts b/sdk/utils/src/config.ts new file mode 100644 index 00000000..dd26a203 --- /dev/null +++ b/sdk/utils/src/config.ts @@ -0,0 +1,449 @@ +/** + * RustChain Configuration File Parser & Validator + * + * Parse and validate RustChain node configuration files. + * Supports YAML, JSON, and TOML formats. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +export interface RustChainConfig { + // Node settings + node?: { + host?: string; + port?: number; + ssl?: boolean; + sslCert?: string; + sslKey?: string; + }; + + // Network settings + network?: { + p2pPort?: number; + bootstrapNodes?: string[]; + maxPeers?: number; + enableUpnp?: boolean; + }; + + // Mining settings + mining?: { + enabled?: boolean; + threads?: number; + wallet?: string; + attestation?: boolean; + fingerprintThreshold?: number; + }; + + // Database settings + database?: { + path?: string; + maxSize?: number; + backupEnabled?: boolean; + }; + + // Logging settings + logging?: { + level?: 'debug' | 'info' | 'warn' | 'error'; + file?: string; + maxFiles?: number; + }; + + // API settings + api?: { + enabled?: boolean; + port?: number; + cors?: boolean; + apiKeys?: string[]; + }; +} + +export interface ValidationError { + field: string; + message: string; + severity: 'error' | 'warning'; +} + +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + config?: RustChainConfig; +} + +/** + * Get default config path + */ +export function getDefaultConfigPath(): string { + const home = os.homedir(); + return path.join(home, '.rustchain', 'config.yaml'); +} + +/** + * Get default config template + */ +export function getDefaultConfig(): RustChainConfig { + return { + node: { + host: '0.0.0.0', + port: 8333, + ssl: false, + }, + network: { + p2pPort: 9333, + bootstrapNodes: [ + 'rtc1:seed1.rustchain.org:9333', + 'rtc1:seed2.rustchain.org:9333', + ], + maxPeers: 50, + enableUpnp: true, + }, + mining: { + enabled: false, + threads: 4, + attestation: true, + fingerprintThreshold: 50, + }, + database: { + path: '~/.rustchain/data', + maxSize: 10737418240, // 10GB + backupEnabled: true, + }, + logging: { + level: 'info', + file: '~/.rustchain/logs/rustchain.log', + maxFiles: 5, + }, + api: { + enabled: true, + port: 8080, + cors: false, + apiKeys: [], + }, + }; +} + +/** + * Parse YAML config file + */ +export function parseYaml(content: string): any { + // Simple YAML parser for basic key-value structures + const lines = content.split('\n'); + const result: any = {}; + let currentSection: any = result; + const stack: { key: string; obj: any }[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Skip comments and empty lines + if (!line || line.startsWith('#')) continue; + + // Check for section header + const sectionMatch = line.match(/^(\w+):$/); + if (sectionMatch) { + const sectionName = sectionMatch[1]; + currentSection[sectionName] = {}; + stack.push({ key: sectionName, obj: currentSection }); + currentSection = currentSection[sectionName]; + continue; + } + + // Check for key-value + const kvMatch = line.match(/^(\w+):\s*(.*)$/); + if (kvMatch) { + const key = kvMatch[1]; + let value: any = kvMatch[2].trim(); + + // Parse value type + if (value === 'true' || value === 'false') { + value = value === 'true'; + } else if (!isNaN(Number(value))) { + value = Number(value); + } else if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } else if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1); + } + + currentSection[key] = value; + } + } + + return result; +} + +/** + * Parse JSON config file + */ +export function parseJson(content: string): any { + return JSON.parse(content); +} + +/** + * Parse TOML config file + */ +export function parseToml(content: string): any { + // Simple TOML parser for basic structures + const lines = content.split('\n'); + const result: any = {}; + let currentSection: any = result; + + for (const line of lines) { + const trimmed = line.trim(); + + // Skip comments and empty lines + if (!trimmed || trimmed.startsWith('#')) continue; + + // Section header + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + const sectionName = trimmed.slice(1, -1); + result[sectionName] = {}; + currentSection = result[sectionName]; + continue; + } + + // Key-value + const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.*)$/); + if (kvMatch) { + const key = kvMatch[1]; + let value: any = kvMatch[2].trim(); + + // Parse value type + if (value === 'true' || value === 'false') { + value = value === 'true'; + } else if (!isNaN(Number(value))) { + value = Number(value); + } else if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } else if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1); + } + + currentSection[key] = value; + } + } + + return result; +} + +/** + * Load config from file + */ +export function loadConfig(configPath: string): RustChainConfig { + const ext = path.extname(configPath).toLowerCase(); + const content = fs.readFileSync(configPath, 'utf-8'); + + switch (ext) { + case '.yaml': + case '.yml': + return parseYaml(content); + case '.json': + return parseJson(content); + case '.toml': + return parseToml(content); + default: + // Try to detect format + if (content.trim().startsWith('{')) { + return parseJson(content); + } else if (content.trim().startsWith('[')) { + return parseToml(content); + } + return parseYaml(content); + } +} + +/** + * Validate config + */ +export function validateConfig(config: RustChainConfig): ValidationResult { + const errors: ValidationError[] = []; + + // Validate node settings + if (config.node) { + if (config.node.port !== undefined && (config.node.port < 1 || config.node.port > 65535)) { + errors.push({ field: 'node.port', message: 'Port must be between 1 and 65535', severity: 'error' }); + } + + if (config.node.host !== undefined && !isValidHost(config.node.host)) { + errors.push({ field: 'node.host', message: 'Invalid host address', severity: 'warning' }); + } + } + + // Validate network settings + if (config.network) { + if (config.network.p2pPort !== undefined && (config.network.p2pPort < 1 || config.network.p2pPort > 65535)) { + errors.push({ field: 'network.p2pPort', message: 'Port must be between 1 and 65535', severity: 'error' }); + } + + if (config.network.maxPeers !== undefined && (config.network.maxPeers < 1 || config.network.maxPeers > 1000)) { + errors.push({ field: 'network.maxPeers', message: 'Max peers should be between 1 and 1000', severity: 'warning' }); + } + } + + // Validate mining settings + if (config.mining) { + if (config.mining.threads !== undefined && (config.mining.threads < 1 || config.mining.threads > 128)) { + errors.push({ field: 'mining.threads', message: 'Threads should be between 1 and 128', severity: 'warning' }); + } + + if (config.mining.fingerprintThreshold !== undefined && (config.mining.fingerprintThreshold < 0 || config.mining.fingerprintThreshold > 100)) { + errors.push({ field: 'mining.fingerprintThreshold', message: 'Fingerprint threshold must be between 0 and 100', severity: 'error' }); + } + } + + // Validate database settings + if (config.database) { + if (config.database.maxSize !== undefined && config.database.maxSize < 1048576) { + errors.push({ field: 'database.maxSize', message: 'Minimum database size is 1MB', severity: 'warning' }); + } + } + + // Validate API settings + if (config.api) { + if (config.api.port !== undefined && (config.api.port < 1 || config.api.port > 65535)) { + errors.push({ field: 'api.port', message: 'Port must be between 1 and 65535', severity: 'error' }); + } + } + + // Validate logging settings + if (config.logging) { + const validLevels = ['debug', 'info', 'warn', 'error']; + if (config.logging.level && !validLevels.includes(config.logging.level)) { + errors.push({ field: 'logging.level', message: `Invalid log level. Must be one of: ${validLevels.join(', ')}`, severity: 'error' }); + } + } + + return { + valid: errors.filter(e => e.severity === 'error').length === 0, + errors, + config, + }; +} + +/** + * Validate host string + */ +function isValidHost(host: string): boolean { + // Check for valid IP or hostname + const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; + const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + + return ipRegex.test(host) || hostnameRegex.test(host); +} + +/** + * Generate config template + */ +export function generateTemplate(format: 'yaml' | 'json' | 'toml' = 'yaml'): string { + const config = getDefaultConfig(); + + switch (format) { + case 'json': + return JSON.stringify(config, null, 2); + case 'toml': + // Simple toml conversion + let toml = ''; + for (const [section, values] of Object.entries(config)) { + toml += `[${section}]\n`; + for (const [key, value] of Object.entries(values as any)) { + if (typeof value === 'string') { + toml += `${key} = "${value}"\n`; + } else if (Array.isArray(value)) { + toml += `${key} = ${JSON.stringify(value)}\n`; + } else { + toml += `${key} = ${value}\n`; + } + } + toml += '\n'; + } + return toml; + default: + // YAML + let yaml = ''; + for (const [section, values] of Object.entries(config)) { + yaml += `${section}:\n`; + for (const [key, value] of Object.entries(values as any)) { + if (typeof value === 'string') { + yaml += ` ${key}: ${value}\n`; + } else if (Array.isArray(value)) { + yaml += ` ${key}:\n`; + for (const item of value) { + yaml += ` - ${item}\n`; + } + } else { + yaml += ` ${key}: ${value}\n`; + } + } + yaml += '\n'; + } + return yaml; + } +} + +// CLI Interface +import { Command } from 'commander'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('rustchain-config') + .description('RustChain Configuration Parser & Validator') + .version('1.0.0'); + +program + .command('validate ') + .description('Validate a RustChain config file') + .action((configFile) => { + console.log(chalk.blue(`\nπŸ” Validating config: ${configFile}\n`)); + + try { + const config = loadConfig(configFile); + const result = validateConfig(config); + + if (result.valid) { + console.log(chalk.green('βœ… Configuration is valid')); + } else { + console.log(chalk.red('❌ Configuration has errors:')); + } + + if (result.errors.length > 0) { + console.log(chalk.yellow('\nIssues found:')); + for (const error of result.errors) { + const icon = error.severity === 'error' ? '❌' : '⚠️'; + console.log(` ${icon} [${error.field}] ${error.message}`); + } + } + console.log(''); + } catch (error: any) { + console.log(chalk.red(`\n❌ Error loading config: ${error.message}\n`)); + } + }); + +program + .command('generate') + .description('Generate default config template') + .option('-f, --format ', 'Output format (yaml, json, toml)', 'yaml') + .option('-o, --output ', 'Output file') + .action((options) => { + const template = generateTemplate(options.format); + + if (options.output) { + fs.writeFileSync(options.output, template); + console.log(chalk.green(`\nβœ… Config template saved to: ${options.output}\n`)); + } else { + console.log(chalk.blue('\nπŸ“„ Default Configuration Template:\n')); + console.log(template); + } + }); + +program + .command('default') + .description('Show default config path') + .action(() => { + console.log(chalk.blue('\nπŸ“ Default config path:')); + console.log(chalk.cyan(getDefaultConfigPath()), '\n'); + }); + +program.parse(); diff --git a/sdk/utils/src/epoch.ts b/sdk/utils/src/epoch.ts new file mode 100644 index 00000000..c2ff798a --- /dev/null +++ b/sdk/utils/src/epoch.ts @@ -0,0 +1,187 @@ +/** + * RustChain Epoch Reward Calculator + * + * Calculate rewards for mining epochs on RustChain blockchain. + * + * Base reward formula considers: + * - Hardware fingerprint score (2.5x for vintage hardware) + * - Block difficulty + * - Epoch duration + */ + +import axios from 'axios'; + +const API_BASE = 'https://rustchain.org'; + +// RustChain epoch parameters +const BASE_REWARD = 1.0; // Base RTC per block +const EPOCH_DURATION_BLOCKS = 1000; +const HARDWARE_BONUS_MULTIPLIER = 2.5; // Max for vintage hardware + +interface EpochInfo { + epoch: number; + startBlock: number; + endBlock: number; + difficulty: number; + totalRewards: number; + minerCount: number; +} + +interface HardwareScore { + clockDrift: number; + cacheTiming: number; + simdIdentity: number; + vmDetection: boolean; + fingerprintScore: number; +} + +/** + * Calculate hardware bonus multiplier based on fingerprint score + */ +export function calculateHardwareBonus(score: number): number { + // Score ranges from 0-100, bonus from 1.0 to 2.5 + return 1.0 + (score / 100) * (HARDWARE_BONUS_MULTIPLIER - 1.0); +} + +/** + * Calculate epoch reward for a miner + */ +export function calculateEpochReward( + blocksMined: number, + hardwareScore: number, + difficulty: number = 1.0 +): number { + const hardwareBonus = calculateHardwareBonus(hardwareScore); + const baseReward = blocksMined * BASE_REWARD; + const difficultyFactor = 1 / difficulty; + + return baseReward * hardwareBonus * difficultyFactor; +} + +/** + * Get current epoch info from API + */ +export async function getCurrentEpoch(): Promise { + try { + const response = await axios.get(`${API_BASE}/epoch`); + return response.data; + } catch (error) { + console.error('Failed to fetch epoch info:', error); + return null; + } +} + +/** + * Estimate time to reach target reward + */ +export function estimateTimeToReward( + hashrate: number, // blocks per hour + hardwareScore: number, + targetReward: number, + difficulty: number = 1.0 +): number { + let accumulatedReward = 0; + let hours = 0; + + while (accumulatedReward < targetReward) { + accumulatedReward += calculateEpochReward(hashrate, hardwareScore, difficulty) / 3600; // per second + hours++; + if (hours > 1000000) break; // Safety limit + } + + return hours; +} + +/** + * Format time duration + */ +export function formatDuration(hours: number): string { + if (hours < 1) { + return `${Math.round(hours * 60)} minutes`; + } else if (hours < 24) { + return `${hours.toFixed(1)} hours`; + } else { + const days = hours / 24; + return `${days.toFixed(1)} days`; + } +} + +// CLI Interface +import { Command } from 'commander'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('rustchain-epoch') + .description('RustChain Epoch Reward Calculator') + .version('1.0.0'); + +program + .command('calculate') + .description('Calculate epoch reward') + .requiredOption('-b, --blocks ', 'Number of blocks mined') + .requiredOption('-s, --score ', 'Hardware fingerprint score (0-100)') + .option('-d, --difficulty ', 'Network difficulty', '1.0') + .action((options) => { + const blocks = parseInt(options.blocks); + const score = parseInt(options.score); + const difficulty = parseFloat(options.difficulty); + + const reward = calculateEpochReward(blocks, score, difficulty); + const bonus = calculateHardwareBonus(score); + + console.log(chalk.blue('\nπŸ“Š Epoch Reward Calculation\n')); + console.log(chalk.cyan('Blocks Mined:'), blocks); + console.log(chalk.cyan('Hardware Score:'), score); + console.log(chalk.cyan('Difficulty:'), difficulty); + console.log(chalk.cyan('Hardware Bonus:'), `${bonus.toFixed(2)}x`); + console.log(chalk.green('\nπŸ’° Estimated Reward:'), `${reward.toFixed(4)} RTC`); + console.log(''); + }); + +program + .command('info') + .description('Get current epoch info') + .action(async () => { + console.log(chalk.blue('\nπŸ“‘ Fetching epoch info...\n')); + const epoch = await getCurrentEpoch(); + + if (epoch) { + console.log(chalk.cyan('Epoch:'), epoch.epoch); + console.log(chalk.cyan('Start Block:'), epoch.startBlock); + console.log(chalk.cyan('End Block:'), epoch.endBlock); + console.log(chalk.cyan('Difficulty:'), epoch.difficulty); + console.log(chalk.cyan('Total Rewards:'), epoch.totalRewards); + console.log(chalk.cyan('Miners:'), epoch.minerCount); + } else { + console.log(chalk.red('Failed to fetch epoch info')); + } + console.log(''); + }); + +program + .command('estimate') + .description('Estimate time to reach target reward') + .requiredOption('-r, --reward ', 'Target reward (RTC)') + .requiredOption('-h, --hashrate ', 'Hashrate (blocks per hour)') + .requiredOption('-s, --score ', 'Hardware fingerprint score (0-100)') + .option('-d, --difficulty ', 'Network difficulty', '1.0') + .action((options) => { + const reward = parseFloat(options.reward); + const hashrate = parseFloat(options.hashrate); + const score = parseInt(options.score); + const difficulty = parseFloat(options.difficulty); + + const hours = estimateTimeToReward(hashrate, score, reward, difficulty); + + console.log(chalk.blue('\n⏱️ Time Estimation\n')); + console.log(chalk.cyan('Target Reward:'), `${reward} RTC`); + console.log(chalk.cyan('Hashrate:'), `${hashrate} blocks/hour`); + console.log(chalk.cyan('Hardware Score:'), score); + console.log(chalk.cyan('Difficulty:'), difficulty); + console.log(chalk.green('\n⏰ Estimated Time:'), formatDuration(hours)); + console.log(''); + }); + +program.parse(); diff --git a/sdk/utils/src/index.ts b/sdk/utils/src/index.ts new file mode 100644 index 00000000..be3f9454 --- /dev/null +++ b/sdk/utils/src/index.ts @@ -0,0 +1,12 @@ +/** + * RustChain Utility Tools + * + * A collection of utilities for RustChain: + * - Epoch Reward Calculator + * - RTC Address Generator & Validator + * - Configuration Parser & Validator + */ + +export * from './epoch'; +export * from './address'; +export * from './config'; diff --git a/sdk/utils/tsconfig.json b/sdk/utils/tsconfig.json new file mode 100644 index 00000000..c952669b --- /dev/null +++ b/sdk/utils/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tools/rustchain-address-validator/.github/workflows/ci.yml b/tools/rustchain-address-validator/.github/workflows/ci.yml new file mode 100644 index 00000000..2d93e015 --- /dev/null +++ b/tools/rustchain-address-validator/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --release + + - name: Run tests + run: cargo test + + - name: Clippy + run: cargo clippy -- -D warnings + + - name: Format check + run: cargo fmt --check diff --git a/tools/rustchain-address-validator/.gitignore b/tools/rustchain-address-validator/.gitignore new file mode 100644 index 00000000..6f43f02b --- /dev/null +++ b/tools/rustchain-address-validator/.gitignore @@ -0,0 +1,13 @@ +# Build artifacts +target/ +Cargo.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/tools/rustchain-address-validator/Cargo.toml b/tools/rustchain-address-validator/Cargo.toml new file mode 100644 index 00000000..8a8b5812 --- /dev/null +++ b/tools/rustchain-address-validator/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rustchain-address-validator" +version = "0.1.0" +edition = "2021" +description = "RTC Address generator and validator for RustChain" +license = "MIT" +authors = ["sososonia-cyber"] +repository = "https://github.com/sososonia-cyber/Rustchain" + +[dependencies] +bs58 = "0.5" +sha2 = "0.10" +ed25519-dalek = { version = "2.1", features = ["rand_core"] } +rand = "0.8" +hex = "0.4" +clap = { version = "4.5", features = ["derive"] } + +[dev-dependencies] + +[[bin]] +name = "rtc-address" +path = "src/main.rs" + +[lib] +name = "rustchain_address" +path = "src/lib.rs" diff --git a/tools/rustchain-address-validator/README.md b/tools/rustchain-address-validator/README.md new file mode 100644 index 00000000..bbc5ecb0 --- /dev/null +++ b/tools/rustchain-address-validator/README.md @@ -0,0 +1,84 @@ +# RustChain Address Validator & Generator + +A Rust CLI tool for generating and validating RTC addresses on the RustChain network. + +## Features + +- Generate new RTC addresses with private keys +- Validate existing RTC addresses +- Derive address from private key + +## Installation + +```bash +cargo install --path . +``` + +Or build and run directly: + +```bash +cargo build --release +./target/release/rtc-address --help +``` + +## Usage + +### Generate a new address + +```bash +rtc-address generate +``` + +Output: +``` +=== Generated RTC Address === + +Address: RTCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +Private Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +IMPORTANT: Save your private securely! + Anyone with your private key can access your funds. +``` + +### Validate an address + +```bash +rtc-address validate RTCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### Derive address from private key + +```bash +rtc-address from-key +``` + +## Development + +### Build + +```bash +cargo build +``` + +### Test + +```bash +cargo test +``` + +### Run + +```bash +cargo run -- generate +``` + +## Bounty + +This tool is submitted for [Bounty #674: Build RustChain Tools & Features in Rust](https://github.com/Scottcjn/rustchain-bounties/issues/674) + +- **Tier**: 1 (Utilities) +- **Target**: RTC address generator + validator + +## License + +MIT diff --git a/tools/rustchain-address-validator/src/lib.rs b/tools/rustchain-address-validator/src/lib.rs new file mode 100644 index 00000000..975336c7 --- /dev/null +++ b/tools/rustchain-address-validator/src/lib.rs @@ -0,0 +1,140 @@ +//! RustChain Address Validator and Generator Library +//! +//! This library provides utilities for validating and generating RTC addresses +//! on the RustChain network. + +use ed25519_dalek::{SigningKey, VerifyingKey}; +use rand::rngs::OsRng; +use sha2::{Digest, Sha256}; + +/// Prefix used for RustChain addresses +pub const ADDRESS_PREFIX: &str = "RTC"; + +/// Length of the address hash (without prefix) +pub const ADDRESS_HASH_LEN: usize = 32; + +/// Validates an RTC address +/// +/// # Arguments +/// * `address` - The address string to validate +/// +/// # Returns +/// * `true` if the address is valid, `false` otherwise +pub fn validate_address(address: &str) -> bool { + if !address.starts_with(ADDRESS_PREFIX) { + return false; + } + + let without_prefix = &address[3..]; + + // Base58 decoded length should be: 32 (pubkey) + 4 (checksum) = 36 + let decoded = match bs58::decode(without_prefix).into_vec() { + Ok(v) => v, + Err(_) => return false, + }; + + if decoded.len() != 36 { + return false; + } + + // Split into pubkey and checksum + let (pubkey, checksum) = decoded.split_at(32); + + // Calculate checksum on version byte + pubkey + let mut payload = vec![0x00]; + payload.extend_from_slice(pubkey); + let calculated_checksum = calculate_checksum(&payload); + + checksum == calculated_checksum.as_slice() +} + +/// Generates a new random RTC address +/// +/// # Returns +/// A tuple of (address, private_key_hex) +pub fn generate_address() -> (String, String) { + let signing_key = SigningKey::generate(&mut OsRng); + let verifying_key: VerifyingKey = (&signing_key).into(); + + // Create payload with version byte + pubkey + let mut payload = vec![0x00]; // Version byte + payload.extend_from_slice(verifying_key.as_bytes()); + + // Calculate checksum on the full payload (version + pubkey) + let checksum = calculate_checksum(&payload); + + // Address is base58 of pubkey + checksum (no version byte in address) + let address_bytes: Vec = verifying_key.as_bytes().iter().cloned().chain(checksum).collect(); + let address = format!("{}{}", ADDRESS_PREFIX, bs58::encode(&address_bytes).into_string()); + + let private_key = hex::encode(signing_key.to_bytes()); + + (address, private_key) +} + +/// Generates address from a private key (hex) +/// +/// # Arguments +/// * `private_key_hex` - The private key as a hex string +/// +/// # Returns +/// The corresponding RTC address +pub fn address_from_private_key(private_key_hex: &str) -> Result { + let key_bytes = hex::decode(private_key_hex).map_err(|e| format!("Invalid hex: {}", e))?; + + if key_bytes.len() != 32 { + return Err("Private key must be 32 bytes".to_string()); + } + + let key_array: [u8; 32] = key_bytes.try_into().map_err(|_| "Invalid key length")?; + let signing_key = SigningKey::from_bytes(&key_array); + let verifying_key: VerifyingKey = (&signing_key).into(); + + // Create payload with version byte + pubkey + let mut payload = vec![0x00]; // Version byte + payload.extend_from_slice(verifying_key.as_bytes()); + + // Calculate checksum on version + pubkey + let checksum = calculate_checksum(&payload); + + // Address is base58 of pubkey + checksum + let address_bytes: Vec = verifying_key.as_bytes().iter().cloned().chain(checksum).collect(); + let address = format!("{}{}", ADDRESS_PREFIX, bs58::encode(&address_bytes).into_string()); + + Ok(address) +} + +/// Calculates checksum for address payload +fn calculate_checksum(payload: &[u8]) -> Vec { + let hash = Sha256::digest(payload); + let hash2 = Sha256::digest(&hash); + hash2[..4].to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_address() { + let (address, private_key) = generate_address(); + assert!(address.starts_with("RTC")); + assert_eq!(private_key.len(), 64); + } + + #[test] + fn test_validate_address() { + let (address, _) = generate_address(); + assert!(validate_address(&address)); + + assert!(!validate_address("INVALID")); + assert!(!validate_address("RTCx")); + } + + #[test] + fn test_round_trip() { + let (address, private_key) = generate_address(); + let derived = address_from_private_key(&private_key).unwrap(); + assert_eq!(address, derived); + } +} diff --git a/tools/rustchain-address-validator/src/main.rs b/tools/rustchain-address-validator/src/main.rs new file mode 100644 index 00000000..15cf49b6 --- /dev/null +++ b/tools/rustchain-address-validator/src/main.rs @@ -0,0 +1,67 @@ +//! RTC Address Tool - CLI for RustChain address operations +//! +//! A command-line tool for generating and validating RTC addresses. + +use clap::{Parser, Subcommand}; +use rustchain_address::{ + address_from_private_key, generate_address, validate_address, +}; + +#[derive(Parser)] +#[command(name = "rtc-address")] +#[command(about = "RustChain Address Generator and Validator", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate a new random RTC address + Generate, + /// Validate an RTC address + Validate { + /// The RTC address to validate + address: String, + }, + /// Derive address from private key + FromKey { + /// Private key in hex format + private_key: String, + }, +} + +fn main() { + let cli = Cli::parse(); + + match cli.command { + Commands::Generate => { + let (address, private_key) = generate_address(); + println!("\n=== Generated RTC Address ===\n"); + println!("Address: {}", address); + println!("Private Key: {}\n", private_key); + println!("IMPORTANT: Save your private key securely!"); + println!(" Anyone with your private key can access your funds.\n"); + } + Commands::Validate { address } => { + if validate_address(&address) { + println!("Valid RTC address: {}", address); + } else { + println!("Invalid RTC address: {}", address); + std::process::exit(1); + } + } + Commands::FromKey { private_key } => { + match address_from_private_key(&private_key) { + Ok(address) => { + println!("\n=== Derived RTC Address ===\n"); + println!("Address: {}\n", address); + } + Err(e) => { + println!("Error: {}", e); + std::process::exit(1); + } + } + } + } +} diff --git a/tools/rustchain-cli/Cargo.lock b/tools/rustchain-cli/Cargo.lock new file mode 100644 index 00000000..de4a9c8a --- /dev/null +++ b/tools/rustchain-cli/Cargo.lock @@ -0,0 +1,1636 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustchain-cli" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tools/rustchain-cli/Cargo.toml b/tools/rustchain-cli/Cargo.toml new file mode 100644 index 00000000..cbe114d4 --- /dev/null +++ b/tools/rustchain-cli/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rustchain-cli" +version = "0.1.0" +edition = "2021" +description = "RustChain CLI - Command line wallet and toolkit" +authors = ["sososonia-cyber"] +license = "MIT" +repository = "https://github.com/sososonia-cyber/Rustchain" + +[dependencies] +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "4.5", features = ["derive"] } +colored = "2.1" +rand = "0.8" + +[dev-dependencies] + +[[bin]] +name = "rustchain" +path = "src/main.rs" + +[profile.release] +opt-level = 3 +lto = true diff --git a/tools/rustchain-cli/README.md b/tools/rustchain-cli/README.md new file mode 100644 index 00000000..c31762b7 --- /dev/null +++ b/tools/rustchain-cli/README.md @@ -0,0 +1,101 @@ +# RustChain CLI + +A Rust-based command-line interface for RustChain blockchain operations. + +## Features + +- Check node health status +- View current epoch information +- List active miners with statistics +- Check wallet balances +- View node statistics +- Generate RTC wallet addresses +- Validate wallet address format +- Verify wallet address on network + +## Installation + +```bash +# Clone the repository +git clone https://github.com/sososonia-cyber/Rustchain.git +cd Rustchain/rustchain-cli + +# Build +cargo build --release + +# Run +./target/release/rustchain +``` + +## Usage + +```bash +# Check node health +rustchain health + +# Get epoch information +rustchain epoch + +# List active miners (top 10) +rustchain miners + +# List specific number of miners +rustchain miners --limit 20 + +# Check wallet balance +rustchain balance my-wallet + +# Get node statistics +rustchain stats + +# Generate a new RTC wallet address +rustchain address generate + +# Generate with custom prefix and length +rustchain address generate --prefix WALLET --length 24 + +# Validate wallet address format +rustchain address validate RTC-mywallet123 + +# Verify address exists on the network +rustchain address verify my-wallet +``` + +## Address Commands + +The `address` subcommand provides wallet address utilities: + +- **generate**: Generate a new RTC wallet address with random identifier +- **validate**: Validate the format of an RTC wallet address +- **verify**: Check if an address exists on the RustChain network + +### Address Format + +RTC wallet addresses follow the format: `PREFIX-identifier` + +- Standard prefixes: RTC, WALLET, NODE, MINER +- Identifier: 3-64 alphanumeric characters, can include - and _ + +Examples: +- `RTC-abc123def456` +- `WALLET-my-wallet` +- `MINER-node-001` + +## Bounty + +This tool was built for the [RustChain Bounty Program](https://github.com/Scottcjn/rustchain-bounties/issues/674): +- **Bounty ID**: #674 +- **Tier**: 1 (Utilities) +- **Features**: CLI wallet, address generator, address validator +- **Reward**: 25-50 RTC + +## API Reference + +- Health: `GET https://rustchain.org/health` +- Miners: `GET https://rustchain.org/api/miners` +- Epoch: `GET https://rustchain.org/epoch` +- Balance: `GET https://rustchain.org/wallet/balance?miner_id={wallet}` + +## License + +MIT diff --git a/tools/rustchain-cli/src/main.rs b/tools/rustchain-cli/src/main.rs new file mode 100644 index 00000000..ae61149b --- /dev/null +++ b/tools/rustchain-cli/src/main.rs @@ -0,0 +1,401 @@ +//! RustChain CLI - Command line wallet and toolkit +//! +//! A Rust implementation of RustChain utilities for the bounty: +//! [BOUNTY: 25-150 RTC] Build RustChain Tools & Features in Rust + +use clap::{Parser, Subcommand}; +use rand::Rng; +use reqwest::Client; +use serde::Deserialize; +use std::collections::HashMap; + +// API Base URL +const API_BASE: &str = "https://rustchain.org"; + +/// Health check response +#[derive(Debug, Deserialize)] +struct HealthResponse { + ok: bool, + version: String, + #[serde(rename = "uptime_s")] + uptime_s: u64, + #[serde(rename = "db_rw")] + db_rw: bool, +} + +/// Epoch info response +#[derive(Debug, Deserialize)] +struct EpochResponse { + epoch: u64, + slot: u64, + #[serde(rename = "blocks_per_epoch")] + blocks_per_epoch: u64, + #[serde(rename = "enrolled_miners")] + enrolled_miners: u64, + #[serde(rename = "epoch_pot")] + epoch_pot: f64, + #[serde(rename = "total_supply_rtc")] + total_supply_rtc: f64, +} + +/// Miner info +#[derive(Debug, Deserialize)] +struct Miner { + miner: String, + #[serde(rename = "antiquity_multiplier")] + antiquity_multiplier: f64, + #[serde(rename = "hardware_type")] + hardware_type: String, + #[serde(rename = "entropy_score")] + entropy_score: f64, +} + +/// Wallet balance response +#[derive(Debug, Deserialize)] +struct BalanceResponse { + #[serde(rename = "miner_id")] + miner_id: String, + #[serde(rename = "amount_rtc")] + amount_rtc: Option, + #[serde(rename = "amount_i64")] + amount_i64: Option, + // For API responses that use different field names + #[serde(default)] + balance: f64, + #[serde(default)] + wallet: String, +} + +/// CLI Arguments +#[derive(Parser)] +#[command(name = "rustchain")] +#[command(about = "RustChain CLI - Wallet and node utilities", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Check node health status + Health, + /// Get current epoch information + Epoch, + /// List active miners + Miners { + /// Number of miners to display (default: 10) + #[arg(short, long, default_value = "10")] + limit: usize, + }, + /// Check wallet balance + Balance { + /// Wallet address to check + wallet: String, + }, + /// Get node statistics + Stats, + /// Generate or validate RTC wallet addresses + Address { + #[command(subcommand)] + action: AddressCommands, + }, +} + +#[derive(Subcommand)] +enum AddressCommands { + /// Generate a new RTC wallet address + Generate { + /// Length of the random part (default: 16) + #[arg(short, long, default_value = "16")] + length: usize, + /// Include prefix (default: RTC) + #[arg(short, long, default_value = "RTC")] + prefix: String, + }, + /// Validate an RTC address format + Validate { + /// Wallet address to validate + address: String, + }, + /// Verify an address exists on the network + Verify { + /// Wallet address to verify + address: String, + }, +} + +/// Get HTTP client +fn get_client() -> Client { + Client::builder() + .danger_accept_invalid_certs(true) + .build() + .expect("Failed to create HTTP client") +} + +/// Print health info +async fn cmd_health(client: &Client) -> Result<(), Box> { + let url = format!("{}/health", API_BASE); + let response = client.get(&url).send().await?; + let health: HealthResponse = response.json().await?; + + println!("\n🟒 RustChain Node Health"); + println!("========================="); + println!("Status: {}", if health.ok { "Healthy βœ“" } else { "Unhealthy βœ—" }); + println!("Version: {}", health.version); + println!("Uptime: {} seconds", health.uptime_s); + println!("Database: {}", if health.db_rw { "Read/Write" } else { "Read-only" }); + + Ok(()) +} + +/// Print epoch info +async fn cmd_epoch(client: &Client) -> Result<(), Box> { + let url = format!("{}/epoch", API_BASE); + let response = client.get(&url).send().await?; + let epoch: EpochResponse = response.json().await?; + + println!("\n⏱️ Current Epoch Information"); + println!("============================="); + println!("Epoch: {}", epoch.epoch); + println!("Slot: {}", epoch.slot); + println!("Blocks/Epoch: {}", epoch.blocks_per_epoch); + println!("Enrolled Miners: {}", epoch.enrolled_miners); + println!("Epoch PoT: {}", epoch.epoch_pot); + + Ok(()) +} + +/// Print miners list +async fn cmd_miners(client: &Client, limit: usize) -> Result<(), Box> { + let url = format!("{}/api/miners", API_BASE); + let response = client.get(&url).send().await?; + let miners: Vec = response.json().await?; + + println!("\n⛏️ Active Miners (Top {})", limit); + println!("{}", "-".repeat(40)); + println!("{:<4} {:<30} {:<15} {:<10}", "#", "Miner", "Hardware", "Multiplier"); + println!("{}", "-".repeat(65)); + + for (i, miner) in miners.iter().take(limit).enumerate() { + let i = i + 1; + let miner_short = if miner.miner.len() > 28 { + format!("{}...", &miner.miner[..25]) + } else { + miner.miner.clone() + }; + println!("{:<4} {:<30} {:<15} {:.2}x", + i, + miner_short, + miner.hardware_type, + miner.antiquity_multiplier + ); + } + + // Statistics + let total = miners.len(); + let multipliers: Vec = miners.iter().map(|m| m.antiquity_multiplier).collect(); + let avg_mult = multipliers.iter().sum::() / total as f64; + + // Hardware distribution + let mut hw_counts: HashMap = HashMap::new(); + for miner in &miners { + *hw_counts.entry(miner.hardware_type.clone()).or_insert(0) += 1; + } + + let mut hw_vec: Vec<_> = hw_counts.iter().collect(); + hw_vec.sort_by(|a, b| b.1.cmp(a.1)); + + println!("\nπŸ“Š Statistics"); + println!("Total Miners: {}", total); + println!("Avg Multiplier: {:.2}x", avg_mult); + println!("\nHardware Distribution:"); + for (hw, count) in hw_vec { + println!(" {}: {}", hw, count); + } + + Ok(()) +} + +/// Print wallet balance +async fn cmd_balance(client: &Client, wallet: &str) -> Result<(), Box> { + let url = format!("{}/api/balance/{}", API_BASE, wallet); + let response = client.get(&url).send().await?; + + if response.status() == 404 { + println!("\n⚠️ Wallet not found or has no balance"); + return Ok(()); + } + + let balance: BalanceResponse = response.json().await?; + + println!("\nπŸ’° Wallet Balance"); + println!("=================="); + println!("Wallet: {}", balance.wallet); + println!("Balance: {:.8} RTC", balance.balance); + + Ok(()) +} + +/// Print node statistics +async fn cmd_stats(client: &Client) -> Result<(), Box> { + // Get epoch + let epoch_url = format!("{}/epoch", API_BASE); + let epoch_response = client.get(&epoch_url).send().await?; + let epoch: EpochResponse = epoch_response.json().await?; + + // Get miners + let miners_url = format!("{}/api/miners", API_BASE); + let miners_response = client.get(&miners_url).send().await?; + let miners: Vec = miners_response.json().await?; + + println!("\nπŸ“ˆ RustChain Node Statistics"); + println!("============================"); + println!("Current Epoch: {}", epoch.epoch); + println!("Current Slot: {}", epoch.slot); + println!("Blocks/Epoch: {}", epoch.blocks_per_epoch); + println!("Enrolled Miners: {}", epoch.enrolled_miners); + println!("Total Miners: {}", miners.len()); + println!("Epoch PoT: {}", epoch.epoch_pot); + + // Calculate total score + let total_score: f64 = miners.iter().map(|m| m.entropy_score).sum(); + println!("Total Network Score: {:.2}", total_score); + + Ok(()) +} + +/// Generate a random RTC wallet address +fn cmd_address_generate(length: usize, prefix: &str) -> Result<(), Box> { + let mut rng = rand::thread_rng(); + let chars: Vec = "abcdefghijklmnopqrstuvwxyz0123456789-_".chars().collect(); + let random_part: String = (0..length) + .map(|_| chars[rng.gen_range(0..chars.len())]) + .collect(); + + let address = format!("{}-{}", prefix.to_uppercase(), random_part); + + println!("\nοΏ½ Generated RTC Wallet Address"); + println!("==============================="); + println!("Address: {}", address); + println!("Prefix: {}", prefix.to_uppercase()); + println!("Length: {} characters", length); + println!("\n⚠️ IMPORTANT: Save this address securely!"); + println!(" This is your wallet identifier on the RustChain network."); + + Ok(()) +} + +/// Validate RTC address format +fn cmd_address_validate(address: &str) -> Result<(), Box> { + let parts: Vec<&str> = address.split('-').collect(); + + println!("\nπŸ” RTC Address Validation"); + println!("==========================="); + println!("Address: {}", address); + + // Check format + if parts.len() < 2 { + println!("❌ Invalid: Address must contain a prefix and identifier separated by '-'"); + println!(" Expected format: PREFIX-identifier (e.g., RTC-abc123)"); + return Ok(()); + } + + let prefix = parts[0]; + let identifier = parts[1..].join("-"); + + // Validate prefix + let valid_prefixes = ["RTC", "WALLET", "NODE", "MINER"]; + let prefix_upper = prefix.to_uppercase(); + + if !valid_prefixes.contains(&prefix_upper.as_str()) { + println!("⚠️ Warning: Prefix '{}' is not standard.", prefix); + println!(" Standard prefixes: {}", valid_prefixes.join(", ")); + } else { + println!("βœ“ Prefix: {} (valid)", prefix_upper); + } + + // Validate identifier + let valid_chars = identifier.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); + + if identifier.len() < 3 { + println!("❌ Invalid: Identifier too short (minimum 3 characters)"); + } else if identifier.len() > 64 { + println!("❌ Invalid: Identifier too long (maximum 64 characters)"); + } else if !valid_chars { + println!("❌ Invalid: Identifier contains invalid characters"); + } else { + println!("βœ“ Identifier: {} (valid)", identifier); + } + + // Overall result + let is_valid = parts.len() >= 2 && identifier.len() >= 3 && identifier.len() <= 64 && valid_chars; + + println!("\nResult: {}", if is_valid { "βœ… VALID" } else { "❌ INVALID" }); + + Ok(()) +} + +/// Verify address exists on the network +async fn cmd_address_verify(client: &Client, address: &str) -> Result<(), Box> { + let url = format!("{}/wallet/balance?miner_id={}", API_BASE, address); + let response = client.get(&url).send().await?; + + println!("\nπŸ”— Network Address Verification"); + println!("==================================="); + println!("Address: {}", address); + println!("Network: {}", API_BASE); + + if response.status() == 404 { + println!("\n⚠️ Address not found on network"); + println!(" The address may not be registered yet."); + } else if response.status() == 200 { + let balance: BalanceResponse = response.json().await?; + let amount = balance.amount_rtc.unwrap_or(balance.balance); + println!("\nβœ… Address verified on network!"); + println!(" Miner ID: {}", balance.miner_id); + println!(" Balance: {:.8} RTC", amount); + } else { + println!("\n❌ Error: Unexpected response status: {}", response.status()); + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + let client = get_client(); + + match cli.command { + Commands::Health => { + cmd_health(&client).await?; + } + Commands::Epoch => { + cmd_epoch(&client).await?; + } + Commands::Miners { limit } => { + cmd_miners(&client, limit).await?; + } + Commands::Balance { wallet } => { + cmd_balance(&client, &wallet).await?; + } + Commands::Stats => { + cmd_stats(&client).await?; + } + Commands::Address { action } => { + match action { + AddressCommands::Generate { length, prefix } => { + cmd_address_generate(length, &prefix)?; + } + AddressCommands::Validate { address } => { + cmd_address_validate(&address)?; + } + AddressCommands::Verify { address } => { + cmd_address_verify(&client, &address).await?; + } + } + } + } + + Ok(()) +}