S Price: $0.486183 (+7.79%)

Contract Diff Checker

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
      });
  }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):