Add PersonToken native contract for soul-bound identity tokens

Implements PersonToken as a native contract following NeoGo patterns:

Core Token Lifecycle (Phase 1):
- register: Create new PersonToken with owner, personHash, isEntity, recoveryHash
- getToken/getTokenByID: Retrieve token by owner address or sequential ID
- exists/totalSupply: Check token existence and get total count
- suspend/reinstate: Committee-controlled token status management

Attribute Management (Phase 2):
- setAttribute/getAttribute: Manage identity attributes with attestation
- revokeAttribute/verifyAttribute: Attribute lifecycle management

Passwordless Authentication (Phase 3):
- createChallenge/fulfillChallenge/verifyAuth: Challenge-response auth flow

Key Recovery (Phase 4):
- initiateRecovery/approveRecovery/executeRecovery/cancelRecovery
- Multi-approval recovery with configurable delay

Cross-Contract Integration (Phase 5):
- validateCaller: Verify calling contract has valid PersonToken
- requireRole: Check caller has specific role assignment
- requireCoreRole: Check caller has core role (User/Verified/Committee/Attestor/Recovery)
- requirePermission: Check caller has resource/action/scope permission

Files added:
- pkg/core/native/person_token.go: Main contract implementation
- pkg/core/state/person_token.go: State structs with serialization
- pkg/core/native/native_test/person_token_test.go: Test coverage

🤖 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-19 19:04:38 +00:00
parent cf481e4137
commit 99ba041a85
7 changed files with 2811 additions and 1 deletions

View File

