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:
parent
cf481e4137
commit
99ba041a85
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue