// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. const isEncryptedTestMode = false const encryptedCardIdentifierPrefix = "card-MAC" let isUpdateCard = false let existingDecryptedCardData = {} let existingEncryptedCardIdentifier = {} let cardMinterName = {} let existingCardMinterNames = [] let isTopic = false let attemptLoadAdminDataCount = 0 let adminMemberCount = 0 let adminPublicKeys = [] let kickTransactions = [] let banTransactions = [] console.log("Attempting to load AdminBoard.js") const loadAdminBoardPage = async () => { // Clear existing content on the 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() } } // Add the "Minter Board" content const mainContent = document.createElement("div") mainContent.innerHTML = `
The Admin Board is an encrypted card publishing board to keep track of minter data for the Minter Admins. Any Admin may publish a card, and related data, make comments on existing cards, and vote on existing card data in support or not of the name on the card. It is essentially a 'project management' tool to assist the Minter Admins in keeping track of the data related to minters they are adding/removing from the minter group.
More functionality will be added over time. One of the first features will be the ability to output the existing card data 'decisions', to a json formatted list in order to allow crowetic to run his script easily until the final Mintership proposal changes are completed, and the MINTER group is transferred to 'null'.
Refreshing cards...
" await fetchAllEncryptedCards(true) }) } const cancelPublishButton = document.getElementById("cancel-publish-button") if (cancelPublishButton) { cancelPublishButton.addEventListener("click", async () => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container") encryptedCardsContainer.style.display = "flex"; // Restore visibility const publishCardView = document.getElementById("publish-card-view") publishCardView.style.display = "none"; // Hide the publish form }) } const addLinkButton = document.getElementById("add-link-button") if (addLinkButton) { addLinkButton.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() const isTopicChecked = document.getElementById("topic-checkbox").checked // Pass that boolean to publishEncryptedCard await publishEncryptedCard(isTopicChecked) }) createScrollToTopButton() // await fetchAndValidateAllAdminCards() await updateOrSaveAdminGroupsDataLocally() await fetchAllKicKBanTxData() await fetchAllEncryptedCards() } const fetchAllKicKBanTxData = async () => { const kickTxType = "GROUP_KICK" const banTxType = "GROUP_BAN" const banArray = [banTxType] const kickArray = [kickTxType] banTransactions = await searchTransactions({ txTypes: banArray, address: '', // or whatever address confirmationStatus: 'CONFIRMED', limit: 0, reverse: true, offset: 0, startBlock: 1990000, blockLimit: 0, txGroupId: 0 }); console.warn(`banTxData`, banTransactions) kickTransactions = await searchTransactions({ txTypes: kickArray, address: '', confirmationStatus: 'CONFIRMED', limit: 0, reverse: true, offset: 0, startBlock: 1990000, blockLimit: 0, txGroupId: 0 }); console.warn(`kickTxData`, kickTransactions) } // Example: fetch and save admin public keys and count const updateOrSaveAdminGroupsDataLocally = async () => { try { // Fetch the array of admin public keys const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys() // Build an object containing the count and the array const adminData = { keysCount: verifiedAdminPublicKeys.length, publicKeys: verifiedAdminPublicKeys }; adminPublicKeys = verifiedAdminPublicKeys // Stringify and save to localStorage localStorage.setItem('savedAdminData', JSON.stringify(adminData)) console.log('Admin public keys saved locally:', adminData) } catch (error) { console.error('Error fetching/storing admin public keys:', error) attemptLoadAdminDataCount++ } } const loadOrFetchAdminGroupsData = async () => { try { // Pull the JSON from localStorage const storedData = localStorage.getItem('savedAdminData') if (!storedData && attemptLoadAdminDataCount <= 3) { console.log('No saved admin public keys found in local storage. Fetching...') await updateOrSaveAdminGroupsDataLocally() attemptLoadAdminDataCount++ return null; } // Parse the JSON, then store the global variables. const parsedData = JSON.parse(storedData) adminMemberCount = parsedData.keysCount adminPublicKeys = parsedData.publicKeys console.log(typeof adminPublicKeys); // Should be "object" console.log(Array.isArray(adminPublicKeys)) console.log(`Loaded admins 'keysCount'=${adminMemberCount}, publicKeys=`, adminPublicKeys) attemptLoadAdminDataCount = 0 return parsedData; // and return { adminMemberCount, adminKeys } to the caller } catch (error) { console.error('Error loading/parsing saved admin public keys:', error) return null } } const extractEncryptedCardsMinterName = (cardIdentifier) => { const parts = cardIdentifier.split('-'); // Ensure the format has at least 3 parts if (parts.length < 3) { throw new Error('Invalid identifier format'); } if (parts.slice(2, -1).join('-') === 'TOPIC') { console.log(`TOPIC found in identifier: ${cardIdentifier} - not including in duplicatesList`) return } // Extract minterName (everything from the second part to the second-to-last part) const minterName = parts.slice(2, -1).join('-') // Return the extracted minterName return minterName } const fetchAllEncryptedCards = async (isRefresh = false) => { const encryptedCardsContainer = document.getElementById("encrypted-cards-container") encryptedCardsContainer.innerHTML = "Loading cards...
" try { const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0) if (!response || !Array.isArray(response) || response.length === 0) { encryptedCardsContainer.innerHTML = "No cards found.
" return } // Validate and decrypt cards asynchronously const validatedCards = await Promise.all( response.map(async (card) => { try { // Validate the card identifier const isValid = await validateEncryptedCardIdentifier(card) if (!isValid) return null // Fetch and decrypt the card data const cardDataResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: card.name, service: "MAIL_PRIVATE", identifier: card.identifier, encoding: "base64", }) if (!cardDataResponse) return null const decryptedCardData = await decryptAndParseObject(cardDataResponse) // Skip cards without polls if (!decryptedCardData.poll) return null return { card, decryptedCardData } } catch (error) { console.warn(`Error processing card ${card.identifier}:`, error) return null } }) ) // Filter out invalid or skipped cards const validCardsWithData = validatedCards.filter((entry) => entry !== null) if (validCardsWithData.length === 0) { encryptedCardsContainer.innerHTML = "No valid cards found.
" return; } // Combine `processCards` logic: Deduplicate cards by identifier and keep latest timestamp const latestCardsMap = new Map() validCardsWithData.forEach(({ card, decryptedCardData }) => { const timestamp = card.updated || card.created || 0 const existingCard = latestCardsMap.get(card.identifier) if (!existingCard || timestamp > (existingCard.card.updated || existingCard.card.created || 0)) { latestCardsMap.set(card.identifier, { card, decryptedCardData }) } }) const uniqueValidCards = Array.from(latestCardsMap.values()) // Map to track the most recent card per minterName const mostRecentCardsMap = new Map() uniqueValidCards.forEach(({ card, decryptedCardData }) => { const obtainedMinterName = decryptedCardData.minterName // Only check for cards that are NOT topic-based cards if ((!decryptedCardData.isTopic) || decryptedCardData.isTopic === 'false') { const cardTimestamp = card.updated || card.created || 0 if (obtainedMinterName) { const existingEntry = mostRecentCardsMap.get(obtainedMinterName) // Replace only if the current card is more recent if (!existingEntry || cardTimestamp > (existingEntry.card.updated || existingEntry.card.created || 0)) { mostRecentCardsMap.set(obtainedMinterName, { card, decryptedCardData }) } } } else { console.log(`topic card detected, skipping most recent by name mapping...`) // We still need to add the topic-based cards to the map, as it will be utilized in the next step mostRecentCardsMap.set(obtainedMinterName, {card, decryptedCardData}) } }) // Convert the map into an array of final cards const finalCards = Array.from(mostRecentCardsMap.values()); // Sort cards by timestamp (most recent first) finalCards.sort((a, b) => { const timestampA = a.card.updated || a.card.created || 0 const timestampB = b.card.updated || b.card.created || 0 return timestampB - timestampA; }) encryptedCardsContainer.innerHTML = "" // Display skeleton cards immediately finalCards.forEach(({ card }) => { const skeletonHTML = createSkeletonCardHTML(card.identifier) encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) }) // Fetch poll results and update each card await Promise.all( finalCards.map(async ({ card, decryptedCardData }) => { try { // Validate poll publisher keys const encryptedCardPollPublisherPublicKey = await getPollPublisherPublicKey(decryptedCardData.poll) const encryptedCardPublisherPublicKey = await getPublicKeyByName(card.name) if (encryptedCardPollPublisherPublicKey !== encryptedCardPublisherPublicKey) { console.warn(`QuickMythril cardPollHijack attack detected! Skipping card: ${card.identifier}`) removeSkeleton(card.identifier) return } // Fetch poll results const pollResults = await fetchPollResults(decryptedCardData.poll) if (pollResults?.error) { console.warn(`Skipping card with failed poll results: ${card.identifier}`) removeSkeleton(card.identifier); return; } const encryptedCommentCount = await getEncryptedCommentCount(card.identifier) // Generate final card HTML const finalCardHTML = await createEncryptedCardHTML( decryptedCardData, pollResults, card.identifier, encryptedCommentCount ) replaceSkeleton(card.identifier, finalCardHTML) } catch (error) { console.error(`Error finalizing card ${card.identifier}:`, error) removeSkeleton(card.identifier) } }) ) } catch (error) { console.error("Error loading cards:", error) encryptedCardsContainer.innerHTML = "Failed to load cards.
" } } // Function to create a skeleton card const createEncryptedSkeletonCardHTML = (cardIdentifier) => { return `${header}
${altText}(click COMMENTS button to open/close card comments)
By: ${creator} - ${formattedDate}
${decryptedCommentData.creator} ${adminBadge}
${decryptedCommentData.content}
${timestamp}