tutus-chain/pkg/core/state/vts.go

464 lines
14 KiB
Go

package state
import (
"errors"
"fmt"
"math/big"
"git.marketally.com/tutus-one/tutus-chain/pkg/util"
"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// Spending category constants (bitmask).
const (
CategoryUnrestricted uint8 = 0 // No restrictions (standard money)
CategoryFood uint8 = 1 << 0 // Bit 0: Food purchases
CategoryShelter uint8 = 1 << 1 // Bit 1: Rent/utilities/housing
CategoryMedical uint8 = 1 << 2 // Bit 2: Healthcare expenses
CategoryEducation uint8 = 1 << 3 // Bit 3: Tuition/books/supplies
CategoryTransport uint8 = 1 << 4 // Bit 4: Transportation
CategoryAll uint8 = 0xFF // All categories
)
// TxType represents the type of VTS transaction.
type TxType uint8
const (
TxTypeTransfer TxType = 0 // P2P transfer (neutral)
TxTypeIncome TxType = 1 // Wages, sales, dividends (taxable)
TxTypeExpense TxType = 2 // Purchases, bills (potentially deductible)
TxTypeBenefit TxType = 3 // Government benefits (tax-exempt income)
TxTypeGift TxType = 4 // Gift (may have gift tax implications)
TxTypeTax TxType = 5 // Tax payment/withholding
)
// VTSBalance represents the balance state of a VTS token holder.
// It tracks both unrestricted and category-restricted balances.
type VTSBalance struct {
Unrestricted big.Int // Unrestricted balance (can be spent anywhere)
Restricted map[uint8]*big.Int // Category -> restricted balance
}
// NewVTSBalance creates a new empty VTSBalance.
func NewVTSBalance() *VTSBalance {
return &VTSBalance{
Restricted: make(map[uint8]*big.Int),
}
}
// Total returns the total balance (unrestricted + all restricted).
func (b *VTSBalance) Total() *big.Int {
total := new(big.Int).Set(&b.Unrestricted)
for _, amt := range b.Restricted {
total.Add(total, amt)
}
return total
}
// ToStackItem converts VTSBalance to stackitem.
func (b *VTSBalance) ToStackItem() (stackitem.Item, error) {
// Format: [unrestricted, [[category, amount], [category, amount], ...]]
restrictedItems := make([]stackitem.Item, 0, len(b.Restricted))
for cat, amt := range b.Restricted {
if amt.Sign() > 0 {
restrictedItems = append(restrictedItems, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(cat))),
stackitem.NewBigInteger(amt),
}))
}
}
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(&b.Unrestricted),
stackitem.NewArray(restrictedItems),
}), nil
}
// FromStackItem converts stackitem to VTSBalance.
func (b *VTSBalance) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 2 {
return errors.New("invalid VTSBalance structure")
}
unrestricted, err := structItems[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid unrestricted balance: %w", err)
}
b.Unrestricted = *unrestricted
restrictedArr, ok := structItems[1].Value().([]stackitem.Item)
if !ok {
return errors.New("invalid restricted balances array")
}
b.Restricted = make(map[uint8]*big.Int)
for _, ri := range restrictedArr {
pair, ok := ri.Value().([]stackitem.Item)
if !ok || len(pair) != 2 {
return errors.New("invalid restricted balance pair")
}
catBI, err := pair[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid category: %w", err)
}
amt, err := pair[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid amount: %w", err)
}
b.Restricted[uint8(catBI.Int64())] = amt
}
return nil
}
// Vendor represents a registered vendor/merchant that can accept VTS payments.
type Vendor struct {
Address util.Uint160 // Vendor's script hash
Name string // Display name (max 64 chars)
Categories uint8 // Bitmask of accepted spending categories
RegisteredAt uint32 // Block height when registered
RegisteredBy util.Uint160 // Who registered this vendor
Active bool // Whether vendor is currently active
AgeRestricted bool // Whether vendor requires age verification (e.g., alcohol, tobacco)
}
// ToStackItem converts Vendor to stackitem.
func (v *Vendor) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(v.Address.BytesBE()),
stackitem.NewByteArray([]byte(v.Name)),
stackitem.NewBigInteger(big.NewInt(int64(v.Categories))),
stackitem.NewBigInteger(big.NewInt(int64(v.RegisteredAt))),
stackitem.NewByteArray(v.RegisteredBy.BytesBE()),
stackitem.NewBool(v.Active),
stackitem.NewBool(v.AgeRestricted),
}), nil
}
// FromStackItem converts stackitem to Vendor.
func (v *Vendor) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 6 {
return errors.New("invalid Vendor structure")
}
addrBytes, err := structItems[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid address: %w", err)
}
v.Address, err = util.Uint160DecodeBytesBE(addrBytes)
if err != nil {
return fmt.Errorf("invalid address bytes: %w", err)
}
nameBytes, err := structItems[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid name: %w", err)
}
v.Name = string(nameBytes)
catBI, err := structItems[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid categories: %w", err)
}
v.Categories = uint8(catBI.Int64())
regAtBI, err := structItems[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid registeredAt: %w", err)
}
v.RegisteredAt = uint32(regAtBI.Int64())
regByBytes, err := structItems[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid registeredBy: %w", err)
}
v.RegisteredBy, err = util.Uint160DecodeBytesBE(regByBytes)
if err != nil {
return fmt.Errorf("invalid registeredBy bytes: %w", err)
}
active, err := structItems[5].TryBool()
if err != nil {
return fmt.Errorf("invalid active: %w", err)
}
v.Active = active
// AgeRestricted is optional for backwards compatibility
if len(structItems) >= 7 {
ageRestricted, err := structItems[6].TryBool()
if err != nil {
return fmt.Errorf("invalid ageRestricted: %w", err)
}
v.AgeRestricted = ageRestricted
}
return nil
}
// TransactionRecord represents a recorded VTS transaction for tax accounting.
type TransactionRecord struct {
TxHash util.Uint256 // Transaction hash
BlockHeight uint32 // When it occurred
From util.Uint160 // Sender
To util.Uint160 // Recipient
Amount big.Int // Gross amount
TxType TxType // Type of transaction
Category uint8 // Spending category (if applicable)
TaxWithheld big.Int // Tax withheld at source
TaxRate uint16 // Rate applied (basis points, e.g., 2500 = 25%)
Memo string // Optional description (max 256 chars)
}
// ToStackItem converts TransactionRecord to stackitem.
func (t *TransactionRecord) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(t.TxHash.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(t.BlockHeight))),
stackitem.NewByteArray(t.From.BytesBE()),
stackitem.NewByteArray(t.To.BytesBE()),
stackitem.NewBigInteger(&t.Amount),
stackitem.NewBigInteger(big.NewInt(int64(t.TxType))),
stackitem.NewBigInteger(big.NewInt(int64(t.Category))),
stackitem.NewBigInteger(&t.TaxWithheld),
stackitem.NewBigInteger(big.NewInt(int64(t.TaxRate))),
stackitem.NewByteArray([]byte(t.Memo)),
}), nil
}
// FromStackItem converts stackitem to TransactionRecord.
func (t *TransactionRecord) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 10 {
return errors.New("invalid TransactionRecord structure")
}
txHashBytes, err := structItems[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid txHash: %w", err)
}
t.TxHash, err = util.Uint256DecodeBytesBE(txHashBytes)
if err != nil {
return fmt.Errorf("invalid txHash bytes: %w", err)
}
blockHeightBI, err := structItems[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid blockHeight: %w", err)
}
t.BlockHeight = uint32(blockHeightBI.Int64())
fromBytes, err := structItems[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid from: %w", err)
}
t.From, err = util.Uint160DecodeBytesBE(fromBytes)
if err != nil {
return fmt.Errorf("invalid from bytes: %w", err)
}
toBytes, err := structItems[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid to: %w", err)
}
t.To, err = util.Uint160DecodeBytesBE(toBytes)
if err != nil {
return fmt.Errorf("invalid to bytes: %w", err)
}
amount, err := structItems[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid amount: %w", err)
}
t.Amount = *amount
txTypeBI, err := structItems[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid txType: %w", err)
}
t.TxType = TxType(txTypeBI.Int64())
catBI, err := structItems[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid category: %w", err)
}
t.Category = uint8(catBI.Int64())
taxWithheld, err := structItems[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid taxWithheld: %w", err)
}
t.TaxWithheld = *taxWithheld
taxRateBI, err := structItems[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid taxRate: %w", err)
}
t.TaxRate = uint16(taxRateBI.Int64())
memoBytes, err := structItems[9].TryBytes()
if err != nil {
return fmt.Errorf("invalid memo: %w", err)
}
t.Memo = string(memoBytes)
return nil
}
// TaxConfig represents the tax configuration for VTS.
type TaxConfig struct {
DefaultIncomeRate uint16 // Default income tax rate (basis points)
DefaultSalesRate uint16 // Default sales tax rate (basis points)
TreasuryAddress util.Uint160 // Where taxes are sent
ExemptCategories uint8 // Categories exempt from sales tax (bitmask)
}
// ToStackItem converts TaxConfig to stackitem.
func (c *TaxConfig) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultIncomeRate))),
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultSalesRate))),
stackitem.NewByteArray(c.TreasuryAddress.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(c.ExemptCategories))),
}), nil
}
// FromStackItem converts stackitem to TaxConfig.
func (c *TaxConfig) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 4 {
return errors.New("invalid TaxConfig structure")
}
incomeRateBI, err := structItems[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid defaultIncomeRate: %w", err)
}
c.DefaultIncomeRate = uint16(incomeRateBI.Int64())
salesRateBI, err := structItems[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid defaultSalesRate: %w", err)
}
c.DefaultSalesRate = uint16(salesRateBI.Int64())
treasuryBytes, err := structItems[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid treasuryAddress: %w", err)
}
c.TreasuryAddress, err = util.Uint160DecodeBytesBE(treasuryBytes)
if err != nil {
return fmt.Errorf("invalid treasuryAddress bytes: %w", err)
}
exemptCatBI, err := structItems[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid exemptCategories: %w", err)
}
c.ExemptCategories = uint8(exemptCatBI.Int64())
return nil
}
// TaxSummary represents a tax summary for a specific period.
type TaxSummary struct {
Account util.Uint160 // Account address
StartBlock uint32 // Period start
EndBlock uint32 // Period end
TotalIncome big.Int // Total taxable income
TotalBenefits big.Int // Total tax-exempt benefits
TotalExpenses big.Int // Total expenses (for deductions)
DeductibleExpenses big.Int // Expenses that are deductible
TaxWithheld big.Int // Total tax withheld
EstimatedOwed big.Int // Estimated tax owed (income * rate)
Balance big.Int // TaxWithheld - EstimatedOwed
}
// ToStackItem converts TaxSummary to stackitem.
func (s *TaxSummary) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(s.Account.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(s.StartBlock))),
stackitem.NewBigInteger(big.NewInt(int64(s.EndBlock))),
stackitem.NewBigInteger(&s.TotalIncome),
stackitem.NewBigInteger(&s.TotalBenefits),
stackitem.NewBigInteger(&s.TotalExpenses),
stackitem.NewBigInteger(&s.DeductibleExpenses),
stackitem.NewBigInteger(&s.TaxWithheld),
stackitem.NewBigInteger(&s.EstimatedOwed),
stackitem.NewBigInteger(&s.Balance),
}), nil
}
// FromStackItem converts stackitem to TaxSummary.
func (s *TaxSummary) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 10 {
return errors.New("invalid TaxSummary structure")
}
accountBytes, err := structItems[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid account: %w", err)
}
s.Account, err = util.Uint160DecodeBytesBE(accountBytes)
if err != nil {
return fmt.Errorf("invalid account bytes: %w", err)
}
startBI, err := structItems[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid startBlock: %w", err)
}
s.StartBlock = uint32(startBI.Int64())
endBI, err := structItems[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid endBlock: %w", err)
}
s.EndBlock = uint32(endBI.Int64())
totalIncome, err := structItems[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalIncome: %w", err)
}
s.TotalIncome = *totalIncome
totalBenefits, err := structItems[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalBenefits: %w", err)
}
s.TotalBenefits = *totalBenefits
totalExpenses, err := structItems[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalExpenses: %w", err)
}
s.TotalExpenses = *totalExpenses
deductible, err := structItems[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid deductibleExpenses: %w", err)
}
s.DeductibleExpenses = *deductible
taxWithheld, err := structItems[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid taxWithheld: %w", err)
}
s.TaxWithheld = *taxWithheld
estimatedOwed, err := structItems[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid estimatedOwed: %w", err)
}
s.EstimatedOwed = *estimatedOwed
balance, err := structItems[9].TryInteger()
if err != nil {
return fmt.Errorf("invalid balance: %w", err)
}
s.Balance = *balance
return nil
}