464 lines
15 KiB
Go
464 lines
15 KiB
Go
package state
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
|
"github.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
|
|
}
|