@ -110,6 +110,16 @@ type (
ExpirationOf(dao *dao.Simple, acc util.Uint160) uint32
GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32
}
// IPersonToken is an interface required from native PersonToken contract
// for interaction with Blockchain and other native contracts.
IPersonToken interface {
interop.Contract
GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.PersonToken, error)
GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.PersonToken, error)
TokenExists(d *dao.Simple, owner util.Uint160) bool
GetAttribute(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error)
}
)
// Contracts is a convenient wrapper around an arbitrary set of native contracts
@ -225,6 +235,12 @@ func (cs *Contracts) Notary() INotary {
return nil
}
// PersonToken returns native IPersonToken contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) PersonToken() IPersonToken {
return cs.ByName(nativenames.PersonToken).(IPersonToken)
}
// NewDefaultContracts returns a new set of default native contracts.
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
mgmt := NewManagement()
@ -261,6 +277,9 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
treasury := newTreasury()
treasury.NEO = neo
personToken := newPersonToken()
personToken.NEO = neo
return []interop.Contract{
mgmt,
s,
@ -273,5 +292,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
oracle,
notary,
treasury,
personToken,
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,290 @@
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/core/state"
"github.com/tutus-one/tutus-chain/pkg/neotest"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
func newPersonTokenClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.PersonToken)
}
// registerPersonToken is a helper to register a PersonToken for a signer.
// Returns the tokenID bytes.
func registerPersonToken(t *testing.T, c *neotest.ContractInvoker, signer neotest.Signer) []byte {
owner := signer.ScriptHash()
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
isEntity := false
recoveryHash := hash.Sha256([]byte("recovery")).BytesBE()
invoker := c.WithSigners(signer)
// Register returns tokenID bytes, not null
txHash := invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
// Result is a ByteArray (tokenID)
_, ok := stack[0].Value().([]byte)
require.True(t, ok, "expected ByteArray result")
}, "register", owner.BytesBE(), personHash, isEntity, recoveryHash)
aer := c.Executor.GetTxExecResult(t, txHash)
require.Equal(t, 1, len(aer.Stack))
tokenIDBytes := aer.Stack[0].Value().([]byte)
return tokenIDBytes
}
// TestPersonToken_Register tests basic registration functionality.
func TestPersonToken_Register(t *testing.T) {
c := newPersonTokenClient(t)
e := c.Executor
acc := e.NewAccount(t)
owner := acc.ScriptHash()
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
isEntity := false
recoveryHash := hash.Sha256([]byte("recovery")).BytesBE()
invoker := c.WithSigners(acc)
// Register token - returns tokenID bytes
txHash := invoker.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)
// Check event was emitted
aer := e.GetTxExecResult(t, txHash)
require.Equal(t, 1, len(aer.Events))
require.Equal(t, "PersonTokenCreated", aer.Events[0].Name)
// Check exists returns true
invoker.Invoke(t, true, "exists", owner.BytesBE())
// Check getToken returns valid token
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr := stack[0].Value().([]stackitem.Item)
require.GreaterOrEqual(t, len(arr), 5) // At least tokenID, owner, personHash, isEntity, status
}, "getToken", owner.BytesBE())
}
// TestPersonToken_ValidateCaller tests the validateCaller method.
// Note: validateCaller uses GetCallingScriptHash() which returns the calling contract's
// script hash, not the transaction signer's address. When called directly from a transaction
// (not from another contract), the caller has no token. These methods are designed for
// cross-contract authorization.
func TestPersonToken_ValidateCaller(t *testing.T) {
c := newPersonTokenClient(t)
t.Run("no token - direct call", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// validateCaller uses GetCallingScriptHash() which returns the transaction script hash
// when called directly, not the signer's account. This will always fail for direct calls.
invoker.InvokeFail(t, "caller does not have a PersonToken", "validateCaller")
})
// Note: Testing validateCaller with a token requires deploying a helper contract
// that has a PersonToken registered to its script hash, then calling validateCaller
// from within that contract. This is the intended usage pattern for cross-contract auth.
}
// TestPersonToken_RequireRole tests the requireRole method.
// Note: requireRole uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestPersonToken_RequireRole(t *testing.T) {
c := newPersonTokenClient(t)
t.Run("no token - direct call", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
invoker.InvokeFail(t, "caller does not have a PersonToken", "requireRole", 0)
})
// Note: Testing requireRole with actual role checks requires a deployed contract
// with a PersonToken registered to its script hash.
}
// TestPersonToken_RequireCoreRole tests the requireCoreRole method.
// Note: requireCoreRole uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestPersonToken_RequireCoreRole(t *testing.T) {
c := newPersonTokenClient(t)
// CoreRole constants
const (
CoreRoleNone = 0
CoreRoleRecovery = 5
)
t.Run("invalid role", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// requireCoreRole with invalid role (> 5) should fail before checking token
invoker.InvokeFail(t, "invalid core role", "requireCoreRole", 10)
})
t.Run("no token - direct call", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
invoker.InvokeFail(t, "caller does not have a PersonToken", "requireCoreRole", CoreRoleNone)
})
// Note: Testing requireCoreRole with actual role checks requires a deployed contract
// with a PersonToken registered to its script hash.
}
// TestPersonToken_RequirePermission tests the requirePermission method.
// Note: requirePermission uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestPersonToken_RequirePermission(t *testing.T) {
c := newPersonTokenClient(t)
t.Run("empty resource", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// requirePermission with empty resource should fail before checking token
invoker.InvokeFail(t, "invalid resource", "requirePermission", "", "read", "global")
})
t.Run("empty action", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// requirePermission with empty action should fail before checking token
invoker.InvokeFail(t, "invalid action", "requirePermission", "documents", "", "global")
})
t.Run("no token - direct call", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
invoker.InvokeFail(t, "caller does not have a PersonToken", "requirePermission", "documents", "read", "global")
})
// Note: Testing requirePermission with actual permission checks requires a deployed contract
// with a PersonToken registered to its script hash.
}
// TestPersonToken_TotalSupply tests the totalSupply method.
func TestPersonToken_TotalSupply(t *testing.T) {
c := newPersonTokenClient(t)
e := c.Executor
// Initially, totalSupply should be 0
c.Invoke(t, 0, "totalSupply")
// Register a token
acc1 := e.NewAccount(t)
registerPersonToken(t, c, acc1)
// Now totalSupply should be 1
c.Invoke(t, 1, "totalSupply")
// Register another token
acc2 := e.NewAccount(t)
registerPersonToken(t, c, acc2)
// Now totalSupply should be 2
c.Invoke(t, 2, "totalSupply")
}
// TestPersonToken_GetTokenByID tests the getTokenByID method.
func TestPersonToken_GetTokenByID(t *testing.T) {
c := newPersonTokenClient(t)
e := c.Executor
// Register a token - the first token gets ID 0 (counter starts at 0)
acc := e.NewAccount(t)
registerPersonToken(t, c, acc)
// Token ID 0 should exist (first registered token)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
// Check that result is an array (not null)
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array result for existing token")
require.GreaterOrEqual(t, len(arr), 9) // PersonToken has 9 fields
// Check owner matches (owner is at index 1)
owner, ok := arr[1].Value().([]byte)
require.True(t, ok, "expected owner to be bytes")
require.Equal(t, acc.ScriptHash().BytesBE(), owner)
}, "getTokenByID", 0)
// Non-existent token should return null
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
// Null returns nil from Value()
require.Nil(t, stack[0].Value(), "expected null for non-existent token")
}, "getTokenByID", 999999)
}
// TestPersonToken_SuspendReinstate tests suspend and reinstate functionality.
func TestPersonToken_SuspendReinstate(t *testing.T) {
c := newPersonTokenClient(t)
e := c.Executor
acc := e.NewAccount(t)
registerPersonToken(t, c, acc)
invoker := c.WithSigners(acc)
committeeInvoker := c.WithSigners(c.Committee)
// Initially token is active
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr := stack[0].Value().([]stackitem.Item)
// Status is at index 6 (after tokenID, owner, personHash, isEntity, createdAt, updatedAt)
status, err := arr[6].TryInteger()
require.NoError(t, err)
require.Equal(t, int64(state.TokenStatusActive), status.Int64())
}, "getToken", acc.ScriptHash().BytesBE())
// Non-committee cannot suspend
invoker.InvokeFail(t, "invalid committee signature", "suspend", acc.ScriptHash().BytesBE(), "test")
// Committee can suspend
committeeInvoker.Invoke(t, true, "suspend", acc.ScriptHash().BytesBE(), "test suspension")
// Token is now suspended
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr := stack[0].Value().([]stackitem.Item)
status, err := arr[6].TryInteger()
require.NoError(t, err)
require.Equal(t, int64(state.TokenStatusSuspended), status.Int64())
}, "getToken", acc.ScriptHash().BytesBE())
// Non-committee cannot reinstate
invoker.InvokeFail(t, "invalid committee signature", "reinstate", acc.ScriptHash().BytesBE())
// Committee can reinstate
committeeInvoker.Invoke(t, true, "reinstate", acc.ScriptHash().BytesBE())
// Token is active again
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr := stack[0].Value().([]stackitem.Item)
status, err := arr[6].TryInteger()
require.NoError(t, err)
require.Equal(t, int64(state.TokenStatusActive), status.Int64())
}, "getToken", acc.ScriptHash().BytesBE())
}
// Note: Full cross-contract testing of validateCaller, requireRole, requireCoreRole, and
// requirePermission would require deploying a helper contract that:
// 1. Has a PersonToken registered to its script hash
// 2. Calls the PersonToken cross-contract methods from within its own methods
// This is the intended usage pattern for these cross-contract authorization methods.

