Imposta app desktop Electron/Vue con backend FastAPI

- Aggiunge scaffold frontend Vue con UI base e build verso renderer
- Introduce backend FastAPI con endpoint generazione e salvataggio JSON
- Sposta i generatori in backend e aggiorna CLI main.py
- Aggiorna README, requirements e .gitignore per artefatti e config
- Configura Electron (main/preload) e script npm per dev/build
This commit is contained in:
2026-01-30 16:22:09 +01:00
parent cc34cf23df
commit 5477555429
20 changed files with 5758 additions and 20 deletions

15
.gitignore vendored
View File

@@ -8,3 +8,18 @@ venv/
# JSON files (default ignore) # JSON files (default ignore)
*.json *.json
# Keep essential config JSON tracked
!package.json
!package-lock.json
# Python caches/build
__pycache__/
*.pyc
backend/build/
backend/dist/
# Node/Electron outputs
node_modules/
desktop/renderer/
release/

View File

@@ -40,7 +40,7 @@ Dopo aver selezionato un'opzione, lo script dedicato verrà eseguito e guida l'u
- Eventuale utilizzo di chiavi compresse/non compresse - Eventuale utilizzo di chiavi compresse/non compresse
- Visualizzazione e salvataggio dei dati in un file `.json` - Visualizzazione e salvataggio dei dati in un file `.json`
Ogni script è indipendente (`p2pk.py`, `p2pkh.py`, `p2sh.py`, `p2wpkh.py`, `p2tr.py`) e implementa le regole specifiche del relativo standard Bitcoin. Ogni script è indipendente (`backend/p2pk.py`, `backend/p2pkh.py`, `backend/p2sh.py`, `backend/p2wpkh.py`, `backend/p2tr.py`) e implementa le regole specifiche del relativo standard Bitcoin.
--- ---
@@ -139,4 +139,4 @@ I dati saranno salvati in un file `.json` leggibile e riutilizzabile.
--- ---
## LICENZA ## LICENZA
Questo progetto è rilasciato sotto licenza MIT Questo progetto è rilasciato sotto licenza MIT

0
backend/__init__.py Normal file
View File

177
backend/app.py Normal file
View File

@@ -0,0 +1,177 @@
import json
import os
import sys
from datetime import datetime
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
import p2pk
import p2pkh
import p2sh
import p2tr
import p2wpkh
app = FastAPI(title="AddressGen API", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class P2PKRequest(BaseModel):
network: str = "mainnet"
compressed: bool = False
save_to_file: bool = False
filename: Optional[str] = None
class P2PKHRequest(BaseModel):
network: str = "mainnet"
compressed: bool = True
save_to_file: bool = False
filename: Optional[str] = None
class P2WPKHRequest(BaseModel):
network: str = "mainnet"
compressed: bool = True
save_to_file: bool = False
filename: Optional[str] = None
class P2TRRequest(BaseModel):
network: str = "mainnet"
save_to_file: bool = False
filename: Optional[str] = None
class P2SHRequest(BaseModel):
network: str = "mainnet"
m: int = 2
n: int = 3
compressed: bool = True
sort_pubkeys: bool = True
save_to_file: bool = False
filename: Optional[str] = None
class SaveRequest(BaseModel):
data: dict
filename: Optional[str] = None
def _save_json(data: dict, filename: Optional[str] = None) -> str:
wallets_dir = os.path.join(ROOT_DIR, "wallets")
os.makedirs(wallets_dir, exist_ok=True)
if filename:
safe = os.path.basename(filename)
if not safe.endswith(".json"):
safe += ".json"
out_name = safe
else:
script_type = data.get("script_type", "wallet")
network = data.get("network", "net")
stamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
out_name = f"{script_type}_{network}_{stamp}.json"
out_path = os.path.join(wallets_dir, out_name)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4)
return out_path
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/p2pk")
def create_p2pk(req: P2PKRequest):
try:
result = p2pk.generate_p2pk(req.network, req.compressed)
if req.save_to_file:
result["saved_path"] = _save_json(result, req.filename)
return result
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@app.post("/p2pkh")
def create_p2pkh(req: P2PKHRequest):
try:
result = p2pkh.generate_legacy_address(req.network, req.compressed)
if req.save_to_file:
result["saved_path"] = _save_json(result, req.filename)
return result
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@app.post("/p2wpkh")
def create_p2wpkh(req: P2WPKHRequest):
try:
result = p2wpkh.generate_segwit_address(req.network, req.compressed)
if req.save_to_file:
result["saved_path"] = _save_json(result, req.filename)
return result
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@app.post("/p2tr")
def create_p2tr(req: P2TRRequest):
try:
result = p2tr.generate_p2tr_address(req.network)
if req.save_to_file:
result["saved_path"] = _save_json(result, req.filename)
return result
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@app.post("/p2sh")
def create_p2sh(req: P2SHRequest):
try:
result = p2sh.generate_p2sh_multisig(
network=req.network,
m=req.m,
n=req.n,
compressed=req.compressed,
sort_pubkeys=req.sort_pubkeys,
)
if req.save_to_file:
result["saved_path"] = _save_json(result, req.filename)
return result
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@app.post("/save")
def save_result(req: SaveRequest):
try:
saved_path = _save_json(req.data, req.filename)
return {"saved_path": saved_path}
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
def run():
import uvicorn
port = int(os.getenv("ADDRESSGEN_PORT", "8732"))
uvicorn.run("backend.app:app", host="127.0.0.1", port=port, log_level="info")
if __name__ == "__main__":
run()

