WIP (to be recommited smaller later)

This commit is contained in:
NicolasCantu 2025-02-14 16:25:27 +01:00
parent a2a7022c08
commit 7bf0fb2b4f
3 changed files with 378 additions and 536 deletions

View File

@ -286,7 +286,10 @@ class ChatElement extends HTMLElement {
try {
const service = await Services.getInstance();
const myAddresses = await service.getMemberFromDevice();
if (!myAddresses) throw new Error('No paired member found');
if (!myAddresses) {
console.error('No paired member found');
return;
}
const timestamp = Date.now();
const message = {
@ -307,37 +310,24 @@ class ChatElement extends HTMLElement {
if (!process) {
console.error('Failed to retrieve process from DB');
return;
} else {
console.log(process);
}
const processTip = process.states[process.states.length - 1].commited_in;
console.log(processTip);
// For a dm process, there are only 2 attributes, description will stay the same, message is the new message
// We don't need to get previous values for now, so let's just skip it
let newState = {
message: message,
description: 'dm'
};
// We take the last commited state and copy all values
const lastCommitedState = process.states.findLast(
state => state.commited_in !== processTip
);
if (!lastCommitedState) {
console.error('Failed to find last commited state');
return;
} else {
console.log(lastCommitedState);
}
let newState = {};
// fetch all the values from diff
for (const [attribute, hash] of Object.entries(lastCommitedState.pcd_commitment)) {
const value = await service.getDiffByValue(hash);
newState[attribute] = value.new_value;
}
newState.message = message;
console.log(`Creating state ${newState}`);
// Now we create a new state for the dm process
const apiReturn = await service.updateProcess(this.processId, newState);
let apiReturn;
try {
console.log(process);
apiReturn = await service.updateProcess(process, newState, null);
} catch (e) {
console.error('Failed to update process:', e);
return;
}
const updatedProcess = apiReturn.updated_process.current_process;
const newStateId = updatedProcess.states[updatedProcess.states.length - 2 ].state_id; // We take the last concurrent state, just before the tip
console.log(`newStateId: ${newStateId}`);
@ -383,7 +373,7 @@ class ChatElement extends HTMLElement {
const processRoles = this.processRoles;
const selectedMember = this.selectedMember;
for (const child of children) {
const roles = await this.getRoles(JSON.parse(child));
const roles = await service.getRoles(JSON.parse(child));
// Check that we and the other members are in the role
if (!service.isChildRole(processRoles, roles)) {
console.error('Child process roles are not a subset of parent')
@ -478,27 +468,31 @@ class ChatElement extends HTMLElement {
}
private async lookForDmProcess(): Promise<string | null> {
// Filter processes for the children of current process
const service = await Services.getInstance();
const processes = await service.getProcesses();
const memberAddresses = await service.getAddressesForMemberId(this.selectedMember);
const processes = await service.getMyProcesses();
console.log(processes);
const recipientAddresses = await service.getAddressesForMemberId(this.selectedMember).sp_addresses;
console.log(recipientAddresses);
const recipientAddresses = memberAddresses.sp_addresses;
for (const [processId, process] of Object.entries(processes)) {
const description = await service.getDescription(processId, process);
if (description !== "dm") {
continue;
for (const processId of processes) {
try {
const process = await service.getProcess(processId);
console.log(process);
const state = process.states[0]; // We assume that description never change and that we are part of the process from the beginning
const description = await service.decryptAttribute(state, 'description');
console.log(description);
if (!description || description !== "dm") {
continue;
}
const roles = await service.getRoles(process);
if (!service.rolesContainsMember(roles, recipientAddresses)) {
console.error('Member is not part of the process');
continue;
}
return processId;
} catch (e) {
console.error(e);
}
const roles = await this.getRoles(process);
if (!service.rolesContainsMember(roles, recipientAddresses)) {
console.error('Member is not part of the process');
continue;
}
if (!service.rolesContainsUs(roles)) {
console.error('We\'re not part of child process');
continue;
}
return processId;
}
return null;
@ -911,43 +905,6 @@ class ChatElement extends HTMLElement {
roleElement.appendChild(memberList);
}
async getRoles(process: Process): Promise<any | null> {
const service = await Services.getInstance();
// Get the `commited_in` value of the last state and remove it from the array
const currentCommitedIn = process.states.pop()?.commited_in;
if (currentCommitedIn === undefined) {
return null; // No states available
}
// Find the last state where `commited_in` is different
let lastDifferentState = process.states.findLast(
state => state.commited_in !== currentCommitedIn
);
if (!lastDifferentState) {
// It means that we only have one state that is not commited yet, that can happen with process we just created
// 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
lastDifferentState = process.states.pop();
}
if (!lastDifferentState || !lastDifferentState.pcd_commitment) {
return null;
}
// Take the roles out of the state
const roles = lastDifferentState!.pcd_commitment['roles'];
if (roles) {
const userDiff = await service.getDiffByValue(roles);
if (userDiff) {
console.log("Successfully retrieved userDiff:", userDiff);
return userDiff.new_value;
}
}
return null;
}
private async switchTab(tabType: string, tabs: NodeListOf<Element>) {
// Mettre à jour les classes des onglets
@ -980,200 +937,172 @@ class ChatElement extends HTMLElement {
}
}
//load all processes from the service
private async loadAllProcesses(processSet: Set<string>) {
console.log('🎯 Loading all processes');
console.log("Je suis le processSet dans loadAllProcesses :", processSet);
this.closeSignature();
const allProcesses = await this.getProcesses();
const dbRequest = indexedDB.open('4nk');
return new Promise((resolve, reject) => {
dbRequest.onerror = (event) => {
console.error('❌ Error opening database:', dbRequest.error);
reject(dbRequest.error);
};
dbRequest.onsuccess = async (event) => {
const db = dbRequest.result;
const transaction = db.transaction(['processes'], 'readonly');
const store = transaction.objectStore('processes');
const request = store.getAll();
request.onsuccess = async () => {
const processResult = request.result;
console.log('🎯 Processed result:', processResult);
// Afficher les processus dans le container #group-list
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) {
console.warn('⚠️ Group list element not found');
return;
}
// Afficher les processus dans le container #group-list
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) {
console.warn('⚠️ Group list element not found');
return;
}
groupList.innerHTML = '';
groupList.innerHTML = '';
const tabContent = document.createElement('div');
tabContent.className = 'tabs';
tabContent.innerHTML = `
<button class="tab active" data-tab="processes">Process</button>
<button class="tab" data-tab="members">Members</button>
`;
groupList.appendChild(tabContent);
const tabContent = document.createElement('div');
tabContent.className = 'tabs';
tabContent.innerHTML = `
<button class="tab active" data-tab="processes">Process</button>
<button class="tab" data-tab="members">Members</button>
`;
groupList.appendChild(tabContent);
// Ajouter les event listeners
const tabs = tabContent.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabType = tab.getAttribute('data-tab');
if (tabType) {
this.switchTab(tabType, tabs);
}
});
});
//trier les processus : ceux de l'utilisateur en premier
processResult.sort((a, b) => {
const aInSet = this.userProcessSet.has(a.states[0].commited_in);
const bInSet = this.userProcessSet.has(b.states[0].commited_in);
return bInSet ? 1 : aInSet ? -1 : 0;
});
for (const process of processResult) {
const li = document.createElement('li');
li.className = 'group-list-item';
const oneProcess = process.states[0].commited_in;
let roles;
try {
roles = await this.getRoles(process);
if (!roles) {
roles = await process.states[0].encrypted_pcd.roles;
}
} catch (e) {
// console.error('Failed to get roles for process:', process);
continue;
}
// Si le processus est dans notre Set, ajouter la classe my-process
if (this.userProcessSet && this.userProcessSet.has(oneProcess)) {
li.style.cssText = `
background-color: var(--accent-color);
transition: background-color 0.3s ease;
cursor: pointer;
`;
li.onmouseover = () => {
li.style.backgroundColor = 'var(--accent-color-hover)';
};
li.onmouseout = () => {
li.style.backgroundColor = 'var(--accent-color)';
};
console.log("✅ Processus trouvé dans le set:", oneProcess);
}
li.setAttribute('data-process-id', oneProcess);
//----MANAGE THE CLICK ON PROCESS ----
li.onclick = async (event) => {
event.stopPropagation();
console.log("CLICKED ON PROCESS:", oneProcess);
//viser le h1 de signature-header
const signatureHeader = this.shadowRoot?.querySelector('.signature-header h1');
if (signatureHeader) {
const emoji = await addressToEmoji(oneProcess);
signatureHeader.textContent = `Signature of ${emoji}`;
}
this.openSignature();
//afficher les roles dans chaque processus
console.log('🎯 Roles de signature:', roles);
await this.loadAllRolesAndMembersInSignature(roles);
//----MANAGE THE CLICK ON NEW REQUEST ----
await this.newRequest(oneProcess);
};
groupList.appendChild(li);
const container = document.createElement('div');
container.className = 'group-item-container';
const nameSpan = document.createElement('span');
nameSpan.textContent = `Process : `;
nameSpan.className = 'process-name';
container.appendChild(nameSpan);
addressToEmoji(oneProcess).then(emojis => {
const emojiSpan = document.createElement('span');
emojiSpan.className = 'process-emoji';
emojiSpan.textContent = emojis;
container.appendChild(emojiSpan);
});
li.appendChild(container);
// afficher les roles dans chaque processus
//console.log('🎯 Roles:', roles);
const roleList = document.createElement('ul');
roleList.className = 'role-list';
(roleList as HTMLElement).style.display = 'none';
// Traiter chaque rôle
Object.entries(roles).forEach(([roleName, roleData]: [string, any]) => {
const roleItem = document.createElement('li');
roleItem.className = 'role-item';
const roleContainer = document.createElement('div');
roleContainer.className = 'role-item-container';
const roleNameSpan = document.createElement('span');
roleNameSpan.className = 'role-name';
roleNameSpan.textContent = roleName;
// Filtrer les membres dupliqués ici, avant de les passer à toggleMembers
const uniqueMembers = new Map<string, any>();
roleData.members?.forEach((member: any) => {
const spAddress = member.sp_addresses?.[0];
if (spAddress && !uniqueMembers.has(spAddress)) {
uniqueMembers.set(spAddress, member);
}
});
// Créer un nouveau roleData avec les membres uniques
const filteredRoleData = {
...roleData,
members: Array.from(uniqueMembers.values())
};
roleContainer.addEventListener('click', async (event) => {
console.log("CLICKED ON ROLE:", roleName);
event.stopPropagation();
await this.toggleMembers(filteredRoleData, roleItem);
});
roleContainer.appendChild(roleNameSpan);
roleItem.appendChild(roleContainer);
roleList.appendChild(roleItem);
});
li.appendChild(roleList);
groupList.appendChild(li);
container.addEventListener('click', (event) => {
event.stopPropagation();
container.classList.toggle('expanded');
roleList.style.display = container.classList.contains('expanded') ? 'block' : 'none';
});
}
resolve(processResult);
};
request.onerror = () => {
console.error('❌ Error getting processes:', request.error);
reject(request.error);
};
};
// Ajouter les event listeners
const tabs = tabContent.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabType = tab.getAttribute('data-tab');
if (tabType) {
this.switchTab(tabType, tabs);
}
});
});
//trier les processus : ceux de l'utilisateur en premier
allProcesses.sort((a, b) => {
const aInSet = this.userProcessSet.has(a.value.states[0].commited_in);
const bInSet = this.userProcessSet.has(b.value.states[0].commited_in);
return bInSet ? 1 : aInSet ? -1 : 0;
});
for (const process of allProcesses) {
const li = document.createElement('li');
li.className = 'group-list-item';
const oneProcess = process.value.states[0].commited_in;
let roles;
try {
//roles = await service.getRoles(process);
if (!roles) {
roles = await process.value.states[0]?.roles;
}
} catch (e) {
// console.error('Failed to get roles for process:', process);
continue;
}
// Si le processus est dans notre Set, ajouter la classe my-process
if (this.userProcessSet && this.userProcessSet.has(oneProcess)) {
li.style.cssText = `
background-color: var(--accent-color);
transition: background-color 0.3s ease;
cursor: pointer;
`;
li.onmouseover = () => {
li.style.backgroundColor = 'var(--accent-color-hover)';
};
li.onmouseout = () => {
li.style.backgroundColor = 'var(--accent-color)';
};
console.log("✅ Processus trouvé dans le set:", oneProcess);
}
li.setAttribute('data-process-id', oneProcess);
//----MANAGE THE CLICK ON PROCESS ----
li.onclick = async (event) => {
event.stopPropagation();
console.log("CLICKED ON PROCESS:", oneProcess);
//viser le h1 de signature-header
const signatureHeader = this.shadowRoot?.querySelector('.signature-header h1');
if (signatureHeader) {
const emoji = await addressToEmoji(oneProcess);
signatureHeader.textContent = `Signature of ${emoji}`;
}
this.openSignature();
//afficher les roles dans chaque processus
console.log('🎯 Roles de signature:', roles);
await this.loadAllRolesAndMembersInSignature(roles);
//----MANAGE THE CLICK ON NEW REQUEST ----
await this.newRequest(oneProcess);
};
groupList.appendChild(li);
const container = document.createElement('div');
container.className = 'group-item-container';
const nameSpan = document.createElement('span');
nameSpan.textContent = `Process : `;
nameSpan.className = 'process-name';
container.appendChild(nameSpan);
addressToEmoji(oneProcess).then(emojis => {
const emojiSpan = document.createElement('span');
emojiSpan.className = 'process-emoji';
emojiSpan.textContent = emojis;
container.appendChild(emojiSpan);
});
li.appendChild(container);
// afficher les roles dans chaque processus
//console.log('🎯 Roles:', roles);
const roleList = document.createElement('ul');
roleList.className = 'role-list';
(roleList as HTMLElement).style.display = 'none';
// Traiter chaque rôle
Object.entries(roles).forEach(([roleName, roleData]: [string, any]) => {
const roleItem = document.createElement('li');
roleItem.className = 'role-item';
const roleContainer = document.createElement('div');
roleContainer.className = 'role-item-container';
const roleNameSpan = document.createElement('span');
roleNameSpan.className = 'role-name';
roleNameSpan.textContent = roleName;
// Filtrer les membres dupliqués ici, avant de les passer à toggleMembers
const uniqueMembers = new Map<string, any>();
roleData.members?.forEach((member: any) => {
const spAddress = member.sp_addresses?.[0];
if (spAddress && !uniqueMembers.has(spAddress)) {
uniqueMembers.set(spAddress, member);
}
});
// Créer un nouveau roleData avec les membres uniques
const filteredRoleData = {
...roleData,
members: Array.from(uniqueMembers.values())
};
roleContainer.addEventListener('click', async (event) => {
console.log("CLICKED ON ROLE:", roleName);
event.stopPropagation();
await this.toggleMembers(filteredRoleData, roleItem);
});
roleContainer.appendChild(roleNameSpan);
roleItem.appendChild(roleContainer);
roleList.appendChild(roleItem);
});
li.appendChild(roleList);
groupList.appendChild(li);
container.addEventListener('click', (event) => {
event.stopPropagation();
container.classList.toggle('expanded');
roleList.style.display = container.classList.contains('expanded') ? 'block' : 'none';
});
}
}
private async newRequest(processId: string) {
@ -1317,7 +1246,7 @@ class ChatElement extends HTMLElement {
console.log("Process récupéré:", process);
// Récupérer les rôles directement depuis le dernier état
const roles = process.states[0].encrypted_pcd.roles;
const roles = await service.getRoles(process);
console.log("Roles trouvés:", roles);
if (!roles) return [];
@ -1419,25 +1348,33 @@ class ChatElement extends HTMLElement {
const processes = await service.getProcesses();
for (const [processId, process] of Object.entries(processes)) {
let roles;
try {
roles = await this.getRoles(process);
const roles = process.states[0]?.roles;
if (!roles) {
roles = await process.states[0].encrypted_pcd.roles;
console.log(`Pas de rôles trouvés pour le processus ${processId}`);
continue;
}
const hasCurrentUser = Object.values(roles).some(role =>
this.rolesContainsUs(role)
);
if (hasCurrentUser) {
this.userProcessSet.add(processId);
console.log("Ajout du process au Set:", processId);
for (const roleName in roles) {
const role = roles[roleName];
if (role.members && Array.isArray(role.members)) {
for (const member of role.members) {
if (member.sp_addresses && Array.isArray(member.sp_addresses)) {
if (member.sp_addresses.includes(currentMember[0])) {
this.userProcessSet.add(processId);
console.log(`Ajout du process ${processId} au Set (trouvé dans le rôle ${roleName})`);
break;
}
}
}
}
}
} catch (e) {
console.log(`Erreur lors du traitement du processus ${processId}:`, e);
continue;
}
}
return this.userProcessSet;
@ -1483,7 +1420,7 @@ class ChatElement extends HTMLElement {
const service = await Services.getInstance();
const process = await service.getProcess(this.processId);
const roles = await this.getRoles(process);
const roles = await service.getRoles(process);
if (roles === null) {
console.error('no roles in process');
return;

View File

@ -213,7 +213,7 @@ export default class Services {
private async ensureSufficientAmount(): Promise<void> {
const availableAmt = this.getAmount();
const target: BigInt = DEFAULTAMOUNT * BigInt(2);
const target: BigInt = DEFAULTAMOUNT * BigInt(10);
if (availableAmt < target) {
const faucetMsg = this.createFaucetMessage();
@ -355,25 +355,26 @@ export default class Services {
throw new Error('No paired member found');
}
const roles = {
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: '',
roles: {
dm: {
members: [
{ sp_addresses: myAddresses },
{ sp_addresses: otherMember }
],
validation_rules: [
{
quorum: 0.01,
fields: ['message', 'description', 'roles'],
min_sig_member: 0.01,
},
],
storages: [STORAGEURL]
}
}
};
console.log('📋 Template final:', JSON.stringify(dmTemplate, null, 2));
@ -382,10 +383,11 @@ export default class Services {
const feeRate = 1;
const initState = JSON.stringify(dmTemplate);
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,
JSON.stringify(roles),
null,
relayAddress,
feeRate
@ -399,12 +401,15 @@ export default class Services {
}
}
public async updateProcess(processId: string, new_state: any): Promise<ApiReturn> {
const roles = new_state.roles;
public async updateProcess(process: Process, new_state: any, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> {
// If roles is null, we just take the last commited state roles
if (!roles) {
throw new Error('new state doesn\'t contain roles');
roles = await 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();
for (const role of Object.values(roles)) {
for (const member of role.members) {
@ -414,7 +419,8 @@ export default class Services {
console.log(members);
await this.checkConnections([...members]);
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) {
throw new Error(`Failed to update process: ${e}`);
}
@ -528,91 +534,18 @@ export default class Services {
}
}
private async getCipherForDiff(diff: UserDiff): Promise<string | null> {
// get the process
public async updateProcessesWorker() {
try {
const process = await this.getProcess(diff.process_id);
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');
}
}
const myProcesses = await this.getMyProcesses();
const db = await Database.getInstance();
await db.updateMyProcesses(myProcesses);
} catch (e) {
console.error('Failed to get process:', e);
return null;
console.error('Failed to update processes worker:', e);
}
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) {
console.log(apiReturn);
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 new Promise(r => setTimeout(r, 500));
@ -652,14 +585,30 @@ export default class Services {
const processId: string = updatedProcess.process_id;
// Save process to db
try {
await this.saveProcessToDb(processId, updatedProcess.current_process);
await this.saveProcessDataToDb(updatedProcess.current_process);
} catch (e) {
throw e;
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 {
await this.saveBlobToDb(hash, blob);
} catch (e) {
console.error(e);
}
}
}
// Save process to db
await this.saveProcessToDb(processId, updatedProcess.current_process);
setTimeout(async () => {
try {
await this.updateProcessesWorker();
} catch (e) {
console.error(e);
}
}, 0)
const isPaired = this.isPaired();
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
@ -677,16 +626,11 @@ export default class Services {
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
for (const hash of apiReturn.push_to_storage) {
try {
const value = await this.getBlobFromDb(hash);
console.log(value);
if (value) {
await this.saveDataToStorage(hash, value, null);
} else {
console.error('Failed to get data from db');
}
} catch (e) {
console.error(e);
const blob = await this.getBlobFromDb(hash);
if (blob) {
await this.saveDataToStorage(hash, blob, null);
} else {
console.error('Failed to get data from db');
}
}
}
@ -825,15 +769,6 @@ export default class Services {
return true;
}
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() {
const wallet = await this.sdkClient.dump_wallet();
console.log('🚀 ~ Services ~ dumpWallet ~ wallet:', wallet);
@ -880,25 +815,6 @@ export default class Services {
}
}
public async saveProcessDataToDb(process: Process) {
for (const state of process.states) {
if (state.state_id === "") continue
for (const field of Object.keys(state.pcd_commitment)) {
try {
const hash = state.pcd_commitment[field];
const encrypted = state.encrypted_pcd[field];
console.log(hash);
console.log(encrypted);
const blob = new Blob([encrypted], { type: 'text/plain' });
console.log(blob);
await this.saveBlobToDb(hash, blob);
} catch (e) {
console.error(e);
}
}
}
}
public async saveProcessToDb(processId: string, process: Process) {
const db = await Database.getInstance();
try {
@ -908,31 +824,7 @@ export default class Services {
key: processId,
});
} catch (e) {
throw new Error(`Failed to save process: ${e}`);
}
}
public async saveStatesToStorage(process: Process, state_ids: string[]) {
// We check how many copies in storage nodes
// We check the storage nodes in the process itself
// this.sdkClient.get_storages(commitedIn);
const storages = [STORAGEURL];
for (const state of process.states) {
if (state.state_id === "") {
continue;
}
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 = await getBlobFromDb(hash);
await storeData(storages, hash, value, null);
}
}
console.error(`Failed to save process ${processId}: ${e}`);
}
}
@ -945,20 +837,20 @@ export default class Services {
key: hash,
});
} catch (e) {
throw new Error(`Failed to save process: ${e}`);
console.error(`Failed to save data to db: ${e}`);
}
}
public async getBlobFromDb(hash: string): Promise<Blob> {
public async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
try {
return await db.getObject('data', hash);
} catch (e) {
throw new Error(`Failed to save process: ${e}`);
return null;
}
}
public async saveDataToStorage(hash: string, data: string, ttl: number | null) {
public async saveDataToStorage(hash: string, data: Blob, ttl: number | null) {
const storages = [STORAGEURL];
try {
@ -1037,18 +929,6 @@ export default class Services {
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
public async restoreProcessesFromDB() {
const db = await Database.getInstance();
@ -1113,35 +993,35 @@ export default class Services {
}
}
async getDescription(processId: string, process: Process): Promise<string | null> {
// Get the `commited_in` value of the last state and remove it from the array
const currentCommitedIn = process.states.at(-1)?.commited_in;
if (currentCommitedIn === undefined) {
return null; // No states available
}
// Find the last state where `commited_in` is different
let lastDifferentState = process.states.findLast(
state => state.commited_in !== currentCommitedIn
);
if (!lastDifferentState) {
// It means that we only have one state that is not commited yet, that can happen with process we just created
// 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
lastDifferentState = process.states.at(-1);
}
if (!lastDifferentState.pcd_commitment) {
async decryptAttribute(state: ProcessState, attribute: string): Promise<string | null> {
let hash;
let key;
try {
hash = state.pcd_commitment[attribute];
} catch (e) {
console.error(`Failed to find hash for attribute ${attribute}`);
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;
try {
key = state.keys[attribute];
} catch (e) {
console.error(`Failed to find key for attribute ${attribute}`);
return null;
}
if (hash && key) {
const blob = await this.getBlobFromDb(hash);
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();
const clear = this.sdkClient.decrypt_data(new Uint8Array(keyBuf), cipher);
if (clear) {
// This is stringified json, we parse it back
return JSON.parse(clear);
}
}
}
@ -1254,14 +1134,19 @@ export default class Services {
if (newProcesses && Object.keys(newProcesses).length !== 0) {
for (const [processId, process] of Object.entries(newProcesses)) {
const existing = await this.getProcess(processId);
if (!existing) {
if (existing) {
console.log(`${processId} already in db`);
// 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 {
// We add it to db
console.log(`Saving ${processId} to db`);
await this.saveProcessToDb(processId, process as Process);
} else {
console.log(`${processId} already in db`);
}
}
await this.updateProcessesWorker();
}
}, 500)
} catch (e) {
@ -1293,27 +1178,12 @@ export default class Services {
}
public async getRoles(process: Process): Promise<any | null> {
const currentCommitedIn = process.states.pop()?.commited_in;
if (currentCommitedIn === undefined) {
return null;
}
let lastDifferentState = process.states.findLast(
state => state.commited_in !== currentCommitedIn
);
if (!lastDifferentState) {
lastDifferentState = process.states.pop();
}
if (lastDifferentState && lastDifferentState.roles) {
return lastDifferentState!.roles;
public async getRoles(process: Process): Promise<record<string, RoleDefinition> | null> {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
return lastCommitedState!.roles;
} else {
return {};
return null;
}
}
@ -1353,4 +1223,36 @@ export default class Services {
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;
}
}
}

View File

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