Created
August 21, 2020 17:00
-
-
Save pingiun/6cd34b8b4564587cee59c301a915c4ad to your computer and use it in GitHub Desktop.
Habits testing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import smartpy as sp | |
class Habits(sp.Contract): | |
def __init__(self, mapping=None): | |
self.init_type( | |
sp.TRecord( | |
burned=sp.TInt, | |
mapping=sp.TBigMap( | |
sp.TKeyHash, | |
sp.TRecord( | |
goal=sp.TBytes, | |
goalAmount=sp.TNat, | |
lengthOfTime=sp.TInt, | |
penalty=sp.TMutez, | |
checker=sp.TAddress, | |
retour=sp.TAddress, | |
startTime=sp.TTimestamp, | |
deadLine=sp.TTimestamp, | |
stake=sp.TMutez, | |
) | |
) | |
) | |
) | |
if mapping is None: | |
mapping = sp.big_map() | |
self.init(burned = 0, | |
mapping = mapping) | |
@sp.entry_point | |
def start_habit(self, key, goal, goalAmount, lengthOfTime, penalty, checker, retour): | |
sp.set_type(key, sp.TKeyHash) | |
sp.set_type(goal, sp.TBytes) | |
sp.set_type(goalAmount, sp.TNat) | |
# Int because add_seconds does not support Nat | |
sp.set_type(lengthOfTime, sp.TInt) | |
sp.set_type(penalty, sp.TMutez) | |
sp.set_type(checker, sp.TAddress) | |
sp.set_type(retour, sp.TAddress) | |
sp.verify(sp.amount > sp.mutez(0), message="Some stake is necessary") | |
sp.verify(goalAmount > 0, message="Goal should be some time in the future") | |
sp.verify(lengthOfTime > 0, message="Goal should be some time in the future") | |
sp.verify(penalty <= sp.amount, message="Penalty can not be higher than stake") | |
sp.verify(~self.data.mapping.contains(key), message="Key must not be used") | |
self.data.mapping[key] = sp.record( | |
goal=goal, | |
goalAmount=goalAmount, | |
lengthOfTime=lengthOfTime, | |
penalty=penalty, | |
checker=checker, | |
retour=retour, | |
startTime=sp.now, | |
deadLine=sp.now.add_seconds(lengthOfTime), | |
stake=sp.amount | |
) | |
@sp.entry_point | |
def mark_done(self, key): | |
sp.set_type(key, sp.TKeyHash) | |
habit = sp.local("habit", self.data.mapping[key]) | |
sp.verify(habit.value.checker == sp.sender, message="Only checker can mark as done") | |
sp.verify(habit.value.goalAmount != 0, message="This habit is finished") | |
sp.verify(habit.value.startTime <= sp.now, message="Current time period must be started") | |
# Safe ABS because non zero check above | |
habit.value.goalAmount = abs(habit.value.goalAmount - 1) | |
wasStartTime = sp.local("wasStartTime", habit.value.startTime) | |
wasDeadline = sp.local("wasDeadline", habit.value.deadLine) | |
habit.value.startTime = habit.value.startTime.add_seconds(habit.value.lengthOfTime) | |
habit.value.deadLine = habit.value.deadLine.add_seconds(habit.value.lengthOfTime) | |
sp.if sp.now > wasDeadline.value: | |
self.process_failure(habit.value) | |
sp.else: | |
sp.if habit.value.goalAmount == 0: | |
sp.send(habit.value.retour, habit.value.stake) | |
habit.value.stake = sp.mutez(0) | |
sp.else: | |
pass | |
@sp.global_lambda | |
def process_failure(habit): | |
sp.set_type( | |
habit, | |
sp.TRecord( | |
goal=sp.TBytes, | |
goalAmount=sp.TNat, | |
lengthOfTime=sp.TInt, | |
penalty=sp.TMutez, | |
checker=sp.TAddress, | |
retour=sp.TAddress, | |
startTime=sp.TTimestamp, | |
deadLine=sp.TTimestamp, | |
stake=sp.TMutez, | |
) | |
) | |
sp.if habit.stake >= habit.penalty && habit.goalAmount > 0: | |
pass | |
# Tests | |
@sp.add_test(name = "Welcome") | |
def test(): | |
# We define a test scenario, together with some outputs and checks | |
scenario = sp.test_scenario() | |
scenario.h1("Welcome") | |
# We first define a contract and add it to the scenario | |
c1 = Habits() | |
scenario += c1 | |
admin = sp.test_account("Administrator") | |
# # And call some of its entry points | |
# scenario += c1.start_habit( | |
# sp.record( | |
# key=sp.hash_key(sp.key('1')), | |
# goal=sp.pack("test"), | |
# goalAmount=sp.nat(1), | |
# lengthOfTime=sp.int(1), | |
# penalty=sp.mutez(1), | |
# checker=admin.address, | |
# retour=admin.address, | |
# ) | |
# ) | |
# scenario += c1.start_habit(2) | |
# scenario += c1.start_habit(2) | |
# scenario += c1.start_habit(2) | |
# # Finally, we check its final storage | |
# scenario.verify(c1.data.burned == 2) | |
# scenario.verify(c1.data.mapping[1] == 1) | |
# scenario.verify(c1.data.mapping[2] == 3) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity ^0.7.0; | |
import "Owner.sol"; | |
/** | |
* Track habits with a financial stake if you fail. | |
*/ | |
contract Habits is Owner { | |
struct Habit { | |
address checker; | |
address payable retour; | |
bytes goal; | |
uint goalAmount; | |
uint lengthOfTime; | |
uint penalty; | |
uint startTime; | |
uint deadline; | |
uint stake; | |
} | |
mapping (uint => Habit) allHabits; | |
uint burned; | |
event DoneSuccess(uint indexed key, address indexed starter, uint startTime); | |
event DoneFail(uint indexed key, address indexed starter, uint startTime); | |
/** | |
* @dev Start habit tracking | |
* @param _key unique key to identify the habit | |
* @param _goal user supplied goal information | |
* @param _goalAmount how long should the habit be tracked? | |
* @param _lengthOfTime amount of seconds each time period should last. e.g. use 60*60*24 for a daily habit | |
* @param _penalty what should be removed from the stake (message value) each time a period of time is missed | |
*/ | |
function startHabit(uint _key, bytes calldata _goal, uint _goalAmount, uint _lengthOfTime, uint _penalty, address _checker, address payable _retour) public payable { | |
require(msg.value != 0, "Some stake is necessary"); | |
require(_goalAmount != 0, "Goal should be some time in the future"); | |
require(_lengthOfTime != 0, "Goal should be some time in the future"); | |
require(_penalty <= msg.value, "Penalty can not be higher than stake"); | |
Habit storage h = allHabits[_key]; | |
require(h.lengthOfTime == 0, "Key is used already"); | |
h.checker = _checker; | |
h.retour = _retour; | |
h.goal = _goal; | |
h.goalAmount = _goalAmount; | |
h.lengthOfTime = _lengthOfTime; | |
h.penalty = _penalty; | |
h.startTime = block.timestamp; | |
h.deadline = block.timestamp + _lengthOfTime; | |
h.stake = msg.value; | |
} | |
/** | |
* @dev Mark this lengthOfTime as done | |
* @param _key the habit key to mark as done | |
*/ | |
function markDone(uint _key) public returns (bool) { | |
Habit storage h = allHabits[_key]; | |
require(msg.sender == h.checker, "Only habit starter can mark as done"); | |
require(h.goalAmount != 0, "This habit is finished"); | |
require(h.startTime <= block.timestamp, "Current time period must be started"); | |
h.goalAmount = h.goalAmount - 1; | |
uint wasStartTime = h.startTime; | |
uint wasDeadline = h.deadline; | |
h.startTime = h.startTime + h.lengthOfTime; | |
h.deadline = h.deadline + h.lengthOfTime; | |
if (block.timestamp > wasDeadline) { | |
// Oh no, you were too late... | |
emit DoneFail(_key, h.checker, wasStartTime); | |
processFailure(h); | |
return false; | |
} else { | |
emit DoneSuccess(_key, h.checker, wasStartTime); | |
if (h.goalAmount == 0) { | |
// You've reached your goal! Congratulations! | |
uint amount = h.stake; | |
h.stake = 0; | |
h.retour.transfer(amount); | |
return true; | |
} else { | |
// A succesful lengthOfTime done, nice. | |
return true; | |
} | |
} | |
} | |
/** | |
* @dev Allow everyone to check a habit for public accountability | |
* @param _key the key to check the deadline for | |
*/ | |
function check(uint _key) public returns (bool) { | |
Habit storage h = allHabits[_key]; | |
if (block.timestamp > h.deadline) { | |
// Oh no, you were too late... | |
emit DoneFail(_key, h.checker, h.startTime); | |
h.startTime = h.startTime + h.lengthOfTime; | |
h.deadline = h.deadline + h.lengthOfTime; | |
processFailure(h); | |
return false; | |
} else { | |
return true; | |
} | |
} | |
/** | |
* @dev Check failure mode and substract penalty if necessary. | |
* Expects deadline/startTime and emitting to be done already. | |
* @param h the habit to process | |
*/ | |
function processFailure(Habit storage h) private { | |
if (h.stake >= h.penalty && h.goalAmount > 0) { | |
// There's enough penalty left and goal time has not been reached | |
// so remove one penalty | |
burned = burned + h.penalty; | |
h.stake = h.stake - h.penalty; | |
} else if (h.stake >= h.penalty && h.goalAmount == 0) { | |
///Too late, but this was the last one. Return remainder after penalty. | |
burned = burned + h.penalty; | |
h.stake = h.stake - h.penalty; | |
uint amount = h.stake; | |
h.stake = 0; | |
h.retour.transfer(amount); | |
} else { | |
// No stake left, this habit has failed :( | |
burned = burned + h.stake; | |
h.stake = 0; | |
} | |
} | |
/** | |
* @dev Raise the stakes by some amount | |
* @param _key the key to raise the stakes for | |
*/ | |
function raiseStakes(uint _key) public payable { | |
require(msg.value != 0, "Some stake is necessary"); | |
Habit storage h = allHabits[_key]; | |
require(h.goalAmount != 0, "This habit is finished"); | |
h.stake = h.stake + msg.value; | |
} | |
/** | |
* @dev Get the deadline for this habit | |
* @param _key the key to check the deadline for | |
*/ | |
function getDeadline(uint _key) external view returns (uint) { | |
Habit storage h = allHabits[_key]; | |
return h.deadline; | |
} | |
/** | |
* @dev Withdraw burned wei | |
*/ | |
function withdraw() public isOwner { | |
uint amount = burned; | |
burned = 0; | |
msg.sender.transfer(amount); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity ^0.7.0; | |
/** | |
* @title Owner | |
* @dev Set & change owner | |
*/ | |
abstract contract Owner { | |
address private owner; | |
// event for EVM logging | |
event OwnerSet(address indexed oldOwner, address indexed newOwner); | |
// modifier to check if caller is owner | |
modifier isOwner() { | |
// If the first argument of 'require' evaluates to 'false', execution terminates and all | |
// changes to the state and to Ether balances are reverted. | |
// This used to consume all gas in old EVM versions, but not anymore. | |
// It is often a good idea to use 'require' to check if functions are called correctly. | |
// As a second argument, you can also provide an explanation about what went wrong. | |
require(msg.sender == owner, "Caller is not owner"); | |
_; | |
} | |
/** | |
* @dev Set contract deployer as owner | |
*/ | |
constructor() { | |
owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor | |
emit OwnerSet(address(0), owner); | |
} | |
/** | |
* @dev Change owner | |
* @param newOwner address of new owner | |
*/ | |
function changeOwner(address newOwner) public isOwner { | |
emit OwnerSet(owner, newOwner); | |
owner = newOwner; | |
} | |
/** | |
* @dev Return owner address | |
* @return address of owner | |
*/ | |
function getOwner() external view returns (address) { | |
return owner; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment