package rpcsrv

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	gio "io"
	"math"
	"math/big"
	"net"
	"net/http"
	"net/http/httptest"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
	"git.marketally.com/tutus-one/tutus-chain/internal/random"
	"git.marketally.com/tutus-one/tutus-chain/internal/testchain"
	"git.marketally.com/tutus-one/tutus-chain/internal/testserdes"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/block"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/fee"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/interopnames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativehashes"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage/dboper"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/address"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutusrpc"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutusrpc/result"
	"git.marketally.com/tutus-one/tutus-chain/pkg/network"
	"git.marketally.com/tutus-one/tutus-chain/pkg/network/payload"
	rpc2 "git.marketally.com/tutus-one/tutus-chain/pkg/services/oracle/broadcaster"
	"git.marketally.com/tutus-one/tutus-chain/pkg/services/rpcsrv/params"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"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/invocations"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/vmstate"
	"git.marketally.com/tutus-one/tutus-chain/pkg/wallet"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zapcore"
)

type executor struct {
	chain   *core.Blockchain
	httpSrv *httptest.Server
}

type rpcTestCase struct {
	name    string
	params  string
	fail    bool
	errCode int64
	result  func(e *executor) any
	check   func(t *testing.T, e *executor, result any)
}

// Constants and variables below are taken from the logs generated by
// [core.TestCreateBasicChain]. These constants and variables should be updated manually
// on basic chain regeneration. Please, refer to TestCreateBasicChain documentation
// to learn about basic testing chain generation.
const (
	// genesisBlockHash is an LE hash of genesis block in basic testing chain.
	genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4"
	// testContractHashLE is an LE hash of NEP-17 "Rubl" contract deployed at block #2
	// of basic testing chain.
	testContractHashLE = "4ae8b123905cf5f17c56c7ba03c3e49dd4de6602"
	// deploymentTxHash is an LE hash of transaction that deploys NEP-17 "Rubl"
	// contract at block #2 of basic testing chain.
	deploymentTxHash = "372dbbbb097ec67171c979f218668854cc91dcb4b73f6f00f69a9b709b67b16b"
	// verifyContractHash is an LE hash of "Verify" contract deployed at block #7 of
	// basic testing chain.
	verifyContractHash = "41ea4991de3c83011ad040c5e0837f4f47d088a4"
	// verifyContractAVM is a base64-encoded AVM of "Verify" contract deployed at block #7 of
	// basic testing chain.
	verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A="
	// verifyWithArgsContractHash is an LE hash of "VerifyWithArgs" contract deployed
	// at block #10 of basic testing chain.
	verifyWithArgsContractHash = "a7b1353ce63445c132b3951185cfda5d3fad3bc9"
	// nnsContractHash is an LE hash of NEP-11 non-divisible "examples/nft-nd-nns"
	// contract deployed at block #11 of basic testing chain.
	nnsContractHash = "9e78165b94215c954e3db4897f8fad0eec57eee5"
	// nnsToken1ID is a hex-encoded ID of the first NEP-11 NNS token minted at block
	// #15 of basic testing chain.
	nnsToken1ID = "74757475732e6f6e65"
	// nfsoContractHash is an LE hash of NEP-11 divisible "examples/nft-d" ("NeoFS
	// Object") contract deployed at block #18 of basic testing chain.
	nfsoContractHash = "01ad70f110f3a42196927b90bd29cda89285e047"
	// nfsoToken1ID is a hex-encoded ID of the first NEP-11 NFSO token minted at
	// block #19 of basic testing chain.
	nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486"
	// storageContractHash is an LE hash of "Storage" contract deployed at block #23
	// of basic testing chain.
	storageContractHash = "4fc98af155564b1540d990346ec793f429827ad8"
	// faultedTxHashLE is an LE hash of FAULTed transaction accepted at block #24 of
	// basic testing chain.
	faultedTxHashLE = "82f9a502799c13b3ced6fdb83f0eec0fee5814b7c59868beca2e39b38c6527d3"
	// faultedTxBlock is the number of block of basic testing chain that contains
	// FAULTed transaction.
	faultedTxBlock uint32 = 24
	// invokescriptContractAVM is a base64-encoded AVM of
	// "pkg/internal/basicchain/testdata/invokescript_contract.go" contract that is
	// not yet deployed to the testing basic chain.
	invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
	// block21StateRootLE is an LE stateroot of block #21 of basic testing chain.
	block21StateRootLE = "81404d5b7bcd88eae9990ce7d80ba963f28e765faa3039d2dcf87dc9a67ea212"
)

var (
	// testContractHashLE is a hash of NEP-11 divisible "Rubl" contract
	// placed in "internal/basicchain/testdata/test_contract.go" and
	// deployed at block #2 of basic testing chain.
	testContractHash, _ = util.Uint160DecodeStringLE(testContractHashLE)
	// nnsHash is a hash of NEP-11 non-divisible "examples/nft-nd-nns" contract
	// deployed at block #11 of basic testing chain.
	nnsHash, _ = util.Uint160DecodeStringLE(nnsContractHash)
	// storageHash is a hash of "Storage" contract
	// placed in "internal/basicchain/testdata/storage/storage_contract.go"
	// deployed at block #22 of basic testing chain.
	storageHash, _ = util.Uint160DecodeStringLE(storageContractHash)
	// nfsoHash is a hash of NEP-11 divisible "examples/nft-d" ("NeoFS
	// Object") contract deployed at block #17 of basic testing chain.
	nfsoHash, _ = util.Uint160DecodeStringLE(nfsoContractHash)
	// nfsoToken1ContainerID contains data used to mint the first NFSO token in the
	// basic testing chain (see [nfsoToken1ID] documentation).
	nfsoToken1ContainerID = util.Uint256{1, 2, 3}
	// nfsoToken1ContainerID contains data used to mint the first NFSO token in the
	// basic testing chain (see [nfsoToken1ID] documentation).
	nfsoToken1ObjectID = util.Uint256{4, 5, 6}
)

