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 NEO INEO 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.NEO.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 }