Compare commits

...

87 Commits

Author SHA1 Message Date
afe45ad960 Add optional argument promptName to
prepareAndSendPairingTx
2025-03-17 17:11:16 +01:00
ed0c11b527 Add dumpNeuteredDevice 2025-03-17 17:10:58 +01:00
0cfb02b06a [bug] empty strings doesn't count as missing data 2025-03-17 17:10:40 +01:00
f00faa1841 fix_bug_create_device 2025-03-17 17:10:17 +01:00
1952ade39c rolesContains{Us, Member} doesn't call wasm 2025-03-12 15:46:39 +01:00
e62a04ea57 Fix createDmProcess 2025-03-12 15:46:39 +01:00
b46a9aa13a Fix createProcessPairing 2025-03-12 15:46:39 +01:00
2aa846b8e5 [bug] don't add empty states to states we need to monitor 2025-03-12 15:46:39 +01:00
57bd945599 Make service worker skips empty states and public fields 2025-03-12 15:46:39 +01:00
c82c219a28 Update approveChange 2025-03-12 15:46:39 +01:00
3a851d1b62 fusionDev1Dev3 2025-03-12 10:56:26 +01:00
4ff2965b49 [WIP] Add an account creation modal 2025-03-07 16:44:11 +01:00
4d0c3e3f56 Add create accont Btn and update prepareAndSendPairingTx() 2025-03-07 16:42:28 +01:00
b9c4dfbfd9 Add fn to populate the member select form 2025-03-07 16:39:20 +01:00
cc0d823125 Update home page 2025-03-07 16:36:39 +01:00
090bdbedaa Various fix in chat 2025-03-04 14:55:57 +01:00
722e08ea82 Fix handleHandshakeMsg 2025-03-04 14:55:35 +01:00
3785285e4d decryptAttribute request missing data 2025-03-04 14:54:56 +01:00
3d09a20512 updateProcess takes a default empty public_data 2025-03-04 14:54:23 +01:00
de6213b1b7 requestDataFromPeers takes roles 2025-03-04 14:53:56 +01:00
dc231ba982 Don't post SCAN message when no process 2025-03-04 14:51:43 +01:00
570f98c51c Add roles to UserDiff when encountering missing data 2025-03-04 14:50:57 +01:00
b6c2d211e5 Update more aggressively processes on relay notifications 2025-03-03 23:24:41 +01:00
bde2ed0e44 Better handling of commitment errors 2025-03-03 23:24:15 +01:00
15626201a8 Replace descriptions with public_data 2025-03-03 23:23:59 +01:00
bb0d998c9f Add memberPublicName to pairing process 2025-03-03 23:23:38 +01:00
1747908d20 Take a username at pairing 2025-03-03 23:22:28 +01:00
7ad8063e87 Remove dead code 2025-03-03 21:27:30 +01:00
c32a7f9e86 Exclude pairing process from the chat list 2025-03-03 21:26:58 +01:00
7ebc5a75ea Redirect to chat after pairing 2025-03-03 21:26:25 +01:00
38b5567b68 Refactor loadAllMembers 2025-03-03 19:03:19 +01:00
e566c17a9a Add getPublicData 2025-03-03 19:02:43 +01:00
d243b58101 Refactor loadProcesses, display process name 2025-03-03 17:19:09 +01:00
6e57225529 Add getProcessName 2025-03-03 17:18:14 +01:00
7f6eabaa90 Remove unnecessary async for getRoles 2025-03-03 17:17:54 +01:00
8f34f2bcbe Change default page to chat 2025-03-03 16:36:19 +01:00
1bdb66b583 Properly compare members addresses instead of stringifying them 2025-03-03 10:53:13 +01:00
837ae35be9 Remove unnecessary check in lookForMyDms 2025-03-03 10:52:24 +01:00
4eb24d9678 [bug] forget to await sendMessage when pressing key 2025-03-03 10:51:47 +01:00
b4d35b5dc8 [bug] didn't set selectedChatProcessId when creating new dm 2025-03-03 10:50:35 +01:00
9f05099338 Remove dead code 2025-02-28 18:33:09 +01:00
f2991dd130 Remove some unused variables 2025-02-28 18:26:19 +01:00
c8d049ce85 getAllMembers and getAllMembersSorted 2025-02-28 18:26:09 +01:00
5bda1736e1 Don't send process-updated event for process unchanged 2025-02-28 18:25:43 +01:00
db07abaa8a Fixed process highlighting + minor improvements 2025-02-28 18:24:51 +01:00
a1aec7cec3 Fix process screen 2025-02-28 16:44:23 +01:00
7c3e263b8a Chat updates when received message 2025-02-27 12:34:56 +01:00
ed578be468 Add isLoading variable 2025-02-26 09:33:07 +01:00
d950ce0a2b refactor service 2025-02-26 09:32:00 +01:00
b7a2f3a058 Refactor database.service 2025-02-26 09:29:36 +01:00
aa22b78121 Refactor worker 2025-02-26 09:28:30 +01:00
85a2296d56 Minor fixes 2025-02-24 11:06:07 +01:00
6180a66405 Refresh Member list when create a Dm Process 2025-02-24 11:05:45 +01:00
8ce490eb7b Update lookForMyDms 2025-02-24 11:03:30 +01:00
0fe9a6f371 [bug] sorted members to the top of the list 2025-02-21 11:52:48 +01:00
a16e984ecd fonction disconnect 2025-02-20 10:20:54 +01:00
a3542a48e9 Button disconnect 2025-02-20 10:20:54 +01:00
512a9025b1 working chat 2025-02-20 10:03:09 +01:00
3be5efd60c Get messages from Db 2025-02-17 15:07:05 +01:00
7bf0fb2b4f WIP (to be recommited smaller later) 2025-02-14 16:25:27 +01:00
a2a7022c08 Fix worker 2025-02-14 16:24:55 +01:00
682ab03861 Minor improvements in the worker 2025-02-13 10:22:05 +01:00
57684eba6b Add getBlobFromDb 2025-02-13 10:21:44 +01:00
422fa2469b Handle push_to_storage in apiReturn 2025-02-13 10:21:21 +01:00
208da9b819 Add saveProcessDataToDb 2025-02-13 10:20:52 +01:00
d6de96910a Fix addObject 2025-02-13 10:20:12 +01:00
9dda743ba0 Add requestDataFromPeers 2025-02-13 10:19:44 +01:00
091a8d4bd2 Fix storage requests 2025-02-13 10:18:41 +01:00
5a94888a78 updateMyProcesses on init of the router 2025-02-11 23:31:26 +01:00
8d267f5b12 refactoring of database.service + fixes 2025-02-11 23:31:09 +01:00
8fbcb769d6 [bug] process page can find all processes 2025-02-11 23:30:51 +01:00
239ddae893 getMyProcesses actually inspect all processes to find ours 2025-02-11 23:29:39 +01:00
a380ee4f29 handleHandshakeMsg only responsible for saving public info of processes 2025-02-11 23:28:25 +01:00
4225f41ca0 [bug] pairing was triggered by any process update 2025-02-11 23:24:20 +01:00
939c640e8f Add getBlob() and addDiff() + fixes to scanMissingData 2025-02-11 23:23:10 +01:00
1cdf61f8fe Add functions to return processes from Service 2025-02-11 16:19:36 +01:00
7403fb7a5e Get processes from Service 2025-02-11 16:01:19 +01:00
1a6e6d595e Add saveBlobToDb 2025-02-11 09:21:03 +01:00
5eef7c6981 Add AnkData store to indexedDB 2025-02-11 09:21:03 +01:00
d5c5224a9f nullable persistent message channels 2025-02-11 09:21:03 +01:00
463f4d952c Add updateMyProcesses 2025-02-11 09:21:03 +01:00
cb2fea029e Update getRoles to work with roles out of pcd 2025-02-11 09:21:03 +01:00
369c83af3a Move roles out of pairing process 2025-02-11 09:21:03 +01:00
0f3d9b4920 Update process list udpate 2025-02-11 09:21:03 +01:00
3813f542f2 Add UPDATE_PROCESSES logic to the worker 2025-02-11 09:21:03 +01:00
51664e62ec getCipherForDiff catch missing fields in encrypted_pcd 2025-02-10 17:59:40 +01:00
cc2f960af0 Update openPairingConfirmationModal 2025-02-10 17:57:33 +01:00
20 changed files with 1707 additions and 1269 deletions

View File

@ -5,7 +5,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client/pkg ../sdk_client --target bundler --dev", "build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
"start": "vite --host 0.0.0.0", "start": "vite --host 0.0.0.0",
"build": "tsc && vite build", "build": "tsc && vite build",
"deploy": "sudo cp -r dist/* /var/www/html/", "deploy": "sudo cp -r dist/* /var/www/html/",

View File

@ -599,7 +599,7 @@ body {
margin-top: 9vh; margin-top: 9vh;
margin-left: -1%; margin-left: -1%;
text-align: left; text-align: left;
width: 209vh; width: 100vw;
} }
/* Liste des information sur l'account */ /* Liste des information sur l'account */
@ -1364,3 +1364,62 @@ body {
border-radius: 50%; border-radius: 50%;
border: 2px solid white; border: 2px solid white;
} }
/* ---------------------Style pour le QR code--------------------- */
.qr-code {
width: 50px;
height: 50px;
cursor: pointer;
transition: transform 0.2s ease;
}
.qr-code:hover {
transform: scale(1.5);
}
.qr-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.qr-modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
position: relative;
text-align: center;
}
.close-qr-modal {
position: absolute;
right: 10px;
top: 5px;
font-size: 24px;
cursor: pointer;
color: #666;
}
.close-qr-modal:hover {
color: #000;
}
.qr-code-large {
max-width: 300px;
margin: 10px 0;
}
.qr-address {
margin-top: 10px;
word-break: break-all;
font-size: 12px;
color: #666;
}

View File

@ -186,6 +186,28 @@ body {
} }
.group-list .member-container {
position: relative;
}
.group-list .member-container button {
margin-left: 40px;
padding: 5px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
border-radius: 50px;
position: absolute;
top: -25px;
right: -25px;
}
.group-list .member-container button:hover {
background: var(--accent-color)
}
/* Zone de chat */ /* Zone de chat */
.chat-area { .chat-area {
display: flex; display: flex;
@ -553,3 +575,23 @@ body {
margin: 0; margin: 0;
} }
} }
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: var(--primary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}

