Skip to content

Instantly share code, notes, and snippets.

@pingiun
Created August 21, 2020 17:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pingiun/6cd34b8b4564587cee59c301a915c4ad to your computer and use it in GitHub Desktop.
Save pingiun/6cd34b8b4564587cee59c301a915c4ad to your computer and use it in GitHub Desktop.
Habits testing
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)
// 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);
}
}
// 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