hypercli-cli 0.7.12__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/PKG-INFO +3 -3
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/cli.py +2 -1
- hypercli_cli-0.8.1/hypercli_cli/keys.py +83 -0
- hypercli_cli-0.8.1/hypercli_cli/wallet.py +519 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/pyproject.toml +3 -3
- hypercli_cli-0.7.12/hypercli_cli/wallet.py +0 -268
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/.gitignore +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/README.md +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/__init__.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/claw.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/llm.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/output.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-0.7.12 → hypercli_cli-0.8.1}/hypercli_cli/user.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: CLI for HyperCLI - GPU orchestration and LLM API
|
|
5
5
|
Project-URL: Homepage, https://hypercli.com
|
|
6
6
|
Project-URL: Documentation, https://docs.hypercli.com
|
|
@@ -21,7 +21,7 @@ Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
|
|
|
21
21
|
Requires-Dist: eth-account>=0.13.0; extra == 'all'
|
|
22
22
|
Requires-Dist: hypercli-sdk[comfyui]>=0.7.1; extra == 'all'
|
|
23
23
|
Requires-Dist: web3>=7.0.0; extra == 'all'
|
|
24
|
-
Requires-Dist: x402[httpx]>=2.0.0; extra == 'all'
|
|
24
|
+
Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
|
|
25
25
|
Provides-Extra: comfyui
|
|
26
26
|
Requires-Dist: hypercli-sdk[comfyui]>=0.7.1; extra == 'comfyui'
|
|
27
27
|
Provides-Extra: dev
|
|
@@ -32,7 +32,7 @@ Requires-Dist: argon2-cffi>=25.0.0; extra == 'wallet'
|
|
|
32
32
|
Requires-Dist: eth-account>=0.13.0; extra == 'wallet'
|
|
33
33
|
Requires-Dist: qrcode[pil]>=7.4.0; extra == 'wallet'
|
|
34
34
|
Requires-Dist: web3>=7.0.0; extra == 'wallet'
|
|
35
|
-
Requires-Dist: x402[httpx]>=2.0.0; extra == 'wallet'
|
|
35
|
+
Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'wallet'
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
|
|
38
38
|
# HyperCLI CLI
|
|
@@ -7,7 +7,7 @@ from rich.prompt import Prompt
|
|
|
7
7
|
from hypercli import HyperCLI, APIError, configure
|
|
8
8
|
from hypercli.config import CONFIG_FILE
|
|
9
9
|
|
|
10
|
-
from . import billing, claw, comfyui, flow, instances, jobs, llm, renders, user, wallet
|
|
10
|
+
from . import billing, claw, comfyui, flow, instances, jobs, keys, llm, renders, user, wallet
|
|
11
11
|
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
@@ -61,6 +61,7 @@ app.add_typer(claw.app, name="claw")
|
|
|
61
61
|
app.add_typer(comfyui.app, name="comfyui")
|
|
62
62
|
app.add_typer(flow.app, name="flow")
|
|
63
63
|
app.add_typer(instances.app, name="instances")
|
|
64
|
+
app.add_typer(keys.app, name="keys")
|
|
64
65
|
app.add_typer(jobs.app, name="jobs")
|
|
65
66
|
app.add_typer(llm.app, name="llm")
|
|
66
67
|
app.add_typer(renders.app, name="renders")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""API Key management CLI commands"""
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="API key management")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _fmt_ts(ts) -> str:
|
|
11
|
+
"""Format a timestamp (epoch float or ISO string) for display."""
|
|
12
|
+
if not ts:
|
|
13
|
+
return ""
|
|
14
|
+
if isinstance(ts, (int, float)):
|
|
15
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
|
|
16
|
+
return str(ts)[:16]
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_client():
|
|
21
|
+
from hypercli import HyperCLI
|
|
22
|
+
return HyperCLI()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("create")
|
|
26
|
+
def create_key(name: str = typer.Option("default", help="Key name")):
|
|
27
|
+
"""Create a new API key"""
|
|
28
|
+
client = _get_client()
|
|
29
|
+
key = client.keys.create(name=name)
|
|
30
|
+
console.print(f"\n[bold green]API key created![/bold green]\n")
|
|
31
|
+
console.print(f" Key ID: {key.key_id}")
|
|
32
|
+
console.print(f" Name: {key.name}")
|
|
33
|
+
console.print(f" API Key: [bold]{key.api_key}[/bold]")
|
|
34
|
+
console.print(f"\n[yellow]⚠ Save this key now — it won't be shown again.[/yellow]\n")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("list")
|
|
38
|
+
def list_keys():
|
|
39
|
+
"""List all API keys"""
|
|
40
|
+
client = _get_client()
|
|
41
|
+
keys = client.keys.list()
|
|
42
|
+
|
|
43
|
+
if not keys:
|
|
44
|
+
console.print("[dim]No API keys found.[/dim]")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
table = Table(title="API Keys")
|
|
48
|
+
table.add_column("Key ID", style="dim")
|
|
49
|
+
table.add_column("Name")
|
|
50
|
+
table.add_column("Key Preview")
|
|
51
|
+
table.add_column("Active", justify="center")
|
|
52
|
+
table.add_column("Created")
|
|
53
|
+
table.add_column("Last Used")
|
|
54
|
+
|
|
55
|
+
for key in keys:
|
|
56
|
+
active = "✓" if key.is_active else "✗"
|
|
57
|
+
active_style = "green" if key.is_active else "red"
|
|
58
|
+
table.add_row(
|
|
59
|
+
key.key_id[:8] + "...",
|
|
60
|
+
key.name or "",
|
|
61
|
+
key.api_key_preview or "",
|
|
62
|
+
f"[{active_style}]{active}[/{active_style}]",
|
|
63
|
+
_fmt_ts(key.created_at),
|
|
64
|
+
_fmt_ts(key.last_used_at) or "never",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
console.print(table)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("disable")
|
|
71
|
+
def disable_key(
|
|
72
|
+
key_id: str = typer.Argument(help="Key ID to disable"),
|
|
73
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
74
|
+
):
|
|
75
|
+
"""Disable an API key (irreversible)"""
|
|
76
|
+
if not yes:
|
|
77
|
+
confirm = typer.confirm(f"Disable key {key_id[:8]}...? This cannot be undone")
|
|
78
|
+
if not confirm:
|
|
79
|
+
raise typer.Abort()
|
|
80
|
+
|
|
81
|
+
client = _get_client()
|
|
82
|
+
result = client.keys.disable(key_id)
|
|
83
|
+
console.print(f"[red]Key {key_id[:8]}... disabled.[/red]")
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""Wallet management commands for HyperCLI"""
|
|
2
|
+
import json
|
|
3
|
+
import getpass
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Wallet management commands")
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
# Check if wallet dependencies are available
|
|
14
|
+
try:
|
|
15
|
+
from web3 import Web3
|
|
16
|
+
from eth_account import Account
|
|
17
|
+
WALLET_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
WALLET_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
WALLET_DIR = Path.home() / ".hypercli"
|
|
22
|
+
WALLET_PATH = WALLET_DIR / "wallet.json"
|
|
23
|
+
BASE_RPC = "https://mainnet.base.org"
|
|
24
|
+
USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def require_wallet_deps():
|
|
28
|
+
"""Check if wallet dependencies are installed"""
|
|
29
|
+
if not WALLET_AVAILABLE:
|
|
30
|
+
console.print("[red]❌ Wallet commands require crypto dependencies[/red]")
|
|
31
|
+
console.print("\nInstall with:")
|
|
32
|
+
console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("create")
|
|
37
|
+
def create():
|
|
38
|
+
"""Create a new wallet with encrypted keystore"""
|
|
39
|
+
require_wallet_deps()
|
|
40
|
+
|
|
41
|
+
if WALLET_PATH.exists():
|
|
42
|
+
console.print(f"[yellow]Wallet already exists at {WALLET_PATH}[/yellow]")
|
|
43
|
+
overwrite = typer.confirm("Overwrite existing wallet?", default=False)
|
|
44
|
+
if not overwrite:
|
|
45
|
+
console.print("Cancelled.")
|
|
46
|
+
raise typer.Exit(0)
|
|
47
|
+
|
|
48
|
+
console.print("\n[bold]Creating new Ethereum wallet...[/bold]\n")
|
|
49
|
+
|
|
50
|
+
# Generate random account
|
|
51
|
+
account = Account.create()
|
|
52
|
+
|
|
53
|
+
console.print(f"[green]✓[/green] Wallet address: [bold]{account.address}[/bold]")
|
|
54
|
+
|
|
55
|
+
# Get passphrase (check env first)
|
|
56
|
+
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
57
|
+
if passphrase_env:
|
|
58
|
+
passphrase = passphrase_env
|
|
59
|
+
console.print("[dim]Using passphrase from HYPERCLI_WALLET_PASSPHRASE[/dim]")
|
|
60
|
+
else:
|
|
61
|
+
passphrase = getpass.getpass("Set keystore passphrase: ")
|
|
62
|
+
confirm = getpass.getpass("Confirm passphrase: ")
|
|
63
|
+
|
|
64
|
+
if passphrase != confirm:
|
|
65
|
+
console.print("[red]❌ Passphrases don't match![/red]")
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
# Encrypt and save keystore
|
|
69
|
+
keystore = account.encrypt(passphrase)
|
|
70
|
+
|
|
71
|
+
WALLET_DIR.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
with open(WALLET_PATH, "w") as f:
|
|
73
|
+
json.dump(keystore, f, indent=2)
|
|
74
|
+
|
|
75
|
+
# Set restrictive permissions
|
|
76
|
+
os.chmod(WALLET_PATH, 0o600)
|
|
77
|
+
|
|
78
|
+
console.print(f"\n[green]✓[/green] Keystore saved to [bold]{WALLET_PATH}[/bold]")
|
|
79
|
+
console.print(f"[green]✓[/green] Address: [bold]{account.address}[/bold]")
|
|
80
|
+
console.print("\n[yellow]⚠️ Fund this address with USDC on Base to use for payments![/yellow]")
|
|
81
|
+
|
|
82
|
+
# Show QR code for easy mobile scanning
|
|
83
|
+
try:
|
|
84
|
+
import qrcode
|
|
85
|
+
console.print("\n[bold]Scan to send USDC:[/bold]\n")
|
|
86
|
+
qr_obj = qrcode.QRCode(
|
|
87
|
+
version=1,
|
|
88
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
89
|
+
box_size=1,
|
|
90
|
+
border=1,
|
|
91
|
+
)
|
|
92
|
+
qr_obj.add_data(account.address)
|
|
93
|
+
qr_obj.make(fit=True)
|
|
94
|
+
qr_obj.print_ascii(invert=True)
|
|
95
|
+
except ImportError:
|
|
96
|
+
console.print("\n[dim]Tip: run 'hyper wallet qr' to display address as QR code[/dim]")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command("address")
|
|
100
|
+
def address():
|
|
101
|
+
"""Show wallet address"""
|
|
102
|
+
require_wallet_deps()
|
|
103
|
+
|
|
104
|
+
if not WALLET_PATH.exists():
|
|
105
|
+
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
106
|
+
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
with open(WALLET_PATH) as f:
|
|
110
|
+
keystore = json.load(f)
|
|
111
|
+
|
|
112
|
+
addr = keystore.get("address", "unknown")
|
|
113
|
+
console.print(f"\n[bold]Wallet address:[/bold] 0x{addr}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command("qr")
|
|
117
|
+
def qr(
|
|
118
|
+
output: str = typer.Option(None, "--output", "-o", help="Save QR code as PNG file"),
|
|
119
|
+
):
|
|
120
|
+
"""Display wallet address as QR code (for easy mobile scanning)"""
|
|
121
|
+
require_wallet_deps()
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
import qrcode
|
|
125
|
+
except ImportError:
|
|
126
|
+
console.print("[red]❌ QR code support requires the qrcode package[/red]")
|
|
127
|
+
console.print("\nInstall with:")
|
|
128
|
+
console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
|
|
131
|
+
if not WALLET_PATH.exists():
|
|
132
|
+
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
133
|
+
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
with open(WALLET_PATH) as f:
|
|
137
|
+
keystore = json.load(f)
|
|
138
|
+
|
|
139
|
+
addr = "0x" + keystore.get("address", "")
|
|
140
|
+
|
|
141
|
+
if output:
|
|
142
|
+
# Save as PNG
|
|
143
|
+
try:
|
|
144
|
+
qr_obj = qrcode.QRCode(
|
|
145
|
+
version=1,
|
|
146
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
147
|
+
box_size=10,
|
|
148
|
+
border=2,
|
|
149
|
+
)
|
|
150
|
+
qr_obj.add_data(addr)
|
|
151
|
+
qr_obj.make(fit=True)
|
|
152
|
+
|
|
153
|
+
img = qr_obj.make_image(fill_color="black", back_color="white")
|
|
154
|
+
|
|
155
|
+
# Ensure .png extension
|
|
156
|
+
if not output.lower().endswith('.png'):
|
|
157
|
+
output = output + '.png'
|
|
158
|
+
|
|
159
|
+
img.save(output)
|
|
160
|
+
console.print(f"[green]✓[/green] QR code saved to [bold]{output}[/bold]")
|
|
161
|
+
console.print(f"[dim]Address: {addr}[/dim]")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
console.print(f"[red]❌ Failed to save PNG: {e}[/red]")
|
|
164
|
+
console.print("[dim]Try: pip install pillow[/dim]")
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
else:
|
|
167
|
+
# Print ASCII to terminal
|
|
168
|
+
console.print(f"\n[bold]Wallet Address:[/bold] {addr}")
|
|
169
|
+
console.print("[dim]Scan to send USDC on Base network[/dim]\n")
|
|
170
|
+
|
|
171
|
+
qr_obj = qrcode.QRCode(
|
|
172
|
+
version=1,
|
|
173
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
174
|
+
box_size=1,
|
|
175
|
+
border=1,
|
|
176
|
+
)
|
|
177
|
+
qr_obj.add_data(addr)
|
|
178
|
+
qr_obj.make(fit=True)
|
|
179
|
+
qr_obj.print_ascii(invert=True)
|
|
180
|
+
|
|
181
|
+
console.print(f"\n[bold]{addr}[/bold]")
|
|
182
|
+
console.print("\n[dim]Tip: use --output wallet.png to save as image[/dim]")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("balance")
|
|
186
|
+
def balance():
|
|
187
|
+
"""Check USDC balance on Base"""
|
|
188
|
+
require_wallet_deps()
|
|
189
|
+
|
|
190
|
+
if not WALLET_PATH.exists():
|
|
191
|
+
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
192
|
+
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
193
|
+
raise typer.Exit(1)
|
|
194
|
+
|
|
195
|
+
with open(WALLET_PATH) as f:
|
|
196
|
+
keystore = json.load(f)
|
|
197
|
+
|
|
198
|
+
# Get passphrase
|
|
199
|
+
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
200
|
+
if passphrase_env:
|
|
201
|
+
passphrase = passphrase_env
|
|
202
|
+
else:
|
|
203
|
+
passphrase = getpass.getpass("Unlock keystore passphrase: ")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
private_key = Account.decrypt(keystore, passphrase)
|
|
207
|
+
account = Account.from_key(private_key)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
|
|
212
|
+
console.print(f"\n[bold]Checking USDC balance on Base...[/bold]\n")
|
|
213
|
+
|
|
214
|
+
# Connect to Base
|
|
215
|
+
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
|
|
216
|
+
|
|
217
|
+
# USDC ERC20 ABI (minimal - just balanceOf)
|
|
218
|
+
usdc_abi = [
|
|
219
|
+
{
|
|
220
|
+
"constant": True,
|
|
221
|
+
"inputs": [{"name": "_owner", "type": "address"}],
|
|
222
|
+
"name": "balanceOf",
|
|
223
|
+
"outputs": [{"name": "balance", "type": "uint256"}],
|
|
224
|
+
"type": "function",
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
|
|
229
|
+
|
|
230
|
+
# Get balance (USDC has 6 decimals)
|
|
231
|
+
balance_raw = usdc.functions.balanceOf(account.address).call()
|
|
232
|
+
balance_usdc = balance_raw / 1_000_000
|
|
233
|
+
|
|
234
|
+
console.print(f"Wallet: [bold]{account.address}[/bold]")
|
|
235
|
+
console.print(f"USDC Balance: [bold green]{balance_usdc:.6f}[/bold green]")
|
|
236
|
+
|
|
237
|
+
if balance_usdc == 0:
|
|
238
|
+
console.print("\n[yellow]⚠️ No USDC! Send some to this address on Base network.[/yellow]")
|
|
239
|
+
else:
|
|
240
|
+
console.print(f"\n[green]✓[/green] You have [bold]{balance_usdc:.2f} USDC[/bold]")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command("topup")
|
|
244
|
+
def topup(
|
|
245
|
+
amount: str = typer.Argument(help="Amount in USDC to top up (max 6 decimals)"),
|
|
246
|
+
api_url: str = typer.Option(None, help="API URL override"),
|
|
247
|
+
):
|
|
248
|
+
"""Top up account balance via Orchestra x402 endpoint.
|
|
249
|
+
|
|
250
|
+
Flow:
|
|
251
|
+
1) Resolve user_id via GET /api/user using your existing API key
|
|
252
|
+
2) POST /api/x402/top_up with {user_id, amount}
|
|
253
|
+
3) Handle 402 and retry with x402 payment headers
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
hyper wallet topup 10
|
|
257
|
+
hyper wallet topup 25.50
|
|
258
|
+
"""
|
|
259
|
+
require_wallet_deps()
|
|
260
|
+
from decimal import Decimal, InvalidOperation
|
|
261
|
+
import httpx
|
|
262
|
+
from hypercli.config import get_api_key, get_api_url
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
from x402 import x402ClientSync
|
|
266
|
+
from x402.http import x402HTTPClientSync
|
|
267
|
+
from x402.mechanisms.evm import EthAccountSigner
|
|
268
|
+
from x402.mechanisms.evm.exact.register import register_exact_evm_client
|
|
269
|
+
except ImportError:
|
|
270
|
+
console.print("[red]❌ x402 payment requires wallet/x402 dependencies[/red]")
|
|
271
|
+
console.print("\nInstall with:")
|
|
272
|
+
console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
|
|
273
|
+
raise typer.Exit(1)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
amount_dec = Decimal(amount)
|
|
277
|
+
except InvalidOperation:
|
|
278
|
+
console.print(f"[red]❌ Invalid amount: {amount}[/red]")
|
|
279
|
+
raise typer.Exit(1)
|
|
280
|
+
|
|
281
|
+
if amount_dec <= 0:
|
|
282
|
+
console.print("[red]❌ Amount must be greater than 0[/red]")
|
|
283
|
+
raise typer.Exit(1)
|
|
284
|
+
if amount_dec.as_tuple().exponent < -6:
|
|
285
|
+
console.print("[red]❌ Amount supports at most 6 decimals[/red]")
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
amount_atomic = int(amount_dec * Decimal("1000000"))
|
|
289
|
+
|
|
290
|
+
# Step 1: Load wallet
|
|
291
|
+
account = load_wallet()
|
|
292
|
+
console.print(f"[green]✓[/green] Wallet: {account.address}")
|
|
293
|
+
|
|
294
|
+
# Step 2: Check USDC balance
|
|
295
|
+
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
|
|
296
|
+
usdc_abi = [
|
|
297
|
+
{
|
|
298
|
+
"constant": True,
|
|
299
|
+
"inputs": [{"name": "_owner", "type": "address"}],
|
|
300
|
+
"name": "balanceOf",
|
|
301
|
+
"outputs": [{"name": "balance", "type": "uint256"}],
|
|
302
|
+
"type": "function",
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
|
|
306
|
+
balance_raw = usdc.functions.balanceOf(account.address).call()
|
|
307
|
+
balance_usdc = Decimal(balance_raw) / Decimal("1000000")
|
|
308
|
+
|
|
309
|
+
console.print(f"[green]✓[/green] Balance: {balance_usdc:.6f} USDC")
|
|
310
|
+
|
|
311
|
+
if balance_raw < amount_atomic:
|
|
312
|
+
console.print(
|
|
313
|
+
f"\n[red]❌ Insufficient balance: {balance_usdc:.6f} < {amount_dec:.6f} USDC[/red]"
|
|
314
|
+
)
|
|
315
|
+
console.print(f"Send USDC on Base to: [bold]{account.address}[/bold]")
|
|
316
|
+
raise typer.Exit(1)
|
|
317
|
+
|
|
318
|
+
# Step 3: Set up x402 v2 client
|
|
319
|
+
api_key = get_api_key()
|
|
320
|
+
if not api_key:
|
|
321
|
+
console.print("[red]❌ API key required for top-up[/red]")
|
|
322
|
+
console.print(
|
|
323
|
+
"Set it with: [bold]hyper configure[/bold] or [bold]hyper wallet login[/bold]"
|
|
324
|
+
)
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
|
|
327
|
+
base_url = (api_url or get_api_url()).rstrip("/")
|
|
328
|
+
user_endpoint = f"{base_url}/api/user"
|
|
329
|
+
topup_endpoint = f"{base_url}/api/x402/top_up"
|
|
330
|
+
auth_headers = {
|
|
331
|
+
"Authorization": f"Bearer {api_key}",
|
|
332
|
+
"Content-Type": "application/json",
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
signer = EthAccountSigner(account)
|
|
336
|
+
x402_client = x402ClientSync()
|
|
337
|
+
register_exact_evm_client(x402_client, signer)
|
|
338
|
+
http_client = x402HTTPClientSync(x402_client)
|
|
339
|
+
|
|
340
|
+
with httpx.Client(timeout=30) as client:
|
|
341
|
+
# Step 4: Resolve user_id from API key
|
|
342
|
+
console.print("\n[bold]Resolving user...[/bold]")
|
|
343
|
+
user_resp = client.get(user_endpoint, headers=auth_headers)
|
|
344
|
+
if user_resp.status_code != 200:
|
|
345
|
+
console.print(
|
|
346
|
+
f"[red]❌ Failed to get user: {user_resp.status_code} {user_resp.text}[/red]"
|
|
347
|
+
)
|
|
348
|
+
raise typer.Exit(1)
|
|
349
|
+
|
|
350
|
+
user_id = user_resp.json().get("user_id")
|
|
351
|
+
if not user_id:
|
|
352
|
+
console.print("[red]❌ /api/user response missing user_id[/red]")
|
|
353
|
+
raise typer.Exit(1)
|
|
354
|
+
|
|
355
|
+
payload = {"user_id": user_id, "amount": float(amount_dec)}
|
|
356
|
+
|
|
357
|
+
# Step 5: Request top-up (expect 402 then retry with payment)
|
|
358
|
+
console.print(f"[bold]Requesting top-up of ${amount_dec:.2f}...[/bold]")
|
|
359
|
+
resp = client.post(topup_endpoint, headers=auth_headers, json=payload)
|
|
360
|
+
|
|
361
|
+
if resp.status_code == 402:
|
|
362
|
+
console.print("[bold]Signing x402 payment...[/bold]")
|
|
363
|
+
try:
|
|
364
|
+
payment_headers, _ = http_client.handle_402_response(
|
|
365
|
+
dict(resp.headers), resp.content
|
|
366
|
+
)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
console.print(f"[red]❌ Failed to build payment header: {e}[/red]")
|
|
369
|
+
raise typer.Exit(1)
|
|
370
|
+
|
|
371
|
+
console.print("[bold]Submitting payment...[/bold]")
|
|
372
|
+
retry_headers = {**auth_headers, **payment_headers}
|
|
373
|
+
retry_headers["Access-Control-Expose-Headers"] = "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
|
|
374
|
+
resp = client.post(topup_endpoint, headers=retry_headers, json=payload)
|
|
375
|
+
|
|
376
|
+
if resp.status_code != 200:
|
|
377
|
+
console.print(f"[red]❌ Top-up failed: {resp.status_code} {resp.text}[/red]")
|
|
378
|
+
try:
|
|
379
|
+
settle = http_client.get_payment_settle_response(lambda name: resp.headers.get(name))
|
|
380
|
+
if getattr(settle, "error_reason", None):
|
|
381
|
+
console.print(f"[red]❌ Payment error: {settle.error_reason}[/red]")
|
|
382
|
+
except Exception:
|
|
383
|
+
pass
|
|
384
|
+
raise typer.Exit(1)
|
|
385
|
+
|
|
386
|
+
result = resp.json()
|
|
387
|
+
|
|
388
|
+
credited = result.get("amount", float(amount_dec))
|
|
389
|
+
wallet = result.get("wallet", account.address)
|
|
390
|
+
tx_id = result.get("transaction_id", "")
|
|
391
|
+
msg = result.get("message", "Top-up successful")
|
|
392
|
+
|
|
393
|
+
console.print(f"\n[bold green]✓ Top-up successful![/bold green]\n")
|
|
394
|
+
console.print(f" User: {result.get('user_id', 'N/A')}")
|
|
395
|
+
console.print(f" Credited: ${credited} USDC")
|
|
396
|
+
console.print(f" Wallet: {wallet}")
|
|
397
|
+
console.print(f" Tx ID: {tx_id or 'N/A'}")
|
|
398
|
+
console.print(f" Message: {msg}")
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
settle = http_client.get_payment_settle_response(lambda name: resp.headers.get(name))
|
|
402
|
+
if getattr(settle, "transaction", None):
|
|
403
|
+
console.print(f" On-chain: {settle.transaction}")
|
|
404
|
+
if getattr(settle, "network", None):
|
|
405
|
+
console.print(f" Network: {settle.network}")
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
console.print()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@app.command("login")
|
|
413
|
+
def wallet_login(
|
|
414
|
+
name: str = typer.Option("cli", help="Name for the generated API key"),
|
|
415
|
+
api_url: str = typer.Option(None, help="API URL override"),
|
|
416
|
+
):
|
|
417
|
+
"""Login with wallet signature, create an API key, and save it.
|
|
418
|
+
|
|
419
|
+
This is the recommended way to set up HyperCLI from scratch:
|
|
420
|
+
1. hyper wallet create
|
|
421
|
+
2. hyper wallet login
|
|
422
|
+
"""
|
|
423
|
+
require_wallet_deps()
|
|
424
|
+
from eth_account.messages import encode_defunct
|
|
425
|
+
import httpx
|
|
426
|
+
from hypercli.config import get_api_url, configure
|
|
427
|
+
|
|
428
|
+
base_url = (api_url or get_api_url()).rstrip("/")
|
|
429
|
+
|
|
430
|
+
# Step 1: Load wallet
|
|
431
|
+
account = load_wallet()
|
|
432
|
+
console.print(f"[green]✓[/green] Wallet: {account.address}\n")
|
|
433
|
+
|
|
434
|
+
# Step 2: Get challenge
|
|
435
|
+
console.print("[bold]Requesting login challenge...[/bold]")
|
|
436
|
+
with httpx.Client(timeout=15) as client:
|
|
437
|
+
resp = client.post(
|
|
438
|
+
f"{base_url}/api/auth/wallet/challenge",
|
|
439
|
+
json={"wallet": account.address},
|
|
440
|
+
)
|
|
441
|
+
if resp.status_code != 200:
|
|
442
|
+
console.print(f"[red]❌ Challenge failed: {resp.text}[/red]")
|
|
443
|
+
raise typer.Exit(1)
|
|
444
|
+
challenge = resp.json()
|
|
445
|
+
|
|
446
|
+
# Step 3: Sign the challenge
|
|
447
|
+
console.print("[bold]Signing challenge...[/bold]")
|
|
448
|
+
message = encode_defunct(text=challenge["message"])
|
|
449
|
+
signed = account.sign_message(message)
|
|
450
|
+
|
|
451
|
+
# Step 4: Login
|
|
452
|
+
console.print("[bold]Logging in...[/bold]")
|
|
453
|
+
with httpx.Client(timeout=15) as client:
|
|
454
|
+
resp = client.post(
|
|
455
|
+
f"{base_url}/api/auth/wallet/login",
|
|
456
|
+
json={
|
|
457
|
+
"wallet": account.address,
|
|
458
|
+
"signature": signed.signature.hex(),
|
|
459
|
+
"timestamp": challenge["timestamp"],
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
if resp.status_code != 200:
|
|
463
|
+
console.print(f"[red]❌ Login failed: {resp.text}[/red]")
|
|
464
|
+
raise typer.Exit(1)
|
|
465
|
+
jwt_token = resp.json()["token"]
|
|
466
|
+
|
|
467
|
+
console.print("[green]✓[/green] Authenticated\n")
|
|
468
|
+
|
|
469
|
+
# Step 5: Create API key using JWT
|
|
470
|
+
console.print(f"[bold]Creating API key '{name}'...[/bold]")
|
|
471
|
+
with httpx.Client(timeout=15) as client:
|
|
472
|
+
resp = client.post(
|
|
473
|
+
f"{base_url}/api/keys",
|
|
474
|
+
json={"name": name},
|
|
475
|
+
headers={"Authorization": f"Bearer {jwt_token}"},
|
|
476
|
+
)
|
|
477
|
+
if resp.status_code != 200:
|
|
478
|
+
console.print(f"[red]❌ Key creation failed: {resp.text}[/red]")
|
|
479
|
+
raise typer.Exit(1)
|
|
480
|
+
key_data = resp.json()
|
|
481
|
+
|
|
482
|
+
api_key = key_data["api_key"]
|
|
483
|
+
|
|
484
|
+
# Step 6: Save to config
|
|
485
|
+
configure(api_key, api_url)
|
|
486
|
+
|
|
487
|
+
console.print(f"[green]✓[/green] API key created and saved!\n")
|
|
488
|
+
console.print(f" Name: {key_data['name']}")
|
|
489
|
+
console.print(f" Key: [bold]{api_key}[/bold]")
|
|
490
|
+
console.print(f" Saved: ~/.hypercli/config")
|
|
491
|
+
console.print(f"\n[green]You're all set! Try:[/green] hyper keys list\n")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def load_wallet():
|
|
495
|
+
"""Load and decrypt wallet (helper function for other commands)"""
|
|
496
|
+
require_wallet_deps()
|
|
497
|
+
|
|
498
|
+
if not WALLET_PATH.exists():
|
|
499
|
+
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
500
|
+
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
501
|
+
raise typer.Exit(1)
|
|
502
|
+
|
|
503
|
+
with open(WALLET_PATH) as f:
|
|
504
|
+
keystore = json.load(f)
|
|
505
|
+
|
|
506
|
+
# Get passphrase
|
|
507
|
+
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
508
|
+
if passphrase_env:
|
|
509
|
+
passphrase = passphrase_env
|
|
510
|
+
else:
|
|
511
|
+
passphrase = getpass.getpass("Unlock keystore passphrase: ")
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
private_key = Account.decrypt(keystore, passphrase)
|
|
515
|
+
account = Account.from_key(private_key)
|
|
516
|
+
return account
|
|
517
|
+
except Exception as e:
|
|
518
|
+
console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
|
|
519
|
+
raise typer.Exit(1)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hypercli-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.1"
|
|
8
8
|
description = "CLI for HyperCLI - GPU orchestration and LLM API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -28,7 +28,7 @@ comfyui = [
|
|
|
28
28
|
"hypercli-sdk[comfyui]>=0.7.1",
|
|
29
29
|
]
|
|
30
30
|
wallet = [
|
|
31
|
-
"x402[httpx]>=2.0.0",
|
|
31
|
+
"x402[httpx,evm]>=2.0.0",
|
|
32
32
|
"eth-account>=0.13.0",
|
|
33
33
|
"web3>=7.0.0",
|
|
34
34
|
"argon2-cffi>=25.0.0",
|
|
@@ -36,7 +36,7 @@ wallet = [
|
|
|
36
36
|
]
|
|
37
37
|
all = [
|
|
38
38
|
"hypercli-sdk[comfyui]>=0.7.1",
|
|
39
|
-
"x402[httpx]>=2.0.0",
|
|
39
|
+
"x402[httpx,evm]>=2.0.0",
|
|
40
40
|
"eth-account>=0.13.0",
|
|
41
41
|
"web3>=7.0.0",
|
|
42
42
|
"argon2-cffi>=25.0.0",
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
"""Wallet management commands for HyperCLI"""
|
|
2
|
-
import json
|
|
3
|
-
import getpass
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
import typer
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
|
|
10
|
-
app = typer.Typer(help="Wallet management commands")
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
13
|
-
# Check if wallet dependencies are available
|
|
14
|
-
try:
|
|
15
|
-
from web3 import Web3
|
|
16
|
-
from eth_account import Account
|
|
17
|
-
WALLET_AVAILABLE = True
|
|
18
|
-
except ImportError:
|
|
19
|
-
WALLET_AVAILABLE = False
|
|
20
|
-
|
|
21
|
-
WALLET_DIR = Path.home() / ".hypercli"
|
|
22
|
-
WALLET_PATH = WALLET_DIR / "wallet.json"
|
|
23
|
-
BASE_RPC = "https://mainnet.base.org"
|
|
24
|
-
USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def require_wallet_deps():
|
|
28
|
-
"""Check if wallet dependencies are installed"""
|
|
29
|
-
if not WALLET_AVAILABLE:
|
|
30
|
-
console.print("[red]❌ Wallet commands require crypto dependencies[/red]")
|
|
31
|
-
console.print("\nInstall with:")
|
|
32
|
-
console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
|
|
33
|
-
raise typer.Exit(1)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@app.command("create")
|
|
37
|
-
def create():
|
|
38
|
-
"""Create a new wallet with encrypted keystore"""
|
|
39
|
-
require_wallet_deps()
|
|
40
|
-
|
|
41
|
-
if WALLET_PATH.exists():
|
|
42
|
-
console.print(f"[yellow]Wallet already exists at {WALLET_PATH}[/yellow]")
|
|
43
|
-
overwrite = typer.confirm("Overwrite existing wallet?", default=False)
|
|
44
|
-
if not overwrite:
|
|
45
|
-
console.print("Cancelled.")
|
|
46
|
-
raise typer.Exit(0)
|
|
47
|
-
|
|
48
|
-
console.print("\n[bold]Creating new Ethereum wallet...[/bold]\n")
|
|
49
|
-
|
|
50
|
-
# Generate random account
|
|
51
|
-
account = Account.create()
|
|
52
|
-
|
|
53
|
-
console.print(f"[green]✓[/green] Wallet address: [bold]{account.address}[/bold]")
|
|
54
|
-
|
|
55
|
-
# Get passphrase (check env first)
|
|
56
|
-
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
57
|
-
if passphrase_env:
|
|
58
|
-
passphrase = passphrase_env
|
|
59
|
-
console.print("[dim]Using passphrase from HYPERCLI_WALLET_PASSPHRASE[/dim]")
|
|
60
|
-
else:
|
|
61
|
-
passphrase = getpass.getpass("Set keystore passphrase: ")
|
|
62
|
-
confirm = getpass.getpass("Confirm passphrase: ")
|
|
63
|
-
|
|
64
|
-
if passphrase != confirm:
|
|
65
|
-
console.print("[red]❌ Passphrases don't match![/red]")
|
|
66
|
-
raise typer.Exit(1)
|
|
67
|
-
|
|
68
|
-
# Encrypt and save keystore
|
|
69
|
-
keystore = account.encrypt(passphrase)
|
|
70
|
-
|
|
71
|
-
WALLET_DIR.mkdir(parents=True, exist_ok=True)
|
|
72
|
-
with open(WALLET_PATH, "w") as f:
|
|
73
|
-
json.dump(keystore, f, indent=2)
|
|
74
|
-
|
|
75
|
-
# Set restrictive permissions
|
|
76
|
-
os.chmod(WALLET_PATH, 0o600)
|
|
77
|
-
|
|
78
|
-
console.print(f"\n[green]✓[/green] Keystore saved to [bold]{WALLET_PATH}[/bold]")
|
|
79
|
-
console.print(f"[green]✓[/green] Address: [bold]{account.address}[/bold]")
|
|
80
|
-
console.print("\n[yellow]⚠️ Fund this address with USDC on Base to use for payments![/yellow]")
|
|
81
|
-
|
|
82
|
-
# Show QR code for easy mobile scanning
|
|
83
|
-
try:
|
|
84
|
-
import qrcode
|
|
85
|
-
console.print("\n[bold]Scan to send USDC:[/bold]\n")
|
|
86
|
-
qr_obj = qrcode.QRCode(
|
|
87
|
-
version=1,
|
|
88
|
-
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
89
|
-
box_size=1,
|
|
90
|
-
border=1,
|
|
91
|
-
)
|
|
92
|
-
qr_obj.add_data(account.address)
|
|
93
|
-
qr_obj.make(fit=True)
|
|
94
|
-
qr_obj.print_ascii(invert=True)
|
|
95
|
-
except ImportError:
|
|
96
|
-
console.print("\n[dim]Tip: run 'hyper wallet qr' to display address as QR code[/dim]")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@app.command("address")
|
|
100
|
-
def address():
|
|
101
|
-
"""Show wallet address"""
|
|
102
|
-
require_wallet_deps()
|
|
103
|
-
|
|
104
|
-
if not WALLET_PATH.exists():
|
|
105
|
-
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
106
|
-
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
107
|
-
raise typer.Exit(1)
|
|
108
|
-
|
|
109
|
-
with open(WALLET_PATH) as f:
|
|
110
|
-
keystore = json.load(f)
|
|
111
|
-
|
|
112
|
-
addr = keystore.get("address", "unknown")
|
|
113
|
-
console.print(f"\n[bold]Wallet address:[/bold] 0x{addr}")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@app.command("qr")
|
|
117
|
-
def qr(
|
|
118
|
-
output: str = typer.Option(None, "--output", "-o", help="Save QR code as PNG file"),
|
|
119
|
-
):
|
|
120
|
-
"""Display wallet address as QR code (for easy mobile scanning)"""
|
|
121
|
-
require_wallet_deps()
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
import qrcode
|
|
125
|
-
except ImportError:
|
|
126
|
-
console.print("[red]❌ QR code support requires the qrcode package[/red]")
|
|
127
|
-
console.print("\nInstall with:")
|
|
128
|
-
console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
|
|
129
|
-
raise typer.Exit(1)
|
|
130
|
-
|
|
131
|
-
if not WALLET_PATH.exists():
|
|
132
|
-
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
133
|
-
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
134
|
-
raise typer.Exit(1)
|
|
135
|
-
|
|
136
|
-
with open(WALLET_PATH) as f:
|
|
137
|
-
keystore = json.load(f)
|
|
138
|
-
|
|
139
|
-
addr = "0x" + keystore.get("address", "")
|
|
140
|
-
|
|
141
|
-
if output:
|
|
142
|
-
# Save as PNG
|
|
143
|
-
try:
|
|
144
|
-
qr_obj = qrcode.QRCode(
|
|
145
|
-
version=1,
|
|
146
|
-
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
147
|
-
box_size=10,
|
|
148
|
-
border=2,
|
|
149
|
-
)
|
|
150
|
-
qr_obj.add_data(addr)
|
|
151
|
-
qr_obj.make(fit=True)
|
|
152
|
-
|
|
153
|
-
img = qr_obj.make_image(fill_color="black", back_color="white")
|
|
154
|
-
|
|
155
|
-
# Ensure .png extension
|
|
156
|
-
if not output.lower().endswith('.png'):
|
|
157
|
-
output = output + '.png'
|
|
158
|
-
|
|
159
|
-
img.save(output)
|
|
160
|
-
console.print(f"[green]✓[/green] QR code saved to [bold]{output}[/bold]")
|
|
161
|
-
console.print(f"[dim]Address: {addr}[/dim]")
|
|
162
|
-
except Exception as e:
|
|
163
|
-
console.print(f"[red]❌ Failed to save PNG: {e}[/red]")
|
|
164
|
-
console.print("[dim]Try: pip install pillow[/dim]")
|
|
165
|
-
raise typer.Exit(1)
|
|
166
|
-
else:
|
|
167
|
-
# Print ASCII to terminal
|
|
168
|
-
console.print(f"\n[bold]Wallet Address:[/bold] {addr}")
|
|
169
|
-
console.print("[dim]Scan to send USDC on Base network[/dim]\n")
|
|
170
|
-
|
|
171
|
-
qr_obj = qrcode.QRCode(
|
|
172
|
-
version=1,
|
|
173
|
-
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
174
|
-
box_size=1,
|
|
175
|
-
border=1,
|
|
176
|
-
)
|
|
177
|
-
qr_obj.add_data(addr)
|
|
178
|
-
qr_obj.make(fit=True)
|
|
179
|
-
qr_obj.print_ascii(invert=True)
|
|
180
|
-
|
|
181
|
-
console.print(f"\n[bold]{addr}[/bold]")
|
|
182
|
-
console.print("\n[dim]Tip: use --output wallet.png to save as image[/dim]")
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
@app.command("balance")
|
|
186
|
-
def balance():
|
|
187
|
-
"""Check USDC balance on Base"""
|
|
188
|
-
require_wallet_deps()
|
|
189
|
-
|
|
190
|
-
if not WALLET_PATH.exists():
|
|
191
|
-
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
192
|
-
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
193
|
-
raise typer.Exit(1)
|
|
194
|
-
|
|
195
|
-
with open(WALLET_PATH) as f:
|
|
196
|
-
keystore = json.load(f)
|
|
197
|
-
|
|
198
|
-
# Get passphrase
|
|
199
|
-
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
200
|
-
if passphrase_env:
|
|
201
|
-
passphrase = passphrase_env
|
|
202
|
-
else:
|
|
203
|
-
passphrase = getpass.getpass("Unlock keystore passphrase: ")
|
|
204
|
-
|
|
205
|
-
try:
|
|
206
|
-
private_key = Account.decrypt(keystore, passphrase)
|
|
207
|
-
account = Account.from_key(private_key)
|
|
208
|
-
except Exception as e:
|
|
209
|
-
console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
|
|
210
|
-
raise typer.Exit(1)
|
|
211
|
-
|
|
212
|
-
console.print(f"\n[bold]Checking USDC balance on Base...[/bold]\n")
|
|
213
|
-
|
|
214
|
-
# Connect to Base
|
|
215
|
-
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
|
|
216
|
-
|
|
217
|
-
# USDC ERC20 ABI (minimal - just balanceOf)
|
|
218
|
-
usdc_abi = [
|
|
219
|
-
{
|
|
220
|
-
"constant": True,
|
|
221
|
-
"inputs": [{"name": "_owner", "type": "address"}],
|
|
222
|
-
"name": "balanceOf",
|
|
223
|
-
"outputs": [{"name": "balance", "type": "uint256"}],
|
|
224
|
-
"type": "function",
|
|
225
|
-
}
|
|
226
|
-
]
|
|
227
|
-
|
|
228
|
-
usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
|
|
229
|
-
|
|
230
|
-
# Get balance (USDC has 6 decimals)
|
|
231
|
-
balance_raw = usdc.functions.balanceOf(account.address).call()
|
|
232
|
-
balance_usdc = balance_raw / 1_000_000
|
|
233
|
-
|
|
234
|
-
console.print(f"Wallet: [bold]{account.address}[/bold]")
|
|
235
|
-
console.print(f"USDC Balance: [bold green]{balance_usdc:.6f}[/bold green]")
|
|
236
|
-
|
|
237
|
-
if balance_usdc == 0:
|
|
238
|
-
console.print("\n[yellow]⚠️ No USDC! Send some to this address on Base network.[/yellow]")
|
|
239
|
-
else:
|
|
240
|
-
console.print(f"\n[green]✓[/green] You have [bold]{balance_usdc:.2f} USDC[/bold]")
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def load_wallet():
|
|
244
|
-
"""Load and decrypt wallet (helper function for other commands)"""
|
|
245
|
-
require_wallet_deps()
|
|
246
|
-
|
|
247
|
-
if not WALLET_PATH.exists():
|
|
248
|
-
console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
|
|
249
|
-
console.print("Create one with: [bold]hyper wallet create[/bold]")
|
|
250
|
-
raise typer.Exit(1)
|
|
251
|
-
|
|
252
|
-
with open(WALLET_PATH) as f:
|
|
253
|
-
keystore = json.load(f)
|
|
254
|
-
|
|
255
|
-
# Get passphrase
|
|
256
|
-
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
257
|
-
if passphrase_env:
|
|
258
|
-
passphrase = passphrase_env
|
|
259
|
-
else:
|
|
260
|
-
passphrase = getpass.getpass("Unlock keystore passphrase: ")
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
private_key = Account.decrypt(keystore, passphrase)
|
|
264
|
-
account = Account.from_key(private_key)
|
|
265
|
-
return account
|
|
266
|
-
except Exception as e:
|
|
267
|
-
console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
|
|
268
|
-
raise typer.Exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|