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/core/storage" "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" ) // Scire represents the universal education native contract. type Scire struct { interop.ContractMD Annos IAnnos Vita IVita RoleRegistry IRoleRegistry Lex ILex } // ScireCache represents the cached state for Scire contract. type ScireCache struct { accountCount uint64 certCount uint64 enrollCount uint64 } // Storage key prefixes for Scire. const ( scirePrefixAccount byte = 0x01 // vitaID -> EducationAccount scirePrefixAccountByOwner byte = 0x02 // owner -> vitaID scirePrefixCertification byte = 0x10 // certID -> Certification scirePrefixCertByOwner byte = 0x11 // vitaID + certID -> exists scirePrefixCertByInstitution byte = 0x12 // institution + certID -> exists scirePrefixCertByType byte = 0x13 // certType hash + certID -> exists scirePrefixEnrollment byte = 0x20 // enrollmentID -> Enrollment scirePrefixEnrollByStudent byte = 0x21 // vitaID + enrollmentID -> exists scirePrefixEnrollByProgram byte = 0x22 // programID hash + enrollmentID -> exists scirePrefixActiveEnrollment byte = 0x23 // vitaID -> active enrollmentID scirePrefixAccountCounter byte = 0xF0 // -> uint64 scirePrefixCertCounter byte = 0xF1 // -> next certification ID scirePrefixEnrollCounter byte = 0xF2 // -> next enrollment ID scirePrefixConfig byte = 0xFF // -> ScireConfig ) // Event names for Scire. const ( AccountCreatedEvent = "AccountCreated" CreditsAllocatedEvent = "CreditsAllocated" EnrollmentCreatedEvent = "EnrollmentCreated" EnrollmentCompletedEvent = "EnrollmentCompleted" EnrollmentWithdrawnEvent = "EnrollmentWithdrawn" EnrollmentTransferredEvent = "EnrollmentTransferred" CertificationIssuedEvent = "CertificationIssued" CertificationRevokedEvent = "CertificationRevoked" CertificationRenewedEvent = "CertificationRenewed" ) // Role constants for educators. const ( RoleEducator uint64 = 20 // Can issue certifications and manage enrollments ) // Various errors for Scire. var ( ErrScireAccountNotFound = errors.New("education account not found") ErrScireAccountExists = errors.New("education account already exists") ErrScireAccountSuspended = errors.New("education account is suspended") ErrScireAccountClosed = errors.New("education account is closed") ErrScireNoVita = errors.New("owner must have an active Vita") ErrScireInsufficientCredits = errors.New("insufficient education credits") ErrScireInvalidCredits = errors.New("invalid credit amount") ErrScireCertNotFound = errors.New("certification not found") ErrScireCertExpired = errors.New("certification has expired") ErrScireCertRevoked = errors.New("certification is revoked") ErrScireEnrollNotFound = errors.New("enrollment not found") ErrScireEnrollNotActive = errors.New("enrollment is not active") ErrScireAlreadyEnrolled = errors.New("already enrolled in a program") ErrScireNotEducator = errors.New("caller is not an authorized educator") ErrScireNotCommittee = errors.New("invalid committee signature") ErrScireInvalidOwner = errors.New("invalid owner address") ErrScireInvalidInstitution = errors.New("invalid institution address") ErrScireInvalidProgramID = errors.New("invalid program ID") ErrScireInvalidCertType = errors.New("invalid certification type") ErrScireInvalidName = errors.New("invalid certification name") ErrScireEducationRestricted = errors.New("education right is restricted") ErrScireNotStudent = errors.New("caller is not the student") ErrScireNotInstitution = errors.New("caller is not the institution") ErrScireExceedsMaxCredits = errors.New("exceeds maximum credits per program") ) var ( _ interop.Contract = (*Scire)(nil) _ dao.NativeContractCache = (*ScireCache)(nil) ) // Copy implements NativeContractCache interface. func (c *ScireCache) Copy() dao.NativeContractCache { return &ScireCache{ accountCount: c.accountCount, certCount: c.certCount, enrollCount: c.enrollCount, } } // checkCommittee checks if the caller has committee authority. func (s *Scire) checkCommittee(ic *interop.Context) bool { if s.RoleRegistry != nil { return s.RoleRegistry.CheckCommittee(ic) } return s.Annos.CheckCommittee(ic) } // checkEducator checks if the caller has educator authority. func (s *Scire) checkEducator(ic *interop.Context) bool { caller := ic.VM.GetCallingScriptHash() if s.RoleRegistry != nil { if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleEducator, ic.Block.Index) { return true } } // Committee members can also act as educators return s.checkCommittee(ic) } // checkEducationRight checks if subject has education rights via Lex. func (s *Scire) checkEducationRight(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.RightEducation, ic.Block.Index) } // newScire creates a new Scire native contract. func newScire() *Scire { s := &Scire{ ContractMD: *interop.NewContractMD(nativenames.Scire, nativeids.Scire), } defer s.BuildHFSpecificMD(s.ActiveIn()) // ===== Account Management ===== // createAccount - Create education account for a Vita holder desc := NewDescriptor("createAccount", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md := NewMethodAndPrice(s.createAccount, 1<<17, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // getAccount - Get education 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) // allocateCredits - Allocate learning credits (committee only) desc = NewDescriptor("allocateCredits", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) md = NewMethodAndPrice(s.allocateCredits, 1<<16, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // getCredits - Get available credits desc = NewDescriptor("getCredits", smartcontract.IntegerType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(s.getCredits, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // ===== Enrollment Management ===== // enroll - Enroll in an education program desc = NewDescriptor("enroll", smartcontract.IntegerType, manifest.NewParameter("student", smartcontract.Hash160Type), manifest.NewParameter("programID", smartcontract.StringType), manifest.NewParameter("institution", smartcontract.Hash160Type), manifest.NewParameter("credits", smartcontract.IntegerType)) md = NewMethodAndPrice(s.enroll, 1<<17, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // completeEnrollment - Mark enrollment as completed (institution only) desc = NewDescriptor("completeEnrollment", smartcontract.BoolType, manifest.NewParameter("enrollmentID", smartcontract.IntegerType), manifest.NewParameter("contentHash", smartcontract.Hash256Type)) md = NewMethodAndPrice(s.completeEnrollment, 1<<16, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // withdrawEnrollment - Withdraw from program desc = NewDescriptor("withdrawEnrollment", smartcontract.BoolType, manifest.NewParameter("enrollmentID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) md = NewMethodAndPrice(s.withdrawEnrollment, 1<<16, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // getEnrollment - Get enrollment details desc = NewDescriptor("getEnrollment", smartcontract.ArrayType, manifest.NewParameter("enrollmentID", smartcontract.IntegerType)) md = NewMethodAndPrice(s.getEnrollment, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // getActiveEnrollment - Get student's active enrollment desc = NewDescriptor("getActiveEnrollment", smartcontract.ArrayType, manifest.NewParameter("student", smartcontract.Hash160Type)) md = NewMethodAndPrice(s.getActiveEnrollment, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // ===== Certification Management ===== // issueCertification - Issue a certification (educator only) desc = NewDescriptor("issueCertification", smartcontract.IntegerType, manifest.NewParameter("owner", smartcontract.Hash160Type), manifest.NewParameter("certType", smartcontract.StringType), manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("contentHash", smartcontract.Hash256Type), manifest.NewParameter("expiresAt", smartcontract.IntegerType)) md = NewMethodAndPrice(s.issueCertification, 1<<17, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // revokeCertification - Revoke a certification (institution only) desc = NewDescriptor("revokeCertification", smartcontract.BoolType, manifest.NewParameter("certID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) md = NewMethodAndPrice(s.revokeCertification, 1<<16, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // renewCertification - Extend certification expiry desc = NewDescriptor("renewCertification", smartcontract.BoolType, manifest.NewParameter("certID", smartcontract.IntegerType), manifest.NewParameter("newExpiresAt", smartcontract.IntegerType)) md = NewMethodAndPrice(s.renewCertification, 1<<16, callflag.States|callflag.AllowNotify) s.AddMethod(md, desc) // getCertification - Get certification details desc = NewDescriptor("getCertification", smartcontract.ArrayType, manifest.NewParameter("certID", smartcontract.IntegerType)) md = NewMethodAndPrice(s.getCertification, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // verifyCertification - Check if certification is valid desc = NewDescriptor("verifyCertification", smartcontract.BoolType, manifest.NewParameter("certID", smartcontract.IntegerType)) md = NewMethodAndPrice(s.verifyCertification, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // hasCertification - Check if owner has specific cert type desc = NewDescriptor("hasCertification", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type), manifest.NewParameter("certType", smartcontract.StringType)) md = NewMethodAndPrice(s.hasCertification, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // ===== Query Methods ===== // getConfig - Get Scire configuration desc = NewDescriptor("getConfig", smartcontract.ArrayType) md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // getTotalAccounts - Get total education accounts desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // getTotalCertifications - Get total certifications issued desc = NewDescriptor("getTotalCertifications", smartcontract.IntegerType) md = NewMethodAndPrice(s.getTotalCertifications, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // getTotalEnrollments - Get total enrollments desc = NewDescriptor("getTotalEnrollments", smartcontract.IntegerType) md = NewMethodAndPrice(s.getTotalEnrollments, 1<<15, callflag.ReadStates) s.AddMethod(md, desc) // ===== Events ===== // AccountCreated event eDesc := NewEventDescriptor(AccountCreatedEvent, manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("owner", smartcontract.Hash160Type)) s.AddEvent(NewEvent(eDesc)) // CreditsAllocated event eDesc = NewEventDescriptor(CreditsAllocatedEvent, manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("total", smartcontract.IntegerType)) s.AddEvent(NewEvent(eDesc)) // EnrollmentCreated event eDesc = NewEventDescriptor(EnrollmentCreatedEvent, manifest.NewParameter("enrollmentID", smartcontract.IntegerType), manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("programID", smartcontract.StringType)) s.AddEvent(NewEvent(eDesc)) // EnrollmentCompleted event eDesc = NewEventDescriptor(EnrollmentCompletedEvent, manifest.NewParameter("enrollmentID", smartcontract.IntegerType), manifest.NewParameter("vitaID", smartcontract.IntegerType)) s.AddEvent(NewEvent(eDesc)) // EnrollmentWithdrawn event eDesc = NewEventDescriptor(EnrollmentWithdrawnEvent, manifest.NewParameter("enrollmentID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) s.AddEvent(NewEvent(eDesc)) // CertificationIssued event eDesc = NewEventDescriptor(CertificationIssuedEvent, manifest.NewParameter("certID", smartcontract.IntegerType), manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("certType", smartcontract.StringType)) s.AddEvent(NewEvent(eDesc)) // CertificationRevoked event eDesc = NewEventDescriptor(CertificationRevokedEvent, manifest.NewParameter("certID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) s.AddEvent(NewEvent(eDesc)) // CertificationRenewed event eDesc = NewEventDescriptor(CertificationRenewedEvent, manifest.NewParameter("certID", smartcontract.IntegerType), manifest.NewParameter("newExpiresAt", smartcontract.IntegerType)) s.AddEvent(NewEvent(eDesc)) return s } // Metadata returns contract metadata. func (s *Scire) Metadata() *interop.ContractMD { return &s.ContractMD } // Initialize initializes the Scire contract. func (s *Scire) 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.setCertCounter(ic.DAO, 0) s.setEnrollCounter(ic.DAO, 0) // Initialize config with defaults cfg := &state.ScireConfig{ AnnualCreditAllocation: 1000, // 1000 credits per year MaxCreditsPerProgram: 500, // Max 500 credits per program CertificationFee: 0, // Free certification MinEnrollmentDuration: 86400, // ~1 day in blocks (1-second blocks) } s.setConfig(ic.DAO, cfg) // Initialize cache cache := &ScireCache{ accountCount: 0, certCount: 0, enrollCount: 0, } ic.DAO.SetCache(s.ID, cache) return nil } // InitializeCache initializes the cache from storage. func (s *Scire) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { cache := &ScireCache{ accountCount: s.getAccountCounter(d), certCount: s.getCertCounter(d), enrollCount: s.getEnrollCounter(d), } d.SetCache(s.ID, cache) return nil } // OnPersist is called before block is committed. func (s *Scire) OnPersist(ic *interop.Context) error { return nil } // PostPersist is called after block is committed. func (s *Scire) PostPersist(ic *interop.Context) error { return nil } // ActiveIn returns the hardfork at which this contract is activated. func (s *Scire) ActiveIn() *config.Hardfork { return nil // Always active } // ===== Storage Helpers ===== func (s *Scire) makeAccountKey(vitaID uint64) []byte { key := make([]byte, 9) key[0] = scirePrefixAccount binary.BigEndian.PutUint64(key[1:], vitaID) return key } func (s *Scire) makeAccountByOwnerKey(owner util.Uint160) []byte { key := make([]byte, 21) key[0] = scirePrefixAccountByOwner copy(key[1:], owner.BytesBE()) return key } func (s *Scire) makeCertificationKey(certID uint64) []byte { key := make([]byte, 9) key[0] = scirePrefixCertification binary.BigEndian.PutUint64(key[1:], certID) return key } func (s *Scire) makeCertByOwnerKey(vitaID, certID uint64) []byte { key := make([]byte, 17) key[0] = scirePrefixCertByOwner binary.BigEndian.PutUint64(key[1:9], vitaID) binary.BigEndian.PutUint64(key[9:], certID) return key } func (s *Scire) makeEnrollmentKey(enrollID uint64) []byte { key := make([]byte, 9) key[0] = scirePrefixEnrollment binary.BigEndian.PutUint64(key[1:], enrollID) return key } func (s *Scire) makeActiveEnrollmentKey(vitaID uint64) []byte { key := make([]byte, 9) key[0] = scirePrefixActiveEnrollment binary.BigEndian.PutUint64(key[1:], vitaID) return key } // Counter getters/setters func (s *Scire) getAccountCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(s.ID, []byte{scirePrefixAccountCounter}) if si == nil { return 0 } return binary.BigEndian.Uint64(si) } func (s *Scire) setAccountCounter(d *dao.Simple, count uint64) { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, count) d.PutStorageItem(s.ID, []byte{scirePrefixAccountCounter}, buf) } func (s *Scire) getCertCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(s.ID, []byte{scirePrefixCertCounter}) if si == nil { return 0 } return binary.BigEndian.Uint64(si) } func (s *Scire) setCertCounter(d *dao.Simple, count uint64) { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, count) d.PutStorageItem(s.ID, []byte{scirePrefixCertCounter}, buf) } func (s *Scire) getEnrollCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(s.ID, []byte{scirePrefixEnrollCounter}) if si == nil { return 0 } return binary.BigEndian.Uint64(si) } func (s *Scire) setEnrollCounter(d *dao.Simple, count uint64) { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, count) d.PutStorageItem(s.ID, []byte{scirePrefixEnrollCounter}, buf) } // Config getter/setter func (s *Scire) getConfigInternal(d *dao.Simple) *state.ScireConfig { si := d.GetStorageItem(s.ID, []byte{scirePrefixConfig}) if si == nil { return &state.ScireConfig{ AnnualCreditAllocation: 1000, MaxCreditsPerProgram: 500, CertificationFee: 0, MinEnrollmentDuration: 86400, } } cfg := new(state.ScireConfig) item, _ := stackitem.Deserialize(si) cfg.FromStackItem(item) return cfg } func (s *Scire) setConfig(d *dao.Simple, cfg *state.ScireConfig) { item, _ := cfg.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(s.ID, []byte{scirePrefixConfig}, data) } // Account storage func (s *Scire) getAccountInternal(d *dao.Simple, vitaID uint64) *state.EducationAccount { si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID)) if si == nil { return nil } acc := new(state.EducationAccount) item, _ := stackitem.Deserialize(si) acc.FromStackItem(item) return acc } func (s *Scire) putAccount(d *dao.Simple, acc *state.EducationAccount) { item, _ := acc.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data) } func (s *Scire) 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 *Scire) 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) } // Enrollment storage func (s *Scire) getEnrollmentInternal(d *dao.Simple, enrollID uint64) *state.Enrollment { si := d.GetStorageItem(s.ID, s.makeEnrollmentKey(enrollID)) if si == nil { return nil } enroll := new(state.Enrollment) item, _ := stackitem.Deserialize(si) enroll.FromStackItem(item) return enroll } func (s *Scire) putEnrollment(d *dao.Simple, enroll *state.Enrollment) { item, _ := enroll.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(s.ID, s.makeEnrollmentKey(enroll.ID), data) } func (s *Scire) getActiveEnrollmentID(d *dao.Simple, vitaID uint64) uint64 { si := d.GetStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID)) if si == nil { return 0 } return binary.BigEndian.Uint64(si) } func (s *Scire) setActiveEnrollmentID(d *dao.Simple, vitaID, enrollID uint64) { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, enrollID) d.PutStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID), buf) } func (s *Scire) clearActiveEnrollment(d *dao.Simple, vitaID uint64) { d.DeleteStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID)) } // Certification storage func (s *Scire) getCertificationInternal(d *dao.Simple, certID uint64) *state.Certification { si := d.GetStorageItem(s.ID, s.makeCertificationKey(certID)) if si == nil { return nil } cert := new(state.Certification) item, _ := stackitem.Deserialize(si) cert.FromStackItem(item) return cert } func (s *Scire) putCertification(d *dao.Simple, cert *state.Certification) { item, _ := cert.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(s.ID, s.makeCertificationKey(cert.ID), data) } func (s *Scire) setCertByOwner(d *dao.Simple, vitaID, certID uint64) { d.PutStorageItem(s.ID, s.makeCertByOwnerKey(vitaID, certID), []byte{1}) } // ===== Contract Methods ===== // createAccount creates an education account for a Vita holder. func (s *Scire) createAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) // Check owner has active Vita if s.Vita == nil { panic(ErrScireNoVita) } vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) if err != nil || vita == nil { panic(ErrScireNoVita) } if vita.Status != state.TokenStatusActive { panic(ErrScireNoVita) } // Check if account already exists existing := s.getAccountInternal(ic.DAO, vita.TokenID) if existing != nil { panic(ErrScireAccountExists) } // Check education rights if !s.checkEducationRight(ic, owner) { // Log but allow (EnforcementLogging) // In the future, we could emit an event here } // Get cache and increment counter cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) accountNum := cache.accountCount cache.accountCount++ s.setAccountCounter(ic.DAO, cache.accountCount) // Create account acc := &state.EducationAccount{ VitaID: vita.TokenID, Owner: owner, TotalCredits: 0, UsedCredits: 0, AvailableCredits: 0, Status: state.EducationAccountActive, 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, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), stackitem.NewByteArray(owner.BytesBE()), })) _ = accountNum // suppress unused warning return stackitem.NewBool(true) } // getAccount returns education account by owner. func (s *Scire) 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 education account by Vita ID. func (s *Scire) 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 } // allocateCredits allocates learning credits to an account (committee only). func (s *Scire) allocateCredits(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(ErrScireNotCommittee) } if amount == 0 { panic(ErrScireInvalidCredits) } // Get or create account vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) if !found { // Auto-create account if Vita exists if s.Vita == nil { panic(ErrScireNoVita) } vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) if err != nil || vita == nil || vita.Status != state.TokenStatusActive { panic(ErrScireNoVita) } vitaID = vita.TokenID // Create account cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) cache.accountCount++ s.setAccountCounter(ic.DAO, cache.accountCount) acc := &state.EducationAccount{ VitaID: vitaID, Owner: owner, TotalCredits: amount, UsedCredits: 0, AvailableCredits: amount, Status: state.EducationAccountActive, CreatedAt: ic.Block.Index, UpdatedAt: ic.Block.Index, } s.putAccount(ic.DAO, acc) s.setOwnerToVitaID(ic.DAO, owner, vitaID) // Emit events ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(vitaID))), stackitem.NewByteArray(owner.BytesBE()), })) ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(vitaID))), stackitem.NewBigInteger(big.NewInt(int64(amount))), stackitem.NewBigInteger(big.NewInt(int64(amount))), })) return stackitem.NewBool(true) } acc := s.getAccountInternal(ic.DAO, vitaID) if acc == nil { panic(ErrScireAccountNotFound) } if acc.Status != state.EducationAccountActive { panic(ErrScireAccountSuspended) } // Add credits acc.TotalCredits += amount acc.AvailableCredits += amount acc.UpdatedAt = ic.Block.Index s.putAccount(ic.DAO, acc) // Emit event ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(vitaID))), stackitem.NewBigInteger(big.NewInt(int64(amount))), stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits))), })) return stackitem.NewBool(true) } // getCredits returns available credits for an owner. func (s *Scire) getCredits(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.AvailableCredits))) } // enroll enrolls a student in an education program. func (s *Scire) enroll(ic *interop.Context, args []stackitem.Item) stackitem.Item { student := toUint160(args[0]) programID := toString(args[1]) institution := toUint160(args[2]) credits := toUint64(args[3]) // Validate inputs if len(programID) == 0 || len(programID) > 128 { panic(ErrScireInvalidProgramID) } if credits == 0 { panic(ErrScireInvalidCredits) } // Check max credits cfg := s.getConfigInternal(ic.DAO) if credits > cfg.MaxCreditsPerProgram { panic(ErrScireExceedsMaxCredits) } // Get student's account vitaID, found := s.getVitaIDByOwner(ic.DAO, student) if !found { panic(ErrScireAccountNotFound) } acc := s.getAccountInternal(ic.DAO, vitaID) if acc == nil { panic(ErrScireAccountNotFound) } if acc.Status != state.EducationAccountActive { panic(ErrScireAccountSuspended) } // Check sufficient credits if acc.AvailableCredits < credits { panic(ErrScireInsufficientCredits) } // Check not already enrolled activeEnrollID := s.getActiveEnrollmentID(ic.DAO, vitaID) if activeEnrollID != 0 { panic(ErrScireAlreadyEnrolled) } // Check education rights if !s.checkEducationRight(ic, student) { // Log but allow } // Get next enrollment ID cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) enrollID := cache.enrollCount cache.enrollCount++ s.setEnrollCounter(ic.DAO, cache.enrollCount) // Deduct credits acc.UsedCredits += credits acc.AvailableCredits -= credits acc.UpdatedAt = ic.Block.Index s.putAccount(ic.DAO, acc) // Create enrollment enroll := &state.Enrollment{ ID: enrollID, VitaID: vitaID, Student: student, ProgramID: programID, Institution: institution, CreditsAllocated: credits, StartedAt: ic.Block.Index, CompletedAt: 0, Status: state.EnrollmentActive, } s.putEnrollment(ic.DAO, enroll) s.setActiveEnrollmentID(ic.DAO, vitaID, enrollID) // Emit event ic.AddNotification(s.Hash, EnrollmentCreatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(enrollID))), stackitem.NewBigInteger(big.NewInt(int64(vitaID))), stackitem.NewByteArray([]byte(programID)), })) return stackitem.NewBigInteger(big.NewInt(int64(enrollID))) } // completeEnrollment marks an enrollment as completed (institution/educator only). func (s *Scire) completeEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { enrollID := toUint64(args[0]) // contentHash := toUint256(args[1]) // for proof storage enroll := s.getEnrollmentInternal(ic.DAO, enrollID) if enroll == nil { panic(ErrScireEnrollNotFound) } if enroll.Status != state.EnrollmentActive { panic(ErrScireEnrollNotActive) } // Check caller is institution or educator caller := ic.VM.GetCallingScriptHash() if caller != enroll.Institution && !s.checkEducator(ic) { panic(ErrScireNotInstitution) } // Update enrollment enroll.Status = state.EnrollmentCompleted enroll.CompletedAt = ic.Block.Index s.putEnrollment(ic.DAO, enroll) // Clear active enrollment s.clearActiveEnrollment(ic.DAO, enroll.VitaID) // Emit event ic.AddNotification(s.Hash, EnrollmentCompletedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(enrollID))), stackitem.NewBigInteger(big.NewInt(int64(enroll.VitaID))), })) return stackitem.NewBool(true) } // withdrawEnrollment withdraws from an enrollment. func (s *Scire) withdrawEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { enrollID := toUint64(args[0]) reason := toString(args[1]) enroll := s.getEnrollmentInternal(ic.DAO, enrollID) if enroll == nil { panic(ErrScireEnrollNotFound) } if enroll.Status != state.EnrollmentActive { panic(ErrScireEnrollNotActive) } // Check caller is student or institution caller := ic.VM.GetCallingScriptHash() if caller != enroll.Student && caller != enroll.Institution && !s.checkEducator(ic) { panic(ErrScireNotStudent) } // Update enrollment enroll.Status = state.EnrollmentWithdrawn enroll.CompletedAt = ic.Block.Index s.putEnrollment(ic.DAO, enroll) // Clear active enrollment s.clearActiveEnrollment(ic.DAO, enroll.VitaID) // Partial refund: return 50% of credits acc := s.getAccountInternal(ic.DAO, enroll.VitaID) if acc != nil { refund := enroll.CreditsAllocated / 2 acc.AvailableCredits += refund acc.UsedCredits -= refund acc.UpdatedAt = ic.Block.Index s.putAccount(ic.DAO, acc) } // Emit event ic.AddNotification(s.Hash, EnrollmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(enrollID))), stackitem.NewByteArray([]byte(reason)), })) return stackitem.NewBool(true) } // getEnrollment returns enrollment details. func (s *Scire) getEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { enrollID := toUint64(args[0]) enroll := s.getEnrollmentInternal(ic.DAO, enrollID) if enroll == nil { return stackitem.Null{} } item, _ := enroll.ToStackItem() return item } // getActiveEnrollment returns a student's active enrollment. func (s *Scire) getActiveEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { student := toUint160(args[0]) vitaID, found := s.getVitaIDByOwner(ic.DAO, student) if !found { return stackitem.Null{} } enrollID := s.getActiveEnrollmentID(ic.DAO, vitaID) if enrollID == 0 { return stackitem.Null{} } enroll := s.getEnrollmentInternal(ic.DAO, enrollID) if enroll == nil { return stackitem.Null{} } item, _ := enroll.ToStackItem() return item } // issueCertification issues a certification (educator only). func (s *Scire) issueCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) certType := toString(args[1]) name := toString(args[2]) contentHashBytes := toBytes(args[3]) expiresAt := toUint32(args[4]) // Convert bytes to Uint256 var contentHash util.Uint256 if len(contentHashBytes) == 32 { copy(contentHash[:], contentHashBytes) } // Validate inputs if len(certType) == 0 || len(certType) > 64 { panic(ErrScireInvalidCertType) } if len(name) == 0 || len(name) > 128 { panic(ErrScireInvalidName) } // Check educator authority if !s.checkEducator(ic) { panic(ErrScireNotEducator) } // Get owner's Vita if s.Vita == nil { panic(ErrScireNoVita) } vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) if err != nil || vita == nil || vita.Status != state.TokenStatusActive { panic(ErrScireNoVita) } // Get issuing institution institution := ic.VM.GetCallingScriptHash() // Get next cert ID cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) certID := cache.certCount cache.certCount++ s.setCertCounter(ic.DAO, cache.certCount) // Create certification cert := &state.Certification{ ID: certID, VitaID: vita.TokenID, Owner: owner, CertType: certType, Name: name, Institution: institution, ContentHash: contentHash, IssuedAt: ic.Block.Index, ExpiresAt: expiresAt, Status: state.CertificationActive, } s.putCertification(ic.DAO, cert) s.setCertByOwner(ic.DAO, vita.TokenID, certID) // Emit event ic.AddNotification(s.Hash, CertificationIssuedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(certID))), stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), stackitem.NewByteArray([]byte(certType)), })) return stackitem.NewBigInteger(big.NewInt(int64(certID))) } // revokeCertification revokes a certification (institution only). func (s *Scire) revokeCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { certID := toUint64(args[0]) reason := toString(args[1]) cert := s.getCertificationInternal(ic.DAO, certID) if cert == nil { panic(ErrScireCertNotFound) } if cert.Status == state.CertificationRevoked { panic(ErrScireCertRevoked) } // Check caller is issuing institution or educator caller := ic.VM.GetCallingScriptHash() if caller != cert.Institution && !s.checkEducator(ic) { panic(ErrScireNotInstitution) } // Revoke cert.Status = state.CertificationRevoked s.putCertification(ic.DAO, cert) // Emit event ic.AddNotification(s.Hash, CertificationRevokedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(certID))), stackitem.NewByteArray([]byte(reason)), })) return stackitem.NewBool(true) } // renewCertification extends a certification's expiry. func (s *Scire) renewCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { certID := toUint64(args[0]) newExpiresAt := toUint32(args[1]) cert := s.getCertificationInternal(ic.DAO, certID) if cert == nil { panic(ErrScireCertNotFound) } if cert.Status == state.CertificationRevoked { panic(ErrScireCertRevoked) } // Check caller is issuing institution or educator caller := ic.VM.GetCallingScriptHash() if caller != cert.Institution && !s.checkEducator(ic) { panic(ErrScireNotInstitution) } // Update expiry cert.ExpiresAt = newExpiresAt cert.Status = state.CertificationActive // Reactivate if was expired s.putCertification(ic.DAO, cert) // Emit event ic.AddNotification(s.Hash, CertificationRenewedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(certID))), stackitem.NewBigInteger(big.NewInt(int64(newExpiresAt))), })) return stackitem.NewBool(true) } // getCertification returns certification details. func (s *Scire) getCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { certID := toUint64(args[0]) cert := s.getCertificationInternal(ic.DAO, certID) if cert == nil { return stackitem.Null{} } item, _ := cert.ToStackItem() return item } // verifyCertification checks if a certification is currently valid. func (s *Scire) verifyCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { certID := toUint64(args[0]) cert := s.getCertificationInternal(ic.DAO, certID) if cert == nil { return stackitem.NewBool(false) } return stackitem.NewBool(cert.IsValid(ic.Block.Index)) } // hasCertification checks if an owner has a specific certification type. func (s *Scire) hasCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) certType := toString(args[1]) vitaID, exists := s.getVitaIDByOwner(ic.DAO, owner) if !exists { return stackitem.NewBool(false) } // Scan certifications for this owner prefix := make([]byte, 9) prefix[0] = scirePrefixCertByOwner binary.BigEndian.PutUint64(prefix[1:], vitaID) var hasCert bool ic.DAO.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 8 { certID := binary.BigEndian.Uint64(k[len(k)-8:]) cert := s.getCertificationInternal(ic.DAO, certID) if cert != nil && cert.CertType == certType && cert.IsValid(ic.Block.Index) { hasCert = true return false // Stop iteration } } return true }) return stackitem.NewBool(hasCert) } // getConfig returns the Scire configuration. func (s *Scire) 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 education accounts. func (s *Scire) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item { cache := ic.DAO.GetROCache(s.ID).(*ScireCache) return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount))) } // getTotalCertifications returns the total number of certifications. func (s *Scire) getTotalCertifications(ic *interop.Context, args []stackitem.Item) stackitem.Item { cache := ic.DAO.GetROCache(s.ID).(*ScireCache) return stackitem.NewBigInteger(big.NewInt(int64(cache.certCount))) } // getTotalEnrollments returns the total number of enrollments. func (s *Scire) getTotalEnrollments(ic *interop.Context, args []stackitem.Item) stackitem.Item { cache := ic.DAO.GetROCache(s.ID).(*ScireCache) return stackitem.NewBigInteger(big.NewInt(int64(cache.enrollCount))) } // ===== Public Interface Methods for Cross-Contract Access ===== // GetAccountByOwner returns an education account by owner address. func (s *Scire) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.EducationAccount, error) { vitaID, found := s.getVitaIDByOwner(d, owner) if !found { return nil, ErrScireAccountNotFound } acc := s.getAccountInternal(d, vitaID) if acc == nil { return nil, ErrScireAccountNotFound } return acc, nil } // HasValidCertification checks if owner has a valid certification of the given type. func (s *Scire) HasValidCertification(d *dao.Simple, owner util.Uint160, certType string, blockHeight uint32) bool { vitaID, exists := s.getVitaIDByOwner(d, owner) if !exists { return false } prefix := make([]byte, 9) prefix[0] = scirePrefixCertByOwner binary.BigEndian.PutUint64(prefix[1:], vitaID) var hasCert bool d.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 8 { certID := binary.BigEndian.Uint64(k[len(k)-8:]) cert := s.getCertificationInternal(d, certID) if cert != nil && cert.CertType == certType { if cert.Status == state.CertificationActive { if cert.ExpiresAt == 0 || cert.ExpiresAt > blockHeight { hasCert = true return false } } } } return true }) return hasCert } // Address returns the contract's script hash. func (s *Scire) Address() util.Uint160 { return s.Hash }