704 lines
21 KiB
Go
704 lines
21 KiB
Go
package native
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/config"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/dao"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/interop"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/state"
|
|
"github.com/tutus-one/tutus-chain/pkg/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"
|
|
)
|
|
|
|
// Annos represents the lifespan/years native contract for tracking Vita holder ages.
|
|
type Annos struct {
|
|
interop.ContractMD
|
|
Tutus ITutus
|
|
Vita IVita
|
|
}
|
|
|
|
// AnnosCache represents the cached state for Annos contract.
|
|
type AnnosCache struct {
|
|
recordCount uint64
|
|
}
|
|
|
|
// Storage key prefixes for Annos.
|
|
const (
|
|
annosPrefixLifespan byte = 0x01 // vitaID -> LifespanRecord
|
|
annosPrefixByOwner byte = 0x02 // owner -> vitaID
|
|
annosPrefixRecordCounter byte = 0xF0 // -> uint64
|
|
annosPrefixConfig byte = 0xFF // -> AnnosConfig
|
|
)
|
|
|
|
// Event names for Annos.
|
|
const (
|
|
BirthRegisteredEvent = "BirthRegistered"
|
|
DeathRecordedEvent = "DeathRecorded"
|
|
LifeStageChangedEvent = "LifeStageChanged"
|
|
)
|
|
|
|
// Seconds per year (approximately, for age calculation).
|
|
const secondsPerYear = 365 * 24 * 60 * 60
|
|
|
|
// Various errors for Annos.
|
|
var (
|
|
ErrAnnosRecordNotFound = errors.New("lifespan record not found")
|
|
ErrAnnosRecordExists = errors.New("lifespan record already exists")
|
|
ErrAnnosNoVita = errors.New("owner must have an active Vita")
|
|
ErrAnnosNotCommittee = errors.New("invalid committee signature")
|
|
ErrAnnosAlreadyDeceased = errors.New("person is already marked as deceased")
|
|
ErrAnnosInvalidTimestamp = errors.New("invalid birth timestamp")
|
|
ErrAnnosNotVita = errors.New("only Vita contract can call this method")
|
|
)
|
|
|
|
var (
|
|
_ interop.Contract = (*Annos)(nil)
|
|
_ dao.NativeContractCache = (*AnnosCache)(nil)
|
|
)
|
|
|
|
// Copy implements NativeContractCache interface.
|
|
func (c *AnnosCache) Copy() dao.NativeContractCache {
|
|
return &AnnosCache{
|
|
recordCount: c.recordCount,
|
|
}
|
|
}
|
|
|
|
// checkCommittee checks if the caller has committee authority.
|
|
func (a *Annos) checkCommittee(ic *interop.Context) bool {
|
|
return a.Tutus.CheckCommittee(ic)
|
|
}
|
|
|
|
// newAnnos creates a new Annos native contract.
|
|
func newAnnos() *Annos {
|
|
ann := &Annos{
|
|
ContractMD: *interop.NewContractMD(nativenames.Annos, nativeids.Annos),
|
|
}
|
|
defer ann.BuildHFSpecificMD(ann.ActiveIn())
|
|
|
|
// ===== Age Queries =====
|
|
|
|
// getAge - Get current age in years
|
|
desc := NewDescriptor("getAge", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md := NewMethodAndPrice(ann.getAge, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// getAgeAtTime - Get age at a specific timestamp
|
|
desc = NewDescriptor("getAgeAtTime", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("timestamp", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(ann.getAgeAtTime, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// getLifeStage - Get life stage (Child, Youth, Adult, Elder)
|
|
desc = NewDescriptor("getLifeStage", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.getLifeStage, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// getBirthTimestamp - Get actual birth timestamp
|
|
desc = NewDescriptor("getBirthTimestamp", smartcontract.IntegerType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.getBirthTimestamp, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// getRecord - Get full lifespan record
|
|
desc = NewDescriptor("getRecord", smartcontract.ArrayType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.getRecord, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// getRecordByVitaID - Get record by Vita ID
|
|
desc = NewDescriptor("getRecordByVitaID", smartcontract.ArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(ann.getRecordByVitaID, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// ===== Entitlement Checks =====
|
|
|
|
// isVotingAge - Check if at voting age
|
|
desc = NewDescriptor("isVotingAge", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.isVotingAge, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// isAdult - Check if adult
|
|
desc = NewDescriptor("isAdult", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.isAdult, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// isRetirementAge - Check if at retirement age
|
|
desc = NewDescriptor("isRetirementAge", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.isRetirementAge, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// isAlive - Check if person is alive
|
|
desc = NewDescriptor("isAlive", smartcontract.BoolType,
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(ann.isAlive, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// ===== Life Events (Committee Only) =====
|
|
|
|
// recordDeath - Record death (committee only)
|
|
desc = NewDescriptor("recordDeath", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("deathTimestamp", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(ann.recordDeath, 1<<17, callflag.States|callflag.AllowNotify)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// ===== Query Methods =====
|
|
|
|
// getConfig - Get Annos configuration
|
|
desc = NewDescriptor("getConfig", smartcontract.ArrayType)
|
|
md = NewMethodAndPrice(ann.getConfig, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// getTotalRecords - Get total lifespan records
|
|
desc = NewDescriptor("getTotalRecords", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(ann.getTotalRecords, 1<<15, callflag.ReadStates)
|
|
ann.AddMethod(md, desc)
|
|
|
|
// ===== Events =====
|
|
|
|
// BirthRegistered event
|
|
eDesc := NewEventDescriptor(BirthRegisteredEvent,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
|
manifest.NewParameter("birthTimestamp", smartcontract.IntegerType))
|
|
ann.AddEvent(NewEvent(eDesc))
|
|
|
|
// DeathRecorded event
|
|
eDesc = NewEventDescriptor(DeathRecordedEvent,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("deathTimestamp", smartcontract.IntegerType))
|
|
ann.AddEvent(NewEvent(eDesc))
|
|
|
|
// LifeStageChanged event
|
|
eDesc = NewEventDescriptor(LifeStageChangedEvent,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("previousStage", smartcontract.IntegerType),
|
|
manifest.NewParameter("newStage", smartcontract.IntegerType))
|
|
ann.AddEvent(NewEvent(eDesc))
|
|
|
|
return ann
|
|
}
|
|
|
|
// Metadata returns contract metadata.
|
|
func (a *Annos) Metadata() *interop.ContractMD {
|
|
return &a.ContractMD
|
|
}
|
|
|
|
// Initialize initializes the Annos contract.
|
|
func (a *Annos) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
|
|
if hf != a.ActiveIn() {
|
|
return nil
|
|
}
|
|
|
|
// Initialize counter
|
|
a.setRecordCounter(ic.DAO, 0)
|
|
|
|
// Initialize config with defaults
|
|
cfg := state.DefaultAnnosConfig()
|
|
a.setConfig(ic.DAO, &cfg)
|
|
|
|
// Initialize cache
|
|
cache := &AnnosCache{
|
|
recordCount: 0,
|
|
}
|
|
ic.DAO.SetCache(a.ID, cache)
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitializeCache initializes the cache from storage.
|
|
func (a *Annos) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
|
|
cache := &AnnosCache{
|
|
recordCount: a.getRecordCounter(d),
|
|
}
|
|
d.SetCache(a.ID, cache)
|
|
return nil
|
|
}
|
|
|
|
// OnPersist is called before block is committed.
|
|
func (a *Annos) OnPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// PostPersist is called after block is committed.
|
|
func (a *Annos) PostPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// ActiveIn returns the hardfork at which this contract is activated.
|
|
func (a *Annos) ActiveIn() *config.Hardfork {
|
|
return nil // Always active
|
|
}
|
|
|
|
// ===== Storage Helpers =====
|
|
|
|
func (a *Annos) makeLifespanKey(vitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = annosPrefixLifespan
|
|
binary.BigEndian.PutUint64(key[1:], vitaID)
|
|
return key
|
|
}
|
|
|
|
func (a *Annos) makeByOwnerKey(owner util.Uint160) []byte {
|
|
key := make([]byte, 21)
|
|
key[0] = annosPrefixByOwner
|
|
copy(key[1:], owner.BytesBE())
|
|
return key
|
|
}
|
|
|
|
func (a *Annos) getRecordCounter(d *dao.Simple) uint64 {
|
|
si := d.GetStorageItem(a.ID, []byte{annosPrefixRecordCounter})
|
|
if si == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (a *Annos) setRecordCounter(d *dao.Simple, count uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, count)
|
|
d.PutStorageItem(a.ID, []byte{annosPrefixRecordCounter}, buf)
|
|
}
|
|
|
|
func (a *Annos) getConfigInternal(d *dao.Simple) *state.AnnosConfig {
|
|
si := d.GetStorageItem(a.ID, []byte{annosPrefixConfig})
|
|
if si == nil {
|
|
cfg := state.DefaultAnnosConfig()
|
|
return &cfg
|
|
}
|
|
cfg := new(state.AnnosConfig)
|
|
item, _ := stackitem.Deserialize(si)
|
|
cfg.FromStackItem(item)
|
|
return cfg
|
|
}
|
|
|
|
func (a *Annos) setConfig(d *dao.Simple, cfg *state.AnnosConfig) {
|
|
item, _ := cfg.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(a.ID, []byte{annosPrefixConfig}, data)
|
|
}
|
|
|
|
func (a *Annos) getRecordInternal(d *dao.Simple, vitaID uint64) *state.LifespanRecord {
|
|
si := d.GetStorageItem(a.ID, a.makeLifespanKey(vitaID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
rec := new(state.LifespanRecord)
|
|
item, _ := stackitem.Deserialize(si)
|
|
rec.FromStackItem(item)
|
|
return rec
|
|
}
|
|
|
|
func (a *Annos) putRecord(d *dao.Simple, rec *state.LifespanRecord) {
|
|
item, _ := rec.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(a.ID, a.makeLifespanKey(rec.VitaID), data)
|
|
}
|
|
|
|
func (a *Annos) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) {
|
|
si := d.GetStorageItem(a.ID, a.makeByOwnerKey(owner))
|
|
if si == nil {
|
|
return 0, false
|
|
}
|
|
return binary.BigEndian.Uint64(si), true
|
|
}
|
|
|
|
func (a *Annos) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) {
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, vitaID)
|
|
d.PutStorageItem(a.ID, a.makeByOwnerKey(owner), buf)
|
|
}
|
|
|
|
// calculateAge calculates age in years from birth timestamp to current timestamp.
|
|
func calculateAge(birthTimestamp, currentTimestamp uint64) uint8 {
|
|
if currentTimestamp <= birthTimestamp {
|
|
return 0
|
|
}
|
|
ageSeconds := currentTimestamp - birthTimestamp
|
|
ageYears := ageSeconds / secondsPerYear
|
|
if ageYears > 255 {
|
|
return 255 // Cap at max uint8
|
|
}
|
|
return uint8(ageYears)
|
|
}
|
|
|
|
// calculateLifeStage determines the life stage based on age.
|
|
func (a *Annos) calculateLifeStage(d *dao.Simple, age uint8) state.LifeStage {
|
|
cfg := a.getConfigInternal(d)
|
|
|
|
if age < cfg.AdultAge {
|
|
return state.LifeStageChild
|
|
} else if age <= cfg.YouthMaxAge {
|
|
return state.LifeStageYouth
|
|
} else if age < cfg.ElderMinAge {
|
|
return state.LifeStageAdult
|
|
}
|
|
return state.LifeStageElder
|
|
}
|
|
|
|
// ===== Contract Methods =====
|
|
|
|
// getAge returns the current age in years.
|
|
func (a *Annos) getAge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := a.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
// If deceased, return age at death
|
|
currentTime := ic.Block.Timestamp / 1000 // Block timestamp is in ms
|
|
if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 {
|
|
currentTime = rec.DeathTimestamp
|
|
}
|
|
|
|
age := calculateAge(rec.BirthTimestamp, currentTime)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(age)))
|
|
}
|
|
|
|
// getAgeAtTime returns the age at a specific timestamp.
|
|
func (a *Annos) getAgeAtTime(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
timestamp := toUint64(args[1])
|
|
|
|
vitaID, found := a.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
// If deceased and query time is after death, use death time
|
|
queryTime := timestamp
|
|
if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 && timestamp > rec.DeathTimestamp {
|
|
queryTime = rec.DeathTimestamp
|
|
}
|
|
|
|
age := calculateAge(rec.BirthTimestamp, queryTime)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(age)))
|
|
}
|
|
|
|
// getLifeStage returns the current life stage.
|
|
func (a *Annos) getLifeStage(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := a.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
currentTime := ic.Block.Timestamp / 1000
|
|
if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 {
|
|
currentTime = rec.DeathTimestamp
|
|
}
|
|
|
|
age := calculateAge(rec.BirthTimestamp, currentTime)
|
|
stage := a.calculateLifeStage(ic.DAO, age)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(stage)))
|
|
}
|
|
|
|
// getBirthTimestamp returns the birth timestamp.
|
|
func (a *Annos) getBirthTimestamp(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := a.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.NewBigInteger(big.NewInt(0))
|
|
}
|
|
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(rec.BirthTimestamp))
|
|
}
|
|
|
|
// getRecord returns the full lifespan record.
|
|
func (a *Annos) getRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := a.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := rec.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getRecordByVitaID returns the record by Vita ID.
|
|
func (a *Annos) getRecordByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
item, _ := rec.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// isVotingAge checks if the owner is at voting age.
|
|
func (a *Annos) isVotingAge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
return stackitem.NewBool(a.IsVotingAgeInternal(ic.DAO, owner, ic.Block.Timestamp/1000))
|
|
}
|
|
|
|
// isAdult checks if the owner is an adult.
|
|
func (a *Annos) isAdult(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
return stackitem.NewBool(a.IsAdultInternal(ic.DAO, owner, ic.Block.Timestamp/1000))
|
|
}
|
|
|
|
// isRetirementAge checks if the owner is at retirement age.
|
|
func (a *Annos) isRetirementAge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
return stackitem.NewBool(a.IsRetirementAgeInternal(ic.DAO, owner, ic.Block.Timestamp/1000))
|
|
}
|
|
|
|
// isAlive checks if the owner is alive.
|
|
func (a *Annos) isAlive(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
owner := toUint160(args[0])
|
|
|
|
vitaID, found := a.getVitaIDByOwner(ic.DAO, owner)
|
|
if !found {
|
|
return stackitem.NewBool(false)
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
return stackitem.NewBool(false)
|
|
}
|
|
|
|
return stackitem.NewBool(rec.Status == state.LifespanActive)
|
|
}
|
|
|
|
// recordDeath records a death (committee only).
|
|
func (a *Annos) recordDeath(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
deathTimestamp := toUint64(args[1])
|
|
|
|
// Committee only
|
|
if !a.checkCommittee(ic) {
|
|
panic(ErrAnnosNotCommittee)
|
|
}
|
|
|
|
rec := a.getRecordInternal(ic.DAO, vitaID)
|
|
if rec == nil {
|
|
panic(ErrAnnosRecordNotFound)
|
|
}
|
|
|
|
if rec.Status == state.LifespanDeceased {
|
|
panic(ErrAnnosAlreadyDeceased)
|
|
}
|
|
|
|
// Validate death timestamp
|
|
if deathTimestamp == 0 {
|
|
deathTimestamp = ic.Block.Timestamp / 1000
|
|
}
|
|
if deathTimestamp < rec.BirthTimestamp {
|
|
panic(ErrAnnosInvalidTimestamp)
|
|
}
|
|
|
|
// Update record
|
|
rec.DeathTimestamp = deathTimestamp
|
|
rec.DeathBlock = ic.Block.Index
|
|
rec.Status = state.LifespanDeceased
|
|
a.putRecord(ic.DAO, rec)
|
|
|
|
// Emit event
|
|
ic.AddNotification(a.Hash, DeathRecordedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(deathTimestamp)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getConfig returns the Annos configuration.
|
|
func (a *Annos) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cfg := a.getConfigInternal(ic.DAO)
|
|
item, _ := cfg.ToStackItem()
|
|
return item
|
|
}
|
|
|
|
// getTotalRecords returns the total number of lifespan records.
|
|
func (a *Annos) getTotalRecords(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(a.ID).(*AnnosCache)
|
|
return stackitem.NewBigInteger(big.NewInt(int64(cache.recordCount)))
|
|
}
|
|
|
|
// ===== Public Interface Methods for Cross-Contract Access =====
|
|
|
|
// RegisterBirthInternal is called by Vita contract to register a birth.
|
|
// This should only be called when minting a new Vita token.
|
|
func (a *Annos) RegisterBirthInternal(d *dao.Simple, ic *interop.Context, vitaID uint64, owner util.Uint160, birthTimestamp uint64) error {
|
|
// Check if record already exists
|
|
existing := a.getRecordInternal(d, vitaID)
|
|
if existing != nil {
|
|
return ErrAnnosRecordExists
|
|
}
|
|
|
|
// Validate birth timestamp (must not be in the future)
|
|
currentTime := ic.Block.Timestamp / 1000
|
|
if birthTimestamp > currentTime {
|
|
birthTimestamp = currentTime // Cap at current time
|
|
}
|
|
|
|
// Increment counter
|
|
cache := d.GetRWCache(a.ID).(*AnnosCache)
|
|
cache.recordCount++
|
|
a.setRecordCounter(d, cache.recordCount)
|
|
|
|
// Create lifespan record
|
|
rec := &state.LifespanRecord{
|
|
VitaID: vitaID,
|
|
Owner: owner,
|
|
BirthTimestamp: birthTimestamp,
|
|
RegistrationBlock: ic.Block.Index,
|
|
RegistrationTime: currentTime,
|
|
DeathTimestamp: 0,
|
|
DeathBlock: 0,
|
|
Status: state.LifespanActive,
|
|
}
|
|
|
|
a.putRecord(d, rec)
|
|
a.setOwnerToVitaID(d, owner, vitaID)
|
|
|
|
// Emit event
|
|
ic.AddNotification(a.Hash, BirthRegisteredEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewByteArray(owner.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(birthTimestamp)),
|
|
}))
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAgeInternal returns the age of an owner at the current timestamp.
|
|
func (a *Annos) GetAgeInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) (uint8, error) {
|
|
vitaID, found := a.getVitaIDByOwner(d, owner)
|
|
if !found {
|
|
return 0, ErrAnnosRecordNotFound
|
|
}
|
|
|
|
rec := a.getRecordInternal(d, vitaID)
|
|
if rec == nil {
|
|
return 0, ErrAnnosRecordNotFound
|
|
}
|
|
|
|
// If deceased, use death time
|
|
queryTime := currentTimestamp
|
|
if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 {
|
|
queryTime = rec.DeathTimestamp
|
|
}
|
|
|
|
return calculateAge(rec.BirthTimestamp, queryTime), nil
|
|
}
|
|
|
|
// IsVotingAgeInternal checks if an owner is at voting age.
|
|
func (a *Annos) IsVotingAgeInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) bool {
|
|
age, err := a.GetAgeInternal(d, owner, currentTimestamp)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
cfg := a.getConfigInternal(d)
|
|
return age >= cfg.VotingAge
|
|
}
|
|
|
|
// IsAdultInternal checks if an owner is an adult.
|
|
func (a *Annos) IsAdultInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) bool {
|
|
age, err := a.GetAgeInternal(d, owner, currentTimestamp)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
cfg := a.getConfigInternal(d)
|
|
return age >= cfg.AdultAge
|
|
}
|
|
|
|
// IsRetirementAgeInternal checks if an owner is at retirement age.
|
|
func (a *Annos) IsRetirementAgeInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) bool {
|
|
age, err := a.GetAgeInternal(d, owner, currentTimestamp)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
cfg := a.getConfigInternal(d)
|
|
return age >= cfg.RetirementAge
|
|
}
|
|
|
|
// GetLifeStageInternal returns the life stage of an owner.
|
|
func (a *Annos) GetLifeStageInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) state.LifeStage {
|
|
age, err := a.GetAgeInternal(d, owner, currentTimestamp)
|
|
if err != nil {
|
|
return state.LifeStageChild // Default
|
|
}
|
|
return a.calculateLifeStage(d, age)
|
|
}
|
|
|
|
// IsAliveInternal checks if an owner is alive.
|
|
func (a *Annos) IsAliveInternal(d *dao.Simple, owner util.Uint160) bool {
|
|
vitaID, found := a.getVitaIDByOwner(d, owner)
|
|
if !found {
|
|
return false
|
|
}
|
|
rec := a.getRecordInternal(d, vitaID)
|
|
if rec == nil {
|
|
return false
|
|
}
|
|
return rec.Status == state.LifespanActive
|
|
}
|
|
|
|
// GetRecordByOwner returns the lifespan record for an owner.
|
|
func (a *Annos) GetRecordByOwner(d *dao.Simple, owner util.Uint160) (*state.LifespanRecord, error) {
|
|
vitaID, found := a.getVitaIDByOwner(d, owner)
|
|
if !found {
|
|
return nil, ErrAnnosRecordNotFound
|
|
}
|
|
rec := a.getRecordInternal(d, vitaID)
|
|
if rec == nil {
|
|
return nil, ErrAnnosRecordNotFound
|
|
}
|
|
return rec, nil
|
|
}
|
|
|
|
// Address returns the contract's script hash.
|
|
func (a *Annos) Address() util.Uint160 {
|
|
return a.Hash
|
|
}
|