diff --git a/src/main.jsx b/src/main.jsx index eef18dd..9837e20 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -6,6 +6,10 @@ import App from './App' globalThis.Buffer = Buffer +BigInt.prototype.toJSON = function() { + return this.toString() +} + ReactDOM.render( diff --git a/src/pages/digitalId/ProfileId.jsx b/src/pages/digitalId/ProfileId.jsx index 4ace576..ff16b3f 100644 --- a/src/pages/digitalId/ProfileId.jsx +++ b/src/pages/digitalId/ProfileId.jsx @@ -129,6 +129,8 @@ const Profile = observer (() => { const deleteDataParameters = () => { const changeData = store.decryptedAccountData.filter(item=>!selectedItems.includes(item.key)) store.pushAccountData(changeData); + if(storeOnBlockchain) + store.pushAccountDataToBlockchain(changeData) }; const changeInitialArray = () => { @@ -153,6 +155,8 @@ const Profile = observer (() => { const changeDataParameters = () => { const updatedData = changeInitialArray(); store.pushAccountData(updatedData); + if(storeOnBlockchain) + store.pushAccountDataToBlockchain(updatedData) }; const addDataParameters = () => { @@ -162,6 +166,8 @@ const Profile = observer (() => { else addedValues.key = addedValues.label store.pushAccountData(store.decryptedAccountData.concat(addedValues)); + if(storeOnBlockchain) + store.pushAccountDataToBlockchain(store.decryptedAccountData.concat(addedValues)) }; const cancelAddPanel = () => { @@ -352,7 +358,7 @@ const Profile = observer (() => { required onChange={(e) => changeUpdatedValues(e.target.value, 'label', item.key)} value={item.label} - disabled='disabled' + disabled />
diff --git a/src/shared/fetchWrapper.js b/src/shared/fetchWrapper.js index aac7117..09ad9b5 100644 --- a/src/shared/fetchWrapper.js +++ b/src/shared/fetchWrapper.js @@ -1,4 +1,5 @@ import {store} from '../store/store'; +import {jsonReplacer} from '../utils/Utils'; function prepareUrl(url) { if (url.startsWith('http')) return url; @@ -14,7 +15,10 @@ function handleRequest(method, url, headers, attempts, token, body) { })() }) .then((res) => res.json()) - .catch(() => []) + .catch((err) => { + console.log(err) + return {} + }) }; /** @@ -47,7 +51,7 @@ function request(method, url, headers = {}, token, body) { 'Content-Type': 'application/json', ...headers }, - body: JSON.stringify(body), + body: JSON.stringify(body, jsonReplacer), signal: controller.signal, } }; diff --git a/src/store/store.js b/src/store/store.js index 510adfe..383ee56 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,10 +1,19 @@ import {reaction, makeAutoObservable, onBecomeObserved, onBecomeUnobserved} from "mobx"; import {cryptography} from "@liskhq/lisk-client"; -import { passphrase } from "@liskhq/lisk-client"; +import { passphrase, transactions } from "@liskhq/lisk-client"; import {getNodeInfo} from '../api/node'; import {fetchWrapper} from '../shared/fetchWrapper'; import {labelMap} from "../shared/labelMap"; import {generateSvgAvatar} from "../images/GenerateOnboardingSvg/GenerateSvg"; +import { + encryptAccountData, + generateSetTransaction, generateTransaction, + hashAccountData, +} from '../utils/Utils'; +import { + removeFeatureAssetSchema, + setFeatureAssetSchema, +} from '../utils/Schemas'; class Store { _accountData = [] @@ -19,17 +28,20 @@ class Store { _transactionsInfo = [] + _accountInfo = {}; + constructor() { makeAutoObservable(this, {}); - this.fetchNodeInfo() - reaction(() => this.keysArray, () => this.fetchSharedData()) + reaction(() => this.address, () => this.fetchAccountInfo()) onBecomeObserved(this, "decryptedAccountData", () => this.fetchNewAccountData()) onBecomeObserved(this, "sharedData", () => this.fetchKeysArray()) onBecomeUnobserved(this, "sharedData", () => this.unobservedSharedData()) onBecomeObserved(this, "transactionsInfo", () => this.fetchTransactionsInfo()) onBecomeUnobserved(this, "transactionsInfo", () => this.unobservedTransactionsInfo()) + + this.fetchNodeInfo() }; fetchNewAccountData() { @@ -44,24 +56,20 @@ class Store { .catch((err) => this.fetchNodeInfoFailed(err)) } - pushAccountData(data = this.accountData) { - fetchWrapper.postAuth('data/private', { - networkIdentifier: this.nodeInfo.networkIdentifier, - lastBlockID: this.nodeInfo.lastBlockID - }, [...this.encryptAccountData(data)]) - .then(() => this.fetchNewAccountData()) + fetchAccountInfo() { + fetchWrapper.get(`accounts/${this.address}`).then((res) => this.fetchAccountInfoSuccess(res)) } fetchKeysArray() { getNodeInfo() - .then((info)=>{ - this.fetchNodeInfoSuccess(info) - fetchWrapper.getAuth('data/shared/', { - networkIdentifier: this.nodeInfo.networkIdentifier, - lastBlockID: this.nodeInfo.lastBlockID - }).then((res) => this.keysArrayFetchChange(res)) - }) - .catch((err) => this.fetchNodeInfoFailed(err)) + .then((info)=>{ + this.fetchNodeInfoSuccess(info) + fetchWrapper.getAuth('data/shared/', { + networkIdentifier: this.nodeInfo.networkIdentifier, + lastBlockID: this.nodeInfo.lastBlockID + }).then((res) => this.keysArrayFetchChange(res)) + }) + .catch((err) => this.fetchNodeInfoFailed(err)) } fetchTransactionsInfo() { @@ -71,6 +79,49 @@ class Store { }).then((res) => this.saveInfoTransactions(res)) }; + fetchPassPhrase() { + this._passPhrase = sessionStorage.getItem('passPhrase') || '' + } + + fetchSharedData() { + this._keysArray.forEach((elem) => fetchWrapper.get(`data/shared/${elem}`).then((res) => this.changeSharedData(res))) + }; + + fetchNodeInfo() { + getNodeInfo() + .then((info)=>this.fetchNodeInfoSuccess(info)) + .catch((err) => this.fetchNodeInfoFailed(err)) + } + + pushAccountData(data = this.accountData) { + fetchWrapper.postAuth('data/private', { + networkIdentifier: this.nodeInfo.networkIdentifier, + lastBlockID: this.nodeInfo.lastBlockID + }, [...encryptAccountData(data, this.passPhrase, this.pubKey)]) + .then(() => this.fetchNewAccountData()) + } + + pushAccountDataToBlockchain(data = this.decryptedAccountData) { + const [removed, changed] = hashAccountData(data, this.decryptedAccountData, this.accountFeaturesMap) + + const builder = generateTransaction(BigInt(this.accountInfo?.sequence?.nonce || 0), this.addressAndPubKey.publicKey, this.nodeInfo.networkIdentifier, this.passPhrase); + + let signedTx = null; + + if(removed.length !== 0) + signedTx = builder.remove(removed); + + if(changed.length !== 0) + signedTx = builder.update(changed) + + if(signedTx) { + fetchWrapper.post('transactions', {}, signedTx); + this.accountInfo.sequence.nonce++; + } + } + + + generatePassPhrase() { this._passPhrase = passphrase.Mnemonic.generateMnemonic(); sessionStorage.setItem('passPhrase', this._passPhrase); @@ -81,14 +132,6 @@ class Store { sessionStorage.setItem('passPhrase', this._passPhrase); }; - fetchPassPhrase() { - this._passPhrase = sessionStorage.getItem('passPhrase') || '' - } - - fetchSharedData() { - this._keysArray.forEach((elem) => fetchWrapper.get(`data/shared/${elem}`).then((res) => this.changeSharedData(res))) - }; - unobservedTransactionsInfo() { this._transactionsInfo = [] }; @@ -111,12 +154,6 @@ class Store { this._accountData = []; }; - fetchNodeInfo() { - getNodeInfo() - .then((info)=>this.fetchNodeInfoSuccess(info)) - .catch((err) => this.fetchNodeInfoFailed(err)) - } - saveInfoTransactions(transaction) { this._transactionsInfo = transaction.data } @@ -133,12 +170,19 @@ class Store { fetchNodeInfoSuccess(info) { this._nodeInfo = info.data; + this.fetchPassPhrase() } fetchNodeInfoFailed(err) { console.log(err); } + fetchAccountInfoSuccess(res) { + this._accountInfo = res.data; + if(this.accountInfo?.sequence?.nonce) + this.accountInfo.sequence.nonce = parseInt(this.accountInfo.sequence.nonce) || 0 + } + get sharedData() { return this._sharedData.map(elem => ({ data: elem.data.map(item => ({ @@ -167,22 +211,14 @@ class Store { } get decryptedAccountData() { - return this._accountData.map((elem) => ({ - ...elem, - key: elem.label, - label: labelMap[elem.label] || elem.label, - value: cryptography.decryptMessageWithPassphrase(elem.value, elem.value_nonce, this.passPhrase, this.pubKey).split(':')[1] - })) - } - - encryptAccountData(data) { - return data.map((item) => { - let value = cryptography.encryptMessageWithPassphrase(cryptography.getRandomBytes(32).toString('hex') + ":" + item.value, this.passPhrase, this.pubKey); + return this._accountData.map((elem) => { + const [seed, value] = cryptography.decryptMessageWithPassphrase(elem.value, elem.value_nonce, this.passPhrase, this.pubKey).split(':'); return { - label: item.key, - value: value.encryptedMessage, - value_nonce: value.nonce, - seed: item.seed, + ...elem, + key: elem.label, + label: labelMap[elem.label] || elem.label, + value, + seed } }) } @@ -204,7 +240,6 @@ class Store { } get passPhrase() { - this.fetchPassPhrase() return this._passPhrase } @@ -212,6 +247,10 @@ class Store { return this._accountData }; + get accountInfo() { + return this._accountInfo; + } + get nodeInfo() { return this._nodeInfo; } @@ -235,6 +274,21 @@ class Store { const sign = cryptography.signDataWithPassphrase(Buffer.from(stringToSign, 'hex'), this.passPhrase).toString('hex') return this.pubKey + ':' +sign } + + get accountIdentity() { + return this.accountInfo?.identity || {}; + } + + get accountFeatures() { + return this.accountIdentity?.features || []; + } + + get accountFeaturesMap() { + return this.accountFeatures.reduce((acc, item) => ({ + ...acc, + [item.label]: item.value + }), {}) + } } export const store = new Store(); diff --git a/src/utils/Schemas.js b/src/utils/Schemas.js new file mode 100644 index 0000000..2645def --- /dev/null +++ b/src/utils/Schemas.js @@ -0,0 +1,127 @@ +export const IdentityModuleSchema = { + $id: 'idntty/identity/module', + title: 'Identity module account schema', + type: 'object', + properties: { + features: { + fieldNumber: 1, + type: 'array', + maxItems: 256, + items: { + type: 'object', + required: ['label', 'value'], + properties: { + label: { fieldNumber: 1, dataType: 'string'}, + value: { fieldNumber: 2, dataType: 'bytes'}, + } + } + }, + verifications: { + fieldNumber: 2, + type: 'array', + items: { + type: 'object', + required: ['label', 'account', 'tx'], + properties: { + label: { fieldNumber: 1, dataType: 'string'}, + account: { fieldNumber: 2, dataType: 'bytes'}, + tx: { fieldNumber: 3, dataType: 'bytes'}, + } + } + }, + }, + default: {features: [], verifications: []} +}; + + +export const setFeatureAssetSchema = { + $id: 'idntty/identity/setfeature', + title: 'Asset schema to set or update account features for identity module', + type: 'object', + required: ['features'], + properties: { + features: { + fieldNumber: 1, + type: 'array', + minItems: 1, + maxItems: 16, + items: { + type: 'object', + required: ['label','value'], + properties: { + label: { fieldNumber: 1, dataType: 'string', maxLength: 16}, + value: { fieldNumber: 2, dataType: 'bytes', maxLength: 32}, + } + } + } + } +}; + +export const removeFeatureAssetSchema = { + $id: 'idntty/identity/removefeature', + title: 'Asset schema to remove account features for identity module', + type: 'object', + required: ['features'], + properties: { + features: { + fieldNumber: 1, + type: 'array', + minItems: 1, + maxItems: 16, + items: { + type: 'object', + required: ['label'], + properties: { + label: { fieldNumber: 1, dataType: 'string', maxLength: 16}, + } + } + } + } +}; + +export const validateFeatureAssetSchema = { + $id: 'idntty/identity/validatefeature', + title: 'Asset schema to validate account features for identity module', + type: 'object', + required: ['recipientAddress','features'], + properties: { + recipientAddress: {fieldNumber: 1, dataType: 'bytes', minLength: 20, maxLength: 20}, + features: { + fieldNumber: 2, + type: 'array', + minItems: 1, + maxItems: 16, + items: { + type: 'object', + required: ['label', 'value'], + properties: { + label: { fieldNumber: 1, dataType: 'string', maxLength: 16}, + value: { fieldNumber: 2, dataType: 'bytes', maxLength: 32}, + } + } + } + } +}; + +export const invalidateFeatureAssetSchema = { + $id: 'idntty/identity/invalidatefeature', + title: 'Asset schema to invalidate account features for identity module', + type: 'object', + required: ['recipientAddress','features'], + properties: { + recipientAddress: {fieldNumber: 1, dataType: 'bytes', minLength: 20, maxLength: 20}, + features: { + fieldNumber: 2, + type: 'array', + minItems: 1, + maxItems: 16, + items: { + type: 'object', + required: ['label'], + properties: { + label: { fieldNumber: 1, dataType: 'string', maxLength: 16}, + } + } + } + } +}; diff --git a/src/utils/Utils.js b/src/utils/Utils.js index b97ab72..8775a5d 100644 --- a/src/utils/Utils.js +++ b/src/utils/Utils.js @@ -1,4 +1,6 @@ import resolveConfig from 'tailwindcss/resolveConfig'; +import {cryptography, transactions} from '@liskhq/lisk-client'; +import {removeFeatureAssetSchema, setFeatureAssetSchema} from './Schemas'; export const tailwindConfig = () => { // Tailwind config @@ -32,3 +34,126 @@ export const formatThousands = (value) => Intl.NumberFormat('en-US', { maximumSignificantDigits: 3, notation: 'compact', }).format(value); + +export const encryptAccountData = (data = [], passPhrase = '', pubKey = '') => { + return data.map((item) => { + let value = cryptography.encryptMessageWithPassphrase(item.seed + ":" + item.value, passPhrase, pubKey); + return { + label: item.key, + value: value.encryptedMessage, + value_nonce: value.nonce, + seed: item.seed, + } + }) +} + +export const hashAccountData = (data = [], oldData = [], hashMap = {}) => { + const accountMap = data.reduce((acc, item) => ({ + ...acc, + [item.label]: item.value + }), {}) + + const oldAccountMap = oldData.reduce((acc, item) => ({ + ...acc, + [item.label]: item.value + }), {}) + + const removed = oldData.reduce((acc, item) => { + if(!accountMap[item.label]) + return [...acc, {label: item.label}]; + return acc + }, []) + + const changed = data.reduce((acc, item) => { + if(!oldAccountMap[item.label]) + return [ ...acc, item ] + const oldValue = hashMap[item.key]; + const newValue = hashValue(item.value, item.seed).toString('hex') + if(oldValue !== newValue) + return [ ...acc, item ] + return acc; + }, []) + + return [ + removed, + changed.map((item) => { + const value = hashValue(item.value, item.seed); + return { + label: item.key, + value + } + }) + ] +} + +export const jsonReplacer = (key, value) => { + if (key === 'big') { + return value.toString(); + } + return value; +} + +export const hashValue = (value = '', seed = '') => cryptography.hash(Buffer.concat([Buffer.from(seed, 'utf8'), cryptography.hash(value, 'utf8')])); + +export const generateTransaction = (nonce = BigInt(0), senderPublicKey = '', networkIdentifier = '', passPhrase = '', fee = BigInt(500000),) => { + return { + update: (features) => generateSetTransaction(features, nonce, senderPublicKey, networkIdentifier, passPhrase, fee), + remove: (features) => generateRemoveTransaction(features, nonce, senderPublicKey, networkIdentifier, passPhrase, fee), + } +} + +export const generateSetTransaction = (features, nonce = BigInt(0), senderPublicKey = '', networkIdentifier = '', passPhrase = '', fee = BigInt(500000)) => { + const tx = { + "moduleID": 1001, + "assetID": 1, + nonce, + senderPublicKey, + fee, + "asset": { + features + }, + } + + if(features.length === 0) + return; + + const signedTx = transactions.signTransaction(setFeatureAssetSchema, + tx, Buffer.from(networkIdentifier, "hex"), + passPhrase) + + signedTx.senderPublicKey = signedTx.senderPublicKey.toString("hex"); + signedTx.signatures[0] = signedTx.signatures[0].toString("hex"); + signedTx.asset.features = signedTx.asset.features.map(feature => ({ + ...feature, + value: feature.value.toString("hex") + })) + + delete signedTx.id; + + return signedTx; +} + +export const generateRemoveTransaction = (features, nonce = BigInt(0), senderPublicKey = '', networkIdentifier = '', passPhrase = '', fee = BigInt(500000)) => { + const tx = { + "moduleID": 1001, + "assetID": 2, + nonce, + senderPublicKey, + fee, + "asset": { + features + }, + } + + const signedTx = transactions.signTransaction(removeFeatureAssetSchema, + tx, Buffer.from(networkIdentifier, "hex"), + passPhrase) + + signedTx.senderPublicKey = signedTx.senderPublicKey.toString( + "hex"); + signedTx.signatures[0] = signedTx.signatures[0].toString("hex"); + + delete signedTx.id; + + return signedTx; +}