diff --git a/src/points/FollowerSincePoints.sol b/src/points/FollowerSincePoints.sol index 5e0972d..ef82836 100644 --- a/src/points/FollowerSincePoints.sol +++ b/src/points/FollowerSincePoints.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import { Points } from "./Points.sol"; +import { Points, IPoints } from "./Points.sol"; import { IFollowerSincePoints } from "./interfaces/IFollowerSincePoints.sol"; import { IFollowerSinceStamp } from "../stamps/interfaces/IFollowerSinceStamp.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -73,4 +73,74 @@ contract FollowerSincePoints is IFollowerSincePoints, Points { uint256 durationInSeconds = timestamp - followerSince; return (Math.sqrt(durationInSeconds) * 1e18) / Math.sqrt(86400); } + + /// @inheritdoc IPoints + function getTopHolders(uint256 start, uint256 end) public view override(IPoints, Points) returns (Holder[] memory) { + // Get total number of stamps + uint256 totalStamps = followerStamp.totalSupply(); + if (totalStamps == 0 || start >= end) { + return new Holder[](0); + } + + // Create temporary array to store all holders with non-zero balances + Holder[] memory holders = new Holder[](totalStamps); + uint256 actualHolderCount = 0; + uint256 currentTimestamp = block.timestamp; + + // Collect all holders with non-zero balances + for (uint256 i = 1; i <= totalStamps; ) { + address owner = followerStamp.ownerOf(i); + uint256 balance = _balanceAtTimestamp(owner, currentTimestamp); + + if (balance > 0) { + holders[actualHolderCount] = Holder({ user: owner, balance: balance }); + unchecked { + ++actualHolderCount; + } + } + unchecked { + ++i; + } + } + + // If no holders with balance, return empty array + if (actualHolderCount == 0) { + return new Holder[](0); + } + + // Sort holders by balance (bubble sort for simplicity) + // Can be optimized with quicksort for larger datasets + for (uint256 i = 0; i < actualHolderCount - 1; ) { + for (uint256 j = 0; j < actualHolderCount - i - 1; ) { + if (holders[j].balance < holders[j + 1].balance) { + Holder memory temp = holders[j]; + holders[j] = holders[j + 1]; + holders[j + 1] = temp; + } + unchecked { + ++j; + } + } + unchecked { + ++i; + } + } + + // Calculate the actual slice size + uint256 sliceSize = end > actualHolderCount ? actualHolderCount - start : end - start; + if (start >= actualHolderCount) { + return new Holder[](0); + } + + // Create and fill result array with the requested slice + Holder[] memory result = new Holder[](sliceSize); + for (uint256 i = 0; i < sliceSize; ) { + result[i] = holders[start + i]; + unchecked { + ++i; + } + } + + return result; + } } diff --git a/src/points/MultipleFollowerSincePoints.sol b/src/points/MultipleFollowerSincePoints.sol index ca3ed49..707bb8d 100644 --- a/src/points/MultipleFollowerSincePoints.sol +++ b/src/points/MultipleFollowerSincePoints.sol @@ -1,18 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import { Points } from "./Points.sol"; +import { Points, IPoints } from "./Points.sol"; import { IFollowerSinceStamp } from "../stamps/interfaces/IFollowerSinceStamp.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { IMultipleFollowerSincePoints } from "./interfaces/IMultipleFollowerSincePoints.sol"; /// @title MultipleFollowerSincePoints - A non-transferable token based on multiple follower durations /// @notice This contract calculates points based on how long a user has been a follower across multiple accounts -/// @dev Inherits from Points and implements IMultipleFollowerSincePoints, using multiple IFollowerSinceStamp for duration calculation +/// @dev Inherits from Points and implements IMultipleFollowerSincePoints contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints { + // State Variables StampInfo[] private _stamps; uint256 private immutable _stampCount; + /// @notice Initializes the contract with multiple follower stamps and their multipliers + /// @param _stampAddresses Array of follower stamp contract addresses + /// @param _multipliers Array of multipliers corresponding to each stamp + /// @param _name Name of the token + /// @param _symbol Symbol of the token constructor( address[] memory _stampAddresses, uint256[] memory _multipliers, @@ -26,6 +32,8 @@ contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints { _stampCount = _stampAddresses.length; } + // External Functions + /// @inheritdoc IMultipleFollowerSincePoints function stamps() external view returns (StampInfo[] memory) { return _stamps; @@ -37,6 +45,29 @@ contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints { return _stamps[index]; } + /// @inheritdoc IMultipleFollowerSincePoints + function getMultipleFollowerSincePointsView( + address user + ) external view returns (MultipleFollowerSincePointsView memory) { + return MultipleFollowerSincePointsView({ pointsView: getPointsView(user), stamps: _getPointsStampViews(user) }); + } + + /// @inheritdoc IPoints + function getTopHolders(uint256 start, uint256 end) public view override(IPoints, Points) returns (Holder[] memory) { + if (start >= end) revert IndexOutOfBounds(); + + uint256 maxSize = _getTotalUniqueHolders(); + if (maxSize == 0) return new Holder[](0); + + Holder[] memory holders = new Holder[](maxSize); + uint256 totalHolders = _collectHolders(holders); + + if (start >= totalHolders) return new Holder[](0); + return _paginateAndSortHolders(holders, totalHolders, start, end); + } + + // Internal Functions + /// @inheritdoc Points function _balanceAtTimestamp(address account, uint256 timestamp) internal view override returns (uint256) { uint256 totalPoints; @@ -63,11 +94,9 @@ contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints { return totalPoints; } + // Private Functions + /// @dev Calculates points for a specific stamp and account at a given timestamp - /// @param stampInfo The StampInfo struct containing stamp and multiplier information - /// @param account The address of the account to calculate points for - /// @param timestamp The timestamp at which to calculate points - /// @return The calculated points for the stamp and account function _calculatePointsForStamp( StampInfo storage stampInfo, address account, @@ -125,143 +154,144 @@ contract MultipleFollowerSincePoints is Points, IMultipleFollowerSincePoints { return (durationInSeconds * 1e18) / 86400; } - /// @notice Gets the top holders between specified indices - /// @param start The starting index (inclusive) - /// @param end The ending index (exclusive) - /// @return addresses Array of addresses sorted by point balance - /// @return balances Array of corresponding point balances - function getTopHolders( - uint256 start, - uint256 end - ) external view returns (address[] memory addresses, uint256[] memory balances) { - if (start >= end) revert IndexOutOfBounds(); + function _getPointsStampViews(address user) private view returns (PointsStampView[] memory) { + PointsStampView[] memory views = new PointsStampView[](_stampCount); - // Get total unique holders - uint256 totalHolders = 0; - uint256 stampTotalSupply; - address[] memory tempHolders = new address[](_getTotalUniqueHolders()); + for (uint256 i = 0; i < _stampCount; ) { + StampInfo storage stampInfo = _stamps[i]; + IFollowerSinceStamp stamp = stampInfo.stamp; - // Collect unique holders across all stamps - for (uint256 i; i < _stampCount; ) { - stampTotalSupply = _stamps[i].stamp.totalSupply(); - for (uint256 j = 1; j <= stampTotalSupply; ) { - address holder = _stamps[i].stamp.ownerOf(j); - if (!_isAddressInArray(tempHolders, holder, totalHolders)) { - tempHolders[totalHolders++] = holder; - } - unchecked { - ++j; - } + // Get the stamp view from the stamp contract + IFollowerSinceStamp.StampView memory stampView = stamp.getStampView(user); + + // Calculate points for this stamp + uint256 followerSince = stamp.getFollowerSinceTimestamp(user); + uint256 points = 0; + if (followerSince != 0 && followerSince <= block.timestamp) { + points = _calculatePointsAtTimestamp(followerSince, block.timestamp) * stampInfo.multiplier; } + + // Create the points stamp data + PointsStampData memory data = PointsStampData({ + contractAddress: address(stamp), + stampType: stampView.data.stampType, + name: stampView.data.name, + symbol: stampView.data.symbol, + totalSupply: stampView.data.totalSupply, + specific: stampView.data.specific, + multiplier: stampInfo.multiplier + }); + + // Create the points stamp user data + PointsStampUser memory userData = PointsStampUser({ + owns: stampView.user.owns, + stampId: stampView.user.stampId, + mintingTimestamp: stampView.user.mintingTimestamp, + specific: stampView.user.specific, + points: points + }); + + // Combine into final view + views[i] = PointsStampView({ data: data, user: userData }); + unchecked { ++i; } } - // If no holders, return empty arrays - if (totalHolders == 0) { - return (new address[](0), new uint256[](0)); - } - - // Validate start index - if (start >= totalHolders) revert IndexOutOfBounds(); - - // Adjust end if it exceeds total holders - end = Math.min(end, totalHolders); - uint256 length = end - start; - - // Create return arrays - addresses = new address[](length); - balances = new uint256[](length); + return views; + } - // Create temporary arrays for sorting - address[] memory sortedAddresses = new address[](totalHolders); - uint256[] memory sortedBalances = new uint256[](totalHolders); + function _getStampsView(address user) private view returns (PointsStampView[] memory) { + return _getPointsStampViews(user); + } - // Get balances and sort - for (uint256 i = 0; i < totalHolders; ) { - sortedAddresses[i] = tempHolders[i]; - sortedBalances[i] = balanceOf(tempHolders[i]); + /// @dev Helper function to get total unique holders across all stamps + function _getTotalUniqueHolders() private view returns (uint256 maxHolders) { + // Sum up all stamp supplies for a conservative upper bound + for (uint256 i; i < _stampCount; ) { unchecked { + maxHolders += _stamps[i].stamp.totalSupply(); ++i; } } + } - // Replace insertion sort with optimized quicksort for larger datasets - if (totalHolders > 10) { - _quickSort(sortedAddresses, sortedBalances, 0, int256(totalHolders - 1)); - } else { - // Keep insertion sort for small arrays - for (uint256 i = 1; i < totalHolders; ) { - uint256 j = i; - while (j > 0 && sortedBalances[j - 1] < sortedBalances[j]) { - (sortedBalances[j], sortedBalances[j - 1]) = (sortedBalances[j - 1], sortedBalances[j]); - (sortedAddresses[j], sortedAddresses[j - 1]) = (sortedAddresses[j - 1], sortedAddresses[j]); - unchecked { - --j; - } - } + /// @dev Insertion sort implementation optimized for small arrays + function _insertionSort(Holder[] memory arr, uint256 length) private pure { + for (uint256 i = 1; i < length; ) { + uint256 j = i; + while (j > 0 && arr[j - 1].balance < arr[j].balance) { + (arr[j], arr[j - 1]) = (arr[j - 1], arr[j]); unchecked { - ++i; + --j; } } - } - - // Copy requested range to return arrays - for (uint256 i = 0; i < length; ) { - addresses[i] = sortedAddresses[start + i]; - balances[i] = sortedBalances[start + i]; unchecked { ++i; } } } - /// @dev Helper function to check if address exists in array - function _isAddressInArray(address[] memory array, address addr, uint256 length) private pure returns (bool) { - for (uint256 i = 0; i < length; ) { - if (array[i] == addr) return true; - unchecked { - ++i; - } - } - return false; - } - - /// @dev Helper function to get total unique holders across all stamps - /// @return maxHolders A conservative estimate of maximum possible unique holders - function _getTotalUniqueHolders() private view returns (uint256 maxHolders) { - // Find the stamp with maximum supply as a better estimate + /// @dev Helper function to collect holders and their balances + function _collectHolders(Holder[] memory holders) private view returns (uint256 totalHolders) { for (uint256 i; i < _stampCount; ) { - uint256 supply = _stamps[i].stamp.totalSupply(); - maxHolders = Math.max(maxHolders, supply); + uint256 stampSupply = _stamps[i].stamp.totalSupply(); + for (uint256 j = 1; j <= stampSupply; ) { + address owner = _stamps[i].stamp.ownerOf(j); + + // Check if holder already exists + bool found = false; + for (uint256 k; k < totalHolders; ) { + if (holders[k].user == owner) { + found = true; + break; + } + unchecked { + ++k; + } + } + + // Add new holder with their balance + if (!found) { + holders[totalHolders] = Holder({ user: owner, balance: balanceOf(owner) }); + unchecked { + ++totalHolders; + } + } + unchecked { + ++j; + } + } unchecked { ++i; } } } - /// @dev Quicksort implementation for more efficient sorting of larger arrays - function _quickSort(address[] memory addresses, uint256[] memory balances, int256 left, int256 right) private pure { - if (left >= right) return; - - int256 i = left; - int256 j = right; - uint256 pivot = balances[uint256(left + (right - left) / 2)]; + /// @dev Helper function to paginate and sort holders + function _paginateAndSortHolders( + Holder[] memory holders, + uint256 totalHolders, + uint256 start, + uint256 end + ) private pure returns (Holder[] memory) { + // Validate pagination + if (start >= totalHolders) return new Holder[](0); + end = Math.min(end, totalHolders); + uint256 length = end - start; - while (i <= j) { - while (balances[uint256(i)] > pivot) i++; - while (balances[uint256(j)] < pivot) j--; + // Sort only the actual holders (not the entire array) + _insertionSort(holders, totalHolders); - if (i <= j) { - (balances[uint256(i)], balances[uint256(j)]) = (balances[uint256(j)], balances[uint256(i)]); - (addresses[uint256(i)], addresses[uint256(j)]) = (addresses[uint256(j)], addresses[uint256(i)]); - i++; - j--; + // Return paginated result + Holder[] memory result = new Holder[](length); + for (uint256 i; i < length; ) { + result[i] = holders[start + i]; + unchecked { + ++i; } } - - if (left < j) _quickSort(addresses, balances, left, j); - if (i < right) _quickSort(addresses, balances, i, right); + return result; } } diff --git a/src/points/Points.sol b/src/points/Points.sol index 1b0e096..fc0b898 100644 --- a/src/points/Points.sol +++ b/src/points/Points.sol @@ -89,11 +89,19 @@ abstract contract Points is IPoints { return _decimals; } + function getTopHolders(uint256 start, uint256 end) public view virtual returns (Holder[] memory); + /// @inheritdoc IPointsView function getPointsView(address user) public view virtual override returns (PointsView memory) { return PointsView({ - data: PointsData({ contractAddress: address(this), name: name(), symbol: symbol() }), + data: PointsData({ + contractAddress: address(this), + name: name(), + symbol: symbol(), + totalSupply: totalSupply(), + top10Holders: getTopHolders(0, 10) + }), user: PointsUser({ currentBalance: balanceOf(user) }) }); } diff --git a/src/points/interfaces/IMultipleFollowerSincePoints.sol b/src/points/interfaces/IMultipleFollowerSincePoints.sol index 6ef3fed..36d8236 100644 --- a/src/points/interfaces/IMultipleFollowerSincePoints.sol +++ b/src/points/interfaces/IMultipleFollowerSincePoints.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.20; import { IPoints } from "./IPoints.sol"; import { IFollowerSinceStamp } from "../../stamps/interfaces/IFollowerSinceStamp.sol"; +import { IPointsStampView } from "./IPointsStampView.sol"; /// @title IMultipleFollowerSincePoints - Interface for managing points based on multiple follower durations /// @notice This interface defines the contract for a non-transferable token system that awards points based on multiple follower duration criteria /// @dev Extends IPoints interface with additional functions specific to managing multiple follower-since stamps and their corresponding point multipliers -interface IMultipleFollowerSincePoints is IPoints { +interface IMultipleFollowerSincePoints is IPoints, IPointsStampView { /// @notice Custom error thrown when input arrays have mismatched lengths /// @dev This error is used in functions that expect arrays of equal length error ArrayLengthMismatch(); @@ -23,6 +24,13 @@ interface IMultipleFollowerSincePoints is IPoints { uint256 multiplier; /// @dev The point multiplier associated with this stamp } + /// @notice Struct combining PointsView and PointsStampView for a comprehensive view + /// @dev Provides a complete snapshot of the Points system and a user's stamps + struct MultipleFollowerSincePointsView { + PointsView pointsView; /// @dev General data about the Points system + PointsStampView[] stamps; /// @dev Array of PointsStampView structs for each registered stamp + } + /// @notice Retrieves the complete array of stamp information /// @dev This function returns all registered stamps and their multipliers used in the contract /// @return An array of StampInfo structs containing all registered stamps and their corresponding multipliers @@ -34,13 +42,11 @@ interface IMultipleFollowerSincePoints is IPoints { /// @return A StampInfo struct containing the stamp and multiplier information at the given index function stampByIndex(uint256 index) external view returns (StampInfo memory); - /// @notice Gets the top holders between specified indices - /// @param start The starting index (inclusive) - /// @param end The ending index (exclusive) - /// @return addresses Array of addresses sorted by point balance - /// @return balances Array of corresponding point balances - function getTopHolders( - uint256 start, - uint256 end - ) external view returns (address[] memory addresses, uint256[] memory balances); + /// @notice Retrieves a comprehensive view of the Points system and a specific user's data + /// @dev This function should return all relevant information about the Points system and the specified user + /// @param user The address of the user to query + /// @return A MultipleFollowerSincePointsView struct containing both system-wide and user-specific information + function getMultipleFollowerSincePointsView( + address user + ) external view returns (MultipleFollowerSincePointsView memory); } diff --git a/src/points/interfaces/IPoints.sol b/src/points/interfaces/IPoints.sol index 33a6e97..d9fe7ea 100644 --- a/src/points/interfaces/IPoints.sol +++ b/src/points/interfaces/IPoints.sol @@ -21,4 +21,10 @@ interface IPoints is IERC20Metadata, IPointsView { /// @param timestamp The timestamp at which to check the total supply /// @return The total supply at the given timestamp function totalSupplyAtTimestamp(uint256 timestamp) external view returns (uint256); + + /// @notice Returns the top holders between specified indices + /// @param start The starting index (inclusive) + /// @param end The ending index (exclusive) + /// @return Array of holders sorted by point balance + function getTopHolders(uint256 start, uint256 end) external view returns (Holder[] memory); } diff --git a/src/points/interfaces/IPointsStampView.sol b/src/points/interfaces/IPointsStampView.sol new file mode 100644 index 0000000..ffe9b0f --- /dev/null +++ b/src/points/interfaces/IPointsStampView.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IStampView } from "../../stamps/interfaces/IStampView.sol"; + +/// @title IPointsStampView - View interface for stamps that award points +/// @notice This interface defines the structure for viewing stamps that are associated with points +/// @dev Extends the concept of StampView to include point-related information +interface IPointsStampView { + /// @notice Struct containing general data about a points-awarding stamp + /// @dev Extends StampData with point multiplier information + struct PointsStampData { + address contractAddress; /// @dev Address of the contract that issued the stamp + IStampView.StampType stampType; /// @dev Type of the stamp + string name; /// @dev Name of the stamp + string symbol; /// @dev Symbol or short identifier for the stamp + uint256 totalSupply; /// @dev Total number of this stamp type issued + bytes specific; /// @dev Additional data specific to the stamp type + uint256 multiplier; /// @dev The point multiplier associated with this stamp + } + + /// @notice Struct containing user-specific data for a points-awarding stamp + /// @dev Extends StampUser with points calculation + struct PointsStampUser { + bool owns; /// @dev Whether the user owns this stamp + uint256 stampId; /// @dev Unique identifier of the stamp for this user + uint256 mintingTimestamp; /// @dev Timestamp when the stamp was minted for this user + bytes specific; /// @dev Additional data specific to the stamp type + uint256 points; /// @dev Current points awarded to the user for this stamp + } + + /// @notice Struct combining points stamp data and user-specific data + /// @dev Provides a complete view of a points-awarding stamp for a specific user + struct PointsStampView { + PointsStampData data; /// @dev General stamp and points data + PointsStampUser user; /// @dev User-specific stamp and points data + } +} diff --git a/src/points/interfaces/IPointsView.sol b/src/points/interfaces/IPointsView.sol index 1b6a709..d94f954 100644 --- a/src/points/interfaces/IPointsView.sol +++ b/src/points/interfaces/IPointsView.sol @@ -5,12 +5,19 @@ pragma solidity ^0.8.20; /// @notice This interface defines the read-only functions for querying the state of a non-transferable token system /// @dev Implement this interface for contracts that need to provide a view into the Points system interface IPointsView { + struct Holder { + address user; + uint256 balance; + } + /// @notice Struct containing basic information about the Points contract /// @dev This struct should be used to return general data about the Points system struct PointsData { address contractAddress; /// @notice The address of the Points contract string name; /// @notice The name of the Points token string symbol; /// @notice The symbol of the Points token + uint256 totalSupply; /// @notice The total supply of Points + Holder[] top10Holders; /// @notice The top 10 holders of the Points } /// @notice Struct containing user-specific information