diff --git a/pkgs/contract/contracts/timeframe/HatsTimeFrameModule.sol b/pkgs/contract/contracts/timeframe/HatsTimeFrameModule.sol index 24b06cc..1541b64 100644 --- a/pkgs/contract/contracts/timeframe/HatsTimeFrameModule.sol +++ b/pkgs/contract/contracts/timeframe/HatsTimeFrameModule.sol @@ -10,8 +10,18 @@ contract HatsTimeFrameModule is HatsModule, IHatsTimeFrameModule { + // hatId => wearer => wore timestamp mapping(uint256 => mapping(address => uint256)) private woreTime; + // hatId => wearer => last deactivation timestamp + mapping(uint256 => mapping(address => uint256)) private deactivatedTime; + + // hatId => wearer => total active time + mapping(uint256 => mapping(address => uint256)) private totalActiveTime; + + // hatId => wearer => isActive + mapping(uint256 => mapping(address => bool)) private isActive; + /** * @dev Constructor to initialize the trusted forwarder. * @param _trustedForwarder Address of the trusted forwarder contract. @@ -22,8 +32,37 @@ contract HatsTimeFrameModule is ) ERC2771Context(_trustedForwarder) HatsModule(_version) {} function mintHat(uint256 hatId, address wearer) external { - HATS().mintHat(hatId, wearer); _setWoreTime(wearer, hatId); + isActive[hatId][wearer] = true; + HATS().mintHat(hatId, wearer); + } + + /** + * @dev Deactivate the hat, pausing the contribution time. + * Calculate the contribution time up to deactivation. + * @param wearer The address of the person who received the hat. + * @param hatId The ID of the hat that was minted. + */ + function deactivate(uint256 hatId, address wearer) external { + // msg.sender should be the owner of the hat or parent hat owner + require(isActive[hatId][wearer], "Hat is already inactive"); + isActive[hatId][wearer] = false; + deactivatedTime[hatId][wearer] = block.timestamp; + totalActiveTime[hatId][wearer] += + block.timestamp - + woreTime[hatId][wearer]; + } + + /** + * @dev Reactivate the hat, resuming the contribution time. + * Reset woreTime for new active period. + * @param wearer The address of the person who received the hat. + * @param hatId The ID of the hat that was minted. + */ + function reactivate(uint256 hatId, address wearer) external { + require(!isActive[hatId][wearer], "Hat is already active"); + isActive[hatId][wearer] = true; + woreTime[hatId][wearer] = block.timestamp; } /** @@ -50,6 +89,8 @@ contract HatsTimeFrameModule is /** * @dev Gets the elapsed time in seconds since the specific hat was minted for a specific address. + * If the hat is active, calculate time from the last wear time to the current time. + * If the hat is inactive, calculate time up to the deactivation. * @param wearer The address of the person who received the hat. * @param hatId The ID of the hat that was minted. * @return The elapsed time in seconds. @@ -58,12 +99,14 @@ contract HatsTimeFrameModule is address wearer, uint256 hatId ) external view returns (uint256) { - uint256 mintTime = woreTime[hatId][wearer]; - require( - mintTime != 0, - "Hat has not been minted for this wearer and hatId" - ); - return block.timestamp - mintTime; + uint256 activeTime = totalActiveTime[hatId][wearer]; + + if (isActive[hatId][wearer]) { + // If active, calculate time from the last woreTime to the current time + activeTime += block.timestamp - woreTime[hatId][wearer]; + } + + return activeTime; } /** diff --git a/pkgs/contract/test/HatsTimeFrameModule.ts b/pkgs/contract/test/HatsTimeFrameModule.ts index 2ca5005..86096e5 100644 --- a/pkgs/contract/test/HatsTimeFrameModule.ts +++ b/pkgs/contract/test/HatsTimeFrameModule.ts @@ -134,29 +134,90 @@ describe("HatsTimeFrameModule", () => { } } + const initialTime = BigInt(await time.latest()); + await HatsTimeFrameModule.write.mintHat([ + roleHatId, + address1.account?.address!, + ]); + + const afterMintTime = BigInt(await time.latest()); + + let woreTime = await HatsTimeFrameModule.read.getWoreTime([ + address1.account?.address!, + roleHatId, + ]); + + expect(woreTime).to.equal(afterMintTime); + + await time.increaseTo(initialTime + 100n); + + const currentTime1 = BigInt(await time.latest()); + + let expectedElapsedTime = currentTime1 - woreTime; + + let elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([ + address1.account?.address!, roleHatId, + ]); + + expect(elapsedTime).to.equal(expectedElapsedTime); + + await time.increaseTo(initialTime + 200n); + + const currentTime2 = BigInt(await time.latest()); + + expectedElapsedTime = currentTime2 - woreTime; + + elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([ address1.account?.address!, + roleHatId, ]); - expect( - await Hats.read.balanceOf([address1.account?.address!, roleHatId]) - ).equal(BigInt(1)); + expect(elapsedTime).to.equal(expectedElapsedTime); - expect( - await HatsTimeFrameModule.read.getWoreTime([ - address1.account?.address!, - roleHatId, - ]) - ).equal(BigInt(await time.latest())); + await HatsTimeFrameModule.write.deactivate([ + roleHatId, + address1.account?.address!, + ]); - await time.increase(100); + const afterDeactivateTime = BigInt(await time.latest()); - expect( - await HatsTimeFrameModule.read.getWearingElapsedTime([ - address1.account?.address!, - roleHatId, - ]) - ).equal(BigInt(100)); + const totalActiveTimeAfterDeactivation = afterDeactivateTime - woreTime; + + expectedElapsedTime = totalActiveTimeAfterDeactivation; + + // Increase time to initialTime + 250 seconds (during inactivity) + await time.increaseTo(initialTime + 250n); + + // Elapsed time should remain the same + elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([ + address1.account?.address!, + roleHatId, + ]); + + expect(elapsedTime).to.equal(expectedElapsedTime); + + // Reactivate the hat + await HatsTimeFrameModule.write.reactivate([ + roleHatId, + address1.account?.address!, + ]); + + // Get woreTime after reactivation + woreTime = BigInt(await time.latest()); + + await time.increaseTo(initialTime + 350n); + + const currentTime3 = BigInt(await time.latest()); + + expectedElapsedTime = totalActiveTimeAfterDeactivation + (currentTime3 - woreTime); + + elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([ + address1.account?.address!, + roleHatId, + ]); + + expect(elapsedTime).to.equal(expectedElapsedTime); }); -}); +}); \ No newline at end of file