From 6568d7600d01a05b3b2055f632b0789c6b98ce85 Mon Sep 17 00:00:00 2001 From: Phillip Date: Thu, 23 Feb 2023 23:20:15 +0000 Subject: [PATCH] added deploy_at, join_group, publish qdn --- .../api/transactions/DeployAtTransaction.js | 91 +++ .../api/transactions/transactions.js | 2 + .../plugins/core/qdn/browser/browser.src.js | 519 +++++++++++++----- .../plugins/utils/publish-image.js | 3 +- 4 files changed, 467 insertions(+), 148 deletions(-) create mode 100644 qortal-ui-crypto/api/transactions/DeployAtTransaction.js diff --git a/qortal-ui-crypto/api/transactions/DeployAtTransaction.js b/qortal-ui-crypto/api/transactions/DeployAtTransaction.js new file mode 100644 index 00000000..c3eb20e4 --- /dev/null +++ b/qortal-ui-crypto/api/transactions/DeployAtTransaction.js @@ -0,0 +1,91 @@ +'use strict' +import TransactionBase from './TransactionBase.js' + +export default class DeployAtTransaction extends TransactionBase { + constructor() { + super() + this.type = 16 + } + + render(html) { + return html` + ${this._groupdialog5} +
+
${this._atDeployDialog1}: ${this._rName}
+
+
${this.atDeployDialog2}: ${this._rDescription}
+
+
+ + ${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': + case 'DEPLOY_AT': { + const requiredFields = ['name', 'description', 'tags', 'creationBytes', 'amount', 'assetId', 'type']; + 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 + } + + + 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 + let coin = data.coin; + if (coin === "QORT") { + let qortAddress = window.parent.reduxStore.getState().app.selectedAddress.address + try { + this.loader.show(); + const QORTBalance = await parentEpml.request('apiCall', { + url: `/addresses/balance/${qortAddress}?apiKey=${this.getApiKey()}`, + }) + return QORTBalance; + } catch (error) { + console.error(error); + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } finally { + this.loader.hide(); + } + } else { + let _url = `` + let _body = null - switch (coin) { - case 'LTC': - _url = `/crosschain/ltc/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet.derivedMasterPublicKey - break - case 'DOGE': - _url = `/crosschain/doge/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet.derivedMasterPublicKey - break - case 'DGB': - _url = `/crosschain/dgb/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet.derivedMasterPublicKey - break - case 'RVN': - _url = `/crosschain/rvn/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet.derivedMasterPublicKey - break - case 'ARRR': - _url = `/crosschain/arrr/walletbalance?apiKey=${this.getApiKey()}` - _body = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet.seed58 - break - default: - break - } - try { - this.loader.show() - await parentEpml.request('apiCall', { - url: _url, - method: 'POST', - body: _body, - }).then((res) => { - if (isNaN(Number(res))) { - throw new Error(get("browserpage.bchange21")); - } else { - console.log((Number(res) / 1e8).toFixed(8), "other wallet balance here"); - return (Number(res) / 1e8).toFixed(8) - } - }) - } catch (error) { - console.error(error); - const data = {}; - const errorMsg = error.message || get("browserpage.bchange21"); - data['error'] = errorMsg; - response = JSON.stringify(data); - return; - } finally { - this.loader.hide() - } - } - } else if (res3.action === 'reject') { + switch (coin) { + case 'LTC': + _url = `/crosschain/ltc/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet.derivedMasterPublicKey + break + case 'DOGE': + _url = `/crosschain/doge/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet.derivedMasterPublicKey + break + case 'DGB': + _url = `/crosschain/dgb/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet.derivedMasterPublicKey + break + case 'RVN': + _url = `/crosschain/rvn/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet.derivedMasterPublicKey + break + case 'ARRR': + _url = `/crosschain/arrr/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet.seed58 + break + default: + break + } + try { + this.loader.show() + 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 = - `