var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
	"getproof": {
		{
			name:    "unsupported state",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.ErrUnsupportedStateCode,
		},
	},
	"verifyproof": {
		{
			name:    "unsupported state",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.ErrUnsupportedStateCode,
		},
	},
	"getstate": {
		{
			name:    "unsupported state",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "QQ=="]`,
			fail:    true,
			errCode: tutusrpc.ErrUnsupportedStateCode,
		},
	},
	"findstates": {
		{
			name:    "unsupported state",
			params:  `["` + block21StateRootLE + `", "0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnsupportedStateCode,
		},
	},
	"findstoragehistoric": {
		{
			name:    "unsupported state",
			params:  `["` + block21StateRootLE + `", "0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnsupportedStateCode,
		},
	},
	"invokefunctionhistoric": {
		{
			name:    "unsupported state",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.ErrUnsupportedStateCode,
		},
	},
}

var rpcTestCases = map[string][]rpcTestCase{
	"getapplicationlog": {
		{
			name:   "positive",
			params: `["` + deploymentTxHash + `"]`,
			result: func(e *executor) any { return &result.ApplicationLog{} },
			check: func(t *testing.T, e *executor, acc any) {
				res, ok := acc.(*result.ApplicationLog)
				require.True(t, ok)
				expectedTxHash, err := util.Uint256DecodeStringLE(deploymentTxHash)
				require.NoError(t, err)
				assert.Equal(t, 1, len(res.Executions))
				assert.Equal(t, expectedTxHash, res.Container)
				assert.Equal(t, trigger.Application, res.Executions[0].Trigger)
				assert.Equal(t, vmstate.Halt, res.Executions[0].VMState)
			},
		},
		{
			name:   "positive, genesis block",
			params: `["` + genesisBlockHash + `"]`,
			result: func(e *executor) any { return &result.ApplicationLog{} },
			check: func(t *testing.T, e *executor, acc any) {
				res, ok := acc.(*result.ApplicationLog)
				require.True(t, ok)
				assert.Equal(t, genesisBlockHash, res.Container.StringLE())
				assert.Equal(t, 2, len(res.Executions))
				assert.Equal(t, trigger.OnPersist, res.Executions[0].Trigger)
				assert.Equal(t, trigger.PostPersist, res.Executions[1].Trigger)
				assert.Equal(t, vmstate.Halt, res.Executions[0].VMState)
			},
		},
		{
			name:   "positive, genesis block, postPersist",
			params: `["` + genesisBlockHash + `", "PostPersist"]`,
			result: func(e *executor) any { return &result.ApplicationLog{} },
			check: func(t *testing.T, e *executor, acc any) {
				res, ok := acc.(*result.ApplicationLog)
				require.True(t, ok)
				assert.Equal(t, genesisBlockHash, res.Container.StringLE())
				assert.Equal(t, 1, len(res.Executions))
				assert.Equal(t, trigger.PostPersist, res.Executions[0].Trigger)
				assert.Equal(t, vmstate.Halt, res.Executions[0].VMState)
			},
		},
		{
			name:   "positive, genesis block, onPersist",
			params: `["` + genesisBlockHash + `", "OnPersist"]`,
			result: func(e *executor) any { return &result.ApplicationLog{} },
			check: func(t *testing.T, e *executor, acc any) {
				res, ok := acc.(*result.ApplicationLog)
				require.True(t, ok)
				assert.Equal(t, genesisBlockHash, res.Container.StringLE())
				assert.Equal(t, 1, len(res.Executions))
				assert.Equal(t, trigger.OnPersist, res.Executions[0].Trigger)
				assert.Equal(t, vmstate.Halt, res.Executions[0].VMState)
			},
		},
		{
			name:    "invalid trigger (not a string)",
			params:  `["` + genesisBlockHash + `", 1]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["notahash"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid tx hash",
			params:  `["d24cc1d52b5c0216cbf3835bb5bac8ccf32639fa1ab6627ec4e2b9f33f7ec02f"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownScriptContainerCode,
		},
	},
	"getcontractstate": {
		{
			name:   "positive, by hash",
			params: fmt.Sprintf(`["%s"]`, testContractHashLE),
			result: func(e *executor) any { return &state.Contract{} },
			check: func(t *testing.T, e *executor, cs any) {
				res, ok := cs.(*state.Contract)
				require.True(t, ok)
				assert.Equal(t, testContractHashLE, res.Hash.StringLE())
			},
		},
		{
			name:   "positive, by id",
			params: `[1]`,
			result: func(e *executor) any { return &state.Contract{} },
			check: func(t *testing.T, e *executor, cs any) {
				res, ok := cs.(*state.Contract)
				require.True(t, ok)
				assert.Equal(t, int32(1), res.ID)
			},
		},
		{
			name:   "positive, native by id",
			params: `[-3]`,
			result: func(e *executor) any { return &state.Contract{} },
			check: func(t *testing.T, e *executor, cs any) {
				res, ok := cs.(*state.Contract)
				require.True(t, ok)
				assert.Equal(t, int32(-3), res.ID)
			},
		},
		{
			name:   "positive, native by name",
			params: `["PolicyContract"]`,
			result: func(e *executor) any { return &state.Contract{} },
			check: func(t *testing.T, e *executor, cs any) {
				res, ok := cs.(*state.Contract)
				require.True(t, ok)
				assert.Equal(t, int32(-7), res.ID)
			},
		},
		{
			name:    "negative, bad hash",
			params:  `["6d1eeca891ee93de2b7a77eb91c26f3b3c04d6c3"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "negative, bad ID",
			params:  `[-100]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "negative, bad native name",
			params:  `["unknown_native"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"getnep11balances": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:   "positive",
			params: `["` + testchain.PrivateKeyByID(0).GetScriptHash().StringLE() + `"]`,
			result: func(e *executor) any { return &result.NEP11Balances{} },
			check:  checkNep11Balances,
		},
		{
			name:   "positive_address",
			params: `["` + address.Uint160ToString(testchain.PrivateKeyByID(0).GetScriptHash()) + `"]`,
			result: func(e *executor) any { return &result.NEP11Balances{} },
			check:  checkNep11Balances,
		},
	},
	"getnep11properties": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no token",
			params:  `["` + nnsContractHash + `"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad token",
			params:  `["` + nnsContractHash + `", "abcdef"]`,
			fail:    true,
			errCode: tutusrpc.ErrExecutionFailedCode,
		},
		{
			name:   "positive",
			params: `["` + nnsContractHash + `", "` + nnsToken1ID + `"]`,
			result: func(e *executor) any {
				return &map[string]any{
					"name":       "tutus.one",
					"expiration": "lxbLRl0B",
					"admin":      nil,
				}
			},
		},
	},
	"getnep11transfers": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid timestamp",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "notanumber"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid stop timestamp",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "blah"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:   "positive",
			params: `["` + testchain.PrivateKeyByID(0).Address() + `", 0]`,
			result: func(e *executor) any { return &result.NEP11Transfers{} },
			check:  checkNep11Transfers,
		},
	},
	"getnep17balances": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:   "positive",
			params: `["` + testchain.PrivateKeyByID(0).GetScriptHash().StringLE() + `"]`,
			result: func(e *executor) any { return &result.NEP17Balances{} },
			check:  checkNep17Balances,
		},
		{
			name:   "positive_address",
			params: `["` + address.Uint160ToString(testchain.PrivateKeyByID(0).GetScriptHash()) + `"]`,
			result: func(e *executor) any { return &result.NEP17Balances{} },
			check:  checkNep17Balances,
		},
	},
	"getnep17transfers": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid timestamp",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "notanumber"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid stop timestamp",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "blah"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid limit",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "0"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid limit 2",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "bleh"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid limit 3",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "100500"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid page",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "3", "-1"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid page 2",
			params:  `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "3", "jajaja"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:   "positive",
			params: `["` + testchain.PrivateKeyByID(0).Address() + `", 0]`,
			result: func(e *executor) any { return &result.NEP17Transfers{} },
			check:  checkNep17Transfers,
		},
		{
			name:   "positive_hash",
			params: `["` + testchain.PrivateKeyByID(0).GetScriptHash().StringLE() + `", 0]`,
			result: func(e *executor) any { return &result.NEP17Transfers{} },
			check:  checkNep17Transfers,
		},
	},
	"getproof": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid root",
			params:  `["0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid contract",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"getstate": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid root",
			params:  `["0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid contract",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "notabase64%"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "unknown contract",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000", "QQ=="]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "unknown root/item",
			params:  `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "QQ=="]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
	},
	"findstates": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid root",
			params:  `["0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid contract",
			params:  `["` + block21StateRootLE + `", "0xabcdef"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid prefix",
			params:  `["` + block21StateRootLE + `", "` + testContractHashLE + `", "notabase64%"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  `["` + block21StateRootLE + `", "` + testContractHashLE + `", "QQ==", "notabase64%"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "unknown contract/large count",
			params:  `["` + block21StateRootLE + `", "0000000000000000000000000000000000000000", "QQ==", "QQ==", 101]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
	},
	"getstateheight": {
		{
			name:   "positive",
			params: `[]`,
			result: func(_ *executor) any { return new(result.StateHeight) },
			check: func(t *testing.T, e *executor, res any) {
				sh, ok := res.(*result.StateHeight)
				require.True(t, ok)

				require.Equal(t, e.chain.BlockHeight(), sh.Local)
				require.Equal(t, uint32(0), sh.Validated)
			},
		},
	},
	"getstateroot": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  `["0x1234567890"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownStateRootCode,
		},
	},
	"getstorage": {
		{
			name:   "positive",
			params: fmt.Sprintf(`["%s", "dGVzdGtleQ=="]`, testContractHashLE),
			result: func(e *executor) any {
				v := base64.StdEncoding.EncodeToString([]byte("newtestvalue"))
				return &v
			},
		},
		{
			name:    "missing key",
			params:  fmt.Sprintf(`["%s", "dGU="]`, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.ErrUnknownStorageItemCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no second parameter",
			params:  fmt.Sprintf(`["%s"]`, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  fmt.Sprintf(`["%s", "notabase64$"]`, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"getstoragehistoric": {
		{
			name:   "positive",
			params: fmt.Sprintf(`["%s", "%s", "%s"]`, block21StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa10"))),
			result: func(e *executor) any {
				v := base64.StdEncoding.EncodeToString([]byte("v2"))
				return &v
			},
		},
		{
			name:    "missing key",
			params:  fmt.Sprintf(`["%s", "%s", "dGU="]`, block21StateRootLE, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.ErrUnknownStorageItemCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no second parameter",
			params:  fmt.Sprintf(`["%s"]`, block21StateRootLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no third parameter",
			params:  fmt.Sprintf(`["%s", "%s"]`, block21StateRootLE, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid stateroot",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  fmt.Sprintf(`["%s", "notahex"]`, block21StateRootLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block21StateRootLE, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"findstorage": {
		{
			name:   "not truncated",
			params: fmt.Sprintf(`["%s", "%s"]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa1"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte("aa10"),
							Value: []byte("v2"),
						},
					},
					Next:      1,
					Truncated: false,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "truncated first page",
			params: fmt.Sprintf(`["%s", "%s"]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte("aa"),
							Value: []byte("v1"),
						},
						{
							Key:   []byte("aa10"),
							Value: []byte("v2"),
						},
					},
					Next:      2,
					Truncated: true,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "truncated second page",
			params: fmt.Sprintf(`["%s", "%s", 2]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte("aa50"),
							Value: []byte("v3"),
						},
					},
					Next:      3,
					Truncated: false,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "empty prefix",
			params: fmt.Sprintf(`["%s", ""]`, storageContractHash),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte{0x01, 0x00},
							Value: []byte{},
						},
						{
							Key:   []byte{0x01, 0x01},
							Value: []byte{0x01},
						},
					},
					Next:      2,
					Truncated: true,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "unknown key",
			params: fmt.Sprintf(`["%s", "%s"]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("unknown-key"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results:   []result.KeyValue{},
					Next:      0,
					Truncated: false,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no second parameter",
			params:  fmt.Sprintf(`["%s"]`, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  fmt.Sprintf(`["%s", "notabase64$"]`, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid page",
			params:  fmt.Sprintf(`["%s", "", "not-an-int"]`, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"findstoragehistoric": {
		{
			name:   "not truncated",
			params: fmt.Sprintf(`["%s", "%s", "%s"]`, block21StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa1"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte("aa10"),
							Value: []byte("v2"),
						},
					},
					Next:      1,
					Truncated: false,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "truncated first page",
			params: fmt.Sprintf(`["%s", "%s", "%s"]`, block21StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte("aa10"), // items traversal order may differ from the one provided by `findstorage` due to MPT traversal strategy.
							Value: []byte("v2"),
						},
						{
							Key:   []byte("aa50"),
							Value: []byte("v3"),
						},
					},
					Next:      2,
					Truncated: true,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "truncated second page",
			params: fmt.Sprintf(`["%s","%s", "%s", 2]`, block21StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte("aa"),
							Value: []byte("v1"),
						},
					},
					Next:      3,
					Truncated: false,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "empty prefix",
			params: fmt.Sprintf(`["%s", "%s", ""]`, block21StateRootLE, nnsContractHash),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results: []result.KeyValue{
						{
							Key:   []byte{0x00}, // total supply
							Value: []byte{0x01},
						},
						{
							Key:   append([]byte{0x01}, testchain.PrivateKeyByID(0).GetScriptHash().BytesBE()...), // balance of priv0
							Value: []byte{0x01},
						},
					},
					Next:      2,
					Truncated: true,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:   "unknown key",
			params: fmt.Sprintf(`["%s", "%s", "%s"]`, block21StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("unknown-key"))),
			result: func(_ *executor) any { return new(result.FindStorage) },
			check: func(t *testing.T, e *executor, res any) {
				actual, ok := res.(*result.FindStorage)
				require.True(t, ok)

				expected := &result.FindStorage{
					Results:   []result.KeyValue{},
					Next:      0,
					Truncated: false,
				}
				require.Equal(t, expected, actual)
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid stateroot",
			params:  `[12345]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no second parameter",
			params:  fmt.Sprintf(`["%s"]`, block21StateRootLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no third parameter",
			params:  fmt.Sprintf(`["%s", "%s"]`, block21StateRootLE, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  fmt.Sprintf(`["%s", "notahex"]`, block21StateRootLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid key",
			params:  fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block21StateRootLE, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid page",
			params:  fmt.Sprintf(`["%s", "%s", "", "not-an-int"]`, block21StateRootLE, testContractHashLE),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"getbestblockhash": {
		{
			params: "[]",
			result: func(e *executor) any {
				v := "0x" + e.chain.CurrentBlockHash().StringLE()
				return &v
			},
		},
	},
	"getblock": {
		{
			name:   "positive",
			params: "[3, 1]",
			result: func(_ *executor) any { return &result.Block{} },
			check: func(t *testing.T, e *executor, blockRes any) {
				res, ok := blockRes.(*result.Block)
				require.True(t, ok)

				block, err := e.chain.GetBlock(e.chain.GetHeaderHash(3))
				require.NoErrorf(t, err, "could not get block")

				assert.Equal(t, block.Hash(), res.Hash())
				for i, tx := range res.Transactions {
					actualTx := block.Transactions[i]
					require.True(t, ok)
					require.Equal(t, actualTx.Nonce, tx.Nonce)
					require.Equal(t, block.Transactions[i].Hash(), tx.Hash())
				}
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad params",
			params:  `[[]]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid height",
			params:  `[-1]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownHeightCode,
		},
		{
			name:    "invalid hash",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "missing hash",
			params:  `["` + util.Uint256{}.String() + `"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownBlockCode,
		},
	},
	"getblockcount": {
		{
			params: "[]",
			result: func(e *executor) any {
				v := int(e.chain.BlockHeight() + 1)
				return &v
			},
		},
	},
	"getblockhash": {
		{
			params: "[1]",
			result: func(e *executor) any {
				// We don't have `t` here for proper handling, but
				// error here would lead to panic down below.
				block, _ := e.chain.GetBlock(e.chain.GetHeaderHash(1))
				expectedHash := "0x" + block.Hash().StringLE()
				return &expectedHash
			},
		},
		{
			name:    "string height",
			params:  `["first"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid number height",
			params:  `[-2]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownHeightCode,
		},
	},
	"getblockheader": {
		{
			name:    "invalid verbose type",
			params:  `["9673799c5b5a294427401cb07d6cc615ada3a0d5c5bf7ed6f0f54f24abb2e2ac", true]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownBlockCode,
		},
		{
			name:    "invalid block hash",
			params:  `["notahash"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "unknown block",
			params:  `["a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c141"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownBlockCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"getblockheadercount": {
		{
			params: "[]",
			result: func(e *executor) any {
				v := int(e.chain.HeaderHeight() + 1)
				return &v
			},
		},
	},
	"getblocksysfee": {
		{
			name:   "positive",
			params: "[1]",
			result: func(e *executor) any {
				block, _ := e.chain.GetBlock(e.chain.GetHeaderHash(1))

				var expectedBlockSysFee int64
				for _, tx := range block.Transactions {
					expectedBlockSysFee += tx.SystemFee
				}
				return &expectedBlockSysFee
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "string height",
			params:  `["first"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid number height",
			params:  `[-2]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownHeightCode,
		},
	},
	"getcommittee": {
		{
			params: "[]",
			result: func(e *executor) any {
				expected, _ := e.chain.GetCommittee()
				slices.SortFunc(expected, (*keys.PublicKey).Cmp)
				return &expected
			},
		},
	},
	"getconnectioncount": {
		{
			params: "[]",
			result: func(*executor) any {
				v := 0
				return &v
			},
		},
	},
	"getnativecontracts": {
		{
			params: "[]",
			result: func(e *executor) any {
				return new([]state.Contract)
			},
			check: func(t *testing.T, e *executor, res any) {
				lst := res.(*[]state.Contract)
				for i := range *lst {
					cs := e.chain.GetContractState((*lst)[i].Hash)
					require.Equal(t, *cs, (*lst)[i])
					require.True(t, cs.ID <= 0)
				}
			},
		},
	},
	"getpeers": {
		{
			params: "[]",
			result: func(*executor) any {
				return &result.GetPeers{
					Unconnected: []result.Peer{},
					Connected:   []result.Peer{},
					Bad:         []result.Peer{},
				}
			},
		},
	},
	"getrawtransaction": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "missing hash",
			params:  `["` + util.Uint256{}.String() + `"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownTransactionCode,
		},
	},
	"gettransactionheight": {
		{
			name:   "positive",
			params: `["` + deploymentTxHash + `"]`,
			result: func(e *executor) any {
				h := 0
				return &h
			},
			check: func(t *testing.T, e *executor, resp any) {
				h, ok := resp.(*int)
				require.True(t, ok)
				assert.Equal(t, 2, *h)
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid hash",
			params:  `["notahex"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "missing hash",
			params:  `["` + util.Uint256{}.String() + `"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownTransactionCode,
		},
	},
	"getunclaimedgas": {
		{
			name:    "no params",
			params:  "[]",
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid address",
			params:  `["invalid"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:   "positive",
			params: `["` + testchain.MultisigAddress() + `"]`,
			result: func(*executor) any {
				return &result.UnclaimedGas{}
			},
			check: func(t *testing.T, e *executor, resp any) {
				actual, ok := resp.(*result.UnclaimedGas)
				require.True(t, ok)
				expected := result.UnclaimedGas{
					Address:   testchain.MultisigScriptHash(),
					Unclaimed: *big.NewInt(12000),
				}
				assert.Equal(t, expected, *actual)
			},
		},
	},
	"getcandidates": {
		{
			params: "[]",
			result: func(*executor) any {
				return &[]result.Candidate{}
			},
		},
	},
	"getnextblockvalidators": {
		{
			params: "[]",
			result: func(*executor) any {
				return &[]result.Validator{}
			},
			check: func(t *testing.T, e *executor, validators any) {
				sBValidators, err := e.chain.GetNextBlockValidators()
				require.NoError(t, err)

				var expected []result.Validator
				for _, sbValidator := range sBValidators {
					expected = append(expected, result.Validator{
						PublicKey: *sbValidator,
						Votes:     -1,
					})
				}

				actual, ok := validators.(*[]result.Validator)
				require.True(t, ok)

				assert.ElementsMatch(t, expected, *actual)
			},
		},
	},
	"getversion": {
		{
			params: "[]",
			result: func(*executor) any { return &result.Version{} },
			check: func(t *testing.T, e *executor, ver any) {
				resp, ok := ver.(*result.Version)
				require.True(t, ok)
				require.Equal(t, "/NEO-GO:0.98.6-test/", resp.UserAgent)

				cfg := e.chain.GetConfig()
				require.EqualValues(t, address.NEO3Prefix, resp.Protocol.AddressVersion)
				require.EqualValues(t, cfg.Magic, resp.Protocol.Network)
				require.EqualValues(t, cfg.TimePerBlock/time.Millisecond, resp.Protocol.MillisecondsPerBlock)
				require.EqualValues(t, cfg.MaxTraceableBlocks, resp.Protocol.MaxTraceableBlocks)
				require.EqualValues(t, cfg.MaxValidUntilBlockIncrement, resp.Protocol.MaxValidUntilBlockIncrement)
				require.EqualValues(t, cfg.MaxTransactionsPerBlock, resp.Protocol.MaxTransactionsPerBlock)
				require.EqualValues(t, cfg.MemPoolSize, resp.Protocol.MemoryPoolMaxTransactions)
				require.EqualValues(t, cfg.ValidatorsCount, resp.Protocol.ValidatorsCount)
				require.EqualValues(t, cfg.InitialLubSupply, resp.Protocol.InitialGasDistribution)
				require.EqualValues(t, cfg.SeedList, resp.Protocol.SeedList)

				require.Equal(t, 0, len(resp.Protocol.CommitteeHistory))
				require.True(t, resp.Protocol.P2PSigExtensions) // Yeah, notary is enabled.
				require.False(t, resp.Protocol.StateRootInHeader)
				require.Equal(t, 0, len(resp.Protocol.ValidatorsHistory))

				standbyCommittee, err := keys.NewPublicKeysFromStrings(cfg.StandbyCommittee)
				require.NoError(t, err)
				require.EqualValues(t, standbyCommittee, resp.Protocol.StandbyCommittee)
			},
		},
	},
	"invokefunction": {
		{
			name:   "positive",
			params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotNil(t, res.Script)
				assert.NotEqual(t, "", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
			},
		},
		{
			name:   "positive, with notifications",
			params: `["` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"tutus.one"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`,
			result: func(e *executor) any {
				script := append([]byte{0x0b, 0x0c, 0x09, 0x74, 0x75, 0x74, 0x75, 0x73, 0x2e, 0x6f, 0x6e, 0x65, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14}, nnsHash.BytesBE()...)
				script = append(script, 0x41, 0x62, 0x7d, 0x5b, 0x52)
				return &result.Invoke{
					State:       "HALT",
					GasConsumed: 32122730,
					Script:      script,
					Stack:       []stackitem.Item{stackitem.Make(true)},
					Notifications: []state.NotificationEvent{{
						ScriptHash: nnsHash,
						Name:       "Transfer",
						Item: stackitem.NewArray([]stackitem.Item{
							stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}),
							stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}),
							stackitem.Make(1),
							stackitem.Make("tutus.one"),
						}),
					}},
				}
			},
		},
		{
			name:   "positive, with storage changes",
			params: `["0x7a8123a18fcbd51f9eba5d50c3bf88bf8f13da88", "transfer", [{"type":"Hash160", "value":"0xb248508f4ef7088e10c48f14d04be3272ca29eee"},{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"Integer", "value":1},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"],true]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotNil(t, res.Script)
				assert.Equal(t, "HALT", res.State)
				assert.Equal(t, []stackitem.Item{stackitem.Make(true)}, res.Stack)
				assert.NotEqual(t, 0, res.GasConsumed)
				chg := []dboper.Operation{{
					State: "Changed",
					Key:   []byte{0xfa, 0xff, 0xff, 0xff, 0xb},
					Value: []byte{0xd6, 0x2c, 0x36, 0x19, 0x5e, 0x79, 0x12},
				}, {
					State: "Added",
					Key:   []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb},
					Value: []byte{0x41, 0x04, 0x21, 0x01, 0x01, 0x21, 0x01, 0x18, 0x00, 0x21, 0x00},
				}, {
					State: "Changed",
					Key:   []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
					Value: []byte{0x41, 0x04, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x18, 0x00, 0x21, 0x00},
				}, {
					State: "Changed",
					Key:   []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
					Value: []byte{0x41, 0x01, 0x21, 0x05, 0xb2, 0x82, 0xa7, 0x54, 0x15},
				}}
				// Can be returned in any order.
				assert.ElementsMatch(t, chg, res.Diagnostics.Changes)
			},
		},
		{
			name:   "positive, verbose",
			params: `["` + nnsContractHash + `", "resolve", [{"type":"String", "value":"tutus.one"},{"type":"Integer","value":1}], [], true]`,
			result: func(e *executor) any {
				script := append([]byte{0x11, 0xc, 0x9, 0x74, 0x75, 0x74, 0x75, 0x73, 0x2e, 0x6f, 0x6e, 0x65, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14}, nnsHash.BytesBE()...)
				script = append(script, 0x41, 0x62, 0x7d, 0x5b, 0x52)
				stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib)
				cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
				return &result.Invoke{
					State:         "HALT",
					GasConsumed:   14507250,
					Script:        script,
					Stack:         []stackitem.Item{stackitem.Make("1.2.3.4")},
					Notifications: []state.NotificationEvent{},
					Diagnostics: &result.InvokeDiag{
						Changes: []dboper.Operation{},
						Invocations: []*invocations.Tree{{
							Current: hash.Hash160(script),
							Calls: []*invocations.Tree{
								{
									Current: nnsHash,
									Calls: []*invocations.Tree{
										{
											Current: stdHash,
										},
										{
											Current: cryptoHash,
										},
										{
											Current: stdHash,
										},
										{
											Current: cryptoHash,
										},
										{
											Current: cryptoHash,
										},
									},
								},
							},
						}},
					},
				}
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "not a string",
			params:  `[42, "test", []]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "not a scripthash",
			params:  `["qwerty", "test", []]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad params",
			params:  `["50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"invokefunctionhistoric": {
		{
			name:   "positive, by index",
			params: `[20, "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotNil(t, res.Script)
				assert.NotEqual(t, "", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
			},
		},
		{
			name:   "positive, by stateroot",
			params: `["` + block21StateRootLE + `", "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotNil(t, res.Script)
				assert.NotEqual(t, "", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
			},
		},
		{
			name:   "positive, with notifications",
			params: `[20, "` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"tutus.one"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`,
			result: func(e *executor) any {
				script := append([]byte{0x0b, 0x0c, 0x09, 0x74, 0x75, 0x74, 0x75, 0x73, 0x2e, 0x6f, 0x6e, 0x65, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14}, nnsHash.BytesBE()...)
				script = append(script, 0x41, 0x62, 0x7d, 0x5b, 0x52)
				return &result.Invoke{
					State:       "HALT",
					GasConsumed: 32122730,
					Script:      script,
					Stack:       []stackitem.Item{stackitem.Make(true)},
					Notifications: []state.NotificationEvent{{
						ScriptHash: nnsHash,
						Name:       "Transfer",
						Item: stackitem.NewArray([]stackitem.Item{
							stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}),
							stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}),
							stackitem.Make(1),
							stackitem.Make("tutus.one"),
						}),
					}},
				}
			},
		},
		{
			name:   "positive, verbose",
			params: `[20, "` + nnsContractHash + `", "resolve", [{"type":"String", "value":"tutus.one"},{"type":"Integer","value":1}], [], true]`,
			result: func(e *executor) any {
				script := append([]byte{0x11, 0xc, 0x9, 0x74, 0x75, 0x74, 0x75, 0x73, 0x2e, 0x6f, 0x6e, 0x65, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14}, nnsHash.BytesBE()...)
				script = append(script, 0x41, 0x62, 0x7d, 0x5b, 0x52)
				stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib)
				cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
				return &result.Invoke{
					State:         "HALT",
					GasConsumed:   14507250,
					Script:        script,
					Stack:         []stackitem.Item{stackitem.Make("1.2.3.4")},
					Notifications: []state.NotificationEvent{},
					Diagnostics: &result.InvokeDiag{
						Changes: []dboper.Operation{},
						Invocations: []*invocations.Tree{{
							Current: hash.Hash160(script),
							Calls: []*invocations.Tree{
								{
									Current: nnsHash,
									Calls: []*invocations.Tree{
										{
											Current: stdHash,
										},
										{
											Current: cryptoHash,
										},
										{
											Current: stdHash,
										},
										{
											Current: cryptoHash,
										},
										{
											Current: cryptoHash,
										},
									},
								},
							},
						}},
					},
				}
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no args",
			params:  `[20]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "not a string",
			params:  `[20, 42, "test", []]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "not a scripthash",
			params:  `[20,"qwerty", "test", []]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad params",
			params:  `[20,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad height",
			params:  `[100500,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad stateroot",
			params:  `["` + util.Uint256{1, 2, 3}.StringLE() + `","50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"invokescript": {
		{
			name:   "positive",
			params: `["UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotEqual(t, "", res.Script)
				assert.NotEqual(t, "", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
			},
		},
		{
			name:   "positive,verbose",
			params: `["UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`,
			result: func(e *executor) any {
				script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66}
				return &result.Invoke{
					State:          "FAULT",
					GasConsumed:    60,
					Script:         script,
					Stack:          []stackitem.Item{},
					FaultException: "at instruction 0 (ROT): too big index",
					Notifications:  []state.NotificationEvent{},
					Diagnostics: &result.InvokeDiag{
						Changes: []dboper.Operation{},
						Invocations: []*invocations.Tree{{
							Current: hash.Hash160(script),
						}},
					},
				}
			},
		},
		{
			name: "positive, good witness",
			// script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix
			params: fmt.Sprintf(`["%s",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				require.Equal(t, 1, len(res.Stack))
				require.Equal(t, big.NewInt(3), res.Stack[0].Value())
			},
		},
		{
			name:   "positive, bad witness of second hash",
			params: fmt.Sprintf(`["%s",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				require.Equal(t, 1, len(res.Stack))
				require.Equal(t, big.NewInt(2), res.Stack[0].Value())
			},
		},
		{
			name:   "positive, no good hashes",
			params: fmt.Sprintf(`["%s"]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				require.Equal(t, 1, len(res.Stack))
				require.Equal(t, big.NewInt(1), res.Stack[0].Value())
			},
		},
		{
			name:   "positive, bad hashes witness",
			params: fmt.Sprintf(`["%s",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				assert.Equal(t, 1, len(res.Stack))
				assert.Equal(t, big.NewInt(1), res.Stack[0].Value())
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "not a string",
			params:  `[42]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bas string",
			params:  `["qwerty"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"invokescripthistoric": {
		{
			name:   "positive, by index",
			params: `[20,"UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotEqual(t, "", res.Script)
				assert.NotEqual(t, "", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
			},
		},
		{
			name:   "positive, by stateroot",
			params: `["` + block21StateRootLE + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.NotEqual(t, "", res.Script)
				assert.NotEqual(t, "", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
			},
		},
		{
			name:   "positive,verbose",
			params: `[20, "UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`,
			result: func(e *executor) any {
				script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66}
				return &result.Invoke{
					State:          "FAULT",
					GasConsumed:    60,
					Script:         script,
					Stack:          []stackitem.Item{},
					FaultException: "at instruction 0 (ROT): too big index",
					Notifications:  []state.NotificationEvent{},
					Diagnostics: &result.InvokeDiag{
						Changes: []dboper.Operation{},
						Invocations: []*invocations.Tree{{
							Current: hash.Hash160(script),
						}},
					},
				}
			},
		},
		{
			name: "positive, good witness",
			// script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix
			params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				require.Equal(t, 1, len(res.Stack))
				require.Equal(t, big.NewInt(3), res.Stack[0].Value())
			},
		},
		{
			name:   "positive, bad witness of second hash",
			params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				require.Equal(t, 1, len(res.Stack))
				require.Equal(t, big.NewInt(2), res.Stack[0].Value())
			},
		},
		{
			name:   "positive, no good hashes",
			params: fmt.Sprintf(`[20,"%s"]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				require.Equal(t, 1, len(res.Stack))
				require.Equal(t, big.NewInt(1), res.Stack[0].Value())
			},
		},
		{
			name:   "positive, bad hashes witness",
			params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, invokescriptContractAVM),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Equal(t, "HALT", res.State)
				assert.Equal(t, 1, len(res.Stack))
				assert.Equal(t, big.NewInt(1), res.Stack[0].Value())
			},
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no script",
			params:  `[20]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "not a string",
			params:  `[20,42]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad string",
			params:  `[20, "qwerty"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad height",
			params:  `[100500,"qwerty"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "bad stateroot",
			params:  `["` + util.Uint256{1, 2, 3}.StringLE() + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"invokecontainedscript": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "null tx",
			params:  `[null]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid tx",
			params:  `["notatx"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"invokecontractverify": {
		{
			name:   "positive",
			params: fmt.Sprintf(`["%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args)
				assert.Equal(t, "HALT", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address()))
			},
		},
		{
			name:   "positive, no signers",
			params: fmt.Sprintf(`["%s", []]`, verifyContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, false, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, no arguments",
			params: fmt.Sprintf(`["%s"]`, verifyContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, false, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, with signers and scripts",
			params: fmt.Sprintf(`["%s", [], [{"account":"%s", "invocation":"MQo=", "verification": ""}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script)
				assert.Equal(t, "HALT", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, with arguments, result=true",
			params: fmt.Sprintf(`["%s", [{"type": "String", "value": "good_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				expectedInvScript := io.NewBufBinWriter()
				emit.Bool(expectedInvScript.BinWriter, false)
				emit.Int(expectedInvScript.BinWriter, int64(4))
				emit.String(expectedInvScript.BinWriter, "good_string")
				require.NoError(t, expectedInvScript.Err)
				assert.Equal(t, expectedInvScript.Bytes(), res.Script) // witness invocation script (pushes args of `verify` on stack)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, with arguments, result=false",
			params: fmt.Sprintf(`["%s", [{"type": "String", "value": "invalid_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				expectedInvScript := io.NewBufBinWriter()
				emit.Bool(expectedInvScript.BinWriter, false)
				emit.Int(expectedInvScript.BinWriter, int64(4))
				emit.String(expectedInvScript.BinWriter, "invalid_string")
				require.NoError(t, expectedInvScript.Err)
				assert.Equal(t, expectedInvScript.Bytes(), res.Script)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, false, res.Stack[0].Value().(bool))
			},
		},
		{
			name:    "invalid call args",
			params:  fmt.Sprintf(`["%s", [{"type":"Map","value":{"key":"value"}}]]`, verifyWithArgsContractHash),
			fail:    true,
			errCode: tutusrpc.InternalServerErrorCode,
		},
		{
			name:    "negative, wrong signer",
			params:  fmt.Sprintf(`["%s", [], [{"account":"aaa"}]]`, verifyContractHash),
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "unknown contract",
			params:  fmt.Sprintf(`["%s", []]`, util.Uint160{}.String()),
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "not a string",
			params:  `[42, []]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
	},
	"invokecontractverifyhistoric": {
		{
			name:   "positive, by index",
			params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args)
				assert.Equal(t, "HALT", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address()))
			},
		},
		{
			name:   "positive, by stateroot",
			params: fmt.Sprintf(`["`+block21StateRootLE+`","%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args)
				assert.Equal(t, "HALT", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address()))
			},
		},
		{
			name:   "positive, no signers",
			params: fmt.Sprintf(`[20,"%s", []]`, verifyContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, false, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, no arguments",
			params: fmt.Sprintf(`[20,"%s"]`, verifyContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, false, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, with signers and scripts",
			params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s", "invocation":"MQo=", "verification": ""}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				assert.Nil(t, res.Script)
				assert.Equal(t, "HALT", res.State)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, with arguments, result=true",
			params: fmt.Sprintf(`[20,"%s", [{"type": "String", "value": "good_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				expectedInvScript := io.NewBufBinWriter()
				emit.Bool(expectedInvScript.BinWriter, false)
				emit.Int(expectedInvScript.BinWriter, int64(4))
				emit.String(expectedInvScript.BinWriter, "good_string")
				require.NoError(t, expectedInvScript.Err)
				assert.Equal(t, expectedInvScript.Bytes(), res.Script) // witness invocation script (pushes args of `verify` on stack)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, true, res.Stack[0].Value().(bool))
			},
		},
		{
			name:   "positive, with arguments, result=false",
			params: fmt.Sprintf(`[20, "%s", [{"type": "String", "value": "invalid_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash),
			result: func(e *executor) any { return &result.Invoke{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.Invoke)
				require.True(t, ok)
				expectedInvScript := io.NewBufBinWriter()
				emit.Bool(expectedInvScript.BinWriter, false)
				emit.Int(expectedInvScript.BinWriter, int64(4))
				emit.String(expectedInvScript.BinWriter, "invalid_string")
				require.NoError(t, expectedInvScript.Err)
				assert.Equal(t, expectedInvScript.Bytes(), res.Script)
				assert.Equal(t, "HALT", res.State, res.FaultException)
				assert.NotEqual(t, 0, res.GasConsumed)
				assert.Equal(t, false, res.Stack[0].Value().(bool))
			},
		},
		{
			name:    "unknown contract",
			params:  fmt.Sprintf(`[20, "%s", []]`, util.Uint160{}.String()),
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no args",
			params:  `[20]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "not a string",
			params:  `[20,42, []]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownContractCode,
		},
	},
	"sendrawtransaction": {
		{
			name:   "positive",
			params: `["AB0AAACWP5gAAAAAAEDaEgAAAAAAGAAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAJ6norhWoZxp+Hj1JFhi+Z3qI9DUkLSbfsbaLSaJIqxTfdmPbNFDVK1G+oa+LWmpRp/bj9+QZM7yC+S6HXUI7rigMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`,
			result: func(e *executor) any { return &result.RelayResult{} },
			check: func(t *testing.T, e *executor, inv any) {
				res, ok := inv.(*result.RelayResult)
				require.True(t, ok)
				expectedHash := "c11861dec1dd0f188608b725095041fcfc90abe51eea044993f122f22472753e"
				assert.Equal(t, expectedHash, res.Hash.StringLE())
			},
		},
		{
			name:    "already in pool",
			params:  `["AB0AAACWP5gAAAAAAEDaEgAAAAAAGAAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAJ6norhWoZxp+Hj1JFhi+Z3qI9DUkLSbfsbaLSaJIqxTfdmPbNFDVK1G+oa+LWmpRp/bj9+QZM7yC+S6HXUI7rigMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`,
			fail:    true,
			errCode: tutusrpc.ErrAlreadyInPoolCode,
		},
		{
			name:    "negative",
			params:  `["AAoAAAAxboUQOQGdOd/Cw31sP+4Z/VgJhwAAAAAAAAAA8q0FAAAAAACwBAAAAAExboUQOQGdOd/Cw31sP+4Z/VgJhwFdAwDodkgXAAAADBQgcoJ0r6/Db0OgcdMoz6PmKdnLsAwUMW6FEDkBnTnfwsN9bD/uGf1YCYcTwAwIdHJhbnNmZXIMFIl3INjNdvTwCr+jfA7diJwgj96bQWJ9W1I4AUIMQN+VMUEnEWlCHOurXSegFj4pTXx/LQUltEmHRTRIFP09bFxZHJsXI9BdQoVvQJrbCEz2esySHPr8YpEzpeteen4pDCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcILQQqQav8="]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid string",
			params:  `["notabase64%"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid tx",
			params:  `["AnTXkgcmF3IGNvbnRyYWNw=="]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"submitblock": {
		{
			name:    "invalid base64",
			params:  `["%%%"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "invalid block bytes",
			params:  `["AAAAACc="]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"submitoracleresponse": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.ErrOracleDisabledCode,
		},
	},
	"submitnotaryrequest": {
		{
			name:    "no params",
			params:  `[]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"validateaddress": {
		{
			name:   "positive",
			params: `["Nbb1qkwcwNSBs9pAnrVVrnFbWnbWBk91U2"]`,
			result: func(*executor) any { return &result.ValidateAddress{} },
			check: func(t *testing.T, e *executor, va any) {
				res, ok := va.(*result.ValidateAddress)
				require.True(t, ok)
				assert.Equal(t, "Nbb1qkwcwNSBs9pAnrVVrnFbWnbWBk91U2", res.Address)
				assert.True(t, res.IsValid)
			},
		},
		{
			name:   "negative",
			params: "[1]",
			result: func(*executor) any {
				return &result.ValidateAddress{
					Address: float64(1),
					IsValid: false,
				}
			},
		},
		{
			name:    "no params",
			params:  "[]",
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
	"getblocknotifications": {
		{
			name:   "positive",
			params: `["` + genesisBlockHash + `"]`,
			result: func(e *executor) any { return &result.BlockNotifications{} },
			check: func(t *testing.T, e *executor, acc any) {
				res, ok := acc.(*result.BlockNotifications)
				require.True(t, ok)
				require.NotNil(t, res)
			},
		},
		{
			name:   "positive with filter",
			params: `["` + genesisBlockHash + `", {"contract":"` + nativehashes.Tutus.StringLE() + `", "name":"Transfer"}]`,
			result: func(e *executor) any { return &result.BlockNotifications{} },
			check: func(t *testing.T, e *executor, acc any) {
				res, ok := acc.(*result.BlockNotifications)
				require.True(t, ok)
				require.NotNil(t, res)
				for _, ne := range res.Application {
					require.Equal(t, nativehashes.Tutus, ne.ScriptHash)
					require.Equal(t, "Transfer", ne.Name)
				}
			},
		},
		{
			name:    "invalid hash",
			params:  `["invalid"]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "unknown block",
			params:  `["` + util.Uint256{}.StringLE() + `"]`,
			fail:    true,
			errCode: tutusrpc.ErrUnknownBlockCode,
		},
		{
			name:    "invalid filter",
			params:  `["` + genesisBlockHash + `", {"contract":"invalid"}]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "filter with unknown fields",
			params:  `["` + genesisBlockHash + `", {"invalid":"something"}]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
		{
			name:    "empty filter",
			params:  `["` + genesisBlockHash + `", []]`,
			fail:    true,
			errCode: tutusrpc.InvalidParamsCode,
		},
	},
}

func TestRPC(t *testing.T) {
	t.Skip("Skipped: requires testblocks.acc regeneration after .one TLD addition - assigned to tutustest framework task")
	t.Run("http", func(t *testing.T) {
		testRPCProtocol(t, doRPCCallOverHTTP)
	})

	t.Run("websocket", func(t *testing.T) {
		testRPCProtocol(t, doRPCCallOverWS)
	})
}

func TestSubmitOracle(t *testing.T) {
	rpc := `{"jsonrpc": "2.0", "id": 1, "method": "submitoracleresponse", "params": %s}`

	t.Run("OracleDisabled", func(t *testing.T) {
		_, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
			c.ApplicationConfiguration.Oracle.Enabled = false
		})
		req := fmt.Sprintf(rpc, "[]")
		body := doRPCCallOverHTTP(req, httpSrv.URL, t)
		checkErrGetResult(t, body, true, tutusrpc.ErrOracleDisabledCode)
	})

	_, _, httpSrv := initClearServerWithServices(t, true, false, false)

	runCase := func(t *testing.T, fail bool, errCode int64, params ...string) func(t *testing.T) {
		return func(t *testing.T) {
			ps := `[` + strings.Join(params, ",") + `]`
			req := fmt.Sprintf(rpc, ps)
			body := doRPCCallOverHTTP(req, httpSrv.URL, t)
			checkErrGetResult(t, body, fail, errCode)
		}
	}
	t.Run("MissingKey", runCase(t, true, tutusrpc.InvalidParamsCode))
	t.Run("InvalidKey", runCase(t, true, tutusrpc.InvalidParamsCode, `"1234"`))

	priv, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubStr := `"` + base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes()) + `"`
	t.Run("InvalidReqID", runCase(t, true, tutusrpc.InvalidParamsCode, pubStr, `"notanumber"`))
	t.Run("InvalidTxSignature", runCase(t, true, tutusrpc.InvalidParamsCode, pubStr, `1`, `"qwerty"`))

	txSig := priv.Sign([]byte{1, 2, 3})
	txSigStr := `"` + base64.StdEncoding.EncodeToString(txSig) + `"`
	t.Run("MissingMsgSignature", runCase(t, true, tutusrpc.InvalidParamsCode, pubStr, `1`, txSigStr))
	t.Run("InvalidMsgSignature", runCase(t, true, tutusrpc.ErrInvalidSignatureCode, pubStr, `1`, txSigStr, `"0123"`))

	msg := rpc2.GetMessage(priv.PublicKey().Bytes(), 1, txSig)
	msgSigStr := `"` + base64.StdEncoding.EncodeToString(priv.Sign(msg)) + `"`
	t.Run("Valid", runCase(t, false, 0, pubStr, `1`, txSigStr, msgSigStr))
}

func TestNotaryRequestRPC(t *testing.T) {
	var notaryRequest1, notaryRequest2 *payload.P2PNotaryRequest
	rpcSubmit := `{"jsonrpc": "2.0", "id": 1, "method": "submitnotaryrequest", "params": %s}`
	rpcPool := `{"jsonrpc": "2.0", "id": 1, "method": "getrawnotarypool", "params": []}`
	rpcTx := `{"jsonrpc": "2.0", "id": 1, "method": "getrawnotarytransaction", "params": ["%s", %d]}`

	t.Run("disabled P2PSigExtensions", func(t *testing.T) {
		_, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
			c.ProtocolConfiguration.P2PSigExtensions = false
		})
		t.Run("submitnotaryrequest", func(t *testing.T) {
			body := doRPCCallOverHTTP(fmt.Sprintf(rpcSubmit, "[]"), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InternalServerErrorCode)
		})
		t.Run("getrawnotarypool", func(t *testing.T) {
			body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InternalServerErrorCode)
		})
		t.Run("getrawnotarytransaction", func(t *testing.T) {
			body := doRPCCallOverHTTP(fmt.Sprintf(rpcTx, " ", 1), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InternalServerErrorCode)
		})
	})

	chain, _, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false)

	submitNotaryRequest := func(t *testing.T, fail bool, errCode int64, params ...string) func(t *testing.T) {
		return func(t *testing.T) {
			ps := `[` + strings.Join(params, ",") + `]`
			req := fmt.Sprintf(rpcSubmit, ps)
			body := doRPCCallOverHTTP(req, httpSrv.URL, t)
			checkErrGetResult(t, body, fail, errCode)
		}
	}

	t.Run("getrawnotarypool", func(t *testing.T) {
		t.Run("empty pool", func(t *testing.T) {
			body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t)
			res := checkErrGetResult(t, body, false, 0)
			actual := new(result.RawNotaryPool)
			require.NoError(t, json.Unmarshal(res, actual))
			require.Equal(t, 0, len(actual.Hashes))
		})

		sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain
		notaryRequest1 = createValidNotaryRequest(chain, sender, 1, 2_0000_0000, nil)
		nrBytes, err := notaryRequest1.Bytes()
		require.NoError(t, err)
		str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes))
		submitNotaryRequest(t, false, 0, str)(t)

		t.Run("nonempty pool", func(t *testing.T) {
			// get notary pool & check tx hashes
			body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t)
			res := checkErrGetResult(t, body, false, 0)
			actual := new(result.RawNotaryPool)
			require.NoError(t, json.Unmarshal(res, actual))
			require.Equal(t, 1, len(actual.Hashes))
			for actMain, actFallbacks := range actual.Hashes {
				require.Equal(t, notaryRequest1.MainTransaction.Hash(), actMain)
				require.Equal(t, 1, len(actFallbacks))
				require.Equal(t, notaryRequest1.FallbackTransaction.Hash(), actFallbacks[0])
			}
		})

		notaryRequest2 = createValidNotaryRequest(chain, sender, 2, 3_0000_0000, notaryRequest1.MainTransaction)
		nrBytes2, err := notaryRequest2.Bytes()
		require.NoError(t, err)
		str2 := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes2))
		submitNotaryRequest(t, false, 0, str2)(t)

		t.Run("pool with 2", func(t *testing.T) {
			// get notary pool & check tx hashes
			body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t)
			res := checkErrGetResult(t, body, false, 0)
			actual := new(result.RawNotaryPool)
			require.NoError(t, json.Unmarshal(res, actual))
			require.Equal(t, 1, len(actual.Hashes))
			for actMain, actFallbacks := range actual.Hashes {
				require.Equal(t, notaryRequest1.MainTransaction.Hash(), actMain)
				require.Equal(t, 2, len(actFallbacks))
				// The second fallback transaction has higher priority, so it's first in the slice.
				require.Equal(t, notaryRequest1.FallbackTransaction.Hash(), actFallbacks[1])
				require.Equal(t, notaryRequest2.FallbackTransaction.Hash(), actFallbacks[0])
			}
		})
	})

	t.Run("submitnotaryrequest", func(t *testing.T) {
		t.Run("missing request", submitNotaryRequest(t, true, tutusrpc.InvalidParamsCode))
		t.Run("not a base64", submitNotaryRequest(t, true, tutusrpc.InvalidParamsCode, `"not-a-base64$"`))
		t.Run("invalid request bytes", submitNotaryRequest(t, true, tutusrpc.InvalidParamsCode, `"not-a-request"`))
		t.Run("invalid request", func(t *testing.T) {
			mainTx := &transaction.Transaction{
				Attributes:      []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
				Script:          []byte{byte(opcode.RET)},
				ValidUntilBlock: 123,
				Signers:         []transaction.Signer{{Account: util.Uint160{1, 5, 9}}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{1, 4, 7},
					VerificationScript: []byte{3, 6, 9},
				}},
			}
			fallbackTx := &transaction.Transaction{
				Script:          []byte{byte(opcode.RET)},
				ValidUntilBlock: 123,
				Attributes: []transaction.Attribute{
					{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
					{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
					{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
				},
				Signers: []transaction.Signer{{Account: util.Uint160{1, 4, 7}}, {Account: util.Uint160{9, 8, 7}}},
				Scripts: []transaction.Witness{
					{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: make([]byte, 0)},
					{InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}},
			}
			p := &payload.P2PNotaryRequest{
				MainTransaction:     mainTx,
				FallbackTransaction: fallbackTx,
				Witness: transaction.Witness{
					InvocationScript:   []byte{1, 2, 3},
					VerificationScript: []byte{7, 8, 9},
				},
			}
			nrBytes, err := p.Bytes()
			require.NoError(t, err)
			str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes))
			submitNotaryRequest(t, true, tutusrpc.ErrVerificationFailedCode, str)(t)
		})
		t.Run("valid request", func(t *testing.T) {
			sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain
			notaryRequest1 = createValidNotaryRequest(chain, sender, 3, 2_0000_0000, nil)
			nrBytes, err := notaryRequest1.Bytes()
			require.NoError(t, err)
			str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes))
			submitNotaryRequest(t, false, 0, str)(t)
		})
	})

	t.Run("getrawnotarytransaction", func(t *testing.T) {
		t.Run("invalid param", func(t *testing.T) {
			req := fmt.Sprintf(rpcTx, "invalid", 1)
			body := doRPCCallOverHTTP(req, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("unknown transaction", func(t *testing.T) {
			req := fmt.Sprintf(rpcTx, (util.Uint256{0, 0, 0}).StringLE(), 1)
			body := doRPCCallOverHTTP(req, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrUnknownTransactionCode)
		})

		checkGetTxVerbose := func(t *testing.T, tx *transaction.Transaction) {
			req := fmt.Sprintf(rpcTx, tx.Hash().StringLE(), 1)
			body := doRPCCallOverHTTP(req, httpSrv.URL, t)
			res := checkErrGetResult(t, body, false, 0)
			actual := new(transaction.Transaction)
			require.NoError(t, json.Unmarshal(res, actual))
			_ = tx.Size()
			require.Equal(t, tx, actual)
		}
		t.Run("mainTx verbose", func(t *testing.T) {
			checkGetTxVerbose(t, notaryRequest1.MainTransaction)
		})
		t.Run("fallbackTx verbose", func(t *testing.T) {
			checkGetTxVerbose(t, notaryRequest1.FallbackTransaction)
			checkGetTxVerbose(t, notaryRequest2.FallbackTransaction)
		})

		checkGetTxBytes := func(t *testing.T, tx *transaction.Transaction) {
			req := fmt.Sprintf(rpcTx, tx.Hash().StringLE(), 0)
			body := doRPCCallOverHTTP(req, httpSrv.URL, t)
			res := checkErrGetResult(t, body, false, 0)

			var s string
			err := json.Unmarshal(res, &s)
			require.NoErrorf(t, err, "could not parse response: %s", res)
			txBin, err := testserdes.EncodeBinary(tx)
			require.NoError(t, err)
			expected := base64.StdEncoding.EncodeToString(txBin)
			assert.Equal(t, expected, s)
		}
		t.Run("mainTx bytes", func(t *testing.T) {
			checkGetTxBytes(t, notaryRequest1.MainTransaction)
		})
		t.Run("fallbackTx bytes", func(t *testing.T) {
			checkGetTxBytes(t, notaryRequest1.FallbackTransaction)
			checkGetTxBytes(t, notaryRequest2.FallbackTransaction)
		})
	})
}

// createValidNotaryRequest creates and signs P2PNotaryRequest payload which can
// pass verification. It uses the provided mainTx if it's a nonempty structure.
func createValidNotaryRequest(chain *core.Blockchain, sender *keys.PrivateKey, nonce uint32, networkFee int64, mainTx *transaction.Transaction) *payload.P2PNotaryRequest {
	h := chain.BlockHeight()
	// If mainTx is nil, then generate it.
	if mainTx == nil {
		mainTx = &transaction.Transaction{
			Nonce:           nonce,
			Attributes:      []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
			Script:          []byte{byte(opcode.RET)},
			ValidUntilBlock: h + 100,
			Signers:         []transaction.Signer{{Account: sender.GetScriptHash()}},
			Scripts: []transaction.Witness{{
				InvocationScript:   []byte{1, 4, 7},
				VerificationScript: []byte{3, 6, 9},
			}},
		}
	}
	fallbackTx := &transaction.Transaction{
		Script:          []byte{byte(opcode.RET)},
		ValidUntilBlock: h + 100,
		Attributes: []transaction.Attribute{
			{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: h + 50}},
			{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
			{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
		},
		Signers: []transaction.Signer{{Account: nativehashes.Notary}, {Account: sender.GetScriptHash()}},
		Scripts: []transaction.Witness{
			{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: []byte{}},
		},
		NetworkFee: networkFee,
	}
	fallbackTx.Scripts = append(fallbackTx.Scripts, transaction.Witness{
		InvocationScript:   append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, sender.SignHashable(uint32(testchain.Network()), fallbackTx)...),
		VerificationScript: sender.PublicKey().GetVerificationScript(),
	})
	p := &payload.P2PNotaryRequest{
		MainTransaction:     mainTx,
		FallbackTransaction: fallbackTx,
	}
	p.Witness = transaction.Witness{
		InvocationScript:   append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, sender.SignHashable(uint32(testchain.Network()), p)...),
		VerificationScript: sender.PublicKey().GetVerificationScript(),
	}
	return p
}

func runTestCasesWithExecutor(t *testing.T, e *executor, rpcCall string, method string, testCases []rpcTestCase, doRPCCall func(string, string, *testing.T) []byte, checkErrResult func(t *testing.T, body []byte, expectingFail bool, expectedErrCode int64, expectedErr ...string) json.RawMessage) {
	t.Run(method, func(t *testing.T) {
		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				body := doRPCCall(fmt.Sprintf(rpcCall, method, tc.params), e.httpSrv.URL, t)
				result := checkErrResult(t, body, tc.fail, tc.errCode)
				if tc.fail {
					return
				}

				expected, res := tc.getResultPair(e)
				err := json.Unmarshal(result, res)
				require.NoErrorf(t, err, "could not parse response: %s", result)

				if tc.check == nil {
					assert.Equal(t, expected, res)
				} else {
					tc.check(t, e, res)
				}
			})
		}
	})
}

// testRPCProtocol runs a full set of tests using given callback to make actual
// calls. Some tests change the chain state, thus we reinitialize the chain from
// scratch here.
func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []byte) {
	chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)

	e := &executor{chain: chain, httpSrv: httpSrv}
	t.Run("single request", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "%s", "params": %s}`
		for method, cases := range rpcTestCases {
			runTestCasesWithExecutor(t, e, rpc, method, cases, doRPCCall, checkErrGetResult)
		}
	})
	t.Run("batch with single request", func(t *testing.T) {
		for method, cases := range rpcTestCases {
			if method == "sendrawtransaction" {
				continue // cannot send the same transaction twice
			}
			rpc := `[{"jsonrpc": "2.0", "id": 1, "method": "%s", "params": %s}]`
			runTestCasesWithExecutor(t, e, rpc, method, cases, doRPCCall, checkErrGetBatchResult)
		}
	})

	t.Run("batch with multiple requests", func(t *testing.T) {
		for method, cases := range rpcTestCases {
			if method == "sendrawtransaction" {
				continue // cannot send the same transaction twice
			}
			t.Run(method, func(t *testing.T) {
				rpc := `{"jsonrpc": "2.0", "id": %d, "method": "%s", "params": %s},`
				var resultRPC string
				for i, tc := range cases {
					resultRPC += fmt.Sprintf(rpc, i, method, tc.params)
				}
				resultRPC = `[` + resultRPC[:len(resultRPC)-1] + `]`
				body := doRPCCall(resultRPC, httpSrv.URL, t)
				var responses []tutusrpc.Response
				err := json.Unmarshal(body, &responses)
				require.Nil(t, err)
				for i, tc := range cases {
					var resp tutusrpc.Response
					for _, r := range responses {
						if bytes.Equal(r.ID, []byte(strconv.Itoa(i))) {
							resp = r
							break
						}
					}
					if tc.fail {
						require.NotNil(t, resp.Error)
						assert.NotEqual(t, 0, resp.Error.Code)
						assert.NotEqual(t, "", resp.Error.Message)
					} else {
						assert.Nil(t, resp.Error)
					}
					if tc.fail {
						return
					}
					expected, res := tc.getResultPair(e)
					err := json.Unmarshal(resp.Result, res)
					require.NoErrorf(t, err, "could not parse response: %s", resp.Result)

					if tc.check == nil {
						assert.Equal(t, expected, res)
					} else {
						tc.check(t, e, res)
					}
				}
			})
		}
	})

	t.Run("getapplicationlog for block", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getapplicationlog", "params": ["%s"]}`
		body := doRPCCall(fmt.Sprintf(rpc, e.chain.GetHeaderHash(1).StringLE()), httpSrv.URL, t)
		data := checkErrGetResult(t, body, false, 0)
		var res result.ApplicationLog
		require.NoError(t, json.Unmarshal(data, &res))
		require.Equal(t, 2, len(res.Executions))
		require.Equal(t, trigger.OnPersist, res.Executions[0].Trigger)
		require.Equal(t, vmstate.Halt, res.Executions[0].VMState)
		require.Equal(t, trigger.PostPersist, res.Executions[1].Trigger)
		require.Equal(t, vmstate.Halt, res.Executions[1].VMState)
	})
	t.Run("getproof", func(t *testing.T) {
		r, err := chain.GetStateModule().GetStateRoot(3)
		require.NoError(t, err)

		rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getproof", "params": ["%s", "%s", "%s"]}`,
			r.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("testkey")))
		body := doRPCCall(rpc, httpSrv.URL, t)
		rawRes := checkErrGetResult(t, body, false, 0)
		res := new(result.ProofWithKey)
		require.NoError(t, json.Unmarshal(rawRes, res))
		h, _ := util.Uint160DecodeStringLE(testContractHashLE)
		skey := makeStorageKey(chain.GetContractState(h).ID, []byte("testkey"))
		require.Equal(t, skey, res.Key)
		require.True(t, len(res.Proof) > 0)

		rpc = fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "verifyproof", "params": ["%s", "%s"]}`,
			r.Root.StringLE(), res.String())
		body = doRPCCall(rpc, httpSrv.URL, t)
		rawRes = checkErrGetResult(t, body, false, 0)
		vp := new(result.VerifyProof)
		require.NoError(t, json.Unmarshal(rawRes, vp))
		require.Equal(t, []byte("testvalue"), vp.Value)
	})
	t.Run("getstateroot", func(t *testing.T) {
		testRoot := func(t *testing.T, p string) {
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getstateroot", "params": [%s]}`, p)
			body := doRPCCall(rpc, httpSrv.URL, t)
			rawRes := checkErrGetResult(t, body, false, 0)

			res := &state.MPTRoot{}
			require.NoError(t, json.Unmarshal(rawRes, res))
			require.NotEqual(t, util.Uint256{}, res.Root) // be sure this test uses valid height

			expected, err := e.chain.GetStateModule().GetStateRoot(5)
			require.NoError(t, err)
			require.Equal(t, expected, res)
		}
		t.Run("ByHeight", func(t *testing.T) { testRoot(t, strconv.FormatInt(5, 10)) })
		t.Run("ByHash", func(t *testing.T) { testRoot(t, `"`+chain.GetHeaderHash(5).StringLE()+`"`) })
		t.Run("21", func(t *testing.T) {
			rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getstateroot", "params": [21]}`
			body := doRPCCall(rpc, httpSrv.URL, t)
			rawRes := checkErrGetResult(t, body, false, 0)

			res := &state.MPTRoot{}
			require.NoError(t, json.Unmarshal(rawRes, res))
			require.Equal(t, block21StateRootLE, res.Root.StringLE())
		})
	})
	t.Run("getstate", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getstate", "params": [%s]}`
		testGetState := func(t *testing.T, p string, expected string) {
			body := doRPCCall(fmt.Sprintf(rpc, p), httpSrv.URL, t)
			rawRes := checkErrGetResult(t, body, false, 0)

			var actual string
			require.NoError(t, json.Unmarshal(rawRes, &actual))
			require.Equal(t, expected, actual)
		}
		t.Run("good: historical state", func(t *testing.T) {
			root, err := e.chain.GetStateModule().GetStateRoot(4)
			require.NoError(t, err)
			// `testkey`-`testvalue` pair was put to the contract storage at block #3
			params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("testkey")))
			testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("testvalue")))
		})
		t.Run("negative: invalid key", func(t *testing.T) {
			root, err := e.chain.GetStateModule().GetStateRoot(4)
			require.NoError(t, err)
			// `testkey`-`testvalue` pair was put to the contract storage at block #3
			params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("invalidkey")))
			body := doRPCCall(fmt.Sprintf(rpc, params), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("good: fresh state", func(t *testing.T) {
			root, err := e.chain.GetStateModule().GetStateRoot(17)
			require.NoError(t, err)
			// `testkey`-`newtestvalue` pair was put to the contract storage at block #17
			params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("testkey")))
			testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("newtestvalue")))
		})
	})
	t.Run("findstates", func(t *testing.T) {
		testFindStates := func(t *testing.T, p string, root util.Uint256, expected result.FindStates) {
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "findstates", "params": [%s]}`, p)
			body := doRPCCall(rpc, httpSrv.URL, t)
			rawRes := checkErrGetResult(t, body, false, 0)

			var actual result.FindStates
			require.NoError(t, json.Unmarshal(rawRes, &actual))
			require.Equal(t, expected.Results, actual.Results)

			checkProof := func(t *testing.T, proof *result.ProofWithKey, value []byte) {
				rpc = fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "verifyproof", "params": ["%s", "%s"]}`,
					root.StringLE(), proof.String())
				body = doRPCCall(rpc, httpSrv.URL, t)
				rawRes = checkErrGetResult(t, body, false, 0)
				vp := new(result.VerifyProof)
				require.NoError(t, json.Unmarshal(rawRes, vp))
				require.Equal(t, value, vp.Value)
			}
			if len(actual.Results) > 0 {
				checkProof(t, actual.FirstProof, actual.Results[0].Value)
			}
			if len(actual.Results) > 1 {
				checkProof(t, actual.LastProof, actual.Results[len(actual.Results)-1].Value)
			}
			require.Equal(t, expected.Truncated, actual.Truncated)
		}
		t.Run("good: no prefix, no limit", func(t *testing.T) {
			// pairs for this test where put to the contract storage at block #17
			root, err := e.chain.GetStateModule().GetStateRoot(17)
			require.NoError(t, err)
			params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")))
			testFindStates(t, params, root.Root, result.FindStates{
				Results: []result.KeyValue{
					{Key: []byte("aa10"), Value: []byte("v2")},
					{Key: []byte("aa50"), Value: []byte("v3")},
					{Key: []byte("aa"), Value: []byte("v1")},
				},
				Truncated: false,
			})
		})
		t.Run("good: empty prefix, no limit", func(t *testing.T) {
			// empty prefix should be considered as no prefix specified.
			root, err := e.chain.GetStateModule().GetStateRoot(17)
			require.NoError(t, err)
			params := fmt.Sprintf(`"%s", "%s", "%s", ""`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")))
			testFindStates(t, params, root.Root, result.FindStates{
				Results: []result.KeyValue{
					{Key: []byte("aa10"), Value: []byte("v2")},
					{Key: []byte("aa50"), Value: []byte("v3")},
					{Key: []byte("aa"), Value: []byte("v1")},
				},
				Truncated: false,
			})
		})
		t.Run("good: empty prefix, no limit, no data", func(t *testing.T) {
			// empty prefix should be considered as no prefix specified.
			root, err := e.chain.GetStateModule().GetStateRoot(21)
			require.NoError(t, err)
			stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) // It has no data.
			params := fmt.Sprintf(`"%s", "%s", ""`, root.Root.StringLE(), stdHash.StringLE())
			testFindStates(t, params, root.Root, result.FindStates{
				Results:   []result.KeyValue{},
				Truncated: false,
			})
		})
		t.Run("good: with prefix, no limit", func(t *testing.T) {
			// pairs for this test where put to the contract storage at block #17
			root, err := e.chain.GetStateModule().GetStateRoot(17)
			require.NoError(t, err)
			params := fmt.Sprintf(`"%s", "%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa10")))
			testFindStates(t, params, root.Root, result.FindStates{
				Results: []result.KeyValue{
					{Key: []byte("aa50"), Value: []byte("v3")},
				},
				Truncated: false,
			})
		})
		t.Run("good: empty prefix, with limit", func(t *testing.T) {
			for limit := 2; limit < 5; limit++ {
				// pairs for this test where put to the contract storage at block #17
				root, err := e.chain.GetStateModule().GetStateRoot(17)
				require.NoError(t, err)
				params := fmt.Sprintf(`"%s", "%s", "%s", "", %d`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")), limit)
				expected := result.FindStates{
					Results: []result.KeyValue{
						{Key: []byte("aa10"), Value: []byte("v2")},
						{Key: []byte("aa50"), Value: []byte("v3")},
					},
					Truncated: limit == 2,
				}
				if limit != 2 {
					expected.Results = append(expected.Results, result.KeyValue{Key: []byte("aa"), Value: []byte("v1")})
				}
				testFindStates(t, params, root.Root, expected)
			}
		})
		t.Run("good: with prefix, with limit", func(t *testing.T) {
			// pairs for this test where put to the contract storage at block #17
			root, err := e.chain.GetStateModule().GetStateRoot(17)
			require.NoError(t, err)
			params := fmt.Sprintf(`"%s", "%s", "%s", "%s", %d`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa00")), 1)
			testFindStates(t, params, root.Root, result.FindStates{
				Results: []result.KeyValue{
					{Key: []byte("aa10"), Value: []byte("v2")},
				},
				Truncated: true,
			})
		})
	})

	t.Run("getrawtransaction", func(t *testing.T) {
		block, _ := chain.GetBlock(chain.GetHeaderHash(1))
		tx := block.Transactions[0]
		rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["%s"]}"`, tx.Hash().StringLE())
		body := doRPCCall(rpc, httpSrv.URL, t)
		result := checkErrGetResult(t, body, false, 0)
		var res string
		err := json.Unmarshal(result, &res)
		require.NoErrorf(t, err, "could not parse response: %s", result)
		txBin, err := testserdes.EncodeBinary(tx)
		require.NoError(t, err)
		expected := base64.StdEncoding.EncodeToString(txBin)
		assert.Equal(t, expected, res)
	})

	t.Run("getrawtransaction 2 arguments", func(t *testing.T) {
		block, _ := chain.GetBlock(chain.GetHeaderHash(1))
		tx := block.Transactions[0]
		rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["%s", 0]}"`, tx.Hash().StringLE())
		body := doRPCCall(rpc, httpSrv.URL, t)
		result := checkErrGetResult(t, body, false, 0)
		var res string
		err := json.Unmarshal(result, &res)
		require.NoErrorf(t, err, "could not parse response: %s", result)
		txBin, err := testserdes.EncodeBinary(tx)
		require.NoError(t, err)
		expected := base64.StdEncoding.EncodeToString(txBin)
		assert.Equal(t, expected, res)
	})

	t.Run("getrawtransaction 2 arguments, verbose", func(t *testing.T) {
		block, _ := chain.GetBlock(chain.GetHeaderHash(1))
		TXHash := block.Transactions[0].Hash()
		_ = block.Transactions[0].Size()
		rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["%s", 1]}"`, TXHash.StringLE())
		body := doRPCCall(rpc, httpSrv.URL, t)
		txOut := checkErrGetResult(t, body, false, 0)
		actual := result.TransactionOutputRaw{Transaction: transaction.Transaction{}}
		err := json.Unmarshal(txOut, &actual)
		require.NoErrorf(t, err, "could not parse response: %s", txOut)

		assert.Equal(t, *block.Transactions[0], actual.Transaction)
		assert.Equal(t, 23, actual.Confirmations)
		assert.Equal(t, TXHash, actual.Hash())
	})

	t.Run("getblockheader_positive", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getblockheader", "params": %s}`
		testHeaderHash := chain.GetHeaderHash(1).StringLE()
		hdr := e.getHeader(testHeaderHash)

		runCase := func(t *testing.T, rpc string, expected, actual any) {
			body := doRPCCall(rpc, httpSrv.URL, t)
			data := checkErrGetResult(t, body, false, 0)
			require.NoError(t, json.Unmarshal(data, actual))
			require.Equal(t, expected, actual)
		}

		t.Run("no verbose", func(t *testing.T) {
			w := io.NewBufBinWriter()
			hdr.EncodeBinary(w.BinWriter)
			require.NoError(t, w.Err)
			encoded := base64.StdEncoding.EncodeToString(w.Bytes())

			t.Run("missing", func(t *testing.T) {
				runCase(t, fmt.Sprintf(rpc, `["`+testHeaderHash+`"]`), &encoded, new(string))
			})

			t.Run("verbose=0", func(t *testing.T) {
				runCase(t, fmt.Sprintf(rpc, `["`+testHeaderHash+`", 0]`), &encoded, new(string))
			})

			t.Run("by number", func(t *testing.T) {
				runCase(t, fmt.Sprintf(rpc, `[1]`), &encoded, new(string))
			})
		})

		t.Run("verbose != 0", func(t *testing.T) {
			nextHash := chain.GetHeaderHash(hdr.Index + 1)
			expected := &result.Header{
				Header: *hdr,
				BlockMetadata: result.BlockMetadata{
					Size:          io.GetVarSize(hdr),
					NextBlockHash: &nextHash,
					Confirmations: e.chain.BlockHeight() - hdr.Index + 1,
				},
			}

			rpc := fmt.Sprintf(rpc, `["`+testHeaderHash+`", 2]`)
			runCase(t, rpc, expected, new(result.Header))
		})
	})

	t.Run("getrawmempool", func(t *testing.T) {
		mp := chain.GetMemPool()
		// `expected` stores hashes of previously added txs
		expected := make([]util.Uint256, 0)
		for _, tx := range mp.GetVerifiedTransactions() {
			expected = append(expected, tx.Hash())
		}
		for range 5 {
			tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
			tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
			assert.NoError(t, mp.Add(tx, &FeerStub{}))
			expected = append(expected, tx.Hash())
		}

		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getrawmempool", "params": []}`
		body := doRPCCall(rpc, httpSrv.URL, t)
		res := checkErrGetResult(t, body, false, 0)

		var actual []util.Uint256
		err := json.Unmarshal(res, &actual)
		require.NoErrorf(t, err, "could not parse response: %s", res)

		assert.ElementsMatch(t, expected, actual)
	})

	t.Run("getnep17transfers", func(t *testing.T) {
		testNEP17T := func(t *testing.T, start, stop, limit, page int, sent, rcvd []int) {
			ps := []string{`"` + testchain.PrivateKeyByID(0).Address() + `"`}
			if start != 0 {
				h, err := e.chain.GetHeader(e.chain.GetHeaderHash(uint32(start)))
				var ts uint64
				if err == nil {
					ts = h.Timestamp
				} else {
					ts = uint64(time.Now().UnixNano() / 1_000_000)
				}
				ps = append(ps, strconv.FormatUint(ts, 10))
			}
			if stop != 0 {
				h, err := e.chain.GetHeader(e.chain.GetHeaderHash(uint32(stop)))
				var ts uint64
				if err == nil {
					ts = h.Timestamp
				} else {
					ts = uint64(time.Now().UnixNano() / 1_000_000)
				}
				ps = append(ps, strconv.FormatUint(ts, 10))
			}
			if limit != 0 {
				ps = append(ps, strconv.FormatInt(int64(limit), 10))
			}
			if page != 0 {
				ps = append(ps, strconv.FormatInt(int64(page), 10))
			}
			p := strings.Join(ps, ", ")
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getnep17transfers", "params": [%s]}`, p)
			body := doRPCCall(rpc, httpSrv.URL, t)
			res := checkErrGetResult(t, body, false, 0)
			actual := new(result.NEP17Transfers)
			require.NoError(t, json.Unmarshal(res, actual))
			checkNep17TransfersAux(t, e, actual, sent, rcvd)
		}
		t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{21, 22, 23, 24}, []int{4, 5}) })
		t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) })
		t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{18, 19}, []int{3}) })
		t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{21}, []int{4}) })
		t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{19, 20}, []int{4}) })
		t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{21, 22}, []int{5}) })
	})

	prepareIteratorSession := func(t *testing.T) (uuid.UUID, uuid.UUID) {
		rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "invokefunction", "params": ["%s", "iterateOverValues"]}"`, storageContractHash)
		body := doRPCCall(rpc, httpSrv.URL, t)
		resp := checkErrGetResult(t, body, false, 0)
		res := new(result.Invoke)
		err := json.Unmarshal(resp, &res)
		require.NoErrorf(t, err, "could not parse response: %s", resp)
		require.NotEmpty(t, res.Session)
		require.Equal(t, 1, len(res.Stack))
		require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
		iterator, ok := res.Stack[0].Value().(result.Iterator)
		require.True(t, ok)
		require.NotEmpty(t, iterator.ID)
		return res.Session, *iterator.ID
	}
	t.Run("traverseiterator", func(t *testing.T) {
		t.Run("sessions disabled", func(t *testing.T) {
			_, _, httpSrv2 := initClearServerWithCustomConfig(t, func(c *config.Config) {
				c.ApplicationConfiguration.RPC.SessionEnabled = false
			})
			body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": []}"`, httpSrv2.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrSessionsDisabledCode)
		})
		t.Run("good", func(t *testing.T) {
			sID, iID := prepareIteratorSession(t)
			expectedCount := 99
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), iID.String(), expectedCount)
			body := doRPCCall(rpc, httpSrv.URL, t)
			resp := checkErrGetResult(t, body, false, 0)
			res := new([]json.RawMessage)
			require.NoError(t, json.Unmarshal(resp, res))
			require.Equal(t, expectedCount, len(*res))
		})
		t.Run("invalid session id", func(t *testing.T) {
			_, iID := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["not-a-uuid", "%s", %d]}"`, iID.String(), 1)
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "invalid session ID: not a valid UUID")
		})
		t.Run("invalid iterator id", func(t *testing.T) {
			sID, _ := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "not-a-uuid", %d]}"`, sID.String(), 1)
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "invalid iterator ID: not a valid UUID")
		})
		t.Run("invalid items count", func(t *testing.T) {
			sID, iID := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s"]}"`, sID.String(), iID.String())
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "invalid iterator items count")
		})
		t.Run("items count is not an int32", func(t *testing.T) {
			sID, iID := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), iID.String(), math.MaxInt32+1)
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "invalid iterator items count: not an int32")
		})
		t.Run("count is out of range", func(t *testing.T) {
			sID, iID := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), iID.String(), config.DefaultMaxIteratorResultItems+1)
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, fmt.Sprintf("iterator items count (%d) is out of range (%d at max)", config.DefaultMaxIteratorResultItems+1, config.DefaultMaxIteratorResultItems))
		})
		t.Run("unknown session", func(t *testing.T) {
			_, iID := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, uuid.NewString(), iID.String(), 1)
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrUnknownSessionCode)
		})
		t.Run("unknown iterator", func(t *testing.T) {
			sID, _ := prepareIteratorSession(t)
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), uuid.NewString(), 1)
			body := doRPCCall(rpc, httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrUnknownIteratorCode)
		})
		t.Run("sessions expansion enabled", func(t *testing.T) {
			chain2, _, httpSrv2 := initClearServerWithCustomConfig(t, func(c *config.Config) {
				c.ApplicationConfiguration.RPC.SessionEnabled = true
				c.ApplicationConfiguration.RPC.SessionExpansionEnabled = true
			})
			for _, b := range getTestBlocks(t) {
				require.NoError(t, chain2.AddBlock(b))
			}
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "invokefunction", "params": ["%s", "iterateOverValues"]}"`, storageContractHash)
			body := doRPCCall(rpc, httpSrv2.URL, t)
			resp := checkErrGetResult(t, body, false, 0)
			res := new(result.Invoke)
			err := json.Unmarshal(resp, &res)
			require.NoErrorf(t, err, "could not parse response: %s", resp)
			require.NotEmpty(t, res.Session)
			require.Equal(t, 1, len(res.Stack))
			require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
			iterator, ok := res.Stack[0].Value().(result.Iterator)
			require.True(t, ok)
			require.NotEmpty(t, iterator.ID)

			expectedCount := 0
			rpc = fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, res.Session.String(), iterator.ID.String(), expectedCount)
			body = doRPCCall(rpc, httpSrv2.URL, t)
			resp = checkErrGetResult(t, body, false, 0)
			resResp := new([]json.RawMessage)
			require.NoError(t, json.Unmarshal(resp, resResp))
			require.Equal(t, expectedCount, len(*resResp))
		})
	})
	t.Run("terminatesession", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "terminatesession", "params": ["%s"]}"`
		t.Run("sessions disabled", func(t *testing.T) {
			_, _, httpSrv2 := initClearServerWithCustomConfig(t, func(c *config.Config) {
				c.ApplicationConfiguration.RPC.SessionEnabled = false
			})
			body := doRPCCall(fmt.Sprintf(rpc, uuid.NewString()), httpSrv2.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrSessionsDisabledCode)
		})
		t.Run("true", func(t *testing.T) {
			sID, _ := prepareIteratorSession(t)
			body := doRPCCall(fmt.Sprintf(rpc, sID.String()), httpSrv.URL, t)
			resp := checkErrGetResult(t, body, false, 0)
			res := new(bool)
			require.NoError(t, json.Unmarshal(resp, res))
			require.Equal(t, true, *res)
		})
		t.Run("false", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, uuid.NewString()), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrUnknownSessionCode)
		})
		t.Run("expired", func(t *testing.T) {
			_, _ = prepareIteratorSession(t)
			// Wait until session is terminated by timer.
			require.Eventually(t, func() bool {
				rpcSrv.sessionsLock.Lock()
				defer rpcSrv.sessionsLock.Unlock()
				return len(rpcSrv.sessions) == 0
			}, 2*rpcSrv.config.SessionLifetime, 10*time.Millisecond)
		})
	})
	t.Run("calculatenetworkfee", func(t *testing.T) {
		t.Run("no parameters", func(t *testing.T) {
			body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": []}"`, httpSrv.URL, t)
			_ = checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "Invalid params")
		})
		t.Run("non-base64 parameter", func(t *testing.T) {
			body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": ["noatbase64"]}"`, httpSrv.URL, t)
			_ = checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "Invalid params")
		})
		t.Run("non-transaction parameter", func(t *testing.T) {
			body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": ["bm90IGEgdHJhbnNhY3Rpb24K"]}"`, httpSrv.URL, t)
			_ = checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode, "Invalid params")
		})
		calcReqExactly := func(t *testing.T, tx string) []byte {
			rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": ["%s"]}"`, tx)
			return doRPCCall(rpc, httpSrv.URL, t)
		}
		calcReq := func(t *testing.T, tx *transaction.Transaction) []byte {
			return calcReqExactly(t, base64.StdEncoding.EncodeToString(tx.Bytes()))
		}
		t.Run("non-contract with zero verification", func(t *testing.T) {
			tx := &transaction.Transaction{
				Script:  []byte{byte(opcode.RET)},
				Signers: []transaction.Signer{{Account: util.Uint160{1, 2, 3}, Scopes: transaction.CalledByEntry}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{},
					VerificationScript: []byte{},
				}},
			}
			body := calcReq(t, tx)
			_ = checkErrGetResult(t, body, true, tutusrpc.ErrInvalidVerificationFunctionCode, fmt.Sprintf("signer 0 (%s) has no verification script and no deployed contract", address.Uint160ToString(util.Uint160{1, 2, 3})))
		})
		t.Run("contract with no verify", func(t *testing.T) {
			tx := &transaction.Transaction{
				Script:  []byte{byte(opcode.RET)},
				Signers: []transaction.Signer{{Account: nnsHash, Scopes: transaction.CalledByEntry}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{},
					VerificationScript: []byte{},
				}},
			}
			body := calcReq(t, tx)
			_ = checkErrGetResult(t, body, true, tutusrpc.ErrInvalidVerificationFunctionCode, "signer 0 has no verify method in deployed contract")
		})
		t.Run("execution limit, fail", func(t *testing.T) {
			// 1_6000_0000 GAS with the default 1.5 allowed by Policy
			verifScript := []byte{byte(opcode.PUSHINT32), 0x00, 0x58, 0x89, 0x09, byte(opcode.SYSCALL), 0, 0, 0, 0, byte(opcode.PUSHT)}
			binary.LittleEndian.PutUint32(verifScript[6:], interopnames.ToID([]byte(interopnames.SystemRuntimeBurnGas)))
			tx := &transaction.Transaction{
				Script:  []byte{byte(opcode.RET)},
				Signers: []transaction.Signer{{Account: hash.Hash160(verifScript)}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{byte(opcode.NOP)},
					VerificationScript: verifScript,
				}},
			}
			body := calcReq(t, tx)
			_ = checkErrGetResult(t, body, true, tutusrpc.ErrInvalidSignatureCode, "GAS limit exceeded")
		})
		checkCalc := func(t *testing.T, tx *transaction.Transaction, fee int64) {
			resp := checkErrGetResult(t, calcReq(t, tx), false, 0)
			res := new(result.NetworkFee)
			require.NoError(t, json.Unmarshal(resp, res))
			require.Equal(t, fee, res.Value)
		}
		t.Run("simple GAS transfer", func(t *testing.T) {
			priv0 := testchain.PrivateKeyByID(0)
			script, err := smartcontract.CreateCallWithAssertScript(chain.UtilityTokenHash(), "transfer",
				priv0.GetScriptHash(), priv0.GetScriptHash(), 1, nil)
			require.NoError(t, err)
			tx := &transaction.Transaction{
				Script:  script,
				Signers: []transaction.Signer{{Account: priv0.GetScriptHash(), Scopes: transaction.CalledByEntry}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{},
					VerificationScript: priv0.PublicKey().GetVerificationScript(),
				}},
			}
			checkCalc(t, tx, 1228520) // Perfectly matches FeeIsSignatureContractDetailed() C# test.
		})
		t.Run("multisignature tx", func(t *testing.T) {
			priv0 := testchain.PrivateKeyByID(0)
			priv1 := testchain.PrivateKeyByID(1)
			accScript, err := smartcontract.CreateDefaultMultiSigRedeemScript(keys.PublicKeys{priv0.PublicKey(), priv1.PublicKey()})
			require.NoError(t, err)
			multiAcc := hash.Hash160(accScript)
			txScript, err := smartcontract.CreateCallWithAssertScript(chain.UtilityTokenHash(), "transfer",
				multiAcc, priv0.GetScriptHash(), 1, nil)
			require.NoError(t, err)
			tx := &transaction.Transaction{
				Script:  txScript,
				Signers: []transaction.Signer{{Account: multiAcc, Scopes: transaction.CalledByEntry}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{},
					VerificationScript: accScript,
				}},
			}
			checkCalc(t, tx, 2315100) // Perfectly matches FeeIsMultiSigContract() C# test.
		})
		checkContract := func(t *testing.T, verAcc util.Uint160, invoc []byte, fee int64) {
			txScript, err := smartcontract.CreateCallWithAssertScript(chain.UtilityTokenHash(), "transfer",
				verAcc, verAcc, 1, nil)
			require.NoError(t, err)
			tx := &transaction.Transaction{
				Script:  txScript,
				Signers: []transaction.Signer{{Account: verAcc, Scopes: transaction.CalledByEntry}},
				Scripts: []transaction.Witness{{
					InvocationScript:   invoc,
					VerificationScript: []byte{},
				}},
			}
			checkCalc(t, tx, fee)
		}
		t.Run("contract-based verification", func(t *testing.T) {
			verAcc, err := util.Uint160DecodeStringLE(verifyContractHash)
			require.NoError(t, err)
			checkContract(t, verAcc, []byte{}, 636610) // No C# match, but we believe it's OK.
		})
		t.Run("contract-based verification with parameters", func(t *testing.T) {
			verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash)
			require.NoError(t, err)
			checkContract(t, verAcc, []byte{}, 244010) // No C# match, but we believe it's OK and it differs from the one above.
		})
		t.Run("contract-based verification with invocation script", func(t *testing.T) {
			verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash)
			require.NoError(t, err)
			invocWriter := io.NewBufBinWriter()
			emit.Bool(invocWriter.BinWriter, false)
			emit.Int(invocWriter.BinWriter, 5)
			emit.String(invocWriter.BinWriter, "")
			invocScript := invocWriter.Bytes()
			checkContract(t, verAcc, invocScript, 146840) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side.
		})
		t.Run("execution limit, ok", func(t *testing.T) {
			// 1_4000_0000 GAS with the default 1.5 allowed by Policy
			verifScript := []byte{byte(opcode.PUSHINT32), 0x00, 0x3b, 0x58, 0x08, byte(opcode.SYSCALL), 0, 0, 0, 0, byte(opcode.PUSHT)}
			binary.LittleEndian.PutUint32(verifScript[6:], interopnames.ToID([]byte(interopnames.SystemRuntimeBurnGas)))
			tx := &transaction.Transaction{
				Script:  []byte{byte(opcode.RET)},
				Signers: []transaction.Signer{{Account: hash.Hash160(verifScript)}},
				Scripts: []transaction.Witness{{
					InvocationScript:   []byte{byte(opcode.NOP)},
					VerificationScript: verifScript,
				}},
			}
			checkCalc(t, tx, 140065570)
		})
	})
	t.Run("sendrawtransaction", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "sendrawtransaction", "params": ["%s"]}`
		t.Run("invalid signature", func(t *testing.T) {
			tx := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 1, false)
			tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10]
			rawTx := encodeBinaryToString(t, tx)
			body := doRPCCall(fmt.Sprintf(rpc, rawTx), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrInvalidSignatureCode)
		})
		t.Run("too big tx", func(t *testing.T) {
			script := make([]byte, transaction.MaxScriptLength)
			for i := range script {
				script[i] = byte(opcode.PUSH0)
			}
			groups := make([]*keys.PublicKey, 16)
			for i := range groups {
				pk, _ := keys.NewPrivateKey()
				groups[i] = pk.PublicKey()
			}
			signers := make([]transaction.Signer, transaction.MaxAttributes)
			for i := range signers {
				signers[i] = transaction.Signer{
					Account:          random.Uint160(),
					Scopes:           transaction.CustomContracts | transaction.CustomGroups,
					AllowedContracts: make([]util.Uint160, 16),
					AllowedGroups:    groups,
				}
			}
			scripts := make([]transaction.Witness, len(signers))
			for i := range scripts {
				scripts[i] = transaction.Witness{
					InvocationScript:   random.Bytes(transaction.MaxInvocationScript),
					VerificationScript: random.Bytes(transaction.MaxVerificationScript),
				}
			}
			tx := &transaction.Transaction{
				ValidUntilBlock: chain.BlockHeight() + 1,
				Script:          script,
				Attributes:      []transaction.Attribute{},
				Signers:         signers,
				Scripts:         scripts,
			}
			rawTx := encodeBinaryToString(t, tx)
			body := doRPCCall(fmt.Sprintf(rpc, rawTx), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.ErrInvalidSizeCode)
		})
		t.Run("mempool OOM", func(t *testing.T) {
			chain, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
				c.ProtocolConfiguration.MemPoolSize = 1
			})

			// create and push the first (prioritized) transaction with increased networkFee
			tx := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 2, false)
			rawTx := encodeBinaryToString(t, tx)
			body := doRPCCall(fmt.Sprintf(rpc, rawTx), httpSrv.URL, t)
			checkErrGetResult(t, body, false, 0)

			// create and push the second transaction with standard networkFee
			tx2 := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 1, false)
			rawTx2 := encodeBinaryToString(t, tx2)
			body2 := doRPCCall(fmt.Sprintf(rpc, rawTx2), httpSrv.URL, t)
			checkErrGetResult(t, body2, true, tutusrpc.ErrMempoolCapReachedCode)
		})
		t.Run("mempool conflict", func(t *testing.T) {
			chain, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
				c.ProtocolConfiguration.MemPoolSize = 2
			})

			// Create and push the first transaction with large network fee.
			tx := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 25, false)
			rawTx := encodeBinaryToString(t, tx)
			body := doRPCCall(fmt.Sprintf(rpc, rawTx), httpSrv.URL, t)
			checkErrGetResult(t, body, false, 0)

			// Create and push the second transaction, sender doesn't have enough balance to pay for two transactions.
			tx2 := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 25, false)
			rawTx2 := encodeBinaryToString(t, tx2)
			body2 := doRPCCall(fmt.Sprintf(rpc, rawTx2), httpSrv.URL, t)
			checkErrGetResult(t, body2, true, tutusrpc.ErrInsufficientFundsCode)
		})
	})
	t.Run("test functions with unsupported states", func(t *testing.T) {
		chain, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
			c.ApplicationConfiguration.KeepOnlyLatestState = true
		})

		e := &executor{chain: chain, httpSrv: httpSrv}
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "%s", "params": %s}`
		for method, cases := range rpcFunctionsWithUnsupportedStatesTestCases {
			runTestCasesWithExecutor(t, e, rpc, method, cases, doRPCCall, checkErrGetResult)
		}
	})
	t.Run("invokecontainedscript", func(t *testing.T) {
		rpc := `{"jsonrpc": "2.0", "id": 1, "method": "invokecontainedscript", "params": [%s]}`
		tx := &transaction.Transaction{
			Version:         1,
			Nonce:           2,
			SystemFee:       3,
			NetworkFee:      4,
			ValidUntilBlock: 5,
			Script:          []byte{byte(opcode.PUSH1)},
			Signers: []transaction.Signer{
				{
					Account: util.Uint160{1, 2, 3},
					Scopes:  transaction.CalledByEntry,
				},
			},
		}
		txStr := encodeBinaryToJSON(t, tx)
		h := &block.Header{
			Version:       1,
			PrevHash:      util.Uint256{1, 2, 3},
			MerkleRoot:    util.Uint256{3, 2, 1},
			Timestamp:     2,
			Nonce:         3,
			Index:         4,
			NextConsensus: util.Uint160{4, 5, 6},
			PrimaryIndex:  5,
		}
		hStr := encodeBinaryToJSON(t, h)
		t.Run("empty tx script", func(t *testing.T) {
			tx := &transaction.Transaction{
				Signers: []transaction.Signer{{Account: util.Uint160{1, 2, 3}}},
			}
			body := doRPCCall(fmt.Sprintf(rpc, encodeBinaryToJSON(t, tx)), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("invalid header", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s`, txStr, txStr)), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("invalid trigger parameter", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s, null`, txStr, hStr)), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("invalid trigger", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s, 123`, txStr, hStr)), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("unsupported trigger", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s, "%s"`, txStr, hStr, trigger.OnPersist)), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("invalid verbose", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s, "%s", null`, txStr, hStr, trigger.Application)), httpSrv.URL, t)
			checkErrGetResult(t, body, true, tutusrpc.InvalidParamsCode)
		})
		t.Run("good, tx", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, txStr), httpSrv.URL, t)
			data := checkErrGetResult(t, body, false, 0)
			var res = new(result.Invoke)
			require.NoError(t, json.Unmarshal(data, res))
			require.Equal(t, &result.Invoke{
				State:         vmstate.Halt.String(),
				GasConsumed:   30,
				Script:        []byte{byte(opcode.PUSH1)},
				Stack:         []stackitem.Item{stackitem.Make(1)},
				Notifications: []state.NotificationEvent{},
			}, res)
		})
		t.Run("good, tx and block", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s`, txStr, hStr)), httpSrv.URL, t)
			data := checkErrGetResult(t, body, false, 0)
			var res = new(result.Invoke)
			require.NoError(t, json.Unmarshal(data, res))
			require.Equal(t, &result.Invoke{
				State:         vmstate.Halt.String(),
				GasConsumed:   30,
				Script:        []byte{byte(opcode.PUSH1)},
				Stack:         []stackitem.Item{stackitem.Make(1)},
				Notifications: []state.NotificationEvent{},
			}, res)
		})
		t.Run("good, all args", func(t *testing.T) {
			body := doRPCCall(fmt.Sprintf(rpc, fmt.Sprintf(`%s, %s, "%s"`, txStr, hStr, trigger.Application)), httpSrv.URL, t)
			data := checkErrGetResult(t, body, false, 0)
			var res = new(result.Invoke)
			require.NoError(t, json.Unmarshal(data, res))
			require.Equal(t, &result.Invoke{
				State:         vmstate.Halt.String(),
				GasConsumed:   30,
				Script:        []byte{byte(opcode.PUSH1)},
				Stack:         []stackitem.Item{stackitem.Make(1)},
				Notifications: []state.NotificationEvent{},
			}, res)
		})
	})
}

