tutus-chain/pkg/core/native/sese.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
Annos IAnnos
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.Annos.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
}