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 checks if address has permission via roles.
|
||||||
HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool
|
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
|
// 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)
|
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.
|
// NewDefaultContracts returns a new set of default native contracts.
|
||||||
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
mgmt := NewManagement()
|
mgmt := NewManagement()
|
||||||
|
|
@ -319,6 +344,11 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
// Set RoleRegistry on PersonToken for cross-contract integration
|
// Set RoleRegistry on PersonToken for cross-contract integration
|
||||||
personToken.RoleRegistry = roleRegistry
|
personToken.RoleRegistry = roleRegistry
|
||||||
|
|
||||||
|
// Create VTS (Value Transfer System) contract
|
||||||
|
vts := newVTS()
|
||||||
|
vts.NEO = neo
|
||||||
|
vts.RoleRegistry = roleRegistry
|
||||||
|
|
||||||
return []interop.Contract{
|
return []interop.Contract{
|
||||||
mgmt,
|
mgmt,
|
||||||
s,
|
s,
|
||||||
|
|
@ -333,5 +363,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
treasury,
|
treasury,
|
||||||
personToken,
|
personToken,
|
||||||
roleRegistry,
|
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}
|
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 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}
|
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
|
PersonToken int32 = -12
|
||||||
// RoleRegistry is an ID of native RoleRegistry contract.
|
// RoleRegistry is an ID of native RoleRegistry contract.
|
||||||
RoleRegistry int32 = -13
|
RoleRegistry int32 = -13
|
||||||
|
// VTS is an ID of native VTS contract.
|
||||||
|
VTS int32 = -14
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const (
|
||||||
Treasury = "Treasury"
|
Treasury = "Treasury"
|
||||||
PersonToken = "PersonToken"
|
PersonToken = "PersonToken"
|
||||||
RoleRegistry = "RoleRegistry"
|
RoleRegistry = "RoleRegistry"
|
||||||
|
VTS = "VTS"
|
||||||
)
|
)
|
||||||
|
|
||||||
// All contains the list of all native contract names ordered by the contract ID.
|
// All contains the list of all native contract names ordered by the contract ID.
|
||||||
|
|
@ -32,6 +33,7 @@ var All = []string{
|
||||||
Treasury,
|
Treasury,
|
||||||
PersonToken,
|
PersonToken,
|
||||||
RoleRegistry,
|
RoleRegistry,
|
||||||
|
VTS,
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid checks if the name is a valid native contract's name.
|
// IsValid checks if the name is a valid native contract's name.
|
||||||
|
|
@ -48,5 +50,6 @@ func IsValid(name string) bool {
|
||||||
name == StdLib ||
|
name == StdLib ||
|
||||||
name == Treasury ||
|
name == Treasury ||
|
||||||
name == PersonToken ||
|
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