Complete Collocatio native contract for PIO/EIO/CIO investments

Implement comprehensive investment infrastructure with three tiers:
- PIO (Public Investment Opportunity): Universal citizen access
- EIO (Employee Investment Opportunity): Workplace democracy
- CIO (Contractor Investment Opportunity): Gig economy empowerment

New Features:

Violation System:
- recordViolation: Record violations with evidence and penalties
- resolveViolation: Resolve violations with committee authority
- getViolation: Query violation records
- Auto-ban after MaxViolationsBeforeBan threshold

Employment Verification (EIO eligibility):
- verifyEmployment: Register employee-employer relationships
- revokeEmployment: End employment verification
- getEmploymentStatus: Query employment records
- EIO eligibility requires active employment

Contractor Verification (CIO eligibility):
- verifyContractor: Register contractor-platform relationships
- revokeContractor: End contractor verification
- getContractorStatus: Query contractor records
- CIO eligibility requires active contractor status

Returns Distribution:
- distributeReturns: Proportional returns to all investors
- cancelOpportunity: Refund investors on cancellation

Education Integration:
- completeInvestmentEducation: Mark investor education complete
- Scire certification tracking for eligibility

Query Methods:
- getInvestmentsByOpportunity: List investments per opportunity
- getInvestmentsByInvestor: List investments per investor
- getOpportunitiesByType: Filter by PIO/EIO/CIO
- getOpportunitiesByStatus: Filter by lifecycle status

Lifecycle Automation (PostPersist):
- Auto-close opportunities past investment deadline
- Auto-fail opportunities below minimum participants
- Runs every 100 blocks for performance

State Serialization:
- Add ToStackItem/FromStackItem for EmploymentVerification
- Add ToStackItem/FromStackItem for ContractorVerification
- Add ToStackItem/FromStackItem for InvestmentViolation