View File

@ -316,7 +316,7 @@ h1 {
margin-top: 0px; margin-top: 0px;
} }
.sp-address-btn { .create-btn {
margin-bottom: 2em; margin-bottom: 2em;
cursor: pointer; cursor: pointer;
background-color: #d0d0d7; background-color: #d0d0d7;
@ -778,3 +778,41 @@ select[data-multi-select-plugin] {
.process-card-action { .process-card-action {
width: 100%; width: 100%;
} }
/**************************************** Select Member Home Page ******************************************************/
.custom-select {
width: 100%;
max-height: 150px;
overflow-y: auto;
direction: ltr;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
}
.custom-select option {
padding: 8px 12px;
cursor: pointer;
}
.custom-select option:hover {
background-color: #f0f0f0;
}
.custom-select::-webkit-scrollbar {
width: 8px;
}
.custom-select::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-select::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.custom-select::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@ -29,7 +29,7 @@
<a onclick="navigate('chat')">Chat</a> <a onclick="navigate('chat')">Chat</a>
<a onclick="navigate('signature')">Signatures</a> <a onclick="navigate('signature')">Signatures</a>
<a onclick="navigate('process')">Process</a> <a onclick="navigate('process')">Process</a>
<a onclick="navigate('home')">Disconnect</a> <a onclick="disconnect()">Disconnect</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -181,3 +181,40 @@ async function createBackUp() {
} }
(window as any).createBackUp = createBackUp; (window as any).createBackUp = createBackUp;
async function disconnect() {
console.log('Disconnecting...');
try {
localStorage.clear();
await new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase('4nk');
request.onsuccess = () => {
console.log('IndexedDB deleted successfully');
resolve();
};
request.onerror = () => reject(request.error);
request.onblocked = () => {
console.log('Database deletion was blocked');
resolve();
};
});
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(registration => registration.unregister()));
console.log('Service worker unregistered');
navigate('home');
setTimeout(() => {
window.location.href = window.location.origin;
}, 100);
} catch (error) {
console.error('Error during disconnect:', error);
// force reload
window.location.href = window.location.origin;
}
}
(window as any).disconnect = disconnect;

View File

@ -0,0 +1,14 @@
<div id="creation-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Do you want to create a 4NK member?<br />
Attempting to create a member with address <br />
<strong>{{device1}}</strong> <br />
</div>
<div class="confirmation-box">
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@ declare global {
interface Window { interface Window {
initAccount: () => void; initAccount: () => void;
showContractPopup: (contractId: string) => void; showContractPopup: (contractId: string) => void;
showPairing: () => void; showPairing: () => Promise<void>;
showWallet: () => void; showWallet: () => void;
showData: () => void; showData: () => void;
addWalletRow: () => void; addWalletRow: () => void;
@ -36,6 +36,7 @@ declare global {
generateRecoveryWords: () => string[]; generateRecoveryWords: () => string[];
exportUserData: () => void; exportUserData: () => void;
updateActionButtons: () => void; updateActionButtons: () => void;
showQRCodeModal: (address: string) => void;
} }
} }
@ -45,6 +46,7 @@ import { Row, WalletRow, DataRow, Notification, Contract, NotificationMessage }
import { addressToEmoji } from '../../utils/sp-address.utils'; import { addressToEmoji } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/document.utils'; import { getCorrectDOM } from '../../utils/document.utils';
import accountStyle from '../../../public/style/account.css?inline'; import accountStyle from '../../../public/style/account.css?inline';
import Services from '../../services/service';
let isAddingRow = false; let isAddingRow = false;
let currentRow: HTMLTableRowElement | null = null; let currentRow: HTMLTableRowElement | null = null;
@ -197,6 +199,7 @@ class AccountElement extends HTMLElement {
window.updateActionButtons = () => this.updateActionButtons(); window.updateActionButtons = () => this.updateActionButtons();
window.openAvatarPopup = () => this.openAvatarPopup(); window.openAvatarPopup = () => this.openAvatarPopup();
window.closeAvatarPopup = () => this.closeAvatarPopup(); window.closeAvatarPopup = () => this.closeAvatarPopup();
window.showQRCodeModal = (address: string) => this.showQRCodeModal(address);
if (!localStorage.getItem('rows')) { if (!localStorage.getItem('rows')) {
localStorage.setItem('rows', JSON.stringify(defaultRows)); localStorage.setItem('rows', JSON.stringify(defaultRows));
@ -552,6 +555,13 @@ private updateTableContent(rows: Row[]): void {
<td>${row.column1}</td> <td>${row.column1}</td>
<td class="device-name" onclick="window.editDeviceName(this)">${row.column2}</td> <td class="device-name" onclick="window.editDeviceName(this)">${row.column2}</td>
<td>${row.column3}</td> <td>${row.column3}</td>
<td>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=50x50&data=${encodeURIComponent(row.column1)}"
alt="QR Code"
title="${row.column1}"
class="qr-code"
onclick="window.showQRCodeModal('${encodeURIComponent(row.column1)}')">
</td>
<td> <td>
<button class="delete-button" onclick="window.deleteRow(this)"> <button class="delete-button" onclick="window.deleteRow(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill="red"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill="red">
@ -627,29 +637,26 @@ private deleteRow(button: HTMLButtonElement): void {
const table = row.closest('tbody'); const table = row.closest('tbody');
if (!table) return; if (!table) return;
// Vérifier le nombre de lignes restantes
const remainingRows = table.getElementsByTagName('tr').length; const remainingRows = table.getElementsByTagName('tr').length;
if (remainingRows <= 2) { if (remainingRows <= 2) {
this.showAlert('You must keep at least 2 devices paired'); this.showAlert('You must keep at least 2 devices paired');
return; return;
} }
// Animation de suppression const index = Array.from(table.children).indexOf(row);
row.style.transition = 'opacity 0.3s'; row.style.transition = 'opacity 0.3s, transform 0.3s';
row.style.opacity = '0'; row.style.opacity = '0';
row.style.transform = 'translateX(-100%)';
setTimeout(() => { setTimeout(() => {
// Obtenir l'index avant la suppression
const index = Array.from(table.children).indexOf(row);
// Supprimer la ligne du DOM
row.remove(); row.remove();
// Mettre à jour le localStorage
const storageKey = STORAGE_KEYS[currentMode]; const storageKey = STORAGE_KEYS[currentMode];
const rows = JSON.parse(localStorage.getItem(storageKey) || '[]'); const rows = JSON.parse(localStorage.getItem(storageKey) || '[]');
if (index > -1) {
rows.splice(index, 1); rows.splice(index, 1);
localStorage.setItem(storageKey, JSON.stringify(rows)); localStorage.setItem(storageKey, JSON.stringify(rows));
}
}, 300); }, 300);
} }
@ -887,9 +894,6 @@ private showContractPopup(contractId: string) {
}); });
} }
// Ajouter à l'objet window
// Fonction utilitaire pour cacher tous les contenus // Fonction utilitaire pour cacher tous les contenus
private hideAllContent(): void { private hideAllContent(): void {
const contents = ['pairing-content', 'wallet-content', 'process-content', 'data-content']; const contents = ['pairing-content', 'wallet-content', 'process-content', 'data-content'];
@ -902,25 +906,22 @@ private hideAllContent(): void {
} }
// Fonctions d'affichage des sections // Fonctions d'affichage des sections
private showPairing(): void { private async showPairing(): Promise<void> {
const service = await Services.getInstance();
const spAddress = await service.getDeviceAddress();
isAddingRow = false; isAddingRow = false;
currentRow = null; currentRow = null;
currentMode = 'pairing'; currentMode = 'pairing';
// Cacher tous les contenus
this.hideAllContent(); this.hideAllContent();
// Mettre à jour le titre
const headerElement = this.shadowRoot?.getElementById('parameter-header'); const headerElement = this.shadowRoot?.getElementById('parameter-header');
if (headerElement) { if (headerElement) {
headerElement.textContent = 'Pairing'; headerElement.textContent = 'Pairing';
} }
// Afficher le contenu de pairing
const pairingContent = this.shadowRoot?.getElementById('pairing-content'); const pairingContent = this.shadowRoot?.getElementById('pairing-content');
if (pairingContent) { if (pairingContent) {
pairingContent.style.display = 'block'; pairingContent.style.display = 'block';
pairingContent.innerHTML = ` pairingContent.innerHTML = `
@ -932,7 +933,8 @@ private showPairing(): void {
<th>SP Address</th> <th>SP Address</th>
<th>Device Name</th> <th>Device Name</th>
<th>SP Emojis</th> <th>SP Emojis</th>
<th></th> <th>QR Code</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -943,8 +945,46 @@ private showPairing(): void {
</div> </div>
`; `;
// Mettre à jour le contenu du tableau let rows = JSON.parse(localStorage.getItem(STORAGE_KEYS.pairing) || '[]');
const rows = JSON.parse(localStorage.getItem(STORAGE_KEYS.pairing) || '[]');
const deviceExists = rows.some((row: Row) => row.column1 === spAddress);
if (!deviceExists && spAddress) {
const emojis = await addressToEmoji(spAddress);
try {
// Déboguer le processus de pairing
const pairingProcessId = await service.getPairingProcessId();
console.log('Pairing Process ID:', pairingProcessId);
const pairingProcess = await service.getProcess(pairingProcessId);
console.log('Pairing Process:', pairingProcess);
const userName = pairingProcess?.states?.[0]?.metadata?.userName
|| pairingProcess?.states?.[0]?.metadata?.name
|| localStorage.getItem('userName')
console.log('Username found:', userName);
const newRow = {
column1: spAddress,
column2: userName,
column3: emojis
};
rows = [newRow, ...rows];
localStorage.setItem(STORAGE_KEYS.pairing, JSON.stringify(rows));
} catch (error) {
console.error('Error getting pairing process:', error);
const newRow = {
column1: spAddress,
column2: 'This Device',
column3: emojis
};
rows = [newRow, ...rows];
localStorage.setItem(STORAGE_KEYS.pairing, JSON.stringify(rows));
}
}
this.updateTableContent(rows); this.updateTableContent(rows);
} }
} }
@ -1419,6 +1459,28 @@ private initializeEventListeners() {
avatarInput.addEventListener('change', this.handleAvatarUpload.bind(this)); avatarInput.addEventListener('change', this.handleAvatarUpload.bind(this));
} }
} }
private showQRCodeModal(address: string): void {
const modal = document.createElement('div');
modal.className = 'qr-modal';
modal.innerHTML = `
<div class="qr-modal-content">
<span class="close-qr-modal">&times;</span>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${address}"
alt="QR Code Large"
class="qr-code-large">
<div class="qr-address">${decodeURIComponent(address)}</div>
</div>
`;
this.shadowRoot?.appendChild(modal);
const closeBtn = modal.querySelector('.close-qr-modal');
closeBtn?.addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
}
} }
customElements.define('account-element', AccountElement); customElements.define('account-element', AccountElement);

