blockchain integration

This commit is contained in:
kandrusyak
2022-08-17 14:05:49 +03:00
parent 027bf0d405
commit f257066d5a
6 changed files with 370 additions and 50 deletions

View File

@@ -6,6 +6,10 @@ import App from './App'
globalThis.Buffer = Buffer globalThis.Buffer = Buffer
BigInt.prototype.toJSON = function() {
return this.toString()
}
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<Router> <Router>

View File

@@ -129,6 +129,8 @@ const Profile = observer (() => {
const deleteDataParameters = () => { const deleteDataParameters = () => {
const changeData = store.decryptedAccountData.filter(item=>!selectedItems.includes(item.key)) const changeData = store.decryptedAccountData.filter(item=>!selectedItems.includes(item.key))
store.pushAccountData(changeData); store.pushAccountData(changeData);
if(storeOnBlockchain)
store.pushAccountDataToBlockchain(changeData)
}; };
const changeInitialArray = () => { const changeInitialArray = () => {
@@ -153,6 +155,8 @@ const Profile = observer (() => {
const changeDataParameters = () => { const changeDataParameters = () => {
const updatedData = changeInitialArray(); const updatedData = changeInitialArray();
store.pushAccountData(updatedData); store.pushAccountData(updatedData);
if(storeOnBlockchain)
store.pushAccountDataToBlockchain(updatedData)
}; };
const addDataParameters = () => { const addDataParameters = () => {
@@ -162,6 +166,8 @@ const Profile = observer (() => {
else else
addedValues.key = addedValues.label addedValues.key = addedValues.label
store.pushAccountData(store.decryptedAccountData.concat(addedValues)); store.pushAccountData(store.decryptedAccountData.concat(addedValues));
if(storeOnBlockchain)
store.pushAccountDataToBlockchain(store.decryptedAccountData.concat(addedValues))
}; };
const cancelAddPanel = () => { const cancelAddPanel = () => {
@@ -352,7 +358,7 @@ const Profile = observer (() => {
required required
onChange={(e) => changeUpdatedValues(e.target.value, 'label', item.key)} onChange={(e) => changeUpdatedValues(e.target.value, 'label', item.key)}
value={item.label} value={item.label}
disabled='disabled' disabled
/> />
</div> </div>
<div> <div>

View File

@@ -1,4 +1,5 @@
import {store} from '../store/store'; import {store} from '../store/store';
import {jsonReplacer} from '../utils/Utils';
function prepareUrl(url) { function prepareUrl(url) {
if (url.startsWith('http')) return url; if (url.startsWith('http')) return url;
@@ -14,7 +15,10 @@ function handleRequest(method, url, headers, attempts, token, body) {
})() })()
}) })
.then((res) => res.json()) .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', 'Content-Type': 'application/json',
...headers ...headers
}, },
body: JSON.stringify(body), body: JSON.stringify(body, jsonReplacer),
signal: controller.signal, signal: controller.signal,
} }
}; };

View File

@@ -1,10 +1,19 @@
import {reaction, makeAutoObservable, onBecomeObserved, onBecomeUnobserved} from "mobx"; import {reaction, makeAutoObservable, onBecomeObserved, onBecomeUnobserved} from "mobx";
import {cryptography} from "@liskhq/lisk-client"; 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 {getNodeInfo} from '../api/node';
import {fetchWrapper} from '../shared/fetchWrapper'; import {fetchWrapper} from '../shared/fetchWrapper';
import {labelMap} from "../shared/labelMap"; import {labelMap} from "../shared/labelMap";
import {generateSvgAvatar} from "../images/GenerateOnboardingSvg/GenerateSvg"; import {generateSvgAvatar} from "../images/GenerateOnboardingSvg/GenerateSvg";
import {
encryptAccountData,
generateSetTransaction, generateTransaction,
hashAccountData,
} from '../utils/Utils';
import {
removeFeatureAssetSchema,
setFeatureAssetSchema,
} from '../utils/Schemas';
class Store { class Store {
_accountData = [] _accountData = []
@@ -19,17 +28,20 @@ class Store {
_transactionsInfo = [] _transactionsInfo = []
_accountInfo = {};
constructor() { constructor() {
makeAutoObservable(this, {}); makeAutoObservable(this, {});
this.fetchNodeInfo()
reaction(() => this.keysArray, () => this.fetchSharedData()) reaction(() => this.keysArray, () => this.fetchSharedData())
reaction(() => this.address, () => this.fetchAccountInfo())
onBecomeObserved(this, "decryptedAccountData", () => this.fetchNewAccountData()) onBecomeObserved(this, "decryptedAccountData", () => this.fetchNewAccountData())
onBecomeObserved(this, "sharedData", () => this.fetchKeysArray()) onBecomeObserved(this, "sharedData", () => this.fetchKeysArray())
onBecomeUnobserved(this, "sharedData", () => this.unobservedSharedData()) onBecomeUnobserved(this, "sharedData", () => this.unobservedSharedData())
onBecomeObserved(this, "transactionsInfo", () => this.fetchTransactionsInfo()) onBecomeObserved(this, "transactionsInfo", () => this.fetchTransactionsInfo())
onBecomeUnobserved(this, "transactionsInfo", () => this.unobservedTransactionsInfo()) onBecomeUnobserved(this, "transactionsInfo", () => this.unobservedTransactionsInfo())
this.fetchNodeInfo()
}; };
fetchNewAccountData() { fetchNewAccountData() {
@@ -44,12 +56,8 @@ class Store {
.catch((err) => this.fetchNodeInfoFailed(err)) .catch((err) => this.fetchNodeInfoFailed(err))
} }
pushAccountData(data = this.accountData) { fetchAccountInfo() {
fetchWrapper.postAuth('data/private', { fetchWrapper.get(`accounts/${this.address}`).then((res) => this.fetchAccountInfoSuccess(res))
networkIdentifier: this.nodeInfo.networkIdentifier,
lastBlockID: this.nodeInfo.lastBlockID
}, [...this.encryptAccountData(data)])
.then(() => this.fetchNewAccountData())
} }
fetchKeysArray() { fetchKeysArray() {
@@ -71,6 +79,49 @@ class Store {
}).then((res) => this.saveInfoTransactions(res)) }).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() { generatePassPhrase() {
this._passPhrase = passphrase.Mnemonic.generateMnemonic(); this._passPhrase = passphrase.Mnemonic.generateMnemonic();
sessionStorage.setItem('passPhrase', this._passPhrase); sessionStorage.setItem('passPhrase', this._passPhrase);
@@ -81,14 +132,6 @@ class Store {
sessionStorage.setItem('passPhrase', this._passPhrase); 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() { unobservedTransactionsInfo() {
this._transactionsInfo = [] this._transactionsInfo = []
}; };
@@ -111,12 +154,6 @@ class Store {
this._accountData = []; this._accountData = [];
}; };
fetchNodeInfo() {
getNodeInfo()
.then((info)=>this.fetchNodeInfoSuccess(info))
.catch((err) => this.fetchNodeInfoFailed(err))
}
saveInfoTransactions(transaction) { saveInfoTransactions(transaction) {
this._transactionsInfo = transaction.data this._transactionsInfo = transaction.data
} }
@@ -133,12 +170,19 @@ class Store {
fetchNodeInfoSuccess(info) { fetchNodeInfoSuccess(info) {
this._nodeInfo = info.data; this._nodeInfo = info.data;
this.fetchPassPhrase()
} }
fetchNodeInfoFailed(err) { fetchNodeInfoFailed(err) {
console.log(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() { get sharedData() {
return this._sharedData.map(elem => ({ return this._sharedData.map(elem => ({
data: elem.data.map(item => ({ data: elem.data.map(item => ({
@@ -167,22 +211,14 @@ class Store {
} }
get decryptedAccountData() { get decryptedAccountData() {
return this._accountData.map((elem) => ({ return this._accountData.map((elem) => {
const [seed, value] = cryptography.decryptMessageWithPassphrase(elem.value, elem.value_nonce, this.passPhrase, this.pubKey).split(':');
return {
...elem, ...elem,
key: elem.label, key: elem.label,
label: labelMap[elem.label] || elem.label, label: labelMap[elem.label] || elem.label,
value: cryptography.decryptMessageWithPassphrase(elem.value, elem.value_nonce, this.passPhrase, this.pubKey).split(':')[1] value,
})) seed
}
encryptAccountData(data) {
return data.map((item) => {
let value = cryptography.encryptMessageWithPassphrase(cryptography.getRandomBytes(32).toString('hex') + ":" + item.value, this.passPhrase, this.pubKey);
return {
label: item.key,
value: value.encryptedMessage,
value_nonce: value.nonce,
seed: item.seed,
} }
}) })
} }
@@ -204,7 +240,6 @@ class Store {
} }
get passPhrase() { get passPhrase() {
this.fetchPassPhrase()
return this._passPhrase return this._passPhrase
} }
@@ -212,6 +247,10 @@ class Store {
return this._accountData return this._accountData
}; };
get accountInfo() {
return this._accountInfo;
}
get nodeInfo() { get nodeInfo() {
return this._nodeInfo; return this._nodeInfo;
} }
@@ -235,6 +274,21 @@ class Store {
const sign = cryptography.signDataWithPassphrase(Buffer.from(stringToSign, 'hex'), this.passPhrase).toString('hex') const sign = cryptography.signDataWithPassphrase(Buffer.from(stringToSign, 'hex'), this.passPhrase).toString('hex')
return this.pubKey + ':' +sign 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(); export const store = new Store();

127
src/utils/Schemas.js Normal file
View File

@@ -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},
}
}
}
}
};

View File

@@ -1,4 +1,6 @@
import resolveConfig from 'tailwindcss/resolveConfig'; import resolveConfig from 'tailwindcss/resolveConfig';
import {cryptography, transactions} from '@liskhq/lisk-client';
import {removeFeatureAssetSchema, setFeatureAssetSchema} from './Schemas';
export const tailwindConfig = () => { export const tailwindConfig = () => {
// Tailwind config // Tailwind config
@@ -32,3 +34,126 @@ export const formatThousands = (value) => Intl.NumberFormat('en-US', {
maximumSignificantDigits: 3, maximumSignificantDigits: 3,
notation: 'compact', notation: 'compact',
}).format(value); }).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;
}