2
backend/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
fastapi
uvicorn[standard]

66
desktop/main.js Normal file
View File

@@ -0,0 +1,66 @@
const { app, BrowserWindow } = require("electron");
const path = require("path");
const { spawn } = require("child_process");
const BACKEND_PORT = process.env.ADDRESSGEN_PORT || "8732";
const API_BASE = `http://127.0.0.1:${BACKEND_PORT}`;
let backendProcess = null;
function startBackend() {
const env = {
...process.env,
ADDRESSGEN_PORT: BACKEND_PORT,
};
if (process.env.BACKEND_BIN) {
backendProcess = spawn(process.env.BACKEND_BIN, [], { env, stdio: "inherit" });
return;
}
if (app.isPackaged) {
const binPath = path.join(process.resourcesPath, "backend", "addressgen-backend");
backendProcess = spawn(binPath, [], { env, stdio: "inherit" });
} else {
const python = process.env.PYTHON || "python3";
const script = path.join(__dirname, "..", "backend", "app.py");
backendProcess = spawn(python, [script], { env, stdio: "inherit" });
}
}
function createWindow() {
process.env.ADDRESSGEN_API_BASE = API_BASE;
const win = new BrowserWindow({
width: 1100,
height: 720,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
if (app.isPackaged) {
win.loadFile(path.join(__dirname, "renderer", "index.html"));
} else {
win.loadURL("http://localhost:5173");
}
}
app.whenReady().then(() => {
startBackend();
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("before-quit", () => {
if (backendProcess) {
backendProcess.kill();
}
});

5
desktop/preload.js Normal file
View File

@@ -0,0 +1,5 @@
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("addressgen", {
apiBase: process.env.ADDRESSGEN_API_BASE || "http://127.0.0.1:8732",
});

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AddressGen</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

265
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,265 @@
<template>
<div class="page">
<header class="hero">
<h1>AddressGen</h1>
<p>Genera indirizzi Bitcoin localmente usando il backend Python.</p>
</header>
<section class="card">
<div class="row">
<label>Tipo</label>
<select v-model="form.type">
<option value="p2pk">P2PK</option>
<option value="p2pkh">P2PKH</option>
<option value="p2wpkh">P2WPKH</option>
<option value="p2tr">P2TR</option>
<option value="p2sh">P2SH Multisig</option>
</select>
</div>
<div class="row">
<label>Network</label>
<select v-model="form.network">
<option value="mainnet">mainnet</option>
<option value="testnet">testnet</option>
<option value="regtest">regtest</option>
</select>
</div>
<div class="row" v-if="form.type !== 'p2tr'">
<label>Chiavi compresse</label>
<input type="checkbox" v-model="form.compressed" />
</div>
<div class="row" v-if="form.type === 'p2sh'">
<label>m-of-n</label>
<div class="inline">
<input type="number" min="1" max="16" v-model.number="form.m" />
<span>/</span>
<input type="number" min="1" max="16" v-model.number="form.n" />
</div>
</div>
<div class="row">
<label>Nome file</label>
<input type="text" v-model="form.filename" placeholder="wallet.json (opzionale)" />
</div>
<div class="actions">
<button class="primary" @click="generate" :disabled="loading">
{{ loading ? "Genero..." : "Genera" }}
</button>
<button class="secondary" @click="save" :disabled="loading || !result">
Salva JSON
</button>
</div>
</section>
<section class="card" v-if="result">
<h2>Risultato</h2>
<pre>{{ result }}</pre>
</section>
<section class="card" v-if="savedPath">
<h2>Salvato</h2>
<pre>{{ savedPath }}</pre>
</section>
<section class="card" v-if="error">
<h2>Errore</h2>
<pre>{{ error }}</pre>
</section>
</div>
</template>
<script setup>
import { ref } from "vue";
const apiBase = window?.addressgen?.apiBase || "http://127.0.0.1:8732";
const form = ref({
type: "p2pkh",
network: "mainnet",
compressed: true,
m: 2,
n: 3,
filename: "",
});
const result = ref("");
const error = ref("");
const loading = ref(false);
const savedPath = ref("");
const generate = async () => {
loading.value = true;
result.value = "";
error.value = "";
savedPath.value = "";
try {
const endpoint = `${apiBase}/${form.value.type}`;
const payload = {
network: form.value.network,
};
if (form.value.type !== "p2tr") {
payload.compressed = form.value.compressed;
}
if (form.value.type === "p2sh") {
payload.m = form.value.m;
payload.n = form.value.n;
}
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data?.detail || "Errore sconosciuto");
}
result.value = JSON.stringify(data, null, 2);
} catch (err) {
error.value = err?.message || String(err);
} finally {
loading.value = false;
}
};
const save = async () => {
if (!result.value) return;
loading.value = true;
error.value = "";
savedPath.value = "";
try {
const payload = {
data: JSON.parse(result.value),
};
if (form.value.filename?.trim()) {
payload.filename = form.value.filename.trim();
}
const res = await fetch(`${apiBase}/save`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data?.detail || "Errore sconosciuto");
}
savedPath.value = data.saved_path || "";
} catch (err) {
error.value = err?.message || String(err);
} finally {
loading.value = false;
}
};
</script>
<style scoped>
:root {
font-family: "Fira Sans", sans-serif;
}
.page {
min-height: 100vh;
background: linear-gradient(135deg, #0f172a, #1e293b);
color: #e2e8f0;
padding: 32px;
}
.hero {
max-width: 720px;
margin: 0 auto 24px;
}
.hero h1 {
font-size: 40px;
margin-bottom: 8px;
}
.card {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 16px;
padding: 20px;
max-width: 720px;
margin: 0 auto 20px;
backdrop-filter: blur(6px);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.inline {
display: flex;
align-items: center;
gap: 8px;
}
input,
select {
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.3);
background: #0f172a;
color: #e2e8f0;
}
.actions {
display: flex;
gap: 12px;
}
.primary {
background: #38bdf8;
color: #0f172a;
border: none;
padding: 10px 16px;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.secondary {
background: transparent;
color: #e2e8f0;
border: 1px solid rgba(148, 163, 184, 0.5);
padding: 10px 16px;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
.secondary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
pre {
white-space: pre-wrap;
background: #0b1120;
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.2);
overflow-x: auto;
}
</style>

4
frontend/src/main.js Normal file
View File

@@ -0,0 +1,4 @@
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

17
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
export default defineConfig({
root: path.resolve(__dirname),
base: "./",
plugins: [vue()],
build: {
outDir: path.resolve(__dirname, "../desktop/renderer"),
emptyOutDir: true,
},
server: {
port: 5173,
strictPort: true,
},
});

39
main.py
View File

@@ -1,5 +1,8 @@
import subprocess import backend.p2pk as p2pk
import sys import backend.p2pkh as p2pkh
import backend.p2sh as p2sh
import backend.p2wpkh as p2wpkh
import backend.p2tr as p2tr
def main(): def main():
print("=== GENERATORE INDIRIZZI BITCOIN ===") print("=== GENERATORE INDIRIZZI BITCOIN ===")
@@ -12,23 +15,23 @@ def main():
choice = input("Inserisci la tua scelta: ").strip() choice = input("Inserisci la tua scelta: ").strip()
scripts = { actions = {
'1': 'p2pk.py', '1': p2pk.main,
'2': 'p2pkh.py', '2': p2pkh.main,
'3': 'p2sh.py', '3': p2sh.main,
'4': 'p2wpkh.py', '4': p2wpkh.main,
'5': 'p2tr.py' '5': p2tr.main,
} }
if choice in scripts: action = actions.get(choice)
try: if not action:
subprocess.run([sys.executable, scripts[choice]], check=True)
except subprocess.CalledProcessError as e:
print(f"Errore nell'esecuzione dello script: {e}")
except KeyboardInterrupt:
print("\nOperazione interrotta.")
else:
print("Scelta non valida.") print("Scelta non valida.")
return
try:
action()
except KeyboardInterrupt:
print("\nOperazione interrotta.")
if __name__ == '__main__': if __name__ == '__main__':
main() main()

5125
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "addressgen",
"version": "0.1.0",
"description": "Desktop app for generating Bitcoin addresses",
"main": "desktop/main.js",
"scripts": {
"setup:py": "python3 -m pip install -r requirements.txt",
"dev:backend": "python3 backend/app.py",
"dev:frontend": "vite --config frontend/vite.config.js",
"dev:electron": "wait-on http://localhost:5173 && electron .",
"dev": "concurrently -k \"npm:dev:backend\" \"npm:dev:frontend\" \"npm:dev:electron\"",
"build:frontend": "vite build --config frontend/vite.config.js",
"build:backend": "pyinstaller --name addressgen-backend --onefile backend/app.py --distpath backend/dist --workpath backend/build --specpath backend",
"build": "npm run build:frontend && npm run build:backend && electron-builder --linux"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"concurrently": "^8.2.2",
"electron": "^30.0.0",
"electron-builder": "^24.13.3",
"vite": "^5.4.0",
"vue": "^3.4.0",
"wait-on": "^7.2.0"
},
"build": {
"appId": "com.addressgen.app",
"productName": "AddressGen",
"directories": {
"output": "release"
},
"files": [
"desktop/**",
"frontend/**"
],
"extraResources": [
{
"from": "backend/dist/addressgen-backend",
"to": "backend/addressgen-backend"
}
],
"linux": {
"target": ["AppImage"]
}
}
}

View File

@@ -2,3 +2,5 @@ base58==2.1.1
bech32==1.2.0 bech32==1.2.0
ecdsa==0.19.0 ecdsa==0.19.0
six==1.17.0 six==1.17.0
fastapi==0.115.0
uvicorn[standard]==0.30.6