Add Scire native contract for universal education

Implement the Scire (Latin for "to know/learn") contract providing
universal education infrastructure for citizens:

Core Features:
- Education accounts: One per Vita holder (soul-bound learning account)
- Learning credits: Annual allocation and spending system
- Certifications: Skill verification with expiry/renewal support
- Enrollments: Program enrollment with credit allocation

Contract Methods:
- Account management: createAccount, getAccount, allocateCredits, getCredits
- Enrollment: enroll, completeEnrollment, withdrawEnrollment, getActiveEnrollment
- Certification: issueCertification, revokeCertification, renewCertification
- Query: verifyCertification, hasCertification, getConfig

Cross-Contract Integration:
- Vita: Account tied to Vita token (one person = one account)
- Lex: Checks RightEducation via HasRightInternal (enforcement logging)
- RoleRegistry: RoleEducator (ID 20) for institutional authorization
- NEO: Committee authority for credit allocation

State Types (pkg/core/state/scire.go):
- EducationAccount, Certification, Enrollment, ScireConfig
- Status enums for each entity type

Technical Details:
- Contract ID: -18
- Storage prefixes: 0x01-0x02 (accounts), 0x10-0x13 (certs), 0x20-0x23 (enrolls)
- Fix: Use (uint64, bool) return for getVitaIDByOwner to properly handle
  TokenID 0 (first registered Vita) instead of using 0 as sentinel value

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Tutus Development 2025-12-20 07:25:08 +00:00
parent 3de7f96fc7
commit 1d96eb7a6e
8 changed files with 1957 additions and 1 deletions

View File

@ -223,6 +223,7 @@ type Blockchain struct {
treasury native.ITreasury
lex native.ILex
eligere native.IEligere
scire native.IScire
extensible atomic.Value
@ -490,6 +491,10 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger, newN
if err := validateNative(bc.eligere, nativeids.Eligere, nativenames.Eligere, nativehashes.Eligere); err != nil {
return nil, err
}
bc.scire = bc.contracts.Scire()
if err := validateNative(bc.scire, nativeids.Scire, nativenames.Scire, nativehashes.Scire); err != nil {
return nil, err
}
bc.persistCond = sync.NewCond(&bc.lock)
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size

View File

@ -225,6 +225,19 @@ type (
// Address returns the contract's script hash.
Address() util.Uint160
}
// IScire is an interface required from native Scire contract for
// interaction with Blockchain and other native contracts.
// Scire provides universal education infrastructure.
IScire interface {
interop.Contract
// GetAccountByOwner returns an education account by owner address.
GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.EducationAccount, error)
// HasValidCertification checks if owner has a valid certification of the given type.
HasValidCertification(d *dao.Simple, owner util.Uint160, certType string, blockHeight uint32) bool
// Address returns the contract's script hash.
Address() util.Uint160
}
)
// Contracts is a convenient wrapper around an arbitrary set of native contracts
@ -382,6 +395,12 @@ func (cs *Contracts) Eligere() IEligere {
return cs.ByName(nativenames.Eligere).(IEligere)
}
// Scire returns native IScire contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) Scire() IScire {
return cs.ByName(nativenames.Scire).(IScire)
}
// NewDefaultContracts returns a new set of default native contracts.
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
mgmt := NewManagement()
@ -475,6 +494,13 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
eligere.RoleRegistry = roleRegistry
eligere.Lex = lex
// Create Scire (Universal Education) contract
scire := newScire()
scire.NEO = neo
scire.Vita = vita
scire.RoleRegistry = roleRegistry
scire.Lex = lex
return []interop.Contract{
mgmt,
s,
@ -493,5 +519,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
federation,
lex,
eligere,
scire,
}
}

View File