// TestRPC_SubmitBlock is a special one since every test-case corrupts chain with
// improperly constructed header making it impossible to add further blocks.
func TestRPC_SubmitBlock(t *testing.T) {
	rpc := `{"jsonrpc": "2.0", "id": 1, "method": "submitblock", "params": ["%s"]}`
	check := func(t *testing.T, newBlock func(chain *core.Blockchain) *block.Block, errCode int64) {
		chain, _, httpSrv := initClearServerWithInMemoryChain(t)
		b := newBlock(chain)
		body := doRPCCallOverHTTP(fmt.Sprintf(rpc, encodeBinaryToString(t, b)), httpSrv.URL, t)
		checkErrGetResult(t, body, true, errCode)
	}

	t.Run("invalid signature", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			b := testchain.NewBlock(t, chain, 1, 0)
			b.Script.VerificationScript[8] ^= 0xff
			return b
		}, tutusrpc.ErrVerificationFailedCode)
	})
	t.Run("already exists", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 0, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 0, 1, false))
		}, tutusrpc.ErrAlreadyExistsCode)
	})
	t.Run("invalid height", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 2, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 0, 1, false))
		}, tutusrpc.ErrVerificationFailedCode)
	})
	t.Run("invalid script", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, 0xDD, 10, 0, 1, false))
		}, tutusrpc.ErrInvalidScriptCode)
	})
	t.Run("invalid ValidUntilBlock", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 0, 0, 1, false))
		}, tutusrpc.ErrExpiredTransactionCode)
	})
	t.Run("invalid SystemFee", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 999999999999, 1, false))
		}, tutusrpc.ErrPolicyFailedCode)
	})
	t.Run("invalid NetworkFee", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 0, 0, false))
		}, tutusrpc.ErrInsufficientNetworkFeeCode)
	})
	t.Run("invalid attribute", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 0, 2, true))
		}, tutusrpc.ErrInvalidAttributeCode)
	})
	t.Run("insufficient funds", func(t *testing.T) {
		check(t, func(chain *core.Blockchain) *block.Block {
			return testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 899999999999, 1, false))
		}, tutusrpc.ErrInsufficientFundsCode)
	})
	t.Run("positive", func(t *testing.T) {
		chain, _, httpSrv := initClearServerWithInMemoryChain(t)
		b := testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 0, 1, false))
		body := doRPCCallOverHTTP(fmt.Sprintf(rpc, encodeBinaryToString(t, b)), httpSrv.URL, t)
		data := checkErrGetResult(t, body, false, 0)
		var res = new(result.RelayResult)
		require.NoError(t, json.Unmarshal(data, res))
		require.Equal(t, b.Hash(), res.Hash)
	})
}