File diff suppressed because it is too large Load Diff

View File

@ -4,24 +4,23 @@
<div class="tab-container"> <div class="tab-container">
<div class="tabs"> <div class="tabs">
<div class="tab active" data-tab="tab1">Scan QR Code</div> <div class="tab active" data-tab="tab1">Create an account</div>
<div class="tab" data-tab="tab2">Scan other device</div> <div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
</div> </div>
</div> </div>
<div class="page-container"> <div class="page-container">
<div id="tab1" class="card tab-content active"> <div id="tab1" class="card tab-content active">
<div class="card-description">Scan with your other device :</div> <div class="card-description">Create an account :</div>
<div class="pairing-request"></div> <div class="pairing-request"></div>
<div class="card-image qr-code"> <!-- <div class="card-image qr-code">
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" /> <img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
</div> </div> -->
<button id="copyBtn" class="sp-address-btn"></button> <button id="createButton" class="create-btn"></button>
<div class="card-image emoji-display" id="emoji-display"></div>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div id="tab2" class="card tab-content"> <div id="tab2" class="card tab-content">
<div class="card-description">Scan your other device :</div> <div class="card-description">Add a device for an existing member :</div>
<div class="card-image camera-card"> <div class="card-image camera-card">
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" /> <img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
<button id="scan-btn" onclick="scanDevice()">Scan</button> <button id="scan-btn" onclick="scanDevice()">Scan</button>
@ -31,8 +30,13 @@
</div> </div>
</div> </div>
<p>Or</p> <p>Or</p>
<input type="text" id="addressInput" placeholder="Paste address" /> <!-- <input type="text" id="addressInput" placeholder="Paste address" />
<div id="emoji-display-2"></div> <div id="emoji-display-2"></div> -->
<div class="card-description">Chose a member :</div>
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
<!-- Options -->
</select>
<button id="okButton" style="display: none">OK</button> <button id="okButton" style="display: none">OK</button>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import Routing from '../../services/modal.service'; import Routing from '../../services/modal.service';
import Services from '../../services/service'; import Services from '../../services/service';
import { addSubscription } from '../../utils/subscription.utils'; import { addSubscription } from '../../utils/subscription.utils';
import { displayEmojis, generateQRCode } from '../../utils/sp-address.utils'; import { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/html.utils'; import { getCorrectDOM } from '../../utils/html.utils';
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component'; import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
export { QrScannerComponent }; export { QrScannerComponent };
@ -20,8 +20,12 @@ export async function initHomePage(): Promise<void> {
const service = await Services.getInstance(); const service = await Services.getInstance();
const spAddress = await service.getDeviceAddress(); const spAddress = await service.getDeviceAddress();
generateQRCode(spAddress); // generateQRCode(spAddress);
generateCreateBtn ();
displayEmojis(spAddress); displayEmojis(spAddress);
// Add this line to populate the select when the page loads
await populateMemberSelect();
} }
//// Modal //// Modal
@ -45,4 +49,46 @@ function scanDevice() {
if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>'; if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';
} }
async function populateMemberSelect() {
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
if (!memberSelect) {
console.error('Could not find memberSelect element');
return;
}
const service = await Services.getInstance();
const members = service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const process = await service.getProcess(processId);
let memberPublicName;
if (process) {
const publicMemberData = service.getPublicData(process);
if (publicMemberData) {
const extractedName = publicMemberData['memberPublicName'];
if (extractedName !== undefined && extractedName !== null) {
memberPublicName = extractedName;
}
}
}
if (!memberPublicName) {
memberPublicName = 'Unnamed Member';
}
// Récupérer les emojis pour ce processId
const emojis = await addressToEmoji(processId);
const option = document.createElement('option');
option.value = processId;
option.textContent = `${memberPublicName} (${emojis})`;
memberSelect.appendChild(option);
}
}
(window as any).populateMemberSelect = populateMemberSelect;
(window as any).scanDevice = scanDevice; (window as any).scanDevice = scanDevice;

View File

@ -2,9 +2,12 @@ import { addSubscription } from '../../utils/subscription.utils';
import Services from '../../services/service'; import Services from '../../services/service';
import { getCorrectDOM } from '~/utils/html.utils'; import { getCorrectDOM } from '~/utils/html.utils';
import { Process } from 'pkg/sdk_client'; import { Process } from 'pkg/sdk_client';
import chatStyle from '../../../public/style/chat.css?inline';
import { Database } from '../../services/database.service';
// Initialize function, create initial tokens with itens that are already selected by the user // Initialize function, create initial tokens with itens that are already selected by the user
export async function init() { export async function init() {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement; const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
const element = container.querySelector('select') as HTMLSelectElement; const element = container.querySelector('select') as HTMLSelectElement;
// Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside // Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
@ -155,62 +158,39 @@ function clearAutocompleteList(select: HTMLSelectElement) {
if (autocomplete_list) autocomplete_list.innerHTML = ''; if (autocomplete_list) autocomplete_list.innerHTML = '';
} }
// Populate the autocomplete list following a given query from the user async function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) {
function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) {
const { autocomplete_options } = getOptions(select); const { autocomplete_options } = getOptions(select);
let options_to_show; let options_to_show = [];
if (dropdown) { const service = await Services.getInstance();
let messagingCounter = 1; const mineArray: string[] = await service.getMyProcesses();
const messagingOptions = select.querySelectorAll('option[value="messaging"]'); const allProcesses = await service.getProcesses();
const allArray: string[] = Object.keys(allProcesses).filter(x => !mineArray.includes(x));
options_to_show = autocomplete_options.map(option => {
if (option === 'messaging') {
// Récupérer l'élément option correspondant au compteur actuel
const currentOption = messagingOptions[messagingCounter - 1];
const processId = currentOption?.getAttribute('data-process-id');
console.log(`Mapping messaging ${messagingCounter} with processId:`, processId);
const optionText = `messaging ${messagingCounter}`;
messagingCounter++;
// Stocker le processId dans un attribut data sur le select
select.setAttribute(`data-messaging-id-${messagingCounter - 1}`, processId || '');
return optionText;
}
return option;
});
} else {
options_to_show = autocomplete(query, autocomplete_options);
}
const wrapper = select.parentNode; const wrapper = select.parentNode;
const input_search = wrapper?.querySelector('.search-container'); const input_search = wrapper?.querySelector('.search-container');
const autocomplete_list = wrapper?.querySelector('.autocomplete-list'); const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
if (autocomplete_list) autocomplete_list.innerHTML = ''; if (autocomplete_list) autocomplete_list.innerHTML = '';
const result_size = options_to_show.length;
if (result_size == 1) { const addProcessToList = (processId:string, isMine: boolean) => {
const li = document.createElement('li'); const li = document.createElement('li');
li.innerText = options_to_show[0]; li.innerText = processId;
li.setAttribute('data-value', options_to_show[0]); li.setAttribute("data-value", processId);
if (isMine) {
li.classList.add("my-process");
li.style.cssText = `color: var(--accent-color)`;
}
if (li) addSubscription(li, 'click', selectOption); if (li) addSubscription(li, 'click', selectOption);
autocomplete_list?.appendChild(li); autocomplete_list?.appendChild(li);
if (query.length == options_to_show[0].length) { };
const event = new Event('click');
li.dispatchEvent(event); mineArray.forEach(processId => addProcessToList(processId, true));
} allArray.forEach(processId => addProcessToList(processId, false));
} else if (result_size > 1) {
for (let i = 0; i < result_size; i++) { if (mineArray.length === 0 && allArray.length === 0) {
const li = document.createElement('li');
li.innerText = options_to_show[i];
li.setAttribute('data-value', options_to_show[i]);
if (li) addSubscription(li, 'click', selectOption);
autocomplete_list?.appendChild(li);
}
} else {
const li = document.createElement('li'); const li = document.createElement('li');
li.classList.add('not-cursor'); li.classList.add('not-cursor');
li.innerText = 'No options found'; li.innerText = 'No options found';
@ -499,7 +479,7 @@ async function createMessagingProcess(): Promise<void> {
await service.handleApiReturn(createProcessReturn); await service.handleApiReturn(createProcessReturn);
const createPrdReturn = await service.createPrdUpdate(processId, stateId); const createPrdReturn = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(createPrdReturn); await service.handleApiReturn(createPrdReturn);
const approveChangeReturn = service.approveChange(processId, stateId); const approveChangeReturn = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveChangeReturn); await service.handleApiReturn(approveChangeReturn);
}, 500) }, 500)
} }
@ -538,50 +518,3 @@ async function getDescription(processId: string, process: Process): Promise<stri
return null; return null;
} }
async function getProcesses(): Promise<any[]> {
const service = await Services.getInstance();
const processes = await service.getProcesses();
const res = Object.entries(processes).map(([key, value]) => ({
key,
value,
}));
return res;
}
async function getMyProcesses() {
const service = await Services.getInstance();
try {
const processes = await service.getProcesses();
const userProcessSet = new Set();
for (const [processId, process] of Object.entries(processes)) {
let roles;
try {
roles = await this.getRoles(process);
if (!roles) {
roles = await process.states[0].encrypted_pcd.roles;
}
const hasCurrentUser = Object.values(roles).some(role =>
service.rolesContainsUs(role)
);
if (hasCurrentUser) {
userProcessSet.add(processId);
}
} catch (e) {
continue;
console.error(`Error processing process ${processId}:`, e);
}
}
return userProcessSet;
} catch (e) {
console.error("Failed to get processes:", e);
}
}

View File

