Added ACTIONS for INVITE, KICK, and BAN to Minter and Admin Boards.

This commit is contained in:
crowetic 2025-01-08 20:26:24 -08:00
parent 040d3fa184
commit 53cc5a5f2a
3 changed files with 722 additions and 255 deletions

View File

@ -10,6 +10,8 @@ let isTopic = false
let attemptLoadAdminDataCount = 0
let adminMemberCount = 0
let adminPublicKeys = []
let kickTransactions = []
let banTransactions = []
console.log("Attempting to load AdminBoard.js")
@ -104,10 +106,45 @@ const loadAdminBoardPage = async () => {
})
createScrollToTopButton()
// await fetchAndValidateAllAdminCards()
await fetchAllEncryptedCards()
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 {
@ -178,154 +215,6 @@ const extractEncryptedCardsMinterName = (cardIdentifier) => {
return minterName
}
// const processCards = async (validEncryptedCards) => {
// const latestCardsMap = new Map()
// await Promise.all(validEncryptedCards.map(async card => {
// const timestamp = card.updated || card.created || 0
// const existingCard = latestCardsMap.get(card.identifier)
// if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
// latestCardsMap.set(card.identifier, card)
// }
// }))
// console.log(`latestCardsMap, by timestamp`, latestCardsMap)
// const uniqueValidCards = Array.from(latestCardsMap.values())
// return uniqueValidCards
// }
//Main function to load the Minter Cards ----------------------------------------
//TODO verify the latest changes work
// const fetchAllEncryptedCards = async (isRefresh=false) => {
// const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
// encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
// try {
// const response = await searchSimple('MAIL_PRIVATE', `${encryptedCardIdentifierPrefix}`, '', 0)
// if (!response || !Array.isArray(response) || response.length === 0) {
// encryptedCardsContainer.innerHTML = "<p>No cards found.</p>"
// return
// }
// // Validate cards and filter
// const validatedEncryptedCards = await Promise.all(
// response.map(async card => {
// const isValid = await validateEncryptedCardIdentifier(card)
// return isValid ? card : null
// })
// )
// console.log(`validatedEncryptedCards:`, validatedEncryptedCards, `... running next filter...`)
// const validEncryptedCards = validatedEncryptedCards.filter(card => card !== null)
// console.log(`validEncryptedcards:`, validEncryptedCards)
// if (validEncryptedCards.length === 0) {
// encryptedCardsContainer.innerHTML = "<p>No valid cards found.</p>";
// return;
// }
// const finalCards = await processCards(validEncryptedCards)
// console.log(`finalCards:`,finalCards)
// // Display skeleton cards immediately
// encryptedCardsContainer.innerHTML = ""
// finalCards.forEach(card => {
// const skeletonHTML = createSkeletonCardHTML(card.identifier)
// encryptedCardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
// })
// // Fetch and update each card
// finalCards.forEach(async card => {
// try {
// // const hasMinterName = await extractEncryptedCardsMinterName(card.identifier)
// // if (hasMinterName) existingCardMinterNames.push(hasMinterName)
// const cardDataResponse = await qortalRequest({
// action: "FETCH_QDN_RESOURCE",
// name: card.name,
// service: "MAIL_PRIVATE",
// identifier: card.identifier,
// encoding: "base64"
// })
// if (!cardDataResponse) {
// console.warn(`Skipping invalid card: ${JSON.stringify(card)}`)
// removeSkeleton(card.identifier)
// return
// }
// const decryptedCardData = await decryptAndParseObject(cardDataResponse)
// // Skip cards without polls
// if (!decryptedCardData.poll) {
// console.warn(`Skipping card with no poll: ${card.identifier}`)
// removeSkeleton(card.identifier)
// return
// }
// const encryptedCardPollPublisherPublicKey = await getPollPublisherPublicKey(decryptedCardData.poll)
// const encryptedCardPublisherPublicKey = await getPublicKeyByName(card.name)
// if (encryptedCardPollPublisherPublicKey != encryptedCardPublisherPublicKey) {
// console.warn(`QuickMythril cardPollHijack attack found! Not including card with identifier: ${card.identifier}`)
// removeSkeleton(card.identifier)
// return
// }
// // Fetch poll results and discard cards with no results
// const pollResults = await fetchPollResults(decryptedCardData.poll)
// if (pollResults?.error) {
// console.warn(`Skipping card with failed poll results?: ${card.identifier}, poll=${decryptedCardData.poll}`)
// removeSkeleton(card.identifier)
// return
// }
// if (!isRefresh) {
// console.log(`This is a REFRESH, NOT adding names to duplicates list...`)
// const obtainedMinterName = decryptedCardData.minterName
// // if ((obtainedMinterName) && existingCardMinterNames.includes(obtainedMinterName)) {
// // console.warn(`name found in existing names array...${obtainedMinterName} skipping duplicate card...${card.identifier}`)
// // removeSkeleton(card.identifier)
// // return
// // } else if ((obtainedMinterName) && (!existingCardMinterNames.includes(obtainedMinterName))) {
// // existingCardMinterNames.push(obtainedMinterName)
// // console.log(`minterName: ${obtainedMinterName} found, doesn't exist in existing array, added to existingCardMinterNames array`)
// // }
// if (obtainedMinterName && existingCardMinterNames.some(item => item.minterName === obtainedMinterName)) {
// console.warn(`name found in existing names array...${obtainedMinterName} skipping duplicate card...${card.identifier}`)
// removeSkeleton(card.identifier)
// return
// } else if (obtainedMinterName) {
// existingCardMinterNames.push({ minterName: obtainedMinterName, identifier: card.identifier })
// console.log(`Added minterName and identifier to existingCardMinterNames array:`, { minterName: obtainedMinterName, identifier: card.identifier })
// }
// }
// // const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
// 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 processing card ${card.identifier}:`, error)
// removeSkeleton(card.identifier)
// }
// })
// } catch (error) {
// console.error("Error loading cards:", error)
// encryptedCardsContainer.innerHTML = "<p>Failed to load cards.</p>"
// }
// }
const fetchAllEncryptedCards = async (isRefresh = false) => {
const encryptedCardsContainer = document.getElementById("encrypted-cards-container")
encryptedCardsContainer.innerHTML = "<p>Loading cards...</p>"
@ -479,22 +368,6 @@ const fetchAllEncryptedCards = async (isRefresh = false) => {
}
}
//TODO verify that this actually isn't necessary. if not, remove it.
// const removeEncryptedSkeleton = (cardIdentifier) => {
// const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
// if (encryptedSkeletonCard) {
// encryptedSkeletonCard.remove(); // Remove the skeleton silently
// }
// }
// const replaceEncryptedSkeleton = (cardIdentifier, htmlContent) => {
// const encryptedSkeletonCard = document.getElementById(`skeleton-${cardIdentifier}`)
// if (encryptedSkeletonCard) {
// encryptedSkeletonCard.outerHTML = htmlContent;
// }
// }
// Function to create a skeleton card
const createEncryptedSkeletonCardHTML = (cardIdentifier) => {
return `
@ -911,28 +784,44 @@ const processQortalLinkForRendering = async (link) => {
return link
}
const checkAndDisplayRemoveActions = async (adminYes, creator, cardIdentifier) => {
const checkAndDisplayRemoveActions = async (adminYes, name, cardIdentifier) => {
const latestBlockInfo = await getLatestBlockInfo()
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
let minAdminCount = 9
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}`)
}
//TODO verify the above functionality to calculate 40% of MINTER group admins, and use that for minAdminCount
if (isBlockPassed && userState.isMinterAdmin) {
console.warn(`feature trigger has passed, checking for approval requirements`)
const addressInfo = await getNameInfo(name)
const address = addressInfo.owner
const kickApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_KICK")
const banApprovalHtml = await checkGroupApprovalAndCreateButton(address, cardIdentifier, "GROUP_BAN")
if (adminYes >= minAdminCount && userState.isMinterAdmin && !isBlockPassed) {
const removeButtonHtml = createRemoveButtonHtml(creator, cardIdentifier)
return removeButtonHtml
if (kickApprovalHtml) {
return kickApprovalHtml
}
if (banApprovalHtml) {
return banApprovalHtml
}
}
return ''
if (adminYes >= minAdminCount && userState.isMinterAdmin) {
const removeButtonHtml = createRemoveButtonHtml(name, cardIdentifier)
return removeButtonHtml
} else{
return ''
}
}
const createRemoveButtonHtml = (name, cardIdentifier) => {
@ -957,9 +846,12 @@ const createRemoveButtonHtml = (name, cardIdentifier) => {
const handleKickMinter = async (minterName) => {
try {
// Optional block check
let txGroupId = 0
const { height: currentHeight } = await getLatestBlockInfo()
if (currentHeight <= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT) {
console.log(`block height is under the removal featureTrigger height`)
const isBlockPassed = currentHeight >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
if (isBlockPassed) {
console.log(`block height above featureTrigger Height, using group approval method...txGroupId 694`)
txGroupId = 694
}
// Get the minter address from name info
@ -970,68 +862,82 @@ const handleKickMinter = async (minterName) => {
return
}
// The admin public key
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const reason = 'Kicked by Minter Admins'
const fee = 0.01
// Create the raw remove transaction
const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress)
const rawKickTransaction = await createGroupKickTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
// Sign the transaction
const signedKickTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: rawKickTransaction
})
// Process the transaction
const processResponse = await processTransaction(signedKickTransaction)
let txToProcess = signedKickTransaction
if (processResponse?.status === "OK") {
alert(`${minterName}'s KICK transaction has been SUCCESSFULLY PROCESSED. Please WAIT FOR CONFIRMATION...`)
const processKickTx = await processTransaction(txToProcess)
if (typeof processKickTx === 'object') {
console.log("transaction success object:", processKickTx)
alert(`${minterName} kick successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processKickTx)}`)
} else {
alert("Failed to process the removal transaction.")
console.log("transaction raw text response:", processKickTx)
alert(`TxResponse: ${JSON.stringify(processKickTx)}`)
}
} catch (error) {
console.error("Error removing minter:", error)
alert("Error removing minter. Please try again.")
alert(`Error:${error}. Please try again.`)
}
}
const handleBanMinter = async (minterName) => {
try {
let txGroupId = 0
const { height: currentHeight } = await getLatestBlockInfo()
if (currentHeight <= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT) {
console.log(`block height is under the removal featureTrigger height`)
console.log(`block height is under the removal featureTrigger height, using txGroupId 0`)
txGroupId = 0
} else {
console.log(`featureTrigger block is passed, using txGroupId 694`)
txGroupId = 694
}
const minterInfo = await getNameInfo(minterName)
const minterAddress = minterInfo?.owner
if (!minterAddress) {
alert(`No valid address found for minter name: ${minterName}`)
return
}
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const reason = 'Banned by Minter Admins'
const fee = 0.01
const rawBanTransaction = await createGroupBanTransaction(minterAddress, adminPublicKey, 694, minterAddress)
const rawBanTransaction = await createGroupBanTransaction(minterAddress, adminPublicKey, 694, minterAddress, reason, txGroupId, fee)
const signedBanTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: rawBanTransaction
})
const processResponse = await processTransaction(signedBanTransaction)
let txToProcess = signedBanTransaction
if (processResponse?.status === "OK") {
alert(`${minterName}'s BAN transaction has been SUCCESSFULLY PROCESSED. Please WAIT FOR CONFIRMATION...`)
const processedTx = await processTransaction(txToProcess)
if (typeof processedTx === 'object') {
console.log("transaction success object:", processedTx)
alert(`${minterName} BAN successfully issued! Wait for confirmation...Transaction Response: ${JSON.stringify(processedTx)}`)
} else {
alert("Failed to process the removal transaction.")
// fallback string or something
console.log("transaction raw text response:", processedTx)
alert(`transaction response:${JSON.stringify(processedTx)}` )
}
} catch (error) {
console.error("Error removing minter:", error)
alert("Error removing minter. Please try again.")
alert(`Error ${error}. Please try again.`)
}
}
@ -1062,7 +968,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
}
}
const cardColorCode = showTopic ? '#0e1b15' : '#151f28'
let cardColorCode = showTopic ? '#0e1b15' : '#151f28'
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
<div class="support-header"><h5> REGARDING (Topic): </h5></div>
@ -1080,11 +986,30 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
createModal('poll-details')
let showRemoveHtml
let altText
const verifiedName = await validateMinterName(minterName)
if (verifiedName) {
const accountInfo = await getNameInfo(verifiedName)
const accountAddress = accountInfo.owner
console.log(`name is validated, utilizing for removal features...${verifiedName}`)
const removeActionsHtml = await checkAndDisplayRemoveActions(adminYes, verifiedName, cardIdentifier)
showRemoveHtml = removeActionsHtml
if (banTransactions.some((banTx) => banTx.groupId === 694 && banTx.offender === accountAddress)){
console.warn(`account was already banned, displaying as such...`)
cardColorCode = 'rgb(24, 3, 3)'
altText = `<h4 style="color:rgb(106, 2, 2); margin-bottom: 0.5em;">BANNED From MINTER Group</h4>`
showRemoveHtml = ''
}
if (kickTransactions.some((kickTx) => kickTx.groupId === 694 && kickTx.member === accountAddress)){
console.warn(`account was already kicked, displaying as such...`)
cardColorCode = 'rgb(29, 7, 4)'
altText = `<h4 style="color:rgb(143, 117, 21); margin-bottom: 0.5em;">KICKED From MINTER Group</h4>`
showRemoveHtml = ''
}
} else {
console.log(`name could not be validated, assuming topic card (or some other issue with name validation) for removalActions`)
showRemoveHtml = ''
@ -1098,6 +1023,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
<h2>${creator}</h2>
${minterOrTopicHtml}
<p>${header}</p>
${altText}
</div>
<div class="info">
${content}

View File

@ -273,10 +273,10 @@ const loadCards = async () => {
}
const pollResults = await fetchPollResults(cardDataResponse.poll)
const BgColor = generateDarkPastelBackgroundBy(card.name)
const bgColor = generateDarkPastelBackgroundBy(card.name)
const commentCount = await countComments(card.identifier)
const cardUpdatedTime = card.updated || null
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, BgColor)
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, cardUpdatedTime, bgColor)
replaceSkeleton(card.identifier, finalCardHTML)
} catch (error) {
@ -1000,30 +1000,46 @@ const handleInviteMinter = async (minterName) => {
try {
const blockInfo = await getLatestBlockInfo()
const blockHeight = blockInfo.height
if (blockHeight <= MINTER_INVITE_BLOCK_HEIGHT) {
console.log(`block height is under the featureTrigger height`)
}
const minterAccountInfo = await getNameInfo(minterName)
const minterAddress = await minterAccountInfo.owner
const adminPublicKey = await getPublicKeyByName(userState.accountName)
console.log(`about to attempt group invite, minterAddress: ${minterAddress}, adminPublicKey: ${adminPublicKey}`)
const inviteTransaction = await createGroupInviteTransaction(minterAddress, adminPublicKey, 694, minterAddress, 864000, 0)
let adminPublicKey
let txGroupId
if (blockHeight >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT){
if (userState.isMinterAdmin){
adminPublicKey = await getPublicKeyByName(userState.accountName)
txGroupId = 694
}else{
console.warn(`user is not a minter admin, cannot create invite!`)
return
}
}else {
adminPublicKey = await getPublicKeyByName(userState.accountName)
txGroupId = 0
}
const fee = 0.01
const timeToLive = 864000
console.log(`about to attempt group invite, minterAddress: ${minterAddress}, adminPublicKey: ${adminPublicKey}`)
const inviteTransaction = await createGroupInviteTransaction(minterAddress, adminPublicKey, 694, minterAddress, timeToLive, txGroupId, fee)
// Step 2: Sign the transaction using qortalRequest
const signedTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: inviteTransaction
})
// Step 3: Process the transaction
console.warn(`signed transaction`,signedTransaction)
const processResponse = await processTransaction(signedTransaction)
if (processResponse?.status === "OK") {
alert(`${minterName} has been successfully invited, please WAIT FOR CONFIRMATION...`)
if (typeof processResponse === 'object') {
// The successful object might have a "signature" or "type" or "approvalStatus"
console.log("Invite transaction success object:", processResponse)
alert(`${minterName} has been successfully invited! Wait for confirmation...Transaction Response: ${JSON.stringify(processResponse)}`)
} else {
alert("Failed to process the invite transaction.")
// fallback string or something
console.log("Invite transaction raw text response:", processResponse)
alert(`Invite transaction response: ${JSON.stringify(processResponse)}`)
}
} catch (error) {
console.error("Error inviting minter:", error)
alert("Error inviting minter. Please try again.")
@ -1047,8 +1063,37 @@ const createInviteButtonHtml = (creator, cardIdentifier) => {
const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
const latestBlockInfo = await getLatestBlockInfo()
const isBlockPassed = latestBlockInfo.height >= GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT
let minAdminCount
const minterAdmins = await fetchMinterGroupAdmins()
if (adminYes >= 9 && userState.isMinterAdmin && !isBlockPassed) {
if (!isBlockPassed){
console.warn(`feature trigger not passed, using static number for minAdminCount`)
minAdminCount = 9
}
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}`)
} 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}`)
}
if (isBlockPassed) {
const minterAddressInfo = await getNameInfo(creator)
const minterAddress = await minterAddressInfo.owner
if (userState.isMinterAdmin){
let groupApprovalHtml = await checkGroupApprovalAndCreateButton(minterAddress, cardIdentifier, 'GROUP_INVITE')
if (groupApprovalHtml) {
return groupApprovalHtml
}
}else{
console.log(`USER NOT ADMIN, no need for group approval buttons...`)
}
}
if (adminYes >= minAdminCount && userState.isMinterAdmin) {
const inviteButtonHtml = createInviteButtonHtml(creator, cardIdentifier)
console.log(`admin votes over 9, creating invite button...`, adminYes)
return inviteButtonHtml
@ -1057,6 +1102,146 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
return null
}
const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transactionType) => {
const txTypes = [`${transactionType}`]
const confirmationStatus = 'CONFIRMED'
const groupApprovalSearchResults = await searchTransactions(txTypes, address, confirmationStatus, limit, reverse, offset)
const pendingApprovals = groupApprovalSearchResults.filter(tx => tx.approvalStatus === 'PENDING')
if (pendingApprovals) {
console.warn(`pendingApprovals FOUND: ${pendingApprovals}`)
}
if (pendingApprovals.length === 0) {
return
}
const txSig = pendingApprovals[0].signature
if (transactionType === `GROUP_INVITE`){
const approvalButtonHtml = `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="padding: 8px; background:rgb(37, 99, 44); color: #000; border: 1px solid #333; border-radius: 5px; cursor: pointer;"
onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout="this.style.backgroundColor='rgb(37, 99, 44) '"
onclick="handleGroupApproval('${address}','${txSig}')">
Approve Invite
</button>
</div>
`
return approvalButtonHtml
}
if (transactionType === `GROUP_KICK`){
const approvalButtonHtml = `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="padding: 8px; background:rgb(119, 91, 21); color: #000; border: 1px solid #333; border-radius: 5px; cursor: pointer;"
onmouseover="this.style.backgroundColor='rgb(50, 52, 51) '"
onmouseout="this.style.backgroundColor='rgb(119, 91, 21) '"
onclick="handleGroupApproval('${address}','${txSig}')">
Approve Kick
</button>
</div>
`
return approvalButtonHtml
}
if (transactionType === `GROUP_BAN`){
const approvalButtonHtml = `
<div id="approval-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="padding: 8px; background:rgb(54, 7, 7); color: #000; border: 1px solid #333; border-radius: 5px; cursor: pointer;"
onmouseover="this.style.backgroundColor='rgb(50, 52, 51) '"
onmouseout="this.style.backgroundColor='rgb(54, 7, 7) '"
onclick="handleGroupApproval('${address}','${txSig}')">
Approve Kick
</button>
</div>
`
return approvalButtonHtml
}
}
const handleGroupApproval = async (address, pendingApprovalSignature) => {
try{
if (!userState.isMinterAdmin) {
console.warn(`non-admin attempting to sign approval!`)
return
}
const fee = 0.01
const adminPublicKey = await getPublicKeyByName(userState.accountName)
const txGroupId = 694
const rawGroupApprovalTransaction = await createGroupApprovalTransaction(address, adminPublicKey, pendingApprovalSignature, txGroupId, fee)
const signedGroupApprovalTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: rawGroupApprovalTransaction
})
// const switchToBase58 = isBase64(signedGroupApprovalTransaction)
let txToProcess = signedGroupApprovalTransaction
// if (switchToBase58){
// console.warn(`base64 tx found, converting to base58`,signedGroupApprovalTransaction)
// const convertedToHex = await base64ToHex(signedGroupApprovalTransaction)
// const base58TxData = await hexToBase58(convertedToHex)
// txToProcess = base58TxData
// console.log(`base58ConvertedSignedTxData to process:`,txToProcess)
// }
const processGroupApprovalTx = await processTransaction(txToProcess)
if (processGroupApprovalTx) {
alert(`transaction processed, please wait for CONFIRMATION: ${JSON.stringify(processGroupApprovalTx)}`)
} else {
alert(`creating tx failed for some reason`)
}
}catch(error){
console.error(error)
throw error
}
}
const handleJoinGroup = async (minterAddress) => {
try{
if (userState.accountAddress === minterAddress) {
console.log(`minter user found `)
const joinerPublicKey = getPublicKeyFromAddress(minterAddress)
fee = 0.01
const joinGroupTransactionData = await createGroupJoinTransaction(minterAddress, joinerPublicKey, 694, 0, fee)
const signedJoinGroupTransaction = await qortalRequest({
action: "SIGN_TRANSACTION",
unsignedBytes: joinGroupTransactionData
})
let txToProcess = signedJoinGroupTransaction
// const switchToBase58 = isBase64(signedJoinGroupTransaction)
// if (switchToBase58){
// console.warn(`base64 tx found, converting to base58`, signedJoinGroupTransaction)
// const convertedToHex = await base64ToHex(signedJoinGroupTransaction)
// const base58TxData = await hexToBase58(convertedToHex)
// txToProcess = base58TxData
// console.log(`base58ConvertedSignedTxData to process:`,txToProcess)
// }
const processJoinGroupTransaction = await processTransaction(txToProcess)
if (processJoinGroupTransaction){
console.warn(`processed JOIN_GROUP tx`,processJoinGroupTransaction)
alert(`JOIN GROUP Transaction Processed Successfully, please WAIT FOR CONFIRMATION txData: ${JSON.stringify(processJoinGroupTransaction)}`)
}
} else {
console.warn(`user is not the minter`)
return ''
}
} catch(error){
throw error
}
}
const getMinterAvatar = async (minterName) => {
const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`
try {
@ -1076,7 +1261,7 @@ const getMinterAvatar = async (minterName) => {
// Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, BgColor) => {
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, cardUpdatedTime, bgColor) => {
const { header, content, links, creator, timestamp, poll } = cardData
const formattedDate = cardUpdatedTime ? new Date(cardUpdatedTime).toLocaleString() : new Date(timestamp).toLocaleString()
const avatarHtml = await getMinterAvatar(creator)
@ -1093,14 +1278,48 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
createModal('poll-details')
const inviteButtonHtml = await checkAndDisplayInviteButton(adminYes, creator, cardIdentifier)
const inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : ''
let inviteHtmlAdd = (inviteButtonHtml) ? inviteButtonHtml : ''
let finalBgColor = bgColor
let invitedText = "" // for "INVITED" label if found
try {
const minterAddress = await fetchOwnerAddressFromName(creator)
const invites = await fetchGroupInvitesByAddress(minterAddress)
const hasMinterInvite = invites.some((invite) => invite.groupId === 694)
if (hasMinterInvite) {
// If so, override background color & add an "INVITED" label
finalBgColor = "black";
invitedText = `<h4 style="color: gold; margin-bottom: 0.5em;">INVITED</h4>`
if (userState.accountName === creator){ //Check also if the creator is the user, and display the join group button if so.
inviteHtmlAdd = `
<div id="join-button-container-${cardIdentifier}" style="margin-top: 1em;">
<button
style="padding: 8px; background:rgb(37, 99, 44); color: #000; border: 1px solid #333; border-radius: 5px; cursor: pointer;"
onmouseover="this.style.backgroundColor='rgb(25, 47, 39) '"
onmouseout="this.style.backgroundColor='rgb(37, 99, 44) '"
onclick="handleJoinGroup('${userState.accountAddress}')">
Approve Invite
</button>
</div>
`
}else{
console.log(`user is not the minter... displaying no join button`)
inviteHtmlAdd = ''
}
}
//do not display invite button as they're already invited. Create a join button instead.
} catch (error) {
console.error("Error checking invites for user:", error)
}
return `
<div class="minter-card" style="background-color: ${BgColor}">
<div class="minter-card" style="background-color: ${finalBgColor}">
<div class="minter-card-header">
${avatarHtml}
<h3>${creator}</h3>
<p>${header}</p>
${invitedText}
</div>
<div class="support-header"><h5>USER'S POST</h5></div>
<div class="info">

View File

@ -28,6 +28,7 @@ const uid = async () => {
console.log('Generated uid:', result)
return result
}
// a non-async version of the uid function, in case non-async functions need it. Ultimately we can probably remove uid but need to ensure no apps are using it asynchronously first. so this is kept for that purpose for now.
const randomID = () => {
console.log('randomID non-async')
@ -40,6 +41,7 @@ const randomID = () => {
console.log('Generated uid:', result)
return result
}
// Turn a unix timestamp into a human-readable date
const timestampToHumanReadableDate = async(timestamp) => {
const date = new Date(timestamp)
@ -54,6 +56,72 @@ const timestampToHumanReadableDate = async(timestamp) => {
console.log('Formatted date:', formattedDate)
return formattedDate
}
// function to check if something is base58
const isBase58 = (str) => {
if (typeof str !== 'string' || !str.length) return false
// Basic regex for typical Base58 alphabet:
// 1) No zero-like chars (0, O, I, l).
// 2) Should be [1-9A-HJ-NP-Za-km-z].
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]+$/
return base58Regex.test(str)
}
//function to check if something is base64
const isBase64 = (str, attemptDecode = false) => {
if (typeof str !== 'string') return false
// Basic length mod check for classic Base64
if (str.length % 4 !== 0) {
return false
}
// Regex for valid Base64 chars + optional = padding
const base64Regex = /^[A-Za-z0-9+/]*(={1,2})?$/
if (!base64Regex.test(str)) {
return false
}
if (attemptDecode) {
try {
// In browser, atob can throw if invalid
atob(str)
} catch {
return false
}
}
return true
}
const base64ToHex = async (base64 = 'string') => {
try {
const response = await fetch (`${baseUrl}/utils/frombase64`, {
headers: { 'Accept': 'text/plain' },
method: 'GET',
body: base64
})
const hex = await response.text()
return hex
}catch(error){
throw error
}
}
const hexToBase58 = async (hex = 'string') => {
try {
const response = await fetch (`${baseUrl}/utils/tobase58`, {
headers: { 'Accept': 'text/plain' },
method: 'GET',
body: hex
})
const base58 = await response.text()
return base58
}catch(error){
throw error
}
}
// Base64 encode a string
const base64EncodeString = async (str) => {
const encodedString = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(str).buffer)))
@ -260,9 +328,14 @@ const getNameInfo = async (name) => {
if (!response.ok) {
console.warn(`Failed to fetch name info for: ${name}, status: ${response.status}`)
return null
}
}
const data = await response.json()
if (!data.name) {
console.warn(`no name info returned, is this not a real registeredName? ${data.name} - returning null to bypass errors...`)
return null
}
console.log('Fetched name info:', data)
return {
name: data.name,
@ -514,6 +587,35 @@ const fetchAdminGroupsMembersPublicKeys = async () => {
}
}
const fetchGroupInvitesByAddress = async (address) => {
try {
const response = await fetch(`${baseUrl}/groups/invites/${encodeURIComponent(address)}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
})
if (!response.ok) {
// Not a 2xx status; read error details
const errorText = await response.text()
throw new Error(`Failed to fetch group invites: HTTP ${response.status}, ${errorText}`)
}
// Attempt to parse the JSON response
const invites = await response.json()
// Example check: ensures the result is an array
if (!Array.isArray(invites)) {
throw new Error('Group invites response is not an array as expected.')
}
return invites // e.g. [{ groupId, inviter, invitee, expiry }, ...]
} catch (error) {
console.error('Error fetching address group invites:', error)
throw error
}
}
// QDN data calls --------------------------------------------------------------------------------------------------
const searchLatestDataByIdentifier = async (identifier) => {
@ -1177,32 +1279,60 @@ const voteYesOnPoll = async (poll) => {
// Qortal Transaction-related calls ---------------------------------------------------------------------------
const processTransaction = async (rawTransaction) => {
const processTransaction = async (signedTransaction) => {
try {
const response = await fetch(`${baseUrl}/transactions/process`, {
method: 'POST',
headers: {
'Accept': 'text/plain',
'X-API-VERSION': '2',
'Content-Type': 'text/plain'
},
body: rawTransaction
})
const response = await fetch(`${baseUrl}/transactions/process`, {
method: 'POST',
headers: {
'Accept': 'text/plain', // or 'application/json' if the API states so
'X-API-VERSION': '2', // version 2
'Content-Type': 'text/plain'
},
body: signedTransaction
})
if (!response.ok) throw new Error(`Transaction processing failed: ${response.status}`)
if (!response.ok) {
// On error, read the text so we can see the error details
const errorText = await response.text();
throw new Error(`Transaction processing failed: ${errorText}`)
}
const result = await response.text()
console.log("Transaction successfully processed:", result)
// Check the content type to see how to parse
const contentType = response.headers.get('Content-Type') || ''
// If the core actually sets Content-Type: application/json
if (contentType.includes('application/json')) {
// We can do .json()
const result = await response.json();
console.log("Transaction processed, got JSON:", result);
return result
} else {
// The core returns raw text that is actually JSON
const rawText = await response.text();
console.log("Raw text from server (version 2 means JSON string in text):", rawText)
// Attempt to parse if its indeed JSON
let parsed;
try {
parsed = JSON.parse(rawText);
} catch {
// If it's not valid JSON, we can at least return the raw text
console.warn("Server returned non-JSON text (version 2 mismatch?).")
return rawText
}
return parsed
}
} catch (error) {
console.error("Error processing transaction:", error)
throw error
console.error("Error processing transaction:", error)
throw error
}
}
}
// Create a group invite transaction. This will utilize a default timeToLive (which is how long the tx will be alive, not the time until it IS live...) of 10 days in seconds, as the legacy UI has a bug that doesn't display invites older than 10 days.
// We will also default to the MINTER group for groupId, AFTER the GROUP_APPROVAL changes, the txGroupId will need to be set for tx that require approval.
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive = 864000, txGroupId = 0, fee=0.01) => {
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive, txGroupId, fee) => {
try {
// Fetch account reference correctly
@ -1210,20 +1340,20 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr
const accountReference = accountInfo.reference
// Validate inputs before making the request
if (!adminPublicKey || !accountReference || !recipientAddress) {
if (!adminPublicKey || !accountReference) {
throw new Error("Missing required parameters for group invite transaction.")
}
const payload = {
timestamp: Date.now(),
reference: accountReference,
fee: fee || 0.01,
txGroupId: txGroupId,
recipient: recipientAddress,
adminPublicKey: adminPublicKey,
groupId: groupId,
fee,
txGroupId: txGroupId || 0,
recipient: null,
adminPublicKey,
groupId,
invitee: invitee || recipientAddress,
timeToLive: timeToLive
timeToLive
}
console.log("Sending group invite transaction payload:", payload)
@ -1251,7 +1381,7 @@ const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, gr
}
}
const createGroupKickTransaction = async (recipientAddress, adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId = 0, fee=0.01) => {
const createGroupKickTransaction = async (recipientAddress, adminPublicKey, groupId=694, member, reason='Kicked by admins', txGroupId, fee) => {
try {
// Fetch account reference correctly
@ -1266,16 +1396,16 @@ const createGroupKickTransaction = async (recipientAddress, adminPublicKey, grou
const payload = {
timestamp: Date.now(),
reference: accountReference,
fee: fee | 0.01,
txGroupId: txGroupId,
recipient: recipientAddress,
adminPublicKey: adminPublicKey,
fee,
txGroupId,
recipient: null,
adminPublicKey,
groupId: groupId,
member: member || recipientAddress,
reason: reason
}
console.log("Sending group invite transaction payload:", payload)
console.log("Sending GROUP_KICK transaction payload:", payload)
const response = await fetch(`${baseUrl}/groups/kick`, {
method: 'POST',
@ -1295,12 +1425,109 @@ const createGroupKickTransaction = async (recipientAddress, adminPublicKey, grou
console.log("Raw unsigned transaction created:", rawTransaction)
return rawTransaction
} catch (error) {
console.error("Error creating group invite transaction:", error)
console.error("Error creating GROUP_KICK transaction:", error)
throw error
}
}
const createGroupBanTransaction = async (recipientAddress, adminPublicKey, groupId=694, offender, reason='Banned by admins', txGroupId = 0, fee=0.01) => {
const createGroupApprovalTransaction = async (recipientAddress, adminPublicKey, pendingApprovalSignature, txGroupId=694, fee=0.01) => {
try {
// Fetch account reference correctly
const accountInfo = await getAddressInfo(recipientAddress)
const accountReference = accountInfo.reference
// Validate inputs before making the request
if (!adminPublicKey || !accountReference || !recipientAddress) {
throw new Error("Missing required parameters for group invite transaction.")
}
const payload = {
timestamp: Date.now(),
reference: accountReference,
fee,
txGroupId,
recipient: null,
adminPublicKey,
pendingApprovalSignature,
approval: true
}
console.log("Sending GROUP_APPROVAL transaction payload:", payload)
const response = await fetch(`${baseUrl}/groups/approval`, {
method: 'POST',
headers: {
'Accept': 'text/plain',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`)
}
const rawTransaction = await response.text()
console.log("Raw unsigned transaction created:", rawTransaction)
return rawTransaction
} catch (error) {
console.error("Error creating GROUP_APPROVAL transaction:", error)
throw error
}
}
const createGroupBanTransaction = async (recipientAddress, adminPublicKey, groupId=694, offender, reason='Banned by admins', txGroupId, fee) => {
try {
// Fetch account reference correctly
const accountInfo = await getAddressInfo(recipientAddress)
const accountReference = accountInfo.reference
// Validate inputs before making the request
if (!adminPublicKey || !accountReference || !recipientAddress) {
throw new Error("Missing required parameters for group invite transaction.")
}
const payload = {
timestamp: Date.now(),
reference: accountReference,
fee,
txGroupId,
recipient: null,
adminPublicKey,
groupId,
offender,
reason,
}
console.log("Sending GROUP_BAN transaction payload:", payload)
const response = await fetch(`${baseUrl}/groups/ban`, {
method: 'POST',
headers: {
'Accept': 'text/plain',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to create transaction: ${response.status}, ${errorText}`)
}
const rawTransaction = await response.text()
console.log("Raw unsigned transaction created:", rawTransaction)
return rawTransaction
} catch (error) {
console.error("Error creating GROUP_BAN transaction:", error)
throw error
}
}
const createGroupJoinTransaction = async (recipientAddress, joinerPublicKey, groupId, txGroupId = 0, fee) => {
try {
// Fetch account reference correctly
@ -1317,16 +1544,14 @@ const createGroupBanTransaction = async (recipientAddress, adminPublicKey, group
reference: accountReference,
fee: fee | 0.01,
txGroupId: txGroupId,
recipient: recipientAddress,
adminPublicKey: adminPublicKey,
recipient: null,
joinerPublicKey,
groupId: groupId,
offender: offender || recipientAddress,
reason: reason
}
console.log("Sending group invite transaction payload:", payload)
console.log("Sending GROUP_JOIN transaction payload:", payload)
const response = await fetch(`${baseUrl}/groups/kick`, {
const response = await fetch(`${baseUrl}/groups/join`, {
method: 'POST',
headers: {
'Accept': 'text/plain',
@ -1344,7 +1569,7 @@ const createGroupBanTransaction = async (recipientAddress, adminPublicKey, group
console.log("Raw unsigned transaction created:", rawTransaction)
return rawTransaction
} catch (error) {
console.error("Error creating group invite transaction:", error)
console.error("Error creating GROUP_JOIN transaction:", error)
throw error
}
}
@ -1392,6 +1617,103 @@ const getLatestBlockInfo = async () => {
return null
}
}
// ALL QORTAL TRANSACTION TYPES BELOW
// 'GENESIS','PAYMENT','REGISTER_NAME','UPDATE_NAME','SELL_NAME','CANCEL_SELL_NAME','BUY_NAME','CREATE_POLL',
// 'VOTE_ON_POLL','ARBITRARY','ISSUE_ASSET','TRANSFER_ASSET','CREATE_ASSET_ORDER','CANCEL_ASSET_ORDER','MULTI_PAYMENT',
// 'DEPLOY_AT','MESSAGE','CHAT','PUBLICIZE','AIRDROP','AT','CREATE_GROUP','UPDATE_GROUP','ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN',
// 'GROUP_BAN','CANCEL_GROUP_BAN','GROUP_KICK','GROUP_INVITE','CANCEL_GROUP_INVITE','JOIN_GROUP','LEAVE_GROUP','GROUP_APPROVAL',
// 'SET_GROUP','UPDATE_ASSET','ACCOUNT_FLAGS','ENABLE_FORGING','REWARD_SHARE','ACCOUNT_LEVEL','TRANSFER_PRIVS','PRESENCE'
const searchTransactions = async ({
txTypes = [],
address,
confirmationStatus = 'CONFIRMED',
limit = 20,
reverse = true,
offset = 0,
startBlock = 0,
blockLimit = 0,
txGroupId = 0,
} = {}) => {
try {
// 1) Build the query string
const queryParams = [];
// Add each txType as multiple "txType=..." params
txTypes.forEach(type => {
queryParams.push(`txType=${encodeURIComponent(type)}`);
});
// If startBlock is nonzero, push "startBlock=..."
if (startBlock) {
queryParams.push(`startBlock=${encodeURIComponent(startBlock)}`);
}
// If blockLimit is nonzero, push "blockLimit=..."
if (blockLimit) {
queryParams.push(`blockLimit=${encodeURIComponent(blockLimit)}`);
}
// If txGroupId is nonzero, push "txGroupId=..."
if (txGroupId) {
queryParams.push(`txGroupId=${encodeURIComponent(txGroupId)}`);
}
// Address
if (address) {
queryParams.push(`address=${encodeURIComponent(address)}`);
}
// Confirmation status
if (confirmationStatus) {
queryParams.push(`confirmationStatus=${encodeURIComponent(confirmationStatus)}`);
}
// Limit (if you want to explicitly pass limit=0, consider whether to skip or not)
if (limit !== undefined) {
queryParams.push(`limit=${limit}`);
}
// Reverse
if (reverse !== undefined) {
queryParams.push(`reverse=${reverse}`);
}
// Offset
if (offset) {
queryParams.push(`offset=${offset}`);
}
const queryString = queryParams.join('&');
const url = `${baseUrl}/transactions/search?${queryString}`;
// 2) Fetch
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': '*/*'
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to search transactions: HTTP ${response.status}, ${errorText}`);
}
// 3) Parse JSON
const txArray = await response.json();
// Check if the response is indeed an array of transactions
if (!Array.isArray(txArray)) {
throw new Error("Expected an array of transactions, but got something else.");
}
return txArray; // e.g. [{ type, timestamp, reference, ... }, ...]
} catch (error) {
console.error("Error in searchTransactions:", error);
throw error;
}
};