func (e *executor) getHeader(s string) *block.Header {
	hash, err := util.Uint256DecodeStringLE(s)
	if err != nil {
		panic("can not decode hash parameter")
	}
	block, err := e.chain.GetBlock(hash)
	if err != nil {
		panic("unknown block (update block hash)")
	}
	return &block.Header
}

func encodeBinaryToString(t *testing.T, a io.Serializable) string {
	bytes, err := testserdes.EncodeBinary(a)
	require.NoError(t, err)
	return base64.StdEncoding.EncodeToString(bytes)
}

func encodeBinaryToJSON(t *testing.T, a json.Marshaler) string {
	b, err := json.Marshal(a)
	require.NoError(t, err)
	return string(b)
}

func newTxWithParams(t *testing.T, chain *core.Blockchain, code opcode.Opcode, validUntilIncr uint32, systemFee int64,
	networkFeeMultiplier int64, addAttrNotValidBeforeT bool) *transaction.Transaction {
	priv0 := testchain.PrivateKeyByID(0)
	acc0 := wallet.NewAccountFromPrivateKey(priv0)

	height := chain.BlockHeight()
	tx := transaction.New([]byte{byte(code)}, 0)
	tx.Nonce = uint32(random.Int(0, math.MaxUint32))
	tx.ValidUntilBlock = height + validUntilIncr
	tx.Signers = []transaction.Signer{{Account: acc0.PrivateKey().GetScriptHash()}}
	tx.SystemFee = systemFee
	// add network fee
	size := io.GetVarSize(tx)
	netFee, sizeDelta := fee.Calculate(chain.GetBaseExecFee(), acc0.Contract.Script)
	tx.NetworkFee += netFee
	size += sizeDelta
	tx.NetworkFee += int64(size) * chain.FeePerByte()
	tx.NetworkFee = tx.NetworkFee * networkFeeMultiplier
	if addAttrNotValidBeforeT {
		tx.Attributes = []transaction.Attribute{
			{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height + 1}},
		}
	}
	require.NoError(t, acc0.SignTx(testchain.Network(), tx))
	return tx
}