@ -0,0 +1,224 @@
package native_test
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/stretchr/testify/require"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
"github.com/tutus-one/tutus-chain/pkg/neotest"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
func newScireClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.Scire)
}
// TestScire_GetConfig tests the getConfig method.
func TestScire_GetConfig(t *testing.T) {
c := newScireClient(t)
// Get default config
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array result")
require.GreaterOrEqual(t, len(arr), 4) // ScireConfig has 4 fields
}, "getConfig")
}
// TestScire_GetTotalAccounts tests the getTotalAccounts method.
func TestScire_GetTotalAccounts(t *testing.T) {
c := newScireClient(t)
// Initially should be 0
c.Invoke(t, 0, "getTotalAccounts")
}
// TestScire_GetTotalCertifications tests the getTotalCertifications method.
func TestScire_GetTotalCertifications(t *testing.T) {
c := newScireClient(t)
// Initially should be 0
c.Invoke(t, 0, "getTotalCertifications")
}
// TestScire_GetTotalEnrollments tests the getTotalEnrollments method.
func TestScire_GetTotalEnrollments(t *testing.T) {
c := newScireClient(t)
// Initially should be 0
c.Invoke(t, 0, "getTotalEnrollments")
}
// TestScire_GetAccount_NonExistent tests getting a non-existent account.
func TestScire_GetAccount_NonExistent(t *testing.T) {
c := newScireClient(t)
e := c.Executor
acc := e.NewAccount(t)
// Non-existent account should return null
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
require.Nil(t, stack[0].Value(), "expected null for non-existent account")
}, "getAccount", acc.ScriptHash())
}
// TestScire_GetCredits_NonExistent tests getting credits for non-existent account.
func TestScire_GetCredits_NonExistent(t *testing.T) {
c := newScireClient(t)
e := c.Executor
acc := e.NewAccount(t)
// Non-existent account should return 0 credits
c.Invoke(t, 0, "getCredits", acc.ScriptHash())
}
// TestScire_CreateAccount_NoVita tests that creating account without Vita fails.
func TestScire_CreateAccount_NoVita(t *testing.T) {
c := newScireClient(t)
e := c.Executor
acc := e.NewAccount(t)
invoker := c.WithSigners(acc)
// Should fail - no Vita registered
invoker.InvokeFail(t, "owner must have an active Vita", "createAccount", acc.ScriptHash())
}
// TestScire_AllocateCredits_NotCommittee tests that non-committee cannot allocate credits.
func TestScire_AllocateCredits_NotCommittee(t *testing.T) {
c := newScireClient(t)
e := c.Executor
acc := e.NewAccount(t)
invoker := c.WithSigners(acc)
// Should fail - not committee
invoker.InvokeFail(t, "invalid committee signature", "allocateCredits",
acc.ScriptHash(), 100, "test allocation")
}
// TestScire_IssueCertification_NotEducator tests that non-educators cannot issue certifications.
func TestScire_IssueCertification_NotEducator(t *testing.T) {
c := newScireClient(t)
e := c.Executor
acc := e.NewAccount(t)
invoker := c.WithSigners(acc)
contentHash := make([]byte, 32) // 32-byte hash
// Should fail - not an educator
invoker.InvokeFail(t, "caller is not an authorized educator", "issueCertification",
acc.ScriptHash(), "programming", "Go Developer", contentHash, int64(0))
}
// TestScire_VerifyCertification_NonExistent tests verifying a non-existent certification.
func TestScire_VerifyCertification_NonExistent(t *testing.T) {
c := newScireClient(t)
// Non-existent certification should return false
c.Invoke(t, false, "verifyCertification", int64(999))
}
// TestScire_HasCertification_NoAccount tests hasCertification for non-existent account.
func TestScire_HasCertification_NoAccount(t *testing.T) {
c := newScireClient(t)
e := c.Executor
acc := e.NewAccount(t)
// No account = no certification
c.Invoke(t, false, "hasCertification", acc.ScriptHash(), "programming")
}
// TestScire_GetEnrollment_NonExistent tests getting a non-existent enrollment.
func TestScire_GetEnrollment_NonExistent(t *testing.T) {
c := newScireClient(t)
// Non-existent enrollment should return null
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
require.Nil(t, stack[0].Value(), "expected null for non-existent enrollment")
}, "getEnrollment", int64(999))
}
// TestScire_CreateAccountWithVita tests account creation with a valid Vita.
func TestScire_CreateAccountWithVita(t *testing.T) {
c := newScireClient(t)
e := c.Executor
// Register Vita first
vitaHash := e.NativeHash(t, nativenames.Vita)
acc := e.NewAccount(t)
vitaInvoker := e.NewInvoker(vitaHash, acc)
owner := acc.ScriptHash()
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
isEntity := false
recoveryHash := hash.Sha256([]byte("recovery")).BytesBE()
// Register Vita token
vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
_, ok := stack[0].Value().([]byte)
require.True(t, ok, "expected ByteArray result")
}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash)
// Now create Scire account - need to pass owner as BytesBE for Hash160 type
scireInvoker := c.WithSigners(acc)
scireInvoker.Invoke(t, true, "createAccount", owner.BytesBE())
// Verify account exists - also pass as BytesBE
scireInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array result for existing account")
require.GreaterOrEqual(t, len(arr), 8) // EducationAccount has 8 fields
}, "getAccount", owner.BytesBE())
// Verify total accounts increased
c.Invoke(t, 1, "getTotalAccounts")
}
// TestScire_AllocateCredits tests credit allocation by committee.
func TestScire_AllocateCredits(t *testing.T) {
c := newScireClient(t)
e := c.Executor
// Register Vita first
vitaHash := e.NativeHash(t, nativenames.Vita)
acc := e.NewAccount(t)
vitaInvoker := e.NewInvoker(vitaHash, acc)
owner := acc.ScriptHash()
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
isEntity := false
recoveryHash := hash.Sha256([]byte("recovery")).BytesBE()
// Register returns ByteArray (tokenID), not Null
vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
_, ok := stack[0].Value().([]byte)
require.True(t, ok, "expected ByteArray result")
}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash)
// Create Scire account - use BytesBE for Hash160
scireInvoker := c.WithSigners(acc)
scireInvoker.Invoke(t, true, "createAccount", owner.BytesBE())
// Allocate credits as committee - use BytesBE for Hash160
committeeInvoker := c.WithSigners(c.Committee)
committeeInvoker.Invoke(t, true, "allocateCredits", owner.BytesBE(), int64(500), "annual allocation")
// Verify credits - use BytesBE for Hash160
c.Invoke(t, 500, "getCredits", owner.BytesBE())
}
// Note: Full cross-contract testing of enrollment and certification with actual Vita
// holders and educators would require deploying helper contracts. The Scire contract
// uses GetCallingScriptHash() for authorization which returns the transaction script
// hash for direct calls. This is the intended design for cross-contract authorization.

