').join('\\"')
+ const finalString = preparedString.replace(/<\/?[^>]+(>|$)/g, '')
+
+ return finalString
+ }
}
async getChatContent(involved) {
@@ -2915,7 +3123,7 @@ class GroupManagement extends LitElement {
let getEditedArray = await parentEpml.request('apiCall', {
url: `/chat/messages?txGroupId=${involved}&haschatreference=true&encoding=BASE64&limit=0&reverse=false`
})
-
+
chaEditedArray = getEditedArray
// Replace messages which got edited in the chatMessageArray
@@ -2978,7 +3186,7 @@ class GroupManagement extends LitElement {
if (this.shadowRoot.getElementById('chat-container').innerHTML === '') {
this.shadowRoot.getElementById('chat-container').innerHTML = ''
}
-
+
if (this.isEmptyArray(renderArray)) {
const chatEmpty = document.createElement('div')
chatEmpty.classList.add('no-messages')
@@ -5613,4 +5821,4 @@ class GroupManagement extends LitElement {
}
}
-window.customElements.define('group-management', GroupManagement)
\ No newline at end of file
+window.customElements.define('group-management', GroupManagement)
diff --git a/plugins/plugins/core/node-management/node-management.src.js b/plugins/plugins/core/node-management/node-management.src.js
index b1c2eeeb..85eedf3b 100644
--- a/plugins/plugins/core/node-management/node-management.src.js
+++ b/plugins/plugins/core/node-management/node-management.src.js
@@ -480,7 +480,8 @@ class NodeManagement extends LitElement {
updateMintingAccounts() {
this.mintingAccounts = []
parentEpml.request('apiCall', {
- url: `/admin/mintingaccounts`
+ url: `/admin/mintingaccounts?apiKey=${this.getApiKey()}`,
+ method: 'GET'
}).then((res) => {
this.mintingAccounts = res
})
diff --git a/plugins/plugins/core/q-chat/q-chat.src.js b/plugins/plugins/core/q-chat/q-chat.src.js
index 71b2f597..cd9a11d1 100644
--- a/plugins/plugins/core/q-chat/q-chat.src.js
+++ b/plugins/plugins/core/q-chat/q-chat.src.js
@@ -5,12 +5,21 @@ import { passiveSupport } from 'passive-events-support/src/utils'
import { Editor, Extension } from '@tiptap/core'
import { supportCountryFlagEmojis } from '../components/ChatEmojiFlags'
import { qchatStyles } from '../components/plugins-css'
+import {
+ decryptGroupData,
+ uint8ArrayToBase64,
+ base64ToUint8Array,
+ uint8ArrayToObject,
+ validateSecretKey
+} from '../components/GroupEncryption'
+import Base58 from '../../../../crypto/api/deps/Base58'
import isElectron from 'is-electron'
import WebWorker from 'web-worker:./computePowWorker'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import Highlight from '@tiptap/extension-highlight'
+import Mention from '@tiptap/extension-mention'
import ShortUniqueId from 'short-unique-id'
import snackbar from '../components/snackbar'
import '../components/ChatWelcomePage'
@@ -429,8 +438,150 @@ class Chat extends LitElement {
}
async setActiveChatHeadUrl(url) {
- this.activeChatHeadUrl = url
- this.requestUpdate()
+ await this.getSymKeyFile(url)
+ }
+
+ async getSymKeyFile(url) {
+ this.secretKeys = {}
+ this.groupAdmins = {}
+
+ let data
+ let supArray = []
+ let allSymKeys = []
+ let gAdmin = ''
+ let gAddress = ''
+ let currentGroupId = url.substring(6)
+ let symIdentifier = 'symmetric-qchat-group-' + currentGroupId
+ let locateString = "Downloading and decrypt keys! Please wait..."
+ let keysToOld = "Wait until an admin re-encrypts the keys. Only unencrypted messages will be displayed."
+ let retryDownload = "Retry downloading and decrypt keys in 5 seconds! Please wait..."
+ let failDownload = "Error downloading and decrypt keys! Only unencrypted messages will be displayed. Please try again later..."
+ let all_ok = false
+ let counter = 0
+
+ const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
+ const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
+ const getNameUrl = `${nodeUrl}/arbitrary/resources?service=DOCUMENT_PRIVATE&identifier=${symIdentifier}&limit=0&reverse=true`
+ const getAdminUrl = `${nodeUrl}/groups/members/${currentGroupId}?onlyAdmins=true&limit=0`
+
+ if (localStorage.getItem("symKeysCurrent") === null) {
+ localStorage.setItem("symKeysCurrent", "")
+ }
+
+ supArray = await fetch(getNameUrl).then(response => {
+ return response.json()
+ })
+
+ if (this.isEmptyArray(supArray) || currentGroupId === 0) {
+ this.activeChatHeadUrl = url
+ this.requestUpdate()
+ } else {
+ parentEpml.request('showSnackBar', `${locateString}`)
+
+ supArray.forEach(item => {
+ const symInfoObj = {
+ name: item.name,
+ identifier: item.identifier,
+ timestamp: item.updated ? item.updated : item.created
+ }
+ allSymKeys.push(symInfoObj)
+ })
+
+ let allSymKeysSorted = allSymKeys.sort(function(a, b) {
+ return b.timestamp - a.timestamp
+ })
+
+ gAdmin = allSymKeysSorted[0].name
+
+ const addressUrl = `${nodeUrl}/names/${gAdmin}`
+
+ let addressObject = await fetch(addressUrl).then(response => {
+ return response.json()
+ })
+
+ gAddress = addressObject.owner
+
+ let adminRes = await fetch(getAdminUrl).then(response => {
+ return response.json()
+ })
+
+ this.groupAdmins = adminRes.members
+
+ const adminExists = (adminAddress) => {
+ return this.groupAdmins.some(function(checkAdmin) {
+ return checkAdmin.member === adminAddress
+ })
+ }
+
+ if (adminExists(gAddress)) {
+ const sleep = (t) => new Promise(r => setTimeout(r, t))
+ const dataUrl = `${nodeUrl}/arbitrary/DOCUMENT_PRIVATE/${gAdmin}/${symIdentifier}?encoding=base64`
+ const res = await fetch(dataUrl)
+
+ do {
+ counter++
+
+ if (!res.ok) {
+ parentEpml.request('showSnackBar', `${retryDownload}`)
+ await sleep(5000)
+ } else {
+ data = await res.text()
+ all_ok = true
+ }
+
+ if (counter > 10) {
+ parentEpml.request('showSnackBar', `${failDownload}`)
+ this.activeChatHeadUrl = url
+ this.requestUpdate()
+ return
+ }
+ } while (!all_ok)
+
+ const decryptedKey = await this.decryptGroupEncryption(data)
+
+ if (decryptedKey === undefined) {
+ parentEpml.request('showSnackBar', `${keysToOld}`)
+ this.activeChatHeadUrl = url
+ this.requestUpdate()
+ } else {
+ const dataint8Array = base64ToUint8Array(decryptedKey.data)
+ const decryptedKeyToObject = uint8ArrayToObject(dataint8Array)
+
+ if (!validateSecretKey(decryptedKeyToObject)) {
+ throw new Error("SecretKey is not valid")
+ }
+
+ localStorage.removeItem("symKeysCurrent")
+ localStorage.setItem("symKeysCurrent", "")
+ let oldSymIdentifier = JSON.parse(localStorage.getItem("symKeysCurrent") || "[]")
+ oldSymIdentifier.push(decryptedKeyToObject)
+ localStorage.setItem("symKeysCurrent", JSON.stringify(oldSymIdentifier))
+
+ let arraySecretKeys = JSON.parse(localStorage.getItem("symKeysCurrent") || "[]")
+
+ this.secretKeys = arraySecretKeys[0]
+ this.activeChatHeadUrl = url
+ this.requestUpdate()
+ }
+ } else {
+ this.activeChatHeadUrl = url
+ this.requestUpdate()
+ }
+ }
+
+ }
+
+ async decryptGroupEncryption(data) {
+ try {
+ const privateKey = Base58.encode(window.parent.reduxStore.getState().app.wallet._addresses[0].keyPair.privateKey)
+ const encryptedData = decryptGroupData(data, privateKey)
+ return {
+ data: uint8ArrayToBase64(encryptedData.decryptedData),
+ count: encryptedData.count
+ }
+ } catch (error) {
+ console.log("Error:", error.message)
+ }
}
resetChatEditor() {
@@ -461,6 +612,7 @@ class Chat extends LitElement {
StarterKit,
Underline,
Highlight,
+ Mention,
Placeholder.configure({
placeholder: 'Write something …'
}),
diff --git a/plugins/plugins/core/qdn/browser/browser.src.js b/plugins/plugins/core/qdn/browser/browser.src.js
index 4134a335..23fc00bf 100644
--- a/plugins/plugins/core/qdn/browser/browser.src.js
+++ b/plugins/plugins/core/qdn/browser/browser.src.js
@@ -12,6 +12,7 @@ import {
uint8ArrayStartsWith,
uint8ArrayToBase64
} from '../../components/qdn-action-encryption'
+import { processTransactionVersion2 } from '../../../../../crypto/api/createTransaction'
import { webBrowserStyles, webBrowserModalStyles } from '../../components/plugins-css'
import * as actions from '../../components/qdn-action-types'
import isElectron from 'is-electron'
@@ -19,6 +20,8 @@ import ShortUniqueId from 'short-unique-id'
import FileSaver from 'file-saver'
import WebWorker from 'web-worker:./computePowWorkerFile.js'
import WebWorkerChat from 'web-worker:./computePowWorker.js'
+import Base58 from '../../../../../crypto/api/deps/Base58'
+import nacl from '../../../../../crypto/api/deps/nacl-fast'
import '@material/mwc-button'
import '@material/mwc-icon'
import '@material/mwc-checkbox'
@@ -84,7 +87,7 @@ class WebBrowser extends LitElement {
this.loader = new Loader()
// Build initial display URL
- let displayUrl = ''
+ let displayUrl
if (this.dev === 'FRAMEWORK') {
displayUrl = 'qortal://app/development'
@@ -2271,6 +2274,140 @@ class WebBrowser extends LitElement {
}
}
+ case actions.SIGN_TRANSACTION: {
+ const signNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
+ const signUrl = signNode.protocol + '://' + signNode.domain + ':' + signNode.port
+ const requiredFields = ['unsignedBytes']
+ const missingFields = []
+
+ requiredFields.forEach((field) => {
+ if (!data[field]) {
+ missingFields.push(field)
+ }
+ })
+
+ if (missingFields.length > 0) {
+ const missingFieldsString = missingFields.join(', ')
+ const errorMsg = `Missing fields: ${missingFieldsString}`
+ let data = {}
+ data['error'] = errorMsg
+ response = JSON.stringify(data)
+ break
+ }
+
+ const shouldProcess = data.process || false
+
+ let url = `${signUrl}/transactions/decode?ignoreValidityChecks=false`
+ let body = data.unsignedBytes
+
+ const resp = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: body
+ })
+
+ if (!resp.ok) {
+ const errorMsg = "Failed to decode transaction"
+ let data = {}
+ data['error'] = errorMsg
+ response = JSON.stringify(data)
+ break
+ }
+
+ const decodedData = await resp.json()
+
+ const signRequest = await showModalAndWait(
+ actions.SIGN_TRANSACTION,
+ {
+ text1: `Do you give this application permission to ${ shouldProcess ? 'SIGN and PROCESS' : 'SIGN' } a transaction?`,
+ text2: "Read the transaction carefully before accepting!",
+ text3: `Tx type: ${decodedData.type}`,
+ json: decodedData
+ }
+ )
+
+ if (signRequest.action === 'accept') {
+ let urlConverted = `${signUrl}/transactions/convert`
+
+ const responseConverted = await fetch(urlConverted, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: data.unsignedBytes
+ })
+
+ const uint8PrivateKey = Base58.encode(window.parent.reduxStore.getState().app.wallet._addresses[0].keyPair.privateKey)
+ const uint8PublicKey = Base58.encode(window.parent.reduxStore.getState().app.wallet._addresses[0].keyPair.publicKey)
+
+ const keyPair = {
+ privateKey: uint8PrivateKey,
+ publicKey: uint8PublicKey,
+ }
+
+ const convertedBytes = await responseConverted.text()
+ const txBytes = Base58.decode(data.unsignedBytes)
+
+ const _arbitraryBytesBuffer = Object.keys(txBytes).map(function (key) {
+ return txBytes[key]
+ })
+
+ const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer)
+ const txByteSigned = Base58.decode(convertedBytes)
+
+ const _bytesForSigningBuffer = Object.keys(txByteSigned).map(function (key) {
+ return txByteSigned[key]
+ })
+
+ const bytesForSigningBuffer = new Uint8Array(_bytesForSigningBuffer)
+
+ const signature = nacl.sign.detached(
+ bytesForSigningBuffer,
+ keyPair.privateKey
+ )
+
+ const signedBytes = utils.appendBuffer(arbitraryBytesBuffer, signature)
+ const signedBytesToBase58 = Base58.encode(signedBytes)
+
+ if(!shouldProcess) {
+ const errorMsg = "Process transaction was not requested! Signed bytes are: " + signedBytesToBase58
+ let data = {}
+ data['error'] = errorMsg
+ response = JSON.stringify(data)
+ break
+ }
+
+ try {
+ this.loader.show()
+
+ const res = await processTransactionVersion2(signedBytesToBase58)
+
+ if (!res.signature) {
+ const errorMsg = "Transaction was not able to be processed" + res.message
+ let data = {}
+ data['error'] = errorMsg
+ response = JSON.stringify(data)
+ break
+ }
+
+ response = JSON.stringify(res)
+ } catch (error) {
+ console.error(error)
+ const data = {}
+ data['error'] = error.message || get("browserpage.bchange21")
+ response = JSON.stringify(data)
+ return
+ } finally {
+ this.loader.hide()
+ }
+ } else if (signRequest.action === 'reject') {
+ response = '{"error": "User declined request"}'
+ }
+ break
+ }
+
case actions.SEND_COIN: {
const requiredFields = ['coin', 'destinationAddress', 'amount']
const missingFields = []
@@ -2356,7 +2493,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -2516,7 +2652,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -2614,7 +2749,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -2712,7 +2846,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -2810,7 +2943,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -2908,7 +3040,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -3006,7 +3137,6 @@ class WebBrowser extends LitElement {
}
)
if (processPayment.action === 'reject') {
- let errorMsg = "User declined request"
let myMsg1 = get("transactions.declined")
let myMsg2 = get("walletpage.wchange44")
await showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2)
@@ -4032,6 +4162,13 @@ async function showModalAndWait(type, data) {
${get("browserpage.bchange22")}
${get("chatpage.cchange4")}: ${data.message}
` : ''}
+
+ ${type === actions.SIGN_TRANSACTION ? `
+ ${data.text1}
+ ${data.text2}
+ ${data.text3}
+ Transaction: ${data.json}
+ ` : ''}
diff --git a/plugins/plugins/utils/functions.js b/plugins/plugins/utils/functions.js
index df85fed0..f94cc32c 100644
--- a/plugins/plugins/utils/functions.js
+++ b/plugins/plugins/utils/functions.js
@@ -56,7 +56,7 @@ export const getUserNameFromAddress = async (address) => {
}
}
-export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isReceipient, decodeMessageFunc, _publicKey, addToUpdateMessageHashmap }) => {
+export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isReceipient, decodeMessageFunc, _publicKey, symKeys, addToUpdateMessageHashmap }) => {
const MAX_CONCURRENT_REQUESTS = 5 // Maximum number of concurrent requests
const executeWithConcurrencyLimit = async (array, asyncFn) => {
@@ -82,7 +82,6 @@ export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isRec
const findUpdatedMessage = async (msg) => {
let msgItem = { ...msg }
-
try {
let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`
if (!isReceipient) {
@@ -96,7 +95,7 @@ export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isRec
})
if (Array.isArray(newMsgResponse) && newMsgResponse.length > 0) {
- const decodeResponseItem = decodeMessageFunc(newMsgResponse[0], isReceipient, _publicKey)
+ const decodeResponseItem = decodeMessageFunc(newMsgResponse[0], isReceipient, _publicKey, symKeys)
delete decodeResponseItem.timestamp
@@ -143,8 +142,8 @@ export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isRec
})
if (originalReplyMessage && Array.isArray(replyResponse) && replyResponse.length !== 0) {
- const decodeOriginalReply = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey)
- const decodeUpdatedReply = decodeMessageFunc(replyResponse[0], isReceipient, _publicKey)
+ const decodeOriginalReply = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey, symKeys)
+ const decodeUpdatedReply = decodeMessageFunc(replyResponse[0], isReceipient, _publicKey, symKeys)
msgItem.repliedToData = {
...decodeUpdatedReply,
@@ -152,7 +151,7 @@ export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isRec
sender: decodeOriginalReply.sender
}
} else if (originalReplyMessage) {
- msgItem.repliedToData = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey)
+ msgItem.repliedToData = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey, symKeys)
}
}
@@ -165,9 +164,10 @@ export const replaceMessagesEdited = async ({ decodedMessages, parentEpml, isRec
}
const sortedMessages = decodedMessages.sort((a, b) => b.timestamp - a.timestamp)
+ const withoutHubReactions = sortedMessages.filter(({decodedMessage}) => !decodedMessage.includes('isReaction'))
// Execute the functions with concurrency limit
- const updatedMessages = await executeWithConcurrencyLimit(sortedMessages, findUpdatedMessage)
+ const updatedMessages = await executeWithConcurrencyLimit(withoutHubReactions, findUpdatedMessage)
addToUpdateMessageHashmap(updatedMessages)
return updatedMessages