func (tc rpcTestCase) getResultPair(e *executor) (expected any, res any) {
	expected = tc.result(e)
	resVal := reflect.New(reflect.TypeOf(expected).Elem())
	res = resVal.Interface()
	return expected, res
}

func checkErrGetResult(t *testing.T, body []byte, expectingFail bool, expectedErrCode int64, expectedErr ...string) json.RawMessage {
	var resp tutusrpc.Response
	err := json.Unmarshal(body, &resp)
	require.Nil(t, err)
	if expectingFail {
		require.NotNil(t, resp.Error)
		assert.NotEqual(t, 0, resp.Error.Code)
		assert.Equal(t, expectedErrCode, resp.Error.Code)
		assert.NotEqual(t, "", resp.Error.Message)
		if len(expectedErr) != 0 {
			assert.True(t, strings.Contains(resp.Error.Error(), expectedErr[0]), fmt.Sprintf("expected: %s, got: %s", expectedErr[0], resp.Error.Error()))
		}
	} else {
		assert.Nil(t, resp.Error)
	}
	return resp.Result
}

func checkErrGetBatchResult(t *testing.T, body []byte, expectingFail bool, expectedErrCode int64, expectedErr ...string) json.RawMessage {
	var resp []tutusrpc.Response
	err := json.Unmarshal(body, &resp)
	require.Nil(t, err)
	require.Equal(t, 1, len(resp))
	if expectingFail {
		require.NotNil(t, resp[0].Error)
		assert.NotEqual(t, 0, resp[0].Error.Code)
		assert.NotEqual(t, "", resp[0].Error.Message)
	} else {
		assert.Nil(t, resp[0].Error)
	}
	return resp[0].Result
}