View File

@ -43,4 +43,6 @@ var (
Lex = util.Uint160{0x2e, 0x3f, 0xb7, 0x5, 0x8, 0x17, 0xef, 0xb1, 0xc2, 0xbe, 0x68, 0xc4, 0xd4, 0xde, 0xc6, 0xf6, 0x2d, 0x92, 0x96, 0xe6}
// Eligere is a hash of native Eligere contract.
Eligere = util.Uint160{0x1, 0x94, 0x73, 0x8e, 0xab, 0x6b, 0xc5, 0xa0, 0xff, 0xab, 0xe0, 0x2a, 0xce, 0xea, 0xd7, 0xb3, 0xa8, 0xe5, 0x7, 0x40}
// Scire is a hash of native Scire contract.
Scire = util.Uint160{0x9f, 0x7, 0x16, 0xd4, 0xd6, 0xb8, 0xae, 0x2d, 0x58, 0x42, 0x94, 0xf8, 0x92, 0x62, 0x5d, 0x8e, 0x63, 0xa0, 0xde, 0x3}
)

View File

@ -41,4 +41,6 @@ const (
Lex int32 = -16
// Eligere is an ID of native Eligere contract.
Eligere int32 = -17
// Scire is an ID of native Scire contract.
Scire int32 = -18
)

View File

@ -19,6 +19,7 @@ const (
Federation = "Federation"
Lex = "Lex"
Eligere = "Eligere"
Scire = "Scire"
)
// All contains the list of all native contract names ordered by the contract ID.
@ -40,6 +41,7 @@ var All = []string{
Federation,
Lex,
Eligere,
Scire,
}
// IsValid checks if the name is a valid native contract's name.
@ -60,5 +62,6 @@ func IsValid(name string) bool {
name == VTS ||
name == Federation ||
name == Lex ||
name == Eligere
name == Eligere ||
name == Scire
}

1277
pkg/core/native/scire.go Normal file

File diff suppressed because it is too large Load Diff

416
pkg/core/state/scire.go Normal file
View File

