tutus-chain/pkg/core/native/scire.go

1278 lines
40 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/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
NEO INEO
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.NEO.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
}