Refactoring, upgrading & adding logs in router.ts

This commit is contained in:
NicolasCantu 2025-11-04 23:04:03 +01:00
parent 614569f5aa
commit bbb0c12506
7 changed files with 2038 additions and 1343 deletions

View File

@ -0,0 +1,385 @@
# Analyse du Workflow de Pairing - Processus de Couplage d'Appareils
## Introduction
Ce document présente une analyse complète du workflow de pairing (processus de couplage) dans l'écosystème 4NK, qui permet à deux appareils de s'associer de manière sécurisée. Le processus implique à la fois le code TypeScript côté client (`ihm_client_dev2`) et les fonctionnalités WebAssembly compilées depuis Rust (`sdk_client`).
## Vue d'ensemble du Processus
Le workflow de pairing est déclenché par la fonction `onCreateButtonClick()` et suit une séquence d'actions coordonnées entre l'interface utilisateur, les services JavaScript et le module WebAssembly.
## Étapes Détaillées du Workflow
### 1. Déclenchement Initial
**Fonction :** `onCreateButtonClick()`
**Localisation :** `src/utils/sp-address.utils.ts:152-160`
```@/home/ank/dev/ihm_client_dev2/src/utils/sp-address.utils.ts#152:160
async function onCreateButtonClick() {
try {
await prepareAndSendPairingTx();
// Don't call confirmPairing immediately - it will be called when the pairing process is complete
console.log('Pairing process initiated. Waiting for completion...');
} catch (e) {
console.error(`onCreateButtonClick error: ${e}`);
}
}
```
**Actions :**
- Appel de `prepareAndSendPairingTx()`
- Gestion d'erreur avec logging
- Attente de la complétion du processus
### 2. Préparation et Envoi de la Transaction de Pairing
**Fonction :** `prepareAndSendPairingTx()`
**Localisation :** `src/utils/sp-address.utils.ts:162-201`
**Actions principales :**
#### 2.1 Initialisation du Service
```typescript
const service = await Services.getInstance();
const relayAddress = service.getAllRelays();
```
#### 2.2 Création du Processus de Pairing
```typescript
const createPairingProcessReturn = await service.createPairingProcess("", []);
```
#### 2.3 Vérification des Connexions
```typescript
await service.checkConnections(
createPairingProcessReturn.updated_process.current_process,
createPairingProcessReturn.updated_process.current_process.states[0].state_id
);
```
#### 2.4 Configuration des Identifiants
```typescript
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
```
#### 2.5 Mise à Jour du Device
```typescript
const currentDevice = await service.getDeviceFromDatabase();
if (currentDevice) {
currentDevice.pairing_process_commitment = createPairingProcessReturn.updated_process.process_id;
await service.saveDeviceInDatabase(currentDevice);
}
```
#### 2.6 Traitement du Retour API
```typescript
await service.handleApiReturn(createPairingProcessReturn);
```
### 3. Création du Processus de Pairing (Côté Service)
**Fonction :** `createPairingProcess()`
**Localisation :** `src/services/service.ts:334-371`
**Paramètres :**
- `userName`: Nom d'utilisateur (vide dans ce cas)
- `pairWith`: Liste des adresses à coupler (vide initialement)
**Actions :**
#### 3.1 Vérification de l'État de Pairing
```typescript
if (this.sdkClient.is_paired()) {
throw new Error('Device already paired');
}
```
#### 3.2 Préparation des Données
```typescript
const myAddress: string = this.sdkClient.get_address();
pairWith.push(myAddress);
const privateData = {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName,
pairedAddresses: pairWith,
};
```
#### 3.3 Définition des Rôles
```typescript
const roles: Record<string, RoleDefinition> = {
pairing: {
members: [],
validation_rules: [{
quorum: 1.0,
fields: validation_fields,
min_sig_member: 1.0,
}],
storages: [STORAGEURL]
},
};
```
#### 3.4 Appel de Création du Processus
```typescript
return this.createProcess(privateData, publicData, roles);
```
### 4. Création du Processus (Côté WebAssembly)
**Fonction :** `create_new_process()`
**Localisation :** `sdk_client/src/api.rs:1218-1264`
**Actions principales :**
#### 4.1 Validation des Rôles
```rust
if roles.is_empty() {
return Err(ApiError { message: "Roles can't be empty".to_owned() });
}
```
#### 4.2 Création de la Transaction
```rust
let relay_address: SilentPaymentAddress = relay_address.try_into()?;
let tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &vec![relay_address], fee_rate_checked)?;
let unsigned_transaction = SpClient::finalize_transaction(tx)?;
```
#### 4.3 Gestion des Secrets Partagés
```rust
let new_secrets = get_shared_secrets_in_transaction(&unsigned_transaction, &vec![relay_address])?;
let mut shared_secrets = lock_shared_secrets()?;
for (address, secret) in new_secrets {
shared_secrets.confirm_secret_for_address(secret, address);
}
```
#### 4.4 Création de l'État du Processus
```rust
let process_id = OutPoint::new(unsigned_transaction.unsigned_tx.as_ref().unwrap().txid(), 0);
let mut new_state = ProcessState::new(process_id, private_data.clone(), public_data.clone(), roles.clone())?;
let mut process = Process::new(process_id);
```
### 5. Vérification des Connexions
**Fonction :** `checkConnections()`
**Localisation :** `src/services/service.ts:232-289`
**Actions :**
#### 5.1 Validation des États
```typescript
if (process.states.length < 2) {
throw new Error('Process doesn\'t have any state yet');
}
```
#### 5.2 Extraction des Rôles et Membres
```typescript
let roles: Record<string, RoleDefinition> | null = null;
if (!stateId) {
roles = process.states[process.states.length - 2].roles;
} else {
roles = process.states.find(state => state.state_id === stateId)?.roles || null;
}
```
#### 5.3 Gestion des Processus de Pairing
```typescript
if (members.size === 0) {
// This must be a pairing process
const publicData = process.states[0]?.public_data;
if (!publicData || !publicData['pairedAddresses']) {
throw new Error('Not a pairing process');
}
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
members.add({ sp_addresses: decodedAddresses });
}
```
#### 5.4 Connexion aux Adresses Non Connectées
```typescript
if (unconnectedAddresses && unconnectedAddresses.size != 0) {
const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses));
await this.handleApiReturn(apiResult);
}
```
### 6. Traitement du Retour API
**Fonction :** `handleApiReturn()`
**Localisation :** `src/services/service.ts:648-775`
**Actions principales :**
#### 6.1 Signature de Transaction
```typescript
if (apiReturn.partial_tx) {
const res = this.sdkClient.sign_transaction(apiReturn.partial_tx);
apiReturn.new_tx_to_send = res.new_tx_to_send;
}
```
#### 6.2 Envoi de Transaction
```typescript
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
await new Promise(r => setTimeout(r, 500));
}
```
#### 6.3 Gestion des Secrets
```typescript
if (apiReturn.secrets) {
const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets;
const confirmedSecrets = apiReturn.secrets.shared_secrets;
// Sauvegarde en base de données
}
```
#### 6.4 Mise à Jour du Processus
```typescript
if (apiReturn.updated_process) {
const updatedProcess = apiReturn.updated_process;
const processId: string = updatedProcess.process_id;
await this.saveProcessToDb(processId, updatedProcess.current_process);
}
```
#### 6.5 Confirmation Automatique du Pairing
```typescript
// Check if this is a pairing process that's ready for confirmation
const existingDevice = await this.getDeviceFromDatabase();
if (existingDevice && existingDevice.pairing_process_commitment === processId) {
const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1];
if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) {
console.log('Pairing process updated with paired addresses, confirming pairing...');
await this.confirmPairing();
}
}
```
### 7. Confirmation du Pairing
**Fonction :** `confirmPairing()`
**Localisation :** `src/services/service.ts:793-851`
**Actions :**
#### 7.1 Récupération du Processus de Pairing
```typescript
const existingDevice = await this.getDeviceFromDatabase();
const pairingProcessId = existingDevice.pairing_process_commitment;
const myPairingProcess = await this.getProcess(pairingProcessId);
```
#### 7.2 Extraction des Adresses Couplées
```typescript
let myPairingState = this.getLastCommitedState(myPairingProcess);
const encodedSpAddressList = myPairingState.public_data['pairedAddresses'];
const spAddressList = this.decodeValue(encodedSpAddressList);
```
#### 7.3 Pairing Effectif du Device
```typescript
this.sdkClient.unpair_device(); // Clear any existing pairing
this.sdkClient.pair_device(pairingProcessId, spAddressList);
```
#### 7.4 Sauvegarde du Device Mis à Jour
```typescript
const newDevice = this.dumpDeviceFromMemory();
newDevice.pairing_process_commitment = pairingProcessId;
await this.saveDeviceInDatabase(newDevice);
```
## Architecture Technique
### Côté Client (TypeScript)
**Composants principaux :**
- **Interface Utilisateur :** Gestion des événements de clic et affichage des emojis
- **Services :** Orchestration du workflow et communication avec WebAssembly
- **Base de Données :** Stockage des devices, secrets et processus
- **WebSocket :** Communication avec les relais
### Côté WebAssembly (Rust)
**Fonctionnalités clés :**
- **Gestion des Wallets :** Création et manipulation des portefeuilles Silent Payment
- **Cryptographie :** Génération de secrets partagés et signatures
- **Transactions :** Création et finalisation des transactions Bitcoin
- **Processus :** Gestion des états et validation des changements
## Types de Données Importants
### Device
```typescript
interface Device {
sp_wallet: SpWallet;
pairing_process_commitment: OutPoint | null;
paired_member: Member;
}
```
### Process
```typescript
interface Process {
states: ProcessState[];
}
```
### ApiReturn
```typescript
interface ApiReturn {
secrets: SecretsStore | null;
updated_process: UpdatedProcess | null;
new_tx_to_send: NewTxMessage | null;
ciphers_to_send: string[];
commit_to_send: CommitMessage | null;
push_to_storage: string[];
partial_tx: TsUnsignedTransaction | null;
}
```
## Flux de Communication
1. **UI → Service :** Déclenchement du processus via `onCreateButtonClick()`
2. **Service → WebAssembly :** Appel des fonctions WASM pour la création du processus
3. **WebAssembly → Service :** Retour des données via `ApiReturn`
4. **Service → WebSocket :** Envoi des messages aux relais
5. **WebSocket → Service :** Réception des réponses des relais
6. **Service → Database :** Sauvegarde des états et secrets
7. **Service → UI :** Mise à jour de l'interface utilisateur
## Points d'Attention
### Sécurité
- Les secrets partagés sont générés côté WebAssembly (Rust)
- Les transactions sont signées de manière sécurisée
- Les données sensibles sont chiffrées avant stockage
### Asynchronisme
- Le processus est entièrement asynchrone
- Utilisation de promesses et callbacks pour la coordination
- Gestion d'erreur à chaque étape critique
### État du Processus
- Suivi de l'état via `processId` et `stateId`
- Sauvegarde persistante en base de données
- Récupération possible en cas d'interruption
## Conclusion
Le workflow de pairing représente un processus complexe mais bien structuré qui coordonne l'interface utilisateur TypeScript avec les fonctionnalités cryptographiques Rust via WebAssembly. Cette architecture permet une sécurité maximale tout en maintenant une expérience utilisateur fluide. Le processus est conçu pour être résilient aux interruptions et permet une récupération d'état en cas de problème.
La séparation claire entre les responsabilités (UI, orchestration, cryptographie) facilite la maintenance et les évolutions futures du système de pairing.

