diff --git a/assets/images/background1.jpg b/assets/images/background1.jpg new file mode 100644 index 0000000..15f02cf Binary files /dev/null and b/assets/images/background1.jpg differ diff --git a/assets/images/mbr-1-1818x1212.jpg b/assets/images/mbr-1-1818x1212.jpg new file mode 100644 index 0000000..1d9138f Binary files /dev/null and b/assets/images/mbr-1-1818x1212.jpg differ diff --git a/assets/images/mbr-1818x1212.jpg b/assets/images/mbr-1818x1212.jpg new file mode 100644 index 0000000..a0797ec Binary files /dev/null and b/assets/images/mbr-1818x1212.jpg differ diff --git a/assets/js/ARBoard.js b/assets/js/ARBoard.js new file mode 100644 index 0000000..fe97c4c --- /dev/null +++ b/assets/js/ARBoard.js @@ -0,0 +1,911 @@ + +let minterGroupAddresses +let minterAdminAddresses +let isTest = false +let isAddRemoveBoard = true +const addRemoveIdentifierPrefix = "QM-AR-card" +const loadAddRemoveAdminPage = async () => { + console.log("Loading Add/Remove Admin page...") + const bodyChildren = document.body.children + + for (let i = bodyChildren.length - 1; i >= 0; i--) { + const child = bodyChildren[i] + + if (!child.classList.contains("menu")) { + child.remove() + } + } + + const mainContainer = document.createElement("div") + mainContainer.className = "add-remove-admin-main" + mainContainer.style = "padding: 20px; text-align: center;" + mainContainer.innerHTML = ` +
+ This page allows proposing the promotion of an existing minter to admin, + or demotion of an existing admin back to a normal minter. +
+ +Refreshing cards...
" + await loadCards(addRemoveIdentifierPrefix) + }) + + document.getElementById("cancel-publish-button").addEventListener("click", async () => { + // const cardsContainer = document.getElementById("existing-proposals-section") + // cardsContainer.style.display = "flex" // Restore visibility + const publishCardView = document.getElementById("promotion-form-container") + publishCardView.style.display = "none" // Hide the publish form + const proposeButton = document.getElementById('propose-promotion-button') + proposeButton.style.display = 'flex' + // proposeButton.style.display === 'flex' ? 'none' : 'flex' + }) + + document.getElementById("add-link-button").addEventListener("click", async () => { + const linksContainer = document.getElementById("links-container") + const newLinkInput = document.createElement("input") + newLinkInput.type = "text" + newLinkInput.className = "card-link" + newLinkInput.placeholder = "Enter QDN link" + linksContainer.appendChild(newLinkInput) + }) + + document.getElementById("publish-card-form").addEventListener("submit", async (event) => { + event.preventDefault() + await publishARCard(addRemoveIdentifierPrefix) + }) + await featureTriggerCheck() + await loadCards(addRemoveIdentifierPrefix) + await displayExistingMinterAdmins() + await fetchAllARTxData() +} + +const toggleProposeButton = () => { + const proposeButton = document.getElementById('propose-promotion-button') + proposeButton.style.display = + proposeButton.style.display === 'flex' ? 'none' : 'flex' +} + +let addAdminTxs +let remAdminTxs + +const fetchAllARTxData = async () => { + const addAdmTx = "ADD_GROUP_ADMIN" + const remAdmTx = "REMOVE_GROUP_ADMIN" + + const filterAddTransactions = (rawTransactions) => { + // Group transactions by member + const memberTxMap = rawTransactions.reduce((map, tx) => { + if (!map[tx.member]) { + map[tx.member] = [] + } + map[tx.member].push(tx) + return map + }, {}) + + // Filter out members with both pending and non-pending transactions + return Object.values(memberTxMap) + .filter(txs => txs.every(tx => tx.approvalStatus !== 'PENDING')) + .flat() + // .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') && + // txs.some(tx => tx.approvalStatus !== 'PENDING'))) + // .flat() + } + + const filterRemoveTransactions = (rawTransactions) => { + // Group transactions by member + const adminTxMap = rawTransactions.reduce((map, tx) => { + if (!map[tx.admin]) { + map[tx.admin] = [] + } + map[tx.admin].push(tx) + return map + }, {}) + + // Filter out members with both pending and non-pending transactions + return Object.values(adminTxMap) + .filter((txs) => !(txs.some(tx => tx.approvalStatus === 'PENDING') && + txs.some(tx => tx.approvalStatus !== 'PENDING'))) + .flat() + } + + // Fetch ban transactions + const allAddTxs = await searchTransactions({ + txTypes: [addAdmTx], + confirmationStatus: 'CONFIRMED', + limit: 0, + reverse: true, + offset: 0, + startBlock: 1990000, + blockLimit: 0, + txGroupId: 694, + }) + // Filter out 'PENDING' + addAdminTxs = filterAddTransactions(allAddTxs) + console.warn('addAdminTxData (no PENDING nor past+PENDING):', addAdminTxs) + + // Fetch kick transactions + const allRemTxs = await searchTransactions({ + txTypes: [remAdmTx], + confirmationStatus: 'CONFIRMED', + limit: 0, + reverse: true, + offset: 0, + startBlock: 1990000, + blockLimit: 0, + txGroupId: 694, + }) + // Filter out 'PENDING' + remAdminTxs = filterRemoveTransactions(allRemTxs) + console.warn('remAdminTxData (no PENDING nor past+PENDING):', remAdminTxs) + } + +const displayExistingMinterAdmins = async () => { + const adminListContainer = document.getElementById("admin-list-container") + adminListContainer.innerHTML = + "Loading existing admins...
" + + try { + // 1) Fetch addresses + const admins = await fetchMinterGroupAdmins() + minterAdminAddresses = admins.map(m => m.member) + let rowsHtml = ""; + for (const adminAddr of admins) { + if (adminAddr.member === nullAddress) { + // Display a "NULL ACCOUNT" row + rowsHtml += ` +Admin Name | +Admin Address | +Actions | +
---|
Failed to load admins.
" + } +} + +const handleProposeDemotionWrapper = (adminName, adminAddress) => { + // Call the async function and handle any unhandled rejections + handleProposeDemotion(adminName, adminAddress).catch(error => { + console.error(`Error in handleProposeDemotionWrapper:`, error) + alert("An unexpected error occurred. Please try again.") + }) + } + +const handleProposeDemotion = async (adminName, adminAddress) => { + console.log(`Proposing demotion for: ${adminName} (${adminAddress})`) + const proposeButton = document.getElementById('propose-promotion-button') + proposeButton.style.display = 'none' + const fetchedCard = await fetchExistingARCard(addRemoveIdentifierPrefix, adminName) + if (fetchedCard) { + alert("A card already exists. Publishing of multiple cards is not allowed. Please update your card.") + isExistingCard = true + await loadCardIntoForm(fetchedCard) + } + + + // Populate the form with the admin's name + const nameInput = document.getElementById("minter-name-input") + nameInput.value = adminName + + // Display the form if it's hidden + const formContainer = document.getElementById("promotion-form-container") + formContainer.style.display = "flex" + + // Optionally hide other sections (e.g., the existing proposals section) + // const proposalsSection = document.getElementById("existing-proposals-section") + // proposalsSection.style.display = "none" + + // Notify the user to fill out the rest + alert(`Admin "${adminName}" has been selected for demotion. Please fill out the rest of the form.`) + } + + const fetchExistingARCard = async (cardIdentifierPrefix, minterName) => { + try { + // 1. Fetch all cards with the specified prefix + const response = await searchSimple( + 'BLOG_POST', + `${cardIdentifierPrefix}`, + '', // Empty name to fetch all cards + 0, + 0, + '', + true + ) + + console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`) + + if (!response || !Array.isArray(response) || response.length === 0) { + console.log("No cards found.") + return null + } + + // 2. Fetch minterGroupAddresses if not already fetched + if (!minterGroupAddresses) { + const groupData = await fetchMinterGroupMembers() + minterGroupAddresses = groupData.map((m) => m.member) + } + + // 3. Validate all fetched cards and check for duplicate `minterName` + const validatedCards = await Promise.all( + response.map(async (card) => { + const isValid = await validateCardStructure(card) + + if (!isValid) return null + + // Fetch full card data for validation + const cardDataResponse = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: card.name, + service: "BLOG_POST", + identifier: card.identifier, + }) + + // Check if `minterName` matches the input or is a duplicate + if (cardDataResponse.minterName === minterName) { + console.log(`Card with the same minterName found: ${minterName}`) + return { + card, + cardData: cardDataResponse, + } + } + + return null + }) + ) + + // 4. Filter out null results and check for duplicates + const matchingCards = validatedCards.filter((result) => result !== null) + + if (matchingCards.length > 0) { + const { card, cardData } = matchingCards[0] // Use the first matching card + + // Determine if the card is a promotion card + // const nameInfo = await getNameInfo(cardData.minterName) + // const ownerAddress = nameInfo?.owner + + // Set flags and return the existing card data + existingCardIdentifier = card.identifier + existingCardData = cardData + isExistingCard = true + + return { + cardData + } + } + + console.log("No valid cards found or no matching minterName.") + return null + } catch (error) { + console.error("Error fetching existing AR card:", error) + return null + } + } + + +const publishARCard = async (cardIdentifierPrefix) => { + const minterNameInput = document.getElementById("minter-name-input").value.trim() + const potentialNameInfo = await getNameInfo(minterNameInput) + let minterName + let address + let isPromotionCard + + if (potentialNameInfo.owner) { + console.log(`MINTER NAME FOUND:`, minterNameInput) + minterName = minterNameInput + address = potentialNameInfo.owner + } else { + console.warn(`user input an address?...`, minterNameInput) + if (!address){ + const validAddress = await getAddressInfo(minterNameInput) + if (validAddress){ + address = minterNameInput + } else { + console.error(`input address by user INVALID`, minterNameInput) + alert(`You have input an invalid address! Please try again...`) + return + } + } + const checkForName = await getNameFromAddress(minterNameInput) + if (checkForName) { + minterName = checkForName + } else if (!checkForName && address){ + console.warn(`user input an address that has no name...`) + alert(`you have input an address that has no name, the address will need to register a name prior to being able to be promoted`) + return + } else { + console.warn(`Input was either an invalid name, or incorrect address?`, minterNameInput) + alert(`Your input could not be validated, check the name/address and try again!`) + return + } + } + + const minterGroupData = await fetchMinterGroupMembers() + minterGroupAddresses = minterGroupData.map(m => m.member) + + const minterAdminGroupData = await fetchMinterGroupAdmins() + minterAdminAddresses = minterAdminGroupData.map(m => m.member) + + if (minterGroupAddresses.includes(address)) { + isPromotionCard = true + console.warn(`address is a MINTER, this is a promotion card...`) + } + + if (minterAdminAddresses.includes(address)){ + isPromotionCard = false + console.warn(`this is a DEMOTION`, address) + } + + if (!minterAdminAddresses.includes(address) && !minterGroupAddresses.includes(address)) { + console.error(`you cannot publish a card here unless the user is a MINTER or an ADMIN!`) + alert(`Card cannot be published for an account that is neither a minter nor an admin! This board is for Promotions and Demotions of Admins ONLY!`) + return + } + + const header = document.getElementById("card-header").value.trim() + const content = document.getElementById("card-content").value.trim() + const links = Array.from(document.querySelectorAll(".card-link")) + .map(input => input.value.trim()) + .filter(link => link.startsWith("qortal://")) + + if (!header || !content) { + alert("Header and content are required!") + return + } + + const cardIdentifier = isExistingCard ? existingCardIdentifier : `${cardIdentifierPrefix}-${await uid()}` + const pollName = `${cardIdentifier}-poll` + const pollDescription = `AR Board Card Proposed By: ${userState.accountName}` + + const cardData = { + minterName, + header, + content, + links, + creator: userState.accountName, + timestamp: Date.now(), + poll: pollName, + promotionCard: isPromotionCard + } + + try { + let base64CardData = await objectToBase64(cardData) + if (!base64CardData) { + console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`) + base64CardData = btoa(JSON.stringify(cardData)) + } + + await qortalRequest({ + action: "PUBLISH_QDN_RESOURCE", + name: userState.accountName, + service: "BLOG_POST", + identifier: cardIdentifier, + data64: base64CardData, + }) + + if (!isExistingCard){ + await qortalRequest({ + action: "CREATE_POLL", + pollName, + pollDescription, + pollOptions: ['Yes, No'], + pollOwnerAddress: userState.accountAddress, + }) + alert("Card and poll published successfully!") + } + + if (isExistingCard){ + alert("Card Updated Successfully! (No poll updates are possible at this time...)") + isExistingCard = false + } + + document.getElementById("publish-card-form").reset() + document.getElementById("promotion-form-container").style.display = "none" + // document.getElementById("cards-container").style.display = "flex" + + await loadCards(addRemoveIdentifierPrefix) + + } catch (error) { + + console.error("Error publishing card or poll:", error) + alert("Failed to publish card and poll.") + } +} + +const checkAndDisplayActions = async (adminYes, name, cardIdentifier) => { + const latestBlockInfo = await getLatestBlockInfo() + const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT + let minAdminCount + const minterAdmins = await fetchMinterGroupAdmins() + + if ((minterAdmins) && (minterAdmins.length === 1)){ + console.warn(`simply a double-check that there is only one MINTER group admin, in which case the group hasn't been transferred to null...keeping default minAdminCount of: ${minAdminCount}`) + minAdminCount = 9 + } else if ((minterAdmins) && (minterAdmins.length > 1) && isBlockPassed){ + const totalAdmins = minterAdmins.length + const fortyPercent = totalAdmins * 0.40 + minAdminCount = Math.round(fortyPercent) + console.warn(`this is another check to ensure minterAdmin group has more than 1 admin. IF so we will calculate the 40% needed for GROUP_APPROVAL, that number is: ${minAdminCount}`) + } + const addressInfo = await getNameInfo(name) + const address = addressInfo.owner + + if (isBlockPassed) { + console.warn(`feature trigger has passed, checking for approval requirements`) + const addAdminApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "ADD_GROUP_ADMIN") + const removeAdminApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "REMOVE_GROUP_ADMIN") + + if (addAdminApprovalHtml) { + return addAdminApprovalHtml + } + + if (removeAdminApprovalHtml) { + return removeAdminApprovalHtml + } + } + + if (!minterGroupAddresses) { + const minterGroupData = await fetchMinterGroupMembers() + minterGroupAddresses = minterGroupData.map(m => m.member) + } + + if (!minterAdminAddresses) { + const adminAddressData = await fetchMinterGroupAdmins() + minterAdminAddresses = adminAddressData.map(m => m.member) + } + + if (!minterGroupAddresses.includes(userState.accountAddress)){ + console.warn(`User is not in the MINTER group, no need for buttons`) + return null + } + + if (adminYes >= minAdminCount && (minterAdminAddresses.includes(address))){ + const removeAdminHtml = createRemoveAdminButton(name, cardIdentifier, address) + return removeAdminHtml + } else if (adminYes >= minAdminCount && (minterGroupAddresses.includes(address))){ + const addAdminHtml = createAddAdminButton(name, cardIdentifier, address) + return addAdminHtml + } +} + +const createAddAdminButton = (name, cardIdentifier, address) => { + return ` + + ` +} + +const createRemoveAdminButton = (name, cardIdentifier, address) => { + return ` + + ` +} + +const handleAddMinterGroupAdmin = async (name, address) => { + try { + // Optional block check + let txGroupId = 0 + let member = address + // const { height: currentHeight } = await getLatestBlockInfo() + const isBlockPassed = await featureTriggerCheck() + if (isBlockPassed) { + console.log(`block height above featureTrigger Height, using group approval method...txGroupId 694`) + txGroupId = 694 + } + + const ownerPublicKey = await getPublicKeyFromAddress(userState.accountAddress) + const fee = 0.01 + + const rawTx = await createAddGroupAdminTransaction(ownerPublicKey, 694, member, txGroupId, fee) + + const signedTx = await qortalRequest({ + action: "SIGN_TRANSACTION", + unsignedBytes: rawTx + }) + + if (!signedTx) { + console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`) + alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`) + return + } + + let txToProcess = signedTx + + const processTx = await processTransaction(txToProcess) + + if (typeof processTx === 'object') { + console.log("transaction success object:", processTx) + alert(`${name} kick successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processTx)}`) + } else { + console.log("transaction raw text response:", processTx) + alert(`TxResponse: ${JSON.stringify(processTx)}`) + } + + } catch (error) { + console.error("Error removing minter:", error) + alert(`Error:${error}. Please try again.`) + } +} + +const handleRemoveMinterGroupAdmin = async (name, address) => { + try { + // Optional block check + let txGroupId = 0 + const admin = address + // const { height: currentHeight } = await getLatestBlockInfo() + const isBlockPassed = await featureTriggerCheck() + if (isBlockPassed) { + console.log(`block height above featureTrigger Height, using group approval method...txGroupId 694`) + txGroupId = 694 + } + + const ownerPublicKey = await getPublicKeyFromAddress(userState.accountAddress) + const fee = 0.01 + + const rawTx = await createRemoveGroupAdminTransaction(ownerPublicKey, 694, admin, txGroupId, fee) + + const signedTx = await qortalRequest({ + action: "SIGN_TRANSACTION", + unsignedBytes: rawTx + }) + if (!signedTx) { + console.warn(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added?`) + alert(`this only happens if the SIGN_TRANSACTION qortalRequest failed... are you using the legacy UI prior to this qortalRequest being added? Please talk to developers.`) + return + } + + let txToProcess = signedTx + + const processTx = await processTransaction(txToProcess) + + if (typeof processTx === 'object') { + console.log("transaction success object:", processTx) + alert(`${name} kick successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processTx)}`) + } else { + console.log("transaction raw text response:", processTx) + alert(`TxResponse: ${JSON.stringify(processTx)}`) + } + + } catch (error) { + console.error("Error removing minter:", error) + alert(`Error:${error}. Please try again.`) + } +} + +const fallbackMinterCheck = async (minterName, minterGroupMembers, minterAdmins) => { + // Ensure we have addresses + if (!minterGroupMembers) { + console.warn("No minterGroupMembers array was passed in fallback check!") + return false + } + const minterGroupAddresses = minterGroupMembers.map(m => m.member) + const adminAddresses = minterAdmins.map(m => m.member) + const minterAcctInfo = await getNameInfo(minterName) + if (!minterAcctInfo || !minterAcctInfo.owner) { + console.warn(`Name info not found or missing 'owner' for ${minterName}`) + return false + } + // If user is already in the group => we call it a "promotion card" + if (adminAddresses.includes(minterAcctInfo.owner)) { + console.warn(`display check found minterAdminCard - NOT a promotion card...`) + return false + } else { + return minterGroupAddresses.includes(minterAcctInfo.owner) + } +} + + +const createARCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => { + const { minterName, header, content, links, creator, timestamp, poll, promotionCard } = cardData + const formattedDate = new Date(timestamp).toLocaleString() + const minterAvatar = await getMinterAvatar(minterName) + const creatorAvatar = await getMinterAvatar(creator) + const linksHTML = links.map((link, index) => ` + + `).join("") + + const minterGroupMembers = await fetchMinterGroupMembers() + const minterAdmins = await fetchMinterGroupAdmins() + + let showPromotionCard = false + showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers, minterAdmins) + + // if (typeof promotionCard === 'boolean') { + // showPromotionCard = promotionCard; + // } else if (typeof promotionCard === 'string') { + // // Could be "true" or "false" or something else + // const lower = promotionCard.trim().toLowerCase() + // if (lower === "true") { + // showPromotionCard = true + // } else if (lower === "false") { + // showPromotionCard = false + // } else { + // // Unexpected string => fallback + // console.warn(`Unexpected string in promotionCard="${promotionCard}"`) + // showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers) + // } + // } else if (promotionCard == null) { + // // null or undefined => fallback check + // console.warn(`No promotionCard field in card data, doing manual check...`) + // showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers) + // } else { + // // If it’s an object or something else weird => fallback + // console.warn(`promotionCard has unexpected type, fallback...`) + // showPromotionCard = await fallbackMinterCheck(minterName, minterGroupMembers) + // } + + let cardColorCode = (showPromotionCard) ? 'rgb(17, 44, 46)' : 'rgb(57, 11, 13)' + + const promotionDemotionHtml = (showPromotionCard) ? ` +${header}
+ ${altText} +(click COMMENTS button to open/close card comments)
+By: ${creator} - ${formattedDate}
+