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:
parent
3de7f96fc7
commit
1d96eb7a6e
|
|
@ -223,6 +223,7 @@ type Blockchain struct {
|
||||||
treasury native.ITreasury
|
treasury native.ITreasury
|
||||||
lex native.ILex
|
lex native.ILex
|
||||||
eligere native.IEligere
|
eligere native.IEligere
|
||||||
|
scire native.IScire
|
||||||
|
|
||||||
extensible atomic.Value
|
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 {
|
if err := validateNative(bc.eligere, nativeids.Eligere, nativenames.Eligere, nativehashes.Eligere); err != nil {
|
||||||
return nil, err
|
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.persistCond = sync.NewCond(&bc.lock)
|
||||||
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
|
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,19 @@ type (
|
||||||
// Address returns the contract's script hash.
|
// Address returns the contract's script hash.
|
||||||
Address() util.Uint160
|
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
|
// 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)
|
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.
|
// NewDefaultContracts returns a new set of default native contracts.
|
||||||
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
mgmt := NewManagement()
|
mgmt := NewManagement()
|
||||||
|
|
@ -475,6 +494,13 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
eligere.RoleRegistry = roleRegistry
|
eligere.RoleRegistry = roleRegistry
|
||||||
eligere.Lex = lex
|
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{
|
return []interop.Contract{
|
||||||
mgmt,
|
mgmt,
|
||||||
s,
|
s,
|
||||||
|
|
@ -493,5 +519,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
federation,
|
federation,
|
||||||
lex,
|
lex,
|
||||||
eligere,
|
eligere,
|
||||||
|
scire,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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}
|
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 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}
|
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}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,6 @@ const (
|
||||||
Lex int32 = -16
|
Lex int32 = -16
|
||||||
// Eligere is an ID of native Eligere contract.
|
// Eligere is an ID of native Eligere contract.
|
||||||
Eligere int32 = -17
|
Eligere int32 = -17
|
||||||
|
// Scire is an ID of native Scire contract.
|
||||||
|
Scire int32 = -18
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const (
|
||||||
Federation = "Federation"
|
Federation = "Federation"
|
||||||
Lex = "Lex"
|
Lex = "Lex"
|
||||||
Eligere = "Eligere"
|
Eligere = "Eligere"
|
||||||
|
Scire = "Scire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// All contains the list of all native contract names ordered by the contract ID.
|
// All contains the list of all native contract names ordered by the contract ID.
|
||||||
|
|
@ -40,6 +41,7 @@ var All = []string{
|
||||||
Federation,
|
Federation,
|
||||||
Lex,
|
Lex,
|
||||||
Eligere,
|
Eligere,
|
||||||
|
Scire,
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid checks if the name is a valid native contract's name.
|
// IsValid checks if the name is a valid native contract's name.
|
||||||
|
|
@ -60,5 +62,6 @@ func IsValid(name string) bool {
|
||||||
name == VTS ||
|
name == VTS ||
|
||||||
name == Federation ||
|
name == Federation ||
|
||||||
name == Lex ||
|
name == Lex ||
|
||||||
name == Eligere
|
name == Eligere ||
|
||||||
|
name == Scire
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue