Add VTS (Value Transfer System) native contract
VTS is programmable money with spending restrictions and automatic tax accounting, designed to replace GAS as the primary user-facing currency. Key features: - NEP-17 compliant token (symbol: VTS, decimals: 8) - Spending category restrictions (Food, Shelter, Medical, Education, Transport) - Vendor registration and management - Restricted VTS can only be spent at matching vendor categories - Automatic tax withholding via payWage() - Transaction recording for tax reporting - Tax summary queries (getTransactions, getIncomeForPeriod, getTaxWithheld, getDeductibleExpenses, getTaxSummary) Files added: - pkg/core/native/vts.go: Main VTS contract implementation - pkg/core/state/vts.go: VTS state structures (VTSBalance, Vendor, TaxConfig, etc.) - pkg/core/native/native_test/vts_test.go: Comprehensive test suite Files modified: - pkg/core/native/contract.go: Added IVTS interface and VTS() accessor - pkg/core/native/nativenames/names.go: Added VTS constant - pkg/core/native/nativeids/ids.go: Added VTS ID (-14) - pkg/core/native/nativehashes/hashes.go: Added VTS hash - pkg/core/native/native_test/management_test.go: Updated test fixture 🤖 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
de34f66286
commit
86f5e127c0
|
|
@ -134,6 +134,25 @@ type (
|
|||
// HasPermissionInternal checks if address has permission via roles.
|
||||
HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool
|
||||
}
|
||||
|
||||
// IVTS is an interface required from native VTS contract for
|
||||
// interaction with Blockchain and other native contracts.
|
||||
// VTS is programmable money with spending restrictions and automatic tax accounting.
|
||||
IVTS interface {
|
||||
interop.Contract
|
||||
// BalanceOf returns total VTS balance (unrestricted + all restricted).
|
||||
BalanceOf(d *dao.Simple, acc util.Uint160) *big.Int
|
||||
// UnrestrictedBalanceOf returns only unrestricted VTS balance.
|
||||
UnrestrictedBalanceOf(d *dao.Simple, acc util.Uint160) *big.Int
|
||||
// Mint mints unrestricted VTS to the account.
|
||||
Mint(ic *interop.Context, to util.Uint160, amount *big.Int)
|
||||
// MintRestricted mints VTS with spending category restrictions.
|
||||
MintRestricted(ic *interop.Context, to util.Uint160, amount *big.Int, category uint8)
|
||||
// Burn burns unrestricted VTS from the account.
|
||||
Burn(ic *interop.Context, from util.Uint160, amount *big.Int)
|
||||
// IsVendor returns true if address is a registered active vendor.
|
||||
IsVendor(d *dao.Simple, addr util.Uint160) bool
|
||||
}
|
||||
)
|
||||
|
||||
// Contracts is a convenient wrapper around an arbitrary set of native contracts
|
||||
|
|
@ -261,6 +280,12 @@ func (cs *Contracts) RoleRegistry() IRoleRegistry {
|
|||
return cs.ByName(nativenames.RoleRegistry).(IRoleRegistry)
|
||||
}
|
||||
|
||||
// VTS returns native IVTS contract implementation. It panics if
|
||||
// there's no contract with proper name in cs.
|
||||
func (cs *Contracts) VTS() IVTS {
|
||||
return cs.ByName(nativenames.VTS).(IVTS)
|
||||
}
|
||||
|
||||
// NewDefaultContracts returns a new set of default native contracts.
|
||||
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||
mgmt := NewManagement()
|
||||
|
|
@ -319,6 +344,11 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
|||
// Set RoleRegistry on PersonToken for cross-contract integration
|
||||
personToken.RoleRegistry = roleRegistry
|
||||
|
||||
// Create VTS (Value Transfer System) contract
|
||||
vts := newVTS()
|
||||
vts.NEO = neo
|
||||
vts.RoleRegistry = roleRegistry
|
||||
|
||||
return []interop.Contract{
|
||||
mgmt,
|
||||
s,
|
||||
|
|
@ -333,5 +363,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
|||
treasury,
|
||||
personToken,
|
||||
roleRegistry,
|
||||
vts,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,527 @@
|
|||
package native_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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 newVTSClient(t *testing.T) *neotest.ContractInvoker {
|
||||
return newNativeClient(t, nativenames.VTS)
|
||||
}
|
||||
|
||||
// TestVTS_NEP17Compliance tests basic NEP-17 methods.
|
||||
func TestVTS_NEP17Compliance(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
|
||||
t.Run("symbol", func(t *testing.T) {
|
||||
c.Invoke(t, "VTS", "symbol")
|
||||
})
|
||||
|
||||
t.Run("decimals", func(t *testing.T) {
|
||||
c.Invoke(t, 8, "decimals")
|
||||
})
|
||||
|
||||
t.Run("totalSupply initial", func(t *testing.T) {
|
||||
c.Invoke(t, 0, "totalSupply")
|
||||
})
|
||||
|
||||
t.Run("balanceOf empty account", func(t *testing.T) {
|
||||
acc := c.NewAccount(t)
|
||||
c.Invoke(t, 0, "balanceOf", acc.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_Mint tests minting unrestricted VTS.
|
||||
func TestVTS_Mint(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
userInvoker := c.WithSigners(acc)
|
||||
|
||||
t.Run("non-committee cannot mint", func(t *testing.T) {
|
||||
userInvoker.InvokeFail(t, "caller is not a committee member", "mint", acc.ScriptHash(), 1000_00000000)
|
||||
})
|
||||
|
||||
t.Run("committee can mint", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 1000_00000000)
|
||||
})
|
||||
|
||||
t.Run("balance updated", func(t *testing.T) {
|
||||
c.Invoke(t, 1000_00000000, "balanceOf", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("unrestricted balance updated", func(t *testing.T) {
|
||||
c.Invoke(t, 1000_00000000, "unrestrictedBalanceOf", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("totalSupply updated", func(t *testing.T) {
|
||||
c.Invoke(t, 1000_00000000, "totalSupply")
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_MintRestricted tests minting restricted VTS.
|
||||
func TestVTS_MintRestricted(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
t.Run("mint food-restricted VTS", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("total balance includes restricted", func(t *testing.T) {
|
||||
c.Invoke(t, 500_00000000, "balanceOf", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("unrestricted is still zero", func(t *testing.T) {
|
||||
c.Invoke(t, 0, "unrestrictedBalanceOf", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("restricted balance correct", func(t *testing.T) {
|
||||
c.Invoke(t, 500_00000000, "restrictedBalanceOf", acc.ScriptHash(), state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("other restricted category is zero", func(t *testing.T) {
|
||||
c.Invoke(t, 0, "restrictedBalanceOf", acc.ScriptHash(), state.CategoryShelter)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_Burn tests burning unrestricted VTS.
|
||||
func TestVTS_Burn(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
userInvoker := c.WithSigners(acc)
|
||||
|
||||
// Mint first
|
||||
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 1000_00000000)
|
||||
|
||||
t.Run("non-committee cannot burn", func(t *testing.T) {
|
||||
userInvoker.InvokeFail(t, "caller is not a committee member", "burn", acc.ScriptHash(), 100_00000000)
|
||||
})
|
||||
|
||||
t.Run("committee can burn", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "burn", acc.ScriptHash(), 100_00000000)
|
||||
})
|
||||
|
||||
t.Run("balance reduced", func(t *testing.T) {
|
||||
c.Invoke(t, 900_00000000, "balanceOf", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("totalSupply reduced", func(t *testing.T) {
|
||||
c.Invoke(t, 900_00000000, "totalSupply")
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_Transfer tests transferring unrestricted VTS.
|
||||
func TestVTS_Transfer(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
sender := e.NewAccount(t)
|
||||
receiver := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
senderInvoker := c.WithSigners(sender)
|
||||
|
||||
// Mint to sender
|
||||
committeeInvoker.Invoke(t, true, "mint", sender.ScriptHash(), 1000_00000000)
|
||||
|
||||
t.Run("transfer without signature fails", func(t *testing.T) {
|
||||
// Transfer returns false (not panic) when signature is missing
|
||||
c.Invoke(t, false, "transfer", sender.ScriptHash(), receiver.ScriptHash(), 100_00000000, nil)
|
||||
})
|
||||
|
||||
t.Run("transfer with signature succeeds", func(t *testing.T) {
|
||||
senderInvoker.Invoke(t, true, "transfer", sender.ScriptHash(), receiver.ScriptHash(), 100_00000000, nil)
|
||||
})
|
||||
|
||||
t.Run("sender balance reduced", func(t *testing.T) {
|
||||
c.Invoke(t, 900_00000000, "balanceOf", sender.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("receiver balance increased", func(t *testing.T) {
|
||||
c.Invoke(t, 100_00000000, "balanceOf", receiver.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("cannot transfer more than balance", func(t *testing.T) {
|
||||
senderInvoker.Invoke(t, false, "transfer", sender.ScriptHash(), receiver.ScriptHash(), 10000_00000000, nil)
|
||||
})
|
||||
|
||||
t.Run("cannot transfer negative", func(t *testing.T) {
|
||||
// Transfer returns false (not panic) for negative amounts
|
||||
senderInvoker.Invoke(t, false, "transfer", sender.ScriptHash(), receiver.ScriptHash(), -1, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_VendorRegistration tests vendor management.
|
||||
func TestVTS_VendorRegistration(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
vendorAcc := e.NewAccount(t)
|
||||
userAcc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
userInvoker := c.WithSigners(userAcc)
|
||||
|
||||
t.Run("non-committee cannot register vendor", func(t *testing.T) {
|
||||
userInvoker.InvokeFail(t, "caller is not a committee member", "registerVendor", vendorAcc.ScriptHash(), "Test Store", state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("committee can register vendor", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "registerVendor", vendorAcc.ScriptHash(), "Test Store", state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("isVendor returns true", func(t *testing.T) {
|
||||
c.Invoke(t, true, "isVendor", vendorAcc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("non-vendor returns false", func(t *testing.T) {
|
||||
c.Invoke(t, false, "isVendor", userAcc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("getVendorCategories", func(t *testing.T) {
|
||||
c.Invoke(t, int64(state.CategoryFood), "getVendorCategories", vendorAcc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("getVendor returns vendor info", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
arr, ok := stack[0].Value().([]stackitem.Item)
|
||||
require.True(t, ok, "expected array")
|
||||
require.Equal(t, 6, len(arr)) // Vendor struct has 6 fields
|
||||
}, "getVendor", vendorAcc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("update vendor categories", func(t *testing.T) {
|
||||
newCategories := state.CategoryFood | state.CategoryShelter
|
||||
committeeInvoker.Invoke(t, true, "updateVendor", vendorAcc.ScriptHash(), "Test Store Updated", newCategories)
|
||||
c.Invoke(t, int64(newCategories), "getVendorCategories", vendorAcc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("deactivate vendor", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "deactivateVendor", vendorAcc.ScriptHash())
|
||||
// isVendor should still return true (vendor exists) but Active=false
|
||||
c.Invoke(t, false, "isVendor", vendorAcc.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_Spend tests spending at vendors.
|
||||
func TestVTS_Spend(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
customer := e.NewAccount(t)
|
||||
foodVendor := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
customerInvoker := c.WithSigners(customer)
|
||||
|
||||
// Setup: Register food vendor and mint to customer
|
||||
committeeInvoker.Invoke(t, true, "registerVendor", foodVendor.ScriptHash(), "Food Store", state.CategoryFood)
|
||||
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 300_00000000)
|
||||
|
||||
t.Run("customer can spend at vendor", func(t *testing.T) {
|
||||
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), foodVendor.ScriptHash(), 50_00000000, nil)
|
||||
})
|
||||
|
||||
t.Run("vendor received VTS", func(t *testing.T) {
|
||||
c.Invoke(t, 50_00000000, "balanceOf", foodVendor.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("customer balance reduced", func(t *testing.T) {
|
||||
c.Invoke(t, 250_00000000, "balanceOf", customer.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("cannot spend at non-vendor", func(t *testing.T) {
|
||||
nonVendor := e.NewAccount(t)
|
||||
customerInvoker.InvokeFail(t, "invalid or inactive vendor", "spend", customer.ScriptHash(), nonVendor.ScriptHash(), 10_00000000, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_CanSpendAt tests the canSpendAt query method.
|
||||
func TestVTS_CanSpendAt(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
customer := e.NewAccount(t)
|
||||
vendor := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
// Setup
|
||||
committeeInvoker.Invoke(t, true, "registerVendor", vendor.ScriptHash(), "Store", state.CategoryFood)
|
||||
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 100_00000000)
|
||||
|
||||
t.Run("can spend within balance", func(t *testing.T) {
|
||||
c.Invoke(t, true, "canSpendAt", customer.ScriptHash(), vendor.ScriptHash(), 50_00000000)
|
||||
})
|
||||
|
||||
t.Run("cannot spend more than balance", func(t *testing.T) {
|
||||
c.Invoke(t, false, "canSpendAt", customer.ScriptHash(), vendor.ScriptHash(), 200_00000000)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_ConvertToUnrestricted tests converting restricted VTS to unrestricted.
|
||||
func TestVTS_ConvertToUnrestricted(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
userInvoker := c.WithSigners(acc)
|
||||
|
||||
// Mint restricted VTS
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
|
||||
|
||||
t.Run("non-committee cannot convert", func(t *testing.T) {
|
||||
userInvoker.InvokeFail(t, "caller is not a committee member", "convertToUnrestricted", acc.ScriptHash(), state.CategoryFood, 100_00000000)
|
||||
})
|
||||
|
||||
t.Run("committee can convert", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "convertToUnrestricted", acc.ScriptHash(), state.CategoryFood, 100_00000000)
|
||||
})
|
||||
|
||||
t.Run("restricted balance reduced", func(t *testing.T) {
|
||||
c.Invoke(t, 400_00000000, "restrictedBalanceOf", acc.ScriptHash(), state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("unrestricted balance increased", func(t *testing.T) {
|
||||
c.Invoke(t, 100_00000000, "unrestrictedBalanceOf", acc.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("total balance unchanged", func(t *testing.T) {
|
||||
c.Invoke(t, 500_00000000, "balanceOf", acc.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_TaxConfig tests tax configuration.
|
||||
func TestVTS_TaxConfig(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
treasuryAcc := e.NewAccount(t)
|
||||
userAcc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
userInvoker := c.WithSigners(userAcc)
|
||||
|
||||
t.Run("non-committee cannot set tax config", func(t *testing.T) {
|
||||
userInvoker.InvokeFail(t, "caller is not a committee member", "setTaxConfig", 2500, 500, treasuryAcc.ScriptHash(), state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("committee can set tax config", func(t *testing.T) {
|
||||
// 25% income tax, 5% sales tax
|
||||
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 500, treasuryAcc.ScriptHash(), state.CategoryFood)
|
||||
})
|
||||
|
||||
t.Run("get tax config", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
arr, ok := stack[0].Value().([]stackitem.Item)
|
||||
require.True(t, ok, "expected array")
|
||||
require.Equal(t, 4, len(arr)) // TaxConfig has 4 fields
|
||||
|
||||
incomeRate, _ := arr[0].TryInteger()
|
||||
require.Equal(t, int64(2500), incomeRate.Int64())
|
||||
}, "getTaxConfig")
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_PayWage tests payroll with automatic tax withholding.
|
||||
func TestVTS_PayWage(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
employer := e.NewAccount(t)
|
||||
employee := e.NewAccount(t)
|
||||
treasury := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
employerInvoker := c.WithSigners(employer)
|
||||
|
||||
// Setup: Set tax config and fund employer
|
||||
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0) // 25% income tax
|
||||
committeeInvoker.Invoke(t, true, "mint", employer.ScriptHash(), 10000_00000000)
|
||||
|
||||
t.Run("payWage with tax withholding", func(t *testing.T) {
|
||||
// Pay $5000 gross, 25% tax rate
|
||||
employerInvoker.Invoke(t, true, "payWage", employer.ScriptHash(), employee.ScriptHash(), 5000_00000000, 2500)
|
||||
})
|
||||
|
||||
t.Run("employer balance reduced by gross", func(t *testing.T) {
|
||||
c.Invoke(t, 5000_00000000, "balanceOf", employer.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("employee receives net (75%)", func(t *testing.T) {
|
||||
c.Invoke(t, 3750_00000000, "balanceOf", employee.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("treasury receives tax (25%)", func(t *testing.T) {
|
||||
c.Invoke(t, 1250_00000000, "balanceOf", treasury.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_TaxRefund tests issuing tax refunds.
|
||||
func TestVTS_TaxRefund(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
treasury := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
userInvoker := c.WithSigners(acc)
|
||||
|
||||
// Setup: Set tax config
|
||||
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0)
|
||||
|
||||
t.Run("non-committee cannot issue refund", func(t *testing.T) {
|
||||
userInvoker.InvokeFail(t, "caller is not a committee member", "issueTaxRefund", acc.ScriptHash(), 100_00000000)
|
||||
})
|
||||
|
||||
t.Run("committee can issue refund", func(t *testing.T) {
|
||||
committeeInvoker.Invoke(t, true, "issueTaxRefund", acc.ScriptHash(), 100_00000000)
|
||||
})
|
||||
|
||||
t.Run("refund credited to account", func(t *testing.T) {
|
||||
c.Invoke(t, 100_00000000, "balanceOf", acc.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_BalanceDetails tests the balanceDetails query.
|
||||
func TestVTS_BalanceDetails(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
// Mint mixed VTS
|
||||
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 100_00000000)
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 200_00000000, state.CategoryFood)
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 300_00000000, state.CategoryShelter)
|
||||
|
||||
t.Run("balanceDetails returns all categories", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
arr, ok := stack[0].Value().([]stackitem.Item)
|
||||
require.True(t, ok, "expected array")
|
||||
// Should have unrestricted + categories with balances
|
||||
require.GreaterOrEqual(t, len(arr), 1)
|
||||
}, "balanceDetails", acc.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_TaxReportingMethods tests the tax reporting query methods.
|
||||
func TestVTS_TaxReportingMethods(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
|
||||
// Block range for queries
|
||||
startBlock := int64(0)
|
||||
endBlock := int64(1000)
|
||||
|
||||
t.Run("getTransactions returns array", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
_, ok := stack[0].Value().([]stackitem.Item)
|
||||
require.True(t, ok, "expected array")
|
||||
}, "getTransactions", acc.ScriptHash(), startBlock, endBlock)
|
||||
})
|
||||
|
||||
t.Run("getIncomeForPeriod returns integer", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
_, err := stack[0].TryInteger()
|
||||
require.NoError(t, err)
|
||||
}, "getIncomeForPeriod", acc.ScriptHash(), startBlock, endBlock)
|
||||
})
|
||||
|
||||
t.Run("getTaxWithheld returns integer", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
_, err := stack[0].TryInteger()
|
||||
require.NoError(t, err)
|
||||
}, "getTaxWithheld", acc.ScriptHash(), startBlock, endBlock)
|
||||
})
|
||||
|
||||
t.Run("getDeductibleExpenses returns integer", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
_, err := stack[0].TryInteger()
|
||||
require.NoError(t, err)
|
||||
}, "getDeductibleExpenses", acc.ScriptHash(), startBlock, endBlock, 0)
|
||||
})
|
||||
|
||||
t.Run("getTaxSummary returns array", func(t *testing.T) {
|
||||
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||
require.Equal(t, 1, len(stack))
|
||||
arr, ok := stack[0].Value().([]stackitem.Item)
|
||||
require.True(t, ok, "expected array for tax summary")
|
||||
// TaxSummary has 10 fields: Account, StartBlock, EndBlock, TotalIncome,
|
||||
// TotalBenefits, TotalExpenses, DeductibleExpenses, TaxWithheld, EstimatedOwed, Balance
|
||||
require.Equal(t, 10, len(arr))
|
||||
}, "getTaxSummary", acc.ScriptHash(), startBlock, endBlock)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_NegativeAmount tests that negative amounts are rejected.
|
||||
func TestVTS_NegativeAmount(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
acc := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
|
||||
t.Run("mint negative fails", func(t *testing.T) {
|
||||
committeeInvoker.InvokeFail(t, "amount must be positive", "mint", acc.ScriptHash(), -1)
|
||||
})
|
||||
|
||||
t.Run("mintRestricted negative fails", func(t *testing.T) {
|
||||
committeeInvoker.InvokeFail(t, "amount must be positive", "mintRestricted", acc.ScriptHash(), -1, state.CategoryFood)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVTS_MultiCategoryVendor tests vendors accepting multiple categories.
|
||||
func TestVTS_MultiCategoryVendor(t *testing.T) {
|
||||
c := newVTSClient(t)
|
||||
e := c.Executor
|
||||
|
||||
customer := e.NewAccount(t)
|
||||
generalStore := e.NewAccount(t)
|
||||
committeeInvoker := c.WithSigners(c.Committee)
|
||||
customerInvoker := c.WithSigners(customer)
|
||||
|
||||
// Register vendor accepting food AND shelter
|
||||
categories := state.CategoryFood | state.CategoryShelter
|
||||
committeeInvoker.Invoke(t, true, "registerVendor", generalStore.ScriptHash(), "General Store", categories)
|
||||
|
||||
// Mint restricted VTS for different categories
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", customer.ScriptHash(), 100_00000000, state.CategoryFood)
|
||||
committeeInvoker.Invoke(t, true, "mintRestricted", customer.ScriptHash(), 100_00000000, state.CategoryShelter)
|
||||
|
||||
t.Run("can spend at multi-category vendor", func(t *testing.T) {
|
||||
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), generalStore.ScriptHash(), 50_00000000, nil)
|
||||
// Total balance should be reduced by 50
|
||||
c.Invoke(t, 150_00000000, "balanceOf", customer.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("can spend more at same vendor", func(t *testing.T) {
|
||||
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), generalStore.ScriptHash(), 50_00000000, nil)
|
||||
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), generalStore.ScriptHash(), 50_00000000, nil)
|
||||
// Total balance should be reduced by 150
|
||||
c.Invoke(t, 50_00000000, "balanceOf", customer.ScriptHash())
|
||||
})
|
||||
|
||||
t.Run("vendor has correct total", func(t *testing.T) {
|
||||
c.Invoke(t, 150_00000000, "balanceOf", generalStore.ScriptHash())
|
||||
})
|
||||
}
|
||||
|
|
@ -31,4 +31,10 @@ 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}
|
||||
// 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}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,4 +33,6 @@ const (
|
|||
PersonToken int32 = -12
|
||||
// RoleRegistry is an ID of native RoleRegistry contract.
|
||||
RoleRegistry int32 = -13
|
||||
// VTS is an ID of native VTS contract.
|
||||
VTS int32 = -14
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const (
|
|||
Treasury = "Treasury"
|
||||
PersonToken = "PersonToken"
|
||||
RoleRegistry = "RoleRegistry"
|
||||
VTS = "VTS"
|
||||
)
|
||||
|
||||
// All contains the list of all native contract names ordered by the contract ID.
|
||||
|
|
@ -32,6 +33,7 @@ var All = []string{
|
|||
Treasury,
|
||||
PersonToken,
|
||||
RoleRegistry,
|
||||
VTS,
|
||||
}
|
||||
|
||||
// IsValid checks if the name is a valid native contract's name.
|
||||
|
|
@ -48,5 +50,6 @@ func IsValid(name string) bool {
|
|||
name == StdLib ||
|
||||
name == Treasury ||
|
||||
name == PersonToken ||
|
||||
name == RoleRegistry
|
||||
name == RoleRegistry ||
|
||||
name == VTS
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,452 @@
|
|||
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
|
||||
}
|
||||
|
||||
// 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),
|
||||
}), 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
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue