diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index 30320079..e0892920 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -498,7 +498,9 @@ "bchange18": "Do you give this application permission to get your user address?", "bchange19": "Do you give this application permission to publish to QDN?", "bchange20": "Do you give this application permission to get your wallet balance?", - "bchange21": "Fetch Wallet Failed. Please try again!" + "bchange21": "Fetch Wallet Failed. Please try again!", + "bchange22": "Do you give this application permission to send a chat message?", + "bchange23": "Message Sent!" }, "datapage": { "dchange1": "Data Management", diff --git a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js index 78bb881b..52c17de5 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -16,6 +16,7 @@ registerTranslateConfig({ import '@material/mwc-button'; import '@material/mwc-icon'; import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; +import WebWorkerChat from 'web-worker:./computePowWorker.src.js'; import { publishData } from '../../../utils/publish-image.js'; import { Loader } from '../../../utils/loader.js'; const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }); @@ -108,7 +109,8 @@ class WebBrowser extends LitElement { constructor() { super(); this.url = 'about:blank'; - + this.myAddress = window.parent.reduxStore.getState().app.selectedAddress; + this._publicKey = { key: '', hasPubKey: false }; const urlParams = new URLSearchParams(window.location.search); this.name = urlParams.get('name'); this.service = urlParams.get('service'); @@ -450,9 +452,9 @@ class WebBrowser extends LitElement { let data = event.data; console.log('UI received event: ' + JSON.stringify(data)); - switch (data.action) { - case 'GET_USER_ACCOUNT': - case actions.GET_USER_ACCOUNT: + switch (data.action) { + case 'GET_USER_ACCOUNT': + case actions.GET_USER_ACCOUNT: const res1 = await showModalAndWait( actions.GET_USER_ACCOUNT ); @@ -562,12 +564,159 @@ class WebBrowser extends LitElement { } - case 'SEND_CHAT_MESSAGE': + case 'SEND_CHAT_MESSAGE': { + const message = data.message; + const recipient = data.destinationAddress; + const sendMessage = async (messageText, chatReference) => { + this.loader.show(); + let _reference = new Uint8Array(64); + window.crypto.getRandomValues(_reference); + let reference = window.parent.Base58.encode(_reference); + const sendMessageRequest = async () => { + let chatResponse = await parentEpml.request('chat', { + type: 18, + nonce: this.selectedAddress.nonce, + params: { + timestamp: Date.now(), + recipient: recipient, + recipientPublicKey: this._publicKey.key, + hasChatReference: 0, + chatReference: chatReference, + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1 + } + }); + const msgResponse = await _computePow(chatResponse) + return msgResponse; + }; + + const _computePow = async (chatBytes) => { + const difficulty = 8; + const path = window.parent.location.origin + '/memory-pow/memory-pow.wasm.full' + const worker = new WebWorkerChat(); + let nonce = null; + let chatBytesArray = null; + + await new Promise((res) => { + worker.postMessage({chatBytes, path, difficulty}); + worker.onmessage = e => { + chatBytesArray = e.data.chatBytesArray; + nonce = e.data.nonce; + res(); + } + }); + + let _response = await parentEpml.request('sign_chat', { + nonce: this.selectedAddress.nonce, + chatBytesArray: chatBytesArray, + chatNonce: nonce + }); + + const chatResponse = getSendChatResponse(_response); + return chatResponse; + }; + + const getSendChatResponse = (res) => { + if (res === true) { + let successString = get("browserpage.bchange23"); + parentEpml.request('showSnackBar', `${successString}`); + } else if (res.error) { + parentEpml.request('showSnackBar', res.message); + } + this.loader.hide(); + return res; + }; + + const chatResponse = await sendMessageRequest(); + return chatResponse; + } + + const result = await showModalAndWait( + actions.SEND_CHAT_MESSAGE + ); + if (result.action === "accept") { + let hasPublicKey = true; + const res = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${recipient}` + }); + + if (res.error === 102) { + this._publicKey.key = '' + this._publicKey.hasPubKey = false + hasPublicKey = false; + } else if (res !== false) { + this._publicKey.key = res + this._publicKey.hasPubKey = true + } else { + this._publicKey.key = '' + this._publicKey.hasPubKey = false + hasPublicKey = false; + } + + if (!hasPublicKey) { + let err4string = get("chatpage.cchange39"); + parentEpml.request('showSnackBar', `${err4string}`) + return + } + + this.loader.show(); + + const tiptapJson = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: message, + }, + + ], + }, + ], + } + + const messageObject = { + messageText: tiptapJson, + images: [''], + repliedTo: '', + version: 2 + }; + + const stringifyMessageObject = JSON.stringify(messageObject); + // if (this.balance < 4) { + // this.myTrimmedMeassage = '' + // this.myTrimmedMeassage = stringifyMessageObject + // this.shadowRoot.getElementById('confirmDialog').open() + // } else { + // this.sendMessage(stringifyMessageObject, typeMessage); + // } + try { + const msgResponse = await sendMessage(stringifyMessageObject); + response = msgResponse; + } catch (error) { + console.error(error); + return '{"error": "Request could not be fulfilled"}'; + } finally { + this.loader.hide(); + console.log("Case completed."); + } + + } else { + response = '{"error": "User declined request"}'; + } + // this.loader.show(); // Params: data.groupId, data.destinationAddress, data.message // TODO: prompt user to send chat message. If they confirm, sign+process a CHAT transaction // then set the response string from the core to the `response` variable (defined above) // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` break; + } case actions.JOIN_GROUP: { const requiredFields = ['groupId']; @@ -674,84 +823,85 @@ class WebBrowser extends LitElement { // TODO: prompt user to share wallet balance. If they confirm, call `GET /crosschain/:coin/walletbalance`, or for QORT, call `GET /addresses/balance/:address` // then set the response string from the core to the `response` variable (defined above) // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` - - const res3 = await showModalAndWait( + const res3 = await showModalAndWait( actions.GET_WALLET_BALANCE ); if (res3.action === 'accept') { - let coin = data.coin; - if (coin === "QORT") { - let qortAddress = window.parent.reduxStore.getState().app.selectedAddress.address - try { - this.loader.show(); - const QORTBalance = await parentEpml.request('apiCall', { - url: `/addresses/balance/${qortAddress}?apiKey=${this.getApiKey()}`, - }) - return QORTBalance; - } catch (error) { - console.error(error); - const data = {}; - const errorMsg = error.message || get("browserpage.bchange21"); - data['error'] = errorMsg; - response = JSON.stringify(data); - return; - } finally { - this.loader.hide(); - } - } else { - let _url = `` - let _body = null + let coin = data.coin; + if (coin === "QORT") { + let qortAddress = window.parent.reduxStore.getState().app.selectedAddress.address + try { + this.loader.show(); + const QORTBalance = await parentEpml.request('apiCall', { + url: `/addresses/balance/${qortAddress}?apiKey=${this.getApiKey()}`, + }) + return QORTBalance; + } catch (error) { + console.error(error); + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } finally { + this.loader.hide(); + } + } else { + let _url = `` + let _body = null - switch (coin) { - case 'LTC': - _url = `/crosschain/ltc/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet.derivedMasterPublicKey - break - case 'DOGE': - _url = `/crosschain/doge/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet.derivedMasterPublicKey - break - case 'DGB': - _url = `/crosschain/dgb/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet.derivedMasterPublicKey - break - case 'RVN': - _url = `/crosschain/rvn/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet.derivedMasterPublicKey - break - case 'ARRR': - _url = `/crosschain/arrr/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet.seed58 - break - default: - break - } - try { - this.loader.show() - await parentEpml.request('apiCall', { - url: _url, - method: 'POST', - body: _body, - }).then((res) => { - if (isNaN(Number(res))) { - throw new Error(get("browserpage.bchange21")); - } else { - console.log((Number(res) / 1e8).toFixed(8), "other wallet balance here"); - return (Number(res) / 1e8).toFixed(8) - } - }) - } catch (error) { - console.error(error); - const data = {}; - const errorMsg = error.message || get("browserpage.bchange21"); - data['error'] = errorMsg; - response = JSON.stringify(data); - return; - } finally { - this.loader.hide() - } - } - } else if (res3.action === 'reject') { + switch (coin) { + case 'LTC': + _url = `/crosschain/ltc/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet.derivedMasterPublicKey + break + case 'DOGE': + _url = `/crosschain/doge/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet.derivedMasterPublicKey + break + case 'DGB': + _url = `/crosschain/dgb/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet.derivedMasterPublicKey + break + case 'RVN': + _url = `/crosschain/rvn/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet.derivedMasterPublicKey + break + case 'ARRR': + _url = `/crosschain/arrr/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet.seed58 + break + default: + break + } + try { + this.loader.show(); + const res = await parentEpml.request('apiCall', { + url: _url, + method: 'POST', + body: _body, + }) + if (isNaN(Number(res))) { + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } else { + response = (Number(res) / 1e8).toFixed(8); + } + } catch (error) { + console.error(error); + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } finally { + this.loader.hide() + } + } + } else if (res3.action === 'reject') { response = '{"error": "User declined request"}'; } break; @@ -761,12 +911,170 @@ class WebBrowser extends LitElement { // TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction // then set the response string from the core to the `response` variable (defined above) // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` - break; + const amount = data.amount; + let recipient = data.destinationAddress; + const fee = data.fee + this.loader.show(); - default: - console.log('Unhandled message: ' + JSON.stringify(data)); - return; - } + const walletBalance = await parentEpml.request('apiCall', { + url: `/addresses/balance/${this.myAddress.address}?apiKey=${this.getApiKey()}`, + }).then((res) => { + if (isNaN(Number(res))) { + let snack4string = get("chatpage.cchange48") + parentEpml.request('showSnackBar', `${snack4string}`) + return; + } else { + return Number(res).toFixed(8); + } + }) + + const myRef = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/lastreference/${this.myAddress.address}`, + }) + + if (parseFloat(amount) + parseFloat(data.fee) > parseFloat(walletBalance)) { + this.loader.hide(); + let snack1string = get("chatpage.cchange51"); + parentEpml.request('showSnackBar', `${snack1string}`); + return false; + } + + if (parseFloat(amount) <= 0) { + this.loader.hide(); + let snack2string = get("chatpage.cchange52"); + parentEpml.request('showSnackBar', `${snack2string}`); + return false; + } + + if (recipient.length === 0) { + this.loader.hide(); + let snack3string = get("chatpage.cchange53"); + parentEpml.request('showSnackBar', `${snack3string}`); + return false; + } + + const validateName = async (receiverName) => { + let myRes; + let myNameRes = await parentEpml.request('apiCall', { + type: 'api', + url: `/names/${receiverName}`, + }) + + if (myNameRes.error === 401) { + myRes = false; + } else { + myRes = myNameRes; + } + return myRes; + } + + const validateAddress = async (receiverAddress) => { + let myAddress = await window.parent.validateAddress(receiverAddress); + return myAddress; + } + + const validateReceiver = async (recipient) => { + let lastRef = myRef; + let isAddress; + + try { + isAddress = await validateAddress(recipient); + } catch (err) { + isAddress = false; + } + + if (isAddress) { + let myTransaction = await makeTransactionRequest(recipient, lastRef); + return getTxnRequestResponse(myTransaction); + } else { + let myNameRes = await validateName(recipient); + if (myNameRes !== false) { + let myNameAddress = myNameRes.owner + let myTransaction = await makeTransactionRequest(myNameAddress, lastRef) + return getTxnRequestResponse(myTransaction) + } else { + console.error(`${translate("chatpage.cchange54")}`) + parentEpml.request('showSnackBar', `${translate("chatpage.cchange54")}`) + this.loader.hide(); + } + } + } + + const getName = async (recipient)=> { + try { + const getNames = await parentEpml.request("apiCall", { + type: "api", + url: `/names/address/${recipient}`, + }); + + if (getNames.length > 0 ) { + return getNames[0].name; + } else { + return ''; + } + } catch (error) { + return ""; + } + } + + const makeTransactionRequest = async (receiver, lastRef) => { + let myReceiver = receiver; + let mylastRef = lastRef; + let dialogamount = get("transactions.amount"); + let dialogAddress = get("login.address"); + let dialogName = get("login.name"); + let dialogto = get("transactions.to"); + let recipientName = await getName(myReceiver); + let myTxnrequest = await parentEpml.request('transaction', { + type: 2, + nonce: this.myAddress.nonce, + params: { + recipient: myReceiver, + recipientName: recipientName, + amount: amount, + lastReference: mylastRef, + fee: fee, + dialogamount: dialogamount, + dialogto: dialogto, + dialogAddress, + dialogName + }, + }) + return myTxnrequest; + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + parentEpml.request('showSnackBar', `${txnResponse.message}`); + this.loader.hide(); + throw new Error(txnResponse); + } else if (txnResponse.success === true && !txnResponse.data.error) { + parentEpml.request('showSnackBar', `${get("chatpage.cchange55")}`) + this.loader.hide(); + } else { + parentEpml.request('showSnackBar', `${txnResponse.data.message}`); + this.loader.hide(); + throw new Error(txnResponse); + } + } + try { + const result = await validateReceiver(recipient); + if (result) { + return result; + } + } catch (error) { + console.error(error); + return '{"error": "Request could not be fulfilled"}'; + } finally { + console.log("Case completed."); + } + break; + + default: + console.log('Unhandled message: ' + JSON.stringify(data)); + return; + } // Parse response let responseObj; @@ -776,7 +1084,6 @@ class WebBrowser extends LitElement { // Not all responses will be JSON responseObj = response; } - console.log({ responseObj }) // Respond to app if (responseObj.error != null) { console.log('hello error') @@ -1113,6 +1420,7 @@ async function showModalAndWait(type, data) { ${type === actions.GET_USER_ACCOUNT ? `` : ''} ${type === actions.PUBLISH_QDN_RESOURCE ? `` : ''} ${type === actions.GET_WALLET_BALANCE ? `` : ''} + ${type === actions.SEND_CHAT_MESSAGE ? `` : ''}