View File

@ -1,5 +1,5 @@
import { SignatureElement } from './signature';
import signatureCss from '../../../public/style/signature.css?raw'
import signatureCss from '../../../style/signature.css?raw'
import Services from '../../services/service.js'
class SignatureComponent extends HTMLElement {

View File

@ -1,4 +1,4 @@
import signatureStyle from '../../../public/style/signature.css?inline';
import signatureStyle from '../../../style/signature.css?inline';
declare global {
interface Window {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,16 +3,31 @@ import axios, { AxiosResponse } from 'axios';
export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
for (const server of servers) {
try {
// Handle relative paths (for development proxy) vs absolute URLs (for production)
// --- DÉBUT DE LA CORRECTION ---
// 1. On vérifie d'abord si la donnée existe en appelant le bon service
// On passe 'server' au lieu de 'url' pour que testData construise la bonne URL
const dataExists = await testData(server, key);
if (dataExists) {
console.log('Data already stored:', key);
continue;
} else {
console.log('Data not stored for server, proceeding to POST:', key, server);
}
// --- FIN DE LA CORRECTION ---
// Construction de l'URL pour le POST (stockage)
// Cette partie était correcte
let url: string;
if (server.startsWith('/')) {
// Relative path - construct manually for proxy
// Relative path
url = `${server}/store/${encodeURIComponent(key)}`;
if (ttl !== null) {
url += `?ttl=${ttl}`;
}
} else {
// Absolute URL - use URL constructor
// Absolute URL
const urlObj = new URL(`${server}/store/${encodeURIComponent(key)}`);
if (ttl !== null) {
urlObj.searchParams.append('ttl', ttl.toString());
@ -20,17 +35,11 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
url = urlObj.toString();
}
// Test first that data is not already stored
const testResponse = await testData(url, key);
if (testResponse) {
console.log('Data already stored:', key);
continue;
} else {
console.log('Data not stored for server:', key, server);
}
// La ligne ci-dessous a été supprimée car le test est fait au-dessus
// const testResponse = await testData(url, key); // <-- LIGNE BOGUÉE SUPPRIMÉE
// Send the encrypted ArrayBuffer as the raw request body.
const response = await axios.post(url, value, {
// Send the encrypted data as the raw request body.
const response = await axios.post(url, value, { // Note: c'est bien un POST sur 'url'
headers: {
'Content-Type': 'application/octet-stream'
},
@ -43,7 +52,7 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
return response;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 409) {
return null;
return null; // 409 Conflict (Key already exists)
}
console.error('Error storing data:', error);
}
@ -51,22 +60,21 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
return null;
}
// Fonction retrieveData (inchangée, elle était correcte)
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
for (const server of servers) {
try {
// Handle relative paths (for development proxy) vs absolute URLs (for production)
const url = server.startsWith('/')
? `${server}/retrieve/${key}` // Relative path - use as-is for proxy
: new URL(`${server}/retrieve/${key}`).toString(); // Absolute URL - construct properly
? `${server}/retrieve/${key}`
: new URL(`${server}/retrieve/${key}`).toString();
console.log('Retrieving data', key,' from:', url);
// When fetching the data from the server:
const response = await axios.get(url, {
responseType: 'arraybuffer'
});
if (response.status === 200) {
// Validate that we received an ArrayBuffer
if (response.data instanceof ArrayBuffer) {
return response.data;
} else {
@ -80,8 +88,9 @@ export async function retrieveData(servers: string[], key: string): Promise<Arra
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
// C'est normal si la donnée n'existe pas
console.log(`Data not found on server ${server} for key ${key}`);
continue; // Try next server
continue;
} else if (error.response?.status) {
console.error(`Server ${server} error ${error.response.status}:`, error.response.statusText);
continue;
@ -103,17 +112,27 @@ interface TestResponse {
value: boolean;
}
export async function testData(url: string, key: string): Promise<boolean | null> {
// --- FONCTION testData CORRIGÉE ---
// Elle prend 'server' au lieu de 'url' et construit sa propre URL '/test/...'
export async function testData(server: string, key: string): Promise<boolean> {
try {
const response = await axios.get(url);
if (response.status !== 200) {
console.error(`Test response status: ${response.status}`);
return false;
}
// Construit l'URL /test/...
const testUrl = server.startsWith('/')
? `${server}/test/${encodeURIComponent(key)}`
: new URL(`${server}/test/${encodeURIComponent(key)}`).toString();
const response = await axios.get(testUrl); // Fait un GET sur /test/...
// 200 OK = la donnée existe
return response.status === 200;
return true;
} catch (error) {
console.error('Error testing data:', error);
return null;
if (axios.isAxiosError(error) && error.response?.status === 404) {
// 404 Not Found = la donnée n'existe pas. C'est une réponse valide.
return false;
}
// Toute autre erreur (serveur offline, 500, etc.)
console.error('Error testing data:', error);
return false; // On considère que le test a échoué
}
}

View File

@ -100,7 +100,7 @@ export async function displayEmojis(text: string) {
// Verify Other address
export function initAddressInput() {
const container = getCorrectDOM('login-4nk-component') as HTMLElement
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const addressInput = container.querySelector('#addressInput') as HTMLInputElement;
const emojiDisplay = container.querySelector('#emoji-display-2');
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
@ -155,7 +155,7 @@ async function onCreateButtonClick() {
// Don't call confirmPairing immediately - it will be called when the pairing process is complete
console.log('Pairing process initiated. Waiting for completion...');
} catch (e) {
console.error(`onCreateButtonClick error: ${e}`);
console.error(`onCreateButtonClick error: ${e}`);
}
}
@ -164,17 +164,14 @@ export async function prepareAndSendPairingTx(): Promise<void> {
try {
const relayAddress = service.getAllRelays();
const createPairingProcessReturn = await service.createPairingProcess(
"",
[],
);
const createPairingProcessReturn = await service.createPairingProcess('', []);
if (!createPairingProcessReturn.updated_process) {
throw new Error('createPairingProcess returned an empty new process');
}
try {
await service.checkConnections(createPairingProcessReturn.updated_process.current_process, createPairingProcessReturn.updated_process.current_process.states[0].state_id);
await service.ensureConnections(createPairingProcessReturn.updated_process.current_process, createPairingProcessReturn.updated_process.current_process.states[0].state_id);
} catch (e) {
throw e;
}
@ -194,7 +191,6 @@ export async function prepareAndSendPairingTx(): Promise<void> {
}
await service.handleApiReturn(createPairingProcessReturn);
} catch (err) {
console.error(err);
}
@ -202,7 +198,7 @@ export async function prepareAndSendPairingTx(): Promise<void> {
export async function generateQRCode(spAddress: string) {
try {
const container = getCorrectDOM('login-4nk-component') as HTMLElement
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const currentUrl = 'https://' + window.location.host;
const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress);
const qrCode = container?.querySelector('.qr-code img');
@ -213,9 +209,9 @@ export async function generateQRCode(spAddress: string) {
}
export async function generateCreateBtn() {
try{
//Generate CreateBtn
const container = getCorrectDOM('login-4nk-component') as HTMLElement
try {
// Generate CreateBtn
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const createBtn = container?.querySelector('.create-btn');
if (createBtn) {
createBtn.textContent = 'CREATE';
@ -223,5 +219,4 @@ export async function generateCreateBtn() {
} catch (err) {
console.error(err);
}
}