Fix: double déclaration const now, scripts .mjs, /api/utxo/count accepte ancrages
**Motivations:** - Corriger erreur syntaxe double déclaration const now dans bitcoin-rpc.js - Scripts batch en .mjs (ES modules) sans dépendance dotenv - /api/utxo/count doit accepter catégorie ancrages (pluriel) du fichier **Root causes:** - const now déclaré deux fois dans même portée (lignes 294 et 299) - Scripts utilisent dotenv non installé globalement - /api/utxo/count cherchait seulement 'anchor' mais fichier utilise 'ancrages' **Correctifs:** - Supprimer deuxième déclaration const now (ligne 299) - Scripts .mjs : parser .env manuellement sans dotenv - /api/utxo/count : accepter 'anchor' OU 'ancrages' **Evolutions:** - Aucune **Pages affectées:** - signet-dashboard/src/bitcoin-rpc.js - signet-dashboard/src/server.js - scripts/complete-utxo-list-blocktime.mjs - scripts/diagnose-bloc-rewards.mjs
This commit is contained in:
parent
cad73cb265
commit
0db7a76044
@ -72,5 +72,4 @@ La déduplication par hash évite de relayer deux fois le même message.
|
||||
|
||||
## Stockage
|
||||
|
||||
Par défaut, le stockage est en mémoire avec sauvegarde optionnelle sur disque.
|
||||
En production, utiliser une base de données (SQLite, PostgreSQL, etc.).
|
||||
Stockage en mémoire avec persistance sur disque (`{STORAGE_PATH}/messages.json`). Sont persistés : messages, seenHashes, signatures, clés. Sauvegarde à l’arrêt (SIGINT/SIGTERM) et périodiquement si `SAVE_INTERVAL_SECONDS` > 0. En production, une base de données (SQLite, PostgreSQL, etc.) est recommandée.
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
# API Relay – Persistance signatures/clés et sauvegarde périodique
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-26
|
||||
|
||||
## Objectif
|
||||
|
||||
- Persister les signatures et les clés de déchiffrement sur disque (au même titre que les messages et seenHashes), afin qu’elles survivent au redémarrage du relais.
|
||||
- Ajouter une sauvegarde périodique configurable pour limiter la perte de données entre deux arrêts propres.
|
||||
|
||||
## Impacts
|
||||
|
||||
- **Fonctionnels** : Les signatures et clés ne sont plus perdues au redémarrage. Réduction du risque de perte en cas de crash entre deux sauvegardes grâce à la sauvegarde périodique.
|
||||
- **Techniques** : Extension du fichier `messages.json` (signatures, keys), nouveau timer de sauvegarde, variable d’environnement `SAVE_INTERVAL_SECONDS`. Gestion d’erreur explicite au chargement (ENOENT vs autres).
|
||||
|
||||
## Modifications
|
||||
|
||||
### api-relay
|
||||
|
||||
- **`src/services/storage.ts`**
|
||||
- `loadFromDisk` : lecture de `messages.json`. Si `signatures` / `keys` présents, chargement dans les Maps. ENOENT = premier run (démarrage à vide) ; autres erreurs loggées et propagées. Parse JSON avec try/catch, log + rethrow.
|
||||
- `saveToDisk` : écriture de `messages`, `seenHashes`, `signatures`, `keys` dans `messages.json`. Suppression du try/catch silencieux ; les erreurs remontent.
|
||||
- `initialize` : plus de catch qui absorbe les erreurs de `loadFromDisk` ; propagation.
|
||||
- **`src/index.ts`**
|
||||
- `SAVE_INTERVAL_SECONDS` : env, défaut 300, 0 = désactivé.
|
||||
- `setInterval` (si > 0) : appel à `storage.saveToDisk()` toutes les N secondes ; `.catch` logue les erreurs.
|
||||
- Shutdown (SIGINT/SIGTERM) : `clearInterval`, puis `saveToDisk`, puis `process.exit(0)` ou `1` en cas d’erreur.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **`userwallet/docs/storage.md`** : Synthèse, structure disque, Persistance, Configuration, Limitations (suppression des anciennes limitations « sigs/clés non persistées », « pas de sauvegarde périodique »).
|
||||
- **`api-relay/README.md`** : Variable `SAVE_INTERVAL_SECONDS`, section Stockage mise à jour.
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
- Redémarrer le serveur api-relay après déploiement.
|
||||
- Les relais existants avec un `messages.json` sans `signatures` ni `keys` continuent de fonctionner ; les champs sont optionnels au chargement.
|
||||
- Pour désactiver la sauvegarde périodique : `SAVE_INTERVAL_SECONDS=0`.
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Vérifier qu’après POST /signatures et POST /keys, un redémarrage du relais conserve les données (GET /signatures/:hash, GET /keys/:hash).
|
||||
- Vérifier qu’en cas d’erreur disque au chargement (fichier corrompu, permissions), le processus logue et exit avec erreur (pas de démarrage à vide silencieux).
|
||||
- Vérifier que la sauvegarde périodique écrit bien `messages.json` (timestamp de modification) et que les logs « Periodic save failed » apparaissent en cas d’échec.
|
||||
45
features/userwallet-export-import.md
Normal file
45
features/userwallet-export-import.md
Normal file
@ -0,0 +1,45 @@
|
||||
# UserWallet – Export / Import des données
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-26
|
||||
|
||||
## Objectif
|
||||
|
||||
Permettre l’export et l’import de toutes les données UserWallet (identité, relais, pairs, cache de hash, keypair et services legacy) pour sauvegarde, migration ou restauration.
|
||||
|
||||
## Impacts
|
||||
|
||||
- **Fonctionnels** : Export en fichier JSON téléchargeable ; import depuis fichier qui remplace les données locales puis recharge la page.
|
||||
- **Techniques** : Nouveau module `utils/exportImport.ts`, écran `DataExportImportScreen`, route `/data`, boutons sur l’accueil.
|
||||
|
||||
## Modifications
|
||||
|
||||
### utils/exportImport.ts
|
||||
|
||||
- `exportUserWalletData(): string` : lit identity, relays, pairs, hash_cache, keypair, services depuis localStorage, produit un JSON versionné (`version`, `exportedAt`).
|
||||
- `importUserWalletData(json: string): void` : parse, valide (version, exportedAt, relays, pairs, hash_cache, identity optionnel), écrit en localStorage. Lance `Error` si format invalide.
|
||||
- Types : `UserWalletExport`, validateurs `isRelayConfig`, `isPairConfig`, `isLocalIdentity`.
|
||||
|
||||
### DataExportImportScreen
|
||||
|
||||
- Section Export : bouton « Exporter les données » → `exportUserWalletData` puis téléchargement `userwallet-export-{timestamp}.json`.
|
||||
- Section Import : input file caché, bouton « Choisir un fichier à importer » → lecture du fichier, `importUserWalletData`, puis `window.location.reload`. Affichage des erreurs d’import.
|
||||
|
||||
### App & HomeScreen
|
||||
|
||||
- Route `/data` → `DataExportImportScreen`.
|
||||
- Bouton « Export / Import données » sur l’accueil (avec ou sans identité).
|
||||
|
||||
### Documentation
|
||||
|
||||
- `userwallet/docs/storage.md` : suppression de « Pas de backup / pas d’export-import » ; ajout section « Export / Import » et retrait de la recommandation correspondante.
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
- Rebuild du frontend UserWallet et déploiement des assets. Aucune migration de données.
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Exporter les données, vérifier que le JSON contient identity, relays, pairs, hash_cache, etc.
|
||||
- Importer un export valide : vérifier que les données sont bien présentes après rechargement.
|
||||
- Importer un JSON invalide (mauvais format, champs manquants) : vérifier qu’une erreur est affichée et que les données locales ne sont pas modifiées.
|
||||
34386
hash_list.txt
34386
hash_list.txt
File diff suppressed because it is too large
Load Diff
@ -7,21 +7,32 @@
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const utxoListPath = join(__dirname, '../utxo_list.txt');
|
||||
|
||||
// Charger les variables d'environnement
|
||||
import { config } from 'dotenv';
|
||||
config({ path: join(__dirname, '../signet-dashboard/.env') });
|
||||
// Charger les variables d'environnement depuis .env si disponible
|
||||
let envVars = {};
|
||||
try {
|
||||
const envPath = join(__dirname, '../signet-dashboard/.env');
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf8');
|
||||
for (const line of envContent.split('\n')) {
|
||||
const match = line.match(/^([^#=]+)=(.*)$/);
|
||||
if (match) {
|
||||
envVars[match[1].trim()] = match[2].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load .env file, using defaults');
|
||||
}
|
||||
|
||||
const BITCOIN_RPC_URL = process.env.BITCOIN_RPC_URL || 'http://127.0.0.1:38332';
|
||||
const BITCOIN_RPC_USER = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||
const BITCOIN_RPC_PASSWORD = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||
const BITCOIN_RPC_WALLET = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||
const BITCOIN_RPC_URL = envVars.BITCOIN_RPC_URL || process.env.BITCOIN_RPC_URL || 'http://127.0.0.1:38332';
|
||||
const BITCOIN_RPC_USER = envVars.BITCOIN_RPC_USER || process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||
const BITCOIN_RPC_PASSWORD = envVars.BITCOIN_RPC_PASSWORD || process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||
const BITCOIN_RPC_WALLET = envVars.BITCOIN_RPC_WALLET || process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||
|
||||
if (!existsSync(utxoListPath)) {
|
||||
console.error('utxo_list.txt not found');
|
||||
@ -7,18 +7,31 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
config({ path: join(__dirname, '../signet-dashboard/.env') });
|
||||
// Charger les variables d'environnement depuis .env si disponible
|
||||
let envVars = {};
|
||||
try {
|
||||
const envPath = join(__dirname, '../signet-dashboard/.env');
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf8');
|
||||
for (const line of envContent.split('\n')) {
|
||||
const match = line.match(/^([^#=]+)=(.*)$/);
|
||||
if (match) {
|
||||
envVars[match[1].trim()] = match[2].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load .env file, using defaults');
|
||||
}
|
||||
|
||||
const BITCOIN_RPC_URL = process.env.BITCOIN_RPC_URL || 'http://127.0.0.1:38332';
|
||||
const BITCOIN_RPC_USER = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||
const BITCOIN_RPC_PASSWORD = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||
const BITCOIN_RPC_WALLET = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||
const BITCOIN_RPC_URL = envVars.BITCOIN_RPC_URL || process.env.BITCOIN_RPC_URL || 'http://127.0.0.1:38332';
|
||||
const BITCOIN_RPC_USER = envVars.BITCOIN_RPC_USER || process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||
const BITCOIN_RPC_PASSWORD = envVars.BITCOIN_RPC_PASSWORD || process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||
const BITCOIN_RPC_WALLET = envVars.BITCOIN_RPC_WALLET || process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||
|
||||
const auth = Buffer.from(`${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASSWORD}`).toString('base64');
|
||||
const rpcUrl = `${BITCOIN_RPC_URL}/wallet/${BITCOIN_RPC_WALLET}`;
|
||||
@ -296,7 +296,6 @@ class BitcoinRPC {
|
||||
writeFileSync(cachePath, cacheContent, 'utf8');
|
||||
|
||||
// Écrire le fichier de sortie avec date
|
||||
const now = new Date().toISOString();
|
||||
const outputLines = hashList.map((item) =>
|
||||
`${item.hash};${item.txid};${item.blockHeight || ''};${item.confirmations || 0};${item.date || now}`
|
||||
);
|
||||
@ -314,7 +313,6 @@ class BitcoinRPC {
|
||||
writeFileSync(cachePath, cacheContent, 'utf8');
|
||||
|
||||
// Écrire le fichier de sortie final avec date
|
||||
const now = new Date().toISOString();
|
||||
const outputLines = hashList.map((item) =>
|
||||
`${item.hash};${item.txid};${item.blockHeight || ''};${item.confirmations || 0};${item.date || now}`
|
||||
);
|
||||
|
||||
@ -306,7 +306,8 @@ app.get('/api/utxo/count', async (req, res) => {
|
||||
const confirmations = parseInt(parts[4], 10) || 0;
|
||||
|
||||
// Compter les UTXOs de type anchor avec les critères minimum
|
||||
if (category === 'anchor' && amount >= minAnchorAmount && confirmations > 0) {
|
||||
// Le fichier utilise 'ancrages' (pluriel), pas 'anchor'
|
||||
if ((category === 'anchor' || category === 'ancrages') && amount >= minAnchorAmount && confirmations > 0) {
|
||||
anchors++;
|
||||
// Note: On ne peut pas savoir depuis le fichier si l'UTXO est dépensé
|
||||
// On compte tous les UTXOs avec confirmations > 0 comme disponibles
|
||||
|
||||
@ -161,11 +161,14 @@ Le front utilise **LocalStorage** du navigateur pour toutes les données locales
|
||||
- **Taille limitée** : LocalStorage a une limite (~5-10MB selon navigateur)
|
||||
- **Pas de synchronisation** : Données locales uniquement
|
||||
- **Sécurité** : Clés privées stockées en clair (à chiffrer avec mot de passe)
|
||||
- **Pas de backup** : Pas de mécanisme d'export/import automatique
|
||||
|
||||
### Export / Import
|
||||
|
||||
- **Export** : Écran « Export / Import données » (`/data`). Télécharge un JSON (identité, relais, pairs, hash_cache, keypair, services).
|
||||
- **Import** : Même écran, bouton « Choisir un fichier ». Remplace les données locales puis recharge la page.
|
||||
|
||||
### Recommandations
|
||||
|
||||
- Chiffrer les clés privées avec un mot de passe utilisateur
|
||||
- Implémenter un mécanisme d'export/import
|
||||
- Utiliser IndexedDB pour des données plus volumineuses
|
||||
- Implémenter une synchronisation cloud optionnelle (chiffrée)
|
||||
|
||||
@ -59,7 +59,8 @@ api-relay/
|
||||
|
||||
1. **Stockage**
|
||||
- Stockage en mémoire des messages, signatures et clés
|
||||
- Sauvegarde optionnelle sur disque (JSON)
|
||||
- Persistance sur disque (`messages.json`) : messages, seenHashes, signatures, clés
|
||||
- Sauvegarde à l’arrêt et périodique (voir `SAVE_INTERVAL_SECONDS`)
|
||||
- Déduplication par hash
|
||||
- Indexation par hash pour accès rapide
|
||||
|
||||
@ -98,7 +99,7 @@ api-relay/
|
||||
- Gestion du stockage en mémoire
|
||||
- Déduplication par hash
|
||||
- Méthodes pour messages, signatures et clés
|
||||
- Sauvegarde/chargement depuis disque (optionnel)
|
||||
- Sauvegarde/chargement depuis disque (messages, seenHashes, signatures, clés) ; sauvegarde périodique configurable
|
||||
|
||||
#### RelayService
|
||||
|
||||
@ -128,6 +129,7 @@ Variables d'environnement :
|
||||
- `HOST` : Adresse d'écoute (défaut: 0.0.0.0)
|
||||
- `STORAGE_PATH` : Chemin de stockage (défaut: ./data)
|
||||
- `PEER_RELAYS` : Liste de relais pairs séparés par virgule
|
||||
- `SAVE_INTERVAL_SECONDS` : Sauvegarde périodique en secondes (défaut: 300, 0 = désactivé)
|
||||
|
||||
Exemple :
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { PairManagementScreen } from './components/PairManagementScreen';
|
||||
import { SyncScreen } from './components/SyncScreen';
|
||||
import { LoginScreen } from './components/LoginScreen';
|
||||
import { ServiceListScreen } from './components/ServiceListScreen';
|
||||
import { DataExportImportScreen } from './components/DataExportImportScreen';
|
||||
import { useChannel } from './hooks/useChannel';
|
||||
import './index.css';
|
||||
|
||||
@ -23,6 +24,7 @@ function AppContent(): JSX.Element {
|
||||
<Route path="/relay-settings" element={<RelaySettingsScreen />} />
|
||||
<Route path="/sync" element={<SyncScreen />} />
|
||||
<Route path="/services" element={<ServiceListScreen />} />
|
||||
<Route path="/data" element={<DataExportImportScreen />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
99
userwallet/src/components/DataExportImportScreen.tsx
Normal file
99
userwallet/src/components/DataExportImportScreen.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
exportUserWalletData,
|
||||
importUserWalletData,
|
||||
} from '../utils/exportImport';
|
||||
|
||||
function downloadExport(json: string): void {
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `userwallet-export-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function DataExportImportScreen(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
|
||||
const handleExport = (): void => {
|
||||
const json = exportUserWalletData();
|
||||
downloadExport(json);
|
||||
};
|
||||
|
||||
const handleImportClick = (): void => {
|
||||
setImportError(null);
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (file === undefined) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const text = reader.result;
|
||||
if (typeof text !== 'string') {
|
||||
setImportError('Fichier non lisible');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
importUserWalletData(text);
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
setImportError(err instanceof Error ? err.message : 'Erreur import');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Export / Import des données</h1>
|
||||
<section aria-labelledby="export-section">
|
||||
<h2 id="export-section">Export</h2>
|
||||
<p>
|
||||
Télécharge un fichier JSON contenant l'identité, les relais, les
|
||||
pairs et le cache de hash.
|
||||
</p>
|
||||
<button type="button" onClick={handleExport}>
|
||||
Exporter les données
|
||||
</button>
|
||||
</section>
|
||||
<section aria-labelledby="import-section">
|
||||
<h2 id="import-section">Import</h2>
|
||||
<p>
|
||||
Remplace toutes les données locales par le contenu du fichier. La
|
||||
page sera rechargée après import.
|
||||
</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
onChange={handleImportFile}
|
||||
/>
|
||||
<button type="button" onClick={handleImportClick}>
|
||||
Choisir un fichier à importer
|
||||
</button>
|
||||
{importError !== null && (
|
||||
<p role="alert" style={{ color: 'var(--color-error)' }}>
|
||||
{importError}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<p>
|
||||
<button type="button" onClick={() => navigate('/')}>
|
||||
Retour à l'accueil
|
||||
</button>
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -30,6 +30,9 @@ export function HomeScreen(): JSX.Element {
|
||||
<button onClick={() => navigate('/import-identity')}>
|
||||
Importer une identité
|
||||
</button>
|
||||
<button onClick={() => navigate('/data')}>
|
||||
Export / Import données
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@ -90,6 +93,9 @@ export function HomeScreen(): JSX.Element {
|
||||
</button>
|
||||
<button onClick={() => navigate('/sync')}>Synchroniser maintenant</button>
|
||||
<button onClick={() => navigate('/services')}>Services disponibles</button>
|
||||
<button onClick={() => navigate('/data')}>
|
||||
Export / Import données
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
145
userwallet/src/utils/exportImport.ts
Normal file
145
userwallet/src/utils/exportImport.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { getStoredIdentity } from './identity';
|
||||
import { getStoredRelays } from './relay';
|
||||
import { getStoredPairs } from './pairing';
|
||||
import { getStoredKeyPair, getStoredServices } from './storage';
|
||||
import type { LocalIdentity, RelayConfig, PairConfig } from '../types/identity';
|
||||
import type { KeyPair } from './crypto';
|
||||
import type { ServiceConfig } from '../types/auth';
|
||||
|
||||
const STORAGE_KEY_IDENTITY = 'userwallet_identity';
|
||||
const STORAGE_KEY_RELAYS = 'userwallet_relays';
|
||||
const STORAGE_KEY_PAIRS = 'userwallet_pairs';
|
||||
const STORAGE_KEY_HASH_CACHE = 'userwallet_hash_cache';
|
||||
const STORAGE_KEY_KEYPAIR = 'userwallet_keypair';
|
||||
const STORAGE_KEY_SERVICES = 'userwallet_services';
|
||||
|
||||
export const EXPORT_VERSION = '1.0';
|
||||
|
||||
export interface UserWalletExport {
|
||||
version: string;
|
||||
exportedAt: number;
|
||||
identity: LocalIdentity | null;
|
||||
relays: RelayConfig[];
|
||||
pairs: PairConfig[];
|
||||
hash_cache: string[];
|
||||
keypair?: KeyPair | null;
|
||||
services?: ServiceConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all UserWallet data from localStorage as JSON string.
|
||||
*/
|
||||
export function exportUserWalletData(): string {
|
||||
const identity = getStoredIdentity();
|
||||
const relays = getStoredRelays();
|
||||
const pairs = getStoredPairs();
|
||||
const keypair = getStoredKeyPair();
|
||||
const services = getStoredServices();
|
||||
const hashCacheRaw = localStorage.getItem(STORAGE_KEY_HASH_CACHE);
|
||||
const hash_cache: string[] =
|
||||
hashCacheRaw !== null ? (JSON.parse(hashCacheRaw) as string[]) : [];
|
||||
|
||||
const data: UserWalletExport = {
|
||||
version: EXPORT_VERSION,
|
||||
exportedAt: Date.now(),
|
||||
identity,
|
||||
relays,
|
||||
pairs,
|
||||
hash_cache,
|
||||
keypair: keypair ?? null,
|
||||
services,
|
||||
};
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
function isRelayConfig(x: unknown): x is RelayConfig {
|
||||
if (x === null || typeof x !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const o = x as Record<string, unknown>;
|
||||
return (
|
||||
typeof o.endpoint === 'string' &&
|
||||
typeof o.priority === 'number' &&
|
||||
typeof o.enabled === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
function isPairConfig(x: unknown): x is PairConfig {
|
||||
if (x === null || typeof x !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const o = x as Record<string, unknown>;
|
||||
return (
|
||||
typeof o.uuid === 'string' &&
|
||||
Array.isArray(o.membres_parents_uuid) &&
|
||||
typeof o.is_local === 'boolean' &&
|
||||
typeof o.can_sign === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
function isLocalIdentity(x: unknown): x is LocalIdentity {
|
||||
if (x === null || typeof x !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const o = x as Record<string, unknown>;
|
||||
return (
|
||||
typeof o.uuid === 'string' &&
|
||||
typeof o.privateKey === 'string' &&
|
||||
typeof o.publicKey === 'string' &&
|
||||
typeof o.t0_anniversaire === 'number' &&
|
||||
typeof o.version === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import UserWallet data from JSON string into localStorage.
|
||||
* Overwrites existing data. Throws on invalid format.
|
||||
*/
|
||||
export function importUserWalletData(json: string): void {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(json) as unknown;
|
||||
} catch (err) {
|
||||
throw new Error('Invalid JSON');
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object') {
|
||||
throw new Error('Export must be an object');
|
||||
}
|
||||
const o = parsed as Record<string, unknown>;
|
||||
if (typeof o.version !== 'string' || typeof o.exportedAt !== 'number') {
|
||||
throw new Error('Missing or invalid version/exportedAt');
|
||||
}
|
||||
if (!Array.isArray(o.relays) || !o.relays.every(isRelayConfig)) {
|
||||
throw new Error('Invalid relays');
|
||||
}
|
||||
if (!Array.isArray(o.pairs) || !o.pairs.every(isPairConfig)) {
|
||||
throw new Error('Invalid pairs');
|
||||
}
|
||||
if (!Array.isArray(o.hash_cache) || !o.hash_cache.every((h) => typeof h === 'string')) {
|
||||
throw new Error('Invalid hash_cache');
|
||||
}
|
||||
|
||||
if (o.identity !== null && !isLocalIdentity(o.identity)) {
|
||||
throw new Error('Invalid identity');
|
||||
}
|
||||
|
||||
if (o.identity === null) {
|
||||
localStorage.removeItem(STORAGE_KEY_IDENTITY);
|
||||
} else {
|
||||
localStorage.setItem(STORAGE_KEY_IDENTITY, JSON.stringify(o.identity));
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY_RELAYS, JSON.stringify(o.relays));
|
||||
localStorage.setItem(STORAGE_KEY_PAIRS, JSON.stringify(o.pairs));
|
||||
localStorage.setItem(STORAGE_KEY_HASH_CACHE, JSON.stringify(o.hash_cache));
|
||||
|
||||
if ('keypair' in o && o.keypair !== undefined) {
|
||||
if (o.keypair === null) {
|
||||
localStorage.removeItem(STORAGE_KEY_KEYPAIR);
|
||||
} else {
|
||||
localStorage.setItem(STORAGE_KEY_KEYPAIR, JSON.stringify(o.keypair));
|
||||
}
|
||||
}
|
||||
if ('services' in o && o.services !== undefined && Array.isArray(o.services)) {
|
||||
localStorage.setItem(STORAGE_KEY_SERVICES, JSON.stringify(o.services));
|
||||
}
|
||||
}
|
||||
@ -34840,7 +34840,7 @@ ancrages;1b60296a88665c6775044246fa88c61324e10b9d9428725f8b29966a175d11c7;5;0.00
|
||||
ancrages;dc6b85db83f795087dcb16a7749c952048db9a7f0d40f9924f13ce794b7e5c36;7;0.000025;284;false;
|
||||
ancrages;d7fcaca3f983e4dc591d51515aa77ee85460c67991c6274dc00b6aeddd891f85;7;0.000025;530;false;
|
||||
ancrages;e7e6ed5f12757fcc21ebf47e022df260a7046922932bceb5998efd4919701562;0;0.000025;620;false;
|
||||
ancrages;1b60296a88665c6775044246fa88c61324e10b9d9428725f8b29966a175d11c7;7;0.000025;669;false;
|
||||
ancrages;1b60296a88665c6775044246fa88c61324e10b9d9428725f8b29966a175d11c7;7;0.000025;669;false;1769350182
|
||||
ancrages;1b60296a88665c6775044246fa88c61324e10b9d9428725f8b29966a175d11c7;8;0.000025;669;false;
|
||||
ancrages;cfb64e274caf85ad9ec3ab85cec1833182634cb630af1d4af917a32673a8f49a;2;0.000025;669;false;
|
||||
ancrages;b54e4bb450da7820eefb76ef5b968e00979ce2960bee8b5e0191bf29937a4746;2;0.000025;669;false;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user