@ -0,0 +1,416 @@
package state
import (
"errors"
"fmt"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// EducationAccountStatus represents the status of an education account.
type EducationAccountStatus uint8
const (
// EducationAccountActive indicates an active account.
EducationAccountActive EducationAccountStatus = 0
// EducationAccountSuspended indicates a temporarily suspended account.
EducationAccountSuspended EducationAccountStatus = 1
// EducationAccountClosed indicates a permanently closed account.
EducationAccountClosed EducationAccountStatus = 2
)
// CertificationStatus represents the validity status of a certification.
type CertificationStatus uint8
const (
// CertificationActive indicates a valid active certification.
CertificationActive CertificationStatus = 0
// CertificationExpired indicates an expired certification.
CertificationExpired CertificationStatus = 1
// CertificationRevoked indicates a revoked certification.
CertificationRevoked CertificationStatus = 2
)
// EnrollmentStatus represents the status of a program enrollment.
type EnrollmentStatus uint8
const (
// EnrollmentActive indicates an active enrollment.
EnrollmentActive EnrollmentStatus = 0
// EnrollmentCompleted indicates successful completion.
EnrollmentCompleted EnrollmentStatus = 1
// EnrollmentWithdrawn indicates voluntary withdrawal.
EnrollmentWithdrawn EnrollmentStatus = 2
// EnrollmentTransferred indicates transfer to another institution.
EnrollmentTransferred EnrollmentStatus = 3
)
// EducationAccount represents a citizen's lifelong learning account.
type EducationAccount struct {
VitaID uint64 // Owner's Vita token ID
Owner util.Uint160 // Owner's address
TotalCredits uint64 // Lifetime credits received
UsedCredits uint64 // Credits spent on education
AvailableCredits uint64 // Current balance
Status EducationAccountStatus // Account status
CreatedAt uint32 // Block height when created
UpdatedAt uint32 // Block height of last modification
}
// ToStackItem implements stackitem.Convertible interface.
func (a *EducationAccount) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(a.VitaID))),
stackitem.NewByteArray(a.Owner.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(a.TotalCredits))),
stackitem.NewBigInteger(big.NewInt(int64(a.UsedCredits))),
stackitem.NewBigInteger(big.NewInt(int64(a.AvailableCredits))),
stackitem.NewBigInteger(big.NewInt(int64(a.Status))),
stackitem.NewBigInteger(big.NewInt(int64(a.CreatedAt))),
stackitem.NewBigInteger(big.NewInt(int64(a.UpdatedAt))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (a *EducationAccount) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 8 {
return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items))
}
vitaID, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid vitaID: %w", err)
}
a.VitaID = vitaID.Uint64()
owner, err := items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid owner: %w", err)
}
a.Owner, err = util.Uint160DecodeBytesBE(owner)
if err != nil {
return fmt.Errorf("invalid owner address: %w", err)
}
totalCredits, err := items[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalCredits: %w", err)
}
a.TotalCredits = totalCredits.Uint64()
usedCredits, err := items[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid usedCredits: %w", err)
}
a.UsedCredits = usedCredits.Uint64()
availableCredits, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid availableCredits: %w", err)
}
a.AvailableCredits = availableCredits.Uint64()
status, err := items[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid status: %w", err)
}
a.Status = EducationAccountStatus(status.Uint64())
createdAt, err := items[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid createdAt: %w", err)
}
a.CreatedAt = uint32(createdAt.Uint64())
updatedAt, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid updatedAt: %w", err)
}
a.UpdatedAt = uint32(updatedAt.Uint64())
return nil
}
// Certification represents a verified skill or credential.
type Certification struct {
ID uint64 // Unique certification ID
VitaID uint64 // Owner's Vita ID
Owner util.Uint160 // Owner's address
CertType string // Type of certification
Name string // Certification name
Institution util.Uint160 // Issuing institution
ContentHash util.Uint256 // Off-chain content hash
IssuedAt uint32 // Block height when issued
ExpiresAt uint32 // 0 = never expires
Status CertificationStatus // Certification status
}
// ToStackItem implements stackitem.Convertible interface.
func (c *Certification) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(c.ID))),
stackitem.NewBigInteger(big.NewInt(int64(c.VitaID))),
stackitem.NewByteArray(c.Owner.BytesBE()),
stackitem.NewByteArray([]byte(c.CertType)),
stackitem.NewByteArray([]byte(c.Name)),
stackitem.NewByteArray(c.Institution.BytesBE()),
stackitem.NewByteArray(c.ContentHash.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(c.IssuedAt))),
stackitem.NewBigInteger(big.NewInt(int64(c.ExpiresAt))),
stackitem.NewBigInteger(big.NewInt(int64(c.Status))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (c *Certification) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 10 {
return fmt.Errorf("wrong number of elements: expected 10, got %d", len(items))
}
id, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid id: %w", err)
}
c.ID = id.Uint64()
vitaID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid vitaID: %w", err)
}
c.VitaID = vitaID.Uint64()
owner, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid owner: %w", err)
}
c.Owner, err = util.Uint160DecodeBytesBE(owner)
if err != nil {
return fmt.Errorf("invalid owner address: %w", err)
}
certType, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid certType: %w", err)
}
c.CertType = string(certType)
name, err := items[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid name: %w", err)
}
c.Name = string(name)
institution, err := items[5].TryBytes()
if err != nil {
return fmt.Errorf("invalid institution: %w", err)
}
c.Institution, err = util.Uint160DecodeBytesBE(institution)
if err != nil {
return fmt.Errorf("invalid institution address: %w", err)
}
contentHash, err := items[6].TryBytes()
if err != nil {
return fmt.Errorf("invalid contentHash: %w", err)
}
c.ContentHash, err = util.Uint256DecodeBytesBE(contentHash)
if err != nil {
return fmt.Errorf("invalid contentHash value: %w", err)
}
issuedAt, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid issuedAt: %w", err)
}
c.IssuedAt = uint32(issuedAt.Uint64())
expiresAt, err := items[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid expiresAt: %w", err)
}
c.ExpiresAt = uint32(expiresAt.Uint64())
status, err := items[9].TryInteger()
if err != nil {
return fmt.Errorf("invalid status: %w", err)
}
c.Status = CertificationStatus(status.Uint64())
return nil
}
// IsExpired checks if the certification has expired.
func (c *Certification) IsExpired(currentBlock uint32) bool {
return c.ExpiresAt != 0 && c.ExpiresAt <= currentBlock
}
// IsValid checks if the certification is currently valid.
func (c *Certification) IsValid(currentBlock uint32) bool {
return c.Status == CertificationActive && !c.IsExpired(currentBlock)
}
// Enrollment represents a program enrollment record.
type Enrollment struct {
ID uint64 // Unique enrollment ID
VitaID uint64 // Student's Vita ID
Student util.Uint160 // Student's address
ProgramID string // Program identifier
Institution util.Uint160 // Educational institution
CreditsAllocated uint64 // Credits committed to program
StartedAt uint32 // Block height when started
CompletedAt uint32 // Block height when completed (0 = ongoing)
Status EnrollmentStatus // Enrollment status
}
// ToStackItem implements stackitem.Convertible interface.
func (e *Enrollment) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(e.ID))),
stackitem.NewBigInteger(big.NewInt(int64(e.VitaID))),
stackitem.NewByteArray(e.Student.BytesBE()),
stackitem.NewByteArray([]byte(e.ProgramID)),
stackitem.NewByteArray(e.Institution.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(e.CreditsAllocated))),
stackitem.NewBigInteger(big.NewInt(int64(e.StartedAt))),
stackitem.NewBigInteger(big.NewInt(int64(e.CompletedAt))),
stackitem.NewBigInteger(big.NewInt(int64(e.Status))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (e *Enrollment) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 9 {
return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items))
}
id, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid id: %w", err)
}
e.ID = id.Uint64()
vitaID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid vitaID: %w", err)
}
e.VitaID = vitaID.Uint64()
student, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid student: %w", err)
}
e.Student, err = util.Uint160DecodeBytesBE(student)
if err != nil {
return fmt.Errorf("invalid student address: %w", err)
}
programID, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid programID: %w", err)
}
e.ProgramID = string(programID)
institution, err := items[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid institution: %w", err)
}
e.Institution, err = util.Uint160DecodeBytesBE(institution)
if err != nil {
return fmt.Errorf("invalid institution address: %w", err)
}
creditsAllocated, err := items[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid creditsAllocated: %w", err)
}
e.CreditsAllocated = creditsAllocated.Uint64()
startedAt, err := items[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid startedAt: %w", err)
}
e.StartedAt = uint32(startedAt.Uint64())
completedAt, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid completedAt: %w", err)
}
e.CompletedAt = uint32(completedAt.Uint64())
status, err := items[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid status: %w", err)
}
e.Status = EnrollmentStatus(status.Uint64())
return nil
}
// ScireConfig represents configurable parameters for the Scire contract.
type ScireConfig struct {
AnnualCreditAllocation uint64 // Default credits per year
MaxCreditsPerProgram uint64 // Maximum credits for single program
CertificationFee uint64 // VTS fee for issuing certifications (0 = free)
MinEnrollmentDuration uint32 // Minimum blocks for enrollment
}
// ToStackItem implements stackitem.Convertible interface.
func (c *ScireConfig) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(c.AnnualCreditAllocation))),
stackitem.NewBigInteger(big.NewInt(int64(c.MaxCreditsPerProgram))),
stackitem.NewBigInteger(big.NewInt(int64(c.CertificationFee))),
stackitem.NewBigInteger(big.NewInt(int64(c.MinEnrollmentDuration))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (c *ScireConfig) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 4 {
return fmt.Errorf("wrong number of elements: expected 4, got %d", len(items))
}
annualCredits, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid annualCreditAllocation: %w", err)
}
c.AnnualCreditAllocation = annualCredits.Uint64()
maxCredits, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid maxCreditsPerProgram: %w", err)
}
c.MaxCreditsPerProgram = maxCredits.Uint64()
certFee, err := items[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid certificationFee: %w", err)
}
c.CertificationFee = certFee.Uint64()
minDuration, err := items[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid minEnrollmentDuration: %w", err)
}
c.MinEnrollmentDuration = uint32(minDuration.Uint64())
return nil
}