let minterGroupAddresses let minterAdminAddresses let isTest = false let isAddRemoveBoard = true let otherPublisher = false const addRemoveIdentifierPrefix = "QM-AR-card" const AR_TX_CACHE_TTL_MS = 30000 let arTxCache = { timestamp: 0, data: null, } const getAllARTxDataCached = async (force = false) => { const now = Date.now() const isStale = now - arTxCache.timestamp > AR_TX_CACHE_TTL_MS if (force || !arTxCache.data || isStale) { arTxCache.data = await fetchAllARTxData() arTxCache.timestamp = now } return arTxCache.data } const loadAddRemoveAdminPage = async () => { // Kakashi Note: Clear other board scroll listeners before loading this board to prevent duplicate lazy-load callbacks. if (typeof detachAdminBoardInfiniteScroll === "function") { detachAdminBoardInfiniteScroll() } if (typeof detachMinterBoardInfiniteScroll === "function") { detachMinterBoardInfiniteScroll() } 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.
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 { const response = await searchSimple( "BLOG_POST", `${cardIdentifierPrefix}`, "", 0, 0, "", false, true ) console.log( `fetchExistingCard searchSimple response: ${JSON.stringify( response, null, 2 )}` ) if (!response || !Array.isArray(response) || response.length === 0) { console.log("No cards found.") return null } 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, }) if (cardDataResponse.minterName === minterName) { console.log(`Card with the same minterName found: ${minterName}`) if (cardDataResponse.creator === userState.accountName) { console.log(`The user is the publisher, adding card...`) return { card, cardData: cardDataResponse, } } else { console.warn(`Card found, but user is not the creator!`) otherPublisher = true return null } } return null }) ) // 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, which should be the first published for the minterName 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 exists = await fetchExistingARCard(cardIdentifierPrefix, minterName) if (exists) { alert( `An existing card was found, you must update it, two cards for the samme name cannot be published! Loading card data...` ) if (exists.creator != userState.accountName) { alert(`You are not the original publisher of this card, exiting.`) return } else { await loadCardIntoForm(existingCardData) minterName = exists.minterName const nameInfo = await getNameInfo(exists.minterName) address = nameInfo.owner isExistingCard = true } } const minterGroupData = await fetchMinterGroupMembers() minterGroupAddresses = minterGroupData.map((m) => m.member) const minterAdminGroupData = await fetchMinterGroupAdmins() minterAdminAddresses = minterAdminGroupData.map((m) => m.member) if (minterAdminAddresses.includes(address)) { isPromotionCard = false console.warn(`this is a DEMOTION`, address) } else if (minterGroupAddresses.includes(address)) { isPromotionCard = true console.warn(`address is a MINTER, this is a promotion card...`) } 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, minterAddress: address, 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 } if (isPromotionCard) { isPromotionCard = 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, true) } 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 = cachedMinterAdmins && cachedMinterAdmins.length > 0 ? cachedMinterAdmins : 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.4 minAdminCount = Math.ceil(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 } if (!minterAdmins) { console.warn("No minterAdmins 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, cardUpdatedTime, bgColor, cardPublisherAddress, illegalDuplicate ) => { const { minterName, minterAddress = "", header, content, links, creator, timestamp, poll, promotionCard, } = cardData const formattedDate = new Date(timestamp).toLocaleString() const minterAvatar = await getMinterAvatar(minterName) const creatorAvatar = await getMinterAvatar(creator) // Kakashi Note: Render links with escaped data attributes and safe modal handlers for untrusted card content. const linksHTML = links .map( (link, index) => ` ` ) .join("") const safeMinterName = qEscapeHtml(minterName) const safeCreator = qEscapeHtml(creator) const safeHeader = qEscapeHtml(header) const safeContent = qEscapeHtml(content).replace(/\n/g, "${safeHeader}
${altText}(click COMMENTS button to open/close card comments)
By: ${safeCreator} - ${safeFormattedDate}