1278 lines
41 KiB
Go
Executable File
1278 lines
41 KiB
Go
Executable File
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
|
|
Tutus ITutus
|
|
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.Tutus.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
|
|
}
|