@ -148,7 +148,7 @@ export async function init(): Promise<void> {
await services.restoreSecretsFromDB(); await services.restoreSecretsFromDB();
if (services.isPaired()) { if (services.isPaired()) {
await navigate('process'); await navigate('chat');
} else { } else {
const queryString = window.location.search; const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString); const urlParams = new URLSearchParams(queryString);

View File

@ -1,3 +1,5 @@
const EMPTY32BYTES = String('').padStart(64, '0');
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting()); // Activate worker immediately event.waitUntil(self.skipWaiting()); // Activate worker immediately
}); });
@ -9,81 +11,24 @@ self.addEventListener('activate', (event) => {
// Event listener for messages from clients // Event listener for messages from clients
self.addEventListener('message', async (event) => { self.addEventListener('message', async (event) => {
const data = event.data; const data = event.data;
if (data.type === 'START') { console.log(data);
const fetchNotifications = async () => {
const itemsWithFlag = await getAllItemsWithFlag();
// Process items with the specific flag if (data.type === 'SCAN') {
itemsWithFlag?.forEach((item) => {
console.log(item); // Do something with each flagged item
});
event.ports[0].postMessage({
type: 'NOTIFICATIONS',
data: itemsWithFlag,
});
};
fetchNotifications();
setInterval(fetchNotifications, 2 * 60 * 1000);
}
if (data.type === 'SCAN_PROCESS') {
try { try {
const { myProcessesId } = data.payload; const myProcessesId = data.payload;
const db = await openDatabase(); if (myProcessesId && myProcessesId.length != 0) {
const toDownload = await scanMissingData(myProcessesId);
// Créer un tableau pour stocker toutes les promesses de processus if (toDownload.length != 0) {
const processPromises = myProcessesId.map(async (processId) => { console.log('Sending TO_DOWNLOAD message');
// Récupérer le processus event.source.postMessage({ type: 'TO_DOWNLOAD', data: toDownload});
const process = await new Promise((resolve, reject) => {
const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes');
const request = store.get(processId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!process || !process.states || process.states.length === 0) {
throw new Error(`Process ${processId} not found or invalid`);
} }
} else {
// Récupérer les diffs pour chaque état event.source.postMessage({ status: 'error', message: 'Empty lists' });
const diffPromises = process.states.map(async (state) => {
return new Promise((resolve, reject) => {
const tx = db.transaction('diffs', 'readonly');
const store = tx.objectStore('diffs');
for (const hash of state.pcd_commitment) {
const request = store.get(hash);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
} }
});
});
const diffs = await Promise.all(diffPromises);
process.diffs = diffs.filter(diff => diff != null);
return process;
});
const results = await Promise.all(processPromises);
event.ports[0].postMessage({
status: 'success',
message: 'All processes scanned',
data: results
});
} catch (error) { } catch (error) {
event.ports[0].postMessage({ status: 'error', message: error.message }); event.source.postMessage({ status: 'error', message: error.message });
} }
setInterval(fetchNotifications, 2 * 1000); } else if (data.type === 'ADD_OBJECT') {
}
if (data.type === 'ADD_OBJECT') {
try { try {
const { storeName, object, key } = data.payload; const { storeName, object, key } = data.payload;
const db = await openDatabase(); const db = await openDatabase();
@ -103,6 +48,44 @@ self.addEventListener('message', async (event) => {
} }
}); });
async function scanMissingData(processesToScan) {
console.log('Scanning for missing data...');
const myProcesses = await getProcesses(processesToScan);
let toDownload = new Set();
// Iterate on each process
if (myProcesses && myProcesses.length != 0) {
for (const process of myProcesses) {
// Iterate on states
const firstState = process.states[0];
const processId = firstState.commited_in;
for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue;
// iterate on pcd_commitment
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
// Skip public fields
if (state.public_data[field] !== undefined || field === 'roles') continue;
// Check if we have the data in db
const existingData = await getBlob(hash);
if (!existingData) {
toDownload.add(hash);
// We also add an entry in diff, in case it doesn't already exist
await addDiff(processId, state.state_id, hash, state.roles, field);
} else {
// We remove it if we have it in the set
if (toDownload.delete(hash)) {
console.log(`Removing ${hash} from the set`);
}
}
}
}
}
}
console.log(toDownload);
return Array.from(toDownload);
}
async function openDatabase() { async function openDatabase() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open('4nk', 1); const request = indexedDB.open('4nk', 1);
@ -123,31 +106,55 @@ async function openDatabase() {
// Function to get all processes because it is asynchronous // Function to get all processes because it is asynchronous
async function getAllProcesses() { async function getAllProcesses() {
const db = await openDatabase();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!db) {
reject(new Error('Database is not available'));
return;
}
const tx = db.transaction('processes', 'readonly'); const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes'); const store = tx.objectStore('processes');
// const request = store.openCursor(); const request = store.getAll();
// const processes = [];
request.onsuccess = (event) => { request.onsuccess = () => {
// const cursor = event.target.result; resolve(request.result);
// if (cursor) {
// processes.push({ key: cursor.key, ...cursor.value });
// cursor.continue();
// } else {
// resolve(processes);
// }
const allProcesses = store.getAll();
resolve(allProcesses);
}; };
request.onerror = (event) => { request.onerror = () => {
reject(event.target.error); reject(request.error);
}; };
}); });
}; };
async function getAllItemsWithFlag() { async function getProcesses(processIds) {
if (!processIds || processIds.length === 0) {
return [];
}
const db = await openDatabase();
if (!db) {
throw new Error('Database is not available');
}
const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes');
const requests = Array.from(processIds).map((processId) => {
return new Promise((resolve) => {
const request = store.get(processId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => {
console.error(`Error fetching process ${processId}:`, request.error);
resolve(undefined);
};
});
});
const results = await Promise.all(requests);
return results.filter(result => result !== undefined);
}
async function getAllDiffsNeedValidation() {
const db = await openDatabase(); const db = await openDatabase();
const allProcesses = await getAllProcesses(); const allProcesses = await getAllProcesses();
@ -204,3 +211,56 @@ async function getAllItemsWithFlag() {
}; };
}); });
} }
async function getBlob(hash) {
const db = await openDatabase();
const storeName = 'data';
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const result = await new Promise((resolve, reject) => {
const getRequest = store.get(hash);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
return result;
}
async function addDiff(processId, stateId, hash, roles, field) {
const db = await openDatabase();
const storeName = 'diffs';
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
// Check if the diff already exists
const existingDiff = await new Promise((resolve, reject) => {
const getRequest = store.get(hash);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
if (!existingDiff) {
const newDiff = {
process_id: processId,
state_id: stateId,
value_commitment: hash,
roles: roles,
field: field,
description: null,
previous_value: null,
new_value: null,
notify_user: false,
need_validation: false,
validation_status: 'None'
};
const insertResult = await new Promise((resolve, reject) => {
const putRequest = store.put(newDiff);
putRequest.onsuccess = () => resolve(putRequest.result);
putRequest.onerror = () => reject(putRequest.error);
});
return insertResult;
}
return existingDiff;
}

View File

@ -1,13 +1,14 @@
import Services from './service'; import Services from './service';
class Database { export class Database {
private static instance: Database; private static instance: Database;
private db: IDBDatabase | null = null; private db: IDBDatabase | null = null;
private dbName: string = '4nk'; private dbName: string = '4nk';
private dbVersion: number = 1; private dbVersion: number = 1;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel = new MessageChannel(); private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel = new MessageChannel(); private messageChannelForGet: MessageChannel | null = null;
private serviceWorkerCheckIntervalId: number | null = null;
private storeDefinitions = { private storeDefinitions = {
AnkLabels: { AnkLabels: {
name: 'labels', name: 'labels',
@ -43,6 +44,11 @@ class Database {
{ name: 'byStatus', keyPath: 'validation_status', options: { unique: false } }, { name: 'byStatus', keyPath: 'validation_status', options: { unique: false } },
], ],
}, },
AnkData: {
name: 'data',
options: {},
indices: [],
},
}; };
// Private constructor to prevent direct instantiation from outside // Private constructor to prevent direct instantiation from outside
@ -76,12 +82,10 @@ class Database {
}); });
}; };
request.onsuccess = () => { request.onsuccess = async () => {
setTimeout(() => {
this.db = request.result; this.db = request.result;
this.initServiceWorker(); await this.initServiceWorker();
resolve(); resolve();
}, 500);
}; };
request.onerror = () => { request.onerror = () => {
@ -106,35 +110,67 @@ class Database {
return objectList; return objectList;
} }
private createMessageChannel(responseHandler: (event: MessageEvent) => void): MessageChannel { private async initServiceWorker() {
const messageChannel = new MessageChannel(); if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
messageChannel.port1.onmessage = responseHandler;
return messageChannel; try {
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length === 0) {
// No existing workers: register a new one.
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
} else if (registrations.length === 1) {
// One existing worker: update it (restart it) without unregistering.
this.serviceWorkerRegistration = registrations[0];
await this.serviceWorkerRegistration.update();
console.log('Service Worker updated');
} else {
// More than one existing worker: unregister them all and register a new one.
console.log('Multiple Service Worker(s) detected. Unregistering all...');
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All previous Service Workers unregistered.');
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
} }
private async initServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', registration.scope);
this.serviceWorkerRegistration = registration
await this.checkForUpdates(); await this.checkForUpdates();
// Set up the message channels // Set up a global message listener for responses from the service worker.
this.messageChannel.port1.onmessage = this.handleAddObjectResponse; navigator.serviceWorker.addEventListener('message', async (event) => {
this.messageChannelForGet.port1.onmessage = this.handleGetObjectResponse; console.log('Received message from service worker:', event.data);
registration.active?.postMessage( await this.handleServiceWorkerMessage(event.data);
{ });
type: 'START',
}, // Set up a periodic check to ensure the service worker is active and to send a SYNC message.
[this.messageChannel.port2], this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
); const activeWorker = this.serviceWorkerRegistration.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration));
// Optionally, initialize service worker with some data const service = await Services.getInstance();
const payload = await service.getMyProcesses();
if (payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
}
}, 5000);
} catch (error) { } catch (error) {
console.error('Service Worker registration failed:', error); console.error('Service Worker registration failed:', error);
} }
} }
// Helper function to wait for service worker activation
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
return new Promise((resolve) => {
if (registration.active) {
resolve(registration.active);
} else {
const listener = () => {
if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener);
resolve(registration.active);
}
};
navigator.serviceWorker.addEventListener('controllerchange', listener);
}
});
} }
private async checkForUpdates() { private async checkForUpdates() {
@ -153,12 +189,92 @@ class Database {
} }
} }
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case 'TO_DOWNLOAD':
await this.handleDownloadList(message.data);
break;
default:
console.warn('Unknown message type received from service worker:', message);
}
}
private async handleDownloadList(downloadList: string[]): void {
// Download the missing data
let requestedStateId = [];
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
if (!diff) {
// This should never happen
console.warn(`Missing a diff for hash ${hash}`);
continue;
}
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(new CustomEvent('newDataReceived', {
detail: {
processId,
stateId,
hash,
}
}));
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
}
private handleAddObjectResponse = async (event: MessageEvent) => { private handleAddObjectResponse = async (event: MessageEvent) => {
const data = event.data; const data = event.data;
console.log('Received response from service worker (ADD_OBJECT):', data); console.log('Received response from service worker (ADD_OBJECT):', data);
if (data.type === 'NOTIFICATIONS') {
const service = await Services.getInstance(); const service = await Services.getInstance();
if (data.type === 'NOTIFICATIONS') {
service.setNotifications(data.data); service.setNotifications(data.data);
} else if (data.type === 'TO_DOWNLOAD') {
console.log(`Received missing data ${data}`);
// Download the missing data
let requestedStateId = [];
for (const hash of data.data) {
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
const diff = await service.getDiffByValue(hash);
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
} }
}; };
@ -167,13 +283,15 @@ class Database {
}; };
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> { public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
// Check if the service worker is active // Check if the service worker is active
if (!this.serviceWorkerRegistration?.active) { if (!this.serviceWorkerRegistration) {
reject(new Error('Service worker is not active')); // console.warn('Service worker registration is not ready. Waiting...');
return; this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
} }
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
// Create a message channel for communication // Create a message channel for communication
const messageChannel = new MessageChannel(); const messageChannel = new MessageChannel();
@ -189,7 +307,7 @@ class Database {
// Send the add object request to the service worker // Send the add object request to the service worker
try { try {
this.serviceWorkerRegistration.active.postMessage( activeWorker?.postMessage(
{ {
type: 'ADD_OBJECT', type: 'ADD_OBJECT',
payload, payload,

View File

@ -14,6 +14,7 @@ export default class ModalService {
private processId: string | null = null; private processId: string | null = null;
private constructor() {} private constructor() {}
private paired_addresses: string[] = []; private paired_addresses: string[] = [];
private modal: HTMLElement | null = null;
// Method to access the singleton instance of Services // Method to access the singleton instance of Services
public static async getInstance(): Promise<ModalService> { public static async getInstance(): Promise<ModalService> {
@ -54,6 +55,21 @@ export default class ModalService {
} }
} }
async injectCreationModal(members: any[]) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text());
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/modal/confirmation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
}
}
// Device 1 wait Device 2 // Device 1 wait Device 2
async injectWaitingModal() { async injectWaitingModal() {
const container = document.querySelector('#containerId'); const container = document.querySelector('#containerId');
@ -95,43 +111,48 @@ export default class ModalService {
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) { public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
let members; let members;
if (roleDefinition['owner']) { if (roleDefinition['pairing']) {
const owner = roleDefinition['owner']; const owner = roleDefinition['pairing'];
members = owner.members; members = owner.members;
} else { } else {
throw new Error('No "owner" role'); throw new Error('No "pairing" role');
} }
if (members.length != 1) { if (members.length != 1) {
throw new Error('Must have exactly 1 member'); throw new Error('Must have exactly 1 member');
} }
console.log("MEMBERS:", members);
// We take all the addresses except our own // We take all the addresses except our own
const service = await Services.getInstance(); const service = await Services.getInstance();
const localAddress = await service.getDeviceAddress(); const localAddress = await service.getDeviceAddress();
for (const member of members) { for (const member of members) {
for (const address of member['sp_addresses']) { if (member.sp_addresses) {
for (const address of member.sp_addresses) {
if (address !== localAddress) { if (address !== localAddress) {
this.paired_addresses.push(address); this.paired_addresses.push(address);
} }
} }
} }
}
this.processId = processId; this.processId = processId;
this.stateId = stateId; this.stateId = stateId;
await this.injectModal(members);
const modal = document.getElementById('modal');
if (modal) modal.style.display = 'flex';
// const newScript = document.createElement('script');
// newScript.setAttribute('type', 'module');
// newScript.textContent = confirmationModalScript;
// document.head.appendChild(newScript).parentNode?.removeChild(newScript);
// Add correct text if (members[0].sp_addresses.length === 1) {
await this.injectCreationModal(members);
this.modal = document.getElementById('creation-modal');
console.log("LENGTH:", members[0].sp_addresses.length);
} else {
await this.injectModal(members);
this.modal = document.getElementById('modal');
console.log("LENGTH:", members[0].sp_addresses.length);
}
if (this.modal) this.modal.style.display = 'flex';
// Close modal when clicking outside of it // Close modal when clicking outside of it
window.onclick = (event) => { window.onclick = (event) => {
const modal = document.getElementById('modal'); if (event.target === this.modal) {
if (event.target === modal) {
this.closeConfirmationModal(); this.closeConfirmationModal();
} }
}; };
@ -140,14 +161,12 @@ export default class ModalService {
console.log('=============> Confirm Login'); console.log('=============> Confirm Login');
} }
async closeLoginModal() { async closeLoginModal() {
const modal = document.getElementById('login-modal'); if (this.modal) this.modal.style.display = 'none';
if (modal) modal.style.display = 'none';
} }
async confirmPairing() { async confirmPairing() {
const service = await Services.getInstance(); const service = await Services.getInstance();
const modal = document.getElementById('modal'); if (this.modal) this.modal.style.display = 'none';
if (modal) modal.style.display = 'none';
if (service.device1) { if (service.device1) {
console.log("Device 1 detected"); console.log("Device 1 detected");
@ -166,7 +185,7 @@ export default class ModalService {
// We send confirmation that we validate the change // We send confirmation that we validate the change
try { try {
const approveChangeReturn = service.approveChange(this.processId!, this.stateId!); const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn); await service.handleApiReturn(approveChangeReturn);
await this.injectWaitingModal(); await this.injectWaitingModal();
@ -186,7 +205,7 @@ export default class ModalService {
const newDevice = service.dumpDeviceFromMemory(); const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice); console.log(newDevice);
await service.saveDeviceInDatabase(newDevice); await service.saveDeviceInDatabase(newDevice);
navigate('process'); navigate('chat');
service.resetState(); service.resetState();
} catch (e) { } catch (e) {
@ -215,7 +234,7 @@ export default class ModalService {
// We send confirmation that we validate the change // We send confirmation that we validate the change
try { try {
const approveChangeReturn = service.approveChange(this.processId!, this.stateId!); const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn); await service.handleApiReturn(approveChangeReturn);
} catch (e) { } catch (e) {
throw e; throw e;
@ -229,14 +248,13 @@ export default class ModalService {
const newDevice = service.dumpDeviceFromMemory(); const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice); console.log(newDevice);
await service.saveDeviceInDatabase(newDevice); await service.saveDeviceInDatabase(newDevice);
navigate('process'); navigate('chat');
} }
} }
async closeConfirmationModal() { async closeConfirmationModal() {
const service = await Services.getInstance(); const service = await Services.getInstance();
await service.unpairDevice(); await service.unpairDevice();
const modal = document.getElementById('modal'); if (this.modal) this.modal.style.display = 'none';
if (modal) modal.style.display = 'none';
} }
} }

View File

@ -11,20 +11,19 @@ import { BackUp } from '~/models/backup.model';
export const U32_MAX = 4294967295; export const U32_MAX = 4294967295;
const storageUrl = `/storage`; const BASEURL = `https://demo.4nkweb.com`;
const BOOTSTRAPURL = [`https://demo.4nkweb.com/ws/`]; const BOOTSTRAPURL = [`${BASEURL}/ws/`];
const STORAGEURL = `${BASEURL}/storage`
const DEFAULTAMOUNT = 1000n; const DEFAULTAMOUNT = 1000n;
const EMPTY32BYTES = String('').padStart(64, '0');
export default class Services { export default class Services {
private static initializing: Promise<Services> | null = null; private static initializing: Promise<Services> | null = null;
private static instance: Services; private static instance: Services;
private currentProcess: string | null = null; private currentProcess: string | null = null;
private pendingUpdates: any | null = null;
private currentUpdateMerkleRoot: string | null = null; private currentUpdateMerkleRoot: string | null = null;
private localAddress: string | null = null;
private pairedAddresses: string[] = [];
private sdkClient: any; private sdkClient: any;
private processes: IProcess[] | null = null; private myProcesses: Set = new Set();
private notifications: any[] | null = null; private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
private database: any; private database: any;
@ -211,7 +210,7 @@ export default class Services {
private async ensureSufficientAmount(): Promise<void> { private async ensureSufficientAmount(): Promise<void> {
const availableAmt = this.getAmount(); const availableAmt = this.getAmount();
const target: BigInt = DEFAULTAMOUNT * BigInt(2); const target: BigInt = DEFAULTAMOUNT * BigInt(10);
if (availableAmt < target) { if (availableAmt < target) {
const faucetMsg = this.createFaucetMessage(); const faucetMsg = this.createFaucetMessage();
@ -239,88 +238,40 @@ export default class Services {
throw new Error('Amount is still 0 after 3 attempts'); throw new Error('Amount is still 0 after 3 attempts');
} }
public async createMessagingProcess(otherMembers: Member[],relayAddress: string, feeRate: number): Promise<ApiReturn> { public async createPairingProcess(userName: string, pairWith: string[], relayAddress: string, feeRate: number): Promise<ApiReturn> {
if (!this.isPaired()) {
throw new Error('Device not paired');
}
const me = await this.getMemberFromDevice();
if (!me) {
throw new Error('No paired member in device');
}
const allMembers: Member[] = otherMembers;
const meAndOne = [{ sp_addresses: me }, otherMembers.pop()!];
allMembers.push({ sp_addresses: me });
const everyOneElse = otherMembers;
const messagingTemplate = {
description: 'messaging',
roles: {
public: {
members: allMembers,
validation_rules: [
{
quorum: 0.0,
fields: ['description', 'roles'],
min_sig_member: 0.0,
},
],
storages: [storageUrl]
},
owner: {
members: meAndOne,
validation_rules: [
{
quorum: 1.0,
fields: ['description', 'roles'],
min_sig_member: 1.0,
},
],
storages: [storageUrl]
},
users: {
members: everyOneElse,
validation_rules: [
{
quorum: 0.0,
fields: ['description', 'roles'],
min_sig_member: 0.0,
},
],
storages: [storageUrl]
},
},
};
try {
return this.sdkClient.create_new_process(JSON.stringify(messagingTemplate), null, relayAddress, feeRate);
} catch (e) {
throw new Error(`Creating process failed: ${e}`);
}
}
public async createPairingProcess(pairWith: string[], relayAddress: string, feeRate: number): Promise<ApiReturn> {
if (this.sdkClient.is_paired()) { if (this.sdkClient.is_paired()) {
throw new Error('Device already paired'); throw new Error('Device already paired');
} }
const myAddress: string = this.sdkClient.get_address(); const myAddress: string = this.sdkClient.get_address();
pairWith.push(myAddress); pairWith.push(myAddress);
const pairingTemplate = { const roles: Record<string, RoleDefinition> = {
description: 'pairing',
counter: 0,
roles: {
pairing: { pairing: {
members: [{ sp_addresses: pairWith }], members: [{ sp_addresses: pairWith }],
validation_rules: [ validation_rules: [
{ {
quorum: 1.0, quorum: 1.0,
fields: ['description', 'roles', 'counter'], fields: ['description', 'counter', 'roles', 'memberPublicName'],
min_sig_member: 1.0, min_sig_member: 1.0,
}, },
], ],
storages: [storageUrl] storages: [STORAGEURL]
},
}, },
}; };
const pairingTemplate = {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName
}
try { try {
return this.sdkClient.create_new_process(JSON.stringify(pairingTemplate), null, relayAddress, feeRate); return this.sdkClient.create_new_process(
pairingTemplate,
roles,
publicData,
relayAddress,
feeRate
);
} catch (e) { } catch (e) {
throw new Error(`Creating process failed:, ${e}`); throw new Error(`Creating process failed:, ${e}`);
} }
@ -347,14 +298,10 @@ export default class Services {
throw new Error('No paired member found'); throw new Error('No paired member found');
} }
const dmTemplate = { const roles = {
description: 'dm', demiurge: {
message: '',
roles: {
dm: {
members: [ members: [
{ sp_addresses: myAddresses }, { sp_addresses: myAddresses },
{ sp_addresses: otherMember }
], ],
validation_rules: [ validation_rules: [
{ {
@ -363,22 +310,41 @@ export default class Services {
min_sig_member: 0.01, min_sig_member: 0.01,
}, },
], ],
storages: [storageUrl] storages: [STORAGEURL]
}
dm: {
members: [
{ sp_addresses: myAddresses },
{ sp_addresses: otherMember }
],
validation_rules: [
{
quorum: 0.01,
fields: ['message', 'description'],
min_sig_member: 0.01,
},
],
storages: [STORAGEURL]
} }
} }
const dmTemplate = {
description: 'dm',
message: '',
}; };
console.log('📋 Template final:', JSON.stringify(dmTemplate, null, 2)); console.log('📋 Template final:', JSON.stringify(dmTemplate, null, 2));
const relayAddress = this.getAllRelays()[0]['spAddress']; const relayAddress = this.getAllRelays()[0]['spAddress'];
const feeRate = 1; const feeRate = 1;
const initState = JSON.stringify(dmTemplate); const publicData = {};
await this.checkConnections ([{ sp_addresses: otherMember}]); await this.checkConnections ([{ sp_addresses: otherMember }]);
const result = this.sdkClient.create_new_process( const result = this.sdkClient.create_new_process (
initState, dmTemplate,
null, roles,
publicData,
relayAddress, relayAddress,
feeRate feeRate
); );
@ -391,12 +357,15 @@ export default class Services {
} }
} }
public async updateProcess(processId: string, new_state: any): Promise<ApiReturn> { public async updateProcess(process: Process, new_state: any, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> {
const roles = new_state.roles; // If roles is null, we just take the last commited state roles
if (!roles) { if (!roles) {
throw new Error('new state doesn\'t contain roles'); roles = this.getRoles(process);
} else {
// We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm
console.log('Provided new roles:', JSON.stringify(roles));
} }
console.log(roles);
let members = new Set(); let members = new Set();
for (const role of Object.values(roles)) { for (const role of Object.values(roles)) {
for (const member of role.members) { for (const member of role.members) {
@ -406,7 +375,8 @@ export default class Services {
console.log(members); console.log(members);
await this.checkConnections([...members]); await this.checkConnections([...members]);
try { try {
return this.sdkClient.update_process(processId, JSON.stringify(new_state)); console.log(process);
return this.sdkClient.update_process(process, new_state, roles, {});
} catch (e) { } catch (e) {
throw new Error(`Failed to update process: ${e}`); throw new Error(`Failed to update process: ${e}`);
} }
@ -433,21 +403,25 @@ export default class Services {
} }
} }
public approveChange(processId: string, stateId: string): ApiReturn { public async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
const process = await this.getProcess(processId);
if (!process) {
throw new Error('Failed to get process from db');
}
try { try {
return this.sdkClient.validate_state(processId, stateId); return this.sdkClient.validate_state(process, stateId);
} catch (e) { } catch (e) {
throw new Error(`Failed to create prd response: ${e}`); throw new Error(`Failed to create prd response: ${e}`);
} }
} }
public rejectChange(): ApiReturn { public async rejectChange(processId: string, stateId: string): Promise<ApiReturn> {
if (!this.currentProcess || !this.currentUpdateMerkleRoot) { const process = await this.getProcess(processId);
throw new Error('No current process and/or current update defined'); if (!process) {
throw new Error('Failed to get process from db');
} }
try { try {
return this.sdkClient.refuse_state(this.currentProcess, this.currentUpdateMerkleRoot); return this.sdkClient.refuse_state(process, stateId);
} catch (e) { } catch (e) {
throw new Error(`Failed to create prd response: ${e}`); throw new Error(`Failed to create prd response: ${e}`);
} }
@ -520,91 +494,8 @@ export default class Services {
} }
} }
private async getCipherForDiff(diff: UserDiff): Promise<string | null> {
// get the process
try {
const process = await this.getProcess(diff.process_id);
} catch (e) {
console.error('Failed to get process:', e);
return null;
}
const state = process.states.find(state => state.state_id === diff.state_id);
if (state) {
// Now we return the encrypted value for that field
const cipher = state.encrypted_pcd[diff.field];
if (cipher) {
return cipher;
} else {
console.error('Failed to get encrypted value');
}
}
return null;
}
public async tryFetchDiffValue(diffs: UserDiff[]): Promise<[UserDiff[], Record<string, string>]>{
if (diffs.length === 0) {
return [[], {}];
}
// We check if we have the value in diffs
let retrievedValues: Record<string, string> = {};
for (const diff of diffs) {
const hash = diff.value_commitment;
if (!hash) {
console.error('No commitment for diff');
continue;
}
const value = diff.new_value;
// Check if `new_value` is missing
if (value === null) {
try {
const res = await this.fetchValueFromStorage(hash);
if (!res) {
console.error('Failed to fetch value for hash', hash);
} else {
diff.new_value = res['value'];
retrievedValues[hash] = res['value'];
}
} catch (error) {
console.error(`Failed to fetch new_value for diff: ${JSON.stringify(diff)}`, error);
}
} else {
// We should have it in db if it came from the wasm, but just in case
try {
await this.saveDiffsToDb([diff]);
} catch (e) {
console.error(`Failed to save diff to db: ${e}`);
}
// We already have this value, so we check if it's on storage and push it if not
const dataIsOnServers: Record<string, boolean | null> = await this.testDataInStorage(hash);
if (dataIsOnServers === null) {
console.error('Failed to test data presence in storage');
continue;
}
for (const [server, status] of Object.entries(dataIsOnServers)) {
if (status === false) {
const cipher = await this.getCipherForDiff(diff);
if (cipher) {
try {
await this.saveDataToStorage(hash, cipher, null);
} catch (e) {
console.error(`Failed to save to storage: ${e}`);
}
} else {
console.error('Failed to get cipher for diff');
}
}
}
}
}
return [diffs, retrievedValues];
}
public async handleApiReturn(apiReturn: ApiReturn) { public async handleApiReturn(apiReturn: ApiReturn) {
console.log(apiReturn);
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 500));
@ -639,46 +530,50 @@ export default class Services {
} }
} }
setTimeout(async () => {
if (apiReturn.updated_process) { if (apiReturn.updated_process) {
const updatedProcess = apiReturn.updated_process; const updatedProcess = apiReturn.updated_process;
const processId: string = updatedProcess.process_id; const processId: string = updatedProcess.process_id;
// Save process to db if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
console.log(hash);
console.log(cipher);
const blob = this.hexToBlob(cipher);
try { try {
await this.saveProcessToDb(processId, updatedProcess.current_process); await this.saveBlobToDb(hash, blob);
} catch (e) { } catch (e) {
throw e; console.error(e);
} }
}
}
// Save process to db
await this.saveProcessToDb(processId, updatedProcess.current_process);
const isPaired = this.isPaired(); const isPaired = this.isPaired();
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
const [updatedDiffs, retrievedValues] = await this.tryFetchDiffValue(updatedProcess.diffs);
if (Object.entries(retrievedValues).length != 0) {
const stateId = updatedDiffs[0].state_id;
const processId = updatedDiffs[0].process_id;
// We update the process with the value we retrieved
const hashToValues = JSON.stringify(retrievedValues);
const apiReturn = this.sdkClient.update_process_state(processId, stateId, hashToValues);
await this.handleApiReturn(apiReturn);
} else {
try { try {
await this.saveDiffsToDb(updatedDiffs); await this.saveDiffsToDb(updatedProcess.diffs);
} catch (e) { } catch (e) {
throw e; console.error('Failed to save diffs to db:', e);
} }
if (!isPaired) { if (!isPaired) {
await this.openPairingConfirmationModal(updatedDiffs); console.log(updatedProcess);
await this.openPairingConfirmationModal(updatedProcess.current_process.states[0]);
} }
} }
} }
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
if (updatedProcess.validated_state) { for (const hash of apiReturn.push_to_storage) {
const responsePrdReturn = this.sdkClient.create_response_prd(processId, updatedProcess.validated_state); const blob = await this.getBlobFromDb(hash);
await this.handleApiReturn(responsePrdReturn); if (blob) {
await this.saveDataToStorage(hash, blob, null);
} else {
console.error('Failed to get data from db');
}
} }
} }
@ -690,20 +585,16 @@ export default class Services {
if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) {
await this.sendCipherMessages(apiReturn.ciphers_to_send); await this.sendCipherMessages(apiReturn.ciphers_to_send);
} }
}, 0);
} }
public async openPairingConfirmationModal(diffs: UserDiff[]) { public async openPairingConfirmationModal(firstState: ProcessState) {
const rolesDiff = diffs.find((diff) => diff.field === 'roles'); const roles = firstState.roles;
if (!rolesDiff) { const processId = firstState.commited_in;
throw new Error('Pairing process must have roles'); const stateId = firstState.state_id;
}
const processId = rolesDiff.process_id;
const stateId = rolesDiff.state_id;
try { try {
await this.routingInstance.openPairingConfirmationModal(rolesDiff.new_value, processId, stateId); await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId);
} catch (e) { } catch (e) {
throw new Error(`${e}`); console.error(e);
} }
} }
@ -732,6 +623,15 @@ export default class Services {
} }
} }
public dumpNeuteredDevice(): Device | null {
try {
return this.sdkClient.dump_neutered_device();
} catch (e) {
console.error(`Failed to dump device: ${e}`);
return null;
}
}
public getPairingProcessId(): string { public getPairingProcessId(): string {
try { try {
return this.sdkClient.get_pairing_process_id(); return this.sdkClient.get_pairing_process_id();
@ -800,35 +700,27 @@ export default class Services {
return true; return true;
} }
rolesContainsUs(roles: any): boolean { rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try { try {
this.sdkClient.roles_contains_us(JSON.stringify(roles)); us = this.sdkClient.get_member();
} catch (e) { } catch (e) {
console.error(e); throw e;
return false;
} }
return true; return this.rolesContainsMember(roles, us.sp_addresses);
} }
rolesContainsMember(roles: any, member: string[]): boolean { rolesContainsMember(roles: Record<string, RoleDefinition>, member: string[]): boolean {
try { let res = false;
this.sdkClient.roles_contains_member(JSON.stringify(roles), member); for (const [roleName, roleDef] of Object.entries(roles)) {
} catch (e) { for (const otherMember of roleDef.members) {
console.error(e); if (res) { return true }
return false; res = this.compareMembers(member, otherMember.sp_addresses);
}
} }
return true; return res;
}
membersInSameRoleThanUs(roles: any): Member[] | null {
try {
return this.sdkClient.members_in_same_roles_me(JSON.stringify(roles));
} catch (e) {
console.error(e);
return null;
}
} }
async dumpWallet() { async dumpWallet() {
@ -886,36 +778,34 @@ export default class Services {
key: processId, key: processId,
}); });
} catch (e) { } catch (e) {
throw new Error(`Failed to save process: ${e}`); console.error(`Failed to save process ${processId}: ${e}`);
} }
} }
public async saveStatesToStorage(process: Process, state_ids: string[]) { public async saveBlobToDb(hash: string, data: Blob) {
// We check how many copies in storage nodes const db = await Database.getInstance();
// We check the storage nodes in the process itself try {
// this.sdkClient.get_storages(commitedIn); await db.addObject({
const storages = [storageUrl]; storeName: 'data',
object: data,
for (const state of process.states) { key: hash,
if (state.state_id === "") { });
continue; } catch (e) {
} console.error(`Failed to save data to db: ${e}`);
if (!state.encrypted_pcd) {
console.warn('Empty encrypted pcd, skipping...');
continue;
}
if (state_ids.includes(state.state_id)) {
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
// get the encrypted value with the field name
const value = state.encrypted_pcd[field];
await storeData(storages, hash, value, null);
}
}
} }
} }
public async saveDataToStorage(hash: string, data: string, ttl: number | null) { public async getBlobFromDb(hash: string): Promise<Blob | null> {
const storages = [storageUrl]; const db = await Database.getInstance();
try {
return await db.getObject('data', hash);
} catch (e) {
return null;
}
}
public async saveDataToStorage(hash: string, data: Blob, ttl: number | null) {
const storages = [STORAGEURL];
try { try {
await storeData(storages, hash, data, ttl); await storeData(storages, hash, data, ttl);
@ -925,13 +815,13 @@ export default class Services {
} }
public async fetchValueFromStorage(hash: string): Promise<any | null> { public async fetchValueFromStorage(hash: string): Promise<any | null> {
const storages = [storageUrl]; const storages = [STORAGEURL];
return await retrieveData(storages, hash); return await retrieveData(storages, hash);
} }
public async testDataInStorage(hash: string): Promise<Record<string, boolean | null> | null> { public async testDataInStorage(hash: string): Promise<Record<string, boolean | null> | null> {
const storages = [storageUrl]; const storages = [STORAGEURL];
return await testData(storages, hash); return await testData(storages, hash);
} }
@ -993,18 +883,6 @@ export default class Services {
await this.restoreProcessesFromDB(); await this.restoreProcessesFromDB();
} }
// Match what we get from relay against what we already know and fetch missing data
public async updateProcessesFromRelay(processes: Record<string, Process>) {
const db = await Database.getInstance();
for (const [processId, process] of Object.entries(processes)) {
try {
this.sdkClient.sync_process_from_relay(processId, JSON.stringify(process));
} catch (e) {
console.error(e);
}
}
}
// Restore process in wasm with persistent storage // Restore process in wasm with persistent storage
public async restoreProcessesFromDB() { public async restoreProcessesFromDB() {
const db = await Database.getInstance(); const db = await Database.getInstance();
@ -1069,35 +947,41 @@ export default class Services {
} }
} }
async getDescription(processId: string, process: Process): Promise<string | null> { async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<string | null> {
// Get the `commited_in` value of the last state and remove it from the array let hash = state.pcd_commitment[attribute];
const currentCommitedIn = process.states.at(-1)?.commited_in; let key = state.keys[attribute];
if (currentCommitedIn === undefined) { // If hash or key is missing, request an update and then retry
return null; // No states available if (!hash || !key) {
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds
let retries = 0;
while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting
hash = state.pcd_commitment[attribute];
key = state.keys[attribute];
retries++;
}
} }
// Find the last state where `commited_in` is different if (hash && key) {
let lastDifferentState = process.states.findLast( const blob = await this.getBlobFromDb(hash);
state => state.commited_in !== currentCommitedIn if (blob) {
); // Decrypt the data
const buf = await blob.arrayBuffer();
const cipher = new Uint8Array(buf);
const keyBlob = this.hexToBlob(key);
const keyBuf = await keyBlob.arrayBuffer();
if (!lastDifferentState) { const clear = this.sdkClient.decrypt_data(new Uint8Array(keyBuf), cipher);
// It means that we only have one state that is not commited yet, that can happen with process we just created if (clear) {
// let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation // Parse the stringified JSON
lastDifferentState = process.states.at(-1); return JSON.parse(clear);
} }
if (!lastDifferentState.pcd_commitment) {
return null;
}
// Take the description out of the state, if any
const description = lastDifferentState!.pcd_commitment['description'];
if (description) {
const userDiff = await this.getDiffByValue(description);
if (userDiff) {
return userDiff.new_value;
} }
} }
@ -1191,18 +1075,15 @@ export default class Services {
// Handle the handshake message // Handle the handshake message
public async handleHandshakeMsg(url: string, parsedMsg: any) { public async handleHandshakeMsg(url: string, parsedMsg: any) {
// Get the current user
const us = this.getMemberFromDevice();
console.log("Je suis le us de la fonction handleHandshakeMsg:", us);
try { try {
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
this.updateRelay(url, handshakeMsg.sp_address); this.updateRelay(url, handshakeMsg.sp_address);
const members = handshakeMsg.peers_list; const members = handshakeMsg.peers_list;
if (this.membersList && Object.keys(this.membersList).length === 0) { if (this.membersList && Object.keys(this.membersList).length === 0) {
// We start from an empty list, just copy it over
this.membersList = handshakeMsg.peers_list; this.membersList = handshakeMsg.peers_list;
} else { } else {
// console.log('Received members:'); // We are incrementing our list
// console.log(handshakeMsg.peers_list);
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) { for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
this.membersList[processId] = member as Member; this.membersList[processId] = member as Member;
} }
@ -1212,67 +1093,87 @@ export default class Services {
const newProcesses = handshakeMsg.processes_list; const newProcesses = handshakeMsg.processes_list;
if (newProcesses && Object.keys(newProcesses).length !== 0) { if (newProcesses && Object.keys(newProcesses).length !== 0) {
for (const [processId, process] of Object.entries(newProcesses)) { for (const [processId, process] of Object.entries(newProcesses)) {
// We check if we're part of the process const existing = await this.getProcess(processId);
if (process.states.length < 2) continue; if (existing) {
let stateIds = []; // Look for state id we don't know yet
let managers = new Set(); let new_states = [];
let roles = [];
for (const state of process.states) { for (const state of process.states) {
if (state.encrypted_pcd === null) continue; if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; }
const roles = state.encrypted_pcd['roles']; if (!this.lookForStateId(existing, state.state_id)) {
if (!roles) { new_states.push(state.state_id);
console.error('Can\'t find roles'); roles.push(state.roles);
continue; }
} }
if (this.rolesContainsUs(roles)) { if (new_states.length != 0) {
// We add this state to the list to request // We request the new states
stateIds.push(state.state_id); await this.requestDataFromPeers(processId, new_states, roles);
}
// Otherwise we're probably just in the initial loading at page initialization
// We may learn an update for this process
// TODO maybe actually check if what the relay is sending us contains more information than what we have
// relay should always have more info than us, but we never know
// For now let's keep it simple and let the worker do the job
} else { } else {
continue; // We add it to db
} console.log(`Saving ${processId} to db`);
await this.saveProcessToDb(processId, process as Process);
// For now we just add everyone that is in the same role than us
// const sendTo = this.membersInSameRoleThanUs(roles);
for (const [_, role] of Object.entries(roles)) {
if (!role.members.includes(us)) continue;
for (const member of role.members) {
if (member !== us) {
managers.push(member);
} }
} }
} }
}
try {
this.sdkClient.request_data(processId, stateIds, managers);
} catch (e) {
console.error(e);
}
}
await this.updateProcessesFromRelay(newProcesses);
}
}, 500) }, 500)
} catch (e) { } catch (e) {
console.error('Failed to parse init message:', e); console.error('Failed to parse init message:', e);
} }
} }
private lookForStateId(process: Process, stateId: string): boolean {
for (const state of process.states) {
if (state.state_id === stateId) {
return true;
}
}
return false;
}
/** /**
* Retourne la liste de tous les membres * Retourne la liste de tous les membres ordonnés par leur process id
* @returns Un tableau contenant tous les membres * @returns Un tableau contenant tous les membres
*/ */
public getAllMembers(): Record<string, Member> { public getAllMembersSorted(): Record<string, Member> {
return Object.fromEntries( return Object.fromEntries(
Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
); );
} }
public getAllMembers(): Record<string, Member> {
return this.membersList;
}
public getAddressesForMemberId(memberId: string): string[] | null { public getAddressesForMemberId(memberId: string): string[] | null {
return this.membersList[memberId]; return this.membersList[memberId];
} }
public compareMembers(memberA: string[], memberB: string[]): boolean {
if (!memberA || !memberB) { return false }
if (memberA.length !== memberB.length) { return false }
const res = memberA.every(item => memberB.includes(item)) && memberB.every(item => memberA.includes(item));
return res;
}
public async handleCommitError(response: string) { public async handleCommitError(response: string) {
const content = JSON.parse(response); const content = JSON.parse(response);
const error = content.error;
const errorMsg = error['GenericError'];
if (errorMsg === 'State is identical to the previous state') {
return;
} else if (errorMsg === 'Not enough valid proofs') { return; }
// Wait and retry // Wait and retry
setTimeout(async () => { setTimeout(async () => {
await this.sendCommitMessage(JSON.stringify(content)); await this.sendCommitMessage(JSON.stringify(content));
@ -1280,37 +1181,111 @@ export default class Services {
} }
public async getRoles(process: Process): Promise<any | null> { public getRoles(process: Process): Record<string, RoleDefinition> | null {
const currentCommitedIn = process.states.pop()?.commited_in; const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
if (currentCommitedIn === undefined) { return lastCommitedState!.roles;
} else {
return null; return null;
} }
let lastDifferentState = process.states.findLast(
state => state.commited_in !== currentCommitedIn
);
if (!lastDifferentState) {
lastDifferentState = process.states.pop();
} }
if (!lastDifferentState || !lastDifferentState.pcd_commitment) { public getPublicData(process: Process): Record<string, any> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) {
return lastCommitedState.public_data;
} else {
return null; return null;
} }
const roles = lastDifferentState!.pcd_commitment['roles'];
if (roles) {
const userDiff = await this.getDiffByValue(roles);
if (userDiff) {
console.log("Successfully retrieved userDiff:", userDiff);
return userDiff.new_value;
}
} }
public getProcessName(process: Process): string | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) {
const processName = lastCommitedState!.public_data['processName'];
if (processName) { return processName }
else { return null }
} else {
return null; return null;
} }
}
public async getMyProcesses(): Promise<string[]> {
try {
const processes = await this.getProcesses();
for (const [processId, process] of Object.entries(processes)) {
// We use myProcesses attribute to not reevaluate all processes everytime
if (this.myProcesses && this.myProcesses.has(processId)) {
continue;
}
try {
const roles = this.getRoles(process);
if (roles && this.rolesContainsUs(roles)) {
this.myProcesses.add(processId);
}
} catch (e) {
console.error(e);
}
}
return Array.from(this.myProcesses);
} catch (e) {
console.error("Failed to get processes:", e);
}
}
public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record<string, RoleDefinition>[]) {
console.log('Requesting data from peers');
console.log(roles);
try {
const res = this.sdkClient.request_data(processId, stateIds, roles);
await this.handleApiReturn(res);
} catch (e) {
console.error(e);
}
}
public hexToBlob(hexString: string): Blob {
if (hexString.length % 2 !== 0) {
throw new Error("Invalid hex string: length must be even");
}
const uint8Array = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
}
return new Blob([uint8Array], { type: "application/octet-stream" });
}
public async blobToHex(blob: Blob): Promise<string> {
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
return Array.from(bytes)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
public getLastCommitedState(process: Process): ProcessState {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip);
if (lastCommitedState) {
return lastCommitedState;
} else {
console.error('Can\'t find last commited state');
return null;
}
}
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
if (Object.keys(roles).length != 1) { return false }
const pairingRole = roles['pairing'];
if (pairingRole) {
// For now that's enough, we should probably test more things
return true;
} else {
return false;
}
}
} }

