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()) } }