255 lines
7.4 KiB
Go
Executable File
255 lines
7.4 KiB
Go
Executable File
package tutustest
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/io"
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/util"
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/vm/emit"
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
|
|
"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// CrossContractHelper provides utilities for testing cross-contract calls.
|
|
// Many Tutus contracts use GetCallingScriptHash() for authorization, which
|
|
// requires deploying a helper contract to properly test these paths.
|
|
type CrossContractHelper struct {
|
|
*Executor
|
|
t testing.TB
|
|
}
|
|
|
|
// NewCrossContractHelper creates a helper for cross-contract testing.
|
|
func NewCrossContractHelper(t testing.TB, e *Executor) *CrossContractHelper {
|
|
return &CrossContractHelper{
|
|
Executor: e,
|
|
t: t,
|
|
}
|
|
}
|
|
|
|
// CallFromContract builds a script that calls a target contract method from
|
|
// another contract's context. This is useful for testing GetCallingScriptHash().
|
|
// The script will:
|
|
// 1. Call the target contract with the specified method and args
|
|
// 2. Return the result
|
|
func (c *CrossContractHelper) CallFromContract(
|
|
caller util.Uint160,
|
|
target util.Uint160,
|
|
method string,
|
|
args ...any,
|
|
) []byte {
|
|
w := io.NewBufBinWriter()
|
|
emit.AppCall(w.BinWriter, target, method, callflag.All, args...)
|
|
require.NoError(c.t, w.Err)
|
|
return w.Bytes()
|
|
}
|
|
|
|
// BuildProxyScript creates a script that acts as a proxy contract.
|
|
// It calls the target method and returns the result.
|
|
// Useful for testing authorization checks that require specific callers.
|
|
func (c *CrossContractHelper) BuildProxyScript(
|
|
target util.Uint160,
|
|
method string,
|
|
args ...any,
|
|
) []byte {
|
|
w := io.NewBufBinWriter()
|
|
emit.AppCall(w.BinWriter, target, method, callflag.All, args...)
|
|
require.NoError(c.t, w.Err)
|
|
return w.Bytes()
|
|
}
|
|
|
|
// BuildMultiCallScript creates a script that calls multiple contracts in sequence.
|
|
// Each call's result is collected and returned as an array.
|
|
func (c *CrossContractHelper) BuildMultiCallScript(calls []ContractCall) []byte {
|
|
w := io.NewBufBinWriter()
|
|
|
|
// Call each contract and collect results
|
|
for _, call := range calls {
|
|
emit.AppCall(w.BinWriter, call.Hash, call.Method, callflag.All, call.Args...)
|
|
}
|
|
|
|
// Pack results into array
|
|
emit.Int(w.BinWriter, int64(len(calls)))
|
|
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
|
|
require.NoError(c.t, w.Err)
|
|
return w.Bytes()
|
|
}
|
|
|
|
// ContractCall represents a single contract invocation.
|
|
type ContractCall struct {
|
|
Hash util.Uint160
|
|
Method string
|
|
Args []any
|
|
}
|
|
|
|
// BuildConditionalScript creates a script that calls one method if condition
|
|
// is true, otherwise calls another method.
|
|
func (c *CrossContractHelper) BuildConditionalScript(
|
|
condition bool,
|
|
target util.Uint160,
|
|
trueMethod string,
|
|
trueArgs []any,
|
|
falseMethod string,
|
|
falseArgs []any,
|
|
) []byte {
|
|
w := io.NewBufBinWriter()
|
|
|
|
if condition {
|
|
emit.AppCall(w.BinWriter, target, trueMethod, callflag.All, trueArgs...)
|
|
} else {
|
|
emit.AppCall(w.BinWriter, target, falseMethod, callflag.All, falseArgs...)
|
|
}
|
|
|
|
require.NoError(c.t, w.Err)
|
|
return w.Bytes()
|
|
}
|
|
|
|
// PrepareProxyCall creates a transaction that calls a contract method
|
|
// through a custom script, allowing the test to control the calling context.
|
|
func (c *CrossContractHelper) PrepareProxyCall(
|
|
signers []Signer,
|
|
script []byte,
|
|
) *transaction.Transaction {
|
|
tx := transaction.New(script, 0)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = c.Chain.BlockHeight() + 1
|
|
|
|
// Add signers
|
|
tx.Signers = make([]transaction.Signer, len(signers))
|
|
for i, s := range signers {
|
|
tx.Signers[i] = transaction.Signer{
|
|
Account: s.ScriptHash(),
|
|
Scopes: transaction.Global,
|
|
}
|
|
}
|
|
|
|
// Calculate fees and sign
|
|
AddNetworkFee(c.t, c.Chain, tx, signers...)
|
|
require.NoError(c.t, c.Chain.PoolTx(tx))
|
|
|
|
return tx
|
|
}
|
|
|
|
// InvokeViaProxy invokes a contract method through a proxy script
|
|
// and returns the result stack.
|
|
func (c *CrossContractHelper) InvokeViaProxy(
|
|
signers []Signer,
|
|
target util.Uint160,
|
|
method string,
|
|
args ...any,
|
|
) []stackitem.Item {
|
|
script := c.BuildProxyScript(target, method, args...)
|
|
tx := c.PrepareProxyCall(signers, script)
|
|
|
|
c.AddNewBlock(c.t, tx)
|
|
aer, err := c.Chain.GetAppExecResults(tx.Hash(), 0)
|
|
require.NoError(c.t, err)
|
|
require.Equal(c.t, 1, len(aer))
|
|
|
|
return aer[0].Stack
|
|
}
|
|
|
|
// TestContractBuilder helps build simple test contracts for cross-contract testing.
|
|
type TestContractBuilder struct {
|
|
t testing.TB
|
|
script []byte
|
|
}
|
|
|
|
// NewTestContractBuilder creates a builder for test contracts.
|
|
func NewTestContractBuilder(t testing.TB) *TestContractBuilder {
|
|
return &TestContractBuilder{t: t}
|
|
}
|
|
|
|
// WithCall adds a contract call to the test contract.
|
|
func (b *TestContractBuilder) WithCall(target util.Uint160, method string, args ...any) *TestContractBuilder {
|
|
w := io.NewBufBinWriter()
|
|
if len(b.script) > 0 {
|
|
w.BinWriter.WriteBytes(b.script)
|
|
}
|
|
emit.AppCall(w.BinWriter, target, method, callflag.All, args...)
|
|
require.NoError(b.t, w.Err)
|
|
b.script = w.Bytes()
|
|
return b
|
|
}
|
|
|
|
// WithAssertion adds an assertion that the top of the stack is true.
|
|
func (b *TestContractBuilder) WithAssertion() *TestContractBuilder {
|
|
w := io.NewBufBinWriter()
|
|
if len(b.script) > 0 {
|
|
w.BinWriter.WriteBytes(b.script)
|
|
}
|
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
|
require.NoError(b.t, w.Err)
|
|
b.script = w.Bytes()
|
|
return b
|
|
}
|
|
|
|
// WithDrop drops the top stack item.
|
|
func (b *TestContractBuilder) WithDrop() *TestContractBuilder {
|
|
w := io.NewBufBinWriter()
|
|
if len(b.script) > 0 {
|
|
w.BinWriter.WriteBytes(b.script)
|
|
}
|
|
emit.Opcodes(w.BinWriter, opcode.DROP)
|
|
require.NoError(b.t, w.Err)
|
|
b.script = w.Bytes()
|
|
return b
|
|
}
|
|
|
|
// Build returns the final script.
|
|
func (b *TestContractBuilder) Build() []byte {
|
|
return b.script
|
|
}
|
|
|
|
// AuthorizationTestHelper provides utilities for testing authorization patterns.
|
|
type AuthorizationTestHelper struct {
|
|
*CrossContractHelper
|
|
}
|
|
|
|
// NewAuthorizationTestHelper creates a helper for testing authorization.
|
|
func NewAuthorizationTestHelper(t testing.TB, e *Executor) *AuthorizationTestHelper {
|
|
return &AuthorizationTestHelper{
|
|
CrossContractHelper: NewCrossContractHelper(t, e),
|
|
}
|
|
}
|
|
|
|
// TestCallerAuthorization tests that a method properly checks GetCallingScriptHash().
|
|
// It calls the method from different contexts and verifies the expected behavior.
|
|
func (a *AuthorizationTestHelper) TestCallerAuthorization(
|
|
target util.Uint160,
|
|
method string,
|
|
args []any,
|
|
authorizedCallers []util.Uint160,
|
|
unauthorizedCallers []util.Uint160,
|
|
signers []Signer,
|
|
) {
|
|
// Test authorized callers succeed
|
|
for _, caller := range authorizedCallers {
|
|
script := a.CallFromContract(caller, target, method, args...)
|
|
tx := a.PrepareProxyCall(signers, script)
|
|
a.AddNewBlock(a.t, tx)
|
|
|
|
aer, err := a.Chain.GetAppExecResults(tx.Hash(), 0)
|
|
require.NoError(a.t, err)
|
|
require.Equal(a.t, 1, len(aer))
|
|
require.Equal(a.t, "HALT", aer[0].VMState.String(),
|
|
"authorized caller %s should succeed", caller.StringLE())
|
|
}
|
|
|
|
// Test unauthorized callers fail
|
|
for _, caller := range unauthorizedCallers {
|
|
script := a.CallFromContract(caller, target, method, args...)
|
|
tx := a.PrepareProxyCall(signers, script)
|
|
a.AddNewBlock(a.t, tx)
|
|
|
|
aer, err := a.Chain.GetAppExecResults(tx.Hash(), 0)
|
|
require.NoError(a.t, err)
|
|
require.Equal(a.t, 1, len(aer))
|
|
require.Equal(a.t, "FAULT", aer[0].VMState.String(),
|
|
"unauthorized caller %s should fail", caller.StringLE())
|
|
}
|
|
}
|