1517 lines
47 KiB
Go
1517 lines
47 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"
|
|
)
|
|
|
|
// Salus represents the universal healthcare native contract.
|
|
type Salus struct {
|
|
interop.ContractMD
|
|
Annos IAnnos
|
|
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.Annos.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
|
|
}
|