diff --git a/pkg/core/native/native_test/salus_test.go b/pkg/core/native/native_test/salus_test.go new file mode 100644 index 0000000..7bba75e --- /dev/null +++ b/pkg/core/native/native_test/salus_test.go @@ -0,0 +1,254 @@ +package native_test + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/stretchr/testify/require" + "github.com/tutus-one/tutus-chain/pkg/core/native/nativenames" + "github.com/tutus-one/tutus-chain/pkg/neotest" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +func newSalusClient(t *testing.T) *neotest.ContractInvoker { + return newNativeClient(t, nativenames.Salus) +} + +// TestSalus_GetConfig tests the getConfig method. +func TestSalus_GetConfig(t *testing.T) { + c := newSalusClient(t) + + // Get default config + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + arr, ok := stack[0].Value().([]stackitem.Item) + require.True(t, ok, "expected array result") + require.GreaterOrEqual(t, len(arr), 4) // SalusConfig has 4 fields + }, "getConfig") +} + +// TestSalus_GetTotalAccounts tests the getTotalAccounts method. +func TestSalus_GetTotalAccounts(t *testing.T) { + c := newSalusClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalAccounts") +} + +// TestSalus_GetTotalRecords tests the getTotalRecords method. +func TestSalus_GetTotalRecords(t *testing.T) { + c := newSalusClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalRecords") +} + +// TestSalus_GetTotalProviders tests the getTotalProviders method. +func TestSalus_GetTotalProviders(t *testing.T) { + c := newSalusClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalProviders") +} + +// TestSalus_GetAccount_NonExistent tests getting a non-existent account. +func TestSalus_GetAccount_NonExistent(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent account") + }, "getAccount", acc.ScriptHash()) +} + +// TestSalus_GetCredits_NonExistent tests getting credits for non-existent account. +func TestSalus_GetCredits_NonExistent(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return 0 credits + c.Invoke(t, 0, "getCredits", acc.ScriptHash()) +} + +// TestSalus_ActivateHealthcare_NoVita tests that activating healthcare without Vita fails. +func TestSalus_ActivateHealthcare_NoVita(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - no Vita registered + invoker.InvokeFail(t, "owner must have an active Vita", "activateHealthcare", acc.ScriptHash()) +} + +// TestSalus_AllocateCredits_NotCommittee tests that non-committee cannot allocate credits. +func TestSalus_AllocateCredits_NotCommittee(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - not committee + invoker.InvokeFail(t, "invalid committee signature", "allocateCredits", + acc.ScriptHash(), 100, "test allocation") +} + +// TestSalus_RegisterProvider_NotCommittee tests that non-committee cannot register providers. +func TestSalus_RegisterProvider_NotCommittee(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + licenseHash := make([]byte, 32) // 32-byte hash + + // Should fail - not committee + invoker.InvokeFail(t, "invalid committee signature", "registerProvider", + acc.ScriptHash(), "Test Hospital", "General", licenseHash) +} + +// TestSalus_GetProvider_NonExistent tests getting a non-existent provider. +func TestSalus_GetProvider_NonExistent(t *testing.T) { + c := newSalusClient(t) + + // Non-existent provider should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent provider") + }, "getProvider", int64(999)) +} + +// TestSalus_GetAuthorization_NonExistent tests getting a non-existent authorization. +func TestSalus_GetAuthorization_NonExistent(t *testing.T) { + c := newSalusClient(t) + + // Non-existent authorization should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent authorization") + }, "getAuthorization", int64(999)) +} + +// TestSalus_HasAccess_NoAccount tests hasAccess for non-existent account. +func TestSalus_HasAccess_NoAccount(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + patient := e.NewAccount(t) + provider := e.NewAccount(t) + + // No account = no access + c.Invoke(t, false, "hasAccess", patient.ScriptHash(), provider.ScriptHash()) +} + +// TestSalus_GetEmergencyAccess_NonExistent tests getting non-existent emergency access. +func TestSalus_GetEmergencyAccess_NonExistent(t *testing.T) { + c := newSalusClient(t) + + // Non-existent emergency access should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent emergency access") + }, "getEmergencyAccess", int64(999)) +} + +// TestSalus_ActivateHealthcareWithVita tests healthcare activation with a valid Vita. +func TestSalus_ActivateHealthcareWithVita(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + // Register Vita first + vitaHash := e.NativeHash(t, nativenames.Vita) + acc := e.NewAccount(t) + vitaInvoker := e.NewInvoker(vitaHash, acc) + + owner := acc.ScriptHash() + personHash := hash.Sha256(owner.BytesBE()).BytesBE() + isEntity := false + recoveryHash := hash.Sha256([]byte("recovery")).BytesBE() + + // Register Vita token + vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + _, ok := stack[0].Value().([]byte) + require.True(t, ok, "expected ByteArray result") + }, "register", owner.BytesBE(), personHash, isEntity, recoveryHash) + + // Now activate Salus account - need to pass owner as BytesBE for Hash160 type + salusInvoker := c.WithSigners(acc) + salusInvoker.Invoke(t, true, "activateHealthcare", owner.BytesBE()) + + // Verify account exists - also pass as BytesBE + salusInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + arr, ok := stack[0].Value().([]stackitem.Item) + require.True(t, ok, "expected array result for existing account") + require.GreaterOrEqual(t, len(arr), 10) // HealthcareAccount has 10 fields + }, "getAccount", owner.BytesBE()) + + // Verify total accounts increased + c.Invoke(t, 1, "getTotalAccounts") +} + +// TestSalus_AllocateCredits tests credit allocation by committee. +func TestSalus_AllocateCredits(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + // Register Vita first + vitaHash := e.NativeHash(t, nativenames.Vita) + acc := e.NewAccount(t) + vitaInvoker := e.NewInvoker(vitaHash, acc) + + owner := acc.ScriptHash() + personHash := hash.Sha256(owner.BytesBE()).BytesBE() + isEntity := false + recoveryHash := hash.Sha256([]byte("recovery")).BytesBE() + + // Register returns ByteArray (tokenID), not Null + vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + _, ok := stack[0].Value().([]byte) + require.True(t, ok, "expected ByteArray result") + }, "register", owner.BytesBE(), personHash, isEntity, recoveryHash) + + // Activate Salus account - use BytesBE for Hash160 + salusInvoker := c.WithSigners(acc) + salusInvoker.Invoke(t, true, "activateHealthcare", owner.BytesBE()) + + // Allocate credits as committee - use BytesBE for Hash160 + committeeInvoker := c.WithSigners(c.Committee) + committeeInvoker.Invoke(t, true, "allocateCredits", owner.BytesBE(), int64(500), "annual allocation") + + // Default annual allocation is 10000, so after adding 500 it should be 10500 + c.Invoke(t, 10500, "getCredits", owner.BytesBE()) +} + +// TestSalus_RegisterProvider tests provider registration by committee. +func TestSalus_RegisterProvider(t *testing.T) { + c := newSalusClient(t) + e := c.Executor + + provider := e.NewAccount(t) + licenseHash := make([]byte, 32) + copy(licenseHash, []byte("license")) + + // Register provider as committee + committeeInvoker := c.WithSigners(c.Committee) + committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + // First provider gets ID 0 + }, "registerProvider", provider.ScriptHash(), "Test Hospital", "General", licenseHash) + + // Verify total providers increased + c.Invoke(t, 1, "getTotalProviders") +} diff --git a/pkg/core/native/salus.go b/pkg/core/native/salus.go new file mode 100644 index 0000000..d5f704b --- /dev/null +++ b/pkg/core/native/salus.go @@ -0,0 +1,1516 @@ +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 + NEO INEO + 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.NEO.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 +} diff --git a/pkg/core/state/salus.go b/pkg/core/state/salus.go new file mode 100644 index 0000000..48ea16a --- /dev/null +++ b/pkg/core/state/salus.go @@ -0,0 +1,632 @@ +package state + +import ( + "errors" + "fmt" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +// HealthcareAccountStatus represents the status of a healthcare account. +type HealthcareAccountStatus uint8 + +const ( + // HealthcareAccountActive indicates an active account. + HealthcareAccountActive HealthcareAccountStatus = 0 + // HealthcareAccountSuspended indicates a temporarily suspended account. + HealthcareAccountSuspended HealthcareAccountStatus = 1 + // HealthcareAccountClosed indicates a permanently closed account. + HealthcareAccountClosed HealthcareAccountStatus = 2 +) + +// MedicalRecordType represents the type of medical record. +type MedicalRecordType uint8 + +const ( + // RecordTypeCheckup indicates a routine checkup. + RecordTypeCheckup MedicalRecordType = 0 + // RecordTypeTreatment indicates a treatment. + RecordTypeTreatment MedicalRecordType = 1 + // RecordTypeEmergency indicates an emergency visit. + RecordTypeEmergency MedicalRecordType = 2 + // RecordTypePrescription indicates a prescription. + RecordTypePrescription MedicalRecordType = 3 + // RecordTypeLabResult indicates lab results. + RecordTypeLabResult MedicalRecordType = 4 + // RecordTypeVaccination indicates a vaccination. + RecordTypeVaccination MedicalRecordType = 5 + // RecordTypeMentalHealth indicates mental health services. + RecordTypeMentalHealth MedicalRecordType = 6 + // RecordTypePreventive indicates preventive care. + RecordTypePreventive MedicalRecordType = 7 +) + +// AccessLevel represents the level of access granted to a provider. +type AccessLevel uint8 + +const ( + // AccessLevelNone indicates no access. + AccessLevelNone AccessLevel = 0 + // AccessLevelEmergency indicates emergency-only access. + AccessLevelEmergency AccessLevel = 1 + // AccessLevelLimited indicates limited access (specific record types). + AccessLevelLimited AccessLevel = 2 + // AccessLevelFull indicates full access to all records. + AccessLevelFull AccessLevel = 3 +) + +// ProviderStatus represents the status of a healthcare provider. +type ProviderStatus uint8 + +const ( + // ProviderStatusActive indicates an active provider. + ProviderStatusActive ProviderStatus = 0 + // ProviderStatusSuspended indicates a suspended provider. + ProviderStatusSuspended ProviderStatus = 1 + // ProviderStatusRevoked indicates a revoked provider. + ProviderStatusRevoked ProviderStatus = 2 +) + +// HealthcareAccount represents a citizen's healthcare account. +type HealthcareAccount struct { + VitaID uint64 // Owner's Vita token ID + Owner util.Uint160 // Owner's address + AnnualAllocation uint64 // Annual healthcare credits + CreditsUsed uint64 // Credits used this year + CreditsAvailable uint64 // Available healthcare credits + BiologicalAge uint32 // Biological age (Salus-adjusted) + LastCheckup uint32 // Block height of last checkup + Status HealthcareAccountStatus // Account status + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Block height of last update +} + +// ToStackItem implements stackitem.Convertible interface. +func (a *HealthcareAccount) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(a.VitaID))), + stackitem.NewByteArray(a.Owner.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(a.AnnualAllocation))), + stackitem.NewBigInteger(big.NewInt(int64(a.CreditsUsed))), + stackitem.NewBigInteger(big.NewInt(int64(a.CreditsAvailable))), + stackitem.NewBigInteger(big.NewInt(int64(a.BiologicalAge))), + stackitem.NewBigInteger(big.NewInt(int64(a.LastCheckup))), + stackitem.NewBigInteger(big.NewInt(int64(a.Status))), + stackitem.NewBigInteger(big.NewInt(int64(a.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(a.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (a *HealthcareAccount) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 10 { + return fmt.Errorf("wrong number of elements: expected 10, got %d", len(items)) + } + + vitaID, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + a.VitaID = vitaID.Uint64() + + owner, err := items[1].TryBytes() + if err != nil { + return fmt.Errorf("invalid owner: %w", err) + } + a.Owner, err = util.Uint160DecodeBytesBE(owner) + if err != nil { + return fmt.Errorf("invalid owner address: %w", err) + } + + annualAllocation, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid annualAllocation: %w", err) + } + a.AnnualAllocation = annualAllocation.Uint64() + + creditsUsed, err := items[3].TryInteger() + if err != nil { + return fmt.Errorf("invalid creditsUsed: %w", err) + } + a.CreditsUsed = creditsUsed.Uint64() + + creditsAvailable, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid creditsAvailable: %w", err) + } + a.CreditsAvailable = creditsAvailable.Uint64() + + biologicalAge, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid biologicalAge: %w", err) + } + a.BiologicalAge = uint32(biologicalAge.Uint64()) + + lastCheckup, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid lastCheckup: %w", err) + } + a.LastCheckup = uint32(lastCheckup.Uint64()) + + status, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid status: %w", err) + } + a.Status = HealthcareAccountStatus(status.Uint64()) + + createdAt, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + a.CreatedAt = uint32(createdAt.Uint64()) + + updatedAt, err := items[9].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + a.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// MedicalRecord represents a medical record reference (data stored off-chain). +type MedicalRecord struct { + ID uint64 // Unique record ID + VitaID uint64 // Patient's Vita ID + Patient util.Uint160 // Patient's address + Provider util.Uint160 // Healthcare provider's address + RecordType MedicalRecordType // Type of medical record + ContentHash util.Uint256 // Hash of encrypted off-chain data + CreditsUsed uint64 // Healthcare credits used + CreatedAt uint32 // Block height when created + IsActive bool // Whether record is valid +} + +// ToStackItem implements stackitem.Convertible interface. +func (r *MedicalRecord) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(r.ID))), + stackitem.NewBigInteger(big.NewInt(int64(r.VitaID))), + stackitem.NewByteArray(r.Patient.BytesBE()), + stackitem.NewByteArray(r.Provider.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(r.RecordType))), + stackitem.NewByteArray(r.ContentHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(r.CreditsUsed))), + stackitem.NewBigInteger(big.NewInt(int64(r.CreatedAt))), + stackitem.NewBool(r.IsActive), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (r *MedicalRecord) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 9 { + return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + r.ID = id.Uint64() + + vitaID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + r.VitaID = vitaID.Uint64() + + patient, err := items[2].TryBytes() + if err != nil { + return fmt.Errorf("invalid patient: %w", err) + } + r.Patient, err = util.Uint160DecodeBytesBE(patient) + if err != nil { + return fmt.Errorf("invalid patient address: %w", err) + } + + provider, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid provider: %w", err) + } + r.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + recordType, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid recordType: %w", err) + } + r.RecordType = MedicalRecordType(recordType.Uint64()) + + contentHash, err := items[5].TryBytes() + if err != nil { + return fmt.Errorf("invalid contentHash: %w", err) + } + r.ContentHash, err = util.Uint256DecodeBytesBE(contentHash) + if err != nil { + return fmt.Errorf("invalid contentHash value: %w", err) + } + + creditsUsed, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid creditsUsed: %w", err) + } + r.CreditsUsed = creditsUsed.Uint64() + + createdAt, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + r.CreatedAt = uint32(createdAt.Uint64()) + + isActive, err := items[8].TryBool() + if err != nil { + return fmt.Errorf("invalid isActive: %w", err) + } + r.IsActive = isActive + + return nil +} + +// ProviderAuthorization represents a healthcare provider's access authorization. +type ProviderAuthorization struct { + ID uint64 // Authorization ID + VitaID uint64 // Patient's Vita ID + Patient util.Uint160 // Patient's address + Provider util.Uint160 // Healthcare provider's address + AccessLevel AccessLevel // Level of access granted + StartsAt uint32 // Block height when access starts + ExpiresAt uint32 // Block height when access expires (0 = no expiry) + IsActive bool // Whether authorization is currently active + GrantedAt uint32 // Block height when granted +} + +// ToStackItem implements stackitem.Convertible interface. +func (p *ProviderAuthorization) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(p.ID))), + stackitem.NewBigInteger(big.NewInt(int64(p.VitaID))), + stackitem.NewByteArray(p.Patient.BytesBE()), + stackitem.NewByteArray(p.Provider.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(p.AccessLevel))), + stackitem.NewBigInteger(big.NewInt(int64(p.StartsAt))), + stackitem.NewBigInteger(big.NewInt(int64(p.ExpiresAt))), + stackitem.NewBool(p.IsActive), + stackitem.NewBigInteger(big.NewInt(int64(p.GrantedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (p *ProviderAuthorization) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 9 { + return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + p.ID = id.Uint64() + + vitaID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + p.VitaID = vitaID.Uint64() + + patient, err := items[2].TryBytes() + if err != nil { + return fmt.Errorf("invalid patient: %w", err) + } + p.Patient, err = util.Uint160DecodeBytesBE(patient) + if err != nil { + return fmt.Errorf("invalid patient address: %w", err) + } + + provider, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid provider: %w", err) + } + p.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + accessLevel, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid accessLevel: %w", err) + } + p.AccessLevel = AccessLevel(accessLevel.Uint64()) + + startsAt, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid startsAt: %w", err) + } + p.StartsAt = uint32(startsAt.Uint64()) + + expiresAt, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid expiresAt: %w", err) + } + p.ExpiresAt = uint32(expiresAt.Uint64()) + + isActive, err := items[7].TryBool() + if err != nil { + return fmt.Errorf("invalid isActive: %w", err) + } + p.IsActive = isActive + + grantedAt, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid grantedAt: %w", err) + } + p.GrantedAt = uint32(grantedAt.Uint64()) + + return nil +} + +// IsExpired checks if the authorization has expired. +func (p *ProviderAuthorization) IsExpired(currentBlock uint32) bool { + return p.ExpiresAt != 0 && p.ExpiresAt <= currentBlock +} + +// IsValid checks if the authorization is currently valid. +func (p *ProviderAuthorization) IsValid(currentBlock uint32) bool { + return p.IsActive && currentBlock >= p.StartsAt && !p.IsExpired(currentBlock) +} + +// HealthcareProvider represents a registered healthcare provider. +type HealthcareProvider struct { + Address util.Uint160 // Provider's address + Name string // Provider name + ProviderID uint64 // Unique provider ID + Specialty string // Medical specialty + LicenseHash util.Uint256 // Hash of license documentation + Status ProviderStatus // Provider status + RegisteredAt uint32 // Block height when registered + UpdatedAt uint32 // Block height of last update +} + +// ToStackItem implements stackitem.Convertible interface. +func (p *HealthcareProvider) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(p.Address.BytesBE()), + stackitem.NewByteArray([]byte(p.Name)), + stackitem.NewBigInteger(big.NewInt(int64(p.ProviderID))), + stackitem.NewByteArray([]byte(p.Specialty)), + stackitem.NewByteArray(p.LicenseHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(p.Status))), + stackitem.NewBigInteger(big.NewInt(int64(p.RegisteredAt))), + stackitem.NewBigInteger(big.NewInt(int64(p.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (p *HealthcareProvider) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 8 { + return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items)) + } + + address, err := items[0].TryBytes() + if err != nil { + return fmt.Errorf("invalid address: %w", err) + } + p.Address, err = util.Uint160DecodeBytesBE(address) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + name, err := items[1].TryBytes() + if err != nil { + return fmt.Errorf("invalid name: %w", err) + } + p.Name = string(name) + + providerID, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid providerID: %w", err) + } + p.ProviderID = providerID.Uint64() + + specialty, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid specialty: %w", err) + } + p.Specialty = string(specialty) + + licenseHash, err := items[4].TryBytes() + if err != nil { + return fmt.Errorf("invalid licenseHash: %w", err) + } + p.LicenseHash, err = util.Uint256DecodeBytesBE(licenseHash) + if err != nil { + return fmt.Errorf("invalid licenseHash value: %w", err) + } + + status, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid status: %w", err) + } + p.Status = ProviderStatus(status.Uint64()) + + registeredAt, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid registeredAt: %w", err) + } + p.RegisteredAt = uint32(registeredAt.Uint64()) + + updatedAt, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + p.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// EmergencyAccess represents an emergency access grant. +type EmergencyAccess struct { + ID uint64 // Emergency access ID + VitaID uint64 // Patient's Vita ID + Patient util.Uint160 // Patient's address + Provider util.Uint160 // Provider who accessed + Reason string // Emergency reason + GrantedAt uint32 // Block height when granted + ExpiresAt uint32 // Block height when expires + WasReviewed bool // Whether access was reviewed +} + +// ToStackItem implements stackitem.Convertible interface. +func (e *EmergencyAccess) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(e.ID))), + stackitem.NewBigInteger(big.NewInt(int64(e.VitaID))), + stackitem.NewByteArray(e.Patient.BytesBE()), + stackitem.NewByteArray(e.Provider.BytesBE()), + stackitem.NewByteArray([]byte(e.Reason)), + stackitem.NewBigInteger(big.NewInt(int64(e.GrantedAt))), + stackitem.NewBigInteger(big.NewInt(int64(e.ExpiresAt))), + stackitem.NewBool(e.WasReviewed), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (e *EmergencyAccess) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 8 { + return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + e.ID = id.Uint64() + + vitaID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + e.VitaID = vitaID.Uint64() + + patient, err := items[2].TryBytes() + if err != nil { + return fmt.Errorf("invalid patient: %w", err) + } + e.Patient, err = util.Uint160DecodeBytesBE(patient) + if err != nil { + return fmt.Errorf("invalid patient address: %w", err) + } + + provider, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid provider: %w", err) + } + e.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + reason, err := items[4].TryBytes() + if err != nil { + return fmt.Errorf("invalid reason: %w", err) + } + e.Reason = string(reason) + + grantedAt, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid grantedAt: %w", err) + } + e.GrantedAt = uint32(grantedAt.Uint64()) + + expiresAt, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid expiresAt: %w", err) + } + e.ExpiresAt = uint32(expiresAt.Uint64()) + + wasReviewed, err := items[7].TryBool() + if err != nil { + return fmt.Errorf("invalid wasReviewed: %w", err) + } + e.WasReviewed = wasReviewed + + return nil +} + +// SalusConfig represents configurable parameters for the Salus contract. +type SalusConfig struct { + DefaultAnnualCredits uint64 // Default annual healthcare credits + EmergencyAccessDuration uint32 // Blocks for emergency access (default ~24 hours) + PreventiveCareBonus uint64 // Bonus credits for preventive care + MaxAuthorizationDuration uint32 // Maximum authorization duration in blocks +} + +// ToStackItem implements stackitem.Convertible interface. +func (c *SalusConfig) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(c.DefaultAnnualCredits))), + stackitem.NewBigInteger(big.NewInt(int64(c.EmergencyAccessDuration))), + stackitem.NewBigInteger(big.NewInt(int64(c.PreventiveCareBonus))), + stackitem.NewBigInteger(big.NewInt(int64(c.MaxAuthorizationDuration))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (c *SalusConfig) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 4 { + return fmt.Errorf("wrong number of elements: expected 4, got %d", len(items)) + } + + defaultCredits, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid defaultAnnualCredits: %w", err) + } + c.DefaultAnnualCredits = defaultCredits.Uint64() + + emergencyDuration, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid emergencyAccessDuration: %w", err) + } + c.EmergencyAccessDuration = uint32(emergencyDuration.Uint64()) + + preventiveBonus, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid preventiveCareBonus: %w", err) + } + c.PreventiveCareBonus = preventiveBonus.Uint64() + + maxAuthDuration, err := items[3].TryInteger() + if err != nil { + return fmt.Errorf("invalid maxAuthorizationDuration: %w", err) + } + c.MaxAuthorizationDuration = uint32(maxAuthDuration.Uint64()) + + return nil +}