func doRPCCallOverWS(rpcCall string, url string, t *testing.T) []byte {
	dialer := websocket.Dialer{
		HandshakeTimeout: 5 * time.Second,
		NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			dialer := net.Dialer{Timeout: 5 * time.Second,
				KeepAlive:     30 * time.Second,
				FallbackDelay: -1}
			return dialer.DialContext(ctx, "tcp4", addr)
		},
	}
	url = "ws" + strings.TrimPrefix(url, "http")
	c, r, err := dialer.Dial(url+"/ws", nil)
	require.NoError(t, err)
	defer r.Body.Close()
	err = c.SetWriteDeadline(time.Now().Add(5 * time.Second))
	require.NoError(t, err)
	require.NoError(t, c.WriteMessage(1, []byte(rpcCall)))
	err = c.SetReadDeadline(time.Now().Add(5 * time.Second))
	require.NoError(t, err)
	_, body, err := c.ReadMessage()
	require.NoError(t, err)
	require.NoError(t, c.Close())
	return bytes.TrimSpace(body)
}

func doRPCCallOverHTTP(rpcCall string, url string, t *testing.T) []byte {
	cl := http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{
		MaxIdleConns:        50,
		MaxConnsPerHost:     50,
		MaxIdleConnsPerHost: 50,
		IdleConnTimeout:     5 * time.Second,
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			dialer := net.Dialer{Timeout: 5 * time.Second,
				KeepAlive:     30 * time.Second,
				FallbackDelay: -1}
			return dialer.DialContext(ctx, "tcp4", addr)
		},
	}}
	resp, err := cl.Post(url, "application/json", strings.NewReader(rpcCall))
	require.NoErrorf(t, err, "could not make a POST request")
	body, err := gio.ReadAll(resp.Body)
	resp.Body.Close()
	assert.NoErrorf(t, err, "could not read response from the request: %s", rpcCall)
	cl.CloseIdleConnections()
	return bytes.TrimSpace(body)
}

