From d2ffea95d688067406cbde58eed6cfd235345b21 Mon Sep 17 00:00:00 2001 From: Duddino Date: Fri, 3 Nov 2023 13:25:55 +0100 Subject: [PATCH 1/4] Improve jsdoc typings and mark functions as private --- js/pivx_shield.js | 382 +++++++++++++++++++++++++++------------------- 1 file changed, 223 insertions(+), 159 deletions(-) diff --git a/js/pivx_shield.js b/js/pivx_shield.js index 2e36fc4..0e0188d 100644 --- a/js/pivx_shield.js +++ b/js/pivx_shield.js @@ -2,36 +2,106 @@ import bs58 from "bs58"; import { v4 as genuuid } from "uuid"; export class PIVXShield { - initWorker() { - this.promises = new Map(); - this.shieldWorker.onmessage = (msg) => { - const { res, rej } = this.promises.get(msg.data.uuid); + /** + * Webassembly object that holds Shield related functions + * @private + */ + #shieldWorker; + + /** + * Extended spending key + * @type {string} + * @private + */ + #extsk; + + /** + * Extended full viewing key + * @type {string} + * @private + */ + #extfvk; + + /** + * Diversifier index of the last generated address. + * @private + */ + #diversifierIndex = new Uint8Array(11); + + /** + * @type {boolean} + * @private + */ + #isTestnet; + + /** + * Last processed block in the blockchain + * @type {number} + * @private + */ + #lastProcessedBlock; + + /** + * Hex encoded commitment tree + * @type {string} + * @private + */ + #commitmentTree; + + /** + * Array of notes, corresponding witness + * @type {[Note, string][]} + * @private + */ + #unspentNotes = []; + + /** + * @type {Map} A map txid->nullifiers, storing pending transaction. + * @private + */ + + #pendingSpentNotes = new Map(); + + /** + * @type {Map} A map txid->Notes, storing incoming spendable notes. + * @private + */ + #pendingUnspentNotes = new Map(); + + /** + * @type{Map void, rej: (...args: any) => void}>} + */ + #promises = new Map(); + + #initWorker() { + this.#shieldWorker.onmessage = (msg) => { + const { res, rej } = this.#promises.get(msg.data.uuid); if (msg.data.rej) { rej(msg.data.rej); } else { res(msg.data.res); } - this.promises.delete(msg.data.uuid); + this.#promises.delete(msg.data.uuid); }; } - async callWorker(name, ...args) { + async #callWorker(name, ...args) { const uuid = genuuid(); return await new Promise((res, rej) => { - this.promises.set(uuid, { res, rej }); - this.shieldWorker.postMessage({ uuid, name, args }); + this.#promises.set(uuid, { res, rej }); + this.#shieldWorker.postMessage({ uuid, name, args }); }); } /** * Creates a PIVXShield object - * @param {Object} o - options - * @param {Array?} o.seed - array of 32 bytes that represents a random seed. - * @param {String?} o.extendedSpendingKey - Extended Spending Key. - * @param {String?} o.extendedFullViewingKey - Full viewing key - * @param {Number} o.blockHeight - number representing the block height of creation of the wallet - * @param {Number} o.coinType - number representing the coin type, 1 represents testnet - * @param {Number} o.accountIndex - index of the account that you want to generate, by default is set to 0 - * @param {Boolean} o.loadSaplingData - if you want to load sapling parameters on creation, by deafult is set to true + * @param {object} o - options + * @param {number[]?} o.seed - array of 32 bytes that represents a random seed. + * @param {string?} o.extendedSpendingKey - Extended Spending Key. + * @param {string?} o.extendedFullViewingKey - Full viewing key + * @param {number} o.blockHeight - number representing the block height of creation of the wallet + * @param {number} o.coinType - number representing the coin type, 1 represents testnet + * @param {number} o.accountIndex - index of the account that you want to generate, by default is set to 0 + * @param {boolean} o.loadSaplingData - if you want to load sapling parameters on creation, by deafult is set to true */ static async create({ seed, @@ -61,13 +131,13 @@ export class PIVXShield { }; }); - const isTestNet = coinType == 1 ? true : false; + const isTestnet = coinType == 1 ? true : false; const pivxShield = new PIVXShield( shieldWorker, extendedSpendingKey, extendedFullViewingKey, - isTestNet, + isTestnet, null, null, ); @@ -83,113 +153,70 @@ export class PIVXShield { coin_type: coinType, account_index: accountIndex, }; - extendedSpendingKey = await pivxShield.callWorker( + extendedSpendingKey = await pivxShield.#callWorker( "generate_extended_spending_key_from_seed", serData, ); - pivxShield.extsk = extendedSpendingKey; + pivxShield.#extsk = extendedSpendingKey; } if (extendedSpendingKey) { - pivxShield.extfvk = await pivxShield.callWorker( + pivxShield.#extfvk = await pivxShield.#callWorker( "generate_extended_full_viewing_key", - pivxShield.extsk, - isTestNet, + pivxShield.#extsk, + isTestnet, ); } + const [effectiveHeight, commitmentTree] = await pivxShield.callWorker( "get_closest_checkpoint", blockHeight, - isTestNet, + isTestnet, ); pivxShield.lastProcessedBlock = effectiveHeight; pivxShield.commitmentTree = commitmentTree; + return pivxShield; } - constructor(shieldWorker, extsk, extfvk, isTestNet, nHeight, commitmentTree) { - /** - * Webassembly object that holds Shield related functions - * @private - */ - this.shieldWorker = shieldWorker; - /** - * Extended spending key - * @type {String} - * @private - */ - this.extsk = extsk; - /** - * Extended full viewing key - * @type {String} - * @private - */ - this.extfvk = extfvk; - /** - * Diversifier index of the last generated address - * @type {Uint8Array} - * @private - */ - this.diversifierIndex = new Uint8Array(11); - /** - * @type {Boolean} - * @private - */ - this.isTestNet = isTestNet; - /** - * Last processed block in the blockchain - * @type {Number} - * @private - */ - this.lastProcessedBlock = nHeight; - /** - * Hex encoded commitment tree - * @type {String} - * @private - */ - this.commitmentTree = commitmentTree; - /** - * Array of notes, corresponding witness - * @type {[Note, String][]} - * @private - */ - this.unspentNotes = []; - - /** - * @type {Map} A map txid->nullifiers, storing pending transaction. - * @private - */ - - this.pendingSpentNotes = new Map(); + /** + * @private + */ + constructor(shieldWorker, extsk, extfvk, isTestnet, nHeight, commitmentTree) { + this.#shieldWorker = shieldWorker; + this.#extsk = extsk; + this.#extfvk = extfvk; + this.#isTestnet = isTestnet; + this.#lastProcessedBlock = nHeight; - /** - * @type {Map} A map txid->Notes, storing incoming spendable notes. - * @private - */ - this.pendingUnspentNotes = new Map(); + this.#commitmentTree = commitmentTree; - this.initWorker(); + this.#initWorker(); } /** * Load an extended spending key in order to have spending authority - * @param {String} enc_extsk - extended spending key + * @param {string} enc_extsk - extended spending key */ async loadExtendedSpendingKey(enc_extsk) { - if (this.extsk) { + if (this.#extsk) { throw new Error("A spending key is aready loaded"); } - const enc_extfvk = await this.callWorker( + const enc_extfvk = await this.#callWorker( "generate_extended_full_viewing_key", enc_extsk, - this.isTestNet, + this.#isTestnet, ); - if (enc_extfvk !== this.extfvk) { + if (enc_extfvk !== this.#extfvk) { throw new Error("Extended full viewing keys do not match"); } - this.extsk = enc_extsk; + this.#extsk = enc_extsk; } - //Save your shield data + /** + * @returns {string} a string that saves the public shield data. + * The seed or extended spending key still needs to be provided + * if spending authority is needed + */ save() { return JSON.stringify( new ShieldData({ @@ -198,7 +225,12 @@ export class PIVXShield { commitmentTree: this.commitmentTree, diversifierIndex: this.diversifierIndex, unspentNotes: this.unspentNotes, - isTestNet: this.isTestNet, + isTestnet: this.isTestnet, + defaultAddress: address, + lastProcessedBlock: this.#lastProcessedBlock, + commitmentTree: this.#commitmentTree, + diversifierIndex: this.#diversifierIndex, + unspentNotes: this.#unspentNotes, }), ); } @@ -212,6 +244,7 @@ export class PIVXShield { const shieldWorker = new Worker( new URL("worker_start.js", import.meta.url), ); + await new Promise((res) => { shieldWorker.onmessage = (msg) => { if (msg.data === "done") res(); @@ -221,7 +254,7 @@ export class PIVXShield { shieldWorker, null, shieldData.extfvk, - shieldData.isTestNet, + shieldData.isTestnet, shieldData.lastProcessedBlock, shieldData.commitmentTree, ); @@ -229,77 +262,78 @@ export class PIVXShield { pivxShield.unspentNotes = shieldData.unspentNotes; return pivxShield; } + /** * Loop through the txs of a block and update useful shield data - * @param {{txs: String[], height: Number}} blockJson - Json of the block outputted from any PIVX node + * @param {{txs: string[], height: number}} blockJson - Json of the block outputted from any PIVX node */ async handleBlock(blockJson) { - if (this.lastProcessedBlock > blockJson.height) { + if (this.#lastProcessedBlock > blockJson.height) { throw new Error( "Blocks must be processed in a monotonically increasing order!", ); } for (const tx of blockJson.txs) { - await this.addTransaction(tx.hex); - this.pendingUnspentNotes.delete(tx.txid); + await this.#addTransaction(tx.hex); + this.#pendingUnspentNotes.delete(tx.txid); } - this.lastProcessedBlock = blockJson.height; + this.#lastProcessedBlock = blockJson.height; } /** * Adds a transaction to the tree. Decrypts notes and stores nullifiers - * @param {String} hex - transaction hex + * @param {string} hex - transaction hex */ - async addTransaction(hex, decryptOnly = false) { - const res = await this.callWorker( + async #addTransaction(hex, decryptOnly = false) { + const res = await this.#callWorker( "handle_transaction", - this.commitmentTree, + this.#commitmentTree, hex, - this.extfvk, - this.isTestNet, - this.unspentNotes, + this.#extfvk, + this.#isTestnet, + this.#unspentNotes, ); if (decryptOnly) { return res.decrypted_notes.filter( (note) => - !this.unspentNotes.some( + !this.#unspentNotes.some( (note2) => JSON.stringify(note2[0]) === JSON.stringify(note[0]), ), ); } else { - this.commitmentTree = res.commitment_tree; - this.unspentNotes = res.decrypted_notes; + this.#commitmentTree = res.commitment_tree; + this.#unspentNotes = res.decrypted_notes; if (res.nullifiers.length > 0) { - await this.removeSpentNotes(res.nullifiers); + await this.#removeSpentNotes(res.nullifiers); } } } /** * Remove the Shield Notes that match the nullifiers given in input - * @param {Array} blockJson - Array of nullifiers + * @param {string[]} blockJson - Array of nullifiers */ - async removeSpentNotes(nullifiers) { - this.unspentNotes = await this.callWorker( + async #removeSpentNotes(nullifiers) { + this.#unspentNotes = await this.#callWorker( "remove_spent_notes", - this.unspentNotes, + this.#unspentNotes, nullifiers, - this.extfvk, - this.isTestNet, + this.#extfvk, + this.#isTestnet, ); } /** - * Return number of shield satoshis of the account + * @returns {number} number of shield satoshis of the account */ getBalance() { - return this.unspentNotes.reduce((acc, [note]) => acc + note.value, 0); + return this.#unspentNotes.reduce((acc, [note]) => acc + note.value, 0); } /** - * Return number of pending satoshis of the account + * @returns {number} number of pending satoshis of the account */ getPendingBalance() { - return Array.from(this.pendingUnspentNotes.values()) + return Array.from(this.#pendingUnspentNotes.values()) .flat() .reduce((acc, v) => acc + v[0].value, 0); } @@ -317,32 +351,35 @@ export class PIVXShield { utxos, transparentChangeAddress, }) { - if (!this.extsk) { + if (!this.#extsk) { throw new Error("You cannot create a transaction in view only mode!"); } if (!useShieldInputs && !transparentChangeAddress) { throw new Error("Change must have the same type of input used!"); } - const { txid, txhex, nullifiers } = await this.callWorker( + const { txid, txhex, nullifiers } = await this.#callWorker( "create_transaction", { - notes: useShieldInputs ? this.unspentNotes : null, + notes: useShieldInputs ? this.#unspentNotes : null, utxos: useShieldInputs ? null : utxos, - extsk: this.extsk, + extsk: this.#extsk, to_address: address, change_address: useShieldInputs ? await this.getNewAddress() : transparentChangeAddress, amount, block_height: blockHeight, - is_testnet: this.isTestNet, + is_testnet: this.#isTestnet, }, ); if (useShieldInputs) { - this.pendingSpentNotes.set(txid, nullifiers); + this.#pendingSpentNotes.set(txid, nullifiers); } - this.pendingUnspentNotes.set(txid, await this.addTransaction(txhex, true)); + this.#pendingUnspentNotes.set( + txid, + await this.#addTransaction(txhex, true), + ); return { hex: txhex, spentUTXOs: useShieldInputs @@ -354,63 +391,87 @@ export class PIVXShield { txid, }; } + + /** + * @returns {Promise} a number from 0.0 to 1.0 rapresenting + * the progress of the transaction proof. If multicore is unavailable, + * it always returns 0.0 + */ async getTxStatus() { - return await this.callWorker("read_tx_progress"); + return await this.#callWorker("read_tx_progress"); } /** * Signals the class that a transaction was sent successfully * and the notes can be marked as spent - * @throws Error if txid is not found - * @param{String} txid - Transaction id + * @throws if txid is not found + * @param{string} txid - Transaction id */ async finalizeTransaction(txid) { - const nullifiers = this.pendingSpentNotes.get(txid); - await this.removeSpentNotes(nullifiers); - this.pendingSpentNotes.delete(txid); + const nullifiers = this.#pendingSpentNotes.get(txid); + await this.#removeSpentNotes(nullifiers); + this.#pendingSpentNotes.delete(txid); } /** * Discards the transaction, for example if * there were errors in sending them. * The notes won't be marked as spent. - * @param{String} txid - Transaction id + * @param {string} txid - Transaction id */ discardTransaction(txid) { - this.pendingSpentNotes.delete(txid); - this.pendingUnspentNotes.delete(txid); + this.#pendingSpentNotes.delete(txid); + this.#pendingUnspentNotes.delete(txid); } /** - * @returns {String} new shield address + * @returns {Promise} new shield address */ async getNewAddress() { - const { address, diversifier_index } = await this.callWorker( + const { address, diversifier_index } = await this.#callWorker( "generate_next_shielding_payment_address", - this.extfvk, - this.diversifierIndex, - this.isTestNet, + this.#extfvk, + this.#diversifierIndex, + this.#isTestnet, ); - this.diversifierIndex = diversifier_index; + this.#diversifierIndex = diversifier_index; return address; } + /** + * Load sapling prover. Must be done to create a transaction, + * But will be done lazily if note called explicitally. + * @returns {Promise} resolves when the sapling prover is loaded + */ async loadSaplingProver() { - return await this.callWorker("load_prover"); + return await this.#callWorker("load_prover"); } /** - * @returns {Number} The last block that has been decoded + * @returns {number} The last block that has been decoded */ getLastSyncedBlock() { - return this.lastProcessedBlock; + return this.#lastProcessedBlock; } } export class Note { + /** + * @type{number[]} + */ + recipient; + /** + * @type{number[]} + */ + value; + /** + * @type{number[]} + */ + rseed; + /** * Class corresponding to an unspent sapling shield note - * @param {Array} o.recipient - Recipient PaymentAddress encoded as a byte array - * @param {Number} o.value - How much PIVs are in the note - * @param {Array} o.rseed - Random seed encoded as a byte array + * @param {number[]} o.recipient - Recipient PaymentAddress encoded as a byte array + * @param {number[]} o.value - How much PIVs are in the note + * @param {number[]} o.rseed - Random seed encoded as a byte array */ constructor({ recipient, value, rseed }) { this.recipient = recipient; @@ -423,16 +484,19 @@ export class UTXO { /** * Add a transparent UTXO, along with its private key * @param {Object} o - Options - * @param {String} o.txid - Transaction ID of the UTXO - * @param {Number} o.vout - output index of the UTXO - * @param {Number?} o.amount - Value in satoshi of the UTXO - * @param {String?} o.privateKey - Private key associated to the UTXO + * @param {string} o.txid - Transaction ID of the UTXO + * @param {number} o.vout - output index of the UTXO + * @param {number?} o.amount - Value in satoshi of the UTXO + * @param {string?} o.privateKey - Private key associated to the UTXO * @param {Uint8Array?} o.script - Tx Script */ constructor({ txid, vout, amount, privateKey, script }) { this.txid = txid; this.vout = vout; this.amount = amount; + /** + * @type {string} + */ this.private_key = privateKey ? bs58.decode(privateKey).slice(1, 33) : null; this.script = script; } @@ -441,13 +505,13 @@ export class UTXO { class ShieldData { /** * Add a transparent UTXO, along with its private key - * @param {Object} o - Options - * @param {String} o.extfvk - Extended full viewing key - * @param {Number} o.lastProcessedBlock - Last processed block in blockchain - * @param {String} o.commitmentTree - Hex encoded commitment tree + * @param {object} o - Options + * @param {string} o.extfvk - Extended full viewing key + * @param {number} o.lastProcessedBlock - Last processed block in blockchain + * @param {string} o.commitmentTree - Hex encoded commitment tree * @param {Uint8Array} o.diversifierIndex - Diversifier index of the last generated address - * @param {[Note, String][]} o.unspentNotes - Array of notes, corresponding witness - * @param {Boolean} o.isTestNet - If this is a testnet instance or not + * @param {[Note, string][]} o.unspentNotes - Array of notes, corresponding witness + * @param {boolean} o.isTestnet - If this is a testnet instance or not */ constructor({ extfvk, @@ -455,13 +519,13 @@ class ShieldData { commitmentTree, diversifierIndex, unspentNotes, - isTestNet, + isTestnet, }) { this.extfvk = extfvk; this.diversifierIndex = diversifierIndex; this.lastProcessedBlock = lastProcessedBlock; this.commitmentTree = commitmentTree; this.unspentNotes = unspentNotes; - this.isTestNet = isTestNet; + this.isTestnet = isTestnet; } } From bc632b1e1ce061ce24e8d99b555e61c33c18bfd8 Mon Sep 17 00:00:00 2001 From: Duddino Date: Fri, 3 Nov 2023 15:12:48 +0100 Subject: [PATCH 2/4] Object->object --- js/pivx_shield.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/pivx_shield.js b/js/pivx_shield.js index 0e0188d..741801b 100644 --- a/js/pivx_shield.js +++ b/js/pivx_shield.js @@ -483,7 +483,7 @@ export class Note { export class UTXO { /** * Add a transparent UTXO, along with its private key - * @param {Object} o - Options + * @param {object} o - Options * @param {string} o.txid - Transaction ID of the UTXO * @param {number} o.vout - output index of the UTXO * @param {number?} o.amount - Value in satoshi of the UTXO From 3f3be5469a91270db99f60c56a627962ab2704df Mon Sep 17 00:00:00 2001 From: Duddino Date: Fri, 3 Nov 2023 15:18:08 +0100 Subject: [PATCH 3/4] Remove rebase duplicate --- js/pivx_shield.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/js/pivx_shield.js b/js/pivx_shield.js index 741801b..686a0e5 100644 --- a/js/pivx_shield.js +++ b/js/pivx_shield.js @@ -226,11 +226,6 @@ export class PIVXShield { diversifierIndex: this.diversifierIndex, unspentNotes: this.unspentNotes, isTestnet: this.isTestnet, - defaultAddress: address, - lastProcessedBlock: this.#lastProcessedBlock, - commitmentTree: this.#commitmentTree, - diversifierIndex: this.#diversifierIndex, - unspentNotes: this.#unspentNotes, }), ); } From 46b91b780f48d5f8348adb90337014271ea7b64c Mon Sep 17 00:00:00 2001 From: Duddino Date: Tue, 7 Nov 2023 11:42:34 +0100 Subject: [PATCH 4/4] Remove whitespace --- js/pivx_shield.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/pivx_shield.js b/js/pivx_shield.js index 686a0e5..81c04e0 100644 --- a/js/pivx_shield.js +++ b/js/pivx_shield.js @@ -59,7 +59,6 @@ export class PIVXShield { * @type {Map} A map txid->nullifiers, storing pending transaction. * @private */ - #pendingSpentNotes = new Map(); /**