Quest Protocol
The Quest Protocol is a standardized way to create quest-based collections that reward users with tokens and coins for completing cryptographically verified tasks. Quest collections are designed for achievement-based systems where users complete quests (tasks) and provide cryptographic proofs (Merkle proofs) to claim tokens along with coin incentives.
Protocol Requirements
Collection Standards
Must include "Quests" in the
standardsTimeline
for the current time periodMust have exactly one valid token ID:
{"start": "1", "end": "1"}
Quest Approval Requirements
From List: Must be "Mint" (minting from the mint address)
Merkle Challenge: Must have exactly one Merkle challenge with:
maxUsesPerLeaf
: 1 (one use per proof)useCreatorAddressAsLeaf
: false (custom proof verification)Valid Merkle root hash for proof verification
Coin Transfers: Must have exactly one coin transfer with:
Exactly one coin denomination and amount
overrideFromWithApproverAddress
: true (coins come from collection creator)overrideToWithInitiator
: true (coins go to the quest completer)
Max Transfers: Must have
overallMaxNumTransfers
> 0Predetermined Balances: Must have:
Exactly one
startBalance
with amount 1 for token ID 1incrementBadgeIdsBy
: 0 (no token ID incrementing)incrementOwnershipTimesBy
: 0 (no time incrementing)durationFromTimestamp
: 0 (no time-based duration)allowOverrideTimestamp
: false (no timestamp overrides)All
recurringOwnershipTimes
fields set to 0 (no recurring)
Additional Constraints:
mustOwnBadges
: empty (no prerequisite tokens)requireToEqualsInitiatedBy
: false (no address matching required)
Validation Functions
Collection Validation
API Documentation: doesCollectionFollowQuestProtocol
export const doesCollectionFollowQuestProtocol = (
collection?: Readonly<iCollectionDoc<bigint>>
) => {
if (!collection) {
return false;
}
// Check if "Quests" standard is active for current time
let found = false;
for (const standard of collection.standardsTimeline) {
const isCurrentTime = UintRangeArray.From(
standard.timelineTimes
).searchIfExists(BigInt(Date.now()));
if (!isCurrentTime) {
continue;
}
if (!standard.standards.includes('Quests')) {
continue;
}
found = true;
}
if (!found) {
return false;
}
// Assert valid token IDs are only 1n-1n
const badgeIds = UintRangeArray.From(collection.validBadgeIds)
.sortAndMerge()
.convert(BigInt);
if (badgeIds.length !== 1 || badgeIds.size() !== 1n) {
return false;
}
if (badgeIds[0].start !== 1n || badgeIds[0].end !== 1n) {
return false;
}
return true;
};
Approval Validation
API Documentation: isQuestApproval
export const isQuestApproval = (approval: iCollectionApproval<bigint>) => {
const approvalCriteria = approval.approvalCriteria;
if (!approvalCriteria?.coinTransfers) {
return false;
}
// Must be minting approval
if (approval.fromListId !== 'Mint') {
return false;
}
// Must have exactly one Merkle challenge
if (
!approvalCriteria.merkleChallenges ||
approvalCriteria.merkleChallenges.length !== 1
) {
return false;
}
let merkleChallenge = approvalCriteria.merkleChallenges?.[0];
if (merkleChallenge.maxUsesPerLeaf !== 1n) {
return false;
}
// Must not require owning other tokens
if (approvalCriteria.mustOwnBadges?.length) {
return false;
}
// Must not use creator address as leaf
if (merkleChallenge.useCreatorAddressAsLeaf) {
return false;
}
// Must have max transfer limit
const maxNumTransfers =
approvalCriteria.maxNumTransfers?.overallMaxNumTransfers;
if (!maxNumTransfers) {
return false;
}
if (maxNumTransfers <= 0n) {
return false;
}
// Must have exactly one coin transfer
if (approvalCriteria.coinTransfers.length !== 1) {
return false;
}
// Validate coin transfer configuration
for (const coinTransfer of approvalCriteria.coinTransfers) {
if (coinTransfer.coins.length !== 1) {
return false;
}
if (
!coinTransfer.overrideFromWithApproverAddress ||
!coinTransfer.overrideToWithInitiator
) {
return false;
}
}
// Validate predetermined balances
const incrementedBalances =
approvalCriteria.predeterminedBalances?.incrementedBalances;
if (!incrementedBalances) {
return false;
}
if (incrementedBalances.startBalances.length !== 1) {
return false;
}
const allBadgeIds = UintRangeArray.From(
incrementedBalances.startBalances[0].badgeIds
)
.sortAndMerge()
.convert(BigInt);
if (allBadgeIds.length !== 1 || allBadgeIds.size() !== 1n) {
return false;
}
if (allBadgeIds[0].start !== 1n || allBadgeIds[0].end !== 1n) {
return false;
}
const amount = incrementedBalances.startBalances[0].amount;
if (amount !== 1n) {
return false;
}
if (incrementedBalances.incrementBadgeIdsBy !== 0n) {
return false;
}
if (incrementedBalances.incrementOwnershipTimesBy !== 0n) {
return false;
}
if (incrementedBalances.durationFromTimestamp !== 0n) {
return false;
}
// Needs this to be false for the subscription faucet to work
if (incrementedBalances.allowOverrideTimestamp) {
return false;
}
if (incrementedBalances.recurringOwnershipTimes.startTime !== 0n) {
return false;
}
if (incrementedBalances.recurringOwnershipTimes.intervalLength !== 0n) {
return false;
}
if (incrementedBalances.recurringOwnershipTimes.chargePeriodLength !== 0n) {
return false;
}
if (approvalCriteria.requireToEqualsInitiatedBy) {
return false;
}
return true;
};
Implementation Example
For a complete implementation example, see the Quest Token Collection Example.
Last updated