Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
b2dde1ea56 | |||
5630f80a54 | |||
59bd5cc760 | |||
6f459d7e0a | |||
fe230a91d3 | |||
5443d159b0 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
/.vscode
|
|
||||||
/.sync*
|
|
Binary file not shown.
Binary file not shown.
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
### Q-Mintership-Alpha
|
||||||
|
|
||||||
|
Q-Mintership-Alpha is the currently utilized version of the Q-Mintership app published on qortal://APP/Q-Mintership.
|
||||||
|
|
||||||
|
As of Feb 27 2025 Q-Mintership-Alpha is still the published and utilized version of the app.
|
||||||
|
|
||||||
|
#### Q-Mintership's 'MinterBoard'
|
||||||
|
|
||||||
|
The MinterBoard of Q-Mintership, is the primary location for users to publish 'cards' with information about themselves, links to things they have published on QDN, etc... and obtain minting rights from the Minter Admins.
|
||||||
|
|
||||||
|
- Cards are created by any non-minter (either previous minter no longer in the group, or new accounts without minting rights).
|
||||||
|
- Existing community members, existing minters, and Minter Admins, can vote/comment on the cards.
|
||||||
|
- Once a card has obtained the minimum required number of admin votes (40% of the Minter Admin count), the card will then display additional features to those that have the rights to see them. (Minter Admins, and 'Forum Admins', however 'Forum Admins' cannot actually make use of the functionality, they are only able to view it for development purposes.)
|
||||||
|
- The Minter Admins then initiate a PENDING GROUP_INVITE transaction.
|
||||||
|
- The Minter Admins are then able to issue GROUP_APPROVAL transactions to approve the invite.
|
||||||
|
- Once the required number of GROUP_APPROVAL transactions have been created, the GROUP_INVITE is no longer pending, and is active.
|
||||||
|
- The would-be minter that published a card, can then see a new 'JOIN_GROUP' button on their card upon returning to the MinterBoard.
|
||||||
|
- The user will then JOIN_GROUP to the MINTER group, ID 694. Thus allowing the ability to mint.
|
||||||
|
- Upon joining the MINTER group, the user will then have the ability to create a MINTING KEY (the same way as it was created prior to the Mintership concept, however now it no longer requires users to be level 1, only requirement now is to be part of the MINTER group)
|
||||||
|
- User then assigns their key to their node, and starts minting.
|
||||||
|
|
||||||
|
#### Q-Mintership 'AdminBoard'
|
||||||
|
|
||||||
|
- The AdminBoard is a separate board, encrypted to admins only, meant to be utilized for private decision-making between the admins.
|
||||||
|
- The AdminBoard was also adapted to allow REMOVAL of MINTER group members, via GROUP_APPROVAL from the Minter Admins.
|
||||||
|
- The REMOVAL functionality, at the moment, is private. Meaning only the admins that have access to the AdminBoard, can see the data. This will be changed in the future, and a new location where the data will be able to be seen publicly, will be created.
|
||||||
|
|
||||||
|
|
||||||
|
#### Q-Mintership Forum
|
||||||
|
|
||||||
|
- The Forum portion of Q-Mintership is a public (and private) forum, allowing communications to take place in the fashion of long-term forum messages, replies, etc.
|
||||||
|
- Publishing of images with previews, and various 'attachments' with data is also possible on the forum.
|
||||||
|
- The forum has two public rooms by default, and one private room. General and Minter rooms are public, and Admin room is private.
|
||||||
|
- The forum will be getting extensive updates in the future, and the Minter room will be made a publicly VIEWABLE room, but only able to be published to by MINTERS.
|
||||||
|
|
||||||
|
|
||||||
|
#### Q-Mintership MAM Board
|
||||||
|
|
||||||
|
- The MAM Board (or ARBoard in the code) is built to allow the adding and removal of Minter Admins from the MINTER group. Proposals for additions or removals of certain accounts from and to the Minter takes place here.
|
||||||
|
- This board also displays a list of the current Minter Admins, and has the ability to propose a removal of that user with a propose removal button.
|
||||||
|
|
||||||
|
|
||||||
|
#### Additional
|
||||||
|
|
||||||
|
Many additional features and functions are planned for Q-Mintership, and increased performance and more will be added as time goes on.
|
||||||
|
|
||||||
|
Longer-term the plan is to re-write the app into React+TypeScript, which will make it MUCH faster and able to accommodate much more, with a component-based development style similar to that of the other React-based applications on Qortal (Q-Tube, Q-Blog, Q-Mail, etc.)
|
||||||
|
|
||||||
|
A fully featured data viewer and explorer function will be built into Q-Mintership in the future, along with a comprehensive notification system, and more.
|
@ -584,7 +584,7 @@ body {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 20px auto; /* center horizontally */
|
margin: 20px auto; /* center horizontally */
|
||||||
max-width: 600px; /* limit width */
|
/* max-width: 600px; */
|
||||||
color: #ddd; /* text color */
|
color: #ddd; /* text color */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -596,7 +596,7 @@ body {
|
|||||||
background-color:#000000;
|
background-color:#000000;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
color: #4d0000;
|
color: #fff3f3;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* you could style the list items or bullet if you like */
|
/* you could style the list items or bullet if you like */
|
||||||
@ -616,7 +616,17 @@ body {
|
|||||||
background-color: #14161a;
|
background-color: #14161a;
|
||||||
border: 1px solid #8caeb0;
|
border: 1px solid #8caeb0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #5c0101;
|
color: #f19c9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-form input.invite-input {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 2;
|
||||||
|
background-color: #14161a;
|
||||||
|
border: 1px solid #8caeb0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #dddddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-card-button {
|
.publish-card-button {
|
||||||
@ -707,6 +717,43 @@ body {
|
|||||||
background-color: #281e1e;
|
background-color: #281e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.approve-invite-list-button {
|
||||||
|
background-color: rgba(32, 88, 34, 0.554);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 1vw;
|
||||||
|
padding: 1vh,2vh;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approve-invite-list-button:hover {
|
||||||
|
background-color: rgba(34, 186, 47, 0.84); /* a darker variant */
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-approvals strong {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-item {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
background-color: rgba(31, 31, 31, 0.595);
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top row: use flex for horizontal arrangement */
|
||||||
|
.invite-top-row {
|
||||||
|
display: flex;
|
||||||
|
background-color:#173c52ae;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
@ -33,7 +33,7 @@ const loadMinterAdminToolsPage = async () => {
|
|||||||
<div id="tools-submenu" class="tools-submenu">
|
<div id="tools-submenu" class="tools-submenu">
|
||||||
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
|
<div class="tools-buttons" style="display: flex; gap: 1em; justify-content: center;">
|
||||||
<button id="toggle-blocklist-button" class="publish-card-button">Add/Remove blockedUsers</button>
|
<button id="toggle-blocklist-button" class="publish-card-button">Add/Remove blockedUsers</button>
|
||||||
<button id="create-group-invite" class="publish-card-button">Create Pending Group Invite</button>
|
<button id="create-group-invite" class="publish-card-button" style="backgroundColor:rgb(82, 114, 145)">Create and Display Pending Group Invites</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tools-window" class="tools-window" style="margin-top: 2em;">
|
<div id="tools-window" class="tools-window" style="margin-top: 2em;">
|
||||||
@ -56,6 +56,30 @@ const loadMinterAdminToolsPage = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="invite-container" class="invite-form" style="display: none; flex-direction: column; padding: 0.75em; align-items: center; justify-content: center;">
|
||||||
|
|
||||||
|
<!-- Existing pending invites display -->
|
||||||
|
<div id="pending-invites-display" class="pending-invites-display" style="margin-bottom: 1em;">
|
||||||
|
<!-- We will fill this dynamically with a list/table of pending invites -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input for name/address -->
|
||||||
|
<h3 style="margin-top: 0;">Manual Group Invite</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="invite-input"
|
||||||
|
class="invite-input"
|
||||||
|
placeholder="Enter name or address to invite"
|
||||||
|
style="margin-bottom: 1em;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Button to create the invite transaction -->
|
||||||
|
<div class="invite-button-container publish-card-form">
|
||||||
|
<button id="invite-user-button" class="publish-card-button">Invite User</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,10 +87,10 @@ const loadMinterAdminToolsPage = async () => {
|
|||||||
|
|
||||||
document.body.appendChild(mainContent)
|
document.body.appendChild(mainContent)
|
||||||
|
|
||||||
addToolsPageEventListeners()
|
await addToolsPageEventListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToolsPageEventListeners() {
|
const addToolsPageEventListeners= async () => {
|
||||||
document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
|
document.getElementById("toggle-blocklist-button").addEventListener("click", async () => {
|
||||||
const container = document.getElementById("blocklist-container")
|
const container = document.getElementById("blocklist-container")
|
||||||
// toggle show/hide
|
// toggle show/hide
|
||||||
@ -116,6 +140,32 @@ function addToolsPageEventListeners() {
|
|||||||
alert(`"${nameToRemove}" removed from the block list (if it was present).`)
|
alert(`"${nameToRemove}" removed from the block list (if it was present).`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.getElementById("invite-user-button").addEventListener("click", async () => {
|
||||||
|
const inviteInput = document.getElementById("invite-input")
|
||||||
|
const nameOrAddress = inviteInput.value.trim()
|
||||||
|
if (!nameOrAddress) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// We'll call some function handleManualInvite(nameOrAddress)
|
||||||
|
await handleManualInvite(nameOrAddress)
|
||||||
|
inviteInput.value = ""
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error inviting user:", err)
|
||||||
|
alert("Failed to invite user.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById("create-group-invite").addEventListener("click", async () => {
|
||||||
|
const inviteContainer = document.getElementById("invite-container")
|
||||||
|
// Toggle display
|
||||||
|
inviteContainer.style.display = (inviteContainer.style.display === "none" ? "flex" : "none")
|
||||||
|
// If showing, load the pending invites
|
||||||
|
if (inviteContainer.style.display === "flex") {
|
||||||
|
const pendingInvites = await fetchPendingInvites()
|
||||||
|
await displayPendingInviteDetails(pendingInvites)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayBlockList = (blockedNames) => {
|
const displayBlockList = (blockedNames) => {
|
||||||
@ -131,4 +181,139 @@ const displayBlockList = (blockedNames) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPendingInvites = async () => {
|
||||||
|
try {
|
||||||
|
const { finalInviteTxs, pendingInviteTxs } = await fetchAllInviteTransactions()
|
||||||
|
return pendingInviteTxs
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching pending invites:", err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleManualInvite = async (nameOrAddress) => {
|
||||||
|
const addressInfo = await getAddressInfo(nameOrAddress)
|
||||||
|
let address = addressInfo.address
|
||||||
|
if (addressInfo && address) {
|
||||||
|
console.log(`address is ${address}`)
|
||||||
|
} else {
|
||||||
|
// it might be a Qortal name => getNameInfo
|
||||||
|
const nameData = await getNameInfo(nameOrAddress)
|
||||||
|
if (!nameData || !nameData.owner) {
|
||||||
|
throw new Error(`Cannot find valid address for ${nameOrAddress}`)
|
||||||
|
}
|
||||||
|
address = nameData.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminPublicKey = await getPublicKeyByName(userState.accountName)
|
||||||
|
const timeToLive = 864000 // e.g. 10 days in seconds
|
||||||
|
const fee = 0.01
|
||||||
|
let txGroupId = 694
|
||||||
|
|
||||||
|
// build the raw invite transaction
|
||||||
|
const rawInviteTransaction = await createGroupInviteTransaction(
|
||||||
|
address,
|
||||||
|
adminPublicKey,
|
||||||
|
694,
|
||||||
|
address,
|
||||||
|
timeToLive,
|
||||||
|
txGroupId,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
|
||||||
|
// sign
|
||||||
|
const signedTransaction = await qortalRequest({
|
||||||
|
action: "SIGN_TRANSACTION",
|
||||||
|
unsignedBytes: rawInviteTransaction
|
||||||
|
})
|
||||||
|
if (!signedTransaction) {
|
||||||
|
throw new Error("SIGN_TRANSACTION returned null. Possibly user canceled or an older UI?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// process
|
||||||
|
const processResponse = await processTransaction(signedTransaction)
|
||||||
|
if (!processResponse) {
|
||||||
|
throw new Error("Failed to process transaction. Possibly canceled or error from Qortal Core.")
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`Invite transaction submitted for ${nameOrAddress}. Wait for confirmation.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const displayPendingInviteDetails = async (pendingInvites) => {
|
||||||
|
const invitesContainer = document.getElementById('pending-invites-display')
|
||||||
|
if (!pendingInvites || pendingInvites.length === 0) {
|
||||||
|
invitesContainer.innerHTML = "<p>No pending invites found.</p>"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `<h4>Current Pending Invites:</h4><div class="pending-invites-list">`
|
||||||
|
|
||||||
|
for (const inviteTx of pendingInvites) {
|
||||||
|
const inviteeAddress = inviteTx.invitee
|
||||||
|
const dateStr = new Date(inviteTx.timestamp).toLocaleString()
|
||||||
|
let inviteeName = ""
|
||||||
|
const txSig = inviteTx.signature
|
||||||
|
const creatorName = await getNameFromAddress(inviteTx.creatorAddress)
|
||||||
|
if (!creatorName) {
|
||||||
|
creatorName = inviteTx.creatorAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// fetch the name from address, if it fails we keep it blank or fallback to the address
|
||||||
|
inviteeName = await getNameFromAddress(inviteeAddress)
|
||||||
|
if (!inviteeName || inviteeName === inviteeAddress) {
|
||||||
|
inviteeName = inviteeAddress // fallback
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
inviteeName = inviteeAddress // fallback if getName fails
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvalSearchResults = await searchTransactions({
|
||||||
|
txTypes: ['GROUP_APPROVAL'],
|
||||||
|
confirmationStatus: 'CONFIRMED',
|
||||||
|
limit: 0,
|
||||||
|
reverse: false,
|
||||||
|
offset: 0,
|
||||||
|
startBlock: 1990000,
|
||||||
|
blockLimit: 0,
|
||||||
|
txGroupId: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const approvals = approvalSearchResults.filter(
|
||||||
|
(approvalTx) => approvalTx.pendingSignature === txSig
|
||||||
|
)
|
||||||
|
|
||||||
|
const { tableHtml, approvalCount = approvals.length } = await buildApprovalTableHtml(approvals, getNameFromAddress)
|
||||||
|
const finalTable = approvals.length > 0 ? tableHtml : "<p>No Approvals Found</p>"
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="invite-item">
|
||||||
|
<div class="invite-top-row">
|
||||||
|
<span><strong>Invite Tx</strong>:<p style="color:lightblue"> ${inviteTx.signature.slice(0, 8)}...</p></span>
|
||||||
|
<span> <strong>Invitee</strong>:<p style="color:lightblue"> ${inviteeName}</p></span>
|
||||||
|
<span> <strong>Date</strong>:<p style="color:lightblue"> ${dateStr}</p></span>
|
||||||
|
<span> <strong>CreatorName</strong>:<p style="color:lightblue"> ${creatorName}</p></span>
|
||||||
|
<span> <strong>Total Approvals</strong>:<p style="color:lightblue"> ${approvalCount}</p></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Next line for approvals -->
|
||||||
|
<div class="invite-approvals">
|
||||||
|
<strong>Existing Approvals:</strong>
|
||||||
|
${finalTable}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="approve-invite-list-button"
|
||||||
|
onclick="handleGroupApproval('${inviteTx.signature}')"
|
||||||
|
>
|
||||||
|
Approve Invite
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</div>"
|
||||||
|
invitesContainer.innerHTML = html
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 2012800 //TODO update this to corr
|
|||||||
let featureTriggerPassed = false
|
let featureTriggerPassed = false
|
||||||
let isApproved = false
|
let isApproved = false
|
||||||
|
|
||||||
|
let cachedMinterAdmins
|
||||||
|
let cachedMinterGroup
|
||||||
|
|
||||||
const loadMinterBoardPage = async () => {
|
const loadMinterBoardPage = async () => {
|
||||||
// Clear existing content on the page
|
// Clear existing content on the page
|
||||||
@ -207,11 +209,42 @@ const loadMinterBoardPage = async () => {
|
|||||||
await loadCards(minterCardIdentifierPrefix)
|
await loadCards(minterCardIdentifierPrefix)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//Initialize Minter Group and Admin Group
|
||||||
|
await initializeCachedGroups()
|
||||||
|
|
||||||
await featureTriggerCheck()
|
await featureTriggerCheck()
|
||||||
await loadCards(minterCardIdentifierPrefix)
|
await loadCards(minterCardIdentifierPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initializeCachedGroups = async () => {
|
||||||
|
try {
|
||||||
|
const [minterGroup, minterAdmins] = await Promise.all([
|
||||||
|
fetchMinterGroupMembers(),
|
||||||
|
fetchMinterGroupAdmins()
|
||||||
|
])
|
||||||
|
cachedMinterGroup = minterGroup
|
||||||
|
cachedMinterAdmins = minterAdmins
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing cached groups:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const runWithConcurrency = async (tasks, concurrency = 5) => {
|
||||||
|
const results = []
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
const workers = new Array(concurrency).fill(null).map(async () => {
|
||||||
|
while (index < tasks.length) {
|
||||||
|
const currentIndex = index++
|
||||||
|
const task = tasks[currentIndex]
|
||||||
|
results[currentIndex] = await task()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(workers)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
const extractMinterCardsMinterName = async (cardIdentifier) => {
|
const extractMinterCardsMinterName = async (cardIdentifier) => {
|
||||||
// Ensure the identifier starts with the prefix
|
// Ensure the identifier starts with the prefix
|
||||||
@ -428,75 +461,59 @@ const processARBoardCards = async (allValidCards) => {
|
|||||||
|
|
||||||
//Main function to load the Minter Cards ----------------------------------------
|
//Main function to load the Minter Cards ----------------------------------------
|
||||||
const loadCards = async (cardIdentifierPrefix) => {
|
const loadCards = async (cardIdentifierPrefix) => {
|
||||||
|
if ((!cachedMinterGroup || cachedMinterGroup.length === 0) || (!cachedMinterAdmins || cachedMinterAdmins.length === 0)) {
|
||||||
|
await initializeCachedGroups()
|
||||||
|
}
|
||||||
const cardsContainer = document.getElementById("cards-container")
|
const cardsContainer = document.getElementById("cards-container")
|
||||||
let isARBoard = false
|
|
||||||
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
cardsContainer.innerHTML = "<p>Loading cards...</p>"
|
||||||
|
|
||||||
const counterSpan = document.getElementById("board-card-counter")
|
const counterSpan = document.getElementById("board-card-counter")
|
||||||
|
if (counterSpan) counterSpan.textContent = "(loading...)"
|
||||||
|
|
||||||
if (counterSpan) {
|
const isARBoard = cardIdentifierPrefix.startsWith("QM-AR-card")
|
||||||
// Clear or show "Loading..."
|
|
||||||
counterSpan.textContent = "(loading...)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cardIdentifierPrefix.startsWith("QM-AR-card")) {
|
|
||||||
isARBoard = true
|
|
||||||
console.warn(`ARBoard determined:`, isARBoard)
|
|
||||||
}
|
|
||||||
let afterTime = 0
|
|
||||||
const timeRangeSelect = document.getElementById("time-range-select")
|
|
||||||
|
|
||||||
const showExistingCheckbox = document.getElementById("show-existing-checkbox")
|
const showExistingCheckbox = document.getElementById("show-existing-checkbox")
|
||||||
const showExisting = showExistingCheckbox && showExistingCheckbox.checked
|
const showExisting = showExistingCheckbox && showExistingCheckbox.checked
|
||||||
|
|
||||||
|
let afterTime = 0
|
||||||
|
const timeRangeSelect = document.getElementById("time-range-select")
|
||||||
if (timeRangeSelect) {
|
if (timeRangeSelect) {
|
||||||
const days = parseInt(timeRangeSelect.value, 10)
|
const days = parseInt(timeRangeSelect.value, 10)
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const dayMs = 24 * 60 * 60 * 1000
|
afterTime = now - days * 24 * 60 * 60 * 1000
|
||||||
afterTime = now - days * dayMs // e.g. last X days
|
|
||||||
console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Fetch raw "BLOG_POST" entries
|
const rawResults = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
|
||||||
const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime)
|
if (!rawResults || !Array.isArray(rawResults) || rawResults.length === 0) {
|
||||||
|
|
||||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
|
||||||
cardsContainer.innerHTML = "<p>No cards found.</p>"
|
cardsContainer.innerHTML = "<p>No cards found.</p>"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 2) Validate structure
|
|
||||||
const validatedCards = await Promise.all(
|
|
||||||
response.map(async (card) => {
|
|
||||||
const isValid = await validateCardStructure(card)
|
|
||||||
return isValid ? card : null
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const validCards = validatedCards.filter((card) => card !== null)
|
|
||||||
|
|
||||||
if (validCards.length === 0) {
|
const validated = (await Promise.all(
|
||||||
|
rawResults.map(async (r) => (await validateCardStructure(r)) ? r : null)
|
||||||
|
)).filter(Boolean)
|
||||||
|
|
||||||
|
if (validated.length === 0) {
|
||||||
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
|
cardsContainer.innerHTML = "<p>No valid cards found.</p>"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Additional logic for ARBoard or MinterCards
|
|
||||||
const finalCards = isARBoard
|
|
||||||
? await processARBoardCards(validCards)
|
|
||||||
: await processMinterBoardCards(validCards)
|
|
||||||
|
|
||||||
// Sort finalCards according to selectedSort
|
let processedCards
|
||||||
let selectedSort = 'newest'
|
if (isARBoard) {
|
||||||
const sortSelect = document.getElementById('sort-select')
|
processedCards = await processARBoardCards(validated)
|
||||||
|
} else {
|
||||||
|
processedCards = await processMinterBoardCards(validated)
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedSort = "newest"
|
||||||
|
const sortSelect = document.getElementById("sort-select")
|
||||||
if (sortSelect) {
|
if (sortSelect) {
|
||||||
selectedSort = sortSelect.value
|
selectedSort = sortSelect.value
|
||||||
}
|
}
|
||||||
|
if (selectedSort === "name") {
|
||||||
if (selectedSort === 'name') {
|
processedCards.sort((a, b) => (a.name||"").localeCompare(b.name||""))
|
||||||
finalCards.sort((a, b) => {
|
|
||||||
const nameA = a.name?.toLowerCase() || ''
|
|
||||||
const nameB = b.name?.toLowerCase() || ''
|
|
||||||
return nameA.localeCompare(nameB)
|
|
||||||
})
|
|
||||||
} else if (selectedSort === 'recent-comments') {
|
} else if (selectedSort === 'recent-comments') {
|
||||||
// If you need the newest comment timestamp
|
// If you need the newest comment timestamp
|
||||||
for (let card of finalCards) {
|
for (let card of finalCards) {
|
||||||
@ -510,89 +527,107 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
} else if (selectedSort === 'most-votes') {
|
} else if (selectedSort === 'most-votes') {
|
||||||
await applyVoteSortingData(finalCards, /* ascending= */ false)
|
await applyVoteSortingData(finalCards, /* ascending= */ false)
|
||||||
}
|
}
|
||||||
// else 'newest' => do nothing (already sorted newest-first by your process functions).
|
|
||||||
// Create the 'finalCardsArray' that includes the data, etc.
|
cardsContainer.innerHTML = "" // reset
|
||||||
let finalCardsArray = []
|
for (const card of processedCards) {
|
||||||
let alreadyMinterCards = []
|
|
||||||
cardsContainer.innerHTML = ''
|
|
||||||
for (const card of finalCards) {
|
|
||||||
try {
|
|
||||||
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
const skeletonHTML = createSkeletonCardHTML(card.identifier)
|
||||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||||
const cardDataResponse = await qortalRequest({
|
}
|
||||||
|
|
||||||
|
const finalCardsArray = []
|
||||||
|
const alreadyMinterCards = []
|
||||||
|
|
||||||
|
const tasks = processedCards.map(card => {
|
||||||
|
return async () => {
|
||||||
|
// We'll store an object with skip info, QDN data, etc.
|
||||||
|
const result = {
|
||||||
|
card,
|
||||||
|
skip: false,
|
||||||
|
skipReason: "",
|
||||||
|
isAlreadyMinter: false,
|
||||||
|
cardData: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await qortalRequest({
|
||||||
action: "FETCH_QDN_RESOURCE",
|
action: "FETCH_QDN_RESOURCE",
|
||||||
name: card.name,
|
name: card.name,
|
||||||
service: "BLOG_POST",
|
service: "BLOG_POST",
|
||||||
identifier: card.identifier
|
identifier: card.identifier
|
||||||
})
|
})
|
||||||
|
if (!data || !data.poll) {
|
||||||
|
result.skip = true
|
||||||
|
result.skipReason = "Missing or invalid poll"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
if (!cardDataResponse || !cardDataResponse.poll) {
|
const pollPublisherAddress = await getPollOwnerAddressCached(data.poll)
|
||||||
// skip
|
const cardPublisherAddress = await fetchOwnerAddressFromNameCached(card.name)
|
||||||
console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`)
|
|
||||||
removeSkeleton(card.identifier)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Extra validation: check poll ownership matches card publisher
|
|
||||||
const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll)
|
|
||||||
const cardPublisherAddress = await fetchOwnerAddressFromName(card.name)
|
|
||||||
if (pollPublisherAddress !== cardPublisherAddress) {
|
if (pollPublisherAddress !== cardPublisherAddress) {
|
||||||
console.warn(`Poll hijack attack found, discarding card ${card.identifier}`)
|
result.skip = true
|
||||||
removeSkeleton(card.identifier)
|
result.skipReason = "Poll hijack mismatch"
|
||||||
continue
|
return result
|
||||||
}
|
}
|
||||||
// If ARBoard, do a quick address check
|
|
||||||
|
// ARBoard => verify user is minter/admin
|
||||||
if (isARBoard) {
|
if (isARBoard) {
|
||||||
const ok = await verifyMinter(cardDataResponse.minterName)
|
const ok = await verifyMinterCached(data.minterName)
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
console.warn(`Card is not a minter nor an admin, not including in ARBoard. identifier: ${card.identifier}`)
|
result.skip = true
|
||||||
removeSkeleton(card.identifier)
|
result.skipReason = "Card user not minter => skip from ARBoard"
|
||||||
continue
|
return result
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const isAlreadyMinter = await verifyMinter(cardDataResponse.creator)
|
// MinterBoard => skip if user is minter
|
||||||
if (isAlreadyMinter) {
|
const isAlready = await verifyMinterCached(data.creator)
|
||||||
console.warn(`card IS ALREADY a minter, adding to alreadyMinterCards array: ${card.identifier}`)
|
if (isAlready) {
|
||||||
removeSkeleton(card.identifier)
|
result.skip = true
|
||||||
alreadyMinterCards.push({
|
result.skipReason = "Already a minter"
|
||||||
...card,
|
result.isAlreadyMinter = true
|
||||||
cardDataResponse,
|
result.cardData = data
|
||||||
pollPublisherAddress,
|
return result
|
||||||
cardPublisherAddress
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// **Push** to finalCardsArray for further processing (duplicates, etc.)
|
// If we get here => it's a keeper
|
||||||
finalCardsArray.push({
|
result.cardData = data
|
||||||
...card,
|
|
||||||
cardDataResponse,
|
|
||||||
pollPublisherAddress,
|
|
||||||
cardPublisherAddress,
|
|
||||||
})
|
|
||||||
if (counterSpan) {
|
|
||||||
const displayedCount = finalCardsArray.length
|
|
||||||
const alreadyMinterCount = alreadyMinterCards.length
|
|
||||||
// If you want to show both
|
|
||||||
counterSpan.textContent = `(${displayedCount} cards, ${alreadyMinterCount} existingMinters)`
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error preparing card ${card.identifier}`, err)
|
console.warn("Error fetching resource or skip logic:", err)
|
||||||
removeSkeleton(card.identifier)
|
result.skip = true
|
||||||
|
result.skipReason = "Error: " + err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// ADJUST THE CONCURRENCY TO INCREASE THE AMOUNT OF CARDS PROCESSED AT ONCE. INCREASE UNTIL THERE ARE ISSUES.
|
||||||
|
const concurrency = 30
|
||||||
|
const results = await runWithConcurrency(tasks, concurrency)
|
||||||
|
|
||||||
|
// Fill final arrays
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.skip && r.isAlreadyMinter) {
|
||||||
|
alreadyMinterCards.push({ ...r.card, cardDataResponse: r.cardData })
|
||||||
|
removeSkeleton(r.card.identifier)
|
||||||
|
} else if (r.skip) {
|
||||||
|
console.warn(`Skipping card ${r.card.identifier}, reason=${r.skipReason}`)
|
||||||
|
removeSkeleton(r.card.identifier)
|
||||||
|
} else {
|
||||||
|
// keeper
|
||||||
|
finalCardsArray.push({
|
||||||
|
...r.card,
|
||||||
|
cardDataResponse: r.cardData
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, do the actual rendering:
|
|
||||||
// cardsContainer.innerHTML = ""
|
|
||||||
for (const cardObj of finalCardsArray) {
|
for (const cardObj of finalCardsArray) {
|
||||||
// Insert a skeleton first if you like
|
try {
|
||||||
// const skeletonHTML = createSkeletonCardHTML(cardObj.identifier)
|
const pollResults = await fetchPollResultsCached(cardObj.cardDataResponse.poll)
|
||||||
// cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
const commentCount = await countCommentsCached(cardObj.identifier)
|
||||||
// Build final HTML
|
const cardUpdatedTime = cardObj.updated || cardObj.created || null
|
||||||
const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll)
|
|
||||||
const commentCount = await countComments(cardObj.identifier)
|
|
||||||
const cardUpdatedTime = cardObj.updated || null
|
|
||||||
const bgColor = generateDarkPastelBackgroundBy(cardObj.name)
|
const bgColor = generateDarkPastelBackgroundBy(cardObj.name)
|
||||||
// Construct the final HTML for each card
|
|
||||||
|
// If ARBoard => createARCardHTML else createCardHTML
|
||||||
const finalCardHTML = isARBoard
|
const finalCardHTML = isARBoard
|
||||||
? await createARCardHTML(
|
? await createARCardHTML(
|
||||||
cardObj.cardDataResponse,
|
cardObj.cardDataResponse,
|
||||||
@ -601,7 +636,7 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
commentCount,
|
commentCount,
|
||||||
cardUpdatedTime,
|
cardUpdatedTime,
|
||||||
bgColor,
|
bgColor,
|
||||||
cardObj.cardPublisherAddress,
|
await fetchOwnerAddressFromNameCached(cardObj.name),
|
||||||
cardObj.isDuplicate
|
cardObj.isDuplicate
|
||||||
)
|
)
|
||||||
: await createCardHTML(
|
: await createCardHTML(
|
||||||
@ -611,38 +646,50 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
commentCount,
|
commentCount,
|
||||||
cardUpdatedTime,
|
cardUpdatedTime,
|
||||||
bgColor,
|
bgColor,
|
||||||
cardObj.cardPublisherAddress
|
await fetchOwnerAddressFromNameCached(cardObj.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
replaceSkeleton(cardObj.identifier, finalCardHTML)
|
replaceSkeleton(cardObj.identifier, finalCardHTML)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error finalizing card ${cardObj.identifier}:`, err)
|
||||||
|
removeSkeleton(cardObj.identifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showExisting && alreadyMinterCards.length > 0) {
|
if (showExisting && alreadyMinterCards.length > 0) {
|
||||||
console.warn(`Rendering Existing Minter cards because user selected showExisting`)
|
console.log(`Rendering minted cards because showExisting is checked, count=${alreadyMinterCards.length}`)
|
||||||
|
for (const minted of alreadyMinterCards) {
|
||||||
for (const mintedCardObj of alreadyMinterCards) {
|
const skeletonHTML = createSkeletonCardHTML(minted.identifier)
|
||||||
const skeletonHTML = createSkeletonCardHTML(mintedCardObj.identifier)
|
|
||||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML)
|
||||||
|
|
||||||
const pollResults = await fetchPollResults(mintedCardObj.cardDataResponse.poll)
|
try {
|
||||||
const commentCount = await countComments(mintedCardObj.identifier)
|
const pollResults = await fetchPollResultsCached(minted.cardDataResponse.poll)
|
||||||
const cardUpdatedTime = mintedCardObj.updated || null
|
const commentCount = await countCommentsCached(minted.identifier)
|
||||||
const bgColor = generateDarkPastelBackgroundBy(mintedCardObj.name)
|
const cardUpdatedTime = minted.updated || minted.created || null
|
||||||
const isExistingMinter = true
|
const bgColor = generateDarkPastelBackgroundBy(minted.name)
|
||||||
|
|
||||||
const finalCardHTML = await createCardHTML(
|
const finalCardHTML = await createCardHTML(
|
||||||
mintedCardObj.cardDataResponse,
|
minted.cardDataResponse,
|
||||||
pollResults,
|
pollResults,
|
||||||
mintedCardObj.identifier,
|
minted.identifier,
|
||||||
commentCount,
|
commentCount,
|
||||||
cardUpdatedTime,
|
cardUpdatedTime,
|
||||||
bgColor,
|
bgColor,
|
||||||
mintedCardObj.cardPublisherAddress,
|
await fetchOwnerAddressFromNameCached(minted.name),
|
||||||
isExistingMinter
|
/* isExistingMinter= */ true
|
||||||
)
|
)
|
||||||
replaceSkeleton(mintedCardObj.identifier, finalCardHTML)
|
replaceSkeleton(minted.identifier, finalCardHTML)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error finalizing minted card ${minted.identifier}:`, err)
|
||||||
|
removeSkeleton(minted.identifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counterSpan) {
|
||||||
|
const displayed = finalCardsArray.length
|
||||||
|
const minted = alreadyMinterCards.length
|
||||||
|
counterSpan.textContent = `(${displayed} displayed, ${minted} minters)`
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading cards:", error)
|
console.error("Error loading cards:", error)
|
||||||
@ -653,9 +700,19 @@ const loadCards = async (cardIdentifierPrefix) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyMinterCache = new Map()
|
||||||
|
const verifyMinterCached = async (nameOrAddress) => {
|
||||||
|
if (verifyMinterCache.has(nameOrAddress)) {
|
||||||
|
return verifyMinterCache.get(nameOrAddress)
|
||||||
|
}
|
||||||
|
const result = await verifyMinter(nameOrAddress)
|
||||||
|
verifyMinterCache.set(nameOrAddress, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const verifyMinter = async (minterName) => {
|
const verifyMinter = async (minterName) => {
|
||||||
try {
|
try {
|
||||||
const nameInfo = await getNameInfo(minterName)
|
const nameInfo = await getNameInfoCached(minterName)
|
||||||
|
|
||||||
if (!nameInfo) return false
|
if (!nameInfo) return false
|
||||||
const minterAddress = nameInfo.owner
|
const minterAddress = nameInfo.owner
|
||||||
@ -663,8 +720,10 @@ const verifyMinter = async (minterName) => {
|
|||||||
|
|
||||||
if (!isValid) return false
|
if (!isValid) return false
|
||||||
// Then check if they're in the minter group
|
// Then check if they're in the minter group
|
||||||
const minterGroup = await fetchMinterGroupMembers()
|
// const minterGroup = await fetchMinterGroupMembers()
|
||||||
const adminGroup = await fetchMinterGroupAdmins()
|
const minterGroup = cachedMinterGroup
|
||||||
|
// const adminGroup = await fetchMinterGroupAdmins()
|
||||||
|
const adminGroup = cachedMinterAdmins
|
||||||
const minterGroupAddresses = minterGroup.map(m => m.member)
|
const minterGroupAddresses = minterGroup.map(m => m.member)
|
||||||
const adminGroupAddresses = adminGroup.map(m => m.member)
|
const adminGroupAddresses = adminGroup.map(m => m.member)
|
||||||
|
|
||||||
@ -677,8 +736,10 @@ const verifyMinter = async (minterName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applyVoteSortingData = async (cards, ascending = true) => {
|
const applyVoteSortingData = async (cards, ascending = true) => {
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
// const minterGroupMembers = await fetchMinterGroupMembers()
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
const minterGroupMembers = cachedMinterGroup
|
||||||
|
// const minterAdmins = await fetchMinterGroupAdmins()
|
||||||
|
const minterAdmins = cachedMinterAdmins
|
||||||
|
|
||||||
for (const card of cards) {
|
for (const card of cards) {
|
||||||
try {
|
try {
|
||||||
@ -695,7 +756,7 @@ const applyVoteSortingData = async (cards, ascending = true) => {
|
|||||||
card._minterYes = 0
|
card._minterYes = 0
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const pollResults = await fetchPollResults(cardDataResponse.poll);
|
const pollResults = await fetchPollResultsCached(cardDataResponse.poll);
|
||||||
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
|
const { adminYes, adminNo, minterYes, minterNo } = await processPollData(
|
||||||
pollResults,
|
pollResults,
|
||||||
minterGroupMembers,
|
minterGroupMembers,
|
||||||
@ -866,7 +927,8 @@ const loadCardIntoForm = async (cardData) => {
|
|||||||
|
|
||||||
// Main function to publish a new Minter Card -----------------------------------------------
|
// Main function to publish a new Minter Card -----------------------------------------------
|
||||||
const publishCard = async (cardIdentifierPrefix) => {
|
const publishCard = async (cardIdentifierPrefix) => {
|
||||||
const minterGroupData = await fetchMinterGroupMembers()
|
// const minterGroupData = await fetchMinterGroupMembers()
|
||||||
|
const minterGroupData = cachedMinterGroup
|
||||||
const minterGroupAddresses = minterGroupData.map(m => m.member)
|
const minterGroupAddresses = minterGroupData.map(m => m.member)
|
||||||
const userAddress = userState.accountAddress
|
const userAddress = userState.accountAddress
|
||||||
|
|
||||||
@ -1353,6 +1415,16 @@ const toggleComments = async (cardIdentifier) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commentCountCache = new Map()
|
||||||
|
const countCommentsCached= async (cardIdentifier) => {
|
||||||
|
if (commentCountCache.has(cardIdentifier)) {
|
||||||
|
return commentCountCache.get(cardIdentifier)
|
||||||
|
}
|
||||||
|
const count = await countComments(cardIdentifier)
|
||||||
|
commentCountCache.set(cardIdentifier, count)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
const countComments = async (cardIdentifier) => {
|
const countComments = async (cardIdentifier) => {
|
||||||
try {
|
try {
|
||||||
const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false')
|
const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false')
|
||||||
@ -1503,7 +1575,7 @@ const handleInviteMinter = async (minterName) => {
|
|||||||
try {
|
try {
|
||||||
const blockInfo = await getLatestBlockInfo()
|
const blockInfo = await getLatestBlockInfo()
|
||||||
const blockHeight = blockInfo.height
|
const blockHeight = blockInfo.height
|
||||||
const minterAccountInfo = await getNameInfo(minterName)
|
const minterAccountInfo = await getNameInfoCached(minterName)
|
||||||
const minterAddress = await minterAccountInfo.owner
|
const minterAddress = await minterAccountInfo.owner
|
||||||
let adminPublicKey
|
let adminPublicKey
|
||||||
let txGroupId
|
let txGroupId
|
||||||
@ -1587,7 +1659,8 @@ const featureTriggerCheck = async () => {
|
|||||||
const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
|
const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => {
|
||||||
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
||||||
const isBlockPassed = await featureTriggerCheck()
|
const isBlockPassed = await featureTriggerCheck()
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
// const minterAdmins = await fetchMinterGroupAdmins()
|
||||||
|
const minterAdmins = cachedMinterAdmins
|
||||||
|
|
||||||
// default needed admin count = 9, or 40% if block has passed
|
// default needed admin count = 9, or 40% if block has passed
|
||||||
let minAdminCount = 9
|
let minAdminCount = 9
|
||||||
@ -1603,7 +1676,7 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
|
|||||||
}
|
}
|
||||||
console.log(`passed initial button creation checks (adminYes >= ${minAdminCount})`)
|
console.log(`passed initial button creation checks (adminYes >= ${minAdminCount})`)
|
||||||
// get user's address from 'creator' name
|
// get user's address from 'creator' name
|
||||||
const minterNameInfo = await getNameInfo(creator)
|
const minterNameInfo = await getNameInfoCached(creator)
|
||||||
if (!minterNameInfo || !minterNameInfo.owner) {
|
if (!minterNameInfo || !minterNameInfo.owner) {
|
||||||
console.warn(`No valid nameInfo for ${creator}, skipping invite button.`)
|
console.warn(`No valid nameInfo for ${creator}, skipping invite button.`)
|
||||||
return null
|
return null
|
||||||
@ -1652,10 +1725,8 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const findPendingApprovalTxForAddress = async (address, txType, limit = 0, offset = 0) => {
|
const findPendingTxForAddress = async (address, txType, limit = 0, offset = 0) => {
|
||||||
// 1) Fetch all pending transactions
|
|
||||||
const pendingTxs = await searchPendingTransactions(limit, offset, false)
|
const pendingTxs = await searchPendingTransactions(limit, offset, false)
|
||||||
// if a txType is passed, return the results related to that type, if not, then return any pending tx of the potential types.
|
|
||||||
let relevantTypes
|
let relevantTypes
|
||||||
if (txType) {
|
if (txType) {
|
||||||
relevantTypes = new Set([txType])
|
relevantTypes = new Set([txType])
|
||||||
@ -1692,7 +1763,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
// We are going to be verifying that the address isn't already a minter, before showing GROUP_APPROVAL buttons potentially...
|
// We are going to be verifying that the address isn't already a minter, before showing GROUP_APPROVAL buttons potentially...
|
||||||
if (transactionType === "GROUP_INVITE") {
|
if (transactionType === "GROUP_INVITE") {
|
||||||
console.log(`This is a GROUP_INVITE check for group approval... Checking that user isn't already a minter...`)
|
console.log(`This is a GROUP_INVITE check for group approval... Checking that user isn't already a minter...`)
|
||||||
const minterMembers = await fetchMinterGroupMembers()
|
// const minterMembers = await fetchMinterGroupMembers()
|
||||||
|
const minterMembers = cachedMinterGroup
|
||||||
const minterGroupAddresses = minterMembers.map(m => m.member)
|
const minterGroupAddresses = minterMembers.map(m => m.member)
|
||||||
if (minterGroupAddresses.includes(address)) {
|
if (minterGroupAddresses.includes(address)) {
|
||||||
console.warn(`User is already a minter, will not be creating group_approval buttons`)
|
console.warn(`User is already a minter, will not be creating group_approval buttons`)
|
||||||
@ -1710,15 +1782,15 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa
|
|||||||
blockLimit: 0,
|
blockLimit: 0,
|
||||||
txGroupId: 0
|
txGroupId: 0
|
||||||
})
|
})
|
||||||
const pendingApprovals = await findPendingApprovalTxForAddress(address, transactionType, 0, 0)
|
const pendingTxs = await findPendingTxForAddress(address, transactionType, 0, 0)
|
||||||
let isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
let isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin
|
||||||
// If no pending transaction found, return null
|
// If no pending transaction found, return null
|
||||||
if (!pendingApprovals || pendingApprovals.length === 0) {
|
if (!pendingTxs || pendingTxs.length === 0) {
|
||||||
console.warn("no pending approval transactions found, returning null...")
|
console.warn("no pending transactions found, returning null...")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const txSig = pendingApprovals[0].signature
|
const txSig = pendingTxs[0].signature
|
||||||
// Find the relevant signature. (First approval)
|
// Find the relevant signature. (signature of the issued transaction pending.)
|
||||||
const relevantApprovals = approvalSearchResults.filter(
|
const relevantApprovals = approvalSearchResults.filter(
|
||||||
(approvalTx) => approvalTx.pendingSignature === txSig
|
(approvalTx) => approvalTx.pendingSignature === txSig
|
||||||
)
|
)
|
||||||
@ -2058,8 +2130,10 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
|||||||
</button>
|
</button>
|
||||||
`).join("")
|
`).join("")
|
||||||
|
|
||||||
const minterGroupMembers = await fetchMinterGroupMembers()
|
// const minterGroupMembers = await fetchMinterGroupMembers()
|
||||||
const minterAdmins = await fetchMinterGroupAdmins()
|
const minterGroupMembers = cachedMinterGroup
|
||||||
|
// const minterAdmins = await fetchMinterGroupAdmins()
|
||||||
|
const minterAdmins = cachedMinterAdmins
|
||||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier)
|
||||||
createModal('links')
|
createModal('links')
|
||||||
createModal('poll-details')
|
createModal('poll-details')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Q_MINTERSHIP_VERSION = "1.06.4"
|
const Q_MINTERSHIP_VERSION = "1.21"
|
||||||
|
|
||||||
const messageIdentifierPrefix = `mintership-forum-message`
|
const messageIdentifierPrefix = `mintership-forum-message`
|
||||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
|
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`
|
||||||
|
@ -6,6 +6,11 @@ let baseUrl = ''
|
|||||||
let isOutsideOfUiDevelopment = false
|
let isOutsideOfUiDevelopment = false
|
||||||
let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG'
|
let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG'
|
||||||
|
|
||||||
|
// Caching to improve performance
|
||||||
|
const nameInfoCache = new Map(); // name -> nameInfo
|
||||||
|
const addressInfoCache = new Map(); // address -> addressInfo
|
||||||
|
const pollResultsCache = new Map(); // pollName -> pollResults
|
||||||
|
|
||||||
if (typeof qortalRequest === 'function') {
|
if (typeof qortalRequest === 'function') {
|
||||||
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
|
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.')
|
||||||
isOutsideOfUiDevelopment = false
|
isOutsideOfUiDevelopment = false
|
||||||
@ -223,6 +228,13 @@ const getUserAddress = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAddressInfoCached = async (address) => {
|
||||||
|
if (addressInfoCache.has(address)) return addressInfoCache.get(address)
|
||||||
|
const result = await getAddressInfo(address)
|
||||||
|
addressInfoCache.set(address, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const getAddressInfo = async (address) => {
|
const getAddressInfo = async (address) => {
|
||||||
const qortalAddressPattern = /^Q[A-Za-z0-9]{33}$/ // Q + 33 almum = 34 total length
|
const qortalAddressPattern = /^Q[A-Za-z0-9]{33}$/ // Q + 33 almum = 34 total length
|
||||||
|
|
||||||
@ -254,6 +266,19 @@ const getAddressInfo = async (address) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameToAddressCache = new Map()
|
||||||
|
const fetchOwnerAddressFromNameCached = async (name) => {
|
||||||
|
if (nameToAddressCache.has(name)) {
|
||||||
|
return nameToAddressCache.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = await fetchOwnerAddressFromName(name)
|
||||||
|
|
||||||
|
nameToAddressCache.set(name, address)
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const fetchOwnerAddressFromName = async (name) => {
|
const fetchOwnerAddressFromName = async (name) => {
|
||||||
console.log('fetchOwnerAddressFromName called')
|
console.log('fetchOwnerAddressFromName called')
|
||||||
console.log('name:', name)
|
console.log('name:', name)
|
||||||
@ -333,6 +358,15 @@ const verifyAddressIsAdmin = async (address) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNameInfoCached = async (name) => {
|
||||||
|
if (nameInfoCache.has(name)) {
|
||||||
|
return nameInfoCache.get(name)
|
||||||
|
}
|
||||||
|
const result = await getNameInfo(name)
|
||||||
|
nameInfoCache.set(name, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const getNameInfo = async (name) => {
|
const getNameInfo = async (name) => {
|
||||||
console.log('getNameInfo called')
|
console.log('getNameInfo called')
|
||||||
console.log('name:', name)
|
console.log('name:', name)
|
||||||
@ -1239,6 +1273,20 @@ const getProductDetails = async (service, name, identifier) => {
|
|||||||
|
|
||||||
// Qortal poll-related calls ----------------------------------------------------------------------
|
// Qortal poll-related calls ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const pollOwnerAddrCache = new Map()
|
||||||
|
|
||||||
|
const getPollOwnerAddressCached = async (pollName) => {
|
||||||
|
if (pollOwnerAddrCache.has(pollName)) {
|
||||||
|
return pollOwnerAddrCache.get(pollName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerAddress = await getPollOwnerAddress(pollName)
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
pollOwnerAddrCache.set(pollName, ownerAddress)
|
||||||
|
return ownerAddress
|
||||||
|
}
|
||||||
|
|
||||||
const getPollOwnerAddress = async (pollName) => {
|
const getPollOwnerAddress = async (pollName) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
|
const response = await fetch(`${baseUrl}/polls/${pollName}`, {
|
||||||
@ -1267,6 +1315,15 @@ const getPollPublisherPublicKey = async (pollName) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPollResultsCached = async (pollName) => {
|
||||||
|
if (pollResultsCache.has(pollName)) {
|
||||||
|
return pollResultsCache.get(pollName)
|
||||||
|
}
|
||||||
|
const result = await fetchPollResults(pollName)
|
||||||
|
pollResultsCache.set(pollName, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const fetchPollResults = async (pollName) => {
|
const fetchPollResults = async (pollName) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
|
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
|
||||||
@ -1354,7 +1411,7 @@ const processTransaction = async (signedTransaction) => {
|
|||||||
|
|
||||||
// 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.
|
// 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.
|
// 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, txGroupId, fee) => {
|
const createGroupInviteTransaction = async (recipientAddress, adminPublicKey, groupId=694, invitee, timeToLive=0, txGroupId, fee) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch account reference correctly
|
// Fetch account reference correctly
|
||||||
|
@ -196,8 +196,8 @@ const fetchAllInviteTransactions = async () => {
|
|||||||
|
|
||||||
const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx)
|
const { finalTx: finalInviteTxs, pendingTx: pendingInviteTxs } = partitionTransactions(allInviteTx)
|
||||||
|
|
||||||
console.log('Final kickTxs:', finalInviteTxs)
|
console.log('Final InviteTxs:', finalInviteTxs)
|
||||||
console.log('Pending kickTxs:', pendingInviteTxs)
|
console.log('Pending InviteTxs:', pendingInviteTxs)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
finalInviteTxs,
|
finalInviteTxs,
|
||||||
@ -205,4 +205,16 @@ const fetchAllInviteTransactions = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findPendingApprovalsForTxSignature = async (txSignature, txType='GROUP_APPROVAL', limit=0, offset=0) => {
|
||||||
|
const pendingTxs = await searchPendingTransactions(limit, offset)
|
||||||
|
|
||||||
|
// Filter only the relevant GROUP_APPROVAL TX referencing txSignature
|
||||||
|
const approvals = pendingTxs.filter(tx =>
|
||||||
|
tx.type === txType && tx.pendingSignature === txSignature
|
||||||
|
)
|
||||||
|
console.log(`approvals found:`,approvals)
|
||||||
|
return approvals
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user