Contract Name:
PromotionsLibrary
Contract Source Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
enum Promotion {
NONE,
STARTER,
HALLOWEEN_2023,
XMAS_2023,
HALLOWEEN_2024,
HOLIDAY4, // Just have placeholders for now
HOLIDAY5,
HOLIDAY6,
HOLIDAY7,
HOLIDAY8,
HOLIDAY9,
HOLIDAY10
}
enum PromotionMintStatus {
NONE,
SUCCESS,
PROMOTION_ALREADY_CLAIMED,
ORACLE_NOT_CALLED,
MINTING_OUTSIDE_AVAILABLE_DATE,
PLAYER_DOES_NOT_QUALIFY,
PLAYER_NOT_HIT_ENOUGH_CLAIMS_FOR_STREAK_BONUS,
DEPENDENT_QUEST_NOT_COMPLETED
}
struct PromotionInfoInput {
Promotion promotion;
uint40 startTime;
uint40 endTime; // Exclusive
uint8 numDailyRandomItemsToPick; // Number of items to pick
uint40 minTotalXP; // Minimum xp required to claim
uint256 tokenCost; // Cost in brush to start the promotion, max 16mil
// Special promotion specific (like 1kin)
uint8 redeemCodeLength; // Length of the redeem code
bool adminOnly; // Only admins can mint the promotion, like for 1kin (Not used yet)
bool promotionTiedToUser; // If the promotion is tied to a user
bool promotionTiedToPlayer; // If the promotion is tied to the player
bool promotionMustOwnPlayer; // Must own the player to get the promotion
// Evolution specific
bool evolvedHeroOnly; // Only allow evolved heroes to claim
// Multiday specific
bool isMultiday; // The promotion is multi-day
uint256 brushCostMissedDay; // Cost in brush to mint the promotion if they miss a day (in ether), max 25.6 (base 100)
uint8 numDaysHitNeededForStreakBonus; // How many days to hit for the streak bonus
uint8 numDaysClaimablePeriodStreakBonus; // If there is a streak bonus, how many days to claim it after the promotion ends. If no final day bonus, set to 0
uint8 numRandomStreakBonusItemsToPick1; // Number of items to pick for the streak bonus
uint8 numRandomStreakBonusItemsToPick2; // Number of random items to pick for the streak bonus
uint16[] randomStreakBonusItemTokenIds1;
uint32[] randomStreakBonusAmounts1;
uint16[] randomStreakBonusItemTokenIds2;
uint32[] randomStreakBonusAmounts2;
uint16[] guaranteedStreakBonusItemTokenIds;
uint16[] guaranteedStreakBonusAmounts;
// Single and multiday
uint16[] guaranteedItemTokenIds; // Guaranteed items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] guaranteedAmounts; // Corresponding amounts to the itemTokenIds
uint16[] randomItemTokenIds; // Possible items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] randomAmounts; // Corresponding amounts to the randomItemTokenIds
// Quests
uint16 questPrerequisiteId;
}
struct PromotionInfo {
Promotion promotion;
uint40 startTime;
uint8 numDays;
uint8 numDailyRandomItemsToPick; // Number of items to pick
uint40 minTotalXP; // Minimum xp required to claim
uint24 tokenCost; // Cost in brush to mint the promotion (in ether), max 16mil
// Quests
uint16 questPrerequisiteId;
// Special promotion specific (like 1kin), could pack these these later
uint8 redeemCodeLength; // Length of the redeem code
bool adminOnly; // Only admins can mint the promotion, like for 1kin
bool promotionTiedToUser; // If the promotion is tied to a user
bool promotionTiedToPlayer; // If the promotion is tied to the player
bool promotionMustOwnPlayer; // Must own the player to get the promotion
// Evolution specific
bool evolvedHeroOnly; // Only allow evolved heroes to claim
// Multiday specific
bool isMultiday; // The promotion is multi-day
uint8 brushCostMissedDay; // Cost in brush to mint the promotion if they miss a day (in ether), max 25.5, base 100
uint8 numDaysHitNeededForStreakBonus; // How many days to hit for the streak bonus
uint8 numDaysClaimablePeriodStreakBonus; // If there is a streak bonus, how many days to claim it after the promotion ends. If no final day bonus, set to 0
uint8 numRandomStreakBonusItemsToPick1; // Number of items to pick for the streak bonus
uint8 numRandomStreakBonusItemsToPick2; // Number of random items to pick for the streak bonus
// Misc
uint16[] randomStreakBonusItemTokenIds1;
uint32[] randomStreakBonusAmounts1;
uint16[] randomStreakBonusItemTokenIds2; // Not used yet
uint32[] randomStreakBonusAmounts2; // Not used yet
uint16[] guaranteedStreakBonusItemTokenIds; // Not used yet
uint16[] guaranteedStreakBonusAmounts; // Not used yet
// Single and multiday
uint16[] guaranteedItemTokenIds; // Guaranteed items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] guaranteedAmounts; // Corresponding amounts to the itemTokenIds
uint16[] randomItemTokenIds; // Possible items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] randomAmounts; // Corresponding amounts to the randomItemTokenIds
}
uint256 constant BRUSH_COST_MISSED_DAY_MUL = 10;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
// solhint-disable-next-line no-global-import
import "./globals/promotions.sol";
// This file contains methods for interacting with the item NFT, used to decrease implementation deployment bytecode code.
library PromotionsLibrary {
error PromotionAlreadyAdded();
error LengthMismatch();
error PromotionNotSet();
error StartTimeMustBeHigherEndTime();
error NoNumItemsToPick();
error InvalidPromotion();
error InvalidBrushCost();
error InvalidMultidayPromotionTimeInterval();
error InvalidStreakBonus();
error InvalidNumDaysHitNeededForStreakBonus();
error PickingTooManyItems();
error MultidaySpecified();
error NoItemsToPickFrom();
function addPromotion(
mapping(Promotion promotion => PromotionInfo) storage activePromotions,
PromotionInfoInput calldata promotionInfoInput
) external {
_checkAddingGenericPromotion(promotionInfoInput);
require(activePromotions[promotionInfoInput.promotion].promotion == Promotion.NONE, PromotionAlreadyAdded());
if (promotionInfoInput.isMultiday) {
_checkAddingMultidayMintPromotion(promotionInfoInput);
} else {
_checkAddingSinglePromotion(promotionInfoInput);
}
activePromotions[promotionInfoInput.promotion] = _packPromotionInfo(promotionInfoInput);
}
function editPromotion(
mapping(Promotion promotion => PromotionInfo) storage activePromotions,
PromotionInfoInput calldata promotionInfoInput
) external {
_checkAddingGenericPromotion(promotionInfoInput);
if (promotionInfoInput.isMultiday) {
_checkAddingMultidayMintPromotion(promotionInfoInput);
} else {
_checkAddingSinglePromotion(promotionInfoInput);
}
activePromotions[promotionInfoInput.promotion] = _packPromotionInfo(promotionInfoInput);
}
function _checkAddingGenericPromotion(PromotionInfoInput calldata promotionInfoInput) private pure {
require(promotionInfoInput.randomItemTokenIds.length == promotionInfoInput.randomAmounts.length, LengthMismatch());
require(promotionInfoInput.promotion != Promotion.NONE, PromotionNotSet());
require(promotionInfoInput.startTime < promotionInfoInput.endTime, StartTimeMustBeHigherEndTime());
require(
(promotionInfoInput.numDailyRandomItemsToPick != 0 || promotionInfoInput.guaranteedItemTokenIds.length != 0),
NoNumItemsToPick()
);
// TODO: Special handling for now, only allowing 1 item to be picked
require(
promotionInfoInput.guaranteedItemTokenIds.length != 0 || promotionInfoInput.numDailyRandomItemsToPick == 1,
InvalidPromotion()
);
// Check brush input is valid
require(promotionInfoInput.tokenCost % 1 ether == 0, InvalidBrushCost());
// start and endTime must be factors of 24 hours apart
require(
(promotionInfoInput.endTime - promotionInfoInput.startTime) % 1 days == 0,
InvalidMultidayPromotionTimeInterval()
);
}
// Precondition that the promotion is multiday
function _checkAddingMultidayMintPromotion(PromotionInfoInput calldata promotionInfoInput) private pure {
bool hasStreakBonus = promotionInfoInput.numDaysClaimablePeriodStreakBonus != 0;
if (hasStreakBonus) {
require(
promotionInfoInput.numRandomStreakBonusItemsToPick1 != 0 &&
promotionInfoInput.randomStreakBonusItemTokenIds1.length != 0 &&
promotionInfoInput.numDaysHitNeededForStreakBonus != 0,
InvalidStreakBonus()
);
// Cannot specify pool2 without pool 1
require(
promotionInfoInput.numRandomStreakBonusItemsToPick1 != 0 ||
promotionInfoInput.numRandomStreakBonusItemsToPick2 == 0,
InvalidStreakBonus()
);
require(
promotionInfoInput.numDaysHitNeededForStreakBonus <=
((promotionInfoInput.endTime - promotionInfoInput.startTime) / 1 days),
InvalidNumDaysHitNeededForStreakBonus()
);
require(
promotionInfoInput.randomStreakBonusItemTokenIds1.length == promotionInfoInput.randomStreakBonusAmounts1.length,
LengthMismatch()
);
require(
promotionInfoInput.randomStreakBonusItemTokenIds2.length == promotionInfoInput.randomStreakBonusAmounts2.length,
LengthMismatch()
);
require(
promotionInfoInput.guaranteedStreakBonusItemTokenIds.length ==
promotionInfoInput.guaranteedStreakBonusAmounts.length,
LengthMismatch()
);
} else {
// No streak bonus
require(
promotionInfoInput.randomStreakBonusItemTokenIds1.length == 0 &&
promotionInfoInput.randomStreakBonusItemTokenIds2.length == 0 &&
promotionInfoInput.numRandomStreakBonusItemsToPick1 == 0 &&
promotionInfoInput.numRandomStreakBonusItemsToPick2 == 0 &&
promotionInfoInput.numDaysHitNeededForStreakBonus == 0 &&
promotionInfoInput.guaranteedStreakBonusItemTokenIds.length == 0 &&
promotionInfoInput.guaranteedStreakBonusAmounts.length == 0,
InvalidStreakBonus()
);
}
require(
promotionInfoInput.numRandomStreakBonusItemsToPick1 <= promotionInfoInput.randomStreakBonusItemTokenIds1.length,
PickingTooManyItems()
);
require(
promotionInfoInput.numRandomStreakBonusItemsToPick2 <= promotionInfoInput.randomStreakBonusItemTokenIds2.length,
PickingTooManyItems()
);
// Check brush input is valid
require(
promotionInfoInput.brushCostMissedDay % 1 ether == 0 && promotionInfoInput.brushCostMissedDay <= 25 ether,
InvalidBrushCost()
);
}
function _checkAddingSinglePromotion(PromotionInfoInput calldata promotionInfoInput) private pure {
// Should not have any multi-day promotion specific fields set
require(
promotionInfoInput.numDaysHitNeededForStreakBonus == 0 &&
promotionInfoInput.numDaysClaimablePeriodStreakBonus == 0 &&
promotionInfoInput.numRandomStreakBonusItemsToPick1 == 0 &&
promotionInfoInput.randomStreakBonusItemTokenIds1.length == 0 &&
promotionInfoInput.randomStreakBonusAmounts1.length == 0 &&
promotionInfoInput.numRandomStreakBonusItemsToPick2 == 0 &&
promotionInfoInput.randomStreakBonusItemTokenIds2.length == 0 &&
promotionInfoInput.randomStreakBonusAmounts2.length == 0,
MultidaySpecified()
);
require(promotionInfoInput.randomItemTokenIds.length != 0, NoItemsToPickFrom());
require(
promotionInfoInput.numDailyRandomItemsToPick <= promotionInfoInput.randomItemTokenIds.length,
PickingTooManyItems()
);
}
function _packPromotionInfo(
PromotionInfoInput calldata promotionInfoInput
) private pure returns (PromotionInfo memory) {
return
PromotionInfo({
promotion: promotionInfoInput.promotion,
startTime: promotionInfoInput.startTime,
numDays: uint8((promotionInfoInput.endTime - promotionInfoInput.startTime) / 1 days),
numDailyRandomItemsToPick: promotionInfoInput.numDailyRandomItemsToPick,
minTotalXP: promotionInfoInput.minTotalXP,
evolvedHeroOnly: promotionInfoInput.evolvedHeroOnly,
tokenCost: uint24(promotionInfoInput.tokenCost / 1 ether),
redeemCodeLength: promotionInfoInput.redeemCodeLength,
adminOnly: promotionInfoInput.adminOnly,
promotionTiedToUser: promotionInfoInput.promotionTiedToUser,
promotionTiedToPlayer: promotionInfoInput.promotionTiedToPlayer,
promotionMustOwnPlayer: promotionInfoInput.promotionMustOwnPlayer,
isMultiday: promotionInfoInput.isMultiday,
brushCostMissedDay: uint8((promotionInfoInput.brushCostMissedDay * BRUSH_COST_MISSED_DAY_MUL) / 1 ether),
numDaysHitNeededForStreakBonus: promotionInfoInput.numDaysHitNeededForStreakBonus,
numDaysClaimablePeriodStreakBonus: promotionInfoInput.numDaysClaimablePeriodStreakBonus,
numRandomStreakBonusItemsToPick1: promotionInfoInput.numRandomStreakBonusItemsToPick1,
randomStreakBonusItemTokenIds1: promotionInfoInput.randomStreakBonusItemTokenIds1,
randomStreakBonusAmounts1: promotionInfoInput.randomStreakBonusAmounts1,
numRandomStreakBonusItemsToPick2: promotionInfoInput.numRandomStreakBonusItemsToPick2,
randomStreakBonusItemTokenIds2: promotionInfoInput.randomStreakBonusItemTokenIds2,
randomStreakBonusAmounts2: promotionInfoInput.randomStreakBonusAmounts2,
guaranteedStreakBonusItemTokenIds: promotionInfoInput.guaranteedStreakBonusItemTokenIds,
guaranteedStreakBonusAmounts: promotionInfoInput.guaranteedStreakBonusAmounts,
guaranteedItemTokenIds: promotionInfoInput.guaranteedItemTokenIds,
guaranteedAmounts: promotionInfoInput.guaranteedAmounts,
randomItemTokenIds: promotionInfoInput.randomItemTokenIds,
randomAmounts: promotionInfoInput.randomAmounts,
questPrerequisiteId: promotionInfoInput.questPrerequisiteId
});
}
}