Tests:
- 14 test cases for config, counters, queries, error handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Tutus Development 2025-12-21 04:18:23 +00:00
parent c07696a18c
commit 5678ae0e46
4 changed files with 1715 additions and 15 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,192 @@
package native_test
import (
"testing"
"github.com/stretchr/testify/require"
"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/neotest"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
func newCollocatioClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.Collocatio)
}
// TestCollocatio_GetConfig tests the getConfig method.
func TestCollocatio_GetConfig(t *testing.T) {
c := newCollocatioClient(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), 13) // CollocatioConfig has 13 fields
}, "getConfig")
}
// TestCollocatio_GetOpportunityCount tests the getOpportunityCount method.
func TestCollocatio_GetOpportunityCount(t *testing.T) {
c := newCollocatioClient(t)
// Initially should be 0
c.Invoke(t, 0, "getOpportunityCount")
}
// TestCollocatio_GetInvestmentCount tests the getInvestmentCount method.
func TestCollocatio_GetInvestmentCount(t *testing.T) {
c := newCollocatioClient(t)
// Initially should be 0
c.Invoke(t, 0, "getInvestmentCount")
}
// TestCollocatio_GetOpportunity_NonExistent tests getting a non-existent opportunity.
func TestCollocatio_GetOpportunity_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Non-existent opportunity 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 opportunity")
}, "getOpportunity", 999)
}
// TestCollocatio_GetInvestment_NonExistent tests getting a non-existent investment.
func TestCollocatio_GetInvestment_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Non-existent investment 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 investment")
}, "getInvestment", 999)
}
// TestCollocatio_GetEligibility_NonExistent tests getting eligibility for non-existent investor.
func TestCollocatio_GetEligibility_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
e := c.Executor
acc := e.NewAccount(t)
// Non-existent eligibility 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 eligibility")
}, "getEligibility", acc.ScriptHash())
}
// TestCollocatio_IsEligible_NoVita tests eligibility check without Vita.
func TestCollocatio_IsEligible_NoVita(t *testing.T) {
c := newCollocatioClient(t)
e := c.Executor
acc := e.NewAccount(t)
// Should return false for all opportunity types without Vita
c.Invoke(t, false, "isEligible", acc.ScriptHash(), int64(state.OpportunityPIO))
c.Invoke(t, false, "isEligible", acc.ScriptHash(), int64(state.OpportunityEIO))
c.Invoke(t, false, "isEligible", acc.ScriptHash(), int64(state.OpportunityCIO))
}
// TestCollocatio_GetViolation_NonExistent tests getting a non-existent violation.
func TestCollocatio_GetViolation_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Non-existent violation 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 violation")
}, "getViolation", 999)
}
// TestCollocatio_GetInvestmentsByOpportunity_Empty tests getting investments for non-existent opportunity.
func TestCollocatio_GetInvestmentsByOpportunity_Empty(t *testing.T) {
c := newCollocatioClient(t)
// Should return empty array
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.Equal(t, 0, len(arr), "expected empty array")
}, "getInvestmentsByOpportunity", 999)
}
// TestCollocatio_GetOpportunitiesByType_Empty tests getting opportunities by type when none exist.
func TestCollocatio_GetOpportunitiesByType_Empty(t *testing.T) {
c := newCollocatioClient(t)
// Should return empty array for each type
for _, oppType := range []state.OpportunityType{state.OpportunityPIO, state.OpportunityEIO, state.OpportunityCIO} {
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.Equal(t, 0, len(arr), "expected empty array")
}, "getOpportunitiesByType", int64(oppType))
}
}
// TestCollocatio_GetOpportunitiesByStatus_Empty tests getting opportunities by status when none exist.
func TestCollocatio_GetOpportunitiesByStatus_Empty(t *testing.T) {
c := newCollocatioClient(t)
// Should return empty array for each status
for _, status := range []state.OpportunityStatus{
state.OpportunityDraft,
state.OpportunityVoting,
state.OpportunityActive,
state.OpportunityClosed,
state.OpportunityCompleted,
state.OpportunityCancelled,
state.OpportunityFailed,
} {
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.Equal(t, 0, len(arr), "expected empty array")
}, "getOpportunitiesByStatus", int64(status))
}
}
// TestCollocatio_ActivateOpportunity_NonExistent tests activating non-existent opportunity.
func TestCollocatio_ActivateOpportunity_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Should fail - opportunity doesn't exist
c.InvokeFail(t, "opportunity not found", "activateOpportunity", 999)
}
// TestCollocatio_DistributeReturns_NonExistent tests distributing returns for non-existent opportunity.
func TestCollocatio_DistributeReturns_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Should fail - opportunity doesn't exist
c.InvokeFail(t, "opportunity not found", "distributeReturns", 999, 1000_00000000)
}
// TestCollocatio_CancelOpportunity_NonExistent tests canceling non-existent opportunity.
func TestCollocatio_CancelOpportunity_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Should fail - opportunity doesn't exist
c.InvokeFail(t, "opportunity not found", "cancelOpportunity", 999)
}
// TestCollocatio_ResolveViolation_NonExistent tests resolving non-existent violation.
func TestCollocatio_ResolveViolation_NonExistent(t *testing.T) {
c := newCollocatioClient(t)
// Should fail - violation doesn't exist
c.InvokeFail(t, "violation not found", "resolveViolation", 999, "Resolved after investigation")
}
// Note: Tests that require a Vita token (like createOpportunity, invest, verifyEmployment, etc.)
// are more complex because they require a deployed helper contract since the native contracts
// use GetCallingScriptHash() for authorization, which returns the transaction script hash for
// direct calls rather than the signer's account. These would require cross-contract integration tests.

File diff suppressed because one or more lines are too long

View File

