Add free GAS for Vita holders with cross-chain fee splitting
Rename PersonToken to Vita (soul-bound identity token) and implement fee exemption system for citizens: - Add Federation native contract for cross-chain Vita coordination - Visitor registry for Vita holders from other chains - Configurable visiting fee percentage (default 50%) - Inter-chain debt tracking for settlement between chains - Modify GAS contract to burn fees from Treasury for Vita holders - Local Vita: 100% paid by local Treasury - Visiting Vita: Split between local Treasury and inter-chain debt - Deficit tracking when Treasury is underfunded - Update mempool and blockchain to skip fee checks for Vita exempt users - Add IsVitaFeeExempt() to Feer interface - Modify verifyAndPoolTx() to allow zero-fee transactions - Rename PersonToken -> Vita across codebase - Update state, native contract, and tests - Maintain same functionality with clearer naming 🤖 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
b5c1dca2c6
commit
f03564d676
|
|
@ -218,6 +218,9 @@ type Blockchain struct {
|
|||
designate native.IDesignate
|
||||
oracle native.IOracle
|
||||
notary native.INotary
|
||||
vita native.IVita
|
||||
federation native.IFederation
|
||||
treasury native.ITreasury
|
||||
|
||||
extensible atomic.Value
|
||||
|
||||
|
|
@ -465,6 +468,18 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger, newN
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
bc.vita = bc.contracts.Vita()
|
||||
if err := validateNative(bc.vita, nativeids.Vita, nativenames.Vita, nativehashes.Vita); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.federation = bc.contracts.Federation()
|
||||
if err := validateNative(bc.federation, nativeids.Federation, nativenames.Federation, nativehashes.Federation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.treasury = bc.contracts.Treasury()
|
||||
if err := validateNative(bc.treasury, nativeids.Treasury, nativenames.Treasury, nativehashes.Treasury); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bc.persistCond = sync.NewCond(&bc.lock)
|
||||
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
|
||||
|
|
@ -2442,6 +2457,26 @@ func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int {
|
|||
return bs
|
||||
}
|
||||
|
||||
// IsVitaFeeExempt returns true if the account has an active Vita token
|
||||
// (local or visiting) and is exempt from paying transaction fees.
|
||||
// This implements the Feer interface.
|
||||
func (bc *Blockchain) IsVitaFeeExempt(acc util.Uint160) bool {
|
||||
// Check local Vita first
|
||||
if bc.vita != nil {
|
||||
token, err := bc.vita.GetTokenByOwner(bc.dao, acc)
|
||||
if err == nil && token != nil && token.Status == state.TokenStatusActive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Check visiting Vita registry
|
||||
if bc.federation != nil {
|
||||
if bc.federation.IsVisitor(bc.dao, acc) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetGoverningTokenBalance returns governing token (NEO) balance and the height
|
||||
// of the last balance change for the account.
|
||||
func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) {
|
||||
|
|
@ -2946,7 +2981,8 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
|
|||
}
|
||||
needNetworkFee := int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(t)
|
||||
netFee := t.NetworkFee - needNetworkFee
|
||||
if netFee < 0 {
|
||||
// Skip fee check for Vita holders (local or visiting) - their fees are paid by Treasury
|
||||
if netFee < 0 && !bc.IsVitaFeeExempt(t.Sender()) {
|
||||
return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
|
||||
}
|
||||
// check that current tx wasn't included in the conflicts attributes of some other transaction which is already in the chain
|
||||
|
|
|
|||
|
|
@ -11,4 +11,7 @@ type Feer interface {
|
|||
FeePerByte() int64
|
||||
GetUtilityTokenBalance(util.Uint160) *big.Int
|
||||
BlockHeight() uint32
|
||||
// IsVitaFeeExempt returns true if the account has an active Vita token
|
||||
// (local or visiting) and is exempt from paying transaction fees.
|
||||
IsVitaFeeExempt(util.Uint160) bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,13 @@ func (mp *Pool) HasConflicts(t *transaction.Transaction, fee Feer) bool {
|
|||
// and returns false if both balance check is required and the sender does not have enough GAS to pay.
|
||||
func (mp *Pool) tryAddSendersFee(tx *transaction.Transaction, feer Feer, needCheck bool) bool {
|
||||
payer := tx.Signers[mp.payerIndex].Account
|
||||
|
||||
// Skip balance check for Vita fee exempt users (local or visiting Vita holders).
|
||||
// Their fees are paid by the Treasury.
|
||||
if feer.IsVitaFeeExempt(payer) {
|
||||
return true
|
||||
}
|
||||
|
||||
senderFee, ok := mp.fees[payer]
|
||||
if !ok {
|
||||
_ = senderFee.balance.SetFromBig(feer.GetUtilityTokenBalance(payer))
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
|
|||
return big.NewInt(fs.balance)
|
||||
}
|
||||
|
||||
func (fs *FeerStub) IsVitaFeeExempt(util.Uint160) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) {
|
||||
mp := New(10, 0, false, nil)
|
||||
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
||||
|
|
|
|||
|
|
@ -111,12 +111,12 @@ type (
|
|||
GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32
|
||||
}
|
||||
|
||||
// IPersonToken is an interface required from native PersonToken contract
|
||||
// IVita is an interface required from native Vita contract
|
||||
// for interaction with Blockchain and other native contracts.
|
||||
IPersonToken interface {
|
||||
IVita interface {
|
||||
interop.Contract
|
||||
GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.PersonToken, error)
|
||||
GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.PersonToken, error)
|
||||
GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.Vita, error)
|
||||
GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.Vita, error)
|
||||
TokenExists(d *dao.Simple, owner util.Uint160) bool
|
||||
GetAttribute(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error)
|
||||
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
|
||||
|
|
@ -156,6 +156,37 @@ type (
|
|||
// IsVendor returns true if address is a registered active vendor.
|
||||
IsVendor(d *dao.Simple, addr util.Uint160) bool
|
||||
}
|
||||
|
||||
// IFederation is an interface required from native Federation contract for
|
||||
// interaction with Blockchain and other native contracts.
|
||||
// Federation manages cross-chain Vita coordination and visiting fee governance.
|
||||
IFederation interface {
|
||||
interop.Contract
|
||||
// GetVisitingFeePercent returns the percentage of fees the host chain pays for visitors.
|
||||
GetVisitingFeePercent(d *dao.Simple) uint8
|
||||
// IsVisitor checks if an address is a registered visitor from another chain.
|
||||
IsVisitor(d *dao.Simple, owner util.Uint160) bool
|
||||
// GetHomeChain returns the home chain ID for a visitor.
|
||||
GetHomeChain(d *dao.Simple, owner util.Uint160) uint32
|
||||
// AddInterChainDebt adds to the inter-chain debt owed to a specific chain.
|
||||
AddInterChainDebt(d *dao.Simple, chainID uint32, amount *big.Int)
|
||||
// GetInterChainDebt returns the inter-chain debt owed to a specific chain.
|
||||
GetInterChainDebt(d *dao.Simple, chainID uint32) *big.Int
|
||||
// Address returns the contract's script hash.
|
||||
Address() util.Uint160
|
||||
}
|
||||
|
||||
// ITreasury is an interface required from native Treasury contract for
|
||||
// interaction with Blockchain and other native contracts.
|
||||
ITreasury interface {
|
||||
interop.Contract
|
||||
// Address returns the contract's script hash.
|
||||
Address() util.Uint160
|
||||
// AddDeficit adds to the cumulative deficit tracking.
|
||||
AddDeficit(d *dao.Simple, amount *big.Int)
|
||||
// GetDeficit returns the current cumulative deficit.
|
||||
GetDeficit(d *dao.Simple) *big.Int
|
||||
}
|
||||
)
|
||||
|
||||
// Contracts is a convenient wrapper around an arbitrary set of native contracts
|
||||
|
|
@ -271,10 +302,10 @@ func (cs *Contracts) Notary() INotary {
|
|||
return nil
|
||||
}
|
||||
|
||||
// PersonToken returns native IPersonToken contract implementation. It panics if
|
||||
// Vita returns native IVita 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)
|
||||
func (cs *Contracts) Vita() IVita {
|
||||
return cs.ByName(nativenames.Vita).(IVita)
|
||||
}
|
||||
|
||||
// RoleRegistry returns native IRoleRegistry contract implementation. It panics if
|
||||
|
|
@ -289,6 +320,18 @@ func (cs *Contracts) VTS() IVTS {
|
|||
return cs.ByName(nativenames.VTS).(IVTS)
|
||||
}
|
||||
|
||||
// Federation returns native IFederation contract implementation. It panics if
|
||||
// there's no contract with proper name in cs.
|
||||
func (cs *Contracts) Federation() IFederation {
|
||||
return cs.ByName(nativenames.Federation).(IFederation)
|
||||
}
|
||||
|
||||
// Treasury returns native ITreasury contract implementation. It panics if
|
||||
// there's no contract with proper name in cs.
|
||||
func (cs *Contracts) Treasury() ITreasury {
|
||||
return cs.ByName(nativenames.Treasury).(ITreasury)
|
||||
}
|
||||
|
||||
// NewDefaultContracts returns a new set of default native contracts.
|
||||
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||
mgmt := NewManagement()
|
||||
|
|
@ -325,8 +368,8 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
|||
treasury := newTreasury()
|
||||
treasury.NEO = neo
|
||||
|
||||
personToken := newPersonToken()
|
||||
personToken.NEO = neo
|
||||
vita := newVita()
|
||||
vita.NEO = neo
|
||||
|
||||
// Parse TutusCommittee addresses from config
|
||||
var tutusCommittee []util.Uint160
|
||||
|
|
@ -344,14 +387,23 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
|||
roleRegistry := newRoleRegistry(tutusCommittee)
|
||||
roleRegistry.NEO = neo
|
||||
|
||||
// Set RoleRegistry on PersonToken for cross-contract integration
|
||||
personToken.RoleRegistry = roleRegistry
|
||||
// Set RoleRegistry on Vita for cross-contract integration
|
||||
vita.RoleRegistry = roleRegistry
|
||||
|
||||
// Create VTS (Value Transfer System) contract
|
||||
vts := newVTS()
|
||||
vts.NEO = neo
|
||||
vts.RoleRegistry = roleRegistry
|
||||
vts.PersonToken = personToken
|
||||
vts.Vita = vita
|
||||
|
||||
// Create Federation contract for cross-chain Vita coordination
|
||||
federation := newFederation()
|
||||
federation.NEO = neo
|
||||
|
||||
// Wire GAS dependencies for Vita fee exemption
|
||||
gas.Vita = vita
|
||||
gas.Federation = federation
|
||||
gas.Treasury = treasury
|
||||
|
||||
return []interop.Contract{
|
||||
mgmt,
|
||||
|
|
@ -365,8 +417,9 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
|||
oracle,
|
||||
notary,
|
||||
treasury,
|
||||
personToken,
|
||||
vita,
|
||||
roleRegistry,
|
||||
vts,
|
||||
federation,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,444 @@
|
|||
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/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"
|
||||
)
|
||||
|
||||
// Federation represents the Federation native contract for cross-chain Vita coordination.
|
||||
// It manages:
|
||||
// - Visiting fee percentage (what % of fees the host chain pays for visitors)
|
||||
// - Visitor registry (tracking Vita holders from other chains)
|
||||
// - Inter-chain debt (tracking fees owed to other chains)
|
||||
type Federation struct {
|
||||
interop.ContractMD
|
||||
NEO INEO
|
||||
}
|
||||
|
||||
// Storage key prefixes for Federation.
|
||||
const (
|
||||
prefixVisitingFeePercent byte = 0x01 // -> uint8 (0-100, % host chain pays for visitors)
|
||||
prefixVisitorRegistry byte = 0x02 // owner (Uint160) -> home chain ID (uint32)
|
||||
prefixInterChainDebt byte = 0x03 // chain ID (uint32) -> *big.Int (amount owed)
|
||||
)
|
||||
|
||||
// Default values.
|
||||
const (
|
||||
defaultVisitingFeePercent uint8 = 50 // 50% local, 50% inter-chain debt
|
||||
)
|
||||
|
||||
// Event names for Federation.
|
||||
const (
|
||||
VisitorRegisteredEvent = "VisitorRegistered"
|
||||
VisitorUnregisteredEvent = "VisitorUnregistered"
|
||||
FeePercentChangedEvent = "FeePercentChanged"
|
||||
DebtSettledEvent = "DebtSettled"
|
||||
)
|
||||
|
||||
// Various errors.
|
||||
var (
|
||||
ErrInvalidFeePercent = errors.New("fee percent must be 0-100")
|
||||
ErrVisitorAlreadyExists = errors.New("visitor already registered")
|
||||
ErrVisitorNotFound = errors.New("visitor not found")
|
||||
ErrInvalidChainID = errors.New("invalid chain ID")
|
||||
ErrInsufficientDebt = errors.New("insufficient debt to settle")
|
||||
)
|
||||
|
||||
var _ interop.Contract = (*Federation)(nil)
|
||||
|
||||
// newFederation creates a new Federation native contract.
|
||||
func newFederation() *Federation {
|
||||
f := &Federation{
|
||||
ContractMD: *interop.NewContractMD(nativenames.Federation, nativeids.Federation),
|
||||
}
|
||||
defer f.BuildHFSpecificMD(f.ActiveIn())
|
||||
|
||||
// getFeePercent method
|
||||
desc := NewDescriptor("getFeePercent", smartcontract.IntegerType)
|
||||
md := NewMethodAndPrice(f.getFeePercent, 1<<15, callflag.ReadStates)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// setFeePercent method (committee only)
|
||||
desc = NewDescriptor("setFeePercent", smartcontract.BoolType,
|
||||
manifest.NewParameter("percent", smartcontract.IntegerType))
|
||||
md = NewMethodAndPrice(f.setFeePercent, 1<<16, callflag.States|callflag.AllowNotify)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// registerVisitor method (committee only)
|
||||
desc = NewDescriptor("registerVisitor", smartcontract.BoolType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
||||
manifest.NewParameter("homeChainID", smartcontract.IntegerType))
|
||||
md = NewMethodAndPrice(f.registerVisitor, 1<<16, callflag.States|callflag.AllowNotify)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// unregisterVisitor method (committee only)
|
||||
desc = NewDescriptor("unregisterVisitor", smartcontract.BoolType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = NewMethodAndPrice(f.unregisterVisitor, 1<<16, callflag.States|callflag.AllowNotify)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// isVisitor method
|
||||
desc = NewDescriptor("isVisitor", smartcontract.BoolType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = NewMethodAndPrice(f.isVisitor, 1<<15, callflag.ReadStates)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// getHomeChain method
|
||||
desc = NewDescriptor("getHomeChain", smartcontract.IntegerType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = NewMethodAndPrice(f.getHomeChain, 1<<15, callflag.ReadStates)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// getInterChainDebt method
|
||||
desc = NewDescriptor("getInterChainDebt", smartcontract.IntegerType,
|
||||
manifest.NewParameter("chainID", smartcontract.IntegerType))
|
||||
md = NewMethodAndPrice(f.getInterChainDebt, 1<<15, callflag.ReadStates)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// settleDebt method (committee only)
|
||||
desc = NewDescriptor("settleDebt", smartcontract.BoolType,
|
||||
manifest.NewParameter("chainID", smartcontract.IntegerType),
|
||||
manifest.NewParameter("amount", smartcontract.IntegerType))
|
||||
md = NewMethodAndPrice(f.settleDebt, 1<<16, callflag.States|callflag.AllowNotify)
|
||||
f.AddMethod(md, desc)
|
||||
|
||||
// Events
|
||||
eDesc := NewEventDescriptor(VisitorRegisteredEvent,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type),
|
||||
manifest.NewParameter("homeChainID", smartcontract.IntegerType))
|
||||
f.AddEvent(NewEvent(eDesc))
|
||||
|
||||
eDesc = NewEventDescriptor(VisitorUnregisteredEvent,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
f.AddEvent(NewEvent(eDesc))
|
||||
|
||||
eDesc = NewEventDescriptor(FeePercentChangedEvent,
|
||||
manifest.NewParameter("oldPercent", smartcontract.IntegerType),
|
||||
manifest.NewParameter("newPercent", smartcontract.IntegerType))
|
||||
f.AddEvent(NewEvent(eDesc))
|
||||
|
||||
eDesc = NewEventDescriptor(DebtSettledEvent,
|
||||
manifest.NewParameter("chainID", smartcontract.IntegerType),
|
||||
manifest.NewParameter("amount", smartcontract.IntegerType),
|
||||
manifest.NewParameter("remaining", smartcontract.IntegerType))
|
||||
f.AddEvent(NewEvent(eDesc))
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// Metadata returns contract metadata.
|
||||
func (f *Federation) Metadata() *interop.ContractMD {
|
||||
return &f.ContractMD
|
||||
}
|
||||
|
||||
// Initialize initializes Federation contract at the specified hardfork.
|
||||
func (f *Federation) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
|
||||
if hf != f.ActiveIn() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize default fee percent
|
||||
f.setFeePercentInternal(ic.DAO, defaultVisitingFeePercent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitializeCache fills native Federation cache from DAO on node restart.
|
||||
func (f *Federation) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnPersist implements the Contract interface.
|
||||
func (f *Federation) OnPersist(ic *interop.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostPersist implements the Contract interface.
|
||||
func (f *Federation) PostPersist(ic *interop.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActiveIn returns the hardfork this contract activates in.
|
||||
func (f *Federation) ActiveIn() *config.Hardfork {
|
||||
var hf = config.HFFaun
|
||||
return &hf
|
||||
}
|
||||
|
||||
// Storage key helpers
|
||||
|
||||
func makeFeePercentKey() []byte {
|
||||
return []byte{prefixVisitingFeePercent}
|
||||
}
|
||||
|
||||
func makeVisitorKey(owner util.Uint160) []byte {
|
||||
key := make([]byte, 1+util.Uint160Size)
|
||||
key[0] = prefixVisitorRegistry
|
||||
copy(key[1:], owner.BytesBE())
|
||||
return key
|
||||
}
|
||||
|
||||
func makeDebtKey(chainID uint32) []byte {
|
||||
key := make([]byte, 5)
|
||||
key[0] = prefixInterChainDebt
|
||||
binary.BigEndian.PutUint32(key[1:], chainID)
|
||||
return key
|
||||
}
|
||||
|
||||
// Internal storage methods
|
||||
|
||||
func (f *Federation) getFeePercentInternal(d *dao.Simple) uint8 {
|
||||
si := d.GetStorageItem(f.ID, makeFeePercentKey())
|
||||
if si == nil {
|
||||
return defaultVisitingFeePercent
|
||||
}
|
||||
return si[0]
|
||||
}
|
||||
|
||||
func (f *Federation) setFeePercentInternal(d *dao.Simple, percent uint8) {
|
||||
d.PutStorageItem(f.ID, makeFeePercentKey(), []byte{percent})
|
||||
}
|
||||
|
||||
func (f *Federation) getVisitorChainInternal(d *dao.Simple, owner util.Uint160) (uint32, bool) {
|
||||
si := d.GetStorageItem(f.ID, makeVisitorKey(owner))
|
||||
if si == nil {
|
||||
return 0, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(si), true
|
||||
}
|
||||
|
||||
func (f *Federation) setVisitorChainInternal(d *dao.Simple, owner util.Uint160, chainID uint32) {
|
||||
buf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(buf, chainID)
|
||||
d.PutStorageItem(f.ID, makeVisitorKey(owner), buf)
|
||||
}
|
||||
|
||||
func (f *Federation) deleteVisitorInternal(d *dao.Simple, owner util.Uint160) {
|
||||
d.DeleteStorageItem(f.ID, makeVisitorKey(owner))
|
||||
}
|
||||
|
||||
func (f *Federation) getDebtInternal(d *dao.Simple, chainID uint32) *big.Int {
|
||||
si := d.GetStorageItem(f.ID, makeDebtKey(chainID))
|
||||
if si == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
return new(big.Int).SetBytes(si)
|
||||
}
|
||||
|
||||
func (f *Federation) setDebtInternal(d *dao.Simple, chainID uint32, amount *big.Int) {
|
||||
if amount.Sign() <= 0 {
|
||||
d.DeleteStorageItem(f.ID, makeDebtKey(chainID))
|
||||
return
|
||||
}
|
||||
d.PutStorageItem(f.ID, makeDebtKey(chainID), amount.Bytes())
|
||||
}
|
||||
|
||||
func (f *Federation) addDebtInternal(d *dao.Simple, chainID uint32, amount *big.Int) {
|
||||
current := f.getDebtInternal(d, chainID)
|
||||
newDebt := new(big.Int).Add(current, amount)
|
||||
f.setDebtInternal(d, chainID, newDebt)
|
||||
}
|
||||
|
||||
// Contract methods
|
||||
|
||||
func (f *Federation) getFeePercent(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||
percent := f.getFeePercentInternal(ic.DAO)
|
||||
return stackitem.NewBigInteger(big.NewInt(int64(percent)))
|
||||
}
|
||||
|
||||
func (f *Federation) setFeePercent(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
percent := toBigInt(args[0]).Int64()
|
||||
|
||||
// Validate percent
|
||||
if percent < 0 || percent > 100 {
|
||||
panic(ErrInvalidFeePercent)
|
||||
}
|
||||
|
||||
// Check committee
|
||||
if !f.NEO.CheckCommittee(ic) {
|
||||
panic(ErrNotCommittee)
|
||||
}
|
||||
|
||||
// Get old value for event
|
||||
oldPercent := f.getFeePercentInternal(ic.DAO)
|
||||
|
||||
// Set new value
|
||||
f.setFeePercentInternal(ic.DAO, uint8(percent))
|
||||
|
||||
// Emit event
|
||||
err := ic.AddNotification(f.Hash, FeePercentChangedEvent, stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewBigInteger(big.NewInt(int64(oldPercent))),
|
||||
stackitem.NewBigInteger(big.NewInt(percent)),
|
||||
}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func (f *Federation) registerVisitor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
owner := toUint160(args[0])
|
||||
chainID := uint32(toBigInt(args[1]).Int64())
|
||||
|
||||
// Validate chain ID
|
||||
if chainID == 0 {
|
||||
panic(ErrInvalidChainID)
|
||||
}
|
||||
|
||||
// Check committee
|
||||
if !f.NEO.CheckCommittee(ic) {
|
||||
panic(ErrNotCommittee)
|
||||
}
|
||||
|
||||
// Check if already registered
|
||||
if _, exists := f.getVisitorChainInternal(ic.DAO, owner); exists {
|
||||
panic(ErrVisitorAlreadyExists)
|
||||
}
|
||||
|
||||
// Register visitor
|
||||
f.setVisitorChainInternal(ic.DAO, owner, chainID)
|
||||
|
||||
// Emit event
|
||||
err := ic.AddNotification(f.Hash, VisitorRegisteredEvent, stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewByteArray(owner.BytesBE()),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(chainID))),
|
||||
}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func (f *Federation) unregisterVisitor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
owner := toUint160(args[0])
|
||||
|
||||
// Check committee
|
||||
if !f.NEO.CheckCommittee(ic) {
|
||||
panic(ErrNotCommittee)
|
||||
}
|
||||
|
||||
// Check if registered
|
||||
if _, exists := f.getVisitorChainInternal(ic.DAO, owner); !exists {
|
||||
panic(ErrVisitorNotFound)
|
||||
}
|
||||
|
||||
// Unregister visitor
|
||||
f.deleteVisitorInternal(ic.DAO, owner)
|
||||
|
||||
// Emit event
|
||||
err := ic.AddNotification(f.Hash, VisitorUnregisteredEvent, stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewByteArray(owner.BytesBE()),
|
||||
}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func (f *Federation) isVisitor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
owner := toUint160(args[0])
|
||||
_, exists := f.getVisitorChainInternal(ic.DAO, owner)
|
||||
return stackitem.NewBool(exists)
|
||||
}
|
||||
|
||||
func (f *Federation) getHomeChain(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
owner := toUint160(args[0])
|
||||
chainID, exists := f.getVisitorChainInternal(ic.DAO, owner)
|
||||
if !exists {
|
||||
return stackitem.NewBigInteger(big.NewInt(0))
|
||||
}
|
||||
return stackitem.NewBigInteger(big.NewInt(int64(chainID)))
|
||||
}
|
||||
|
||||
func (f *Federation) getInterChainDebt(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
chainID := uint32(toBigInt(args[0]).Int64())
|
||||
debt := f.getDebtInternal(ic.DAO, chainID)
|
||||
return stackitem.NewBigInteger(debt)
|
||||
}
|
||||
|
||||
func (f *Federation) settleDebt(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
chainID := uint32(toBigInt(args[0]).Int64())
|
||||
amount := toBigInt(args[1])
|
||||
|
||||
// Validate chain ID
|
||||
if chainID == 0 {
|
||||
panic(ErrInvalidChainID)
|
||||
}
|
||||
|
||||
// Check committee
|
||||
if !f.NEO.CheckCommittee(ic) {
|
||||
panic(ErrNotCommittee)
|
||||
}
|
||||
|
||||
// Get current debt
|
||||
currentDebt := f.getDebtInternal(ic.DAO, chainID)
|
||||
|
||||
// Check sufficient debt
|
||||
if currentDebt.Cmp(amount) < 0 {
|
||||
panic(ErrInsufficientDebt)
|
||||
}
|
||||
|
||||
// Subtract settled amount
|
||||
remaining := new(big.Int).Sub(currentDebt, amount)
|
||||
f.setDebtInternal(ic.DAO, chainID, remaining)
|
||||
|
||||
// Emit event
|
||||
err := ic.AddNotification(f.Hash, DebtSettledEvent, stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewBigInteger(big.NewInt(int64(chainID))),
|
||||
stackitem.NewBigInteger(amount),
|
||||
stackitem.NewBigInteger(remaining),
|
||||
}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
// Public methods for cross-native access
|
||||
|
||||
// GetVisitingFeePercent returns the visiting fee percent (for cross-native access).
|
||||
func (f *Federation) GetVisitingFeePercent(d *dao.Simple) uint8 {
|
||||
return f.getFeePercentInternal(d)
|
||||
}
|
||||
|
||||
// IsVisitor checks if an address is a registered visitor (for cross-native access).
|
||||
func (f *Federation) IsVisitor(d *dao.Simple, owner util.Uint160) bool {
|
||||
_, exists := f.getVisitorChainInternal(d, owner)
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetHomeChain returns the home chain ID for a visitor (for cross-native access).
|
||||
func (f *Federation) GetHomeChain(d *dao.Simple, owner util.Uint160) uint32 {
|
||||
chainID, _ := f.getVisitorChainInternal(d, owner)
|
||||
return chainID
|
||||
}
|
||||
|
||||
// AddInterChainDebt adds to the inter-chain debt for a specific chain (for cross-native access).
|
||||
func (f *Federation) AddInterChainDebt(d *dao.Simple, chainID uint32, amount *big.Int) {
|
||||
f.addDebtInternal(d, chainID, amount)
|
||||
}
|
||||
|
||||
// GetInterChainDebt returns the inter-chain debt for a specific chain (for cross-native access).
|
||||
func (f *Federation) GetInterChainDebt(d *dao.Simple, chainID uint32) *big.Int {
|
||||
return f.getDebtInternal(d, chainID)
|
||||
}
|
||||
|
||||
// Address returns the contract's script hash.
|
||||
func (f *Federation) Address() util.Uint160 {
|
||||
return f.Hash
|
||||
}
|
||||
|
|
@ -20,12 +20,27 @@ import (
|
|||
// GAS represents GAS native contract.
|
||||
type GAS struct {
|
||||
nep17TokenNative
|
||||
NEO INEO
|
||||
Policy IPolicy
|
||||
NEO INEO
|
||||
Policy IPolicy
|
||||
Vita IVita // For checking local Vita status
|
||||
Federation IFederation // For visiting Vita and fee split
|
||||
Treasury ITreasury // For subsidizing fees
|
||||
|
||||
initialSupply int64
|
||||
}
|
||||
|
||||
// VitaExemptType indicates the type of Vita exemption for fee purposes.
|
||||
type VitaExemptType int
|
||||
|
||||
const (
|
||||
// VitaExemptNone indicates no Vita exemption.
|
||||
VitaExemptNone VitaExemptType = iota
|
||||
// VitaExemptLocal indicates a local Vita holder (100% paid by local Treasury).
|
||||
VitaExemptLocal
|
||||
// VitaExemptVisitor indicates a visiting Vita holder (split between local and home chain).
|
||||
VitaExemptVisitor
|
||||
)
|
||||
|
||||
// GASFactor is a divisor for finding GAS integral value.
|
||||
const GASFactor = NEOTotalSupply
|
||||
|
||||
|
|
@ -112,7 +127,26 @@ func (g *GAS) OnPersist(ic *interop.Context) error {
|
|||
}
|
||||
for _, tx := range ic.Block.Transactions {
|
||||
absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee)
|
||||
g.Burn(ic, tx.Sender(), absAmount)
|
||||
sender := tx.Sender()
|
||||
|
||||
// Check fee exemption type
|
||||
exemptType := g.getVitaExemptType(ic.DAO, sender)
|
||||
|
||||
switch exemptType {
|
||||
case VitaExemptLocal:
|
||||
// Local citizen: 100% from Treasury
|
||||
g.burnFromTreasury(ic, absAmount)
|
||||
case VitaExemptVisitor:
|
||||
// Visitor: split between local Treasury and inter-chain debt
|
||||
var homeChain uint32
|
||||
if g.Federation != nil {
|
||||
homeChain = g.Federation.GetHomeChain(ic.DAO, sender)
|
||||
}
|
||||
g.burnFromTreasuryWithSplit(ic, absAmount, homeChain)
|
||||
default:
|
||||
// Non-citizen: burn from sender
|
||||
g.Burn(ic, sender, absAmount)
|
||||
}
|
||||
}
|
||||
validators := g.NEO.GetNextBlockValidatorsInternal(ic.DAO)
|
||||
primary := validators[ic.Block.PrimaryIndex].GetScriptHash()
|
||||
|
|
@ -136,6 +170,67 @@ func (g *GAS) PostPersist(ic *interop.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// getVitaExemptType determines if sender is exempt from fees and what type.
|
||||
func (g *GAS) getVitaExemptType(d *dao.Simple, sender util.Uint160) VitaExemptType {
|
||||
// Check local Vita first
|
||||
if g.Vita != nil {
|
||||
token, err := g.Vita.GetTokenByOwner(d, sender)
|
||||
if err == nil && token != nil && token.Status == state.TokenStatusActive {
|
||||
return VitaExemptLocal
|
||||
}
|
||||
}
|
||||
// Check visitor registry
|
||||
if g.Federation != nil && g.Federation.IsVisitor(d, sender) {
|
||||
return VitaExemptVisitor
|
||||
}
|
||||
return VitaExemptNone
|
||||
}
|
||||
|
||||
// burnFromTreasury burns GAS from Treasury for local Vita holders (100% local).
|
||||
func (g *GAS) burnFromTreasury(ic *interop.Context, amount *big.Int) {
|
||||
g.burnFromTreasuryInternal(ic, amount, 100, 0)
|
||||
}
|
||||
|
||||
// burnFromTreasuryWithSplit burns GAS with visiting fee split between local Treasury and inter-chain debt.
|
||||
func (g *GAS) burnFromTreasuryWithSplit(ic *interop.Context, amount *big.Int, homeChain uint32) {
|
||||
localPercent := uint8(100)
|
||||
if g.Federation != nil {
|
||||
localPercent = g.Federation.GetVisitingFeePercent(ic.DAO)
|
||||
}
|
||||
g.burnFromTreasuryInternal(ic, amount, localPercent, homeChain)
|
||||
}
|
||||
|
||||
// burnFromTreasuryInternal handles the actual burn with optional split between local and inter-chain.
|
||||
func (g *GAS) burnFromTreasuryInternal(ic *interop.Context, amount *big.Int, localPercent uint8, homeChain uint32) {
|
||||
if g.Treasury == nil {
|
||||
return
|
||||
}
|
||||
|
||||
treasuryAddr := g.Treasury.Address()
|
||||
treasuryBalance := g.BalanceOf(ic.DAO, treasuryAddr)
|
||||
|
||||
// Calculate local vs inter-chain portions
|
||||
localAmount := new(big.Int).Mul(amount, big.NewInt(int64(localPercent)))
|
||||
localAmount.Div(localAmount, big.NewInt(100))
|
||||
interChainAmount := new(big.Int).Sub(amount, localAmount)
|
||||
|
||||
// Burn from Treasury what we can
|
||||
if treasuryBalance.Cmp(localAmount) >= 0 {
|
||||
g.Burn(ic, treasuryAddr, localAmount)
|
||||
} else if treasuryBalance.Sign() > 0 {
|
||||
deficit := new(big.Int).Sub(localAmount, treasuryBalance)
|
||||
g.Burn(ic, treasuryAddr, treasuryBalance)
|
||||
g.Treasury.AddDeficit(ic.DAO, deficit)
|
||||
} else {
|
||||
g.Treasury.AddDeficit(ic.DAO, localAmount)
|
||||
}
|
||||
|
||||
// Record inter-chain debt (if visitor)
|
||||
if interChainAmount.Sign() > 0 && homeChain != 0 && g.Federation != nil {
|
||||
g.Federation.AddInterChainDebt(ic.DAO, homeChain, interChainAmount)
|
||||
}
|
||||
}
|
||||
|
||||
// ActiveIn implements the Contract interface.
|
||||
func (g *GAS) ActiveIn() *config.Hardfork {
|
||||
return nil
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,13 +11,13 @@ import (
|
|||
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
func newPersonTokenClient(t *testing.T) *neotest.ContractInvoker {
|
||||
return newNativeClient(t, nativenames.PersonToken)
|
||||
func newVitaClient(t *testing.T) *neotest.ContractInvoker {
|
||||
return newNativeClient(t, nativenames.Vita)
|
||||
}
|
||||
|
||||
// registerPersonToken is a helper to register a PersonToken for a signer.
|
||||
// registerVita is a helper to register a Vita for a signer.
|
||||
// Returns the tokenID bytes.
|
||||
func registerPersonToken(t *testing.T, c *neotest.ContractInvoker, signer neotest.Signer) []byte {
|
||||
func registerVita(t *testing.T, c *neotest.ContractInvoker, signer neotest.Signer) []byte {
|
||||
owner := signer.ScriptHash()
|
||||
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
|
||||
isEntity := false
|
||||
|
|
@ -38,9 +38,9 @@ func registerPersonToken(t *testing.T, c *neotest.ContractInvoker, signer neotes
|
|||
return tokenIDBytes
|
||||
}
|
||||
|
||||
// TestPersonToken_Register tests basic registration functionality.
|
||||
func TestPersonToken_Register(t *testing.T) {
|
||||
c := newPersonTokenClient(t)
|
||||
// TestVita_Register tests basic registration functionality.
|
||||
func TestVita_Register(t *testing.T) {
|
||||
c := newVitaClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
|
|
@ -61,7 +61,7 @@ func TestPersonToken_Register(t *testing.T) {
|
|||
// Check event was emitted
|
||||
aer := e.GetTxExecResult(t, txHash)
|
||||
require.Equal(t, 1, len(aer.Events))
|
||||
require.Equal(t, "PersonTokenCreated", aer.Events[0].Name)
|
||||
require.Equal(t, "VitaCreated", aer.Events[0].Name)
|
||||
|
||||
// Check exists returns true
|
||||
invoker.Invoke(t, true, "exists", owner.BytesBE())
|
||||
|
|
@ -74,13 +74,13 @@ func TestPersonToken_Register(t *testing.T) {
|
|||
}, "getToken", owner.BytesBE())
|
||||
}
|
||||
|
||||
// TestPersonToken_ValidateCaller tests the validateCaller method.
|
||||
// TestVita_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)
|
||||
func TestVita_ValidateCaller(t *testing.T) {
|
||||
c := newVitaClient(t)
|
||||
|
||||
t.Run("no token - direct call", func(t *testing.T) {
|
||||
acc := c.Executor.NewAccount(t)
|
||||
|
|
@ -88,35 +88,35 @@ func TestPersonToken_ValidateCaller(t *testing.T) {
|
|||
|
||||
// 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")
|
||||
invoker.InvokeFail(t, "caller does not have a Vita", "validateCaller")
|
||||
})
|
||||
|
||||
// Note: Testing validateCaller with a token requires deploying a helper contract
|
||||
// that has a PersonToken registered to its script hash, then calling validateCaller
|
||||
// that has a Vita 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.
|
||||
// TestVita_RequireRole tests the requireRole method.
|
||||
// Note: requireRole uses GetCallingScriptHash() - designed for cross-contract authorization.
|
||||
func TestPersonToken_RequireRole(t *testing.T) {
|
||||
c := newPersonTokenClient(t)
|
||||
func TestVita_RequireRole(t *testing.T) {
|
||||
c := newVitaClient(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)
|
||||
invoker.InvokeFail(t, "caller does not have a Vita", "requireRole", 0)
|
||||
})
|
||||
|
||||
// Note: Testing requireRole with actual role checks requires a deployed contract
|
||||
// with a PersonToken registered to its script hash.
|
||||
// with a Vita registered to its script hash.
|
||||
}
|
||||
|
||||
// TestPersonToken_RequireCoreRole tests the requireCoreRole method.
|
||||
// TestVita_RequireCoreRole tests the requireCoreRole method.
|
||||
// Note: requireCoreRole uses GetCallingScriptHash() - designed for cross-contract authorization.
|
||||
func TestPersonToken_RequireCoreRole(t *testing.T) {
|
||||
c := newPersonTokenClient(t)
|
||||
func TestVita_RequireCoreRole(t *testing.T) {
|
||||
c := newVitaClient(t)
|
||||
|
||||
// CoreRole constants
|
||||
const (
|
||||
|
|
@ -137,17 +137,17 @@ func TestPersonToken_RequireCoreRole(t *testing.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)
|
||||
invoker.InvokeFail(t, "caller does not have a Vita", "requireCoreRole", CoreRoleNone)
|
||||
})
|
||||
|
||||
// Note: Testing requireCoreRole with actual role checks requires a deployed contract
|
||||
// with a PersonToken registered to its script hash.
|
||||
// with a Vita registered to its script hash.
|
||||
}
|
||||
|
||||
// TestPersonToken_RequirePermission tests the requirePermission method.
|
||||
// TestVita_RequirePermission tests the requirePermission method.
|
||||
// Note: requirePermission uses GetCallingScriptHash() - designed for cross-contract authorization.
|
||||
func TestPersonToken_RequirePermission(t *testing.T) {
|
||||
c := newPersonTokenClient(t)
|
||||
func TestVita_RequirePermission(t *testing.T) {
|
||||
c := newVitaClient(t)
|
||||
|
||||
t.Run("empty resource", func(t *testing.T) {
|
||||
acc := c.Executor.NewAccount(t)
|
||||
|
|
@ -170,16 +170,16 @@ func TestPersonToken_RequirePermission(t *testing.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")
|
||||
invoker.InvokeFail(t, "caller does not have a Vita", "requirePermission", "documents", "read", "global")
|
||||
})
|
||||
|
||||
// Note: Testing requirePermission with actual permission checks requires a deployed contract
|
||||
// with a PersonToken registered to its script hash.
|
||||
// with a Vita registered to its script hash.
|
||||
}
|
||||
|
||||
// TestPersonToken_TotalSupply tests the totalSupply method.
|
||||
func TestPersonToken_TotalSupply(t *testing.T) {
|
||||
c := newPersonTokenClient(t)
|
||||
// TestVita_TotalSupply tests the totalSupply method.
|
||||
func TestVita_TotalSupply(t *testing.T) {
|
||||
c := newVitaClient(t)
|
||||
e := c.Executor
|
||||
|
||||
// Initially, totalSupply should be 0
|
||||
|
|
@ -187,27 +187,27 @@ func TestPersonToken_TotalSupply(t *testing.T) {
|
|||
|
||||
// Register a token
|
||||
acc1 := e.NewAccount(t)
|
||||
registerPersonToken(t, c, acc1)
|
||||
registerVita(t, c, acc1)
|
||||
|
||||
// Now totalSupply should be 1
|
||||
c.Invoke(t, 1, "totalSupply")
|
||||
|
||||
// Register another token
|
||||
acc2 := e.NewAccount(t)
|
||||
registerPersonToken(t, c, acc2)
|
||||
registerVita(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)
|
||||
// TestVita_GetTokenByID tests the getTokenByID method.
|
||||
func TestVita_GetTokenByID(t *testing.T) {
|
||||
c := newVitaClient(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)
|
||||
registerVita(t, c, acc)
|
||||
|
||||
// Token ID 0 should exist (first registered token)
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
|
|
@ -215,7 +215,7 @@ func TestPersonToken_GetTokenByID(t *testing.T) {
|
|||
// 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
|
||||
require.GreaterOrEqual(t, len(arr), 9) // Vita has 9 fields
|
||||
|
||||
// Check owner matches (owner is at index 1)
|
||||
owner, ok := arr[1].Value().([]byte)
|
||||
|
|
@ -231,13 +231,13 @@ func TestPersonToken_GetTokenByID(t *testing.T) {
|
|||
}, "getTokenByID", 999999)
|
||||
}
|
||||
|
||||
// TestPersonToken_SuspendReinstate tests suspend and reinstate functionality.
|
||||
func TestPersonToken_SuspendReinstate(t *testing.T) {
|
||||
c := newPersonTokenClient(t)
|
||||
// TestVita_SuspendReinstate tests suspend and reinstate functionality.
|
||||
func TestVita_SuspendReinstate(t *testing.T) {
|
||||
c := newVitaClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
registerPersonToken(t, c, acc)
|
||||
registerVita(t, c, acc)
|
||||
|
||||
invoker := c.WithSigners(acc)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
|
@ -285,6 +285,6 @@ func TestPersonToken_SuspendReinstate(t *testing.T) {
|
|||
|
||||
// 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
|
||||
// 1. Has a Vita registered to its script hash
|
||||
// 2. Calls the Vita cross-contract methods from within its own methods
|
||||
// This is the intended usage pattern for these cross-contract authorization methods.
|
||||
|
|
@ -15,12 +15,12 @@ func newVTSClient(t *testing.T) *neotest.ContractInvoker {
|
|||
return newNativeClient(t, nativenames.VTS)
|
||||
}
|
||||
|
||||
// registerVita is a helper to register a PersonToken (Vita) for an account.
|
||||
// registerVitaForVTS is a helper to register a Vita for an account.
|
||||
// This is required for accounts receiving restricted VTS.
|
||||
// Uses the existing executor to ensure we're on the same blockchain.
|
||||
func registerVita(t *testing.T, e *neotest.Executor, acc neotest.Signer) {
|
||||
personTokenHash := e.NativeHash(t, nativenames.PersonToken)
|
||||
c := e.NewInvoker(personTokenHash, acc)
|
||||
func registerVitaForVTS(t *testing.T, e *neotest.Executor, acc neotest.Signer) {
|
||||
vitaHash := e.NativeHash(t, nativenames.Vita)
|
||||
c := e.NewInvoker(vitaHash, acc)
|
||||
owner := acc.ScriptHash()
|
||||
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
|
||||
isEntity := false
|
||||
|
|
@ -37,8 +37,8 @@ func registerVita(t *testing.T, e *neotest.Executor, acc neotest.Signer) {
|
|||
// This is required for spending at age-restricted vendors.
|
||||
// Uses the existing executor to ensure we're on the same blockchain.
|
||||
func addAgeVerifiedAttribute(t *testing.T, e *neotest.Executor, committee neotest.Signer, acc neotest.Signer) {
|
||||
personTokenHash := e.NativeHash(t, nativenames.PersonToken)
|
||||
c := e.CommitteeInvoker(personTokenHash)
|
||||
vitaHash := e.NativeHash(t, nativenames.Vita)
|
||||
c := e.CommitteeInvoker(vitaHash)
|
||||
owner := acc.ScriptHash()
|
||||
|
||||
// Get the token ID from getToken result
|
||||
|
|
@ -126,7 +126,7 @@ func TestVTS_MintRestricted(t *testing.T) {
|
|||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
// Restricted VTS requires recipient to have a Vita
|
||||
registerVita(t, e, acc)
|
||||
registerVitaForVTS(t, e, acc)
|
||||
|
||||
t.Run("mint food-restricted VTS", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
|
||||
|
|
@ -334,7 +334,7 @@ func TestVTS_ConvertToUnrestricted(t *testing.T) {
|
|||
userInvoker := c.WithSigners(acc)
|
||||
|
||||
// Restricted VTS requires Vita
|
||||
registerVita(t, e, acc)
|
||||
registerVitaForVTS(t, e, acc)
|
||||
|
||||
// Mint restricted VTS
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
|
||||
|
|
@ -460,7 +460,7 @@ func TestVTS_BalanceDetails(t *testing.T) {
|
|||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
// Restricted VTS requires Vita
|
||||
registerVita(t, e, acc)
|
||||
registerVitaForVTS(t, e, acc)
|
||||
|
||||
// Mint mixed VTS
|
||||
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 100_00000000)
|
||||
|
|
@ -542,7 +542,7 @@ func TestVTS_NegativeAmount(t *testing.T) {
|
|||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
// Restricted VTS requires Vita
|
||||
registerVita(t, e, acc)
|
||||
registerVitaForVTS(t, e, acc)
|
||||
|
||||
t.Run("mint negative fails", func(t *testing.T) {
|
||||
committeeInvoker.InvokeFail(t, "amount must be positive", "mint", acc.ScriptHash(), -1)
|
||||
|
|
@ -564,7 +564,7 @@ func TestVTS_MultiCategoryVendor(t *testing.T) {
|
|||
customerInvoker := c.WithSigners(customer)
|
||||
|
||||
// Restricted VTS requires Vita
|
||||
registerVita(t, e, customer)
|
||||
registerVitaForVTS(t, e, customer)
|
||||
|
||||
// Register vendor accepting food AND shelter
|
||||
categories := state.CategoryFood | state.CategoryShelter
|
||||
|
|
@ -605,8 +605,8 @@ func TestVTS_AgeRestrictedVendor(t *testing.T) {
|
|||
minorInvoker := c.WithSigners(minorCustomer)
|
||||
|
||||
// Register Vita for both customers
|
||||
registerVita(t, e, adultCustomer)
|
||||
registerVita(t, e, minorCustomer)
|
||||
registerVitaForVTS(t, e, adultCustomer)
|
||||
registerVitaForVTS(t, e, minorCustomer)
|
||||
|
||||
// Only adult has age_verified attribute
|
||||
addAgeVerifiedAttribute(t, e, c.Committee, adultCustomer)
|
||||
|
|
@ -645,10 +645,10 @@ func TestVTS_MintRestrictedRequiresVita(t *testing.T) {
|
|||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
// Only register Vita for one account
|
||||
registerVita(t, e, accWithVita)
|
||||
registerVitaForVTS(t, e, accWithVita)
|
||||
|
||||
t.Run("cannot mint restricted VTS to account without Vita", func(t *testing.T) {
|
||||
committeeInvoker.InvokeFail(t, "restricted VTS requires Vita (PersonToken)", "mintRestricted", accWithoutVita.ScriptHash(), 100_00000000, state.CategoryFood)
|
||||
committeeInvoker.InvokeFail(t, "restricted VTS requires Vita", "mintRestricted", accWithoutVita.ScriptHash(), 100_00000000, state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("can mint restricted VTS to account with Vita", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@ var (
|
|||
Notary = util.Uint160{0x3b, 0xec, 0x35, 0x31, 0x11, 0x9b, 0xba, 0xd7, 0x6d, 0xd0, 0x44, 0x92, 0xb, 0xd, 0xe6, 0xc3, 0x19, 0x4f, 0xe1, 0xc1}
|
||||
// Treasury is a hash of native Treasury contract.
|
||||
Treasury = util.Uint160{0xc1, 0x3a, 0x56, 0xc9, 0x83, 0x53, 0xa7, 0xea, 0x6a, 0x32, 0x4d, 0x9a, 0x83, 0x5d, 0x1b, 0x5b, 0xf2, 0x26, 0x63, 0x15}
|
||||
// PersonToken is a hash of native PersonToken contract.
|
||||
PersonToken = util.Uint160{0x4, 0xaf, 0x34, 0xf1, 0xde, 0xdb, 0xa4, 0x7a, 0xd4, 0x30, 0xdf, 0xc7, 0x77, 0x1c, 0x26, 0x3a, 0x8a, 0x72, 0xa5, 0x21}
|
||||
// Vita is a hash of native Vita contract.
|
||||
Vita = util.Uint160{0x7d, 0x21, 0xb3, 0xd4, 0x1d, 0x79, 0xd0, 0x82, 0xcb, 0x1a, 0x24, 0xc8, 0xf9, 0xb8, 0xdf, 0x3f, 0x4, 0x7f, 0x43, 0xde}
|
||||
// RoleRegistry is a hash of native RoleRegistry contract.
|
||||
RoleRegistry = util.Uint160{0xa9, 0x77, 0x74, 0xdc, 0x77, 0xc5, 0xcc, 0xf8, 0x1a, 0xd4, 0x90, 0xb5, 0x81, 0xb5, 0xf0, 0xc6, 0x61, 0x1, 0x20, 0x52}
|
||||
// VTS is a hash of native VTS contract.
|
||||
VTS = util.Uint160{0x68, 0x34, 0x5e, 0x82, 0x6d, 0x8a, 0xff, 0x41, 0x48, 0x23, 0x60, 0xd9, 0x83, 0xa3, 0xd0, 0xf9, 0xb7, 0x59, 0x36, 0x89}
|
||||
// Federation is a hash of native Federation contract.
|
||||
Federation = util.Uint160{0xfd, 0x78, 0x70, 0xb, 0xa9, 0x19, 0xed, 0xb0, 0x19, 0x40, 0xdd, 0xc7, 0x97, 0x48, 0xf4, 0x25, 0x1d, 0xb0, 0x5, 0x1f}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ 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
|
||||
// Vita is an ID of native Vita contract.
|
||||
Vita int32 = -12
|
||||
// RoleRegistry is an ID of native RoleRegistry contract.
|
||||
RoleRegistry int32 = -13
|
||||
// VTS is an ID of native VTS contract.
|
||||
VTS int32 = -14
|
||||
// Federation is an ID of native Federation contract.
|
||||
Federation int32 = -15
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ const (
|
|||
CryptoLib = "CryptoLib"
|
||||
StdLib = "StdLib"
|
||||
Treasury = "Treasury"
|
||||
PersonToken = "PersonToken"
|
||||
Vita = "Vita"
|
||||
RoleRegistry = "RoleRegistry"
|
||||
VTS = "VTS"
|
||||
Federation = "Federation"
|
||||
)
|
||||
|
||||
// All contains the list of all native contract names ordered by the contract ID.
|
||||
|
|
@ -31,9 +32,10 @@ var All = []string{
|
|||
Oracle,
|
||||
Notary,
|
||||
Treasury,
|
||||
PersonToken,
|
||||
Vita,
|
||||
RoleRegistry,
|
||||
VTS,
|
||||
Federation,
|
||||
}
|
||||
|
||||
// IsValid checks if the name is a valid native contract's name.
|
||||
|
|
@ -49,7 +51,8 @@ func IsValid(name string) bool {
|
|||
name == CryptoLib ||
|
||||
name == StdLib ||
|
||||
name == Treasury ||
|
||||
name == PersonToken ||
|
||||
name == Vita ||
|
||||
name == RoleRegistry ||
|
||||
name == VTS
|
||||
name == VTS ||
|
||||
name == Federation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
)
|
||||
|
||||
// RoleRegistry is a native contract for hierarchical role-based access control.
|
||||
// It replaces NEO.CheckCommittee() with democratic governance based on Vita (PersonToken).
|
||||
// It replaces NEO.CheckCommittee() with democratic governance based on Vita.
|
||||
type RoleRegistry struct {
|
||||
interop.ContractMD
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ type RoleRegistryCache struct {
|
|||
const (
|
||||
// RoleCommittee is the Tutus Committee role (designated officials).
|
||||
RoleCommittee uint64 = 1
|
||||
// RoleRegistrar can register new PersonTokens.
|
||||
// RoleRegistrar can register new Vitas.
|
||||
RoleRegistrar uint64 = 2
|
||||
// RoleAttestor can attest identity attributes.
|
||||
RoleAttestor uint64 = 3
|
||||
|
|
@ -269,7 +269,7 @@ func (r *RoleRegistry) Initialize(ic *interop.Context, hf *config.Hardfork, newM
|
|||
description string
|
||||
}{
|
||||
{RoleCommittee, "COMMITTEE", "Tutus Committee member (designated officials)"},
|
||||
{RoleRegistrar, "REGISTRAR", "Can register new PersonTokens"},
|
||||
{RoleRegistrar, "REGISTRAR", "Can register new Vitas"},
|
||||
{RoleAttestor, "ATTESTOR", "Can attest identity attributes"},
|
||||
{RoleOperator, "OPERATOR", "System operator (node management)"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"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"
|
||||
|
|
@ -9,6 +11,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
|
@ -18,6 +21,11 @@ type Treasury struct {
|
|||
NEO INEO
|
||||
}
|
||||
|
||||
// Storage key prefixes for Treasury.
|
||||
const (
|
||||
prefixTreasuryDeficit byte = 0x01 // -> *big.Int (cumulative deficit from fee subsidies)
|
||||
)
|
||||
|
||||
var _ interop.Contract = (*Treasury)(nil)
|
||||
|
||||
func newTreasury() *Treasury {
|
||||
|
|
@ -105,3 +113,36 @@ func (t *Treasury) onNEP17Payment(ic *interop.Context, args []stackitem.Item) st
|
|||
var _, _, _ = from, amount, data
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
// Public methods for cross-native access
|
||||
|
||||
// Address returns the contract's script hash.
|
||||
func (t *Treasury) Address() util.Uint160 {
|
||||
return t.Hash
|
||||
}
|
||||
|
||||
// Storage key helpers
|
||||
|
||||
func makeDeficitKey() []byte {
|
||||
return []byte{prefixTreasuryDeficit}
|
||||
}
|
||||
|
||||
// AddDeficit adds to the cumulative deficit (for cross-native access).
|
||||
// This is called when Treasury cannot cover fee subsidies for Vita holders.
|
||||
func (t *Treasury) AddDeficit(d *dao.Simple, amount *big.Int) {
|
||||
if amount.Sign() <= 0 {
|
||||
return
|
||||
}
|
||||
current := t.GetDeficit(d)
|
||||
newDeficit := new(big.Int).Add(current, amount)
|
||||
d.PutStorageItem(t.ID, makeDeficitKey(), newDeficit.Bytes())
|
||||
}
|
||||
|
||||
// GetDeficit returns the current cumulative deficit (for cross-native access).
|
||||
func (t *Treasury) GetDeficit(d *dao.Simple) *big.Int {
|
||||
si := d.GetStorageItem(t.ID, makeDeficitKey())
|
||||
if si == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
return new(big.Int).SetBytes(si)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -41,7 +41,7 @@ type VTS struct {
|
|||
|
||||
NEO INEO
|
||||
RoleRegistry IRoleRegistry
|
||||
PersonToken IPersonToken
|
||||
Vita IVita
|
||||
|
||||
symbol string
|
||||
decimals int64
|
||||
|
|
@ -646,10 +646,10 @@ func (v *VTS) mintRestricted(ic *interop.Context, args []stackitem.Item) stackit
|
|||
panic("use mint() for unrestricted tokens")
|
||||
}
|
||||
|
||||
// Restricted VTS requires recipient to have a Vita (PersonToken)
|
||||
// Restricted VTS requires recipient to have a Vita
|
||||
// This ensures benefits go to verified identities
|
||||
if !v.PersonToken.TokenExists(ic.DAO, to) {
|
||||
panic("restricted VTS requires Vita (PersonToken)")
|
||||
if !v.Vita.TokenExists(ic.DAO, to) {
|
||||
panic("restricted VTS requires Vita")
|
||||
}
|
||||
|
||||
v.mintRestrictedInternal(ic, to, amount, category)
|
||||
|
|
@ -948,7 +948,7 @@ func (v *VTS) spend(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|||
|
||||
// Check age verification for age-restricted vendors (e.g., alcohol, tobacco)
|
||||
if vendorInfo.AgeRestricted {
|
||||
if !v.PersonToken.IsAdultVerified(ic.DAO, from) {
|
||||
if !v.Vita.IsAdultVerified(ic.DAO, from) {
|
||||
panic("age verification required for this vendor")
|
||||
}
|
||||
}
|
||||
|
|
@ -1027,7 +1027,7 @@ func (v *VTS) canSpendAt(ic *interop.Context, args []stackitem.Item) stackitem.I
|
|||
|
||||
// Check age verification for age-restricted vendors
|
||||
if vendorInfo.AgeRestricted {
|
||||
if !v.PersonToken.IsAdultVerified(ic.DAO, account) {
|
||||
if !v.Vita.IsAdultVerified(ic.DAO, account) {
|
||||
return stackitem.NewBool(false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,9 +102,9 @@ func (r *Role) FromStackItem(item stackitem.Item) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// RoleAssignment represents a role granted to a PersonToken holder.
|
||||
// RoleAssignment represents a role granted to a Vita holder.
|
||||
type RoleAssignment struct {
|
||||
TokenID uint64 // PersonToken ID (or script hash as uint64 for address-based)
|
||||
TokenID uint64 // Vita ID (or script hash as uint64 for address-based)
|
||||
RoleID uint64 // Role ID
|
||||
GrantedAt uint32 // Block height when granted
|
||||
GrantedBy util.Uint160 // Granter's script hash
|
||||
|
|
@ -250,7 +250,7 @@ func (p *PermissionGrant) FromStackItem(item stackitem.Item) error {
|
|||
}
|
||||
|
||||
// AddressRoleAssignment represents a role granted directly to an address (script hash).
|
||||
// This is used for bootstrapping when PersonTokens may not exist yet.
|
||||
// This is used for bootstrapping when Vitas may not exist yet.
|
||||
type AddressRoleAssignment struct {
|
||||
Address util.Uint160 // Script hash of the address
|
||||
RoleID uint64 // Role ID
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
// TokenStatus represents the status of a PersonToken.
|
||||
// TokenStatus represents the status of a Vita token.
|
||||
type TokenStatus uint8
|
||||
|
||||
const (
|
||||
|
|
@ -54,8 +54,8 @@ const (
|
|||
RecoveryStatusExpired RecoveryStatus = 4
|
||||
)
|
||||
|
||||
// PersonToken represents a soul-bound identity token.
|
||||
type PersonToken struct {
|
||||
// Vita represents a soul-bound identity token.
|
||||
type Vita struct {
|
||||
TokenID uint64 // Unique sequential identifier
|
||||
Owner util.Uint160 // Owner's script hash
|
||||
PersonHash []byte // Hash of biometric/identity proof
|
||||
|
|
@ -68,7 +68,7 @@ type PersonToken struct {
|
|||
}
|
||||
|
||||
// ToStackItem implements stackitem.Convertible interface.
|
||||
func (t *PersonToken) ToStackItem() (stackitem.Item, error) {
|
||||
func (t *Vita) ToStackItem() (stackitem.Item, error) {
|
||||
return stackitem.NewStruct([]stackitem.Item{
|
||||
stackitem.NewBigInteger(big.NewInt(int64(t.TokenID))),
|
||||
stackitem.NewByteArray(t.Owner.BytesBE()),
|
||||
|
|
@ -83,7 +83,7 @@ func (t *PersonToken) ToStackItem() (stackitem.Item, error) {
|
|||
}
|
||||
|
||||
// FromStackItem implements stackitem.Convertible interface.
|
||||
func (t *PersonToken) FromStackItem(item stackitem.Item) error {
|
||||
func (t *Vita) FromStackItem(item stackitem.Item) error {
|
||||
items, ok := item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
return errors.New("not a struct")
|
||||
|
|
@ -248,7 +248,7 @@ func (a *Attribute) FromStackItem(item stackitem.Item) error {
|
|||
// AuthChallenge represents a passwordless authentication challenge.
|
||||
type AuthChallenge struct {
|
||||
ChallengeID util.Uint256 // Unique challenge identifier
|
||||
TokenID uint64 // Associated PersonToken
|
||||
TokenID uint64 // Associated Vita token
|
||||
Nonce []byte // Random bytes to sign
|
||||
CreatedAt uint32 // Block height when created
|
||||
ExpiresAt uint32 // Block height when expires
|
||||
|
|
@ -26,6 +26,12 @@ func (f NotaryFeer) BlockHeight() uint32 {
|
|||
return f.bc.BlockHeight()
|
||||
}
|
||||
|
||||
// IsVitaFeeExempt implements mempool.Feer interface.
|
||||
// Notary requests are not subject to Vita fee exemption.
|
||||
func (f NotaryFeer) IsVitaFeeExempt(util.Uint160) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NewNotaryFeer returns new NotaryFeer instance.
|
||||
func NewNotaryFeer(bc Ledger) NotaryFeer {
|
||||
return NotaryFeer{
|
||||
|
|
|
|||
Loading…
Reference in New Issue