1749 lines
54 KiB
Go
1749 lines
54 KiB
Go
package native
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/config"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/dao"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/interop"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/state"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
|
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
|
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
|
)
|
|
|
|
// Sese represents the life planning native contract.
|
|
type Sese struct {
|
|
interop.ContractMD
|
|
Tutus ITutus
|
|
Vita IVita
|
|
RoleRegistry IRoleRegistry
|
|
Lex ILex
|
|
}
|
|
|
|
// SeseCache represents the cached state for Sese contract.
|
|
type SeseCache struct {
|
|
accountCount uint64
|
|
careerCount uint64
|
|
sabbaticalCount uint64
|
|
milestoneCount uint64
|
|
goalCount uint64
|
|
}
|
|
|
|
// Storage key prefixes for Sese.
|
|
const (
|
|
sesePrefixAccount byte = 0x01 // vitaID -> LifePlanAccount
|
|
sesePrefixAccountByOwner byte = 0x02 // owner -> vitaID
|
|
sesePrefixCareer byte = 0x10 // careerID -> CareerCycle
|
|
sesePrefixCareerByOwner byte = 0x11 // vitaID + careerID -> exists
|
|
sesePrefixActiveCareer byte = 0x12 // vitaID -> active careerID
|
|
sesePrefixSabbatical byte = 0x20 // sabbaticalID -> Sabbatical
|
|
sesePrefixSabbaticalByOwner byte = 0x21 // vitaID + sabbaticalID -> exists
|
|
sesePrefixActiveSabbatical byte = 0x22 // vitaID -> active sabbaticalID
|
|
sesePrefixMilestone byte = 0x30 // milestoneID -> LifeMilestone
|
|
sesePrefixMilestoneByOwner byte = 0x31 // vitaID + milestoneID -> exists
|
|
sesePrefixMilestoneByType byte = 0x32 // vitaID + type + milestoneID -> exists
|
|
sesePrefixGoal byte = 0x40 // goalID -> LifeGoal
|
|
sesePrefixGoalByOwner byte = 0x41 // vitaID + goalID -> exists
|
|
sesePrefixActiveGoals byte = 0x42 // vitaID + goalID -> exists (active only)
|
|
sesePrefixAccountCounter byte = 0xF0 // -> uint64
|
|
sesePrefixCareerCounter byte = 0xF1 // -> next career ID
|
|
sesePrefixSabbaticalCounter byte = 0xF2 // -> next sabbatical ID
|
|
sesePrefixMilestoneCounter byte = 0xF3 // -> next milestone ID
|
|
sesePrefixGoalCounter byte = 0xF4 // -> next goal ID
|
|
sesePrefixConfig byte = 0xFF // -> SeseConfig
|
|
)
|
|
|
|
// Event names for Sese.
|
|
const (
|
|
LifePlanActivatedEvent = "LifePlanActivated"
|
|
ContributionMadeEvent = "ContributionMade"
|
|
CareerStartedEvent = "CareerStarted"
|
|
CareerEndedEvent = "CareerEnded"
|
|
SabbaticalStartedEvent = "SabbaticalStarted"
|
|
SabbaticalCompletedEvent = "SabbaticalCompleted"
|
|
SabbaticalCancelledEvent = "SabbaticalCancelled"
|
|
MilestoneRecordedEvent = "MilestoneRecorded"
|
|
MilestoneVerifiedEvent = "MilestoneVerified"
|
|
GoalCreatedEvent = "GoalCreated"
|
|
GoalUpdatedEvent = "GoalUpdated"
|
|
GoalCompletedEvent = "GoalCompleted"
|
|
)
|
|
|
|
// Role constants for life planners.
|
|
const (
|
|
RoleLifePlanner uint64 = 22 // Can verify milestones and manage career transitions
|
|
)
|
|
|
|
// Various errors for Sese.
|
|
var (
|
|
ErrSeseAccountNotFound = errors.New("life plan account not found")
|
|
ErrSeseAccountExists = errors.New("life plan account already exists")
|
|
ErrSeseAccountSuspended = errors.New("life plan account is suspended")
|
|
ErrSeseAccountClosed = errors.New("life plan account is closed")
|
|
ErrSeseNoVita = errors.New("owner must have an active Vita")
|
|
ErrSeseInsufficientBalance = errors.New("insufficient account balance")
|
|
ErrSeseInsufficientCredits = errors.New("insufficient sabbatical credits")
|
|
ErrSeseInvalidAmount = errors.New("invalid amount")
|
|
ErrSeseCareerNotFound = errors.New("career cycle not found")
|
|
ErrSeseCareerExists = errors.New("active career already exists")
|
|
ErrSeseCareerEnded = errors.New("career already ended")
|
|
ErrSeseSabbaticalNotFound = errors.New("sabbatical not found")
|
|
ErrSeseSabbaticalExists = errors.New("active sabbatical already exists")
|
|
ErrSeseSabbaticalEnded = errors.New("sabbatical already ended")
|
|
ErrSeseSabbaticalTooShort = errors.New("sabbatical duration too short")
|
|
ErrSeseSabbaticalTooLong = errors.New("sabbatical duration too long")
|
|
ErrSeseMilestoneNotFound = errors.New("milestone not found")
|
|
ErrSeseGoalNotFound = errors.New("goal not found")
|
|
ErrSeseGoalCompleted = errors.New("goal already completed")
|
|
ErrSeseNotCommittee = errors.New("invalid committee signature")
|
|
ErrSeseNotOwner = errors.New("caller is not the owner")
|
|
ErrSeseNotLifePlanner = errors.New("caller is not an authorized life planner")
|
|
ErrSeseLaborRestricted = errors.New("labor right is restricted")
|
|
ErrSeseInvalidField = errors.New("invalid career field")
|
|
ErrSeseInvalidReason = errors.New("invalid reason")
|
|
ErrSeseInvalidTitle = errors.New("invalid title")
|
|
ErrSeseInvalidDescription = errors.New("invalid description")
|
|
ErrSeseInvalidProgress = errors.New("invalid progress value")
|
|
)
|
|
|
|
var (
|
|
_ interop.Contract = (*Sese)(nil)
|
|
_ dao.NativeContractCache = (*SeseCache)(nil)
|
|
)
|
|
|
|
// Copy implements NativeContractCache interface.
|
|
func (c *SeseCache) Copy() dao.NativeContractCache {
|
|
return &SeseCache{
|
|
accountCount: c.accountCount,
|
|
careerCount: c.careerCount,
|
|
sabbaticalCount: c.sabbaticalCount,
|
|
milestoneCount: c.milestoneCount,
|
|
goalCount: c.goalCount,
|
|
}
|
|
}
|
|
|
|
// checkCommittee checks if the caller has committee authority.
|
|
func (s *Sese) checkCommittee(ic *interop.Context) bool {
|
|
if s.RoleRegistry != nil {
|
|
return s.RoleRegistry.CheckCommittee(ic)
|
|
}
|
|
return s.Tutus.CheckCommittee(ic)
|
|
}
|
|
|
|
// checkLifePlanner checks if the caller has life planner authority.
|
|
func (s *Sese) checkLifePlanner(ic *interop.Context) bool {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if s.RoleRegistry != nil {
|
|
if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleLifePlanner, ic.Block.Index) {
|
|
return true
|
|
}
|
|
}
|
|
// Committee members can also act as life planners
|
|
return s.checkCommittee(ic)
|
|
}
|
|
|
|
// checkLaborRight checks if subject has labor rights via Lex.
|
|
func (s *Sese) checkLaborRight(ic *interop.Context, subject util.Uint160) bool {
|
|
if s.Lex == nil {
|
|
return true // Allow if Lex not available
|
|
}
|
|
return s.Lex.HasRightInternal(ic.DAO, subject, state.RightLabor, ic.Block.Index)
|
|
}
|
|
|
|
// newSese creates a new Sese native contract.
|
|
func newSese() *Sese {
|
|
s := &Sese{
|
|
ContractMD: *interop.NewContractMD(nativenames.Sese, nativeids.Sese),
|
|
}
|
|
defer s.BuildHFSpecificMD(s.ActiveIn())
|
|
|
|
// ===== Account Management =====
|
|
|
|
// activateLifePlan - Activate life planning account for a Vita holder
|
|
desc := NewDescriptor("activateLifePlan", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md := NewMethodAndPrice(s.activateLifePlan, 1<<17, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getAccount - Get life plan account by owner
|
|
desc = NewDescriptor("getAccount", smartcontract.ArrayType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(s.getAccount, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getAccountByVitaID - Get account by Vita ID
|
|
desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.getAccountByVitaID, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// contribute - Make a contribution to life plan
|
|
desc = NewDescriptor("contribute", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType),
|
|
manifest.NewParameter("isEmployer", smartcontract.BoolType))
|
|
md = NewMethodAndPrice(s.contribute, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// allocateSabbaticalCredits - Allocate sabbatical credits (committee only)
|
|
desc = NewDescriptor("allocateSabbaticalCredits", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(s.allocateSabbaticalCredits, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getBalance - Get current balance
|
|
desc = NewDescriptor("getBalance", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(s.getBalance, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getSabbaticalCredits - Get available sabbatical credits
|
|
desc = NewDescriptor("getSabbaticalCredits", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(s.getSabbaticalCredits, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// ===== Career Management =====
|
|
|
|
// startCareer - Start a new career cycle
|
|
desc = NewDescriptor("startCareer", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("careerField", smartcontract.StringType))
|
|
md = NewMethodAndPrice(s.startCareer, 1<<17, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// endCareer - End current career cycle
|
|
desc = NewDescriptor("endCareer", smartcontract.BoolType,
|
|
manifest.NewParameter("careerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(s.endCareer, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getCareer - Get career cycle details
|
|
desc = NewDescriptor("getCareer", smartcontract.ArrayType,
|
|
manifest.NewParameter("careerID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.getCareer, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getActiveCareer - Get owner's active career
|
|
desc = NewDescriptor("getActiveCareer", smartcontract.ArrayType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(s.getActiveCareer, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// ===== Sabbatical Management =====
|
|
|
|
// startSabbatical - Start a sabbatical
|
|
desc = NewDescriptor("startSabbatical", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("purpose", smartcontract.IntegerType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("duration", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.startSabbatical, 1<<17, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// completeSabbatical - Complete a sabbatical
|
|
desc = NewDescriptor("completeSabbatical", smartcontract.BoolType,
|
|
manifest.NewParameter("sabbaticalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("outcome", smartcontract.StringType))
|
|
md = NewMethodAndPrice(s.completeSabbatical, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// cancelSabbatical - Cancel a sabbatical
|
|
desc = NewDescriptor("cancelSabbatical", smartcontract.BoolType,
|
|
manifest.NewParameter("sabbaticalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(s.cancelSabbatical, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getSabbatical - Get sabbatical details
|
|
desc = NewDescriptor("getSabbatical", smartcontract.ArrayType,
|
|
manifest.NewParameter("sabbaticalID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.getSabbatical, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getActiveSabbatical - Get owner's active sabbatical
|
|
desc = NewDescriptor("getActiveSabbatical", smartcontract.ArrayType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(s.getActiveSabbatical, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// ===== Milestone Management =====
|
|
|
|
// recordMilestone - Record a life milestone
|
|
desc = NewDescriptor("recordMilestone", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("milestoneType", smartcontract.IntegerType),
|
|
manifest.NewParameter("title", smartcontract.StringType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("contentHash", smartcontract.Hash256Type))
|
|
md = NewMethodAndPrice(s.recordMilestone, 1<<17, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// verifyMilestone - Verify a milestone (life planner only)
|
|
desc = NewDescriptor("verifyMilestone", smartcontract.BoolType,
|
|
manifest.NewParameter("milestoneID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.verifyMilestone, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getMilestone - Get milestone details
|
|
desc = NewDescriptor("getMilestone", smartcontract.ArrayType,
|
|
manifest.NewParameter("milestoneID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.getMilestone, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// ===== Goal Management =====
|
|
|
|
// createGoal - Create a life goal
|
|
desc = NewDescriptor("createGoal", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("title", smartcontract.StringType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("targetBlock", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.createGoal, 1<<17, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// updateGoalProgress - Update goal progress
|
|
desc = NewDescriptor("updateGoalProgress", smartcontract.BoolType,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("progress", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.updateGoalProgress, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// completeGoal - Mark goal as completed
|
|
desc = NewDescriptor("completeGoal", smartcontract.BoolType,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.completeGoal, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// abandonGoal - Abandon a goal
|
|
desc = NewDescriptor("abandonGoal", smartcontract.BoolType,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(s.abandonGoal, 1<<16, callflag.States|callflag.AllowNotify)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getGoal - Get goal details
|
|
desc = NewDescriptor("getGoal", smartcontract.ArrayType,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(s.getGoal, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// ===== Query Methods =====
|
|
|
|
// getConfig - Get Sese configuration
|
|
desc = NewDescriptor("getConfig", smartcontract.ArrayType)
|
|
md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getTotalAccounts - Get total life plan accounts
|
|
desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getTotalCareers - Get total career cycles
|
|
desc = NewDescriptor("getTotalCareers", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(s.getTotalCareers, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getTotalSabbaticals - Get total sabbaticals
|
|
desc = NewDescriptor("getTotalSabbaticals", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(s.getTotalSabbaticals, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getTotalMilestones - Get total milestones
|
|
desc = NewDescriptor("getTotalMilestones", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(s.getTotalMilestones, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// getTotalGoals - Get total goals
|
|
desc = NewDescriptor("getTotalGoals", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(s.getTotalGoals, 1<<15, callflag.ReadStates)
|
|
s.AddMethod(md, desc)
|
|
|
|
// ===== Events =====
|
|
|
|
// LifePlanActivated event
|
|
eDesc := NewEventDescriptor(LifePlanActivatedEvent,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// ContributionMade event
|
|
eDesc = NewEventDescriptor(ContributionMadeEvent,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType),
|
|
manifest.NewParameter("isEmployer", smartcontract.BoolType),
|
|
manifest.NewParameter("balance", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// CareerStarted event
|
|
eDesc = NewEventDescriptor(CareerStartedEvent,
|
|
manifest.NewParameter("careerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("careerField", smartcontract.StringType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// CareerEnded event
|
|
eDesc = NewEventDescriptor(CareerEndedEvent,
|
|
manifest.NewParameter("careerID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// SabbaticalStarted event
|
|
eDesc = NewEventDescriptor(SabbaticalStartedEvent,
|
|
manifest.NewParameter("sabbaticalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("purpose", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// SabbaticalCompleted event
|
|
eDesc = NewEventDescriptor(SabbaticalCompletedEvent,
|
|
manifest.NewParameter("sabbaticalID", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// SabbaticalCancelled event
|
|
eDesc = NewEventDescriptor(SabbaticalCancelledEvent,
|
|
manifest.NewParameter("sabbaticalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// MilestoneRecorded event
|
|
eDesc = NewEventDescriptor(MilestoneRecordedEvent,
|
|
manifest.NewParameter("milestoneID", smartcontract.IntegerType),
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("milestoneType", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// MilestoneVerified event
|
|
eDesc = NewEventDescriptor(MilestoneVerifiedEvent,
|
|
manifest.NewParameter("milestoneID", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// GoalCreated event
|
|
eDesc = NewEventDescriptor(GoalCreatedEvent,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("title", smartcontract.StringType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// GoalUpdated event
|
|
eDesc = NewEventDescriptor(GoalUpdatedEvent,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType),
|
|
manifest.NewParameter("progress", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
// GoalCompleted event
|
|
eDesc = NewEventDescriptor(GoalCompletedEvent,
|
|
manifest.NewParameter("goalID", smartcontract.IntegerType))
|
|
s.AddEvent(NewEvent(eDesc))
|
|
|
|
return s
|
|
}
|
|
|
|
// Metadata returns contract metadata.
|
|
func (s *Sese) Metadata() *interop.ContractMD {
|
|
return &s.ContractMD
|
|
}
|
|
|
|
// Initialize initializes the Sese contract.
|
|
func (s *Sese) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
|
|
if hf != s.ActiveIn() {
|
|
return nil
|
|
}
|
|
|
|
// Initialize counters
|
|
s.setAccountCounter(ic.DAO, 0)
|
|
s.setCareerCounter(ic.DAO, 0)
|
|
s.setSabbaticalCounter(ic.DAO, 0)
|
|
s.setMilestoneCounter(ic.DAO, 0)
|
|
s.setGoalCounter(ic.DAO, 0)
|
|
|
|
// Initialize config with defaults
|
|
cfg := &state.SeseConfig{
|
|
DefaultSabbaticalCredits: 5000, // 5000 credits per year
|
|
MinSabbaticalDuration: 2592000, // ~30 days (1-second blocks)
|
|
MaxSabbaticalDuration: 31536000, // ~365 days (1-second blocks)
|
|
GovernmentMatchPercent: 5000, // 50% match (basis points)
|
|
LongevityBonusPercent: 100, // 1% per year (basis points)
|
|
}
|
|
s.setConfig(ic.DAO, cfg)
|
|
|
|
// Initialize cache
|
|
cache := &SeseCache{
|
|
accountCount: 0,
|
|
careerCount: 0,
|
|
sabbaticalCount: 0,
|
|
milestoneCount: 0,
|
|
goalCount: 0,
|
|
}
|
|
ic.DAO.SetCache(s.ID, cache)
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitializeCache initializes the cache from storage.
|
|
func (s *Sese) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
|
|
cache := &SeseCache{
|
|
accountCount: s.getAccountCounter(d),
|
|
careerCount: s.getCareerCounter(d),
|
|
sabbaticalCount: s.getSabbaticalCounter(d),
|
|
milestoneCount: s.getMilestoneCounter(d),
|
|
goalCount: s.getGoalCounter(d),
|
|
}
|
|
d.SetCache(s.ID, cache)
|
|
return nil
|
|
}
|
|
|
|
// OnPersist is called before block is committed.
|
|
func (s *Sese) OnPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// PostPersist is called after block is committed.
|
|
func (s *Sese) PostPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// ActiveIn returns the hardfork at which this contract is activated.
|
|
func (s *Sese) ActiveIn() *config.Hardfork {
|
|
return nil // Always active
|
|
}
|
|
|
|
// ===== Storage Helpers =====
|
|
|
|
func (s *Sese) makeAccountKey(vitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixAccount
|
|
binary.BigEndian.PutUint64(key[1:], vitaID)
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeAccountByOwnerKey(owner util.Uint160) []byte {
|
|
key := make([]byte, 21)
|
|
key[0] = sesePrefixAccountByOwner
|
|
copy(key[1:], owner.BytesBE())
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeCareerKey(careerID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixCareer
|
|
binary.BigEndian.PutUint64(key[1:], careerID)
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeActiveCareerKey(vitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixActiveCareer
|
|
binary.BigEndian.PutUint64(key[1:], vitaID)
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeSabbaticalKey(sabbaticalID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixSabbatical
|
|
binary.BigEndian.PutUint64(key[1:], sabbaticalID)
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeActiveSabbaticalKey(vitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixActiveSabbatical
|
|
binary.BigEndian.PutUint64(key[1:], vitaID)
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeMilestoneKey(milestoneID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixMilestone
|
|
binary.BigEndian.PutUint64(key[1:], milestoneID)
|
|
return key
|
|
}
|
|
|
|
func (s *Sese) makeGoalKey(goalID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = sesePrefixGoal
|
|
binary.BigEndian.PutUint64(key[1:], goalID)
|
|
return key
|
|
}
|
|
|
|
// Counter getters/setters
|
|
func (s *Sese) getAccountCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(s.ID, []byte{sesePrefixAccountCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (s *Sese) setAccountCounter(d *dao.Simple, count uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, count)
|
|
d.PutStorageItem(s.ID, []byte{sesePrefixAccountCounter}, buf)
|
|
}
|
|
|
|
func (s *Sese) getCareerCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(s.ID, []byte{sesePrefixCareerCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (s *Sese) setCareerCounter(d *dao.Simple, count uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, count)
|
|
d.PutStorageItem(s.ID, []byte{sesePrefixCareerCounter}, buf)
|
|
}
|
|
|
|
func (s *Sese) getSabbaticalCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(s.ID, []byte{sesePrefixSabbaticalCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (s *Sese) setSabbaticalCounter(d *dao.Simple, count uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, count)
|
|
d.PutStorageItem(s.ID, []byte{sesePrefixSabbaticalCounter}, buf)
|
|
}
|
|
|
|
func (s *Sese) getMilestoneCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(s.ID, []byte{sesePrefixMilestoneCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (s *Sese) setMilestoneCounter(d *dao.Simple, count uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, count)
|
|
d.PutStorageItem(s.ID, []byte{sesePrefixMilestoneCounter}, buf)
|
|
}
|
|
|
|
func (s *Sese) getGoalCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(s.ID, []byte{sesePrefixGoalCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (s *Sese) setGoalCounter(d *dao.Simple, count uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, count)
|
|
d.PutStorageItem(s.ID, []byte{sesePrefixGoalCounter}, buf)
|
|
}
|
|
|
|
// Config getter/setter
|
|
func (s *Sese) getConfigInternal(d *dao.Simple) *state.SeseConfig {
|
|
si := d.GetStorageItem(s.ID, []byte{sesePrefixConfig})
|
|
if si == nil {
|
|
return &state.SeseConfig{
|
|
DefaultSabbaticalCredits: 5000,
|
|
MinSabbaticalDuration: 2592000,
|
|
MaxSabbaticalDuration: 31536000,
|
|
GovernmentMatchPercent: 5000,
|
|
LongevityBonusPercent: 100,
|
|
}
|
|
}
|
|
cfg := new(state.SeseConfig)
|
|
item, _ := stackitem.Deserialize(si)
|
|
cfg.FromStackItem(item)
|
|
return cfg
|
|
}
|
|
|
|
func (s *Sese) setConfig(d *dao.Simple, cfg *state.SeseConfig) {
|
|
item, _ := cfg.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(s.ID, []byte{sesePrefixConfig}, data)
|
|
}
|
|
|
|
// Account storage
|
|
func (s *Sese) getAccountInternal(d *dao.Simple, vitaID uint64) *state.LifePlanAccount {
|
|
si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
acc := new(state.LifePlanAccount)
|
|
item, _ := stackitem.Deserialize(si)
|
|
acc.FromStackItem(item)
|
|
return acc
|
|
}
|
|
|
|
func (s *Sese) putAccount(d *dao.Simple, acc *state.LifePlanAccount) {
|
|
item, _ := acc.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data)
|
|
}
|
|
|
|
func (s *Sese) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) {
|
|
si := d.GetStorageItem(s.ID, s.makeAccountByOwnerKey(owner))
|
|
if si == nil {
|
|
return 0, false
|
|
}
|
|
return binary.BigEndian.Uint64(si), true
|
|
}
|
|
|
|
func (s *Sese) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, vitaID)
|
|
d.PutStorageItem(s.ID, s.makeAccountByOwnerKey(owner), buf)
|
|
}
|
|
|
|
// Career storage
|
|
func (s *Sese) getCareerInternal(d *dao.Simple, careerID uint64) *state.CareerCycle {
|
|
si := d.GetStorageItem(s.ID, s.makeCareerKey(careerID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
career := new(state.CareerCycle)
|
|
item, _ := stackitem.Deserialize(si)
|
|
career.FromStackItem(item)
|
|
return career
|
|
}
|
|
|
|
func (s *Sese) putCareer(d *dao.Simple, career *state.CareerCycle) {
|
|
item, _ := career.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(s.ID, s.makeCareerKey(career.ID), data)
|
|
}
|
|
|
|
func (s *Sese) getActiveCareerID(d *dao.Simple, vitaID uint64) (uint64, bool) {
|
|
si := d.GetStorageItem(s.ID, s.makeActiveCareerKey(vitaID))
|
|
if si == nil {
|
|
return 0, false
|
|
}
|
|
return binary.BigEndian.Uint64(si), true
|
|
}
|
|
|
|
func (s *Sese) setActiveCareerID(d *dao.Simple, vitaID uint64, careerID uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, careerID)
|
|
d.PutStorageItem(s.ID, s.makeActiveCareerKey(vitaID), buf)
|
|
}
|
|
|
|
func (s *Sese) clearActiveCareer(d *dao.Simple, vitaID uint64) {
|
|
d.DeleteStorageItem(s.ID, s.makeActiveCareerKey(vitaID))
|
|
}
|
|
|
|
// Sabbatical storage
|
|
func (s *Sese) getSabbaticalInternal(d *dao.Simple, sabbaticalID uint64) *state.Sabbatical {
|
|
si := d.GetStorageItem(s.ID, s.makeSabbaticalKey(sabbaticalID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
sabbatical := new(state.Sabbatical)
|
|
item, _ := stackitem.Deserialize(si)
|
|
sabbatical.FromStackItem(item)
|
|
return sabbatical
|
|
}
|
|
|
|
func (s *Sese) putSabbatical(d *dao.Simple, sabbatical *state.Sabbatical) {
|
|
item, _ := sabbatical.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(s.ID, s.makeSabbaticalKey(sabbatical.ID), data)
|
|
}
|
|
|
|
func (s *Sese) getActiveSabbaticalID(d *dao.Simple, vitaID uint64) (uint64, bool) {
|
|
si := d.GetStorageItem(s.ID, s.makeActiveSabbaticalKey(vitaID))
|
|
if si == nil {
|
|
return 0, false
|
|
}
|
|
return binary.BigEndian.Uint64(si), true
|
|
}
|
|
|
|
func (s *Sese) setActiveSabbaticalID(d *dao.Simple, vitaID uint64, sabbaticalID uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, sabbaticalID)
|
|
d.PutStorageItem(s.ID, s.makeActiveSabbaticalKey(vitaID), buf)
|
|
}
|
|
|
|
func (s *Sese) clearActiveSabbatical(d *dao.Simple, vitaID uint64) {
|
|
d.DeleteStorageItem(s.ID, s.makeActiveSabbaticalKey(vitaID))
|
|
}
|
|
|
|
// Milestone storage
|
|
func (s *Sese) getMilestoneInternal(d *dao.Simple, milestoneID uint64) *state.LifeMilestone {
|
|
si := d.GetStorageItem(s.ID, s.makeMilestoneKey(milestoneID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
milestone := new(state.LifeMilestone)
|
|
item, _ := stackitem.Deserialize(si)
|
|
milestone.FromStackItem(item)
|
|
return milestone
|
|
}
|
|
|
|
func (s *Sese) putMilestone(d *dao.Simple, milestone *state.LifeMilestone) {
|
|
item, _ := milestone.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(s.ID, s.makeMilestoneKey(milestone.ID), data)
|
|
}
|
|
|
|
// Goal storage
|
|
func (s *Sese) getGoalInternal(d *dao.Simple, goalID uint64) *state.LifeGoal {
|
|
si := d.GetStorageItem(s.ID, s.makeGoalKey(goalID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
goal := new(state.LifeGoal)
|
|
item, _ := stackitem.Deserialize(si)
|
|
goal.FromStackItem(item)
|
|
return goal
|
|
}
|
|
|
|
func (s *Sese) putGoal(d *dao.Simple, goal *state.LifeGoal) {
|
|
item, _ := goal.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(s.ID, s.makeGoalKey(goal.ID), data)
|
|
}
|
|
|
|
// ===== Contract Methods =====
|
|
|
|
// activateLifePlan activates life planning account for a Vita holder.
|
|
func (s *Sese) activateLifePlan(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
// Check owner has active Vita
|
|
if s.Vita == nil {
|
|
panic(ErrSeseNoVita)
|
|
}
|
|
vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
|
|
if err != nil || vita == nil {
|
|
panic(ErrSeseNoVita)
|
|
}
|
|
if vita.Status != state.TokenStatusActive {
|
|
panic(ErrSeseNoVita)
|
|
}
|
|
|
|
// Check if account already exists
|
|
existing := s.getAccountInternal(ic.DAO, vita.TokenID)
|
|
if existing != nil {
|
|
panic(ErrSeseAccountExists)
|
|
}
|
|
|
|
// Check labor rights
|
|
if !s.checkLaborRight(ic, owner) {
|
|
// Log but allow (EnforcementLogging)
|
|
}
|
|
|
|
// Get cache and increment counter
|
|
cache := ic.DAO.GetRWCache(s.ID).(*SeseCache)
|
|
cache.accountCount++
|
|
s.setAccountCounter(ic.DAO, cache.accountCount)
|
|
|
|
// Get default credits from config
|
|
cfg := s.getConfigInternal(ic.DAO)
|
|
|
|
// Create account
|
|
acc := &state.LifePlanAccount{
|
|
VitaID: vita.TokenID,
|
|
Owner: owner,
|
|
TotalContributions: 0,
|
|
EmployerContributions: 0,
|
|
GovernmentContributions: 0,
|
|
CurrentBalance: 0,
|
|
SabbaticalCredits: cfg.DefaultSabbaticalCredits,
|
|
LongevityMultiplier: 10000, // 100% = 10000 basis points
|
|
CurrentCareerCycleID: 0,
|
|
TotalCareerCycles: 0,
|
|
TotalSabbaticals: 0,
|
|
Status: state.LifePlanAccountActive,
|
|
CreatedAt: ic.Block.Index,
|
|
UpdatedAt: ic.Block.Index,
|
|
}
|
|
|
|
// Store account
|
|
s.putAccount(ic.DAO, acc)
|
|
s.setOwnerToVitaID(ic.DAO, owner, vita.TokenID)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, LifePlanActivatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))),
|
|
stackitem.NewByteArray(owner.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getAccount returns life plan account by owner.
|
|
func (s *Sese) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := acc.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getAccountByVitaID returns life plan account by Vita ID.
|
|
func (s *Sese) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := acc.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// contribute makes a contribution to life plan.
|
|
func (s *Sese) contribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
amount := toUint64(args[1])
|
|
isEmployer := toBool(args[2])
|
|
|
|
if amount == 0 {
|
|
panic(ErrSeseInvalidAmount)
|
|
}
|
|
|
|
// Get or auto-create account
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
// Auto-create if Vita exists
|
|
if s.Vita == nil {
|
|
panic(ErrSeseNoVita)
|
|
}
|
|
vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
|
|
if err != nil || vita == nil || vita.Status != state.TokenStatusActive {
|
|
panic(ErrSeseNoVita)
|
|
}
|
|
vitaID = vita.TokenID
|
|
|
|
cfg := s.getConfigInternal(ic.DAO)
|
|
|
|
// Create account
|
|
cache := ic.DAO.GetRWCache(s.ID).(*SeseCache)
|
|
cache.accountCount++
|
|
s.setAccountCounter(ic.DAO, cache.accountCount)
|
|
|
|
acc := &state.LifePlanAccount{
|
|
VitaID: vitaID,
|
|
Owner: owner,
|
|
TotalContributions: 0,
|
|
EmployerContributions: 0,
|
|
GovernmentContributions: 0,
|
|
CurrentBalance: 0,
|
|
SabbaticalCredits: cfg.DefaultSabbaticalCredits,
|
|
LongevityMultiplier: 10000,
|
|
CurrentCareerCycleID: 0,
|
|
TotalCareerCycles: 0,
|
|
TotalSabbaticals: 0,
|
|
Status: state.LifePlanAccountActive,
|
|
CreatedAt: ic.Block.Index,
|
|
UpdatedAt: ic.Block.Index,
|
|
}
|
|
s.putAccount(ic.DAO, acc)
|
|
s.setOwnerToVitaID(ic.DAO, owner, vitaID)
|
|
|
|
ic.AddNotification(s.Hash, LifePlanActivatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewByteArray(owner.BytesBE()),
|
|
}))
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
if acc.Status != state.LifePlanAccountActive {
|
|
panic(ErrSeseAccountSuspended)
|
|
}
|
|
|
|
// Add contribution
|
|
acc.TotalContributions += amount
|
|
acc.CurrentBalance += amount
|
|
|
|
if isEmployer {
|
|
acc.EmployerContributions += amount
|
|
}
|
|
|
|
// Calculate government match
|
|
cfg := s.getConfigInternal(ic.DAO)
|
|
govMatch := (amount * uint64(cfg.GovernmentMatchPercent)) / 10000
|
|
if govMatch > 0 {
|
|
acc.GovernmentContributions += govMatch
|
|
acc.CurrentBalance += govMatch
|
|
}
|
|
|
|
acc.UpdatedAt = ic.Block.Index
|
|
s.putAccount(ic.DAO, acc)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, ContributionMadeEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(amount))),
|
|
stackitem.NewBool(isEmployer),
|
|
stackitem.NewBigInteger(big.NewInt(int64(acc.CurrentBalance))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// allocateSabbaticalCredits allocates sabbatical credits (committee only).
|
|
func (s *Sese) allocateSabbaticalCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
amount := toUint64(args[1])
|
|
// reason := toString(args[2]) // for logging
|
|
|
|
// Committee only
|
|
if !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotCommittee)
|
|
}
|
|
|
|
if amount == 0 {
|
|
panic(ErrSeseInvalidAmount)
|
|
}
|
|
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
if acc.Status != state.LifePlanAccountActive {
|
|
panic(ErrSeseAccountSuspended)
|
|
}
|
|
|
|
acc.SabbaticalCredits += amount
|
|
acc.UpdatedAt = ic.Block.Index
|
|
s.putAccount(ic.DAO, acc)
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getBalance returns current balance.
|
|
func (s *Sese) getBalance(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(acc.CurrentBalance)))
|
|
}
|
|
|
|
// getSabbaticalCredits returns available sabbatical credits.
|
|
func (s *Sese) getSabbaticalCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(acc.SabbaticalCredits)))
|
|
}
|
|
|
|
// startCareer starts a new career cycle.
|
|
func (s *Sese) startCareer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
careerField := toString(args[1])
|
|
|
|
if len(careerField) == 0 || len(careerField) > 128 {
|
|
panic(ErrSeseInvalidField)
|
|
}
|
|
|
|
// Get account
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
if acc.Status != state.LifePlanAccountActive {
|
|
panic(ErrSeseAccountSuspended)
|
|
}
|
|
|
|
// Check no active career
|
|
_, hasActive := s.getActiveCareerID(ic.DAO, vitaID)
|
|
if hasActive {
|
|
panic(ErrSeseCareerExists)
|
|
}
|
|
|
|
// Get next career ID
|
|
cache := ic.DAO.GetRWCache(s.ID).(*SeseCache)
|
|
careerID := cache.careerCount
|
|
cache.careerCount++
|
|
s.setCareerCounter(ic.DAO, cache.careerCount)
|
|
|
|
// Create career
|
|
career := &state.CareerCycle{
|
|
ID: careerID,
|
|
VitaID: vitaID,
|
|
Owner: owner,
|
|
CareerField: careerField,
|
|
StartedAt: ic.Block.Index,
|
|
EndedAt: 0,
|
|
ContributionTotal: 0,
|
|
TransitionReason: "",
|
|
Status: state.CareerCycleActive,
|
|
}
|
|
|
|
s.putCareer(ic.DAO, career)
|
|
s.setActiveCareerID(ic.DAO, vitaID, careerID)
|
|
|
|
// Update account
|
|
acc.CurrentCareerCycleID = careerID
|
|
acc.TotalCareerCycles++
|
|
acc.UpdatedAt = ic.Block.Index
|
|
s.putAccount(ic.DAO, acc)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, CareerStartedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(careerID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewByteArray([]byte(careerField)),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(careerID)))
|
|
}
|
|
|
|
// endCareer ends current career cycle.
|
|
func (s *Sese) endCareer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
careerID := toUint64(args[0])
|
|
reason := toString(args[1])
|
|
|
|
career := s.getCareerInternal(ic.DAO, careerID)
|
|
if career == nil {
|
|
panic(ErrSeseCareerNotFound)
|
|
}
|
|
if career.Status != state.CareerCycleActive {
|
|
panic(ErrSeseCareerEnded)
|
|
}
|
|
|
|
// Check caller is owner or committee
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != career.Owner && !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotOwner)
|
|
}
|
|
|
|
// End career
|
|
career.EndedAt = ic.Block.Index
|
|
career.TransitionReason = reason
|
|
career.Status = state.CareerCycleCompleted
|
|
s.putCareer(ic.DAO, career)
|
|
s.clearActiveCareer(ic.DAO, career.VitaID)
|
|
|
|
// Update account
|
|
acc := s.getAccountInternal(ic.DAO, career.VitaID)
|
|
if acc != nil {
|
|
acc.CurrentCareerCycleID = 0
|
|
acc.UpdatedAt = ic.Block.Index
|
|
s.putAccount(ic.DAO, acc)
|
|
}
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, CareerEndedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(careerID))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getCareer returns career cycle details.
|
|
func (s *Sese) getCareer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
careerID := toUint64(args[0])
|
|
|
|
career := s.getCareerInternal(ic.DAO, careerID)
|
|
if career == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := career.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getActiveCareer returns owner's active career.
|
|
func (s *Sese) getActiveCareer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
careerID, hasActive := s.getActiveCareerID(ic.DAO, vitaID)
|
|
if !hasActive {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
career := s.getCareerInternal(ic.DAO, careerID)
|
|
if career == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := career.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// startSabbatical starts a sabbatical.
|
|
func (s *Sese) startSabbatical(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
purpose := state.SabbaticalPurpose(toUint64(args[1]))
|
|
description := toString(args[2])
|
|
duration := toUint32(args[3])
|
|
|
|
if len(description) == 0 || len(description) > 256 {
|
|
panic(ErrSeseInvalidDescription)
|
|
}
|
|
|
|
// Get account
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
if acc.Status != state.LifePlanAccountActive {
|
|
panic(ErrSeseAccountSuspended)
|
|
}
|
|
|
|
// Check no active sabbatical
|
|
_, hasActive := s.getActiveSabbaticalID(ic.DAO, vitaID)
|
|
if hasActive {
|
|
panic(ErrSeseSabbaticalExists)
|
|
}
|
|
|
|
// Check duration limits
|
|
cfg := s.getConfigInternal(ic.DAO)
|
|
if duration < cfg.MinSabbaticalDuration {
|
|
panic(ErrSeseSabbaticalTooShort)
|
|
}
|
|
if duration > cfg.MaxSabbaticalDuration {
|
|
panic(ErrSeseSabbaticalTooLong)
|
|
}
|
|
|
|
// Check sufficient credits
|
|
if acc.SabbaticalCredits == 0 {
|
|
panic(ErrSeseInsufficientCredits)
|
|
}
|
|
|
|
// Get next sabbatical ID
|
|
cache := ic.DAO.GetRWCache(s.ID).(*SeseCache)
|
|
sabbaticalID := cache.sabbaticalCount
|
|
cache.sabbaticalCount++
|
|
s.setSabbaticalCounter(ic.DAO, cache.sabbaticalCount)
|
|
|
|
// Create sabbatical
|
|
sabbatical := &state.Sabbatical{
|
|
ID: sabbaticalID,
|
|
VitaID: vitaID,
|
|
Owner: owner,
|
|
Purpose: purpose,
|
|
Description: description,
|
|
StartedAt: ic.Block.Index,
|
|
Duration: duration,
|
|
EndedAt: 0,
|
|
FundingUsed: 0,
|
|
Outcome: "",
|
|
Status: state.SabbaticalActive,
|
|
}
|
|
|
|
s.putSabbatical(ic.DAO, sabbatical)
|
|
s.setActiveSabbaticalID(ic.DAO, vitaID, sabbaticalID)
|
|
|
|
// Update account
|
|
acc.TotalSabbaticals++
|
|
acc.UpdatedAt = ic.Block.Index
|
|
s.putAccount(ic.DAO, acc)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, SabbaticalStartedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(sabbaticalID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(purpose))),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(sabbaticalID)))
|
|
}
|
|
|
|
// completeSabbatical completes a sabbatical.
|
|
func (s *Sese) completeSabbatical(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
sabbaticalID := toUint64(args[0])
|
|
outcome := toString(args[1])
|
|
|
|
sabbatical := s.getSabbaticalInternal(ic.DAO, sabbaticalID)
|
|
if sabbatical == nil {
|
|
panic(ErrSeseSabbaticalNotFound)
|
|
}
|
|
if sabbatical.Status != state.SabbaticalActive {
|
|
panic(ErrSeseSabbaticalEnded)
|
|
}
|
|
|
|
// Check caller is owner or committee
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != sabbatical.Owner && !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotOwner)
|
|
}
|
|
|
|
// Complete sabbatical
|
|
sabbatical.EndedAt = ic.Block.Index
|
|
sabbatical.Outcome = outcome
|
|
sabbatical.Status = state.SabbaticalCompleted
|
|
s.putSabbatical(ic.DAO, sabbatical)
|
|
s.clearActiveSabbatical(ic.DAO, sabbatical.VitaID)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, SabbaticalCompletedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(sabbaticalID))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// cancelSabbatical cancels a sabbatical.
|
|
func (s *Sese) cancelSabbatical(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
sabbaticalID := toUint64(args[0])
|
|
reason := toString(args[1])
|
|
|
|
sabbatical := s.getSabbaticalInternal(ic.DAO, sabbaticalID)
|
|
if sabbatical == nil {
|
|
panic(ErrSeseSabbaticalNotFound)
|
|
}
|
|
if sabbatical.Status != state.SabbaticalActive && sabbatical.Status != state.SabbaticalPlanned {
|
|
panic(ErrSeseSabbaticalEnded)
|
|
}
|
|
|
|
// Check caller is owner or committee
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != sabbatical.Owner && !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotOwner)
|
|
}
|
|
|
|
// Cancel sabbatical
|
|
sabbatical.EndedAt = ic.Block.Index
|
|
sabbatical.Outcome = reason
|
|
sabbatical.Status = state.SabbaticalCancelled
|
|
s.putSabbatical(ic.DAO, sabbatical)
|
|
s.clearActiveSabbatical(ic.DAO, sabbatical.VitaID)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, SabbaticalCancelledEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(sabbaticalID))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getSabbatical returns sabbatical details.
|
|
func (s *Sese) getSabbatical(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
sabbaticalID := toUint64(args[0])
|
|
|
|
sabbatical := s.getSabbaticalInternal(ic.DAO, sabbaticalID)
|
|
if sabbatical == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := sabbatical.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getActiveSabbatical returns owner's active sabbatical.
|
|
func (s *Sese) getActiveSabbatical(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
sabbaticalID, hasActive := s.getActiveSabbaticalID(ic.DAO, vitaID)
|
|
if !hasActive {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
sabbatical := s.getSabbaticalInternal(ic.DAO, sabbaticalID)
|
|
if sabbatical == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := sabbatical.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// recordMilestone records a life milestone.
|
|
func (s *Sese) recordMilestone(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
milestoneType := state.MilestoneType(toUint64(args[1]))
|
|
title := toString(args[2])
|
|
description := toString(args[3])
|
|
contentHashBytes := toBytes(args[4])
|
|
|
|
// Convert bytes to Uint256
|
|
var contentHash util.Uint256
|
|
if len(contentHashBytes) == 32 {
|
|
copy(contentHash[:], contentHashBytes)
|
|
}
|
|
|
|
if len(title) == 0 || len(title) > 128 {
|
|
panic(ErrSeseInvalidTitle)
|
|
}
|
|
if len(description) > 512 {
|
|
panic(ErrSeseInvalidDescription)
|
|
}
|
|
|
|
// Get account
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
if acc.Status != state.LifePlanAccountActive {
|
|
panic(ErrSeseAccountSuspended)
|
|
}
|
|
|
|
// Get next milestone ID
|
|
cache := ic.DAO.GetRWCache(s.ID).(*SeseCache)
|
|
milestoneID := cache.milestoneCount
|
|
cache.milestoneCount++
|
|
s.setMilestoneCounter(ic.DAO, cache.milestoneCount)
|
|
|
|
// Create milestone
|
|
milestone := &state.LifeMilestone{
|
|
ID: milestoneID,
|
|
VitaID: vitaID,
|
|
Owner: owner,
|
|
MilestoneType: milestoneType,
|
|
Title: title,
|
|
Description: description,
|
|
ContentHash: contentHash,
|
|
AchievedAt: ic.Block.Index,
|
|
IsVerified: false,
|
|
}
|
|
|
|
s.putMilestone(ic.DAO, milestone)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, MilestoneRecordedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(milestoneID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(milestoneType))),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(milestoneID)))
|
|
}
|
|
|
|
// verifyMilestone verifies a milestone (life planner only).
|
|
func (s *Sese) verifyMilestone(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
milestoneID := toUint64(args[0])
|
|
|
|
// Life planner only
|
|
if !s.checkLifePlanner(ic) {
|
|
panic(ErrSeseNotLifePlanner)
|
|
}
|
|
|
|
milestone := s.getMilestoneInternal(ic.DAO, milestoneID)
|
|
if milestone == nil {
|
|
panic(ErrSeseMilestoneNotFound)
|
|
}
|
|
|
|
milestone.IsVerified = true
|
|
s.putMilestone(ic.DAO, milestone)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, MilestoneVerifiedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(milestoneID))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getMilestone returns milestone details.
|
|
func (s *Sese) getMilestone(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
milestoneID := toUint64(args[0])
|
|
|
|
milestone := s.getMilestoneInternal(ic.DAO, milestoneID)
|
|
if milestone == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := milestone.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// createGoal creates a life goal.
|
|
func (s *Sese) createGoal(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
title := toString(args[1])
|
|
description := toString(args[2])
|
|
targetBlock := toUint32(args[3])
|
|
|
|
if len(title) == 0 || len(title) > 128 {
|
|
panic(ErrSeseInvalidTitle)
|
|
}
|
|
if len(description) > 512 {
|
|
panic(ErrSeseInvalidDescription)
|
|
}
|
|
|
|
// Get account
|
|
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
|
|
acc := s.getAccountInternal(ic.DAO, vitaID)
|
|
if acc == nil {
|
|
panic(ErrSeseAccountNotFound)
|
|
}
|
|
if acc.Status != state.LifePlanAccountActive {
|
|
panic(ErrSeseAccountSuspended)
|
|
}
|
|
|
|
// Get next goal ID
|
|
cache := ic.DAO.GetRWCache(s.ID).(*SeseCache)
|
|
goalID := cache.goalCount
|
|
cache.goalCount++
|
|
s.setGoalCounter(ic.DAO, cache.goalCount)
|
|
|
|
// Create goal
|
|
goal := &state.LifeGoal{
|
|
ID: goalID,
|
|
VitaID: vitaID,
|
|
Owner: owner,
|
|
Title: title,
|
|
Description: description,
|
|
TargetBlock: targetBlock,
|
|
Progress: 0,
|
|
Status: state.GoalStatusPlanned,
|
|
CreatedAt: ic.Block.Index,
|
|
CompletedAt: 0,
|
|
}
|
|
|
|
s.putGoal(ic.DAO, goal)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, GoalCreatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(goalID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewByteArray([]byte(title)),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(big.NewInt(int64(goalID)))
|
|
}
|
|
|
|
// updateGoalProgress updates goal progress.
|
|
func (s *Sese) updateGoalProgress(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
goalID := toUint64(args[0])
|
|
progress := toUint32(args[1])
|
|
|
|
if progress > 10000 {
|
|
panic(ErrSeseInvalidProgress)
|
|
}
|
|
|
|
goal := s.getGoalInternal(ic.DAO, goalID)
|
|
if goal == nil {
|
|
panic(ErrSeseGoalNotFound)
|
|
}
|
|
if goal.Status == state.GoalStatusCompleted || goal.Status == state.GoalStatusAbandoned {
|
|
panic(ErrSeseGoalCompleted)
|
|
}
|
|
|
|
// Check caller is owner
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != goal.Owner && !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotOwner)
|
|
}
|
|
|
|
goal.Progress = progress
|
|
if goal.Status == state.GoalStatusPlanned {
|
|
goal.Status = state.GoalStatusInProgress
|
|
}
|
|
s.putGoal(ic.DAO, goal)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, GoalUpdatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(goalID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(progress))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// completeGoal marks goal as completed.
|
|
func (s *Sese) completeGoal(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
goalID := toUint64(args[0])
|
|
|
|
goal := s.getGoalInternal(ic.DAO, goalID)
|
|
if goal == nil {
|
|
panic(ErrSeseGoalNotFound)
|
|
}
|
|
if goal.Status == state.GoalStatusCompleted || goal.Status == state.GoalStatusAbandoned {
|
|
panic(ErrSeseGoalCompleted)
|
|
}
|
|
|
|
// Check caller is owner
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != goal.Owner && !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotOwner)
|
|
}
|
|
|
|
goal.Progress = 10000 // 100%
|
|
goal.Status = state.GoalStatusCompleted
|
|
goal.CompletedAt = ic.Block.Index
|
|
s.putGoal(ic.DAO, goal)
|
|
|
|
// Emit event
|
|
ic.AddNotification(s.Hash, GoalCompletedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(goalID))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// abandonGoal abandons a goal.
|
|
func (s *Sese) abandonGoal(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
goalID := toUint64(args[0])
|
|
// reason := toString(args[1]) // for logging
|
|
|
|
goal := s.getGoalInternal(ic.DAO, goalID)
|
|
if goal == nil {
|
|
panic(ErrSeseGoalNotFound)
|
|
}
|
|
if goal.Status == state.GoalStatusCompleted || goal.Status == state.GoalStatusAbandoned {
|
|
panic(ErrSeseGoalCompleted)
|
|
}
|
|
|
|
// Check caller is owner
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != goal.Owner && !s.checkCommittee(ic) {
|
|
panic(ErrSeseNotOwner)
|
|
}
|
|
|
|
goal.Status = state.GoalStatusAbandoned
|
|
goal.CompletedAt = ic.Block.Index
|
|
s.putGoal(ic.DAO, goal)
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getGoal returns goal details.
|
|
func (s *Sese) getGoal(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
goalID := toUint64(args[0])
|
|
|
|
goal := s.getGoalInternal(ic.DAO, goalID)
|
|
if goal == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := goal.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getConfig returns the Sese configuration.
|
|
func (s *Sese) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cfg := s.getConfigInternal(ic.DAO)
|
|
item, _ := cfg.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getTotalAccounts returns the total number of life plan accounts.
|
|
func (s *Sese) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(s.ID).(*SeseCache)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount)))
|
|
}
|
|
|
|
// getTotalCareers returns the total number of career cycles.
|
|
func (s *Sese) getTotalCareers(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(s.ID).(*SeseCache)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(cache.careerCount)))
|
|
}
|
|
|
|
// getTotalSabbaticals returns the total number of sabbaticals.
|
|
func (s *Sese) getTotalSabbaticals(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(s.ID).(*SeseCache)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(cache.sabbaticalCount)))
|
|
}
|
|
|
|
// getTotalMilestones returns the total number of milestones.
|
|
func (s *Sese) getTotalMilestones(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(s.ID).(*SeseCache)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(cache.milestoneCount)))
|
|
}
|
|
|
|
// getTotalGoals returns the total number of goals.
|
|
func (s *Sese) getTotalGoals(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(s.ID).(*SeseCache)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(cache.goalCount)))
|
|
}
|
|
|
|
// ===== Public Interface Methods for Cross-Contract Access =====
|
|
|
|
// GetAccountByOwner returns a life plan account by owner address.
|
|
func (s *Sese) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.LifePlanAccount, error) {
|
|
vitaID, found := s.getVitaIDByOwner(d, owner)
|
|
if !found {
|
|
return nil, ErrSeseAccountNotFound
|
|
}
|
|
acc := s.getAccountInternal(d, vitaID)
|
|
if acc == nil {
|
|
return nil, ErrSeseAccountNotFound
|
|
}
|
|
return acc, nil
|
|
}
|
|
|
|
// HasActiveCareer checks if owner has an active career.
|
|
func (s *Sese) HasActiveCareer(d *dao.Simple, owner util.Uint160) bool {
|
|
vitaID, found := s.getVitaIDByOwner(d, owner)
|
|
if !found {
|
|
return false
|
|
}
|
|
_, hasActive := s.getActiveCareerID(d, vitaID)
|
|
return hasActive
|
|
}
|
|
|
|
// Address returns the contract's script hash.
|
|
func (s *Sese) Address() util.Uint160 {
|
|
return s.Hash
|
|
}
|