@ -664,6 +664,104 @@ func (ev *EmploymentVerification) EncodeBinary(bw *io.BinWriter) {
bw.WriteBytes(ev.VerifiedBy[:]) bw.WriteBytes(ev.VerifiedBy[:])
} }
// ToStackItem implements stackitem.Convertible interface.
func (ev *EmploymentVerification) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(ev.VitaID))),
stackitem.NewByteArray(ev.Employee.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(ev.EmployerVitaID))),
stackitem.NewByteArray(ev.Employer.BytesBE()),
stackitem.NewByteArray([]byte(ev.Position)),
stackitem.NewBigInteger(big.NewInt(int64(ev.StartDate))),
stackitem.NewBigInteger(big.NewInt(int64(ev.EndDate))),
stackitem.NewBool(ev.IsActive),
stackitem.NewBigInteger(big.NewInt(int64(ev.VerifiedAt))),
stackitem.NewByteArray(ev.VerifiedBy.BytesBE()),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (ev *EmploymentVerification) 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)
}
ev.VitaID = vitaID.Uint64()
employee, err := items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid employee: %w", err)
}
ev.Employee, err = util.Uint160DecodeBytesBE(employee)
if err != nil {
return fmt.Errorf("invalid employee address: %w", err)
}
employerVitaID, err := items[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid employerVitaID: %w", err)
}
ev.EmployerVitaID = employerVitaID.Uint64()
employer, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid employer: %w", err)
}
ev.Employer, err = util.Uint160DecodeBytesBE(employer)
if err != nil {
return fmt.Errorf("invalid employer address: %w", err)
}
position, err := items[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid position: %w", err)
}
ev.Position = string(position)
startDate, err := items[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid startDate: %w", err)
}
ev.StartDate = uint32(startDate.Uint64())
endDate, err := items[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid endDate: %w", err)
}
ev.EndDate = uint32(endDate.Uint64())
isActive, err := items[7].TryBool()
if err != nil {
return fmt.Errorf("invalid isActive: %w", err)
}
ev.IsActive = isActive
verifiedAt, err := items[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid verifiedAt: %w", err)
}
ev.VerifiedAt = uint32(verifiedAt.Uint64())
verifiedBy, err := items[9].TryBytes()
if err != nil {
return fmt.Errorf("invalid verifiedBy: %w", err)
}
ev.VerifiedBy, err = util.Uint160DecodeBytesBE(verifiedBy)
if err != nil {
return fmt.Errorf("invalid verifiedBy address: %w", err)
}
return nil
}
// ContractorVerification represents verified contractor status for CIO eligibility. // ContractorVerification represents verified contractor status for CIO eligibility.
type ContractorVerification struct { type ContractorVerification struct {
VitaID uint64 // Contractor's Vita ID VitaID uint64 // Contractor's Vita ID
@ -706,6 +804,104 @@ func (cv *ContractorVerification) EncodeBinary(bw *io.BinWriter) {
bw.WriteBytes(cv.VerifiedBy[:]) bw.WriteBytes(cv.VerifiedBy[:])
} }
// ToStackItem implements stackitem.Convertible interface.
func (cv *ContractorVerification) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(cv.VitaID))),
stackitem.NewByteArray(cv.Contractor.BytesBE()),
stackitem.NewByteArray([]byte(cv.PlatformID)),
stackitem.NewByteArray(cv.Platform.BytesBE()),
stackitem.NewByteArray([]byte(cv.ContractorID)),
stackitem.NewBigInteger(big.NewInt(int64(cv.StartDate))),
stackitem.NewBigInteger(big.NewInt(int64(cv.EndDate))),
stackitem.NewBool(cv.IsActive),
stackitem.NewBigInteger(big.NewInt(int64(cv.VerifiedAt))),
stackitem.NewByteArray(cv.VerifiedBy.BytesBE()),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (cv *ContractorVerification) 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)
}
cv.VitaID = vitaID.Uint64()
contractor, err := items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid contractor: %w", err)
}
cv.Contractor, err = util.Uint160DecodeBytesBE(contractor)
if err != nil {
return fmt.Errorf("invalid contractor address: %w", err)
}
platformID, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid platformID: %w", err)
}
cv.PlatformID = string(platformID)
platform, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid platform: %w", err)
}
cv.Platform, err = util.Uint160DecodeBytesBE(platform)
if err != nil {
return fmt.Errorf("invalid platform address: %w", err)
}
contractorID, err := items[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid contractorID: %w", err)
}
cv.ContractorID = string(contractorID)
startDate, err := items[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid startDate: %w", err)
}
cv.StartDate = uint32(startDate.Uint64())
endDate, err := items[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid endDate: %w", err)
}
cv.EndDate = uint32(endDate.Uint64())
isActive, err := items[7].TryBool()
if err != nil {
return fmt.Errorf("invalid isActive: %w", err)
}
cv.IsActive = isActive
verifiedAt, err := items[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid verifiedAt: %w", err)
}
cv.VerifiedAt = uint32(verifiedAt.Uint64())
verifiedBy, err := items[9].TryBytes()
if err != nil {
return fmt.Errorf("invalid verifiedBy: %w", err)
}
cv.VerifiedBy, err = util.Uint160DecodeBytesBE(verifiedBy)
if err != nil {
return fmt.Errorf("invalid verifiedBy address: %w", err)
}
return nil
}
// InvestmentViolation represents a recorded investment abuse violation. // InvestmentViolation represents a recorded investment abuse violation.
type InvestmentViolation struct { type InvestmentViolation struct {
ID uint64 // Unique violation ID ID uint64 // Unique violation ID
@ -754,6 +950,118 @@ func (v *InvestmentViolation) EncodeBinary(bw *io.BinWriter) {
bw.WriteString(v.Resolution) bw.WriteString(v.Resolution)
} }
// ToStackItem implements stackitem.Convertible interface.
func (v *InvestmentViolation) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(v.ID))),
stackitem.NewBigInteger(big.NewInt(int64(v.VitaID))),
stackitem.NewByteArray(v.Violator.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(v.OpportunityID))),
stackitem.NewByteArray([]byte(v.ViolationType)),
stackitem.NewByteArray([]byte(v.Description)),
stackitem.NewByteArray(v.EvidenceHash.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(v.Penalty))),
stackitem.NewByteArray(v.ReportedBy.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(v.ReportedAt))),
stackitem.NewBigInteger(big.NewInt(int64(v.ResolvedAt))),
stackitem.NewByteArray([]byte(v.Resolution)),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (v *InvestmentViolation) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 12 {
return fmt.Errorf("wrong number of elements: expected 12, got %d", len(items))
}
id, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid id: %w", err)
}
v.ID = id.Uint64()
vitaID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid vitaID: %w", err)
}
v.VitaID = vitaID.Uint64()
violator, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid violator: %w", err)
}
v.Violator, err = util.Uint160DecodeBytesBE(violator)
if err != nil {
return fmt.Errorf("invalid violator address: %w", err)
}
oppID, err := items[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid opportunityID: %w", err)
}
v.OpportunityID = oppID.Uint64()
violationType, err := items[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid violationType: %w", err)
}
v.ViolationType = string(violationType)
description, err := items[5].TryBytes()
if err != nil {
return fmt.Errorf("invalid description: %w", err)
}
v.Description = string(description)
evidenceHash, err := items[6].TryBytes()
if err != nil {
return fmt.Errorf("invalid evidenceHash: %w", err)
}
v.EvidenceHash, err = util.Uint256DecodeBytesBE(evidenceHash)
if err != nil {
return fmt.Errorf("invalid evidenceHash: %w", err)
}
penalty, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid penalty: %w", err)
}
v.Penalty = penalty.Uint64()
reportedBy, err := items[8].TryBytes()
if err != nil {
return fmt.Errorf("invalid reportedBy: %w", err)
}
v.ReportedBy, err = util.Uint160DecodeBytesBE(reportedBy)
if err != nil {
return fmt.Errorf("invalid reportedBy address: %w", err)
}
reportedAt, err := items[9].TryInteger()
if err != nil {
return fmt.Errorf("invalid reportedAt: %w", err)
}
v.ReportedAt = uint32(reportedAt.Uint64())
resolvedAt, err := items[10].TryInteger()
if err != nil {
return fmt.Errorf("invalid resolvedAt: %w", err)
}
v.ResolvedAt = uint32(resolvedAt.Uint64())
resolution, err := items[11].TryBytes()
if err != nil {
return fmt.Errorf("invalid resolution: %w", err)
}
v.Resolution = string(resolution)
return nil
}
// CollocatioConfig represents configurable parameters for the investment contract. // CollocatioConfig represents configurable parameters for the investment contract.
type CollocatioConfig struct { type CollocatioConfig struct {
// Minimum requirements // Minimum requirements