func checkNep11Balances(t *testing.T, e *executor, acc any) {
	res, ok := acc.(*result.NEP11Balances)
	require.True(t, ok)

	expected := result.NEP11Balances{
		Balances: []result.NEP11AssetBalance{
			{
				Asset:  nnsHash,
				Name:   "NameService",
				Symbol: "NNS",
				Tokens: []result.NEP11TokenBalance{
					{
						ID:          nnsToken1ID,
						Amount:      "1",
						LastUpdated: 15,
					},
				},
			},
			{
				Asset:    nfsoHash,
				Decimals: 2,
				Name:     "NeoFS Object NFT",
				Symbol:   "NFSO",
				Tokens: []result.NEP11TokenBalance{
					{
						ID:          nfsoToken1ID,
						Amount:      "80",
						LastUpdated: 22,
					},
				},
			},
		},
		Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
	}
	require.Equal(t, testchain.PrivateKeyByID(0).Address(), res.Address)
	require.ElementsMatch(t, expected.Balances, res.Balances)
}

func checkNep17Balances(t *testing.T, e *executor, acc any) {
	res, ok := acc.(*result.NEP17Balances)
	require.True(t, ok)
	rubles, err := util.Uint160DecodeStringLE(testContractHashLE)
	require.NoError(t, err)
	expected := result.NEP17Balances{
		Balances: []result.NEP17Balance{
			{
				Asset:       rubles,
				Amount:      "877",
				Decimals:    2,
				LastUpdated: 6,
				Name:        "Rubl",
				Symbol:      "RUB",
			},
			{
				Asset:       e.chain.GoverningTokenHash(),
				Amount:      "99998000",
				LastUpdated: 4,
				Name:        "Tutus",
				Symbol:      "TUT",
			},
			{
				Asset:       e.chain.UtilityTokenHash(),
				Amount:      "90663858090",
				LastUpdated: 24,
				Decimals:    8,
				Name:        "Lub",
				Symbol:      "LUB",
			}},
		Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
	}
	require.Equal(t, testchain.PrivateKeyByID(0).Address(), res.Address)
	require.ElementsMatch(t, expected.Balances, res.Balances)
}

func checkNep11Transfers(t *testing.T, e *executor, acc any) {
	checkNep11TransfersAux(t, e, acc, []int{0}, []int{0, 1, 2})
}

func checkNep11TransfersAux(t *testing.T, e *executor, acc any, sent, rcvd []int) {
	res, ok := acc.(*result.NEP11Transfers)
	require.True(t, ok)

	blockReceiveNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(22)) // transfer 0.05 NFSO from priv1 back to priv0.
	require.NoError(t, err)
	require.Equal(t, 1, len(blockReceiveNFSO.Transactions))
	txReceiveNFSO := blockReceiveNFSO.Transactions[0]

	blockSendNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(20)) // transfer 0.25 NFSO from priv0 to priv1.
	require.NoError(t, err)
	require.Equal(t, 1, len(blockSendNFSO.Transactions))
	txSendNFSO := blockSendNFSO.Transactions[0]

	blockMintNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(19)) // mint 1.00 NFSO token by transferring 10 GAS to NFSO contract.
	require.NoError(t, err)
	require.Equal(t, 1, len(blockMintNFSO.Transactions))
	txMintNFSO := blockMintNFSO.Transactions[0]

	blockRegisterNSRecordA, err := e.chain.GetBlock(e.chain.GetHeaderHash(15)) // register `tutus.one` with A record type and priv0 owner via NS
	require.NoError(t, err)
	require.Equal(t, 1, len(blockRegisterNSRecordA.Transactions))
	txRegisterNSRecordA := blockRegisterNSRecordA.Transactions[0]

	// These are laid out here explicitly for 2 purposes:
	//  * to be able to reference any particular event for paging
	//  * to check chain events consistency
	// Technically these could be retrieved from application log, but that would almost
	// duplicate the Server method.
	expected := result.NEP11Transfers{
		Sent: []result.NEP11Transfer{
			{
				Timestamp: blockSendNFSO.Timestamp,
				Asset:     nfsoHash,
				Address:   testchain.PrivateKeyByID(1).Address(), // to priv1
				ID:        nfsoToken1ID,                          // NFSO ID
				Amount:    big.NewInt(25).String(),
				Index:     20,
				TxHash:    txSendNFSO.Hash(),
			},
		},
		Received: []result.NEP11Transfer{
			{
				Timestamp: blockReceiveNFSO.Timestamp,
				Asset:     nfsoHash,
				ID:        nfsoToken1ID,
				Address:   testchain.PrivateKeyByID(1).Address(), // from priv1
				Amount:    "5",
				Index:     22,
				TxHash:    txReceiveNFSO.Hash(),
			},
			{
				Timestamp: blockMintNFSO.Timestamp,
				Asset:     nfsoHash,
				ID:        nfsoToken1ID,
				Address:   "", // minting
				Amount:    "100",
				Index:     19,
				TxHash:    txMintNFSO.Hash(),
			},
			{
				Timestamp: blockRegisterNSRecordA.Timestamp,
				Asset:     nnsHash,
				ID:        nnsToken1ID,
				Address:   "", // minting
				Amount:    "1",
				Index:     15,
				TxHash:    txRegisterNSRecordA.Hash(),
			},
		},
		Address: testchain.PrivateKeyByID(0).Address(),
	}

	require.Equal(t, expected.Address, res.Address)

	arr := make([]result.NEP11Transfer, 0, len(expected.Sent))
	for i := range expected.Sent {
		if slices.Contains(sent, i) {
			arr = append(arr, expected.Sent[i])
		}
	}
	require.Equal(t, arr, res.Sent)

	arr = arr[:0]
	for i := range expected.Received {
		if slices.Contains(rcvd, i) {
			arr = append(arr, expected.Received[i])
		}
	}
	require.Equal(t, arr, res.Received)
}

func checkNep17Transfers(t *testing.T, e *executor, acc any) {
	checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
}

