diff --git a/qortal-ui-core/src/components/sidenav-menu.js b/qortal-ui-core/src/components/sidenav-menu.js index 3b058c7e..bc29d8d4 100644 --- a/qortal-ui-core/src/components/sidenav-menu.js +++ b/qortal-ui-core/src/components/sidenav-menu.js @@ -158,6 +158,12 @@ class SidenavMenu extends connect(store)(LitElement) { > + + + { in: 'plugins/core/become-minter/become-minter.src.js', out: 'plugins/core/become-minter/become-minter.js', }, + { + in: 'plugins/core/sponsorship-list/sponsorship-list.src.js', + out: 'plugins/core/sponsorship-list/sponsorship-list.js', + }, { in: 'plugins/core/puzzles/puzzles.src.js', out: 'plugins/core/puzzles/puzzles.js', diff --git a/qortal-ui-plugins/plugins/core/main.src.js b/qortal-ui-plugins/plugins/core/main.src.js index b4efcb9f..7c554a84 100644 --- a/qortal-ui-plugins/plugins/core/main.src.js +++ b/qortal-ui-plugins/plugins/core/main.src.js @@ -25,6 +25,15 @@ parentEpml.ready().then(() => { menus: [], parent: false, }, + { + url: 'sponsorship-list', + domain: 'core', + page: 'sponsorship-list/index.html', + title: 'Become a Minter', + icon: 'vaadin:info-circle', + menus: [], + parent: false, + }, { url: 'wallet', domain: 'core', diff --git a/qortal-ui-plugins/plugins/core/sponsorship-list/index.html b/qortal-ui-plugins/plugins/core/sponsorship-list/index.html new file mode 100644 index 00000000..b4e15984 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/sponsorship-list/index.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + diff --git a/qortal-ui-plugins/plugins/core/sponsorship-list/sponsorship-list-css.src.js b/qortal-ui-plugins/plugins/core/sponsorship-list/sponsorship-list-css.src.js new file mode 100644 index 00000000..07eec912 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/sponsorship-list/sponsorship-list-css.src.js @@ -0,0 +1,306 @@ +import { css } from "lit" + +export const pageStyles = css` + * { + --mdc-theme-surface: var(--white); + --mdc-dialog-content-ink-color: var(--black); + } + + .header-title { + font-size: 40px; + color: var(--black); + font-weight: 400; + text-align: center; + } + .divider { + color: #eee; + border-radius: 80%; + margin-bottom: 2rem; + } + .fullWidth { + width: 100%; + } + .page-container { + display: flex; + align-items: center; + flex-direction: column; + margin-bottom: 75px; + } + .inner-container { + display: flex; + align-items: center; + flex-direction: column; + width: 95%; + max-width: 1100px; + } + + .description { + color: var(--black); + } + + .message { + color: var(--gray); + } + + .sub-main { + width: 95%; + display: flex; + + flex-direction: column; + max-width: 1100px; + } + + .level-black { + font-size: 32px; + color: var(--black); + font-weight: 400; + text-align: center; + margin-top: 2rem; + } + + .form-wrapper { + display: flex; + align-items: center; + width: 100%; + max-width: 700px; + height: 50px; + } + + .row { + display: flex; + width: 100%; + } + .column { + display: flex; + flex-direction: column; + width: 100%; + } + + .column-center { + align-items: center; + } + .no-margin { + margin: 0; + } + .no-wrap { + flex-wrap: nowrap !important; + } + + .row-center { + justify-content: center; + flex-wrap: wrap; + } + .form-item { + display: flex; + height: 100%; + } + + .form-item--button { + flex-grow: 0; + } + + .form-item--input { + flex-grow: 1; + margin-right: 25px; + } + + .center-box { + position: absolute; + width: 100%; + top: 50%; + left: 50%; + transform: translate(-50%, 0%); + text-align: center; + } + + .content-box { + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px 25px; + text-align: center; + display: inline-block; + + margin-bottom: 5px; + flex-basis: 250px; + } + .gap { + gap: 10px; + } + .level-black { + font-size: 32px; + color: var(--black); + font-weight: 400; + text-align: center; + margin-top: 2rem; + text-align: center; + } + .title { + font-weight: 600; + font-size: 20px; + line-height: 28px; + opacity: 0.66; + color: var(--switchborder); + } + + .address { + overflow-wrap: anywhere; + color: var(--black); + } + + h4 { + font-weight: 600; + font-size: 20px; + line-height: 28px; + color: var(--black); + } + mwc-textfield { + width: 100%; + } + vaadin-button { + height: 100%; + margin: 0; + cursor: pointer; + outline: 1px var(--black) solid; + min-width: 80px; + } + .loader, + .loader:after { + border-radius: 50%; + width: 10em; + height: 10em; + } + .loadingContainer { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; + } + + .backdrop { + height: 100vh; + width: 100vw; + opacity: 0.6; + background-color: var(--border); + z-index: 9; + position: fixed; + } + + .loading, + .loading:after { + border-radius: 50%; + width: 5em; + height: 5em; + } + + .loading { + margin: 10px auto; + border-width: 0.6em; + border-style: solid; + border-color: rgba(3, 169, 244, 0.2) rgba(3, 169, 244, 0.2) + rgba(3, 169, 244, 0.2) rgb(3, 169, 244); + font-size: 10px; + position: relative; + text-indent: -9999em; + transform: translateZ(0px); + animation: 1.1s linear 0s infinite normal none running loadingAnimation; + } + + @-webkit-keyframes loadingAnimation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + @keyframes loadingAnimation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + .tableGrid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax( + 0, + 1fr + ) minmax(0, 1fr); + align-items: center; + gap: 5px; + width: 100%; + margin-bottom: 15px; + + padding: 5px; + + } + + + .grid-item { + text-align: center; + color: var(--black); + word-break: break-all; + overflow: hidden; + } + + .grid-item p { + text-decoration: underline; + } + + ul { + list-style-type: none; + margin: 0; + padding: 0; + } + .red { + --mdc-theme-primary: #f44336; + } + + .grid-item-text { + display: none; + } + + @media (max-width: 710px) { + .table-header { + display: none; + } + .grid-item-text { + display: inline; + color: var(--black); + text-decoration: none; + margin: 0px; + margin-right: 10px; + } + + .grid-item { + text-align: start; + align-items: center; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + + .grid-item p { + text-decoration: none; + } + + .tableGrid { + grid-template-columns: minmax(0, 1fr); + border-radius: 5px; + border: 1px solid var(--black); + padding: 10px; + margin-bottom: 20px; + } + + mwc-button { + grid-column: 1 / -1; + } + } +` diff --git a/qortal-ui-plugins/plugins/core/sponsorship-list/sponsorship-list.src.js b/qortal-ui-plugins/plugins/core/sponsorship-list/sponsorship-list.src.js new file mode 100644 index 00000000..adc3251b --- /dev/null +++ b/qortal-ui-plugins/plugins/core/sponsorship-list/sponsorship-list.src.js @@ -0,0 +1,573 @@ +import { LitElement, html } from "lit" +import { Epml } from "../../../epml.js" +import "../components/ButtonIconCopy.js" +import { use, get, translate, registerTranslateConfig } from "lit-translate" +import { blocksNeed } from "../../utils/blocks-needed.js" +import "../components/ButtonIconCopy.js" + +registerTranslateConfig({ + loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()), +}) + +import "@polymer/paper-spinner/paper-spinner-lite.js" +import "@material/mwc-button" +import "@material/mwc-textfield" +import "@vaadin/button" +import "@material/mwc-button" +import "@polymer/paper-spinner/paper-spinner-lite.js" + +import { pageStyles } from "./sponsorship-list-css.src.js" + +const parentEpml = new Epml({ type: "WINDOW", source: window.parent }) + +class SponsorshipList extends LitElement { + static get properties() { + return { + theme: { type: String, reflect: true }, + sponsorshipKeyValue: { type: String }, + nodeInfo: { type: Object }, + isPageLoading: { type: Boolean }, + addressInfo: { type: Object }, + rewardSharePublicKey: { type: String }, + mintingAccountData: { type: Array }, + sponsorships: { type: Array }, + removeRewardShareLoading: { type: Array }, + createSponsorshipMessage: { type: String }, + isLoadingCreateSponsorship: { type: Array }, + publicKeyValue: { type: String }, + error: { type: Boolean }, + } + } + + static styles = [pageStyles] + + constructor() { + super() + this.theme = localStorage.getItem("qortalTheme") + ? localStorage.getItem("qortalTheme") + : "light" + this.sponsorshipKeyValue = "" + this.isPageLoading = true + this.nodeInfo = {} + this.addressInfo = {} + this.rewardSharePublicKey = "" + this.mintingAccountData = null + this.sponsorships = [] + this.removeRewardShareLoading = false + this.error = false + this.createSponsorshipMessage = "" + this.isLoadingCreateSponsorship = false + this.publicKeyValue = "" + } + + inputHandler(e) { + this.publicKeyValue = e.target.value + } + + changeLanguage() { + const checkLanguage = localStorage.getItem("qortalLanguage") + + if (checkLanguage === null || checkLanguage.length === 0) { + localStorage.setItem("qortalLanguage", "us") + use("us") + } else { + use(checkLanguage) + } + } + + _handleStorage() { + 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) + } + + connectedCallback() { + super.connectedCallback() + window.addEventListener("storage", this._handleStorage) + } + + disconnectedCallback() { + window.removeEventListener("storage", this._handleStorage) + super.disconnectedCallback() + } + + async getNodeInfo() { + const nodeInfo = await parentEpml.request("apiCall", { + url: `/admin/status`, + }) + + return nodeInfo + } + + async atMount() { + this.changeLanguage() + + this.addressInfo = + window.parent.reduxStore.getState().app.accountInfo.addressInfo + this.isPageLoading = true + try { + const address = + window.parent.reduxStore.getState().app?.selectedAddress + ?.address + let rewardShares = await this.getRewardShareRelationship( + "QPsjHoKhugEADrtSQP5xjFgsaQPn9WmE3Y" + ) + + rewardShares = rewardShares.filter((rs) => rs.recipient !== address) + + const getAccountInfo = rewardShares.map(async (rs) => { + const addressInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/${rs.recipient}`, + }) + const recentBlocksInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/blocks/signer/${rs.recipient}?limit=5&reverse=true`, + }) + let blocksRemaining = this._levelUpBlocks(addressInfo) + blocksRemaining = +blocksRemaining > 0 ? +blocksRemaining : 0 + return { + ...addressInfo, + ...rs, + lastActiveBlock: recentBlocksInfo[0]?.height, + blocksRemaining: blocksRemaining, + } + }) + const accountInfoValues = await Promise.all(getAccountInfo) + + this.sponsorships = accountInfoValues + this.nextSponsorshipEnding = accountInfoValues + .filter((sponsorship) => sponsorship.blocksRemaining !== 0) + .sort((a, b) => a.blocksRemaining - b.blocksRemaining)[0] + this.isPageLoading = false + } catch (error) { + console.error(error) + + this.isPageLoading = false + } + } + + async firstUpdated() { + await this.atMount() + } + + async getRewardShareRelationship(recipientAddress) { + const myRewardShareArray = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/rewardshares?minters=${recipientAddress}`, + }) + + return myRewardShareArray + } + + _levelUpBlocks(accountInfo) { + let countBlocksString = ( + blocksNeed(0) - + (accountInfo?.blocksMinted + accountInfo?.blocksMintedAdjustment) + ).toString() + return countBlocksString + } + + async removeRewardShare(rewardShareObject) { + const selectedAddress = + window.parent.reduxStore.getState().app?.selectedAddress + + const myPercentageShare = -1 + + // Check for valid... + this.removeRewardShareLoading = true + + // Get Last Ref + const getLastRef = async () => { + let myRef = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/lastreference/${selectedAddress?.address}`, + }) + return myRef + } + + // Remove Reward Share + const removeReceiver = async () => { + let lastRef = await getLastRef() + + let myTransaction = await makeTransactionRequest(lastRef) + getTxnRequestResponse(myTransaction) + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + let mylastRef = lastRef + let rewarddialog5 = get("transactions.rewarddialog5") + let rewarddialog6 = get("transactions.rewarddialog6") + let myTxnrequest = await parentEpml.request("transaction", { + type: 381, + nonce: selectedAddress?.nonce, + params: { + rewardShareKeyPairPublicKey: + rewardShareObject.rewardSharePublicKey, + recipient: rewardShareObject.recipient, + percentageShare: myPercentageShare, + lastReference: mylastRef, + rewarddialog5: rewarddialog5, + rewarddialog6: rewarddialog6, + }, + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + this.removeRewardShareLoading = false + parentEpml.request("showSnackBar", txnResponse.message) + throw new Error(txnResponse) + } else if ( + txnResponse.success === true && + !txnResponse.data.error + ) { + let err7tring = get("rewardsharepage.rchange22") + this.removeRewardShareLoading = false + parentEpml.request("showSnackBar", `${err7tring}`) + this.atMount() + } else { + this.removeRewardShareLoading = false + parentEpml.request("showSnackBar", txnResponse.data.message) + throw new Error(txnResponse) + } + } + removeReceiver() + } + + async createRewardShare(e) { + this.error = false + this.createSponsorshipMessage = "" + const recipientPublicKey = this.publicKeyValue + const percentageShare = 0 + const selectedAddress = + window.parent.reduxStore.getState().app?.selectedAddress + // Check for valid... + this.isLoadingCreateSponsorship = true + + let recipientAddress = + window.parent.base58PublicKeyToAddress(recipientPublicKey) + + // Get Last Ref + const getLastRef = async () => { + let myRef = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/lastreference/${selectedAddress.address}`, + }) + return myRef + } + + // Get Account Details + const getAccountDetails = async () => { + let myAccountDetails = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/${selectedAddress.address}`, + }) + return myAccountDetails + } + + // Get Reward Relationship if it already exists + const getRewardShareRelationship = async (minterAddr) => { + let isRewardShareExisting = false + let myRewardShareArray = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/rewardshares?minters=${minterAddr}&recipients=${recipientAddress}`, + }) + isRewardShareExisting = + myRewardShareArray.length !== 0 ? true : false + return isRewardShareExisting + } + + // Validate Reward Share by Level + const validateReceiver = async () => { + let accountDetails = await getAccountDetails() + let lastRef = await getLastRef() + let isExisting = await getRewardShareRelationship( + selectedAddress.address + ) + + // Check for creating self share at different levels (also adding check for flags...) + if (accountDetails.flags === 1) { + this.error = false + this.createSponsorshipMessage = "" + let myTransaction = await makeTransactionRequest(lastRef) + if (isExisting === true) { + this.error = true + this.createSponsorshipMessage = `Cannot Create Multiple Reward Shares!` + } else { + // Send the transaction for confirmation by the user + this.error = false + this.createSponsorshipMessage = "" + getTxnRequestResponse(myTransaction) + } + } else if (accountDetails.address === recipientAddress) { + if (accountDetails.level >= 1 && accountDetails.level <= 4) { + this.error = false + this.createSponsorshipMessage = "" + let myTransaction = await makeTransactionRequest(lastRef) + if (isExisting === true) { + let err1string = get("rewardsharepage.rchange18") + this.error = true + this.createSponsorshipMessage = `${err1string}` + } else { + // Send the transaction for confirmation by the user + this.error = false + this.createSponsorshipMessage = "" + getTxnRequestResponse(myTransaction) + } + } else if (accountDetails.level >= 5) { + this.error = false + this.createSponsorshipMessage = "" + let myTransaction = await makeTransactionRequest(lastRef) + if (isExisting === true) { + let err2string = get("rewardsharepage.rchange19") + this.error = true + this.createSponsorshipMessage = `${err2string}` + } else { + // Send the transaction for confirmation by the user + this.error = false + this.createSponsorshipMessage = "" + getTxnRequestResponse(myTransaction) + } + } else { + let err3string = get("rewardsharepage.rchange20") + this.error = true + this.createSponsorshipMessage = `${err3string} ${accountDetails.level}` + } + } else { + //Check for creating reward shares + if (accountDetails.level >= 5) { + this.error = false + this.createSponsorshipMessage = "" + let myTransaction = await makeTransactionRequest(lastRef) + if (isExisting === true) { + let err4string = get("rewardsharepage.rchange18") + this.error = true + this.createSponsorshipMessage = `${err4string}` + } else { + // Send the transaction for confirmation by the user + this.error = false + this.createSponsorshipMessage = "" + getTxnRequestResponse(myTransaction) + } + } else { + this.error = true + let err5string = get("rewardsharepage.rchange20") + this.createSponsorshipMessage = `${err5string} ${accountDetails.level}` + } + } + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + let mylastRef = lastRef + let rewarddialog1 = get("transactions.rewarddialog1") + let rewarddialog2 = get("transactions.rewarddialog2") + let rewarddialog3 = get("transactions.rewarddialog3") + let rewarddialog4 = get("transactions.rewarddialog4") + let myTxnrequest = await parentEpml.request("transaction", { + type: 38, + nonce: selectedAddress.nonce, + params: { + recipientPublicKey, + percentageShare, + lastReference: mylastRef, + rewarddialog1: rewarddialog1, + rewarddialog2: rewarddialog2, + rewarddialog3: rewarddialog3, + rewarddialog4: rewarddialog4, + }, + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + this.error = true + this.createSponsorshipMessage = txnResponse.message + throw new Error(txnResponse) + } else if ( + txnResponse.success === true && + !txnResponse.data.error + ) { + let err6string = get("rewardsharepage.rchange21") + this.createSponsorshipMessage = err6string + this.error = false + } else { + this.error = true + this.createSponsorshipMessage = txnResponse.data.message + throw new Error(txnResponse) + } + } + validateReceiver() + this.isLoadingCreateSponsorship = false + } + + render() { + console.log({ sponsorships: this.sponsorships }) + + return html` + ${ + this.isPageLoading + ? html` +
+
+
+
+ ` + : "" + } + +
+

+ ${translate("mintingpage.mchange35")} +

+
+
+
+
+
+
+

Account Address

+
+
+

Blocks Minted

+
+
+

Last Active Block

+
+
+

Sponsorship Key

+
+
+ +
+
+ + + ${this.sponsorships.map( + (sponsorship) => html` +
    +
  • +

    + Account Address +

    + ${sponsorship.address} +
  • +
  • +

    + Blocks Minted +

    + ${+sponsorship.blocksMinted + + +sponsorship.blocksMintedAdjustment} +
  • +
  • +

    + Last Active Block +

    + ${sponsorship.lastActiveBlock} +
  • +
  • +

    + Copy Sponsorship Key +

    + +
  • +
  • + + this.removeRewardShare( + sponsorship + )} + >create${translate( + "rewardsharepage.rchange17" + )} +
  • +
+ ` + )} + + ${ + this.sponsorships.length > 0 && + html` +
+

+ Total Sponsorships active + ${this.sponsorships.length} +

+

+ Next sponsorship ending in + ${this.nextSponsorshipEnding + ?.blocksRemaining} + blocks +

+
+ ` + } +

${this.createSponsorshipMessage}

+
+
+ + +
+
+ + ${ + this.isLoadingCreateSponsorship === false + ? html`${translate( + "puzzlepage.pchange15" + )}` + : html`` + } + +
+
+
+
+ ` + } +} + +window.customElements.define("sponsorship-list", SponsorshipList)