From 391f4eabb332f5c872173af4735e00ef9aa9981a Mon Sep 17 00:00:00 2001 From: CalDescent <> Date: Fri, 13 Jan 2023 17:38:23 +0000 Subject: [PATCH 01/25] Initial UI support and placeholders for Q-Apps --- .../plugins/core/qdn/browser/browser.src.js | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) 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 283e07f3..7c8d4570 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -234,6 +234,104 @@ class WebBrowser extends LitElement { parentEpml.request('closeCopyTextMenu', null) } } + + window.addEventListener("message", (event) => { + if (event == null || event.data == null || event.data.length == 0 || event.data.action == null) { + return; + } + + let response = "{\"error\": \"Request could not be fulfilled\"}"; + let data = event.data; + console.log("UI received event: " + JSON.stringify(data)); + + switch (data.action) { + case "GET_ACCOUNT_ADDRESS": + // For now, we will return this without prompting the user, but we may need to add a prompt later + response = this.selectedAddress.address; + break; + + case "GET_ACCOUNT_PUBLIC_KEY": + // For now, we will return this without prompting the user, but we may need to add a prompt later + response = this.selectedAddress.base58PublicKey; + break; + + case "PUBLISH_QDN_RESOURCE": + // Use "default" if user hasn't specified an identifer + if (data.identifier == null) { + data.identifier = "default"; + } + + // Params: data.service, data.name, data.identifier, data.data64, + // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process 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 "SEND_CHAT_MESSAGE": + // 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 "JOIN_GROUP": + // Params: data.groupId + // TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP 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 "DEPLOY_AT": + // Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee + // TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT 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 "GET_WALLET_BALANCE": + // Params: data.coin (QORT / LTC / DOGE / DGB / RVN / ARRR) + // 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"}` + break; + + case "SEND_COIN": + // Params: data.coin, data.destinationAddress, data.amount, data.fee + // 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; + + default: + console.log("Unhandled message: " + JSON.stringify(data)); + return; + } + + + // Parse response + let responseObj; + try { + responseObj = JSON.parse(response); + } catch (e) { + // Not all responses will be JSON + responseObj = response; + } + + // Respond to app + if (responseObj.error != null) { + event.ports[0].postMessage({ + result: null, + error: responseObj + }); + } + else { + event.ports[0].postMessage({ + result: responseObj, + error: null + }); + } + + }); } changeTheme() { From 3008fae6b3cd490933ef6bbc5afcc6a76e7c53ed Mon Sep 17 00:00:00 2001 From: CalDescent <> Date: Sun, 29 Jan 2023 13:22:12 +0000 Subject: [PATCH 02/25] GET_ACCOUNT_ADDRESS and GET_ACCOUNT_PUBLIC_KEY replaced with a single action: GET_USER_ACCOUNT, as it doesn't make sense to request address and public key separately (they are essentially the same thing). --- .../plugins/core/qdn/browser/browser.src.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 7c8d4570..09f68ac4 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -245,14 +245,12 @@ class WebBrowser extends LitElement { console.log("UI received event: " + JSON.stringify(data)); switch (data.action) { - case "GET_ACCOUNT_ADDRESS": + case "GET_USER_ACCOUNT": // For now, we will return this without prompting the user, but we may need to add a prompt later - response = this.selectedAddress.address; - break; - - case "GET_ACCOUNT_PUBLIC_KEY": - // For now, we will return this without prompting the user, but we may need to add a prompt later - response = this.selectedAddress.base58PublicKey; + let account = {}; + account["address"] = this.selectedAddress.address; + account["publicKey"] = this.selectedAddress.base58PublicKey; + response = JSON.stringify(account); break; case "PUBLISH_QDN_RESOURCE": From 2625c58ba818b27e226308a251c74e392f2948cd Mon Sep 17 00:00:00 2001 From: CalDescent <> Date: Fri, 17 Feb 2023 15:38:54 +0000 Subject: [PATCH 03/25] Upgraded browser functionality to support identifier, paths, and a dynamic address bar. Requires Q-Apps functionality in the core. --- .../plugins/core/qdn/browser/browser.src.js | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) 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 09f68ac4..6c5962f9 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -19,6 +19,8 @@ class WebBrowser extends LitElement { name: { type: String }, service: { type: String }, identifier: { type: String }, + path: { type: String }, + displayUrl: {type: String }, followedNames: { type: Array }, blockedNames: { type: Array }, theme: { type: String, reflect: true } @@ -103,12 +105,18 @@ class WebBrowser extends LitElement { const urlParams = new URLSearchParams(window.location.search); this.name = urlParams.get('name'); this.service = urlParams.get('service'); - // FUTURE: add support for identifiers - this.identifier = null; + this.identifier = urlParams.get('identifier') != null ? urlParams.get('identifier') : null; + this.path = urlParams.get('path') != null ? ((urlParams.get('path').startsWith("/") ? "" : "/") + urlParams.get('path')) : ""; this.followedNames = [] this.blockedNames = [] this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' + // Build initial display URL + let displayUrl = "qortal://" + this.service + "/" + this.name; + if (this.identifier != null && data.identifier != "" && this.identifier != "default") displayUrl = displayUrl.concat("/" + this.identifier); + if (this.path != null && this.path != "/") displayUrl = displayUrl.concat(this.path); + this.displayUrl = displayUrl; + const getFollowedNames = async () => { let followedNames = await parentEpml.request('apiCall', { @@ -132,7 +140,7 @@ class WebBrowser extends LitElement { const render = () => { const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - this.url = `${nodeUrl}/render/${this.service}/${this.name}?theme=${this.theme}`; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`; } const authorizeAndRender = () => { @@ -186,7 +194,7 @@ class WebBrowser extends LitElement { this.goForward()} title="${translate("browserpage.bchange1")}" class="address-bar-button">arrow_forward_ios this.refresh()} title="${translate("browserpage.bchange2")}" class="address-bar-button">refresh this.goBackToList()} title="${translate("browserpage.bchange3")}" class="address-bar-button">home - + this.delete()} title="${translate("browserpage.bchange4")} ${this.service} ${this.name} ${translate("browserpage.bchange5")}" class="address-bar-button float-right">delete ${this.renderBlockUnblockButton()} ${this.renderFollowUnfollowButton()} @@ -253,6 +261,20 @@ class WebBrowser extends LitElement { response = JSON.stringify(account); break; + case "LINK_TO_QDN_RESOURCE": + case "QDN_RESOURCE_DISPLAYED": + // Links are handled by the core, but the UI also listens for these actions in order to update the address bar. + // Note: don't update this.url here, as we don't want to force reload the iframe each time. + let url = "qortal://" + data.service + "/" + data.name; + this.path = data.path != null ? ((data.path.startsWith("/") ? "" : "/") + data.path) : null; + if (data.identifier != null && data.identifier != "" && data.identifier != "default") url = url.concat("/" + data.identifier); + if (this.path != null && this.path != "/") url = url.concat(this.path); + this.name = data.name; + this.service = data.service; + this.identifier = data.identifier; + this.displayUrl = url; + return; + case "PUBLISH_QDN_RESOURCE": // Use "default" if user hasn't specified an identifer if (data.identifier == null) { @@ -397,7 +419,9 @@ class WebBrowser extends LitElement { } refresh() { - window.location.reload(); + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`; } goBackToList() { From 07df3d357051d50a2aed2c3938548d972bc71a4a Mon Sep 17 00:00:00 2001 From: Phillip Date: Sat, 18 Feb 2023 16:12:57 +0200 Subject: [PATCH 04/25] add types --- .../core/components/qdn-action-types.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 qortal-ui-plugins/plugins/core/components/qdn-action-types.js diff --git a/qortal-ui-plugins/plugins/core/components/qdn-action-types.js b/qortal-ui-plugins/plugins/core/components/qdn-action-types.js new file mode 100644 index 00000000..dd9df85c --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/qdn-action-types.js @@ -0,0 +1,26 @@ +// GET_USER_ACCOUNT action +export const GET_USER_ACCOUNT = 'GET_USER_ACCOUNT'; + +// LINK_TO_QDN_RESOURCE action +export const LINK_TO_QDN_RESOURCE = 'LINK_TO_QDN_RESOURCE'; + +// QDN_RESOURCE_DISPLAYED action +export const QDN_RESOURCE_DISPLAYED = 'QDN_RESOURCE_DISPLAYED'; + +// PUBLISH_QDN_RESOURCE action +export const PUBLISH_QDN_RESOURCE = 'PUBLISH_QDN_RESOURCE'; + +// SEND_CHAT_MESSAGE action +export const SEND_CHAT_MESSAGE = 'SEND_CHAT_MESSAGE'; + +// JOIN_GROUP action +export const JOIN_GROUP = 'JOIN_GROUP'; + +// DEPLOY_AT action +export const DEPLOY_AT = 'DEPLOY_AT'; + +// GET_WALLET_BALANCE action +export const GET_WALLET_BALANCE = 'GET_WALLET_BALANCE'; + +// SEND_COIN action +export const SEND_COIN = 'SEND_COIN'; \ No newline at end of file From a0d2323305314b72b8dd9915bdebbecd082187d1 Mon Sep 17 00:00:00 2001 From: Phillip Date: Sat, 18 Feb 2023 16:13:13 +0200 Subject: [PATCH 05/25] create modal and logic for it- rough draft --- .../plugins/core/qdn/browser/browser.src.js | 126 +++++++++++++++++- 1 file changed, 121 insertions(+), 5 deletions(-) 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 6c5962f9..3346d1ff 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -2,7 +2,7 @@ import { LitElement, html, css } from 'lit' import { render } from 'lit/html.js' import { Epml } from '../../../../epml' import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' - +import * as actions from '../../components/qdn-action-types'; registerTranslateConfig({ loader: lang => fetch(`/language/${lang}.json`).then(res => res.json()) }) @@ -243,7 +243,7 @@ class WebBrowser extends LitElement { } } - window.addEventListener("message", (event) => { + window.addEventListener("message", async (event) => { if (event == null || event.data == null || event.data.length == 0 || event.data.action == null) { return; } @@ -262,7 +262,7 @@ class WebBrowser extends LitElement { break; case "LINK_TO_QDN_RESOURCE": - case "QDN_RESOURCE_DISPLAYED": + case actions.QDN_RESOURCE_DISPLAYED: // Links are handled by the core, but the UI also listens for these actions in order to update the address bar. // Note: don't update this.url here, as we don't want to force reload the iframe each time. let url = "qortal://" + data.service + "/" + data.name; @@ -275,12 +275,18 @@ class WebBrowser extends LitElement { this.displayUrl = url; return; - case "PUBLISH_QDN_RESOURCE": + case actions.PUBLISH_QDN_RESOURCE: // Use "default" if user hasn't specified an identifer if (data.identifier == null) { data.identifier = "default"; } - + console.log('hello') + const result = await showModalAndWait(actions.PUBLISH_QDN_RESOURCE); + if (result.action === 'accept') { + console.log('User accepted:', result.userData); + } else if (result.action === 'reject') { + console.log('User rejected'); + } // Params: data.service, data.name, data.identifier, data.data64, // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process transaction // then set the response string from the core to the `response` variable (defined above) @@ -624,3 +630,113 @@ class WebBrowser extends LitElement { } window.customElements.define('web-browser', WebBrowser) + + +async function showModalAndWait(type, data) { + // Create a new Promise that resolves with user data and an action when the user clicks a button + return new Promise((resolve) => { + // Create the modal and add it to the DOM + const modal = document.createElement('div'); + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + + // Add click event listeners to the buttons + const okButton = modal.querySelector('#ok-button'); + okButton.addEventListener('click', () => { + const userData = { + + }; + document.body.removeChild(modal); + resolve({ action: 'accept', userData }); + }); + const cancelButton = modal.querySelector('#cancel-button'); + cancelButton.addEventListener('click', () => { + document.body.removeChild(modal); + resolve({ action: 'reject' }); + }); + }); + } + + // Add the styles for the modal +const styles = ` +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + } + + .modal-content { + background-color: #fff; + border-radius: 10px; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + max-width: 80%; + min-width: 300px; + min-height: 200px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + .modal-body { + + } + + .modal-buttons { + display: flex; + justify-content: space-between; + margin-top: 20px; + } + + .modal-buttons button { + background-color: #4caf50; + border: none; + color: #fff; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; + } + + .modal-buttons button:hover { + background-color: #3e8e41; + } + + #cancel-button { + background-color: #f44336; + } + + #cancel-button:hover { + background-color: #d32f2f; + } +`; + +const styleSheet = new CSSStyleSheet(); +styleSheet.replaceSync(styles); + +document.adoptedStyleSheets = [styleSheet]; \ No newline at end of file From 43169de98bd22cbc7aecd8ce749d747bcc9707d8 Mon Sep 17 00:00:00 2001 From: Phillip Date: Sat, 18 Feb 2023 16:22:26 +0200 Subject: [PATCH 06/25] error msg --- qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js | 1 + 1 file changed, 1 insertion(+) 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 3346d1ff..c8aae815 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -286,6 +286,7 @@ class WebBrowser extends LitElement { console.log('User accepted:', result.userData); } else if (result.action === 'reject') { console.log('User rejected'); + response = "{\"error\": \"User declined request\"}" } // Params: data.service, data.name, data.identifier, data.data64, // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process transaction From 48af282ba05a8134cb5faf80424530b8b587ed8c Mon Sep 17 00:00:00 2001 From: Phillip Date: Sat, 18 Feb 2023 19:17:50 +0200 Subject: [PATCH 07/25] add publish-data function --- .../plugins/core/qdn/browser/browser.src.js | 34 ++++++- .../qdn/browser/computePowWorkerFile.src.js | 92 +++++++++++++++++++ .../plugins/utils/publish-image.js | 18 +++- 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 qortal-ui-plugins/plugins/core/qdn/browser/computePowWorkerFile.src.js 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 c8aae815..8bb465b0 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -9,6 +9,8 @@ registerTranslateConfig({ import '@material/mwc-button' import '@material/mwc-icon' +import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; +import {publishData} from '../../../utils/publish-image.js'; const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) @@ -277,12 +279,42 @@ class WebBrowser extends LitElement { case actions.PUBLISH_QDN_RESOURCE: // Use "default" if user hasn't specified an identifer + const service = data.service + const name = data.name + let identifier = data.identifier + const data64 = data.data64 + if(!service || !name || !data64){ + return + } if (data.identifier == null) { - data.identifier = "default"; + identifier = "default"; } + console.log('hello') const result = await showModalAndWait(actions.PUBLISH_QDN_RESOURCE); + console.log({result}) if (result.action === 'accept') { + const worker = new WebWorker(); + console.log({worker}) + try { + await publishData({ + registeredName: name, + file: data64, + service: service, + identifier: identifier, + parentEpml, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: worker, + isBase64: true, + }); + + worker.terminate(); + } catch (error) { + worker.terminate(); + return + } + console.log('User accepted:', result.userData); } else if (result.action === 'reject') { console.log('User rejected'); diff --git a/qortal-ui-plugins/plugins/core/qdn/browser/computePowWorkerFile.src.js b/qortal-ui-plugins/plugins/core/qdn/browser/computePowWorkerFile.src.js new file mode 100644 index 00000000..d9f5f662 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/qdn/browser/computePowWorkerFile.src.js @@ -0,0 +1,92 @@ +import { Sha256 } from 'asmcrypto.js' + + + +function sbrk(size, heap){ + let brk = 512 * 1024 // stack top + let old = brk + brk += size + + if (brk > heap.length) + throw new Error('heap exhausted') + + return old +} + + + + +self.addEventListener('message', async e => { + const response = await computePow(e.data.convertedBytes, e.data.path) + postMessage(response) + +}) + + +const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }) +const heap = new Uint8Array(memory.buffer) + + + +const computePow = async (convertedBytes, path) => { + + + let response = null + + await new Promise((resolve, reject)=> { + + const _convertedBytesArray = Object.keys(convertedBytes).map( + function (key) { + return convertedBytes[key] + } +) +const convertedBytesArray = new Uint8Array(_convertedBytesArray) +const convertedBytesHash = new Sha256() + .process(convertedBytesArray) + .finish().result +const hashPtr = sbrk(32, heap) +const hashAry = new Uint8Array( + memory.buffer, + hashPtr, + 32 +) + +hashAry.set(convertedBytesHash) +const difficulty = 14 +const workBufferLength = 8 * 1024 * 1024 +const workBufferPtr = sbrk( + workBufferLength, + heap +) + + const importObject = { + env: { + memory: memory + }, + }; + + function loadWebAssembly(filename, imports) { + return fetch(filename) + .then(response => response.arrayBuffer()) + .then(buffer => WebAssembly.compile(buffer)) + .then(module => { + return new WebAssembly.Instance(module, importObject); + }); +} + + +loadWebAssembly(path) + .then(wasmModule => { + response = { + nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty), + + } + resolve() + + }); + + + }) + + return response +} \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/utils/publish-image.js b/qortal-ui-plugins/plugins/utils/publish-image.js index 05594d1c..9f03644a 100644 --- a/qortal-ui-plugins/plugins/utils/publish-image.js +++ b/qortal-ui-plugins/plugins/utils/publish-image.js @@ -16,7 +16,9 @@ export const publishData = async ({ parentEpml, uploadType, selectedAddress, - worker + worker, + isBase64, + metaData }) => { const validateName = async (receiverName) => { let nameRes = await parentEpml.request("apiCall", { @@ -115,16 +117,26 @@ export const publishData = async ({ } // Base64 encode the file to work around compatibility issues between javascript and java byte arrays + if(isBase64){ + postBody = file + } + if(!isBase64){ let fileBuffer = new Uint8Array(await file.arrayBuffer()) postBody = Buffer.from(fileBuffer).toString("base64") + } + } - - let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}?apiKey=${getApiKey()}` if (identifier != null && identifier.trim().length > 0) { uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}?apiKey=${getApiKey()}` + + if(metaData){ + uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}?${metaData}&apiKey=${getApiKey()}` + + } } + let uploadDataRes = await parentEpml.request("apiCall", { type: "api", method: "POST", From 7c6f9e90bbf6c56ea684846ef22e8e4b0363b63e Mon Sep 17 00:00:00 2001 From: Phillip Date: Sat, 18 Feb 2023 19:35:01 +0200 Subject: [PATCH 08/25] add throw error to publish-data --- .../plugins/core/qdn/browser/browser.src.js | 6 ++++-- qortal-ui-plugins/plugins/utils/publish-image.js | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) 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 8bb465b0..64bdb65b 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -297,7 +297,7 @@ class WebBrowser extends LitElement { const worker = new WebWorker(); console.log({worker}) try { - await publishData({ + const resPublish = await publishData({ registeredName: name, file: data64, service: service, @@ -308,7 +308,9 @@ class WebBrowser extends LitElement { worker: worker, isBase64: true, }); - + let data = {}; + data["data"] = resPublish; + response = JSON.stringify(data); worker.terminate(); } catch (error) { worker.terminate(); diff --git a/qortal-ui-plugins/plugins/utils/publish-image.js b/qortal-ui-plugins/plugins/utils/publish-image.js index 9f03644a..69f4861e 100644 --- a/qortal-ui-plugins/plugins/utils/publish-image.js +++ b/qortal-ui-plugins/plugins/utils/publish-image.js @@ -44,7 +44,7 @@ export const publishData = async ({ transactionBytesBase58 ) if (convertedBytesBase58.error) { - return + throw new Error('Error when signing'); } const convertedBytes = @@ -74,7 +74,7 @@ export const publishData = async ({ }) let myResponse = { error: "" } if (response === false) { - return + throw new Error('Error when signing'); } else { myResponse = response } @@ -85,21 +85,22 @@ export const publishData = async ({ const validate = async () => { let validNameRes = await validateName(registeredName) if (validNameRes.error) { - return + throw new Error('Name not found'); } let transactionBytes = await uploadData(registeredName, path, file) if (transactionBytes.error) { - return + throw new Error('Error when uploading'); } else if ( transactionBytes.includes("Error 500 Internal Server Error") ) { - return + throw new Error('Error when uploading'); } let signAndProcessRes = await signAndProcess(transactionBytes) if (signAndProcessRes.error) { - return + throw new Error('Error when signing'); } + return signAndProcessRes } const uploadData = async (registeredName, path, file) => { From cd4dd739b7f9ac952a0403402f24d4363786019d Mon Sep 17 00:00:00 2001 From: Phillip Date: Sat, 18 Feb 2023 19:40:14 +0200 Subject: [PATCH 09/25] pass error --- qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js | 5 +++++ 1 file changed, 5 insertions(+) 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 64bdb65b..a1e9bbb6 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -314,6 +314,11 @@ class WebBrowser extends LitElement { worker.terminate(); } catch (error) { worker.terminate(); + const data = {} + const errorMsg = error.message || 'Upload failed' + data["error"] = errorMsg + response = JSON.stringify(data); + return } From 7acdb30eb6b8ba85addee0a80df84ae9c744ec71 Mon Sep 17 00:00:00 2001 From: Phillip Date: Sun, 19 Feb 2023 09:10:20 +0200 Subject: [PATCH 10/25] added loader class --- .../plugins/core/qdn/browser/browser.src.js | 22 ++++++-- qortal-ui-plugins/plugins/utils/loader.js | 52 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 qortal-ui-plugins/plugins/utils/loader.js 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 a1e9bbb6..7142e714 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -11,9 +11,11 @@ import '@material/mwc-button' import '@material/mwc-icon' import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; import {publishData} from '../../../utils/publish-image.js'; - +import { Loader } from '../../../utils/loader.js'; const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + + class WebBrowser extends LitElement { static get properties() { return { @@ -112,7 +114,7 @@ class WebBrowser extends LitElement { this.followedNames = [] this.blockedNames = [] this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' - + this.loader = new Loader(); // Build initial display URL let displayUrl = "qortal://" + this.service + "/" + this.name; if (this.identifier != null && data.identifier != "" && this.identifier != "default") displayUrl = displayUrl.concat("/" + this.identifier); @@ -290,13 +292,14 @@ class WebBrowser extends LitElement { identifier = "default"; } - console.log('hello') + console.log('hello2') const result = await showModalAndWait(actions.PUBLISH_QDN_RESOURCE); console.log({result}) if (result.action === 'accept') { const worker = new WebWorker(); console.log({worker}) try { + this.loader.show(); const resPublish = await publishData({ registeredName: name, file: data64, @@ -320,6 +323,8 @@ class WebBrowser extends LitElement { response = JSON.stringify(data); return + } finally { + this.loader.hide(); } console.log('User accepted:', result.userData); @@ -340,7 +345,14 @@ class WebBrowser extends LitElement { // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` break; - case "JOIN_GROUP": + case actions.JOIN_GROUP: + const groupId = data.groupId + + if(!groupId){ + return + } + + // Params: data.groupId // TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP transaction // then set the response string from the core to the `response` variable (defined above) @@ -725,7 +737,7 @@ const styles = ` width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); - z-index: 100; + z-index: 1000000; display: flex; justify-content: center; align-items: center; diff --git a/qortal-ui-plugins/plugins/utils/loader.js b/qortal-ui-plugins/plugins/utils/loader.js new file mode 100644 index 00000000..18123c3d --- /dev/null +++ b/qortal-ui-plugins/plugins/utils/loader.js @@ -0,0 +1,52 @@ +export class Loader { + constructor() { + this.loader = document.createElement("div"); + this.loader.className = "loader"; + this.loader.innerHTML = ` +
+ `; + this.styles = document.createElement("style"); + this.styles.innerHTML = ` + .loader { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000001 + } + + .loader-spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 32px; + height: 32px; + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `; + } + + show() { + document.head.appendChild(this.styles); + document.body.appendChild(this.loader); + } + + hide() { + if (this.loader.parentNode) { + this.loader.parentNode.removeChild(this.loader); + } + if (this.styles.parentNode) { + this.styles.parentNode.removeChild(this.styles); + } + } + } \ No newline at end of file From d22f71b6ed0837e0e4832d09c69efea7364a8e3b Mon Sep 17 00:00:00 2001 From: Justin Ferrari Date: Sun, 19 Feb 2023 22:07:48 -0500 Subject: [PATCH 11/25] Changed modal UI + added two cases --- qortal-ui-core/language/us.json | 7 +- .../plugins/core/qdn/browser/browser.src.js | 1648 ++++++++++------- 2 files changed, 956 insertions(+), 699 deletions(-) diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index 008bee19..30320079 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -493,7 +493,12 @@ "bchange13": "Error occurred when trying to block this registered name. Please try again!", "bchange14": "Error occurred when trying to unblock this registered name. Please try again!", "bchange15": "Can't delete data from followed names. Please unfollow first.", - "bchange16": "Error occurred when trying to delete this resource. Please try again!" + "bchange16": "Error occurred when trying to delete this resource. Please try again!", + "bchange17": "User declined to share account details", + "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!" }, "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 7142e714..2f58042b 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -1,42 +1,46 @@ -import { LitElement, html, css } from 'lit' -import { render } from 'lit/html.js' -import { Epml } from '../../../../epml' -import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { Epml } from '../../../../epml'; +import { + use, + get, + translate, + translateUnsafeHTML, + registerTranslateConfig, +} from 'lit-translate'; import * as actions from '../../components/qdn-action-types'; registerTranslateConfig({ - loader: lang => fetch(`/language/${lang}.json`).then(res => res.json()) -}) + loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()), +}); -import '@material/mwc-button' -import '@material/mwc-icon' +import '@material/mwc-button'; +import '@material/mwc-icon'; import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; -import {publishData} from '../../../utils/publish-image.js'; +import { publishData } from '../../../utils/publish-image.js'; import { Loader } from '../../../utils/loader.js'; -const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) - - +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }); class WebBrowser extends LitElement { - static get properties() { - return { - url: { type: String }, - name: { type: String }, - service: { type: String }, - identifier: { type: String }, - path: { type: String }, - displayUrl: {type: String }, - followedNames: { type: Array }, - blockedNames: { type: Array }, - theme: { type: String, reflect: true } - } - } - - static get observers() { - return ['_kmxKeyUp(amount)'] - } - - static get styles() { - return css` + static get properties() { + return { + url: { type: String }, + name: { type: String }, + service: { type: String }, + identifier: { type: String }, + path: { type: String }, + displayUrl: { type: String }, + followedNames: { type: Array }, + blockedNames: { type: Array }, + theme: { type: String, reflect: true }, + }; + } + + static get observers() { + return ['_kmxKeyUp(amount)']; + } + + static get styles() { + return css` * { --mdc-theme-primary: rgb(3, 169, 244); --mdc-theme-secondary: var(--mdc-theme-primary); @@ -82,7 +86,7 @@ class WebBrowser extends LitElement { background-color: var(--white); } - input[type=text] { + input[type='text'] { margin: 0; padding: 2px 0 0 20px; border: 0; @@ -98,697 +102,945 @@ class WebBrowser extends LitElement { .float-right { float: right; } - - ` - } - - constructor() { - super() - this.url = 'about:blank' - - const urlParams = new URLSearchParams(window.location.search); - this.name = urlParams.get('name'); - this.service = urlParams.get('service'); - this.identifier = urlParams.get('identifier') != null ? urlParams.get('identifier') : null; - this.path = urlParams.get('path') != null ? ((urlParams.get('path').startsWith("/") ? "" : "/") + urlParams.get('path')) : ""; - this.followedNames = [] - this.blockedNames = [] - this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' - this.loader = new Loader(); - // Build initial display URL - let displayUrl = "qortal://" + this.service + "/" + this.name; - if (this.identifier != null && data.identifier != "" && this.identifier != "default") displayUrl = displayUrl.concat("/" + this.identifier); - if (this.path != null && this.path != "/") displayUrl = displayUrl.concat(this.path); - this.displayUrl = displayUrl; - - const getFollowedNames = async () => { - - let followedNames = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}` - }) - - this.followedNames = followedNames - setTimeout(getFollowedNames, this.config.user.nodeSettings.pingInterval) - } - - const getBlockedNames = async () => { - - let blockedNames = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}` - }) - - this.blockedNames = blockedNames - setTimeout(getBlockedNames, this.config.user.nodeSettings.pingInterval) - } - - const render = () => { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] - const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`; - } - - const authorizeAndRender = () => { - parentEpml.request('apiCall', { - url: `/render/authorize/${this.name}?apiKey=${this.getApiKey()}`, - method: "POST" - }).then(res => { - if (res.error) { - // Authorization problem - API key incorrect? - } - else { - render() - } - }) - } - - let configLoaded = false - - parentEpml.ready().then(() => { - parentEpml.subscribe('selected_address', async selectedAddress => { - this.selectedAddress = {} - selectedAddress = JSON.parse(selectedAddress) - if (!selectedAddress || Object.entries(selectedAddress).length === 0) return - this.selectedAddress = selectedAddress - }) - parentEpml.subscribe('config', c => { - this.config = JSON.parse(c) - if (!configLoaded) { - authorizeAndRender() - setTimeout(getFollowedNames, 1) - setTimeout(getBlockedNames, 1) - configLoaded = true - } - }) - parentEpml.subscribe('copy_menu_switch', async value => { - - if (value === 'false' && window.getSelection().toString().length !== 0) { - - this.clearSelection() - } - }) - }) - } - - render() { - return html` -
-
-
- this.goBack()} title="${translate("general.back")}" class="address-bar-button">arrow_back_ios - this.goForward()} title="${translate("browserpage.bchange1")}" class="address-bar-button">arrow_forward_ios - this.refresh()} title="${translate("browserpage.bchange2")}" class="address-bar-button">refresh - this.goBackToList()} title="${translate("browserpage.bchange3")}" class="address-bar-button">home - - this.delete()} title="${translate("browserpage.bchange4")} ${this.service} ${this.name} ${translate("browserpage.bchange5")}" class="address-bar-button float-right">delete - ${this.renderBlockUnblockButton()} - ${this.renderFollowUnfollowButton()} -
-
- -
-
-
- ` - } - - firstUpdated() { - - this.changeTheme() - this.changeLanguage() - - window.addEventListener('contextmenu', (event) => { - event.preventDefault() - this._textMenu(event) - }) - - window.addEventListener('click', () => { - parentEpml.request('closeCopyTextMenu', null) - }) - - window.addEventListener('storage', () => { - const checkLanguage = localStorage.getItem('qortalLanguage') - const checkTheme = localStorage.getItem('qortalTheme') - - use(checkLanguage) - - if (checkTheme === 'dark') { - this.theme = 'dark' - } else { - this.theme = 'light' - } - document.querySelector('html').setAttribute('theme', this.theme) - }) + `; + } + + constructor() { + super(); + this.url = 'about:blank'; + + const urlParams = new URLSearchParams(window.location.search); + this.name = urlParams.get('name'); + this.service = urlParams.get('service'); + this.identifier = + urlParams.get('identifier') != null + ? urlParams.get('identifier') + : null; + this.path = + urlParams.get('path') != null + ? (urlParams.get('path').startsWith('/') ? '' : '/') + + urlParams.get('path') + : ''; + this.followedNames = []; + this.blockedNames = []; + this.theme = localStorage.getItem('qortalTheme') + ? localStorage.getItem('qortalTheme') + : 'light'; + this.loader = new Loader(); + // Build initial display URL + let displayUrl = 'qortal://' + this.service + '/' + this.name; + if ( + this.identifier != null && + data.identifier != '' && + this.identifier != 'default' + ) + displayUrl = displayUrl.concat('/' + this.identifier); + if (this.path != null && this.path != '/') + displayUrl = displayUrl.concat(this.path); + this.displayUrl = displayUrl; + + const getFollowedNames = async () => { + let followedNames = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + }); + + this.followedNames = followedNames; + setTimeout( + getFollowedNames, + this.config.user.nodeSettings.pingInterval + ); + }; + + const getBlockedNames = async () => { + let blockedNames = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + }); + + this.blockedNames = blockedNames; + setTimeout( + getBlockedNames, + this.config.user.nodeSettings.pingInterval + ); + }; + + const render = () => { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${ + this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${ + this.identifier != null ? this.identifier : '' + }`; + }; + + const authorizeAndRender = () => { + parentEpml + .request('apiCall', { + url: `/render/authorize/${ + this.name + }?apiKey=${this.getApiKey()}`, + method: 'POST', + }) + .then((res) => { + if (res.error) { + // Authorization problem - API key incorrect? + } else { + render(); + } + }); + }; + + let configLoaded = false; + + parentEpml.ready().then(() => { + parentEpml.subscribe( + 'selected_address', + async (selectedAddress) => { + this.selectedAddress = {}; + selectedAddress = JSON.parse(selectedAddress); + if ( + !selectedAddress || + Object.entries(selectedAddress).length === 0 + ) + return; + this.selectedAddress = selectedAddress; + } + ); + parentEpml.subscribe('config', (c) => { + this.config = JSON.parse(c); + if (!configLoaded) { + authorizeAndRender(); + setTimeout(getFollowedNames, 1); + setTimeout(getBlockedNames, 1); + configLoaded = true; + } + }); + parentEpml.subscribe('copy_menu_switch', async (value) => { + if ( + value === 'false' && + window.getSelection().toString().length !== 0 + ) { + this.clearSelection(); + } + }); + }); + } + + render() { + console.log(2, 'browser page here'); + return html` +
+
+
+ this.goBack()} title="${translate( + 'general.back' + )}" class="address-bar-button">arrow_back_ios + this.goForward()} title="${translate( + 'browserpage.bchange1' + )}" class="address-bar-button">arrow_forward_ios + this.refresh()} title="${translate( + 'browserpage.bchange2' + )}" class="address-bar-button">refresh + this.goBackToList()} title="${translate( + 'browserpage.bchange3' + )}" class="address-bar-button">home + + this.delete()} title="${translate( + 'browserpage.bchange4' + )} ${this.service} ${this.name} ${translate( + 'browserpage.bchange5' + )}" class="address-bar-button float-right">delete + ${this.renderBlockUnblockButton()} + ${this.renderFollowUnfollowButton()} +
+
+ +
+
+
+ `; + } + + firstUpdated() { + this.changeTheme(); + this.changeLanguage(); + + window.addEventListener('contextmenu', (event) => { + event.preventDefault(); + this._textMenu(event); + }); + + window.addEventListener('click', () => { + parentEpml.request('closeCopyTextMenu', null); + }); + + window.addEventListener('storage', () => { + const checkLanguage = localStorage.getItem('qortalLanguage'); + const checkTheme = localStorage.getItem('qortalTheme'); + + use(checkLanguage); + + if (checkTheme === 'dark') { + this.theme = 'dark'; + } else { + this.theme = 'light'; + } + document.querySelector('html').setAttribute('theme', this.theme); + }); - window.onkeyup = (e) => { - if (e.keyCode === 27) { - parentEpml.request('closeCopyTextMenu', null) - } - } + window.onkeyup = (e) => { + if (e.keyCode === 27) { + parentEpml.request('closeCopyTextMenu', null); + } + }; + + window.addEventListener('message', async (event) => { + if ( + event == null || + event.data == null || + event.data.length == 0 || + event.data.action == null + ) { + return; + } - window.addEventListener("message", async (event) => { - if (event == null || event.data == null || event.data.length == 0 || event.data.action == null) { - return; - } + let response = '{"error": "Request could not be fulfilled"}'; + let data = event.data; + console.log('UI received event: ' + JSON.stringify(data)); + + switch (data.action) { + case 'GET_USER_ACCOUNT': + case actions.GET_USER_ACCOUNT: + const res1 = await showModalAndWait( + actions.GET_USER_ACCOUNT + ); + if (res1.action === 'accept') { + let account = {}; + account['address'] = this.selectedAddress.address; + account['publicKey'] = + this.selectedAddress.base58PublicKey; + response = JSON.stringify(account); + break; + } else { + const data = {}; + const errorMsg = get('browserpage.bchange17'); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } + case 'LINK_TO_QDN_RESOURCE': + case actions.QDN_RESOURCE_DISPLAYED: + // Links are handled by the core, but the UI also listens for these actions in order to update the address bar. + // Note: don't update this.url here, as we don't want to force reload the iframe each time. + let url = 'qortal://' + data.service + '/' + data.name; + this.path = + data.path != null + ? (data.path.startsWith('/') ? '' : '/') + data.path + : null; + if ( + data.identifier != null && + data.identifier != '' && + data.identifier != 'default' + ) + url = url.concat('/' + data.identifier); + if (this.path != null && this.path != '/') + url = url.concat(this.path); + this.name = data.name; + this.service = data.service; + this.identifier = data.identifier; + this.displayUrl = url; + return; + + case actions.PUBLISH_QDN_RESOURCE: + // Use "default" if user hasn't specified an identifer + const service = data.service; + const name = data.name; + let identifier = data.identifier; + const data64 = data.data64; + + if (!service || !name || !data64) { + return; + } + if (data.identifier == null) { + identifier = 'default'; + } + const res2 = await showModalAndWait( + actions.PUBLISH_QDN_RESOURCE + ); + if (res2.action === 'accept') { + const worker = new WebWorker(); + try { + this.loader.show(); + const resPublish = await publishData({ + registeredName: name, + file: data64, + service: service, + identifier: identifier, + parentEpml, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: worker, + isBase64: true, + }); + let data = {}; + data['data'] = resPublish; + response = JSON.stringify(data); + worker.terminate(); + } catch (error) { + worker.terminate(); + const data = {}; + const errorMsg = error.message || 'Upload failed'; + data['error'] = errorMsg; + response = JSON.stringify(data); + console.error(error); + return; + } finally { + this.loader.hide(); + } + } else if (res2.action === 'reject') { + response = '{"error": "User declined request"}'; + } + // Params: data.service, data.name, data.identifier, data.data64, + // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process 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 'SEND_CHAT_MESSAGE': + // 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 groupId = data.groupId; + + if (!groupId) { + return; + } + + // Params: data.groupId + // TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP 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 'DEPLOY_AT': + // Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee + // TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT 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 'GET_WALLET_BALANCE': + // Params: data.coin (QORT / LTC / DOGE / DGB / C / ARRR) + // 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"}` + console.log('case passed here'); + console.log(data.coin, "data coin here"); + 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()}`, + }) + console.log({QORTBalance}) + 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') { + response = '{"error": "User declined request"}'; + } + break; + + case 'SEND_COIN': + // Params: data.coin, data.destinationAddress, data.amount, data.fee + // 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; + + default: + console.log('Unhandled message: ' + JSON.stringify(data)); + return; + } - let response = "{\"error\": \"Request could not be fulfilled\"}"; - let data = event.data; - console.log("UI received event: " + JSON.stringify(data)); - - switch (data.action) { - case "GET_USER_ACCOUNT": - // For now, we will return this without prompting the user, but we may need to add a prompt later - let account = {}; - account["address"] = this.selectedAddress.address; - account["publicKey"] = this.selectedAddress.base58PublicKey; - response = JSON.stringify(account); - break; - - case "LINK_TO_QDN_RESOURCE": - case actions.QDN_RESOURCE_DISPLAYED: - // Links are handled by the core, but the UI also listens for these actions in order to update the address bar. - // Note: don't update this.url here, as we don't want to force reload the iframe each time. - let url = "qortal://" + data.service + "/" + data.name; - this.path = data.path != null ? ((data.path.startsWith("/") ? "" : "/") + data.path) : null; - if (data.identifier != null && data.identifier != "" && data.identifier != "default") url = url.concat("/" + data.identifier); - if (this.path != null && this.path != "/") url = url.concat(this.path); - this.name = data.name; - this.service = data.service; - this.identifier = data.identifier; - this.displayUrl = url; - return; - - case actions.PUBLISH_QDN_RESOURCE: - // Use "default" if user hasn't specified an identifer - const service = data.service - const name = data.name - let identifier = data.identifier - const data64 = data.data64 - if(!service || !name || !data64){ - return - } - if (data.identifier == null) { - identifier = "default"; - } - - console.log('hello2') - const result = await showModalAndWait(actions.PUBLISH_QDN_RESOURCE); - console.log({result}) - if (result.action === 'accept') { - const worker = new WebWorker(); - console.log({worker}) - try { - this.loader.show(); - const resPublish = await publishData({ - registeredName: name, - file: data64, - service: service, - identifier: identifier, - parentEpml, - uploadType: 'file', - selectedAddress: this.selectedAddress, - worker: worker, - isBase64: true, - }); - let data = {}; - data["data"] = resPublish; - response = JSON.stringify(data); - worker.terminate(); - } catch (error) { - worker.terminate(); - const data = {} - const errorMsg = error.message || 'Upload failed' - data["error"] = errorMsg - response = JSON.stringify(data); - - return - } finally { - this.loader.hide(); - } - - console.log('User accepted:', result.userData); - } else if (result.action === 'reject') { - console.log('User rejected'); - response = "{\"error\": \"User declined request\"}" - } - // Params: data.service, data.name, data.identifier, data.data64, - // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process 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 "SEND_CHAT_MESSAGE": - // 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 groupId = data.groupId - - if(!groupId){ - return - } - - - // Params: data.groupId - // TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP 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 "DEPLOY_AT": - // Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee - // TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT 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 "GET_WALLET_BALANCE": - // Params: data.coin (QORT / LTC / DOGE / DGB / RVN / ARRR) - // 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"}` - break; - - case "SEND_COIN": - // Params: data.coin, data.destinationAddress, data.amount, data.fee - // 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; - - default: - console.log("Unhandled message: " + JSON.stringify(data)); - return; - } + // Parse response + let responseObj; + try { + responseObj = JSON.parse(response); + } catch (e) { + // Not all responses will be JSON + responseObj = response; + } + // Respond to app + if (responseObj.error != null) { + event.ports[0].postMessage({ + result: null, + error: responseObj, + }); + } else { + event.ports[0].postMessage({ + result: responseObj, + error: null, + }); + } + }); + } + + changeTheme() { + const checkTheme = localStorage.getItem('qortalTheme'); + if (checkTheme === 'dark') { + this.theme = 'dark'; + } else { + this.theme = 'light'; + } + document.querySelector('html').setAttribute('theme', this.theme); + } + + changeLanguage() { + const checkLanguage = localStorage.getItem('qortalLanguage'); + + if (checkLanguage === null || checkLanguage.length === 0) { + localStorage.setItem('qortalLanguage', 'us'); + use('us'); + } else { + use(checkLanguage); + } + } + + renderFollowUnfollowButton() { + // Only show the follow/unfollow button if we have permission to modify the list on this node + if (this.followedNames == null || !Array.isArray(this.followedNames)) { + return html``; + } + + if (this.followedNames.indexOf(this.name) === -1) { + // render follow button + return html` this.follow()} + title="${translate('browserpage.bchange7')} ${this.name}" + class="address-bar-button float-right" + >add_to_queue`; + } else { + // render unfollow button + return html` this.unfollow()} + title="${translate('browserpage.bchange8')} ${this.name}" + class="address-bar-button float-right" + >remove_from_queue`; + } + } + + renderBlockUnblockButton() { + // Only show the block/unblock button if we have permission to modify the list on this node + if (this.blockedNames == null || !Array.isArray(this.blockedNames)) { + return html``; + } + + if (this.blockedNames.indexOf(this.name) === -1) { + // render block button + return html` this.block()} + title="${translate('browserpage.bchange9')} ${this.name}" + class="address-bar-button float-right" + >block`; + } else { + // render unblock button + return html` this.unblock()} + title="${translate('browserpage.bchange10')} ${this.name}" + class="address-bar-button float-right" + >radio_button_unchecked`; + } + } + + // Navigation + + goBack() { + window.history.back(); + } + + goForward() { + window.history.forward(); + } + + refresh() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${ + this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${ + this.identifier != null ? this.identifier : '' + }`; + } + + goBackToList() { + window.location = '../index.html'; + } + + follow() { + this.followName(this.name); + } + + unfollow() { + this.unfollowName(this.name); + } + + block() { + this.blockName(this.name); + } + + unblock() { + this.unblockName(this.name); + } + + delete() { + this.deleteCurrentResource(); + } + + async followName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully followed - add to local list + // Remove it first by filtering the list - doing it this way ensures the UI updates + // immediately, as apposed to only adding if it doesn't already exist + this.followedNames = this.followedNames.filter( + (item) => item != name + ); + this.followedNames.push(name); + } else { + let err1string = get('browserpage.bchange11'); + parentEpml.request('showSnackBar', `${err1string}`); + } + + return ret; + } + + async unfollowName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully unfollowed - remove from local list + this.followedNames = this.followedNames.filter( + (item) => item != name + ); + } else { + let err2string = get('browserpage.bchange12'); + parentEpml.request('showSnackBar', `${err2string}`); + } + + return ret; + } + + async blockName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully blocked - add to local list + // Remove it first by filtering the list - doing it this way ensures the UI updates + // immediately, as apposed to only adding if it doesn't already exist + this.blockedNames = this.blockedNames.filter( + (item) => item != name + ); + this.blockedNames.push(name); + } else { + let err3string = get('browserpage.bchange13'); + parentEpml.request('showSnackBar', `${err3string}`); + } + + return ret; + } + + async unblockName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully unblocked - remove from local list + this.blockedNames = this.blockedNames.filter( + (item) => item != name + ); + } else { + let err4string = get('browserpage.bchange14'); + parentEpml.request('showSnackBar', `${err4string}`); + } + + return ret; + } + + async deleteCurrentResource() { + if (this.followedNames.indexOf(this.name) != -1) { + // Following name - so deleting won't work + let err5string = get('browserpage.bchange15'); + parentEpml.request('showSnackBar', `${err5string}`); + return; + } + + let identifier = + this.identifier == null ? 'default' : resource.identifier; + + let ret = await parentEpml.request('apiCall', { + url: `/arbitrary/resource/${this.service}/${ + this.name + }/${identifier}?apiKey=${this.getApiKey()}`, + method: 'DELETE', + }); + + if (ret === true) { + this.goBackToList(); + } else { + let err6string = get('browserpage.bchange16'); + parentEpml.request('showSnackBar', `${err6string}`); + } + + return ret; + } + + _textMenu(event) { + const getSelectedText = () => { + var text = ''; + if (typeof window.getSelection != 'undefined') { + text = window.getSelection().toString(); + } else if ( + typeof this.shadowRoot.selection != 'undefined' && + this.shadowRoot.selection.type == 'Text' + ) { + text = this.shadowRoot.selection.createRange().text; + } + return text; + }; + + const checkSelectedTextAndShowMenu = () => { + let selectedText = getSelectedText(); + if (selectedText && typeof selectedText === 'string') { + let _eve = { + pageX: event.pageX, + pageY: event.pageY, + clientX: event.clientX, + clientY: event.clientY, + }; + let textMenuObject = { + selectedText: selectedText, + eventObject: _eve, + isFrame: true, + }; + parentEpml.request('openCopyTextMenu', textMenuObject); + } + }; + checkSelectedTextAndShowMenu(); + } + + getApiKey() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + let apiKey = myNode.apiKey; + return apiKey; + } + + clearSelection() { + window.getSelection().removeAllRanges(); + window.parent.getSelection().removeAllRanges(); + } +} - // Parse response - let responseObj; - try { - responseObj = JSON.parse(response); - } catch (e) { - // Not all responses will be JSON - responseObj = response; - } +window.customElements.define('web-browser', WebBrowser); - // Respond to app - if (responseObj.error != null) { - event.ports[0].postMessage({ - result: null, - error: responseObj - }); +async function showModalAndWait(type, data) { + // Create a new Promise that resolves with user data and an action when the user clicks a button + return new Promise((resolve) => { + // Create the modal and add it to the DOM + const modal = document.createElement('div'); + modal.id = "backdrop" + modal.classList.add("backdrop"); + modal.innerHTML = + ` + `; + document.body.appendChild(modal); + + // Add click event listeners to the buttons + const okButton = modal.querySelector('#ok-button'); + okButton.addEventListener('click', () => { + const userData = {}; + if (modal.parentNode === document.body) { + document.body.removeChild(modal); } - else { - event.ports[0].postMessage({ - result: responseObj, - error: null - }); + resolve({ action: 'accept', userData }); + }); + const backdropClick = document.getElementById('backdrop'); + backdropClick.addEventListener('click', () => { + if (modal.parentNode === document.body) { + document.body.removeChild(modal); } + resolve({ action: 'reject' }); + }); + const cancelButton = modal.querySelector('#cancel-button'); + cancelButton.addEventListener('click', () => { + if (modal.parentNode === document.body) { + document.body.removeChild(modal); + } + resolve({ action: 'reject' }); + }); + }); +} - }); - } - - changeTheme() { - const checkTheme = localStorage.getItem('qortalTheme') - if (checkTheme === 'dark') { - this.theme = 'dark'; - } else { - this.theme = 'light'; - } - document.querySelector('html').setAttribute('theme', this.theme); - } - - changeLanguage() { - const checkLanguage = localStorage.getItem('qortalLanguage') - - if (checkLanguage === null || checkLanguage.length === 0) { - localStorage.setItem('qortalLanguage', 'us') - use('us') - } else { - use(checkLanguage) - } - } - - renderFollowUnfollowButton() { - // Only show the follow/unfollow button if we have permission to modify the list on this node - if (this.followedNames == null || !Array.isArray(this.followedNames)) { - return html`` - } - - if (this.followedNames.indexOf(this.name) === -1) { - // render follow button - return html` this.follow()} title="${translate("browserpage.bchange7")} ${this.name}" class="address-bar-button float-right">add_to_queue` - } - else { - // render unfollow button - return html` this.unfollow()} title="${translate("browserpage.bchange8")} ${this.name}" class="address-bar-button float-right">remove_from_queue` - } - } - - renderBlockUnblockButton() { - // Only show the block/unblock button if we have permission to modify the list on this node - if (this.blockedNames == null || !Array.isArray(this.blockedNames)) { - return html`` - } - - if (this.blockedNames.indexOf(this.name) === -1) { - // render block button - return html` this.block()} title="${translate("browserpage.bchange9")} ${this.name}" class="address-bar-button float-right">block` - } - else { - // render unblock button - return html` this.unblock()} title="${translate("browserpage.bchange10")} ${this.name}" class="address-bar-button float-right">radio_button_unchecked` - } - } - - - // Navigation - - goBack() { - window.history.back(); - } - - goForward() { - window.history.forward(); - } - - refresh() { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] - const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`; - } - - goBackToList() { - window.location = "../index.html"; - } - - follow() { - this.followName(this.name); - } - - unfollow() { - this.unfollowName(this.name); - } - - block() { - this.blockName(this.name); - } +// Add the styles for the modal +const styles = ` +.backdrop { +position: fixed; +top: 0; +left: 0; +width: 100%; +height: 100%; +background: rgb(186 186 186 / 26%); +overflow: hidden; +animation: backdrop_blur cubic-bezier(0.22, 1, 0.36, 1) 1s forwards; +z-index: 1000000; +} - unblock() { - this.unblockName(this.name); +@keyframes backdrop_blur { +0% { + backdrop-filter: blur(0px); + background: transparent; } - - delete() { - this.deleteCurrentResource(); +100% { + backdrop-filter: blur(5px); + background: rgb(186 186 186 / 26%); } +} +@keyframes modal_transition { +0% { + visibility: hidden; + opacity: 0; +} +100% { + visibility: visible; + opacity: 1; +} +} - async followName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully followed - add to local list - // Remove it first by filtering the list - doing it this way ensures the UI updates - // immediately, as apposed to only adding if it doesn't already exist - this.followedNames = this.followedNames.filter(item => item != name); - this.followedNames.push(name) - } - else { - let err1string = get("browserpage.bchange11") - parentEpml.request('showSnackBar', `${err1string}`) - } - - return ret - } - - async unfollowName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}`, - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully unfollowed - remove from local list - this.followedNames = this.followedNames.filter(item => item != name); - } - else { - let err2string = get("browserpage.bchange12") - parentEpml.request('showSnackBar', `${err2string}`) - } - - return ret - } +.modal { +position: relative; +display: flex; +justify-content: center; +align-items: center; +width: 100%; +height: 100%; +animation: 1s cubic-bezier(0.22, 1, 0.36, 1) 0s 1 normal forwards running modal_transition; +z-index: 1000001; +} - async blockName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully blocked - add to local list - // Remove it first by filtering the list - doing it this way ensures the UI updates - // immediately, as apposed to only adding if it doesn't already exist - this.blockedNames = this.blockedNames.filter(item => item != name); - this.blockedNames.push(name) - } - else { - let err3string = get("browserpage.bchange13") - parentEpml.request('showSnackBar', `${err3string}`) - } - - return ret - } +@keyframes modal_transition { +0% { + visibility: hidden; + opacity: 0; +} +100% { + visibility: visible; + opacity: 1; +} +} - async unblockName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully unblocked - remove from local list - this.blockedNames = this.blockedNames.filter(item => item != name); - } - else { - let err4string = get("browserpage.bchange14") - parentEpml.request('showSnackBar', `${err4string}`) - } - - return ret - } +.modal-content { +background-color: #fff; +border-radius: 10px; +padding: 20px; +box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +max-width: 80%; +min-width: 300px; +display: flex; +flex-direction: column; +justify-content: space-between; +} - async deleteCurrentResource() { - if (this.followedNames.indexOf(this.name) != -1) { - // Following name - so deleting won't work - let err5string = get("browserpage.bchange15") - parentEpml.request('showSnackBar', `${err5string}`) - return; - } - - let identifier = this.identifier == null ? "default" : resource.identifier; - - let ret = await parentEpml.request('apiCall', { - url: `/arbitrary/resource/${this.service}/${this.name}/${identifier}?apiKey=${this.getApiKey()}`, - method: 'DELETE' - }) - - if (ret === true) { - this.goBackToList(); - } - else { - let err6string = get("browserpage.bchange16") - parentEpml.request('showSnackBar', `${err6string}`) - } - - return ret - } +.modal-body { +padding: 25px; +} - _textMenu(event) { - const getSelectedText = () => { - var text = '' - if (typeof window.getSelection != 'undefined') { - text = window.getSelection().toString() - } else if (typeof this.shadowRoot.selection != 'undefined' && this.shadowRoot.selection.type == 'Text') { - text = this.shadowRoot.selection.createRange().text - } - return text - } - - const checkSelectedTextAndShowMenu = () => { - let selectedText = getSelectedText() - if (selectedText && typeof selectedText === 'string') { - let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY } - let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true } - parentEpml.request('openCopyTextMenu', textMenuObject) - } - } - checkSelectedTextAndShowMenu() - } +.modal-paragraph { +font-family: Roboto, sans-serif; +font-size: 18px; +letter-spacing: 0.3px; +font-weight: 300; +color: black; +margin: 0; +} - getApiKey() { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; - let apiKey = myNode.apiKey; - return apiKey; - } +.modal-buttons { +display: flex; +justify-content: space-between; +margin-top: 20px; +} - clearSelection() { - window.getSelection().removeAllRanges() - window.parent.getSelection().removeAllRanges() - } +.modal-buttons button { +background-color: #4caf50; +border: none; +color: #fff; +padding: 10px 20px; +border-radius: 5px; +cursor: pointer; +transition: background-color 0.2s; } -window.customElements.define('web-browser', WebBrowser) +.modal-buttons button:hover { +background-color: #3e8e41; +} +#cancel-button { +background-color: #f44336; +} -async function showModalAndWait(type, data) { - // Create a new Promise that resolves with user data and an action when the user clicks a button - return new Promise((resolve) => { - // Create the modal and add it to the DOM - const modal = document.createElement('div'); - modal.innerHTML = ` - - `; - document.body.appendChild(modal); - - // Add click event listeners to the buttons - const okButton = modal.querySelector('#ok-button'); - okButton.addEventListener('click', () => { - const userData = { - - }; - document.body.removeChild(modal); - resolve({ action: 'accept', userData }); - }); - const cancelButton = modal.querySelector('#cancel-button'); - cancelButton.addEventListener('click', () => { - document.body.removeChild(modal); - resolve({ action: 'reject' }); - }); - }); - } - - // Add the styles for the modal -const styles = ` -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.6); - z-index: 1000000; - display: flex; - justify-content: center; - align-items: center; - } - - .modal-content { - background-color: #fff; - border-radius: 10px; - padding: 20px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); - max-width: 80%; - min-width: 300px; - min-height: 200px; - display: flex; - flex-direction: column; - justify-content: space-between; - } - .modal-body { - - } - - .modal-buttons { - display: flex; - justify-content: space-between; - margin-top: 20px; - } - - .modal-buttons button { - background-color: #4caf50; - border: none; - color: #fff; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.2s; - } - - .modal-buttons button:hover { - background-color: #3e8e41; - } - - #cancel-button { - background-color: #f44336; - } - - #cancel-button:hover { - background-color: #d32f2f; - } +#cancel-button:hover { +background-color: #d32f2f; +} `; const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(styles); -document.adoptedStyleSheets = [styleSheet]; \ No newline at end of file +document.adoptedStyleSheets = [styleSheet]; From 48fff837325431ae3def483b767e125bc3ae671b Mon Sep 17 00:00:00 2001 From: Justin Ferrari Date: Mon, 20 Feb 2023 22:54:13 -0500 Subject: [PATCH 12/25] Added SEND_COIN case --- .../plugins/core/qdn/browser/browser.src.js | 156 +++++++++++++++++- 1 file changed, 152 insertions(+), 4 deletions(-) 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 2f58042b..5622772e 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -108,7 +108,7 @@ class WebBrowser extends LitElement { constructor() { super(); this.url = 'about:blank'; - + this.myAddress = window.parent.reduxStore.getState().app.selectedAddress; const urlParams = new URLSearchParams(window.location.search); this.name = urlParams.get('name'); this.service = urlParams.get('service'); @@ -231,7 +231,7 @@ class WebBrowser extends LitElement { } render() { - console.log(2, 'browser page here'); + console.log(3, 'browser page here'); return html`
@@ -450,8 +450,7 @@ 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"}` - console.log('case passed here'); - console.log(data.coin, "data coin here"); + console.log({data}); const res3 = await showModalAndWait( actions.GET_WALLET_BALANCE ); @@ -535,10 +534,159 @@ class WebBrowser extends LitElement { break; case 'SEND_COIN': + console.log({data}); // Params: data.coin, data.destinationAddress, data.amount, data.fee // 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"}` + const amount = data.amount; + let recipient = data.destinationAddress; + const fee = data.fee + this.loader.show(); + + 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); + getTxnRequestResponse(myTransaction); + } else { + let myNameRes = await validateName(recipient); + if (myNameRes !== false) { + let myNameAddress = myNameRes.owner + let myTransaction = await makeTransactionRequest(myNameAddress, lastRef) + 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); + } + } + validateReceiver(recipient); break; default: From 2d276d583f5da6ce3ad622f223df7a908948ef89 Mon Sep 17 00:00:00 2001 From: Justin Ferrari Date: Tue, 21 Feb 2023 13:32:24 -0500 Subject: [PATCH 13/25] Fixed SEND_COIN issue --- .../plugins/core/qdn/browser/browser.src.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 5622772e..2664556a 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -614,13 +614,13 @@ class WebBrowser extends LitElement { if (isAddress) { let myTransaction = await makeTransactionRequest(recipient, lastRef); - getTxnRequestResponse(myTransaction); + return getTxnRequestResponse(myTransaction); } else { let myNameRes = await validateName(recipient); if (myNameRes !== false) { let myNameAddress = myNameRes.owner let myTransaction = await makeTransactionRequest(myNameAddress, lastRef) - getTxnRequestResponse(myTransaction) + return getTxnRequestResponse(myTransaction) } else { console.error(`${translate("chatpage.cchange54")}`) parentEpml.request('showSnackBar', `${translate("chatpage.cchange54")}`) @@ -686,7 +686,17 @@ class WebBrowser extends LitElement { throw new Error(txnResponse); } } - validateReceiver(recipient); + 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: From 9a7cf9e4d4901c430dc7cee88f3b63ac589799dd Mon Sep 17 00:00:00 2001 From: Justin Ferrari Date: Thu, 23 Feb 2023 12:52:51 -0500 Subject: [PATCH 14/25] Fixed SEND_CHAT_MESSAGE bugs --- qortal-ui-core/language/us.json | 4 +- .../plugins/core/qdn/browser/browser.src.js | 195 +++++++++++++++--- .../core/qdn/browser/computePowWorker.src.js | 82 ++++++++ 3 files changed, 256 insertions(+), 25 deletions(-) create mode 100644 qortal-ui-plugins/plugins/core/qdn/browser/computePowWorker.src.js 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 2664556a..862a5206 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.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'); @@ -231,7 +233,6 @@ class WebBrowser extends LitElement { } render() { - console.log(3, 'browser page here'); return html`
@@ -321,8 +322,8 @@ class WebBrowser extends LitElement { console.log('UI received event: ' + JSON.stringify(data)); switch (data.action) { - case 'GET_USER_ACCOUNT': - case actions.GET_USER_ACCOUNT: + case 'GET_USER_ACCOUNT': + case actions.GET_USER_ACCOUNT: const res1 = await showModalAndWait( actions.GET_USER_ACCOUNT ); @@ -418,12 +419,159 @@ class WebBrowser extends LitElement { // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` break; - 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 groupId = data.groupId; @@ -450,8 +598,7 @@ 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"}` - console.log({data}); - const res3 = await showModalAndWait( + const res3 = await showModalAndWait( actions.GET_WALLET_BALANCE ); if (res3.action === 'accept') { @@ -463,7 +610,6 @@ class WebBrowser extends LitElement { const QORTBalance = await parentEpml.request('apiCall', { url: `/addresses/balance/${qortAddress}?apiKey=${this.getApiKey()}`, }) - console.log({QORTBalance}) return QORTBalance; } catch (error) { console.error(error); @@ -504,19 +650,21 @@ class WebBrowser extends LitElement { break } try { - this.loader.show() - await parentEpml.request('apiCall', { + this.loader.show(); + const res = 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) - } }) + 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 = {}; @@ -534,7 +682,6 @@ class WebBrowser extends LitElement { break; case 'SEND_COIN': - console.log({data}); // Params: data.coin, data.destinationAddress, data.amount, data.fee // 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) @@ -697,12 +844,12 @@ class WebBrowser extends LitElement { } finally { console.log("Case completed."); } - break; + break; - default: - console.log('Unhandled message: ' + JSON.stringify(data)); - return; - } + default: + console.log('Unhandled message: ' + JSON.stringify(data)); + return; + } // Parse response let responseObj; @@ -712,7 +859,6 @@ class WebBrowser extends LitElement { // Not all responses will be JSON responseObj = response; } - // Respond to app if (responseObj.error != null) { event.ports[0].postMessage({ @@ -1050,6 +1196,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 ? `` : ''}
+ ${this._groupdialog6} + ` + } + + set atDeployDialog1(atDeployDialog1) { + this._atDeployDialog1 = atDeployDialog1 + } + set atDeployDialog2(atDeployDialog2) { + this._atDeployDialog2 = atDeployDialog2 + } + + set fee(fee) { + this._fee = fee + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + set rAmount(rAmount) { + this._rAmount = rAmount + this._rAmountBytes = this.constructor.utils.int64ToBytes(this._rAmount) + } + + set rName(rName) { + this._rName = rName + this._rNameBytes = this.constructor.utils.stringtoUTF8Array(this._rName.toLocaleLowerCase()) + this._rNameLength = this.constructor.utils.int32ToBytes(this._rNameBytes.length) + } + + set rDescription(rDescription) { + this._rDescription = rDescription + this._rDescriptionBytes = this.constructor.utils.stringtoUTF8Array(this._rDescription.toLocaleLowerCase()) + this._rDescriptionLength = this.constructor.utils.int32ToBytes(this._rDescriptionBytes.length) + } + set atType(atType) { + this._atType = atType + this._atTypeBytes = this.constructor.utils.stringtoUTF8Array(this._atType) + this._atTypeLength = this.constructor.utils.int32ToBytes(this._atTypeBytes.length) + } + set rTags(rTags) { + this._rTags = rTags + this._rTagsBytes = this.constructor.utils.stringtoUTF8Array(this._rTags.toLocaleLowerCase()) + this._rTagsLength = this.constructor.utils.int32ToBytes(this._rTagsBytes.length) + } + set rCreationBytes(rCreationBytes) { + const decode = this.constructor.Base58.decode(rCreationBytes) + console.log({decode}) + this._rCreationBytes = this.constructor.utils.stringtoUTF8Array(decode) + this._rCreationBytesLength = this.constructor.utils.int32ToBytes(this._rCreationBytes.length) + } + set rAssetId(rAssetId) { + this._rAssetId = this.constructor.utils.int64ToBytes(rAssetId) + } + + + get params() { + const params = super.params + params.push( + this._rNameLength, + this._rNameBytes, + this._rDescriptionLength, + this._rDescriptionBytes, + this._atTypeLength, + this._atTypeBytes, + this._rTagsLength, + this._rTagsBytes, + this._rCreationBytesLength, + this._rCreationBytes, + this._rAmountBytes, + this._rAssetId, + this._feeBytes + ) + return params + } +} diff --git a/qortal-ui-crypto/api/transactions/transactions.js b/qortal-ui-crypto/api/transactions/transactions.js index 9fa87e99..0423d3ae 100644 --- a/qortal-ui-crypto/api/transactions/transactions.js +++ b/qortal-ui-crypto/api/transactions/transactions.js @@ -22,6 +22,7 @@ import LeaveGroupTransaction from './groups/LeaveGroupTransaction.js' import RewardShareTransaction from './reward-share/RewardShareTransaction.js' import RemoveRewardShareTransaction from './reward-share/RemoveRewardShareTransaction.js' import TransferPrivsTransaction from './TransferPrivsTransaction.js' +import DeployAtTransaction from './DeployAtTransaction.js' export const transactionTypes = { 2: PaymentTransaction, @@ -30,6 +31,7 @@ export const transactionTypes = { 5: SellNameTransacion, 6: CancelSellNameTransacion, 7: BuyNameTransacion, + 16: DeployAtTransaction, 17: MessageTransaction, 18: ChatTransaction, 181: GroupChatTransaction, 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 2f58042b..8266c5db 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -119,7 +119,7 @@ class WebBrowser extends LitElement { this.path = urlParams.get('path') != null ? (urlParams.get('path').startsWith('/') ? '' : '/') + - urlParams.get('path') + urlParams.get('path') : ''; this.followedNames = []; this.blockedNames = []; @@ -166,23 +166,20 @@ class WebBrowser extends LitElement { const render = () => { const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ - window.parent.reduxStore.getState().app.nodeConfig.node + window.parent.reduxStore.getState().app.nodeConfig.node ]; const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; - this.url = `${nodeUrl}/render/${this.service}/${this.name}${ - this.path != null ? this.path : '' - }?theme=${this.theme}&identifier=${ - this.identifier != null ? this.identifier : '' - }`; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : '' + }`; }; const authorizeAndRender = () => { parentEpml .request('apiCall', { - url: `/render/authorize/${ - this.name - }?apiKey=${this.getApiKey()}`, + url: `/render/authorize/${this.name + }?apiKey=${this.getApiKey()}`, method: 'POST', }) .then((res) => { @@ -231,7 +228,6 @@ class WebBrowser extends LitElement { } render() { - console.log(2, 'browser page here'); return html`
@@ -248,24 +244,22 @@ class WebBrowser extends LitElement { this.goBackToList()} title="${translate( 'browserpage.bchange3' )}" class="address-bar-button">home - + this.delete()} title="${translate( - 'browserpage.bchange4' - )} ${this.service} ${this.name} ${translate( - 'browserpage.bchange5' - )}" class="address-bar-button float-right">delete + 'browserpage.bchange4' + )} ${this.service} ${this.name} ${translate( + 'browserpage.bchange5' + )}" class="address-bar-button float-right">delete ${this.renderBlockUnblockButton()} ${this.renderFollowUnfollowButton()}
-
@@ -273,6 +267,142 @@ class WebBrowser extends LitElement { `; } + async unitJoinFee() { + 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 url = `${nodeUrl}/transactions/unitfee?txType=JOIN_GROUP` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching join fee'); + } + + const data = await response.json() + const joinFee = (Number(data) / 1e8).toFixed(8) + return joinFee + } + + async deployAtFee() { + 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 url = `${nodeUrl}/transactions/unitfee?txType=DEPLOY_AT` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching join fee'); + } + + const data = await response.json() + const joinFee = data + return joinFee + } + + async _joinGroup(groupId, groupName) { + const joinFeeInput = await this.unitJoinFee() + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } + + const makeTransactionRequest = async (lastRef) => { + let groupdialog1 = get("transactions.groupdialog1") + let groupdialog2 = get("transactions.groupdialog2") + let myTxnrequest = await parentEpml.request('transaction', { + type: 31, + nonce: this.selectedAddress.nonce, + params: { + fee: joinFeeInput, + registrantAddress: this.selectedAddress.address, + rGroupName: groupName, + rGroupId: groupId, + lastReference: lastRef, + groupdialog1: groupdialog1, + groupdialog2: groupdialog2 + } + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + return txnResponse.data + } else if (txnResponse.data && txnResponse.data.message) { + throw new Error(txnResponse.data.message) + } else { + throw new Error('Server error. Could not perform action.') + } + } + const groupRes = await validateReceiver() + return groupRes + + } + + async _deployAt(name, description, tags, creationBytes, amount, assetId, fee, atType) { + const deployAtFee = await this.deployAtFee() + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } + + const makeTransactionRequest = async (lastRef) => { + let groupdialog1 = get("transactions.groupdialog1") + let groupdialog2 = get("transactions.groupdialog2") + let myTxnrequest = await parentEpml.request('transaction', { + type: 16, + nonce: this.selectedAddress.nonce, + params: { + fee: fee || deployAtFee, + rName: name, + rDescription: description, + rTags: tags, + rAmount: amount, + rAssetId: assetId, + rCreationBytes: creationBytes, + atType: atType, + lastReference: lastRef, + atDeployDialog1: groupdialog1, + atDeployDialog2: groupdialog2 + } + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + return txnResponse.data + } else if (txnResponse.data && txnResponse.data.message) { + throw new Error(txnResponse.data.message) + } else { + throw new Error('Server error. Could not perform action.') + } + } + const groupRes = await validateReceiver() + return groupRes + + } + firstUpdated() { this.changeTheme(); this.changeLanguage(); @@ -320,9 +450,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 ); @@ -338,7 +468,7 @@ class WebBrowser extends LitElement { const errorMsg = get('browserpage.bchange17'); data['error'] = errorMsg; response = JSON.stringify(data); - return; + break; } case 'LINK_TO_QDN_RESOURCE': case actions.QDN_RESOURCE_DISPLAYED: @@ -363,16 +493,29 @@ class WebBrowser extends LitElement { this.displayUrl = url; return; - case actions.PUBLISH_QDN_RESOURCE: + case actions.PUBLISH_QDN_RESOURCE: { + const requiredFields = ['service', 'name', 'data64']; + 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 + } // Use "default" if user hasn't specified an identifer const service = data.service; const name = data.name; let identifier = data.identifier; const data64 = data.data64; - - if (!service || !name || !data64) { - return; - } if (data.identifier == null) { identifier = 'default'; } @@ -394,17 +537,16 @@ class WebBrowser extends LitElement { worker: worker, isBase64: true, }); - let data = {}; - data['data'] = resPublish; - response = JSON.stringify(data); + + response = JSON.stringify(resPublish); worker.terminate(); } catch (error) { worker.terminate(); - const data = {}; + const obj = {}; const errorMsg = error.message || 'Upload failed'; - data['error'] = errorMsg; - response = JSON.stringify(data); - console.error(error); + obj['error'] = errorMsg; + response = JSON.stringify(obj); + console.error(error); return; } finally { this.loader.hide(); @@ -417,6 +559,8 @@ class WebBrowser extends LitElement { // 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 'SEND_CHAT_MESSAGE': // Params: data.groupId, data.destinationAddress, data.message @@ -425,11 +569,60 @@ class WebBrowser extends LitElement { // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` break; - case actions.JOIN_GROUP: + case actions.JOIN_GROUP: { + const requiredFields = ['groupId']; + 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 groupId = data.groupId; - if (!groupId) { - return; + + let groupInfo = null + try { + groupInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/${groupId}`, + }); + } catch (error) { + const errorMsg = (error && error.message) || 'Group not found'; + let obj = {}; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + if (!groupInfo || groupInfo.error) { + const errorMsg = (groupInfo && groupInfo.message) || 'Group not found'; + let obj = {}; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + try { + this.loader.show(); + const resJoinGroup = await this._joinGroup(groupId, groupInfo.groupName) + response = JSON.stringify(resJoinGroup); + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to join the group.'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } finally { + this.loader.hide(); } // Params: data.groupId @@ -437,99 +630,132 @@ class WebBrowser extends LitElement { // 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 'DEPLOY_AT': { + const requiredFields = ['name', 'description', 'tags', 'creationBytes', 'amount', 'assetId', 'type']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); - case 'DEPLOY_AT': + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + + + try { + this.loader.show(); + const fee = data.fee || undefined + const resJoinGroup = await this._deployAt(data.name, data.description, data.tags, data.creationBytes, data.amount, data.assetId, fee, data.type) + response = JSON.stringify(resJoinGroup); + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to join the group.'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } finally { + this.loader.hide(); + } // Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee // TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT 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 'GET_WALLET_BALANCE': // Params: data.coin (QORT / LTC / DOGE / DGB / C / ARRR) // 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"}` - console.log('case passed here'); - console.log(data.coin, "data coin here"); - 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()}`, - }) - console.log({QORTBalance}) - 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') { + 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') { response = '{"error": "User declined request"}'; } break; @@ -554,14 +780,16 @@ 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') event.ports[0].postMessage({ result: null, error: responseObj, }); } else { + console.log('hello success') event.ports[0].postMessage({ result: responseObj, error: null, @@ -654,15 +882,13 @@ class WebBrowser extends LitElement { refresh() { const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ - window.parent.reduxStore.getState().app.nodeConfig.node + window.parent.reduxStore.getState().app.nodeConfig.node ]; const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; - this.url = `${nodeUrl}/render/${this.service}/${this.name}${ - this.path != null ? this.path : '' - }?theme=${this.theme}&identifier=${ - this.identifier != null ? this.identifier : '' - }`; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : '' + }`; } goBackToList() { @@ -811,9 +1037,8 @@ class WebBrowser extends LitElement { this.identifier == null ? 'default' : resource.identifier; let ret = await parentEpml.request('apiCall', { - url: `/arbitrary/resource/${this.service}/${ - this.name - }/${identifier}?apiKey=${this.getApiKey()}`, + url: `/arbitrary/resource/${this.service}/${this.name + }/${identifier}?apiKey=${this.getApiKey()}`, method: 'DELETE', }); @@ -864,7 +1089,7 @@ class WebBrowser extends LitElement { getApiKey() { const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ - window.parent.reduxStore.getState().app.nodeConfig.node + window.parent.reduxStore.getState().app.nodeConfig.node ]; let apiKey = myNode.apiKey; return apiKey; @@ -883,10 +1108,10 @@ async function showModalAndWait(type, data) { return new Promise((resolve) => { // Create the modal and add it to the DOM const modal = document.createElement('div'); - modal.id = "backdrop" - modal.classList.add("backdrop"); - modal.innerHTML = - `