View File

@ -1,9 +1,21 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
export async function storeData(servers: string[], key: string, value: any, ttl: number | null): Promise<AxiosResponse | null> { export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
for (const server of servers) { for (const server of servers) {
try { try {
const response = await axios.post(`${server}/store`, { key, value, ttl }); // Append key and ttl as query parameters
const url = new URL(`${server}/store`);
url.searchParams.append('key', key);
if (ttl !== null) {
url.searchParams.append('ttl', ttl.toString());
}
// Send the encrypted ArrayBuffer as the raw request body.
const response = await axios.post(url.toString(), value, {
headers: {
'Content-Type': 'application/octet-stream'
},
});
console.log('Data stored successfully:', key); console.log('Data stored successfully:', key);
if (response.status !== 200) { if (response.status !== 200) {
console.error('Received response status', response.status); console.error('Received response status', response.status);
@ -20,15 +32,18 @@ export async function storeData(servers: string[], key: string, value: any, ttl:
return null; return null;
} }
export async function retrieveData(servers: string[], key: string): Promise<any | null> { export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
for (const server of servers) { for (const server of servers) {
try { try {
const response = await axios.get(`${server}/retrieve/${key}`); // When fetching the data from the server:
const response = await axios.get(`${server}/retrieve/${key}`, {
responseType: 'arraybuffer'
});
if (response.status !== 200) { if (response.status !== 200) {
console.error('Received response status', response.status); console.error('Received response status', response.status);
continue; continue;
} }
console.log('Retrieved data:', response.data); // console.log('Retrieved data:', response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Error retrieving data:', error); console.error('Error retrieving data:', error);

View File

@ -104,6 +104,7 @@ export function initAddressInput() {
const addressInput = container.querySelector('#addressInput') as HTMLInputElement; const addressInput = container.querySelector('#addressInput') as HTMLInputElement;
const emojiDisplay = container.querySelector('#emoji-display-2'); const emojiDisplay = container.querySelector('#emoji-display-2');
const okButton = container.querySelector('#okButton') as HTMLButtonElement; const okButton = container.querySelector('#okButton') as HTMLButtonElement;
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
addSubscription(addressInput, 'input', async () => { addSubscription(addressInput, 'input', async () => {
let address = addressInput.value; let address = addressInput.value;
@ -145,6 +146,12 @@ export function initAddressInput() {
onOkButtonClick(); onOkButtonClick();
}); });
} }
if (createButton) {
addSubscription(createButton, 'click', () => {
onCreateButtonClick();
});
}
} }
async function onOkButtonClick() { async function onOkButtonClick() {
@ -158,21 +165,46 @@ async function onOkButtonClick() {
} }
} }
export async function prepareAndSendPairingTx(secondDeviceAddress: string) { async function onCreateButtonClick() {
try {
await prepareAndSendPairingTx();
} catch (e) {
console.error(`onCreateButtonClick error: ${e}`);
}
}
export async function prepareAndSendPairingTx(promptName: boolean = false) {
const service = await Services.getInstance(); const service = await Services.getInstance();
// Device 1 wait Device 2 // Device 1 wait Device 2
service.device1 = true; // service.device1 = true;
try { try {
await service.checkConnections([{ sp_addresses: [secondDeviceAddress] }]); await service.checkConnections([]);
} catch (e) { } catch (e) {
throw e; throw e;
} }
// Create the process
// Prompt the user for a username.
let userName;
if (promptName) {
userName = prompt("Please enter your user name:");
} else {
userName = "";
}
// Create the process after a delay.
setTimeout(async () => { setTimeout(async () => {
const relayAddress = service.getAllRelays(); const relayAddress = service.getAllRelays();
const createPairingProcessReturn = await service.createPairingProcess([secondDeviceAddress], relayAddress[0].spAddress, 1);
// Pass the userName as an additional parameter.
const createPairingProcessReturn = await service.createPairingProcess(
userName,
[],
relayAddress[0].spAddress,
1,
userName
);
if (!createPairingProcessReturn.updated_process) { if (!createPairingProcessReturn.updated_process) {
throw new Error('createPairingProcess returned an empty new process'); // This should never happen throw new Error('createPairingProcess returned an empty new process'); // This should never happen
@ -189,17 +221,21 @@ export async function generateQRCode(spAddress: string) {
const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress); const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress);
const qrCode = container?.querySelector('.qr-code img'); const qrCode = container?.querySelector('.qr-code img');
qrCode?.setAttribute('src', url); qrCode?.setAttribute('src', url);
//Generate Address CopyBtn
const address = container?.querySelector('.sp-address-btn');
if (address) {
address.textContent = 'Copy address';
}
const copyBtn = container.querySelector('#copyBtn');
if (copyBtn) {
addSubscription(copyBtn, 'click', () => copyToClipboard(currentUrl + '?sp_address=' + spAddress));
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} }
export async function generateCreateBtn() {
try{
//Generate CreateBtn
const container = getCorrectDOM('login-4nk-component') as HTMLElement
const createBtn = container?.querySelector('.create-btn');
if (createBtn) {
createBtn.textContent = 'CREATE';
}
} catch (err) {
console.error(err);
}
}

View File

@ -5,12 +5,15 @@ export function cleanSubscriptions(): void {
for (const sub of subscriptions) { for (const sub of subscriptions) {
const el = sub.element; const el = sub.element;
const eventHandler = sub.eventHandler; const eventHandler = sub.eventHandler;
if (el) {
el.removeEventListener(sub.event, eventHandler); el.removeEventListener(sub.event, eventHandler);
} }
}
subscriptions = []; subscriptions = [];
} }
export function addSubscription(element: Element | Document, event: any, eventHandler: EventListenerOrEventListenerObject): void { export function addSubscription(element: Element | Document, event: any, eventHandler: EventListenerOrEventListenerObject): void {
if (!element) return;
subscriptions.push({ element, event, eventHandler }); subscriptions.push({ element, event, eventHandler });
element.addEventListener(event, eventHandler); element.addEventListener(event, eventHandler);
} }