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

1517 lines
49 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/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"
)
// Salus represents the universal healthcare native contract.
type Salus struct {
interop.ContractMD
Tutus ITutus
Vita IVita
RoleRegistry IRoleRegistry
Lex ILex
}
// SalusCache represents the cached state for Salus contract.
type SalusCache struct {
accountCount uint64
recordCount uint64
providerCount uint64
authorizationCount uint64
emergencyCount uint64
}
// Storage key prefixes for Salus.
const (
salusPrefixAccount byte = 0x01 // vitaID -> HealthcareAccount
salusPrefixAccountByOwner byte = 0x02 // owner -> vitaID
salusPrefixRecord byte = 0x10 // recordID -> MedicalRecord
salusPrefixRecordByPatient byte = 0x11 // vitaID + recordID -> exists
salusPrefixRecordByProvider byte = 0x12 // provider + recordID -> exists
salusPrefixProvider byte = 0x20 // providerID -> HealthcareProvider
salusPrefixProviderByAddress byte = 0x21 // provider address -> providerID
salusPrefixAuthorization byte = 0x30 // authID -> ProviderAuthorization
salusPrefixAuthByPatient byte = 0x31 // vitaID + authID -> exists
salusPrefixAuthByProvider byte = 0x32 // provider + authID -> exists
salusPrefixActiveAuth byte = 0x33 // vitaID + provider -> authID
salusPrefixEmergencyAccess byte = 0x40 // emergencyID -> EmergencyAccess
salusPrefixEmergencyByPatient byte = 0x41 // vitaID + emergencyID -> exists
salusPrefixAccountCounter byte = 0xF0 // -> uint64
salusPrefixRecordCounter byte = 0xF1 // -> next record ID
salusPrefixProviderCounter byte = 0xF2 // -> next provider ID
salusPrefixAuthCounter byte = 0xF3 // -> next authorization ID
salusPrefixEmergencyCounter byte = 0xF4 // -> next emergency access ID
salusPrefixConfig byte = 0xFF // -> SalusConfig
)
// Event names for Salus.
const (
HealthcareActivatedEvent = "HealthcareActivated"
CreditsAllocatedEventSalus = "CreditsAllocated"
MedicalRecordCreatedEvent = "MedicalRecordCreated"
ProviderRegisteredEvent = "ProviderRegistered"
ProviderSuspendedEvent = "ProviderSuspended"
AuthorizationGrantedEvent = "AuthorizationGranted"
AuthorizationRevokedEvent = "AuthorizationRevoked"
EmergencyAccessGrantedEvent = "EmergencyAccessGranted"
EmergencyAccessReviewedEvent = "EmergencyAccessReviewed"
)
// Role constants for healthcare providers.
const (
RoleHealthcare uint64 = 21 // Can record medical events and access authorized records
)
// Various errors for Salus.
var (
ErrSalusAccountNotFound = errors.New("healthcare account not found")
ErrSalusAccountExists = errors.New("healthcare account already exists")
ErrSalusAccountSuspended = errors.New("healthcare account is suspended")
ErrSalusAccountClosed = errors.New("healthcare account is closed")
ErrSalusNoVita = errors.New("owner must have an active Vita")
ErrSalusInsufficientCredits = errors.New("insufficient healthcare credits")
ErrSalusInvalidCredits = errors.New("invalid credit amount")
ErrSalusRecordNotFound = errors.New("medical record not found")
ErrSalusProviderNotFound = errors.New("healthcare provider not found")
ErrSalusProviderExists = errors.New("healthcare provider already registered")
ErrSalusProviderSuspended = errors.New("healthcare provider is suspended")
ErrSalusProviderRevoked = errors.New("healthcare provider is revoked")
ErrSalusNotProvider = errors.New("caller is not an authorized healthcare provider")
ErrSalusNotCommittee = errors.New("invalid committee signature")
ErrSalusInvalidOwner = errors.New("invalid owner address")
ErrSalusInvalidProvider = errors.New("invalid provider address")
ErrSalusAuthorizationNotFound = errors.New("authorization not found")
ErrSalusAuthorizationExpired = errors.New("authorization has expired")
ErrSalusAuthorizationExists = errors.New("authorization already exists")
ErrSalusNotPatient = errors.New("caller is not the patient")
ErrSalusHealthcareRestricted = errors.New("healthcare right is restricted")
ErrSalusEmergencyNotFound = errors.New("emergency access not found")
ErrSalusInvalidReason = errors.New("invalid reason")
ErrSalusInvalidName = errors.New("invalid name")
ErrSalusInvalidSpecialty = errors.New("invalid specialty")
ErrSalusNoAccess = errors.New("no access to patient records")
ErrSalusExceedsMaxDuration = errors.New("exceeds maximum authorization duration")
)
var (
_ interop.Contract = (*Salus)(nil)
_ dao.NativeContractCache = (*SalusCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *SalusCache) Copy() dao.NativeContractCache {
return &SalusCache{
accountCount: c.accountCount,
recordCount: c.recordCount,
providerCount: c.providerCount,
authorizationCount: c.authorizationCount,
emergencyCount: c.emergencyCount,
}
}
// checkCommittee checks if the caller has committee authority.
func (s *Salus) checkCommittee(ic *interop.Context) bool {
if s.RoleRegistry != nil {
return s.RoleRegistry.CheckCommittee(ic)
}
return s.Tutus.CheckCommittee(ic)
}
// checkHealthcareProvider checks if the caller has healthcare provider authority.
func (s *Salus) checkHealthcareProvider(ic *interop.Context) bool {
caller := ic.VM.GetCallingScriptHash()
if s.RoleRegistry != nil {
if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleHealthcare, ic.Block.Index) {
return true
}
}
// Committee members can also act as healthcare providers
return s.checkCommittee(ic)
}
// checkHealthcareRight checks if subject has healthcare rights via Lex.
func (s *Salus) checkHealthcareRight(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.RightHealthcare, ic.Block.Index)
}
// newSalus creates a new Salus native contract.
func newSalus() *Salus {
s := &Salus{
ContractMD: *interop.NewContractMD(nativenames.Salus, nativeids.Salus),
}
defer s.BuildHFSpecificMD(s.ActiveIn())
// ===== Account Management =====
// activateHealthcare - Activate healthcare account for a Vita holder
desc := NewDescriptor("activateHealthcare", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md := NewMethodAndPrice(s.activateHealthcare, 1<<17, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// getAccount - Get healthcare 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 healthcare 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)
// ===== Medical Records =====
// recordMedicalEvent - Record a medical event (provider only)
desc = NewDescriptor("recordMedicalEvent", smartcontract.IntegerType,
manifest.NewParameter("patient", smartcontract.Hash160Type),
manifest.NewParameter("recordType", smartcontract.IntegerType),
manifest.NewParameter("contentHash", smartcontract.Hash256Type),
manifest.NewParameter("credits", smartcontract.IntegerType))
md = NewMethodAndPrice(s.recordMedicalEvent, 1<<17, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// getMedicalRecord - Get medical record by ID
desc = NewDescriptor("getMedicalRecord", smartcontract.ArrayType,
manifest.NewParameter("recordID", smartcontract.IntegerType))
md = NewMethodAndPrice(s.getMedicalRecord, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// ===== Provider Management =====
// registerProvider - Register a healthcare provider (committee only)
desc = NewDescriptor("registerProvider", smartcontract.IntegerType,
manifest.NewParameter("address", smartcontract.Hash160Type),
manifest.NewParameter("name", smartcontract.StringType),
manifest.NewParameter("specialty", smartcontract.StringType),
manifest.NewParameter("licenseHash", smartcontract.Hash256Type))
md = NewMethodAndPrice(s.registerProvider, 1<<17, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// suspendProvider - Suspend a healthcare provider (committee only)
desc = NewDescriptor("suspendProvider", smartcontract.BoolType,
manifest.NewParameter("providerID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(s.suspendProvider, 1<<16, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// getProvider - Get provider details
desc = NewDescriptor("getProvider", smartcontract.ArrayType,
manifest.NewParameter("providerID", smartcontract.IntegerType))
md = NewMethodAndPrice(s.getProvider, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// getProviderByAddress - Get provider by address
desc = NewDescriptor("getProviderByAddress", smartcontract.ArrayType,
manifest.NewParameter("address", smartcontract.Hash160Type))
md = NewMethodAndPrice(s.getProviderByAddress, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// ===== Authorization Management =====
// authorizeAccess - Grant provider access to patient records
desc = NewDescriptor("authorizeAccess", smartcontract.IntegerType,
manifest.NewParameter("patient", smartcontract.Hash160Type),
manifest.NewParameter("provider", smartcontract.Hash160Type),
manifest.NewParameter("accessLevel", smartcontract.IntegerType),
manifest.NewParameter("duration", smartcontract.IntegerType))
md = NewMethodAndPrice(s.authorizeAccess, 1<<17, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// revokeAccess - Revoke provider access
desc = NewDescriptor("revokeAccess", smartcontract.BoolType,
manifest.NewParameter("authID", smartcontract.IntegerType))
md = NewMethodAndPrice(s.revokeAccess, 1<<16, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// getAuthorization - Get authorization details
desc = NewDescriptor("getAuthorization", smartcontract.ArrayType,
manifest.NewParameter("authID", smartcontract.IntegerType))
md = NewMethodAndPrice(s.getAuthorization, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// hasAccess - Check if provider has access to patient
desc = NewDescriptor("hasAccess", smartcontract.BoolType,
manifest.NewParameter("patient", smartcontract.Hash160Type),
manifest.NewParameter("provider", smartcontract.Hash160Type))
md = NewMethodAndPrice(s.hasAccess, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// ===== Emergency Access =====
// emergencyAccess - Request emergency access (provider only)
desc = NewDescriptor("emergencyAccess", smartcontract.IntegerType,
manifest.NewParameter("patient", smartcontract.Hash160Type),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(s.emergencyAccess, 1<<17, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// reviewEmergencyAccess - Review emergency access (committee only)
desc = NewDescriptor("reviewEmergencyAccess", smartcontract.BoolType,
manifest.NewParameter("emergencyID", smartcontract.IntegerType))
md = NewMethodAndPrice(s.reviewEmergencyAccess, 1<<16, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
// getEmergencyAccess - Get emergency access details
desc = NewDescriptor("getEmergencyAccess", smartcontract.ArrayType,
manifest.NewParameter("emergencyID", smartcontract.IntegerType))
md = NewMethodAndPrice(s.getEmergencyAccess, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// ===== Query Methods =====
// getConfig - Get Salus configuration
desc = NewDescriptor("getConfig", smartcontract.ArrayType)
md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// getTotalAccounts - Get total healthcare accounts
desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType)
md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// getTotalRecords - Get total medical records
desc = NewDescriptor("getTotalRecords", smartcontract.IntegerType)
md = NewMethodAndPrice(s.getTotalRecords, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// getTotalProviders - Get total healthcare providers
desc = NewDescriptor("getTotalProviders", smartcontract.IntegerType)
md = NewMethodAndPrice(s.getTotalProviders, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
// ===== Events =====
// HealthcareActivated event
eDesc := NewEventDescriptor(HealthcareActivatedEvent,
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("owner", smartcontract.Hash160Type))
s.AddEvent(NewEvent(eDesc))
// CreditsAllocated event
eDesc = NewEventDescriptor(CreditsAllocatedEventSalus,
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("total", smartcontract.IntegerType))
s.AddEvent(NewEvent(eDesc))
// MedicalRecordCreated event
eDesc = NewEventDescriptor(MedicalRecordCreatedEvent,
manifest.NewParameter("recordID", smartcontract.IntegerType),
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("recordType", smartcontract.IntegerType))
s.AddEvent(NewEvent(eDesc))
// ProviderRegistered event
eDesc = NewEventDescriptor(ProviderRegisteredEvent,
manifest.NewParameter("providerID", smartcontract.IntegerType),
manifest.NewParameter("address", smartcontract.Hash160Type),
manifest.NewParameter("specialty", smartcontract.StringType))
s.AddEvent(NewEvent(eDesc))
// ProviderSuspended event
eDesc = NewEventDescriptor(ProviderSuspendedEvent,
manifest.NewParameter("providerID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
s.AddEvent(NewEvent(eDesc))
// AuthorizationGranted event
eDesc = NewEventDescriptor(AuthorizationGrantedEvent,
manifest.NewParameter("authID", smartcontract.IntegerType),
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("provider", smartcontract.Hash160Type))
s.AddEvent(NewEvent(eDesc))
// AuthorizationRevoked event
eDesc = NewEventDescriptor(AuthorizationRevokedEvent,
manifest.NewParameter("authID", smartcontract.IntegerType))
s.AddEvent(NewEvent(eDesc))
// EmergencyAccessGranted event
eDesc = NewEventDescriptor(EmergencyAccessGrantedEvent,
manifest.NewParameter("emergencyID", smartcontract.IntegerType),
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("provider", smartcontract.Hash160Type))
s.AddEvent(NewEvent(eDesc))
// EmergencyAccessReviewed event
eDesc = NewEventDescriptor(EmergencyAccessReviewedEvent,
manifest.NewParameter("emergencyID", smartcontract.IntegerType))
s.AddEvent(NewEvent(eDesc))
return s
}
// Metadata returns contract metadata.
func (s *Salus) Metadata() *interop.ContractMD {
return &s.ContractMD
}
// Initialize initializes the Salus contract.
func (s *Salus) 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.setRecordCounter(ic.DAO, 0)
s.setProviderCounter(ic.DAO, 0)
s.setAuthCounter(ic.DAO, 0)
s.setEmergencyCounter(ic.DAO, 0)
// Initialize config with defaults
cfg := &state.SalusConfig{
DefaultAnnualCredits: 10000, // 10000 healthcare credits per year
EmergencyAccessDuration: 86400, // ~24 hours (1-second blocks)
PreventiveCareBonus: 500, // Bonus for preventive care visits
MaxAuthorizationDuration: 2592000, // ~30 days (1-second blocks)
}
s.setConfig(ic.DAO, cfg)
// Initialize cache
cache := &SalusCache{
accountCount: 0,
recordCount: 0,
providerCount: 0,
authorizationCount: 0,
emergencyCount: 0,
}
ic.DAO.SetCache(s.ID, cache)
return nil
}
// InitializeCache initializes the cache from storage.
func (s *Salus) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
cache := &SalusCache{
accountCount: s.getAccountCounter(d),
recordCount: s.getRecordCounter(d),
providerCount: s.getProviderCounter(d),
authorizationCount: s.getAuthCounter(d),
emergencyCount: s.getEmergencyCounter(d),
}
d.SetCache(s.ID, cache)
return nil
}
// OnPersist is called before block is committed.
func (s *Salus) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist is called after block is committed.
func (s *Salus) PostPersist(ic *interop.Context) error {
return nil
}
// ActiveIn returns the hardfork at which this contract is activated.
func (s *Salus) ActiveIn() *config.Hardfork {
return nil // Always active
}
// ===== Storage Helpers =====
func (s *Salus) makeAccountKey(vitaID uint64) []byte {
key := make([]byte, 9)
key[0] = salusPrefixAccount
binary.BigEndian.PutUint64(key[1:], vitaID)
return key
}
func (s *Salus) makeAccountByOwnerKey(owner util.Uint160) []byte {
key := make([]byte, 21)
key[0] = salusPrefixAccountByOwner
copy(key[1:], owner.BytesBE())
return key
}
func (s *Salus) makeRecordKey(recordID uint64) []byte {
key := make([]byte, 9)
key[0] = salusPrefixRecord
binary.BigEndian.PutUint64(key[1:], recordID)
return key
}
func (s *Salus) makeRecordByPatientKey(vitaID, recordID uint64) []byte {
key := make([]byte, 17)
key[0] = salusPrefixRecordByPatient
binary.BigEndian.PutUint64(key[1:9], vitaID)
binary.BigEndian.PutUint64(key[9:], recordID)
return key
}
func (s *Salus) makeProviderKey(providerID uint64) []byte {
key := make([]byte, 9)
key[0] = salusPrefixProvider
binary.BigEndian.PutUint64(key[1:], providerID)
return key
}
func (s *Salus) makeProviderByAddressKey(address util.Uint160) []byte {
key := make([]byte, 21)
key[0] = salusPrefixProviderByAddress
copy(key[1:], address.BytesBE())
return key
}
func (s *Salus) makeAuthorizationKey(authID uint64) []byte {
key := make([]byte, 9)
key[0] = salusPrefixAuthorization
binary.BigEndian.PutUint64(key[1:], authID)
return key
}
func (s *Salus) makeActiveAuthKey(vitaID uint64, provider util.Uint160) []byte {
key := make([]byte, 29)
key[0] = salusPrefixActiveAuth
binary.BigEndian.PutUint64(key[1:9], vitaID)
copy(key[9:], provider.BytesBE())
return key
}
func (s *Salus) makeEmergencyKey(emergencyID uint64) []byte {
key := make([]byte, 9)
key[0] = salusPrefixEmergencyAccess
binary.BigEndian.PutUint64(key[1:], emergencyID)
return key
}
// Counter getters/setters
func (s *Salus) getAccountCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(s.ID, []byte{salusPrefixAccountCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (s *Salus) setAccountCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(s.ID, []byte{salusPrefixAccountCounter}, buf)
}
func (s *Salus) getRecordCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(s.ID, []byte{salusPrefixRecordCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (s *Salus) setRecordCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(s.ID, []byte{salusPrefixRecordCounter}, buf)
}
func (s *Salus) getProviderCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(s.ID, []byte{salusPrefixProviderCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (s *Salus) setProviderCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(s.ID, []byte{salusPrefixProviderCounter}, buf)
}
func (s *Salus) getAuthCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(s.ID, []byte{salusPrefixAuthCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (s *Salus) setAuthCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(s.ID, []byte{salusPrefixAuthCounter}, buf)
}
func (s *Salus) getEmergencyCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(s.ID, []byte{salusPrefixEmergencyCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (s *Salus) setEmergencyCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(s.ID, []byte{salusPrefixEmergencyCounter}, buf)
}
// Config getter/setter
func (s *Salus) getConfigInternal(d *dao.Simple) *state.SalusConfig {
si := d.GetStorageItem(s.ID, []byte{salusPrefixConfig})
if si == nil {
return &state.SalusConfig{
DefaultAnnualCredits: 10000,
EmergencyAccessDuration: 86400,
PreventiveCareBonus: 500,
MaxAuthorizationDuration: 2592000,
}
}
cfg := new(state.SalusConfig)
item, _ := stackitem.Deserialize(si)
cfg.FromStackItem(item)
return cfg
}
func (s *Salus) setConfig(d *dao.Simple, cfg *state.SalusConfig) {
item, _ := cfg.ToStackItem()
data, _ := stackitem.Serialize(item)
d.PutStorageItem(s.ID, []byte{salusPrefixConfig}, data)
}
// Account storage
func (s *Salus) getAccountInternal(d *dao.Simple, vitaID uint64) *state.HealthcareAccount {
si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID))
if si == nil {
return nil
}
acc := new(state.HealthcareAccount)
item, _ := stackitem.Deserialize(si)
acc.FromStackItem(item)
return acc
}
func (s *Salus) putAccount(d *dao.Simple, acc *state.HealthcareAccount) {
item, _ := acc.ToStackItem()
data, _ := stackitem.Serialize(item)
d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data)
}
func (s *Salus) 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 *Salus) 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)
}
// Record storage
func (s *Salus) getRecordInternal(d *dao.Simple, recordID uint64) *state.MedicalRecord {
si := d.GetStorageItem(s.ID, s.makeRecordKey(recordID))
if si == nil {
return nil
}
record := new(state.MedicalRecord)
item, _ := stackitem.Deserialize(si)
record.FromStackItem(item)
return record
}
func (s *Salus) putRecord(d *dao.Simple, record *state.MedicalRecord) {
item, _ := record.ToStackItem()
data, _ := stackitem.Serialize(item)
d.PutStorageItem(s.ID, s.makeRecordKey(record.ID), data)
}
func (s *Salus) setRecordByPatient(d *dao.Simple, vitaID, recordID uint64) {
d.PutStorageItem(s.ID, s.makeRecordByPatientKey(vitaID, recordID), []byte{1})
}
// Provider storage
func (s *Salus) getProviderInternal(d *dao.Simple, providerID uint64) *state.HealthcareProvider {
si := d.GetStorageItem(s.ID, s.makeProviderKey(providerID))
if si == nil {
return nil
}
provider := new(state.HealthcareProvider)
item, _ := stackitem.Deserialize(si)
provider.FromStackItem(item)
return provider
}
func (s *Salus) putProvider(d *dao.Simple, provider *state.HealthcareProvider) {
item, _ := provider.ToStackItem()
data, _ := stackitem.Serialize(item)
d.PutStorageItem(s.ID, s.makeProviderKey(provider.ProviderID), data)
}
func (s *Salus) getProviderIDByAddress(d *dao.Simple, address util.Uint160) (uint64, bool) {
si := d.GetStorageItem(s.ID, s.makeProviderByAddressKey(address))
if si == nil {
return 0, false
}
return binary.BigEndian.Uint64(si), true
}
func (s *Salus) setProviderByAddress(d *dao.Simple, address util.Uint160, providerID uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, providerID)
d.PutStorageItem(s.ID, s.makeProviderByAddressKey(address), buf)
}
// Authorization storage
func (s *Salus) getAuthInternal(d *dao.Simple, authID uint64) *state.ProviderAuthorization {
si := d.GetStorageItem(s.ID, s.makeAuthorizationKey(authID))
if si == nil {
return nil
}
auth := new(state.ProviderAuthorization)
item, _ := stackitem.Deserialize(si)
auth.FromStackItem(item)
return auth
}
func (s *Salus) putAuth(d *dao.Simple, auth *state.ProviderAuthorization) {
item, _ := auth.ToStackItem()
data, _ := stackitem.Serialize(item)
d.PutStorageItem(s.ID, s.makeAuthorizationKey(auth.ID), data)
}
func (s *Salus) getActiveAuthID(d *dao.Simple, vitaID uint64, provider util.Uint160) (uint64, bool) {
si := d.GetStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider))
if si == nil {
return 0, false
}
return binary.BigEndian.Uint64(si), true
}
func (s *Salus) setActiveAuthID(d *dao.Simple, vitaID uint64, provider util.Uint160, authID uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, authID)
d.PutStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider), buf)
}
func (s *Salus) clearActiveAuth(d *dao.Simple, vitaID uint64, provider util.Uint160) {
d.DeleteStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider))
}
// Emergency access storage
func (s *Salus) getEmergencyInternal(d *dao.Simple, emergencyID uint64) *state.EmergencyAccess {
si := d.GetStorageItem(s.ID, s.makeEmergencyKey(emergencyID))
if si == nil {
return nil
}
emergency := new(state.EmergencyAccess)
item, _ := stackitem.Deserialize(si)
emergency.FromStackItem(item)
return emergency
}
func (s *Salus) putEmergency(d *dao.Simple, emergency *state.EmergencyAccess) {
item, _ := emergency.ToStackItem()
data, _ := stackitem.Serialize(item)
d.PutStorageItem(s.ID, s.makeEmergencyKey(emergency.ID), data)
}
// ===== Contract Methods =====
// activateHealthcare activates healthcare account for a Vita holder.
func (s *Salus) activateHealthcare(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
// Check owner has active Vita
if s.Vita == nil {
panic(ErrSalusNoVita)
}
vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
if err != nil || vita == nil {
panic(ErrSalusNoVita)
}
if vita.Status != state.TokenStatusActive {
panic(ErrSalusNoVita)
}
// Check if account already exists
existing := s.getAccountInternal(ic.DAO, vita.TokenID)
if existing != nil {
panic(ErrSalusAccountExists)
}
// Check healthcare rights
if !s.checkHealthcareRight(ic, owner) {
// Log but allow (EnforcementLogging)
}
// Get cache and increment counter
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
cache.accountCount++
s.setAccountCounter(ic.DAO, cache.accountCount)
// Get default credits from config
cfg := s.getConfigInternal(ic.DAO)
// Create account
acc := &state.HealthcareAccount{
VitaID: vita.TokenID,
Owner: owner,
AnnualAllocation: cfg.DefaultAnnualCredits,
CreditsUsed: 0,
CreditsAvailable: cfg.DefaultAnnualCredits,
BiologicalAge: 0,
LastCheckup: 0,
Status: state.HealthcareAccountActive,
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, HealthcareActivatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))),
stackitem.NewByteArray(owner.BytesBE()),
}))
return stackitem.NewBool(true)
}
// getAccount returns healthcare account by owner.
func (s *Salus) 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 healthcare account by Vita ID.
func (s *Salus) 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 healthcare credits to an account (committee only).
func (s *Salus) 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(ErrSalusNotCommittee)
}
if amount == 0 {
panic(ErrSalusInvalidCredits)
}
// Get or create account
vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
if !found {
// Auto-create account if Vita exists
if s.Vita == nil {
panic(ErrSalusNoVita)
}
vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
if err != nil || vita == nil || vita.Status != state.TokenStatusActive {
panic(ErrSalusNoVita)
}
vitaID = vita.TokenID
cfg := s.getConfigInternal(ic.DAO)
// Create account
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
cache.accountCount++
s.setAccountCounter(ic.DAO, cache.accountCount)
acc := &state.HealthcareAccount{
VitaID: vitaID,
Owner: owner,
AnnualAllocation: cfg.DefaultAnnualCredits,
CreditsUsed: 0,
CreditsAvailable: amount,
BiologicalAge: 0,
LastCheckup: 0,
Status: state.HealthcareAccountActive,
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, HealthcareActivatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
stackitem.NewByteArray(owner.BytesBE()),
}))
ic.AddNotification(s.Hash, CreditsAllocatedEventSalus, 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(ErrSalusAccountNotFound)
}
if acc.Status != state.HealthcareAccountActive {
panic(ErrSalusAccountSuspended)
}
// Add credits
acc.CreditsAvailable += amount
acc.UpdatedAt = ic.Block.Index
s.putAccount(ic.DAO, acc)
// Emit event
ic.AddNotification(s.Hash, CreditsAllocatedEventSalus, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
stackitem.NewBigInteger(big.NewInt(int64(amount))),
stackitem.NewBigInteger(big.NewInt(int64(acc.CreditsAvailable))),
}))
return stackitem.NewBool(true)
}
// getCredits returns available credits for an owner.
func (s *Salus) 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.CreditsAvailable)))
}
// recordMedicalEvent records a medical event (provider only).
func (s *Salus) recordMedicalEvent(ic *interop.Context, args []stackitem.Item) stackitem.Item {
patient := toUint160(args[0])
recordType := state.MedicalRecordType(toUint64(args[1]))
contentHashBytes := toBytes(args[2])
credits := toUint64(args[3])
// Convert bytes to Uint256
var contentHash util.Uint256
if len(contentHashBytes) == 32 {
copy(contentHash[:], contentHashBytes)
}
// Check provider authority
if !s.checkHealthcareProvider(ic) {
panic(ErrSalusNotProvider)
}
// Get provider address
provider := ic.VM.GetCallingScriptHash()
// Get patient's Vita
if s.Vita == nil {
panic(ErrSalusNoVita)
}
vita, err := s.Vita.GetTokenByOwner(ic.DAO, patient)
if err != nil || vita == nil || vita.Status != state.TokenStatusActive {
panic(ErrSalusNoVita)
}
// Check provider has access (unless emergency or authorized)
vitaID, found := s.getVitaIDByOwner(ic.DAO, patient)
if found {
authID, hasAuth := s.getActiveAuthID(ic.DAO, vitaID, provider)
if hasAuth {
auth := s.getAuthInternal(ic.DAO, authID)
if auth == nil || !auth.IsValid(ic.Block.Index) {
panic(ErrSalusNoAccess)
}
}
// If no auth, still allow (emergency can be logged separately)
}
// Get or auto-create account
if !found {
// Auto-create
cfg := s.getConfigInternal(ic.DAO)
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
cache.accountCount++
s.setAccountCounter(ic.DAO, cache.accountCount)
acc := &state.HealthcareAccount{
VitaID: vita.TokenID,
Owner: patient,
AnnualAllocation: cfg.DefaultAnnualCredits,
CreditsUsed: 0,
CreditsAvailable: cfg.DefaultAnnualCredits,
BiologicalAge: 0,
LastCheckup: 0,
Status: state.HealthcareAccountActive,
CreatedAt: ic.Block.Index,
UpdatedAt: ic.Block.Index,
}
s.putAccount(ic.DAO, acc)
s.setOwnerToVitaID(ic.DAO, patient, vita.TokenID)
vitaID = vita.TokenID
}
acc := s.getAccountInternal(ic.DAO, vitaID)
if acc == nil {
panic(ErrSalusAccountNotFound)
}
if acc.Status != state.HealthcareAccountActive {
panic(ErrSalusAccountSuspended)
}
// Check sufficient credits
if acc.CreditsAvailable < credits {
panic(ErrSalusInsufficientCredits)
}
// Get next record ID
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
recordID := cache.recordCount
cache.recordCount++
s.setRecordCounter(ic.DAO, cache.recordCount)
// Deduct credits
acc.CreditsUsed += credits
acc.CreditsAvailable -= credits
acc.UpdatedAt = ic.Block.Index
// Update last checkup if appropriate
if recordType == state.RecordTypeCheckup || recordType == state.RecordTypePreventive {
acc.LastCheckup = ic.Block.Index
}
s.putAccount(ic.DAO, acc)
// Create record
record := &state.MedicalRecord{
ID: recordID,
VitaID: vitaID,
Patient: patient,
Provider: provider,
RecordType: recordType,
ContentHash: contentHash,
CreditsUsed: credits,
CreatedAt: ic.Block.Index,
IsActive: true,
}
s.putRecord(ic.DAO, record)
s.setRecordByPatient(ic.DAO, vitaID, recordID)
// Emit event
ic.AddNotification(s.Hash, MedicalRecordCreatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(recordID))),
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
stackitem.NewBigInteger(big.NewInt(int64(recordType))),
}))
return stackitem.NewBigInteger(big.NewInt(int64(recordID)))
}
// getMedicalRecord returns medical record by ID.
func (s *Salus) getMedicalRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
recordID := toUint64(args[0])
record := s.getRecordInternal(ic.DAO, recordID)
if record == nil {
return stackitem.Null{}
}
item, _ := record.ToStackItem()
return item
}
// registerProvider registers a healthcare provider (committee only).
func (s *Salus) registerProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
address := toUint160(args[0])
name := toString(args[1])
specialty := toString(args[2])
licenseHashBytes := toBytes(args[3])
// Convert bytes to Uint256
var licenseHash util.Uint256
if len(licenseHashBytes) == 32 {
copy(licenseHash[:], licenseHashBytes)
}
// Committee only
if !s.checkCommittee(ic) {
panic(ErrSalusNotCommittee)
}
// Validate inputs
if len(name) == 0 || len(name) > 128 {
panic(ErrSalusInvalidName)
}
if len(specialty) == 0 || len(specialty) > 64 {
panic(ErrSalusInvalidSpecialty)
}
// Check if provider already exists
_, exists := s.getProviderIDByAddress(ic.DAO, address)
if exists {
panic(ErrSalusProviderExists)
}
// Get next provider ID
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
providerID := cache.providerCount
cache.providerCount++
s.setProviderCounter(ic.DAO, cache.providerCount)
// Create provider
provider := &state.HealthcareProvider{
Address: address,
Name: name,
ProviderID: providerID,
Specialty: specialty,
LicenseHash: licenseHash,
Status: state.ProviderStatusActive,
RegisteredAt: ic.Block.Index,
UpdatedAt: ic.Block.Index,
}
s.putProvider(ic.DAO, provider)
s.setProviderByAddress(ic.DAO, address, providerID)
// Emit event
ic.AddNotification(s.Hash, ProviderRegisteredEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(providerID))),
stackitem.NewByteArray(address.BytesBE()),
stackitem.NewByteArray([]byte(specialty)),
}))
return stackitem.NewBigInteger(big.NewInt(int64(providerID)))
}
// suspendProvider suspends a healthcare provider (committee only).
func (s *Salus) suspendProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
providerID := toUint64(args[0])
reason := toString(args[1])
// Committee only
if !s.checkCommittee(ic) {
panic(ErrSalusNotCommittee)
}
provider := s.getProviderInternal(ic.DAO, providerID)
if provider == nil {
panic(ErrSalusProviderNotFound)
}
if provider.Status == state.ProviderStatusRevoked {
panic(ErrSalusProviderRevoked)
}
// Suspend
provider.Status = state.ProviderStatusSuspended
provider.UpdatedAt = ic.Block.Index
s.putProvider(ic.DAO, provider)
// Emit event
ic.AddNotification(s.Hash, ProviderSuspendedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(providerID))),
stackitem.NewByteArray([]byte(reason)),
}))
return stackitem.NewBool(true)
}
// getProvider returns provider details.
func (s *Salus) getProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
providerID := toUint64(args[0])
provider := s.getProviderInternal(ic.DAO, providerID)
if provider == nil {
return stackitem.Null{}
}
item, _ := provider.ToStackItem()
return item
}
// getProviderByAddress returns provider by address.
func (s *Salus) getProviderByAddress(ic *interop.Context, args []stackitem.Item) stackitem.Item {
address := toUint160(args[0])
providerID, found := s.getProviderIDByAddress(ic.DAO, address)
if !found {
return stackitem.Null{}
}
provider := s.getProviderInternal(ic.DAO, providerID)
if provider == nil {
return stackitem.Null{}
}
item, _ := provider.ToStackItem()
return item
}
// authorizeAccess grants provider access to patient records.
func (s *Salus) authorizeAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item {
patient := toUint160(args[0])
provider := toUint160(args[1])
accessLevel := state.AccessLevel(toUint64(args[2]))
duration := toUint32(args[3])
// Get patient's Vita and account
vitaID, found := s.getVitaIDByOwner(ic.DAO, patient)
if !found {
panic(ErrSalusAccountNotFound)
}
acc := s.getAccountInternal(ic.DAO, vitaID)
if acc == nil {
panic(ErrSalusAccountNotFound)
}
if acc.Status != state.HealthcareAccountActive {
panic(ErrSalusAccountSuspended)
}
// Check caller is patient (self-authorization)
caller := ic.VM.GetCallingScriptHash()
if caller != patient && !s.checkCommittee(ic) {
panic(ErrSalusNotPatient)
}
// Check max duration
cfg := s.getConfigInternal(ic.DAO)
if duration > cfg.MaxAuthorizationDuration {
panic(ErrSalusExceedsMaxDuration)
}
// Check if authorization already exists
existingAuthID, exists := s.getActiveAuthID(ic.DAO, vitaID, provider)
if exists {
existingAuth := s.getAuthInternal(ic.DAO, existingAuthID)
if existingAuth != nil && existingAuth.IsValid(ic.Block.Index) {
panic(ErrSalusAuthorizationExists)
}
}
// Get next auth ID
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
authID := cache.authorizationCount
cache.authorizationCount++
s.setAuthCounter(ic.DAO, cache.authorizationCount)
// Calculate expiry
expiresAt := uint32(0)
if duration > 0 {
expiresAt = ic.Block.Index + duration
}
// Create authorization
auth := &state.ProviderAuthorization{
ID: authID,
VitaID: vitaID,
Patient: patient,
Provider: provider,
AccessLevel: accessLevel,
StartsAt: ic.Block.Index,
ExpiresAt: expiresAt,
IsActive: true,
GrantedAt: ic.Block.Index,
}
s.putAuth(ic.DAO, auth)
s.setActiveAuthID(ic.DAO, vitaID, provider, authID)
// Emit event
ic.AddNotification(s.Hash, AuthorizationGrantedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(authID))),
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
stackitem.NewByteArray(provider.BytesBE()),
}))
return stackitem.NewBigInteger(big.NewInt(int64(authID)))
}
// revokeAccess revokes provider access.
func (s *Salus) revokeAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item {
authID := toUint64(args[0])
auth := s.getAuthInternal(ic.DAO, authID)
if auth == nil {
panic(ErrSalusAuthorizationNotFound)
}
// Check caller is patient or committee
caller := ic.VM.GetCallingScriptHash()
if caller != auth.Patient && !s.checkCommittee(ic) {
panic(ErrSalusNotPatient)
}
// Revoke
auth.IsActive = false
s.putAuth(ic.DAO, auth)
s.clearActiveAuth(ic.DAO, auth.VitaID, auth.Provider)
// Emit event
ic.AddNotification(s.Hash, AuthorizationRevokedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(authID))),
}))
return stackitem.NewBool(true)
}
// getAuthorization returns authorization details.
func (s *Salus) getAuthorization(ic *interop.Context, args []stackitem.Item) stackitem.Item {
authID := toUint64(args[0])
auth := s.getAuthInternal(ic.DAO, authID)
if auth == nil {
return stackitem.Null{}
}
item, _ := auth.ToStackItem()
return item
}
// hasAccess checks if provider has access to patient.
func (s *Salus) hasAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item {
patient := toUint160(args[0])
provider := toUint160(args[1])
vitaID, found := s.getVitaIDByOwner(ic.DAO, patient)
if !found {
return stackitem.NewBool(false)
}
authID, exists := s.getActiveAuthID(ic.DAO, vitaID, provider)
if !exists {
return stackitem.NewBool(false)
}
auth := s.getAuthInternal(ic.DAO, authID)
if auth == nil {
return stackitem.NewBool(false)
}
return stackitem.NewBool(auth.IsValid(ic.Block.Index))
}
// emergencyAccess requests emergency access to patient records.
func (s *Salus) emergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item {
patient := toUint160(args[0])
reason := toString(args[1])
// Check provider authority
if !s.checkHealthcareProvider(ic) {
panic(ErrSalusNotProvider)
}
if len(reason) == 0 || len(reason) > 256 {
panic(ErrSalusInvalidReason)
}
// Get provider address
provider := ic.VM.GetCallingScriptHash()
// Get patient's Vita
if s.Vita == nil {
panic(ErrSalusNoVita)
}
vita, err := s.Vita.GetTokenByOwner(ic.DAO, patient)
if err != nil || vita == nil || vita.Status != state.TokenStatusActive {
panic(ErrSalusNoVita)
}
// Get config for emergency duration
cfg := s.getConfigInternal(ic.DAO)
// Get next emergency ID
cache := ic.DAO.GetRWCache(s.ID).(*SalusCache)
emergencyID := cache.emergencyCount
cache.emergencyCount++
s.setEmergencyCounter(ic.DAO, cache.emergencyCount)
// Create emergency access
emergency := &state.EmergencyAccess{
ID: emergencyID,
VitaID: vita.TokenID,
Patient: patient,
Provider: provider,
Reason: reason,
GrantedAt: ic.Block.Index,
ExpiresAt: ic.Block.Index + cfg.EmergencyAccessDuration,
WasReviewed: false,
}
s.putEmergency(ic.DAO, emergency)
// Emit event
ic.AddNotification(s.Hash, EmergencyAccessGrantedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(emergencyID))),
stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))),
stackitem.NewByteArray(provider.BytesBE()),
}))
return stackitem.NewBigInteger(big.NewInt(int64(emergencyID)))
}
// reviewEmergencyAccess marks emergency access as reviewed (committee only).
func (s *Salus) reviewEmergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item {
emergencyID := toUint64(args[0])
// Committee only
if !s.checkCommittee(ic) {
panic(ErrSalusNotCommittee)
}
emergency := s.getEmergencyInternal(ic.DAO, emergencyID)
if emergency == nil {
panic(ErrSalusEmergencyNotFound)
}
// Mark as reviewed
emergency.WasReviewed = true
s.putEmergency(ic.DAO, emergency)
// Emit event
ic.AddNotification(s.Hash, EmergencyAccessReviewedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(emergencyID))),
}))
return stackitem.NewBool(true)
}
// getEmergencyAccess returns emergency access details.
func (s *Salus) getEmergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item {
emergencyID := toUint64(args[0])
emergency := s.getEmergencyInternal(ic.DAO, emergencyID)
if emergency == nil {
return stackitem.Null{}
}
item, _ := emergency.ToStackItem()
return item
}
// getConfig returns the Salus configuration.
func (s *Salus) 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 healthcare accounts.
func (s *Salus) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item {
cache := ic.DAO.GetROCache(s.ID).(*SalusCache)
return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount)))
}
// getTotalRecords returns the total number of medical records.
func (s *Salus) getTotalRecords(ic *interop.Context, args []stackitem.Item) stackitem.Item {
cache := ic.DAO.GetROCache(s.ID).(*SalusCache)
return stackitem.NewBigInteger(big.NewInt(int64(cache.recordCount)))
}
// getTotalProviders returns the total number of healthcare providers.
func (s *Salus) getTotalProviders(ic *interop.Context, args []stackitem.Item) stackitem.Item {
cache := ic.DAO.GetROCache(s.ID).(*SalusCache)
return stackitem.NewBigInteger(big.NewInt(int64(cache.providerCount)))
}
// ===== Public Interface Methods for Cross-Contract Access =====
// GetAccountByOwner returns a healthcare account by owner address.
func (s *Salus) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.HealthcareAccount, error) {
vitaID, found := s.getVitaIDByOwner(d, owner)
if !found {
return nil, ErrSalusAccountNotFound
}
acc := s.getAccountInternal(d, vitaID)
if acc == nil {
return nil, ErrSalusAccountNotFound
}
return acc, nil
}
// HasValidAuthorization checks if provider has valid authorization for patient.
func (s *Salus) HasValidAuthorization(d *dao.Simple, patient util.Uint160, provider util.Uint160, blockHeight uint32) bool {
vitaID, found := s.getVitaIDByOwner(d, patient)
if !found {
return false
}
authID, exists := s.getActiveAuthID(d, vitaID, provider)
if !exists {
return false
}
auth := s.getAuthInternal(d, authID)
if auth == nil {
return false
}
return auth.IsValid(blockHeight)
}
// Address returns the contract's script hash.
func (s *Salus) Address() util.Uint160 {
return s.Hash
}