func checkNep17TransfersAux(t *testing.T, e *executor, acc any, sent, rcvd []int) {
	res, ok := acc.(*result.NEP17Transfers)
	require.True(t, ok)
	rublesHash, err := util.Uint160DecodeStringLE(testContractHashLE)
	require.NoError(t, err)

	blockWithFAULTedTx, err := e.chain.GetBlock(e.chain.GetHeaderHash(faultedTxBlock)) // Transaction with ABORT inside.
	require.NoError(t, err)
	require.Equal(t, 1, len(blockWithFAULTedTx.Transactions))
	txFAULTed := blockWithFAULTedTx.Transactions[0]

	blockDeploy6, err := e.chain.GetBlock(e.chain.GetHeaderHash(23)) // deploy Storage contract (storage_contract.go)
	require.NoError(t, err)
	require.Equal(t, 1, len(blockDeploy6.Transactions))
	txDeploy6 := blockDeploy6.Transactions[0]

	blockTransferNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(20)) // transfer 0.25 NFSO from priv0 to priv1.
	require.NoError(t, err)
	require.Equal(t, 1, len(blockTransferNFSO.Transactions))
	txTransferNFSO := blockTransferNFSO.Transactions[0]

	blockMintNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(19)) // mint 1.00 NFSO token for priv0 by transferring 10 GAS to NFSO contract.
	require.NoError(t, err)
	require.Equal(t, 1, len(blockMintNFSO.Transactions))
	txMintNFSO := blockMintNFSO.Transactions[0]

	blockDeploy5, err := e.chain.GetBlock(e.chain.GetHeaderHash(18)) // deploy NeoFS Object contract (NEP11-Divisible)
	require.NoError(t, err)
	require.Equal(t, 1, len(blockDeploy5.Transactions))
	txDeploy5 := blockDeploy5.Transactions[0]

	blockPutNewTestValue, err := e.chain.GetBlock(e.chain.GetHeaderHash(17)) // invoke `put` method of `test_contract.go` with `testkey`, `newtestvalue` args
	require.NoError(t, err)
	require.Equal(t, 4, len(blockPutNewTestValue.Transactions))
	txPutNewTestValue := blockPutNewTestValue.Transactions[0]
	txPutValue1 := blockPutNewTestValue.Transactions[1] // invoke `put` method of `test_contract.go` with `aa`, `v1` args
	txPutValue2 := blockPutNewTestValue.Transactions[2] // invoke `put` method of `test_contract.go` with `aa10`, `v2` args
	txPutValue3 := blockPutNewTestValue.Transactions[3] // invoke `put` method of `test_contract.go` with `aa50`, `v3` args

	blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(16)) // add type A record to `tutus.one` domain via NNS
	require.NoError(t, err)
	require.Equal(t, 1, len(blockSetRecord.Transactions))
	txSetRecord := blockSetRecord.Transactions[0]

	blockRegisterDomain, err := e.chain.GetBlock(e.chain.GetHeaderHash(15)) // register `tutus.one` domain via NNS
	require.NoError(t, err)
	require.Equal(t, 1, len(blockRegisterDomain.Transactions))
	txRegisterDomain := blockRegisterDomain.Transactions[0]

	blockAddRoot, err := e.chain.GetBlock(e.chain.GetHeaderHash(14)) // add `.one` root to NNS
	require.NoError(t, err)
	require.Equal(t, 1, len(blockAddRoot.Transactions))
	txAddRoot := blockAddRoot.Transactions[0]

	blockGASBounty2, err := e.chain.GetBlock(e.chain.GetHeaderHash(12)) // size of committee = 6
	require.NoError(t, err)

	blockGASBounty3 := blockDeploy5 // block 18 is multiple of committee size 6

	blockGASBounty4 := blockWithFAULTedTx // block 24 is multiple of committee size 6

	blockDeploy4, err := e.chain.GetBlock(e.chain.GetHeaderHash(11)) // deploy ns.go (non-native neo name service contract)
	require.NoError(t, err)
	require.Equal(t, 1, len(blockDeploy4.Transactions))
	txDeploy4 := blockDeploy4.Transactions[0]

	blockDeploy3, err := e.chain.GetBlock(e.chain.GetHeaderHash(10)) // deploy verification_with_args_contract.go
	require.NoError(t, err)
	require.Equal(t, 1, len(blockDeploy3.Transactions))
	txDeploy3 := blockDeploy3.Transactions[0]

	blockDepositGAS, err := e.chain.GetBlock(e.chain.GetHeaderHash(8))
	require.NoError(t, err)
	require.Equal(t, 1, len(blockDepositGAS.Transactions))
	txDepositGAS := blockDepositGAS.Transactions[0]

	blockDeploy2, err := e.chain.GetBlock(e.chain.GetHeaderHash(7)) // deploy verification_contract.go
	require.NoError(t, err)
	require.Equal(t, 1, len(blockDeploy2.Transactions))
	txDeploy2 := blockDeploy2.Transactions[0]

	blockSendRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(6))
	require.NoError(t, err)
	require.Equal(t, 1, len(blockSendRubles.Transactions))
	txSendRubles := blockSendRubles.Transactions[0]
	blockGASBounty1 := blockSendRubles // index 6 = size of committee

	blockReceiveRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(5))
	require.NoError(t, err)
	require.Equal(t, 2, len(blockReceiveRubles.Transactions))
	txInitCall := blockReceiveRubles.Transactions[0]
	txReceiveRubles := blockReceiveRubles.Transactions[1]

	blockSendNEO, err := e.chain.GetBlock(e.chain.GetHeaderHash(4))
	require.NoError(t, err)
	require.Equal(t, 1, len(blockSendNEO.Transactions))
	txSendNEO := blockSendNEO.Transactions[0]

	blockCtrInv1, err := e.chain.GetBlock(e.chain.GetHeaderHash(3))
	require.NoError(t, err)
	require.Equal(t, 1, len(blockCtrInv1.Transactions))
	txCtrInv1 := blockCtrInv1.Transactions[0]

	blockCtrDeploy, err := e.chain.GetBlock(e.chain.GetHeaderHash(2))
	require.NoError(t, err)
	require.Equal(t, 1, len(blockCtrDeploy.Transactions))
	txCtrDeploy := blockCtrDeploy.Transactions[0]

	blockReceiveGAS, err := e.chain.GetBlock(e.chain.GetHeaderHash(1))
	require.NoError(t, err)
	require.Equal(t, 2, len(blockReceiveGAS.Transactions))
	txReceiveNEO := blockReceiveGAS.Transactions[0]
	txReceiveGAS := blockReceiveGAS.Transactions[1]

	blockGASBounty0, err := e.chain.GetBlock(e.chain.GetHeaderHash(0))
	require.NoError(t, err)

	// These are laid out here explicitly for 2 purposes:
	//  * to be able to reference any particular event for paging
	//  * to check chain events consistency
	// Technically these could be retrieved from application log, but that would almost
	// duplicate the Server method.
	expected := result.NEP17Transfers{
		Sent: []result.NEP17Transfer{
			{
				Timestamp: blockWithFAULTedTx.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txFAULTed.SystemFee + txFAULTed.NetworkFee).String(),
				Index:     24,
				TxHash:    blockWithFAULTedTx.Hash(),
			},
			{
				Timestamp: blockDeploy6.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txDeploy6.SystemFee + txDeploy6.NetworkFee).String(),
				Index:     23,
				TxHash:    blockDeploy6.Hash(),
			},
			{
				Timestamp: blockTransferNFSO.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txTransferNFSO.SystemFee + txTransferNFSO.NetworkFee).String(),
				Index:     20,
				TxHash:    blockTransferNFSO.Hash(),
			},
			{
				Timestamp:   blockMintNFSO.Timestamp,
				Asset:       e.chain.UtilityTokenHash(),
				Address:     address.Uint160ToString(nfsoHash),
				Amount:      "1000000000",
				Index:       19,
				NotifyIndex: 0,
				TxHash:      txMintNFSO.Hash(),
			},
			{
				Timestamp: blockMintNFSO.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txMintNFSO.SystemFee + txMintNFSO.NetworkFee).String(),
				Index:     19,
				TxHash:    blockMintNFSO.Hash(),
			},
			{
				Timestamp: blockDeploy5.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txDeploy5.SystemFee + txDeploy5.NetworkFee).String(),
				Index:     18,
				TxHash:    blockDeploy5.Hash(),
			},
			{
				Timestamp: blockPutNewTestValue.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txPutValue3.SystemFee + txPutValue3.NetworkFee).String(),
				Index:     17,
				TxHash:    blockPutNewTestValue.Hash(),
			},
			{
				Timestamp: blockPutNewTestValue.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txPutValue2.SystemFee + txPutValue2.NetworkFee).String(),
				Index:     17,
				TxHash:    blockPutNewTestValue.Hash(),
			},
			{
				Timestamp: blockPutNewTestValue.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txPutValue1.SystemFee + txPutValue1.NetworkFee).String(),
				Index:     17,
				TxHash:    blockPutNewTestValue.Hash(),
			},
			{
				Timestamp: blockPutNewTestValue.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txPutNewTestValue.SystemFee + txPutNewTestValue.NetworkFee).String(),
				Index:     17,
				TxHash:    blockPutNewTestValue.Hash(),
			},
			{
				Timestamp: blockSetRecord.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txSetRecord.SystemFee + txSetRecord.NetworkFee).String(),
				Index:     16,
				TxHash:    blockSetRecord.Hash(),
			},
			{
				Timestamp: blockRegisterDomain.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txRegisterDomain.SystemFee + txRegisterDomain.NetworkFee).String(),
				Index:     15,
				TxHash:    blockRegisterDomain.Hash(),
			},
			{
				Timestamp: blockAddRoot.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txAddRoot.SystemFee + txAddRoot.NetworkFee).String(),
				Index:     14,
				TxHash:    blockAddRoot.Hash(),
			},
			{
				Timestamp: blockDeploy4.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txDeploy4.SystemFee + txDeploy4.NetworkFee).String(),
				Index:     11,
				TxHash:    blockDeploy4.Hash(),
			},
			{
				Timestamp: blockDeploy3.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txDeploy3.SystemFee + txDeploy3.NetworkFee).String(),
				Index:     10,
				TxHash:    blockDeploy3.Hash(),
			},
			{
				Timestamp:   blockDepositGAS.Timestamp,
				Asset:       e.chain.UtilityTokenHash(),
				Address:     address.Uint160ToString(nativehashes.Notary),
				Amount:      "1000000000",
				Index:       8,
				NotifyIndex: 0,
				TxHash:      txDepositGAS.Hash(),
			},
			{
				Timestamp: blockDepositGAS.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txDepositGAS.SystemFee + txDepositGAS.NetworkFee).String(),
				Index:     8,
				TxHash:    blockDepositGAS.Hash(),
			},
			{
				Timestamp: blockDeploy2.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txDeploy2.SystemFee + txDeploy2.NetworkFee).String(),
				Index:     7,
				TxHash:    blockDeploy2.Hash(),
			},
			{
				Timestamp:   blockSendRubles.Timestamp,
				Asset:       rublesHash,
				Address:     testchain.PrivateKeyByID(1).Address(),
				Amount:      "123",
				Index:       6,
				NotifyIndex: 0,
				TxHash:      txSendRubles.Hash(),
			},
			{
				Timestamp: blockSendRubles.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txSendRubles.SystemFee + txSendRubles.NetworkFee).String(),
				Index:     6,
				TxHash:    blockSendRubles.Hash(),
			},
			{
				Timestamp: blockReceiveRubles.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txReceiveRubles.SystemFee + txReceiveRubles.NetworkFee).String(),
				Index:     5,
				TxHash:    blockReceiveRubles.Hash(),
			},
			{
				Timestamp: blockReceiveRubles.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txInitCall.SystemFee + txInitCall.NetworkFee).String(),
				Index:     5,
				TxHash:    blockReceiveRubles.Hash(),
			},
			{
				Timestamp:   blockSendNEO.Timestamp,
				Asset:       e.chain.GoverningTokenHash(),
				Address:     testchain.PrivateKeyByID(1).Address(),
				Amount:      "1000",
				Index:       4,
				NotifyIndex: 0,
				TxHash:      txSendNEO.Hash(),
			},
			{
				Timestamp: blockSendNEO.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn
				Amount:    big.NewInt(txSendNEO.SystemFee + txSendNEO.NetworkFee).String(),
				Index:     4,
				TxHash:    blockSendNEO.Hash(),
			},
			{
				Timestamp: blockCtrInv1.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn has empty receiver
				Amount:    big.NewInt(txCtrInv1.SystemFee + txCtrInv1.NetworkFee).String(),
				Index:     3,
				TxHash:    blockCtrInv1.Hash(),
			},
			{
				Timestamp: blockCtrDeploy.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "", // burn has empty receiver
				Amount:    big.NewInt(txCtrDeploy.SystemFee + txCtrDeploy.NetworkFee).String(),
				Index:     2,
				TxHash:    blockCtrDeploy.Hash(),
			},
		},
		Received: []result.NEP17Transfer{
			{
				Timestamp:   blockGASBounty4.Timestamp, // GAS bounty block 24
				Asset:       e.chain.UtilityTokenHash(),
				Address:     "",
				Amount:      "50000000",
				Index:       24,
				NotifyIndex: 0,
				TxHash:      blockGASBounty4.Hash(),
			},
			{
				Timestamp:   blockGASBounty3.Timestamp, // GAS bounty block 18
				Asset:       e.chain.UtilityTokenHash(),
				Address:     "",
				Amount:      "50000000",
				Index:       18,
				NotifyIndex: 0,
				TxHash:      blockGASBounty3.Hash(),
			},
			{
				Timestamp:   blockGASBounty2.Timestamp,
				Asset:       e.chain.UtilityTokenHash(),
				Address:     "",
				Amount:      "50000000",
				Index:       12,
				NotifyIndex: 0,
				TxHash:      blockGASBounty2.Hash(),
			},
			{
				Timestamp:   blockGASBounty1.Timestamp,
				Asset:       e.chain.UtilityTokenHash(),
				Address:     "",
				Amount:      "50000000",
				Index:       6,
				NotifyIndex: 0,
				TxHash:      blockGASBounty1.Hash(),
			},
			{
				Timestamp:   blockReceiveRubles.Timestamp,
				Asset:       rublesHash,
				Address:     address.Uint160ToString(rublesHash),
				Amount:      "1000",
				Index:       5,
				NotifyIndex: 0,
				TxHash:      txReceiveRubles.Hash(),
			},
			{
				Timestamp:   blockSendNEO.Timestamp,
				Asset:       e.chain.UtilityTokenHash(),
				Address:     "", // Minted GAS.
				Amount:      "149998500",
				Index:       4,
				NotifyIndex: 0,
				TxHash:      txSendNEO.Hash(),
			},
			{
				Timestamp:   blockReceiveGAS.Timestamp,
				Asset:       e.chain.UtilityTokenHash(),
				Address:     testchain.MultisigAddress(),
				Amount:      "100000000000",
				Index:       1,
				NotifyIndex: 0,
				TxHash:      txReceiveGAS.Hash(),
			},
			{
				Timestamp:   blockReceiveGAS.Timestamp,
				Asset:       e.chain.GoverningTokenHash(),
				Address:     testchain.MultisigAddress(),
				Amount:      "99999000",
				Index:       1,
				NotifyIndex: 0,
				TxHash:      txReceiveNEO.Hash(),
			},
			{
				Timestamp: blockGASBounty0.Timestamp,
				Asset:     e.chain.UtilityTokenHash(),
				Address:   "",
				Amount:    "50000000",
				Index:     0,
				TxHash:    blockGASBounty0.Hash(),
			},
		},
		Address: testchain.PrivateKeyByID(0).Address(),
	}

	require.Equal(t, expected.Address, res.Address)

	arr := make([]result.NEP17Transfer, 0, len(expected.Sent))
	for i := range expected.Sent {
		if slices.Contains(sent, i) {
			arr = append(arr, expected.Sent[i])
		}
	}
	require.Equal(t, arr, res.Sent)

	arr = arr[:0]
	for i := range expected.Received {
		if slices.Contains(rcvd, i) {
			arr = append(arr, expected.Received[i])
		}
	}
	require.Equal(t, arr, res.Received)
}

func TestEscapeForLog(t *testing.T) {
	in := "\n\tbad"
	require.Equal(t, "bad", escapeForLog(in))
}

func BenchmarkHandleIn(b *testing.B) {
	chain, orc, cfg, logger := getUnitTestChain(b, false, false, false)

	serverConfig, err := network.NewServerConfig(cfg)
	require.NoError(b, err)
	serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test")
	serverConfig.LogLevel = zapcore.FatalLevel
	server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger)
	require.NoError(b, err)
	rpcServer := New(chain, cfg.ApplicationConfiguration.RPC, server, orc, logger, make(chan error))

	do := func(b *testing.B, req []byte) {
		b.ReportAllocs()
		for b.Loop() {
			b.StopTimer()
			in := new(params.In)
			b.StartTimer()
			err := json.Unmarshal(req, in)
			if err != nil {
				b.FailNow()
			}

			res := rpcServer.handleIn(in, nil)
			if res.Error != nil {
				b.FailNow()
			}
		}
		b.StopTimer()
	}

	b.Run("no extra params", func(b *testing.B) {
		do(b, []byte(`{"jsonrpc":"2.0", "method":"validateaddress","params":["Nbb1qkwcwNSBs9pAnrVVrnFbWnbWBk91U2"]}`))
	})

	b.Run("with extra params", func(b *testing.B) {
		do(b, []byte(`{"jsonrpc":"2.0", "method":"validateaddress","params":["Nbb1qkwcwNSBs9pAnrVVrnFbWnbWBk91U2", 
"set", "of", "different", "parameters", "to", "see", "the", "difference", "between", "unmarshalling", "algorithms", 1234, 5678, 1234567, 765432, true, false, null,
"0x50befd26fdf6e4d957c11e078b24ebce6291456f", "someMethod", [{"type": "String", "value": "50befd26fdf6e4d957c11e078b24ebce6291456f"}, 
{"type": "Integer", "value": "42"}, {"type": "Boolean", "value": false}]]}`))
	})
}

func TestFailedPreconditionShutdown(t *testing.T) {
	_, srv, _ := initClearServerWithCustomConfig(t, func(c *config.Config) {
		c.ApplicationConfiguration.RPC.Addresses = []string{"not an address"}
	})

	srv.Start()
	require.Positive(t, len(srv.errChan)) // this is how Start reports internal failures

	var stopped atomic.Bool

	go func() {
		srv.Shutdown()
		stopped.Store(true)
	}()

	require.Eventually(t, stopped.Load, 5*time.Second, 100*time.Millisecond, "Shutdown should return")
}

func TestErrorResponseContentType(t *testing.T) {
	_, _, httpSrv := initClearServerWithServices(t, true, false, false)

	const (
		expectedContentType = "application/json; charset=utf-8"
		req                 = `{"jsonrpc":"2.0", "method":"unknown","params":[]}`
	)

	cl := http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{MaxIdleConns: 50, MaxIdleConnsPerHost: 50, IdleConnTimeout: 5 * time.Second,
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			dialer := net.Dialer{Timeout: 5 * time.Second}
			return dialer.DialContext(ctx, "tcp4", addr)
		},
	}}
	resp, err := cl.Post(httpSrv.URL, "application/json", strings.NewReader(req))
	require.NoErrorf(t, err, "could not make a POST request")
	resp.Body.Close()
	contentType := resp.Header.Get("Content-Type")
	require.Equal(t, expectedContentType, contentType)
}