View File

@ -29,4 +29,6 @@ const (
Notary int32 = -10
// Treasury is an ID of native Treasury contract.
Treasury int32 = -11
// PersonToken is an ID of native PersonToken contract.
PersonToken int32 = -12
)

View File

@ -13,6 +13,7 @@ const (
CryptoLib = "CryptoLib"
StdLib = "StdLib"
Treasury = "Treasury"
PersonToken = "PersonToken"
)
// All contains the list of all native contract names ordered by the contract ID.
@ -28,6 +29,7 @@ var All = []string{
Oracle,
Notary,
Treasury,
PersonToken,
}
// IsValid checks if the name is a valid native contract's name.
@ -42,5 +44,6 @@ func IsValid(name string) bool {
name == Notary ||
name == CryptoLib ||
name == StdLib ||
name == Treasury
name == Treasury ||
name == PersonToken
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,472 @@
package state
import (
"errors"
"fmt"
"math"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// TokenStatus represents the status of a PersonToken.
type TokenStatus uint8
const (
// TokenStatusActive indicates an active token.
TokenStatusActive TokenStatus = 0
// TokenStatusSuspended indicates a temporarily suspended token.
TokenStatusSuspended TokenStatus = 1
// TokenStatusRevoked indicates a permanently revoked token.
TokenStatusRevoked TokenStatus = 2
// TokenStatusRecovering indicates recovery is in progress.
TokenStatusRecovering TokenStatus = 3
)
// DisclosureLevel represents the visibility level of an attribute.
type DisclosureLevel uint8
const (
// DisclosurePrivate means only the owner can see the attribute.
DisclosurePrivate DisclosureLevel = 0
// DisclosureVerifier means owner and designated verifiers can see it.
DisclosureVerifier DisclosureLevel = 1
// DisclosureRole means owner and callers with specified roles can see it.
DisclosureRole DisclosureLevel = 2
// DisclosurePublic means anyone can see the attribute.
DisclosurePublic DisclosureLevel = 3
)
// RecoveryStatus represents the status of a recovery request.
type RecoveryStatus uint8
const (
// RecoveryStatusPending indicates recovery is pending approval.
RecoveryStatusPending RecoveryStatus = 0
// RecoveryStatusApproved indicates recovery has been approved.
RecoveryStatusApproved RecoveryStatus = 1
// RecoveryStatusExecuted indicates recovery has been executed.
RecoveryStatusExecuted RecoveryStatus = 2
// RecoveryStatusDenied indicates recovery has been denied.
RecoveryStatusDenied RecoveryStatus = 3
// RecoveryStatusExpired indicates recovery request has expired.
RecoveryStatusExpired RecoveryStatus = 4
)
// PersonToken represents a soul-bound identity token.
type PersonToken struct {
TokenID uint64 // Unique sequential identifier
Owner util.Uint160 // Owner's script hash
PersonHash []byte // Hash of biometric/identity proof
IsEntity bool // True if organization, false if natural person
CreatedAt uint32 // Block height when created
UpdatedAt uint32 // Block height of last modification
Status TokenStatus // Current status
StatusReason string // Reason for status change
RecoveryHash []byte // Hash of recovery mechanism
}
// ToStackItem implements stackitem.Convertible interface.
func (t *PersonToken) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(t.TokenID))),
stackitem.NewByteArray(t.Owner.BytesBE()),
stackitem.NewByteArray(t.PersonHash),
stackitem.NewBool(t.IsEntity),
stackitem.NewBigInteger(big.NewInt(int64(t.CreatedAt))),
stackitem.NewBigInteger(big.NewInt(int64(t.UpdatedAt))),
stackitem.NewBigInteger(big.NewInt(int64(t.Status))),
stackitem.NewByteArray([]byte(t.StatusReason)),
stackitem.NewByteArray(t.RecoveryHash),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (t *PersonToken) 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))
}
tokenID, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid tokenID: %w", err)
}
t.TokenID = tokenID.Uint64()
ownerBytes, err := items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid owner: %w", err)
}
t.Owner, err = util.Uint160DecodeBytesBE(ownerBytes)
if err != nil {
return fmt.Errorf("invalid owner hash: %w", err)
}
t.PersonHash, err = items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid personHash: %w", err)
}
isEntity, err := items[3].TryBool()
if err != nil {
return fmt.Errorf("invalid isEntity: %w", err)
}
t.IsEntity = isEntity
createdAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid createdAt: %w", err)
}
t.CreatedAt = uint32(createdAt.Int64())
updatedAt, err := items[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid updatedAt: %w", err)
}
t.UpdatedAt = uint32(updatedAt.Int64())
status, err := items[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid status: %w", err)
}
t.Status = TokenStatus(status.Int64())
statusReasonBytes, err := items[7].TryBytes()
if err != nil {
return fmt.Errorf("invalid statusReason: %w", err)
}
t.StatusReason = string(statusReasonBytes)
t.RecoveryHash, err = items[8].TryBytes()
if err != nil {
return fmt.Errorf("invalid recoveryHash: %w", err)
}
return nil
}
// Attribute represents an identity attribute with disclosure control.
type Attribute struct {
Key string // Attribute name
ValueHash []byte // Hash of value (privacy)
ValueEnc []byte // Encrypted value (optional)
Attestor util.Uint160 // Who attested this (script hash)
AttestedAt uint32 // Block height when attested
ExpiresAt uint32 // Optional expiration (0 = never)
Revoked bool // Whether attribute has been revoked
RevokedAt uint32 // Block height when revoked
DisclosureLevel DisclosureLevel // Visibility level
}
// ToStackItem implements stackitem.Convertible interface.
func (a *Attribute) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte(a.Key)),
stackitem.NewByteArray(a.ValueHash),
stackitem.NewByteArray(a.ValueEnc),
stackitem.NewByteArray(a.Attestor.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(a.AttestedAt))),
stackitem.NewBigInteger(big.NewInt(int64(a.ExpiresAt))),
stackitem.NewBool(a.Revoked),
stackitem.NewBigInteger(big.NewInt(int64(a.RevokedAt))),
stackitem.NewBigInteger(big.NewInt(int64(a.DisclosureLevel))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (a *Attribute) 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))
}
keyBytes, err := items[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid key: %w", err)
}
a.Key = string(keyBytes)
a.ValueHash, err = items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid valueHash: %w", err)
}
a.ValueEnc, err = items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid valueEnc: %w", err)
}
attestorBytes, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid attestor: %w", err)
}
a.Attestor, err = util.Uint160DecodeBytesBE(attestorBytes)
if err != nil {
return fmt.Errorf("invalid attestor hash: %w", err)
}
attestedAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid attestedAt: %w", err)
}
a.AttestedAt = uint32(attestedAt.Int64())
expiresAt, err := items[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid expiresAt: %w", err)
}
a.ExpiresAt = uint32(expiresAt.Int64())
a.Revoked, err = items[6].TryBool()
if err != nil {
return fmt.Errorf("invalid revoked: %w", err)
}
revokedAt, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid revokedAt: %w", err)
}
a.RevokedAt = uint32(revokedAt.Int64())
disclosureLevel, err := items[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid disclosureLevel: %w", err)
}
a.DisclosureLevel = DisclosureLevel(disclosureLevel.Int64())
return nil
}
// AuthChallenge represents a passwordless authentication challenge.
type AuthChallenge struct {
ChallengeID util.Uint256 // Unique challenge identifier
TokenID uint64 // Associated PersonToken
Nonce []byte // Random bytes to sign
CreatedAt uint32 // Block height when created
ExpiresAt uint32 // Block height when expires
Purpose string // "login", "sign", "recover"
Fulfilled bool // Whether challenge has been fulfilled
FulfilledAt uint32 // Block height when fulfilled
}
// ToStackItem implements stackitem.Convertible interface.
func (c *AuthChallenge) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(c.ChallengeID.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(c.TokenID))),
stackitem.NewByteArray(c.Nonce),
stackitem.NewBigInteger(big.NewInt(int64(c.CreatedAt))),
stackitem.NewBigInteger(big.NewInt(int64(c.ExpiresAt))),
stackitem.NewByteArray([]byte(c.Purpose)),
stackitem.NewBool(c.Fulfilled),
stackitem.NewBigInteger(big.NewInt(int64(c.FulfilledAt))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (c *AuthChallenge) 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))
}
challengeIDBytes, err := items[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid challengeID: %w", err)
}
c.ChallengeID, err = util.Uint256DecodeBytesBE(challengeIDBytes)
if err != nil {
return fmt.Errorf("invalid challengeID hash: %w", err)
}
tokenID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid tokenID: %w", err)
}
c.TokenID = tokenID.Uint64()
c.Nonce, err = items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid nonce: %w", err)
}
createdAt, err := items[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid createdAt: %w", err)
}
c.CreatedAt = uint32(createdAt.Int64())
expiresAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid expiresAt: %w", err)
}
c.ExpiresAt = uint32(expiresAt.Int64())
purposeBytes, err := items[5].TryBytes()
if err != nil {
return fmt.Errorf("invalid purpose: %w", err)
}
c.Purpose = string(purposeBytes)
c.Fulfilled, err = items[6].TryBool()
if err != nil {
return fmt.Errorf("invalid fulfilled: %w", err)
}
fulfilledAt, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid fulfilledAt: %w", err)
}
c.FulfilledAt = uint32(fulfilledAt.Int64())
return nil
}
// RecoveryRequest represents a key recovery request.
type RecoveryRequest struct {
RequestID util.Uint256 // Unique request identifier
TokenID uint64 // Token being recovered
NewOwner util.Uint160 // Proposed new owner
Requester util.Uint160 // Who initiated recovery
Evidence []byte // Encrypted evidence hash
Approvals []util.Uint160 // Approvers who have approved
RequiredApprovals int // Required number of approvals
CreatedAt uint32 // Block height when created
DelayUntil uint32 // Block height when executable
ExpiresAt uint32 // Block height when expires
Status RecoveryStatus // Current status
}
// ToStackItem implements stackitem.Convertible interface.
func (r *RecoveryRequest) ToStackItem() (stackitem.Item, error) {
approvals := make([]stackitem.Item, len(r.Approvals))
for i, a := range r.Approvals {
approvals[i] = stackitem.NewByteArray(a.BytesBE())
}
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(r.RequestID.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(r.TokenID))),
stackitem.NewByteArray(r.NewOwner.BytesBE()),
stackitem.NewByteArray(r.Requester.BytesBE()),
stackitem.NewByteArray(r.Evidence),
stackitem.NewArray(approvals),
stackitem.NewBigInteger(big.NewInt(int64(r.RequiredApprovals))),
stackitem.NewBigInteger(big.NewInt(int64(r.CreatedAt))),
stackitem.NewBigInteger(big.NewInt(int64(r.DelayUntil))),
stackitem.NewBigInteger(big.NewInt(int64(r.ExpiresAt))),
stackitem.NewBigInteger(big.NewInt(int64(r.Status))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (r *RecoveryRequest) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 11 {
return fmt.Errorf("wrong number of elements: expected 11, got %d", len(items))
}
requestIDBytes, err := items[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid requestID: %w", err)
}
r.RequestID, err = util.Uint256DecodeBytesBE(requestIDBytes)
if err != nil {
return fmt.Errorf("invalid requestID hash: %w", err)
}
tokenID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid tokenID: %w", err)
}
r.TokenID = tokenID.Uint64()
newOwnerBytes, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid newOwner: %w", err)
}
r.NewOwner, err = util.Uint160DecodeBytesBE(newOwnerBytes)
if err != nil {
return fmt.Errorf("invalid newOwner hash: %w", err)
}
requesterBytes, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid requester: %w", err)
}
r.Requester, err = util.Uint160DecodeBytesBE(requesterBytes)
if err != nil {
return fmt.Errorf("invalid requester hash: %w", err)
}
r.Evidence, err = items[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid evidence: %w", err)
}
approvalsArray, ok := items[5].Value().([]stackitem.Item)
if !ok {
return errors.New("approvals is not an array")
}
r.Approvals = make([]util.Uint160, len(approvalsArray))
for i, a := range approvalsArray {
approvalBytes, err := a.TryBytes()
if err != nil {
return fmt.Errorf("invalid approval %d: %w", i, err)
}
r.Approvals[i], err = util.Uint160DecodeBytesBE(approvalBytes)
if err != nil {
return fmt.Errorf("invalid approval %d hash: %w", i, err)
}
}
requiredApprovals, err := items[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid requiredApprovals: %w", err)
}
ra := requiredApprovals.Int64()
if ra < 0 || ra > math.MaxInt {
return errors.New("requiredApprovals out of range")
}
r.RequiredApprovals = int(ra)
createdAt, err := items[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid createdAt: %w", err)
}
r.CreatedAt = uint32(createdAt.Int64())
delayUntil, err := items[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid delayUntil: %w", err)
}
r.DelayUntil = uint32(delayUntil.Int64())
expiresAt, err := items[9].TryInteger()
if err != nil {
return fmt.Errorf("invalid expiresAt: %w", err)
}
r.ExpiresAt = uint32(expiresAt.Int64())
status, err := items[10].TryInteger()
if err != nil {
return fmt.Errorf("invalid status: %w", err)
}
r.Status = RecoveryStatus(status.Int64())
return nil
}