From d1132155a695fc280af395b8cc1610a5660510d0 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 26 Nov 2024 21:19:46 +0100 Subject: [PATCH] Complete pairing --- src/services/modal.service.ts | 45 ++++-- src/services/service.ts | 285 ++++++++++++++++------------------ src/utils/sp-address.utils.ts | 19 ++- 3 files changed, 181 insertions(+), 168 deletions(-) diff --git a/src/services/modal.service.ts b/src/services/modal.service.ts index fc6c155..ee171ec 100755 --- a/src/services/modal.service.ts +++ b/src/services/modal.service.ts @@ -8,7 +8,7 @@ import { RoleDefinition } from 'dist/pkg/sdk_client'; export default class ModalService { private static instance: ModalService; - private currentPrd: any; + private currentPcdCommitment: string | null = null; private currentOutpoint: string | null = null; private constructor() {} private paired_addresses: string[] = []; @@ -52,7 +52,8 @@ export default class ModalService { } } - public async openConfirmationModal(pcd: any, commitmentTx: string) { + // this is kind of too specific for pairing though + public async openConfirmationModal(pcd: any, commitmentTx: string, merkleRoot: string) { let map: Record; if (pcd['roles']) { const roles = pcd['roles']; @@ -65,6 +66,7 @@ export default class ModalService { throw new Error('Pcd doesn\'t have a \"roles\" field'); } + // pairing specifics let members; if (map['owner']) { const owner = map['owner']; @@ -73,6 +75,7 @@ export default class ModalService { throw new Error('No \"owner\" role'); } + // pairing specifics if (members.length != 1) { throw new Error('Must have exactly 1 member'); } @@ -81,12 +84,15 @@ export default class ModalService { const service = await Services.getInstance(); const localAddress = await service.getDeviceAddress(); console.log('🚀 ~ Routing ~ openConfirmationModal ~ pcd:', pcd); - for (const address of members[0]['sp_addresses']) { - if (address !== localAddress) { - this.paired_addresses.push(address); + for (const member of members) { + for (const address of member['sp_addresses']) { + if (address !== localAddress) { + this.paired_addresses.push(address); + } } } this.currentOutpoint = commitmentTx; + this.currentPcdCommitment = merkleRoot; await this.injectModal(members); const modal = document.getElementById('modal'); if (modal) modal.style.display = 'flex'; @@ -116,18 +122,33 @@ export default class ModalService { async confirmPairing() { const service = await Services.getInstance(); const modal = document.getElementById('modal'); - // console.log("🚀 ~ Routing ~ confirm ~ prd:", prd) if (modal) modal.style.display = 'none'; - if (this.currentOutpoint === null || this.paired_addresses.length === 0) { - throw new Error('Missing outpoint and/or paired addresses'); - } + // We send the prd update + if (this.currentPcdCommitment) { + try { + const createPrdUpdateReturn = await service.createPrdUpdate(this.currentPcdCommitment); + await service.handleApiReturn(createPrdUpdateReturn); + } catch (e) { + throw e + } + } else { + throw new Error('No currentPcdCommitment'); + } - // We take the paired device(s) from the contract - await service.pairDevice(this.currentOutpoint, this.paired_addresses); + // We send confirmation that we validate the change + try { + const approveChangeReturn = await service.approveChange(this.currentPcdCommitment!); + await service.handleApiReturn(approveChangeReturn); + } catch (e) { + throw e + } + + service.pairDevice(this.paired_addresses); this.paired_addresses = []; this.currentOutpoint = null; - const newDevice = await service.dumpDevice(); + this.currentPcdCommitment = null; + const newDevice = service.dumpDevice(); await service.saveDevice(newDevice); navigate('process'); } diff --git a/src/services/service.ts b/src/services/service.ts index 8596d76..d09efc5 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -8,17 +8,18 @@ import ModalService from './modal.service'; import { navigate } from '../router'; import Database from './database.service'; -type ProcessesCache = { - [key: string]: any; -}; - export const U32_MAX = 4294967295; +const RELAY_ADDRESS = "sprt1qqdg4x69xdyhxpz4weuel0985qyswa0x9ycl4q6xc0fngf78jtj27gqj5vff4fvlt3fydx4g7vv0mh7vqv8jncgusp6n2zv860nufdzkyy59pqrdr"; const wsurl = `https://demo.4nkweb.com/ws/`; export default class Services { private static initializing: Promise | null = null; private static instance: Services; - private current_process: string | null = null; + private currentProcess: string | null = null; + private pendingUpdates: Record = {}; + private currentUpdateMerkleRoot: string | null = null; + private localAddress: string | null = null; + private pairedAddresses: string[] = []; private sdkClient: any; private processes: IProcess[] | null = null; private notifications: INotification[] | null = null; @@ -57,50 +58,31 @@ export default class Services { } public async addWebsocketConnection(url: string): Promise { - // const services = await Services.getInstance(); console.log('Opening new websocket connection'); - const newClient = initWebsocket(url); + await initWebsocket(url); } - public isPaired(): boolean | undefined { + public async getRelayAddresses(): Promise { + // We just return one hardcoded address for now + return [RELAY_ADDRESS]; + } + + public isPaired(): boolean { try { return this.sdkClient.is_linking(); } catch (e) { - console.error('isPaired ~ Error:', e); + throw new Error(`isPaired ~ Error: ${e}`); } } public async unpairDevice(): Promise { - const service = await Services.getInstance(); - await service.sdkClient.unpair_device(); - const newDevice = await this.dumpDevice(); - await this.saveDevice(newDevice); - } - - private prepareProcessTx(myAddress: string, recipientAddress: string) { - const initial_session_privkey = new Uint8Array(32); - const initial_session_pubkey = new Uint8Array(32); - const pairingTemplate = { - description: 'AliceBob', - roles: { - owner: { - members: [{ sp_addresses: [myAddress, recipientAddress] }], - validation_rules: [ - { - quorum: 1.0, - fields: ['description', 'roles', 'session_privkey', 'session_pubkey', 'key_parity'], - min_sig_member: 1.0, - }, - ], - }, - }, - session_privkey: initial_session_privkey, - session_pubkey: initial_session_pubkey, - key_parity: true, - }; - - const apiReturn = this.sdkClient.create_update_transaction(undefined, JSON.stringify(pairingTemplate), 1); - return apiReturn; + try { + this.sdkClient.unpair_device(); + const newDevice = this.dumpDevice(); + await this.saveDevice(newDevice); + } catch (e) { + throw new Error(`Failed to unpair device: ${e}`); + } } public async getSecretForAddress(address: string): Promise { @@ -108,7 +90,7 @@ export default class Services { return await db.getObject('shared_secrets', address); } - public async connectMember(members: Member[]): Promise { + public async connectMember(members: Member[]): Promise { if (members.length === 0) { throw new Error('Trying to connect to empty members list'); } @@ -144,15 +126,13 @@ export default class Services { } try { - const apiReturn = this.sdkClient.create_connect_transaction(members_str, 1); - - await this.handleApiReturn(apiReturn); + return this.sdkClient.create_connect_transaction(members_str, 1); } catch (e) { - console.error('Failed to connect:', e); + throw e; } } - public async createPairingProcess(pairWith: string[], relayAddress: string, feeRate: number): Promise { + public async createPairingProcess(pairWith: string[], relayAddress: string, feeRate: number): Promise { const myAddress: string = this.sdkClient.get_address(); pairWith.push(myAddress); const newKey = this.sdkClient.get_new_keypair(); @@ -175,14 +155,61 @@ export default class Services { key_parity: newKey['key_parity'], }; try { - const newProcessReturn = this.sdkClient.create_new_process(JSON.stringify(pairingTemplate), relayAddress, feeRate); - console.log('newProcessReturn:', newProcessReturn); - await this.handleApiReturn(newProcessReturn); + return this.sdkClient.create_new_process(JSON.stringify(pairingTemplate), relayAddress, feeRate); } catch (e) { throw new Error(`Creating process failed:, ${e}`); } } + // Create prd update for current process and update + public createPrdUpdate(pcdMerkleRoot: string): ApiReturn { + if (!this.currentProcess) { + throw new Error('No current process defined'); + } + + try { + return this.sdkClient.create_update_message(this.currentProcess, pcdMerkleRoot); + } catch (e) { + throw new Error(`Failed to create prd update: ${e}`); + } + } + + public createPrdResponse(pcdMerkleRoot: string): ApiReturn { + if (!this.currentProcess) { + throw new Error('No current process defined'); + } + + try { + return this.sdkClient.create_response_prd(this.currentProcess, pcdMerkleRoot); + } catch (e) { + throw e + } + } + + public approveChange(currentPcdMerkleRoot: string): ApiReturn { + if (!this.currentProcess) { + throw new Error('No current process defined'); + } + + try { + return this.sdkClient.validate_state(this.currentProcess, currentPcdMerkleRoot); + } catch (e) { + throw new Error(`Failed to create prd response: ${e}`); + } + } + + public rejectChange(): ApiReturn { + if (!this.currentProcess || !this.currentUpdateMerkleRoot) { + throw new Error('No current process and/or current update defined'); + } + + try { + return this.sdkClient.refuse_state(this.currentProcess, this.currentUpdateMerkleRoot); + } catch (e) { + throw new Error(`Failed to create prd response: ${e}`); + } + } + async resetDevice() { await this.sdkClient.reset_device(); } @@ -209,11 +236,11 @@ export default class Services { async parseCipher(message: string) { try { console.log('parsing new cipher'); - const apiReturn = await this.sdkClient.parse_cipher(message, 0.00001); + const apiReturn = await this.sdkClient.parse_cipher(message); console.log('🚀 ~ Services ~ parseCipher ~ apiReturn:', apiReturn); await this.handleApiReturn(apiReturn); } catch (e) { - console.log("Cipher isn't for us"); + console.error(`Parsed cipher with error: ${e}`); } // await this.saveCipherTxToDb(parsedTx) } @@ -236,7 +263,21 @@ export default class Services { } } - private async handleApiReturn(apiReturn: ApiReturn) { + public getUpdateProposals(commitmentOutpoint: string) { + try { + const proposals: ApiReturn = this.sdkClient.get_update_proposals(commitmentOutpoint); + if (proposals.decrypted_pcds && proposals.decrypted_pcds.length != 0) { + this.currentProcess = commitmentOutpoint; + this.pendingUpdates = proposals.decrypted_pcds; + } else { + throw new Error('No pending proposals'); + } + } catch (e) { + throw new Error(`Failed to get proposal updates for process ${commitmentOutpoint}: ${e}`); + } + } + + public async handleApiReturn(apiReturn: 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)); } @@ -245,7 +286,6 @@ export default class Services { const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; const confirmedSecrets = apiReturn.secrets.shared_secrets; - console.log('confirmedSecrets:', confirmedSecrets); const db = await Database.getInstance(); for (const secret of unconfirmedSecrets) { db.addObject({ @@ -256,8 +296,6 @@ export default class Services { } const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value })); for (const entry of entries) { - console.log('entry:', entry); - db.addObject({ storeName: 'shared_secrets', object: entry, @@ -277,18 +315,6 @@ export default class Services { object: { id: commitmentTx, process }, key: null, }); - // Check if the newly updated process reveals some new information - try { - const proposals: ApiReturn = this.sdkClient.get_update_proposals(commitmentTx); - const decrypted_pcds = proposals.decrypted_pcds; - if (decrypted_pcds && decrypted_pcds.length != 0) { - for (const actual_proposal of Object.values(decrypted_pcds)) { - await this.routingInstance.openConfirmationModal(actual_proposal, commitmentTx); - } - } - } catch (e) { - console.error(e); - } } if (apiReturn.commit_to_send) { @@ -296,62 +322,45 @@ export default class Services { await this.sendCommitMessage(JSON.stringify(commit)); } - if (apiReturn.decrypted_pcds && apiReturn.decrypted_pcds.length != 0) { - // TODO - } - if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { await this.sendCipherMessages(apiReturn.ciphers_to_send); } }, 0); } - async pairDevice(commitmentTx: string, spAddressList: string[]) { - await this.sdkClient.pair_device(commitmentTx, spAddressList); + public async evaluatePendingUpdates() { + if (!this.currentProcess) { + throw new Error('No current process'); + } + + try { + await this.openConfirmationModal(); + } catch (e) { + throw new Error(`Error while evaluating pending updates for process ${this.currentProcess}: ${e}`) + } } - async validatePairingDevice(prd: any, outpointCommitment: string) { - console.log('🚀 ~ Services ~ pairDevice ~ prd:', prd); - const spAddress = (await this.getDeviceAddress()) as any; - const sender = JSON.parse(prd?.sender); - const senderAddress = sender?.sp_addresses?.find((address: string) => address !== spAddress); - console.log('🚀 ~ Services ~ pairDevice ~ senderAddress:', senderAddress); - if (senderAddress) { - const proposal = this.sdkClient.get_update_proposals(outpointCommitment); - console.log('🚀 ~ Services ~ pairDevice ~ proposal:', proposal); - // const pairingTx = proposal.pairing_tx.replace(/^\"+|\"+$/g, '') - const parsedProposal = JSON.parse(proposal[0]); + private async openConfirmationModal() { + if (this.pendingUpdates.length === 0) { + console.log('No pending updates'); + } - console.log('🚀 ~ Services ~ pairDevice ~ parsedProposal:', parsedProposal); - const roles = JSON.parse(parsedProposal.roles); - console.log('🚀 ~ Services ~ pairDevice ~ roles:', roles, Array.isArray(roles), !roles.owner); - if (Array.isArray(roles) || !roles.owner) return; - const proposalMembers = roles?.owner?.members; - const isFirstDevice = proposalMembers.some((member: Member) => member.sp_addresses.some((address) => address === spAddress)); - const isSecondDevice = proposalMembers.some((member: Member) => member.sp_addresses.some((address) => address === senderAddress)); - console.log('🚀 ~ Services ~ pairDevice ~ proposalMembers:', proposalMembers); - if (proposalMembers?.length !== 2 || !isFirstDevice || !isSecondDevice) return; - const pairingTx = parsedProposal?.pairing_tx?.replace(/^\"+|\"+$/g, ''); - - let txid = '0'.repeat(64); - console.log('🚀 ~ Services ~ pairDevice ~ pairingTx:', pairingTx, `${txid}:4294967295`); - - const pairing = await this.sdkClient.pair_device(`${txid}:4294967295`, [senderAddress]); - const device = this.dumpDevice(); - console.log('🚀 ~ Services ~ pairDevice ~ device:', device); - this.saveDevice(device); - // await service.sdkClient.pair_device(pairingTx, [senderAddress]) - console.log('🚀 ~ Services ~ pairDevice ~ pairing:', pairing); - // const process = await this.prepareProcessTx(spAddress, senderAddress) - console.log('🚀 ~ Services ~ pairDevice ~ process:', outpointCommitment, prd, prd.payload); - const prdString = JSON.stringify(prd).trim(); - console.log('🚀 ~ Services ~ pairDevice ~ prdString:', prdString); - let tx = await this.sdkClient.response_prd(outpointCommitment, prdString, true); - console.log('🚀 ~ Services ~ pairDevice ~ tx:', tx); - if (tx.ciphers_to_send) { - tx.ciphers_to_send.forEach((cipher: string) => sendMessage('Cipher', cipher)); + try { + for (const [merkleRoot, actual_proposal] of Object.entries(this.pendingUpdates)) { + await this.routingInstance.openConfirmationModal(actual_proposal, this.currentProcess!, merkleRoot); + } + } catch (e) { + throw new Error(`${e}`); + } + } + + pairDevice(spAddressList: string[]) { + if (this.currentProcess) { + try { + this.sdkClient.pair_device(this.currentProcess, spAddressList); + } catch (e) { + throw new Error(`Failed to pair device: ${e}`); } - navigate('process'); } } @@ -364,12 +373,15 @@ export default class Services { return await this.sdkClient.get_address(); } - async dumpDevice() { - const device = await this.sdkClient.dump_device(); - return device; + public dumpDevice(): string { + try { + return this.sdkClient.dump_device(); + } catch (e) { + throw new Error(`Failed to dump device: ${e}`); + } } - async saveDevice(device: any): Promise { + async saveDevice(device: any): Promise { const db = await Database.getInstance(); try { await db.addObject({ @@ -506,47 +518,12 @@ export default class Services { return process; } - private getProcessesCache(): ProcessesCache { - // Regular expression to match 64-character hexadecimal strings - const hexU32KeyRegex: RegExp = /^[0-9a-fA-F]{64}:\d+$/; - const hexObjects: ProcessesCache = {}; - - // Iterate over all keys in localStorage - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - - if (!key) { - return hexObjects; - } - - // Check if the key matches the 32-byte hex pattern - if (hexU32KeyRegex.test(key)) { - const value = localStorage.getItem(key); - if (!value) { - continue; - } - - hexObjects[key] = JSON.parse(value); - } - } - - return hexObjects; + private getProcessesCache() { + console.log('TODO get processes from indexedDB') } public async restoreProcesses() { - const processesCache = this.getProcessesCache(); - console.log('🚀 ~ Services ~ restoreProcesses ~ processesCache:', processesCache); - - if (processesCache.length == 0) { - console.debug('🚀 ~ Services ~ restoreProcesses ~ no processes in local storage'); - return; - } - - try { - await this.sdkClient.set_process_cache(JSON.stringify(processesCache)); - } catch (e) { - console.error('Services ~ restoreProcesses ~ Error:', e); - } + console.log('TODO: restore processes from indexedDB'); } getNotifications(): INotification[] { diff --git a/src/utils/sp-address.utils.ts b/src/utils/sp-address.utils.ts index b0e2cc4..725e67f 100755 --- a/src/utils/sp-address.utils.ts +++ b/src/utils/sp-address.utils.ts @@ -149,14 +149,29 @@ async function onOkButtonClick() { const service = await Services.getInstance(); const addressInput = (document.getElementById('addressInput') as HTMLInputElement).value; try { + // Connect to target, if necessary const sharedSecret = await service.getSecretForAddress(addressInput); if (!sharedSecret) { const member = { sp_addresses: [addressInput], } - await service.connectMember([member]); + const connectMemberResult = await service.connectMember([member]); + await service.handleApiReturn(connectMemberResult); } - await service.createPairingProcess([addressInput], "sprt1qqdg4x69xdyhxpz4weuel0985qyswa0x9ycl4q6xc0fngf78jtj27gqj5vff4fvlt3fydx4g7vv0mh7vqv8jncgusp6n2zv860nufdzkyy59pqrdr", 1); + // Create the process + const relayAddress = await service.getRelayAddresses(); // Get one (or more?) relay addresses + const createPairingProcessReturn = await service.createPairingProcess([addressInput], relayAddress[0], 1); + await service.handleApiReturn(createPairingProcessReturn); + + if (!createPairingProcessReturn.updated_process) { + throw new Error('createPairingProcess returned an empty new process'); // This should never happen + } + const [commitmentOutpoint, process] = createPairingProcessReturn.updated_process; + + // We set the service to the process + service.getUpdateProposals(commitmentOutpoint); + + await service.evaluatePendingUpdates(); } catch (e) { console.error('onOkButtonClick error:', e); }