package rpcsrv import ( "bytes" "context" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "math/big" "net/http" "net/http/httptest" "slices" "strings" "sync" "testing" "time" "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/tutus-one/tutus-chain/internal/basicchain" "github.com/tutus-one/tutus-chain/internal/testchain" "github.com/tutus-one/tutus-chain/pkg/config" "github.com/tutus-one/tutus-chain/pkg/core" "github.com/tutus-one/tutus-chain/pkg/core/block" "github.com/tutus-one/tutus-chain/pkg/core/fee" "github.com/tutus-one/tutus-chain/pkg/core/interop/interopnames" "github.com/tutus-one/tutus-chain/pkg/core/mempoolevent" "github.com/tutus-one/tutus-chain/pkg/core/native" "github.com/tutus-one/tutus-chain/pkg/core/native/nativehashes" "github.com/tutus-one/tutus-chain/pkg/core/native/nativenames" "github.com/tutus-one/tutus-chain/pkg/core/native/noderoles" "github.com/tutus-one/tutus-chain/pkg/core/state" "github.com/tutus-one/tutus-chain/pkg/core/transaction" "github.com/tutus-one/tutus-chain/pkg/crypto/hash" "github.com/tutus-one/tutus-chain/pkg/crypto/keys" "github.com/tutus-one/tutus-chain/pkg/encoding/bigint" "github.com/tutus-one/tutus-chain/pkg/io" "github.com/tutus-one/tutus-chain/pkg/tutusrpc" "github.com/tutus-one/tutus-chain/pkg/tutusrpc/result" "github.com/tutus-one/tutus-chain/pkg/network" "github.com/tutus-one/tutus-chain/pkg/rpcclient" "github.com/tutus-one/tutus-chain/pkg/rpcclient/actor" "github.com/tutus-one/tutus-chain/pkg/rpcclient/gas" "github.com/tutus-one/tutus-chain/pkg/rpcclient/invoker" "github.com/tutus-one/tutus-chain/pkg/rpcclient/management" "github.com/tutus-one/tutus-chain/pkg/rpcclient/tutus" "github.com/tutus-one/tutus-chain/pkg/rpcclient/nep11" "github.com/tutus-one/tutus-chain/pkg/rpcclient/nep17" "github.com/tutus-one/tutus-chain/pkg/rpcclient/nep24" "github.com/tutus-one/tutus-chain/pkg/rpcclient/neptoken" "github.com/tutus-one/tutus-chain/pkg/rpcclient/nns" "github.com/tutus-one/tutus-chain/pkg/rpcclient/notary" "github.com/tutus-one/tutus-chain/pkg/rpcclient/oracle" "github.com/tutus-one/tutus-chain/pkg/rpcclient/policy" "github.com/tutus-one/tutus-chain/pkg/rpcclient/rolemgmt" "github.com/tutus-one/tutus-chain/pkg/rpcclient/waiter" "github.com/tutus-one/tutus-chain/pkg/smartcontract" "github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag" "github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest" "github.com/tutus-one/tutus-chain/pkg/smartcontract/trigger" "github.com/tutus-one/tutus-chain/pkg/util" "github.com/tutus-one/tutus-chain/pkg/vm" "github.com/tutus-one/tutus-chain/pkg/vm/emit" "github.com/tutus-one/tutus-chain/pkg/vm/opcode" "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" "github.com/tutus-one/tutus-chain/pkg/vm/vmstate" "github.com/tutus-one/tutus-chain/pkg/wallet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_NEP17(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) h, err := util.Uint160DecodeStringLE(testContractHashLE) require.NoError(t, err) rub := nep17.NewReader(invoker.New(c, nil), h) t.Run("Decimals", func(t *testing.T) { d, err := rub.Decimals() require.NoError(t, err) require.EqualValues(t, 2, d) }) t.Run("TotalSupply", func(t *testing.T) { s, err := rub.TotalSupply() require.NoError(t, err) require.EqualValues(t, big.NewInt(1_000_000), s) }) t.Run("Symbol", func(t *testing.T) { sym, err := rub.Symbol() require.NoError(t, err) require.Equal(t, "RUB", sym) }) t.Run("TokenInfo", func(t *testing.T) { tok, err := neptoken.Info(c, h) require.NoError(t, err) require.Equal(t, h, tok.Hash) require.Equal(t, "Rubl", tok.Name) require.Equal(t, "RUB", tok.Symbol) require.EqualValues(t, 2, tok.Decimals) }) t.Run("BalanceOf", func(t *testing.T) { acc := testchain.PrivateKeyByID(0).GetScriptHash() b, err := rub.BalanceOf(acc) require.NoError(t, err) require.EqualValues(t, big.NewInt(877), b) }) } func TestClientRoleManagement(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, Account: &wallet.Account{ Address: testchain.CommitteeAddress(), Contract: &wallet.Contract{ Script: testchain.CommitteeVerificationScript(), }, }, }}) require.NoError(t, err) height, err := c.GetBlockCount() require.NoError(t, err) rm := rolemgmt.New(act) ks, err := rm.GetDesignatedByRole(noderoles.Oracle, height) require.NoError(t, err) require.Equal(t, 0, len(ks)) testKeys := keys.PublicKeys{ testchain.PrivateKeyByID(0).PublicKey(), testchain.PrivateKeyByID(1).PublicKey(), testchain.PrivateKeyByID(2).PublicKey(), testchain.PrivateKeyByID(3).PublicKey(), } tx, err := rm.DesignateAsRoleUnsigned(noderoles.Oracle, testKeys) require.NoError(t, err) tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) bl := testchain.NewBlock(t, chain, 1, 0, tx) _, err = c.SubmitBlock(*bl) require.NoError(t, err) slices.SortFunc(testKeys, (*keys.PublicKey).Cmp) ks, err = rm.GetDesignatedByRole(noderoles.Oracle, height+1) require.NoError(t, err) require.Equal(t, testKeys, ks) } func TestClientPolicyContract(t *testing.T) { chain, _, httpSrv := initClearServerWithCustomConfig(t, func(cfg *config.Config) { cfg.ProtocolConfiguration.Hardforks = map[string]uint32{ config.HFFaun.String(): 0, } }) for _, b := range getTestBlocks(t) { require.NoError(t, chain.AddBlock(b)) } c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) polizei := policy.NewReader(invoker.New(c, nil)) val, err := polizei.GetExecFeeFactor() require.NoError(t, err) require.Equal(t, int64(30), val) val, err = polizei.GetFeePerByte() require.NoError(t, err) require.Equal(t, int64(1000), val) val, err = polizei.GetStoragePrice() require.NoError(t, err) require.Equal(t, int64(100000), val) val, err = polizei.GetAttributeFee(transaction.NotaryAssistedT) require.NoError(t, err) require.Equal(t, int64(1000_0000), val) ret, err := polizei.IsBlocked(util.Uint160{}) require.NoError(t, err) require.False(t, ret) act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, Account: &wallet.Account{ Address: testchain.CommitteeAddress(), Contract: &wallet.Contract{ Script: testchain.CommitteeVerificationScript(), }, }, }}) require.NoError(t, err) polis := policy.New(act) txexec, err := polis.SetExecFeeFactorUnsigned(100 * vm.ExecFeeFactorMultiplier) require.NoError(t, err) txnetfee, err := polis.SetFeePerByteUnsigned(500) require.NoError(t, err) txstorage, err := polis.SetStoragePriceUnsigned(100500) require.NoError(t, err) txattr, err := polis.SetAttributeFeeUnsigned(transaction.NotaryAssistedT, 100500) require.NoError(t, err) blocked := []util.Uint160{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}} txblock1, err := polis.BlockAccountUnsigned(blocked[0]) require.NoError(t, err) txblock2, err := polis.BlockAccountUnsigned(blocked[1]) require.NoError(t, err) txblock3, err := polis.BlockAccountUnsigned(blocked[2]) require.NoError(t, err) var stdM *manifest.Manifest for _, cs := range chain.GetNatives() { if cs.Manifest.Name == nativenames.StdLib { stdM = &cs.Manifest break } } m1 := stdM.ABI.GetMethod("hexDecode", 1) m2 := stdM.ABI.GetMethod("hexEncode", 1) require.NotNil(t, m1) require.NotNil(t, m2) whitelisted := []policy.WhitelistedContract{ {Hash: nativehashes.StdLib, Offset: uint32(m1.Offset)}, {Hash: nativehashes.StdLib, Offset: uint32(m2.Offset)}, } txwhitelist1, err := polis.SetWhitelistFeeContractUnsigned(whitelisted[0].Hash, "hexDecode", 1, 0) require.NoError(t, err) txwhitelist2, err := polis.SetWhitelistFeeContractUnsigned(whitelisted[1].Hash, "hexEncode", 1, 0) require.NoError(t, err) for _, tx := range []*transaction.Transaction{txattr, txblock1, txblock2, txblock3, txwhitelist1, txwhitelist2, txstorage, txnetfee, txexec} { tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) } bl := testchain.NewBlock(t, chain, 1, 0, txattr, txblock1, txblock2, txblock3, txwhitelist1, txwhitelist2, txstorage, txnetfee, txexec) _, err = c.SubmitBlock(*bl) require.NoError(t, err) val, err = polizei.GetExecFeeFactor() require.NoError(t, err) require.Equal(t, int64(100), val) val, err = polizei.GetFeePerByte() require.NoError(t, err) require.Equal(t, int64(500), val) val, err = polizei.GetStoragePrice() require.NoError(t, err) require.Equal(t, int64(100500), val) val, err = polizei.GetAttributeFee(transaction.NotaryAssistedT) require.NoError(t, err) require.Equal(t, int64(100500), val) ret, err = polizei.IsBlocked(util.Uint160{1, 2, 3}) require.NoError(t, err) require.True(t, ret) all, err := polizei.GetBlockedAccountsExpanded(100) require.NoError(t, err) slices.SortFunc(blocked, func(a, b util.Uint160) int { return bytes.Compare(a[:], b[:]) }) require.Equal(t, blocked, all) it, err := polizei.GetBlockedAccounts() require.NoError(t, err) all, err = it.Next(100) require.NoError(t, err) require.Equal(t, blocked, all) allWhitelisted, err := polizei.GetWhitelistFeeContractsExpanded(100) require.NoError(t, err) require.Equal(t, whitelisted, allWhitelisted) whitelistedIt, err := polizei.GetWhitelistFeeContracts() require.NoError(t, err) allWhitelisted, err = whitelistedIt.Next(100) require.NoError(t, err) require.Equal(t, whitelisted, allWhitelisted) require.NoError(t, whitelistedIt.Terminate()) txwhitelist3, err := polis.RemoveWhitelistFeeContractUnsigned(whitelisted[0].Hash, "hexDecode", 1) require.NoError(t, err) for _, tx := range []*transaction.Transaction{txwhitelist3} { tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) } bl = testchain.NewBlock(t, chain, 1, 0, txwhitelist3) _, err = c.SubmitBlock(*bl) require.NoError(t, err) allWhitelisted, err = polizei.GetWhitelistFeeContractsExpanded(100) require.NoError(t, err) require.Equal(t, []policy.WhitelistedContract{whitelisted[1]}, allWhitelisted) } func TestClientManagementContract(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) manReader := management.NewReader(invoker.New(c, nil)) fee, err := manReader.GetMinimumDeploymentFee() require.NoError(t, err) require.Equal(t, big.NewInt(10*1_0000_0000), fee) cs1, err := manReader.GetContract(gas.Hash) require.NoError(t, err) cs2, err := c.GetContractStateByHash(gas.Hash) require.NoError(t, err) require.Equal(t, cs2, cs1) cs1, err = manReader.GetContractByID(-6) require.NoError(t, err) require.Equal(t, cs2, cs1) ret, err := manReader.HasMethod(gas.Hash, "transfer", 4) require.NoError(t, err) require.True(t, ret) act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, Account: &wallet.Account{ Address: testchain.CommitteeAddress(), Contract: &wallet.Contract{ Script: testchain.CommitteeVerificationScript(), }, }, }}) require.NoError(t, err) ids, err := manReader.GetContractHashesExpanded(10) require.NoError(t, err) ctrs := make([]management.IDHash, 0) for i, s := range []string{testContractHashLE, verifyContractHash, verifyWithArgsContractHash, nnsContractHash, nfsoContractHash, storageContractHash} { h, err := util.Uint160DecodeStringLE(s) require.NoError(t, err) ctrs = append(ctrs, management.IDHash{ID: int32(i) + 1, Hash: h}) } require.Equal(t, ctrs, ids) iter, err := manReader.GetContractHashes() require.NoError(t, err) ids, err = iter.Next(3) require.NoError(t, err) require.Equal(t, ctrs[:3], ids) ids, err = iter.Next(10) require.NoError(t, err) require.Equal(t, ctrs[3:], ids) man := management.New(act) txfee, err := man.SetMinimumDeploymentFeeUnsigned(big.NewInt(1 * 1_0000_0000)) require.NoError(t, err) txdepl, err := man.DeployUnsigned(&cs1.NEF, &cs1.Manifest, nil) // Redeploy from a different account. require.NoError(t, err) for _, tx := range []*transaction.Transaction{txfee, txdepl} { tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) } bl := testchain.NewBlock(t, chain, 1, 0, txfee, txdepl) _, err = c.SubmitBlock(*bl) require.NoError(t, err) fee, err = manReader.GetMinimumDeploymentFee() require.NoError(t, err) require.Equal(t, big.NewInt(1_0000_0000), fee) appLog, err := c.GetApplicationLog(txdepl.Hash(), nil) require.NoError(t, err) require.Equal(t, vmstate.Halt, appLog.Executions[0].VMState) require.Equal(t, 1, len(appLog.Executions[0].Events)) } func TestClientNEOContract(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) neoR := tutus.NewReader(invoker.New(c, nil)) sym, err := neoR.Symbol() require.NoError(t, err) require.Equal(t, "TUT", sym) dec, err := neoR.Decimals() require.NoError(t, err) require.Equal(t, 0, dec) ts, err := neoR.TotalSupply() require.NoError(t, err) require.Equal(t, big.NewInt(1_0000_0000), ts) comm, err := neoR.GetCommittee() require.NoError(t, err) commScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(comm) require.NoError(t, err) require.Equal(t, testchain.CommitteeScriptHash(), hash.Hash160(commScript)) vals, err := neoR.GetNextBlockValidators() require.NoError(t, err) valsScript, err := smartcontract.CreateDefaultMultiSigRedeemScript(vals) require.NoError(t, err) require.Equal(t, testchain.MultisigScriptHash(), hash.Hash160(valsScript)) gpb, err := neoR.GetGasPerBlock() require.NoError(t, err) require.Equal(t, int64(5_0000_0000), gpb) regP, err := neoR.GetRegisterPrice() require.NoError(t, err) require.Equal(t, int64(1000_0000_0000), regP) acc0 := testchain.PrivateKey(0).PublicKey().GetScriptHash() uncl, err := neoR.UnclaimedGas(acc0, chain.BlockHeight()+1) require.NoError(t, err) require.Equal(t, big.NewInt(10500), uncl) accState, err := neoR.GetAccountState(acc0) require.NoError(t, err) require.Equal(t, big.NewInt(1000), &accState.Balance) require.Equal(t, uint32(4), accState.BalanceHeight) cands, err := neoR.GetCandidates() require.NoError(t, err) require.Equal(t, 0, len(cands)) // No registrations. cands, err = neoR.GetAllCandidatesExpanded(100) require.NoError(t, err) require.Equal(t, 0, len(cands)) // No registrations. iter, err := neoR.GetAllCandidates() require.NoError(t, err) cands, err = iter.Next(10) require.NoError(t, err) require.Equal(t, 0, len(cands)) // No registrations. require.NoError(t, iter.Terminate()) act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, Account: &wallet.Account{ Address: testchain.CommitteeAddress(), Contract: &wallet.Contract{ Script: testchain.CommitteeVerificationScript(), }, }, }}) require.NoError(t, err) neoC := tutus.New(act) txgpb, err := neoC.SetGasPerBlockUnsigned(10 * 1_0000_0000) require.NoError(t, err) txregp, err := neoC.SetRegisterPriceUnsigned(1_0000) require.NoError(t, err) for _, tx := range []*transaction.Transaction{txgpb, txregp} { tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) } bl := testchain.NewBlock(t, chain, 1, 0, txgpb, txregp) _, err = c.SubmitBlock(*bl) require.NoError(t, err) gpb, err = neoR.GetGasPerBlock() require.NoError(t, err) require.Equal(t, int64(10_0000_0000), gpb) regP, err = neoR.GetRegisterPrice() require.NoError(t, err) require.Equal(t, int64(10000), regP) act0, err := actor.NewSimple(c, wallet.NewAccountFromPrivateKey(testchain.PrivateKey(0))) require.NoError(t, err) neo0 := tutus.New(act0) txreg, err := neo0.RegisterCandidateTransaction(testchain.PrivateKey(0).PublicKey()) require.NoError(t, err) bl = testchain.NewBlock(t, chain, 1, 0, txreg) _, err = c.SubmitBlock(*bl) require.NoError(t, err) txvote, err := neo0.VoteTransaction(acc0, testchain.PrivateKey(0).PublicKey()) require.NoError(t, err) bl = testchain.NewBlock(t, chain, 1, 0, txvote) _, err = c.SubmitBlock(*bl) require.NoError(t, err) txunreg, err := neo0.UnregisterCandidateTransaction(testchain.PrivateKey(0).PublicKey()) require.NoError(t, err) bl = testchain.NewBlock(t, chain, 1, 0, txunreg) _, err = c.SubmitBlock(*bl) require.NoError(t, err) } func TestClientNotary(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) // Echidna should be enabled since this test uses Notary contract. _, ok := chain.GetConfig().Hardforks[config.HFEchidna.String()] require.True(t, ok) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) notaReader := notary.NewReader(invoker.New(c, nil)) priv0 := testchain.PrivateKeyByID(0) priv0Hash := priv0.PublicKey().GetScriptHash() bal, err := notaReader.BalanceOf(priv0Hash) require.NoError(t, err) require.Equal(t, big.NewInt(10_0000_0000), bal) expir, err := notaReader.ExpirationOf(priv0Hash) require.NoError(t, err) require.Equal(t, uint32(1007), expir) maxNVBd, err := notaReader.GetMaxNotValidBeforeDelta() require.NoError(t, err) require.Equal(t, uint32(140), maxNVBd) commAct, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, Account: &wallet.Account{ Address: testchain.CommitteeAddress(), Contract: &wallet.Contract{ Script: testchain.CommitteeVerificationScript(), }, }, }}) require.NoError(t, err) notaComm := notary.New(commAct) txNVB, err := notaComm.SetMaxNotValidBeforeDeltaUnsigned(210) require.NoError(t, err) txNVB.Scripts[0].InvocationScript = testchain.SignCommittee(txNVB) bl := testchain.NewBlock(t, chain, 1, 0, txNVB) _, err = c.SubmitBlock(*bl) require.NoError(t, err) maxNVBd, err = notaReader.GetMaxNotValidBeforeDelta() require.NoError(t, err) require.Equal(t, uint32(210), maxNVBd) privAct, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: priv0Hash, Scopes: transaction.CalledByEntry, }, Account: wallet.NewAccountFromPrivateKey(priv0), }}) require.NoError(t, err) notaPriv := notary.New(privAct) txLock, err := notaPriv.LockDepositUntilTransaction(priv0Hash, 1111) require.NoError(t, err) bl = testchain.NewBlock(t, chain, 1, 0, txLock) _, err = c.SubmitBlock(*bl) require.NoError(t, err) expir, err = notaReader.ExpirationOf(priv0Hash) require.NoError(t, err) require.Equal(t, uint32(1111), expir) _, err = notaPriv.WithdrawTransaction(priv0Hash, priv0Hash) require.Error(t, err) // Can't be withdrawn until 1111. } func TestCalculateNetworkFee_Base(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) const extraFee = 10 var nonce uint32 c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) feePerByte := chain.FeePerByte() t.Run("Simple", func(t *testing.T) { acc0 := wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)) check := func(t *testing.T, extraFee int64) { tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) tx.ValidUntilBlock = 25 tx.Signers = []transaction.Signer{{ Account: acc0.PrivateKey().GetScriptHash(), Scopes: transaction.CalledByEntry, }} tx.Nonce = nonce nonce++ tx.Scripts = []transaction.Witness{ {VerificationScript: acc0.GetVerificationScript()}, } actualCalculatedNetFee, err := c.CalculateNetworkFee(tx) require.NoError(t, err) tx.NetworkFee = actualCalculatedNetFee + extraFee require.NoError(t, acc0.SignTx(testchain.Network(), tx)) cFee, _ := fee.Calculate(chain.GetBaseExecFee(), acc0.Contract.Script) expected := int64(io.GetVarSize(tx))*feePerByte + cFee + extraFee require.Equal(t, expected, actualCalculatedNetFee+extraFee) err = chain.VerifyTx(tx) if extraFee < 0 { require.Error(t, err) } else { require.NoError(t, err) } } t.Run("with extra fee", func(t *testing.T) { // check that calculated network fee with extra value is enough check(t, extraFee) }) t.Run("without extra fee", func(t *testing.T) { // check that calculated network fee without extra value is enough check(t, 0) }) t.Run("exactFee-1", func(t *testing.T) { // check that we don't add unexpected extra GAS check(t, -1) }) }) t.Run("Multi", func(t *testing.T) { acc0 := wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)) acc1 := wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)) err = acc1.ConvertMultisig(3, keys.PublicKeys{ testchain.PrivateKeyByID(0).PublicKey(), testchain.PrivateKeyByID(1).PublicKey(), testchain.PrivateKeyByID(2).PublicKey(), testchain.PrivateKeyByID(3).PublicKey(), }) require.NoError(t, err) check := func(t *testing.T, extraFee int64) { tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) tx.ValidUntilBlock = 25 tx.Signers = []transaction.Signer{ { Account: acc0.PrivateKey().GetScriptHash(), Scopes: transaction.CalledByEntry, }, { Account: hash.Hash160(acc1.Contract.Script), Scopes: transaction.Global, }, } tx.Nonce = nonce nonce++ tx.Scripts = []transaction.Witness{ {VerificationScript: acc0.GetVerificationScript()}, {VerificationScript: acc1.GetVerificationScript()}, } actualCalculatedNetFee, err := c.CalculateNetworkFee(tx) require.NoError(t, err) tx.NetworkFee = actualCalculatedNetFee + extraFee tx.Scripts = nil require.NoError(t, acc0.SignTx(testchain.Network(), tx)) tx.Scripts = append(tx.Scripts, transaction.Witness{ InvocationScript: testchain.Sign(tx), VerificationScript: acc1.Contract.Script, }) cFee, _ := fee.Calculate(chain.GetBaseExecFee(), acc0.Contract.Script) cFeeM, _ := fee.Calculate(chain.GetBaseExecFee(), acc1.Contract.Script) expected := int64(io.GetVarSize(tx))*feePerByte + cFee + cFeeM + extraFee require.Equal(t, expected, actualCalculatedNetFee+extraFee) err = chain.VerifyTx(tx) if extraFee < 0 { require.Error(t, err) } else { require.NoError(t, err) } } t.Run("with extra fee", func(t *testing.T) { // check that calculated network fee with extra value is enough check(t, extraFee) }) t.Run("without extra fee", func(t *testing.T) { // check that calculated network fee without extra value is enough check(t, 0) }) t.Run("exactFee-1", func(t *testing.T) { // check that we don't add unexpected extra GAS check(t, -1) }) }) t.Run("Contract", func(t *testing.T) { h, err := util.Uint160DecodeStringLE(verifyContractHash) require.NoError(t, err) priv := testchain.PrivateKeyByID(0) acc0 := wallet.NewAccountFromPrivateKey(priv) acc1 := wallet.NewAccountFromPrivateKey(priv) // contract account acc1.Contract.Deployed = true acc1.Contract.Script, err = base64.StdEncoding.DecodeString(verifyContractAVM) require.NoError(t, err) newTx := func(t *testing.T) *transaction.Transaction { tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) tx.ValidUntilBlock = chain.BlockHeight() + 10 return tx } t.Run("Valid", func(t *testing.T) { check := func(t *testing.T, extraFee int64) { tx := newTx(t) tx.Signers = []transaction.Signer{ { Account: acc0.PrivateKey().GetScriptHash(), Scopes: transaction.CalledByEntry, }, { Account: h, Scopes: transaction.Global, }, } // we need to fill standard verification scripts to use CalculateNetworkFee. tx.Scripts = []transaction.Witness{ {VerificationScript: acc0.GetVerificationScript()}, {}, } actual, err := c.CalculateNetworkFee(tx) require.NoError(t, err) tx.NetworkFee = actual + extraFee tx.Scripts = nil require.NoError(t, acc0.SignTx(testchain.Network(), tx)) tx.Scripts = append(tx.Scripts, transaction.Witness{}) err = chain.VerifyTx(tx) if extraFee < 0 { require.Error(t, err) } else { require.NoError(t, err) } } t.Run("with extra fee", func(t *testing.T) { // check that calculated network fee with extra value is enough check(t, extraFee) }) t.Run("without extra fee", func(t *testing.T) { // check that calculated network fee without extra value is enough check(t, 0) }) t.Run("exactFee-1", func(t *testing.T) { // check that we don't add unexpected extra GAS check(t, -1) }) }) }) } func TestCalculateNetworkFee(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) const extraFee = 10 c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) h, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash) require.NoError(t, err) priv := testchain.PrivateKeyByID(0) acc0 := wallet.NewAccountFromPrivateKey(priv) t.Run("ContractWithArgs", func(t *testing.T) { check := func(t *testing.T, extraFee int64) { tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) require.NoError(t, err) tx.ValidUntilBlock = chain.BlockHeight() + 10 tx.Signers = []transaction.Signer{ { Account: acc0.PrivateKey().GetScriptHash(), Scopes: transaction.CalledByEntry, }, { Account: h, Scopes: transaction.Global, }, } bw := io.NewBufBinWriter() emit.Bool(bw.BinWriter, false) emit.Int(bw.BinWriter, int64(4)) emit.String(bw.BinWriter, "good_string") // contract's `verify` return `true` with this string require.NoError(t, bw.Err) contractInv := bw.Bytes() // we need to fill standard verification scripts to use CalculateNetworkFee. tx.Scripts = []transaction.Witness{ {VerificationScript: acc0.GetVerificationScript()}, {InvocationScript: contractInv}, } tx.NetworkFee, err = c.CalculateNetworkFee(tx) require.NoError(t, err) tx.NetworkFee += extraFee tx.Scripts = nil require.NoError(t, acc0.SignTx(testchain.Network(), tx)) tx.Scripts = append(tx.Scripts, transaction.Witness{InvocationScript: contractInv}) err = chain.VerifyTx(tx) if extraFee < 0 { require.Error(t, err) } else { require.NoError(t, err) } } t.Run("with extra fee", func(t *testing.T) { // check that calculated network fee with extra value is enough check(t, extraFee) }) t.Run("without extra fee", func(t *testing.T) { // check that calculated network fee without extra value is enough check(t, 0) }) t.Run("exactFee-1", func(t *testing.T) { // check that we don't add unexpected extra GAS check(t, -1) }) }) t.Run("extra attribute fee", func(t *testing.T) { const conflictsFee = 100 tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) tx.ValidUntilBlock = chain.BlockHeight() + 10 signer0 := transaction.Signer{ Account: acc0.ScriptHash(), Scopes: transaction.CalledByEntry, } priv1 := testchain.PrivateKeyByID(1) acc1 := wallet.NewAccountFromPrivateKey(priv1) signer1 := transaction.Signer{ Account: acc1.ScriptHash(), Scopes: transaction.CalledByEntry, } tx.Signers = []transaction.Signer{signer0, signer1} tx.Attributes = []transaction.Attribute{ { Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: util.Uint256{1, 2, 3}}, }, } tx.Scripts = []transaction.Witness{ {VerificationScript: acc0.Contract.Script}, {VerificationScript: acc1.Contract.Script}, } oldFee, err := c.CalculateNetworkFee(tx) require.NoError(t, err) // Set fee per Conflicts attribute. script, err := smartcontract.CreateCallScript(nativehashes.PolicyContract, "setAttributeFee", byte(transaction.ConflictsT), conflictsFee) require.NoError(t, err) txSetFee := transaction.New(script, 1_0000_0000) txSetFee.ValidUntilBlock = chain.BlockHeight() + 1 txSetFee.Signers = []transaction.Signer{ signer0, { Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, } txSetFee.NetworkFee = 10_0000_0000 require.NoError(t, acc0.SignTx(testchain.Network(), txSetFee)) txSetFee.Scripts = append(txSetFee.Scripts, transaction.Witness{ InvocationScript: testchain.SignCommittee(txSetFee), VerificationScript: testchain.CommitteeVerificationScript(), }) require.NoError(t, chain.AddBlock(testchain.NewBlock(t, chain, 1, 0, txSetFee))) // Calculate network fee one more time with updated Conflicts price. newFee, err := c.CalculateNetworkFee(tx) require.NoError(t, err) expectedDiff := len(tx.Signers) * len(tx.GetAttributes(transaction.ConflictsT)) * conflictsFee require.Equal(t, int64(expectedDiff), newFee-oldFee) }) } func TestNotaryActor(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain acc := wallet.NewAccountFromPrivateKey(sender) comm, err := c.GetCommittee() require.NoError(t, err) multiAcc := &wallet.Account{} *multiAcc = *acc require.NoError(t, multiAcc.ConvertMultisig(smartcontract.GetMajorityHonestNodeCount(len(comm)), comm)) nact, err := notary.NewActor(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: multiAcc.Contract.ScriptHash(), Scopes: transaction.CalledByEntry, }, Account: multiAcc, }}, acc) require.NoError(t, err) neoW := tutus.New(nact) _, _, _, err = nact.Notarize(neoW.SetRegisterPriceTransaction(1_0000_0000)) require.NoError(t, err) } func TestGetRawNotaryPoolAndTransaction(t *testing.T) { var ( mainHash1, fallbackHash1, mainHash2, fallbackHash2 util.Uint256 tx1, tx2 *transaction.Transaction ) _, _, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) t.Run("getrawnotarypool", func(t *testing.T) { t.Run("empty pool", func(t *testing.T) { np, err := c.GetRawNotaryPool() require.NoError(t, err) require.Equal(t, 0, len(np.Hashes)) }) sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain acc := wallet.NewAccountFromPrivateKey(sender) comm, err := c.GetCommittee() require.NoError(t, err) multiAcc := &wallet.Account{} *multiAcc = *acc require.NoError(t, multiAcc.ConvertMultisig(smartcontract.GetMajorityHonestNodeCount(len(comm)), comm)) nact, err := notary.NewActor(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: multiAcc.Contract.ScriptHash(), Scopes: transaction.CalledByEntry, }, Account: multiAcc, }}, acc) require.NoError(t, err) neoW := tutus.New(nact) // Send the 1st notary request tx1, err = neoW.SetRegisterPriceTransaction(1_0000_0000) require.NoError(t, err) mainHash1, fallbackHash1, _, err = nact.Notarize(tx1, err) require.NoError(t, err) checkTxInPool := func(t *testing.T, mainHash, fallbackHash util.Uint256, res *result.RawNotaryPool) { actFallbacks, ok := res.Hashes[mainHash] require.Equal(t, true, ok) require.Equal(t, 1, len(actFallbacks)) require.Equal(t, fallbackHash, actFallbacks[0]) } t.Run("nonempty pool", func(t *testing.T) { actNotaryPool, err := c.GetRawNotaryPool() require.NoError(t, err) require.Equal(t, 1, len(actNotaryPool.Hashes)) checkTxInPool(t, mainHash1, fallbackHash1, actNotaryPool) }) // Send the 2nd notary request tx2, err = neoW.SetRegisterPriceTransaction(2_0000_0000) require.NoError(t, err) mainHash2, fallbackHash2, _, err = nact.Notarize(tx2, err) require.NoError(t, err) t.Run("pool with 2", func(t *testing.T) { actNotaryPool, err := c.GetRawNotaryPool() require.NoError(t, err) require.Equal(t, 2, len(actNotaryPool.Hashes)) checkTxInPool(t, mainHash1, fallbackHash1, actNotaryPool) checkTxInPool(t, mainHash2, fallbackHash2, actNotaryPool) }) }) t.Run("getrawnotarytransaction", func(t *testing.T) { t.Run("client GetRawNotaryTransaction", func(t *testing.T) { t.Run("unknown transaction", func(t *testing.T) { _, err := c.GetRawNotaryTransaction(util.Uint256{0, 0, 0}) require.Error(t, err) require.ErrorIs(t, err, tutusrpc.ErrUnknownTransaction) }) _ = tx1.Size() _ = tx2.Size() // RPC server returns empty scripts in transaction.Witness, // thus here the nil-value was changed to empty value. if tx1.Scripts[1].InvocationScript == nil && tx1.Scripts[1].VerificationScript == nil { tx1.Scripts[1] = transaction.Witness{ InvocationScript: []byte{}, VerificationScript: []byte{}, } } if tx2.Scripts[1].InvocationScript == nil && tx2.Scripts[1].VerificationScript == nil { tx2.Scripts[1] = transaction.Witness{ InvocationScript: []byte{}, VerificationScript: []byte{}, } } t.Run("transactions from pool", func(t *testing.T) { mainTx1, err := c.GetRawNotaryTransaction(mainHash1) require.NoError(t, err) require.Equal(t, tx1, mainTx1) _, err = c.GetRawNotaryTransaction(fallbackHash1) require.NoError(t, err) mainTx2, err := c.GetRawNotaryTransaction(mainHash2) require.NoError(t, err) require.Equal(t, tx2, mainTx2) _, err = c.GetRawNotaryTransaction(fallbackHash2) require.NoError(t, err) }) }) t.Run("client GetRawNotaryTransactionVerbose", func(t *testing.T) { t.Run("unknown transaction", func(t *testing.T) { _, err := c.GetRawNotaryTransactionVerbose(util.Uint256{0, 0, 0}) require.Error(t, err) require.ErrorIs(t, err, tutusrpc.ErrUnknownTransaction) }) t.Run("transactions from pool", func(t *testing.T) { mainTx1, err := c.GetRawNotaryTransactionVerbose(mainHash1) require.NoError(t, err) require.Equal(t, tx1, mainTx1) _, err = c.GetRawNotaryTransactionVerbose(fallbackHash1) require.NoError(t, err) mainTx2, err := c.GetRawNotaryTransactionVerbose(mainHash2) require.NoError(t, err) require.Equal(t, tx2, mainTx2) _, err = c.GetRawNotaryTransactionVerbose(fallbackHash2) require.NoError(t, err) }) }) }) } func TestPing(t *testing.T) { _, rpcSrv, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) require.NoError(t, c.Ping()) rpcSrv.Shutdown() httpSrv.Close() require.Error(t, c.Ping()) } func TestCreateNEP17TransferTx(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) priv := testchain.PrivateKeyByID(0) acc := wallet.NewAccountFromPrivateKey(priv) addr := priv.PublicKey().GetScriptHash() t.Run("default scope", func(t *testing.T) { act, err := actor.NewSimple(c, acc) require.NoError(t, err) gasprom := gas.New(act) tx, err := gasprom.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) require.NoError(t, err) require.NoError(t, acc.SignTx(testchain.Network(), tx)) require.NoError(t, chain.VerifyTx(tx)) ic, err := chain.GetTestVM(trigger.Application, tx, nil) require.NoError(t, err) ic.VM.LoadScriptWithFlags(tx.Script, callflag.All) require.NoError(t, ic.VM.Run()) }) t.Run("default scope, multitransfer", func(t *testing.T) { act, err := actor.NewSimple(c, acc) require.NoError(t, err) gazprom := gas.New(act) tx, err := gazprom.MultiTransferTransaction([]nep17.TransferParameters{ {From: addr, To: util.Uint160{3, 2, 1}, Amount: big.NewInt(1000), Data: nil}, {From: addr, To: util.Uint160{1, 2, 3}, Amount: big.NewInt(1000), Data: nil}, }) require.NoError(t, err) require.NoError(t, chain.VerifyTx(tx)) ic, err := chain.GetTestVM(trigger.Application, tx, nil) require.NoError(t, err) ic.VM.LoadScriptWithFlags(tx.Script, callflag.All) require.NoError(t, ic.VM.Run()) require.Equal(t, 2, len(ic.Notifications)) }) t.Run("none scope", func(t *testing.T) { act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: addr, Scopes: transaction.None, }, Account: acc, }}) require.NoError(t, err) gasprom := gas.New(act) _, err = gasprom.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) require.Error(t, err) }) t.Run("customcontracts scope", func(t *testing.T) { act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: priv.PublicKey().GetScriptHash(), Scopes: transaction.CustomContracts, AllowedContracts: []util.Uint160{gas.Hash}, }, Account: acc, }}) require.NoError(t, err) gasprom := gas.New(act) tx, err := gasprom.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) require.NoError(t, err) require.NoError(t, acc.SignTx(testchain.Network(), tx)) require.NoError(t, chain.VerifyTx(tx)) ic, err := chain.GetTestVM(trigger.Application, tx, nil) require.NoError(t, err) ic.VM.LoadScriptWithFlags(tx.Script, callflag.All) require.NoError(t, ic.VM.Run()) }) } func TestInvokeVerify(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) contract, err := util.Uint160DecodeStringLE(verifyContractHash) require.NoError(t, err) t.Run("positive, with signer", func(t *testing.T) { res, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) require.True(t, res.Stack[0].Value().(bool)) }) t.Run("positive, historic, by height, with signer", func(t *testing.T) { h := chain.BlockHeight() - 1 res, err := c.InvokeContractVerifyAtHeight(h, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) require.True(t, res.Stack[0].Value().(bool)) }) t.Run("positive, historic, by block, with signer", func(t *testing.T) { res, err := c.InvokeContractVerifyWithState(chain.GetHeaderHash(chain.BlockHeight()-1), contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) require.True(t, res.Stack[0].Value().(bool)) }) t.Run("positive, historic, by stateroot, with signer", func(t *testing.T) { h := chain.BlockHeight() - 1 sr, err := chain.GetStateModule().GetStateRoot(h) require.NoError(t, err) res, err := c.InvokeContractVerifyWithState(sr.Root, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) require.True(t, res.Stack[0].Value().(bool)) }) t.Run("bad, historic, by hash: contract not found", func(t *testing.T) { var h uint32 = 1 _, err = c.InvokeContractVerifyAtHeight(h, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet }) t.Run("bad, historic, by block: contract not found", func(t *testing.T) { _, err = c.InvokeContractVerifyWithState(chain.GetHeaderHash(1), contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet }) t.Run("bad, historic, by stateroot: contract not found", func(t *testing.T) { var h uint32 = 1 sr, err := chain.GetStateModule().GetStateRoot(h) require.NoError(t, err) _, err = c.InvokeContractVerifyWithState(sr.Root, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet }) t.Run("positive, with signer and witness", func(t *testing.T) { res, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) require.True(t, res.Stack[0].Value().(bool)) }) t.Run("error, invalid witness number", func(t *testing.T) { _, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}, transaction.Witness{InvocationScript: []byte{byte(opcode.RET)}}) require.Error(t, err) }) t.Run("false", func(t *testing.T) { res, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: util.Uint160{}}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) require.False(t, res.Stack[0].Value().(bool)) }) } func TestClient_GetNativeContracts(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) cs, err := c.GetNativeContracts() require.NoError(t, err) require.Equal(t, chain.GetNatives(), cs) } func TestClient_NEP11_ND(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) h, err := util.Uint160DecodeStringLE(nnsContractHash) require.NoError(t, err) priv0 := testchain.PrivateKeyByID(0) act, err := actor.NewSimple(c, wallet.NewAccountFromPrivateKey(priv0)) require.NoError(t, err) n11 := nep11.NewNonDivisible(act, h) acc := priv0.GetScriptHash() t.Run("Decimals", func(t *testing.T) { d, err := n11.Decimals() require.NoError(t, err) require.EqualValues(t, 0, d) // non-divisible }) t.Run("TotalSupply", func(t *testing.T) { s, err := n11.TotalSupply() require.NoError(t, err) require.EqualValues(t, big.NewInt(1), s) // the only `tutus.one` of acc0 }) t.Run("Symbol", func(t *testing.T) { sym, err := n11.Symbol() require.NoError(t, err) require.Equal(t, "NNS", sym) }) t.Run("TokenInfo", func(t *testing.T) { tok, err := neptoken.Info(c, h) require.NoError(t, err) require.Equal(t, &wallet.Token{ Name: "NameService", Hash: h, Decimals: 0, Symbol: "NNS", Standard: manifest.NEP11StandardName, }, tok) }) t.Run("BalanceOf", func(t *testing.T) { b, err := n11.BalanceOf(acc) require.NoError(t, err) require.EqualValues(t, big.NewInt(1), b) }) t.Run("OwnerOf", func(t *testing.T) { b, err := n11.OwnerOf([]byte("tutus.one")) require.NoError(t, err) require.EqualValues(t, acc, b) }) t.Run("Tokens", func(t *testing.T) { iter, err := n11.Tokens() require.NoError(t, err) items, err := iter.Next(config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, 1, len(items)) require.Equal(t, [][]byte{[]byte("tutus.one")}, items) require.NoError(t, iter.Terminate()) }) t.Run("TokensExpanded", func(t *testing.T) { items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, [][]byte{[]byte("tutus.one")}, items) }) t.Run("Properties", func(t *testing.T) { p, err := n11.Properties([]byte("tutus.one")) require.NoError(t, err) blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(15)) // `tutus.one` domain was registered in 14th block require.NoError(t, err) require.Equal(t, 1, len(blockRegisterDomain.Transactions)) expected := stackitem.NewMap() expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("tutus.one"))) expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula expected.Add(stackitem.Make([]byte("admin")), stackitem.Null{}) require.EqualValues(t, expected, p) }) t.Run("Transfer", func(t *testing.T) { _, _, err := n11.Transfer(testchain.PrivateKeyByID(1).GetScriptHash(), []byte("tutus.one"), nil) require.NoError(t, err) }) } func TestClient_NEP11_D(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) pkey0 := testchain.PrivateKeyByID(0) priv0 := pkey0.GetScriptHash() priv1 := testchain.PrivateKeyByID(1).GetScriptHash() token1ID, err := hex.DecodeString(nfsoToken1ID) require.NoError(t, err) act, err := actor.NewSimple(c, wallet.NewAccountFromPrivateKey(pkey0)) require.NoError(t, err) n11 := nep11.NewDivisible(act, nfsoHash) t.Run("Decimals", func(t *testing.T) { d, err := n11.Decimals() require.NoError(t, err) require.EqualValues(t, 2, d) // Divisible. }) t.Run("TotalSupply", func(t *testing.T) { s, err := n11.TotalSupply() require.NoError(t, err) require.EqualValues(t, big.NewInt(1), s) // the only NFSO of acc0 }) t.Run("Symbol", func(t *testing.T) { sym, err := n11.Symbol() require.NoError(t, err) require.Equal(t, "NFSO", sym) }) t.Run("TokenInfo", func(t *testing.T) { tok, err := neptoken.Info(c, nfsoHash) require.NoError(t, err) require.Equal(t, &wallet.Token{ Name: "NeoFS Object NFT", Hash: nfsoHash, Decimals: 2, Symbol: "NFSO", Standard: manifest.NEP11StandardName, }, tok) }) t.Run("BalanceOf", func(t *testing.T) { b, err := n11.BalanceOf(priv0) require.NoError(t, err) require.EqualValues(t, big.NewInt(80), b) }) t.Run("BalanceOfD", func(t *testing.T) { b, err := n11.BalanceOfD(priv0, token1ID) require.NoError(t, err) require.EqualValues(t, big.NewInt(80), b) }) t.Run("OwnerOf", func(t *testing.T) { iter, err := n11.OwnerOf(token1ID) require.NoError(t, err) items, err := iter.Next(config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, 2, len(items)) require.Equal(t, []util.Uint160{priv1, priv0}, items) require.NoError(t, iter.Terminate()) }) t.Run("OwnerOfExpanded", func(t *testing.T) { b, err := n11.OwnerOfExpanded(token1ID, config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, []util.Uint160{priv1, priv0}, b) }) t.Run("Properties", func(t *testing.T) { p, err := n11.Properties(token1ID) require.NoError(t, err) expected := stackitem.NewMap() expected.Add(stackitem.Make([]byte("name")), stackitem.NewBuffer([]byte("NeoFS Object "+base64.StdEncoding.EncodeToString(token1ID)))) expected.Add(stackitem.Make([]byte("containerID")), stackitem.Make([]byte(base64.StdEncoding.EncodeToString(nfsoToken1ContainerID.BytesBE())))) expected.Add(stackitem.Make([]byte("objectID")), stackitem.Make([]byte(base64.StdEncoding.EncodeToString(nfsoToken1ObjectID.BytesBE())))) require.EqualValues(t, expected, p) }) t.Run("Transfer", func(t *testing.T) { _, _, err := n11.TransferD(priv0, priv1, big.NewInt(20), token1ID, nil) require.NoError(t, err) }) } func TestClient_NNS(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) nnc := nns.NewReader(invoker.New(c, nil), nnsHash) t.Run("IsAvailable, false", func(t *testing.T) { b, err := nnc.IsAvailable("tutus.one") require.NoError(t, err) require.Equal(t, false, b) }) t.Run("IsAvailable, true", func(t *testing.T) { b, err := nnc.IsAvailable("neogo.com") require.NoError(t, err) require.Equal(t, true, b) }) t.Run("Resolve, good", func(t *testing.T) { b, err := nnc.Resolve("tutus.one", nns.A) require.NoError(t, err) require.Equal(t, "1.2.3.4", b) }) t.Run("Resolve, bad", func(t *testing.T) { _, err := nnc.Resolve("neogo.com", nns.A) require.Error(t, err) }) t.Run("Resolve, CNAME", func(t *testing.T) { _, err := nnc.Resolve("neogo.com", nns.CNAME) require.Error(t, err) }) t.Run("GetAllRecords, good", func(t *testing.T) { iter, err := nnc.GetAllRecords("tutus.one") require.NoError(t, err) arr, err := iter.Next(config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, 1, len(arr)) require.Equal(t, nns.RecordState{ Name: "tutus.one", Type: nns.A, Data: "1.2.3.4", }, arr[0]) }) t.Run("GetAllRecordsExpanded, good", func(t *testing.T) { rss, err := nnc.GetAllRecordsExpanded("tutus.one", 42) require.NoError(t, err) require.Equal(t, []nns.RecordState{ { Name: "tutus.one", Type: nns.A, Data: "1.2.3.4", }, }, rss) }) t.Run("GetAllRecords, bad", func(t *testing.T) { _, err := nnc.GetAllRecords("neopython.com") require.Error(t, err) }) t.Run("GetAllRecordsExpanded, bad", func(t *testing.T) { _, err := nnc.GetAllRecordsExpanded("neopython.com", 7) require.Error(t, err) }) } func TestClient_IteratorSessions(t *testing.T) { _, rpcSrv, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{MaxConnsPerHost: 50}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) // storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code. const storageItemsCount = 255 expected := make([][]byte, storageItemsCount) for i := range storageItemsCount { expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes() } slices.SortFunc(expected, func(a, b []byte) int { if len(a) != len(b) { return len(a) - len(b) } return bytes.Compare(a, b) }) prepareSession := func(t *testing.T) (uuid.UUID, uuid.UUID) { res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) require.NoError(t, err) 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("traverse with max constraint", func(t *testing.T) { sID, iID := prepareSession(t) check := func(t *testing.T, start, end int) { maxNum := end - start set, err := c.TraverseIterator(sID, iID, maxNum) require.NoError(t, err) require.Equal(t, maxNum, len(set)) for i := range maxNum { // According to the Storage contract code. require.Equal(t, expected[start+i], set[i].Value().([]byte), start+i) } } check(t, 0, 30) check(t, 30, 48) check(t, 48, 49) check(t, 49, 49+config.DefaultMaxIteratorResultItems) check(t, 49+config.DefaultMaxIteratorResultItems, 49+2*config.DefaultMaxIteratorResultItems-1) check(t, 49+2*config.DefaultMaxIteratorResultItems-1, 255) // Iterator ends on 255-th element, so no more elements should be returned. set, err := c.TraverseIterator(sID, iID, config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, 0, len(set)) }) t.Run("traverse, request more than exists", func(t *testing.T) { sID, iID := prepareSession(t) for range storageItemsCount / config.DefaultMaxIteratorResultItems { set, err := c.TraverseIterator(sID, iID, config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, config.DefaultMaxIteratorResultItems, len(set)) } // Request more items than left untraversed. set, err := c.TraverseIterator(sID, iID, config.DefaultMaxIteratorResultItems) require.NoError(t, err) require.Equal(t, storageItemsCount%config.DefaultMaxIteratorResultItems, len(set)) }) t.Run("traverse, no max constraint", func(t *testing.T) { sID, iID := prepareSession(t) set, err := c.TraverseIterator(sID, iID, -1) require.NoError(t, err) require.Equal(t, config.DefaultMaxIteratorResultItems, len(set)) }) t.Run("traverse, concurrent access", func(t *testing.T) { sID, iID := prepareSession(t) wg := sync.WaitGroup{} wg.Add(storageItemsCount) check := func(t *testing.T) { set, err := c.TraverseIterator(sID, iID, 1) assert.NoError(t, err) assert.Equal(t, 1, len(set)) wg.Done() } for range storageItemsCount { go check(t) } wg.Wait() }) t.Run("terminate session", func(t *testing.T) { t.Run("manually", func(t *testing.T) { sID, iID := prepareSession(t) // Check session is created. set, err := c.TraverseIterator(sID, iID, 1) require.NoError(t, err) require.Equal(t, 1, len(set)) ok, err := c.TerminateSession(sID) require.NoError(t, err) require.True(t, ok) ok, err = c.TerminateSession(sID) require.Error(t, err) require.ErrorIs(t, err, tutusrpc.ErrUnknownSession) require.False(t, ok) // session has already been terminated. }) t.Run("automatically", func(t *testing.T) { sID, iID := prepareSession(t) // Check session is created. set, err := c.TraverseIterator(sID, iID, 1) require.NoError(t, err) require.Equal(t, 1, len(set)) require.Eventually(t, func() bool { rpcSrv.sessionsLock.Lock() defer rpcSrv.sessionsLock.Unlock() _, ok := rpcSrv.sessions[sID.String()] return !ok }, rpcSrv.config.SessionLifetime*3, // Sessions list is updated once per SessionLifetime, thus, no need to ask for update more frequently than // sessions cleaning occurs. rpcSrv.config.SessionLifetime/4) ok, err := c.TerminateSession(sID) require.Error(t, err) require.ErrorIs(t, err, tutusrpc.ErrUnknownSession) require.False(t, ok) // session has already been terminated. }) }) } func TestClient_States(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) stateheight, err := c.GetStateHeight() assert.NoError(t, err) assert.Equal(t, chain.BlockHeight(), stateheight.Local) stateroot, err := c.GetStateRootByHeight(stateheight.Local) assert.NoError(t, err) t.Run("proof", func(t *testing.T) { policy, err := chain.GetNativeContractScriptHash(nativenames.Policy) assert.NoError(t, err) proof, err := c.GetProof(stateroot.Root, policy, []byte{19}) // storagePrice key in policy contract assert.NoError(t, err) value, err := c.VerifyProof(stateroot.Root, proof) assert.NoError(t, err) assert.Equal(t, big.NewInt(native.DefaultStoragePrice), bigint.FromBytes(value)) }) } func TestClientOracle(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) oraRe := oracle.NewReader(invoker.New(c, nil)) var defaultOracleRequestPrice = big.NewInt(5000_0000) actual, err := oraRe.GetPrice() require.NoError(t, err) require.Equal(t, defaultOracleRequestPrice, actual) act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.CalledByEntry, }, Account: &wallet.Account{ Address: testchain.CommitteeAddress(), Contract: &wallet.Contract{ Script: testchain.CommitteeVerificationScript(), }, }, }}) require.NoError(t, err) ora := oracle.New(act) newPrice := big.NewInt(1_0000_0000) tx, err := ora.SetPriceUnsigned(newPrice) require.NoError(t, err) tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) bl := testchain.NewBlock(t, chain, 1, 0, tx) _, err = c.SubmitBlock(*bl) require.NoError(t, err) actual, err = ora.GetPrice() require.NoError(t, err) require.Equal(t, newPrice, actual) } func TestClient_Iterator_SessionConfigVariations(t *testing.T) { var expected [][]byte // storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code. const storageItemsCount = 255 checkSessionEnabled := func(t *testing.T, c *rpcclient.Client) { // We expect Iterator with designated ID to be presented on stack. It should be possible to retrieve its values via `traverseiterator` call. res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) require.NoError(t, err) 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) require.Empty(t, iterator.Values) maxNum := 84 actual, err := c.TraverseIterator(res.Session, *iterator.ID, maxNum) require.NoError(t, err) require.Equal(t, maxNum, len(actual)) for i := range maxNum { // According to the Storage contract code. require.Equal(t, expected[i], actual[i].Value().([]byte), i) } } t.Run("default sessions enabled", func(t *testing.T) { chain, _, httpSrv := initClearServerWithServices(t, false, false, false) for _, b := range getTestBlocks(t) { require.NoError(t, chain.AddBlock(b)) } c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) // Fill in expected stackitems set during the first test. expected = make([][]byte, storageItemsCount) for i := range storageItemsCount { expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes() } slices.SortFunc(expected, func(a, b []byte) int { if len(a) != len(b) { return len(a) - len(b) } return bytes.Compare(a, b) }) checkSessionEnabled(t, c) }) t.Run("MPT-based sessions enables", func(t *testing.T) { // Prepare MPT-enabled RPC server. chain, orc, cfg, logger := getUnitTestChainWithCustomConfig(t, false, false, func(cfg *config.Config) { cfg.ApplicationConfiguration.RPC.SessionEnabled = true cfg.ApplicationConfiguration.RPC.SessionBackedByMPT = true }) serverConfig, err := network.NewServerConfig(cfg) require.NoError(t, err) serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test") serverConfig.Addresses = []config.AnnounceableAddress{{Address: ":0"}} server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger) require.NoError(t, err) errCh := make(chan error, 2) rpcSrv := New(chain, cfg.ApplicationConfiguration.RPC, server, orc, logger, errCh) rpcSrv.Start() handler := http.HandlerFunc(rpcSrv.handleHTTPRequest) httpSrv := httptest.NewServer(handler) t.Cleanup(httpSrv.Close) defer rpcSrv.Shutdown() for _, b := range getTestBlocks(t) { require.NoError(t, chain.AddBlock(b)) } c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) checkSessionEnabled(t, c) }) t.Run("sessions disabled", func(t *testing.T) { chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true) for _, b := range getTestBlocks(t) { require.NoError(t, chain.AddBlock(b)) } c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) // We expect unpacked iterator values to be present on stack under InteropInterface cover. res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) require.NoError(t, err) require.Empty(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.Empty(t, iterator.ID) require.NotEmpty(t, iterator.Values) require.True(t, iterator.Truncated) require.Equal(t, rpcSrv.config.MaxIteratorResultItems, len(iterator.Values)) for i := range rpcSrv.config.MaxIteratorResultItems { // According to the Storage contract code. require.Equal(t, expected[i], iterator.Values[i].Value().([]byte), i) } }) } func TestClient_Wait(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) run := func(t *testing.T, ws bool) { acc, err := wallet.NewAccount() require.NoError(t, err) var act *actor.Actor if ws { c, err := rpcclient.NewWS(context.Background(), "ws"+strings.TrimPrefix(httpSrv.URL, "http")+"/ws", rpcclient.WSOptions{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) act, err = actor.New(c, []actor.SignerAccount{ { Signer: transaction.Signer{ Account: acc.ScriptHash(), }, Account: acc, }, }) require.NoError(t, err) } else { c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) act, err = actor.New(c, []actor.SignerAccount{ { Signer: transaction.Signer{ Account: acc.ScriptHash(), }, Account: acc, }, }) require.NoError(t, err) } b1, err := chain.GetBlock(chain.GetHeaderHash(1)) require.NoError(t, err) require.True(t, len(b1.Transactions) > 0) b23, err := chain.GetBlock(chain.GetHeaderHash(24)) // block with faulted tx and extended VUB. require.NoError(t, err) require.True(t, len(b23.Transactions) > 0) // Ensure Waiter constructor works properly. if ws { _, ok := act.Waiter.(*waiter.EventBased) require.True(t, ok) } else { _, ok := act.Waiter.(*waiter.PollingBased) require.True(t, ok) } check := func(t *testing.T, tx *transaction.Transaction, h util.Uint256, vub uint32, errExpected bool) { rcvr := make(chan struct{}) go func() { var err error if tx != nil { // Send one more time to ensure [tutusrpc.ErrAlreadyExists] is properly handled by Waiter. h, vub, err = act.Send(tx) } aer, err := act.Wait(context.Background(), h, vub, err) if errExpected { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, h, aer.Container) } rcvr <- struct{}{} }() waitloop: for { select { case <-rcvr: break waitloop case <-time.NewTimer(chain.GetConfig().TimePerBlock).C: t.Fatal("transaction failed to be awaited") } } } // Wait for a transaction that is already on-chain and double-sent. check(t, b23.Transactions[0], util.Uint256{}, 0, false) // Wait for transaction that has been persisted and VUB block has been persisted. check(t, nil, b1.Transactions[0].Hash(), chain.BlockHeight()-1, false) // Wait for transaction that has been persisted and VUB block hasn't yet been persisted. check(t, nil, b1.Transactions[0].Hash(), chain.BlockHeight()+1, false) if !ws { // Wait for transaction that hasn't been persisted and VUB block has been persisted. // WS client waits for the next block to be accepted to ensure that transaction wasn't // persisted, and this test doesn't run chain, thus, don't run this test for WS client. check(t, nil, util.Uint256{1, 2, 3}, chain.BlockHeight()-1, true) } } t.Run("client", func(t *testing.T) { run(t, false) }) t.Run("ws client", func(t *testing.T) { run(t, true) }) } func mkSubsClient(t *testing.T, rpcSrv *Server, httpSrv *httptest.Server, local bool) *rpcclient.WSClient { var ( c *rpcclient.WSClient err error icl *rpcclient.Internal ) if local { icl, err = rpcclient.NewInternal(context.Background(), rpcSrv.RegisterLocal) } else { url := "ws" + strings.TrimPrefix(httpSrv.URL, "http") + "/ws" c, err = rpcclient.NewWS(context.Background(), url, rpcclient.WSOptions{}) t.Cleanup(c.Close) } require.NoError(t, err) if local { c = &icl.WSClient } require.NoError(t, c.Init()) return c } func runWSAndLocal(t *testing.T, test func(*testing.T, bool)) { t.Run("ws", func(t *testing.T) { test(t, false) }) t.Run("local", func(t *testing.T) { test(t, true) }) } func TestSubClientWait(t *testing.T) { runWSAndLocal(t, testSubClientWait) } func testSubClientWait(t *testing.T, local bool) { chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true) c := mkSubsClient(t, rpcSrv, httpSrv, local) acc, err := wallet.NewAccount() require.NoError(t, err) act, err := actor.New(c, []actor.SignerAccount{ { Signer: transaction.Signer{ Account: acc.ScriptHash(), }, Account: acc, }, }) require.NoError(t, err) rcvr := make(chan *state.AppExecResult) check := func(t *testing.T, b *block.Block, h util.Uint256, vub uint32) { go func() { aer, err := act.Wait(context.Background(), h, vub, nil) require.NoError(t, err, b.Index) rcvr <- aer }() go func() { // Wait until client is properly subscribed. The real node won't behave like this, // but the real node has the subsequent blocks to be added that will trigger client's // waitloops to finish anyway (and the test has only single block, thus, use it careful). require.Eventually(t, func() bool { rpcSrv.subsLock.Lock() defer rpcSrv.subsLock.Unlock() if len(rpcSrv.subscribers) == 1 { // single client for s := range rpcSrv.subscribers { var count int for _, f := range s.feeds { if f.event != tutusrpc.InvalidEventID { count++ } } return count == 2 // subscription for blocks + AERs } } return false }, time.Second, 100*time.Millisecond) require.NoError(t, chain.AddBlock(b)) }() waitloop: for { select { case aer := <-rcvr: require.Equal(t, h, aer.Container) require.Equal(t, trigger.Application, aer.Trigger) if h.StringLE() == faultedTxHashLE { require.Equal(t, vmstate.Fault, aer.VMState) } else { require.Equal(t, vmstate.Halt, aer.VMState) } break waitloop case <-time.NewTimer(chain.GetConfig().TimePerBlock).C: t.Fatalf("transaction from block %d failed to be awaited: deadline exceeded", b.Index) } } // Wait for server/client to properly unsubscribe. In real life subsequent awaiter // requests may be run concurrently, and it's OK, but it's important for the test // not to run subscription requests in parallel because block addition is bounded to // the number of subscribers. require.Eventually(t, func() bool { rpcSrv.subsLock.Lock() defer rpcSrv.subsLock.Unlock() if len(rpcSrv.subscribers) != 1 { return false } for s := range rpcSrv.subscribers { for _, f := range s.feeds { if f.event != tutusrpc.InvalidEventID { return false } } } return true }, time.Second, 100*time.Millisecond) } var faultedChecked bool for _, b := range getTestBlocks(t) { if len(b.Transactions) > 0 { tx := b.Transactions[0] check(t, b, tx.Hash(), tx.ValidUntilBlock) if tx.Hash().StringLE() == faultedTxHashLE { faultedChecked = true } } else { require.NoError(t, chain.AddBlock(b)) } } require.True(t, faultedChecked, "FAULTed transaction wasn't checked") } func TestSubClientWaitWithLateSubscription(t *testing.T) { runWSAndLocal(t, testSubClientWaitWithLateSubscription) } func testSubClientWaitWithLateSubscription(t *testing.T, local bool) { chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true) c := mkSubsClient(t, rpcSrv, httpSrv, local) acc, err := wallet.NewAccount() require.NoError(t, err) act, err := actor.New(c, []actor.SignerAccount{ { Signer: transaction.Signer{ Account: acc.ScriptHash(), }, Account: acc, }, }) require.NoError(t, err) // Firstly, accept the block. blocks := getTestBlocks(t) b1 := blocks[0] tx := b1.Transactions[0] require.NoError(t, chain.AddBlock(b1)) // After that, wait and get the result immediately. aer, err := act.Wait(context.Background(), tx.Hash(), tx.ValidUntilBlock, nil) require.NoError(t, err) require.Equal(t, tx.Hash(), aer.Container) require.Equal(t, trigger.Application, aer.Trigger) require.Equal(t, vmstate.Halt, aer.VMState) } func TestWSClientHandshakeError(t *testing.T) { _, _, httpSrv := initClearServerWithCustomConfig(t, func(cfg *config.Config) { cfg.ApplicationConfiguration.RPC.MaxWebSocketClients = -1 }) url := "ws" + strings.TrimPrefix(httpSrv.URL, "http") + "/ws" _, err := rpcclient.NewWS(context.Background(), url, rpcclient.WSOptions{}) require.ErrorContains(t, err, "websocket users limit reached") } func TestSubClientWaitWithMissedEvent(t *testing.T) { runWSAndLocal(t, testSubClientWaitWithMissedEvent) } func testSubClientWaitWithMissedEvent(t *testing.T, local bool) { chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true) c := mkSubsClient(t, rpcSrv, httpSrv, local) acc, err := wallet.NewAccount() require.NoError(t, err) act, err := actor.New(c, []actor.SignerAccount{ { Signer: transaction.Signer{ Account: acc.ScriptHash(), }, Account: acc, }, }) require.NoError(t, err) blocks := getTestBlocks(t) b1 := blocks[0] tx := b1.Transactions[0] rcvr := make(chan *state.AppExecResult) errCh := make(chan error) // Error channel for goroutine errors go func() { aer, err := act.Wait(context.Background(), tx.Hash(), tx.ValidUntilBlock, nil) if err != nil { errCh <- err return } rcvr <- aer }() // Wait until client is properly subscribed. The real node won't behave like this, // but the real node has the subsequent blocks to be added that will trigger client's // waitloops to finish anyway (and the test has only single block, thus, use it careful). require.Eventually(t, func() bool { rpcSrv.subsLock.Lock() defer rpcSrv.subsLock.Unlock() return len(rpcSrv.subscribers) == 1 }, 2*time.Second, 100*time.Millisecond) rpcSrv.subsLock.Lock() // Suppress normal event delivery. for s := range rpcSrv.subscribers { s.overflown.Store(true) } rpcSrv.subsLock.Unlock() // Accept the next block, but subscriber will get no events because it's overflown. require.NoError(t, chain.AddBlock(b1)) overNotification := tutusrpc.Notification{ JSONRPC: tutusrpc.JSONRPCVersion, Event: tutusrpc.MissedEventID, Payload: make([]any, 0), } overEvent, err := json.Marshal(overNotification) require.NoError(t, err) overflowMsg, err := websocket.NewPreparedMessage(websocket.TextMessage, overEvent) require.NoError(t, err) rpcSrv.subsLock.Lock() // Deliver overflow message -> triggers subscriber to retry with polling waiter. for s := range rpcSrv.subscribers { s.writer <- intEvent{overflowMsg, &overNotification} } rpcSrv.subsLock.Unlock() // Wait for the result. waitloop: for { select { case aer := <-rcvr: require.Equal(t, tx.Hash(), aer.Container) require.Equal(t, trigger.Application, aer.Trigger) require.Equal(t, vmstate.Halt, aer.VMState) break waitloop case err := <-errCh: t.Fatalf("Error waiting for transaction: %v", err) case <-time.NewTimer(chain.GetConfig().TimePerBlock).C: t.Fatal("transaction failed to be awaited") } } } // TestWSClient_SubscriptionsCompat is aimed to test both deprecated and relevant // subscriptions API with filtered and non-filtered subscriptions from the WSClient // user side. func TestWSClient_SubscriptionsCompat(t *testing.T) { chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true) c := mkSubsClient(t, rpcSrv, httpSrv, false) blocks := getTestBlocks(t) bCount := uint32(0) getData := func(t *testing.T) (*block.Block, *block.Block, byte, util.Uint160, string, string) { b1 := blocks[bCount] primary := b1.PrimaryIndex tx := b1.Transactions[0] sender := tx.Sender() ntfName := "Transfer" st := vmstate.Halt.String() b2 := blocks[bCount+1] bCount += 2 return b1, b2, primary, sender, ntfName, st } checkRelevant := func(t *testing.T, filtered bool) { b, bNext, primary, sender, ntfName, st := getData(t) typ := mempoolevent.TransactionAdded var ( bID, txID, ntfID, aerID, memID string blockCh = make(chan *block.Block) txCh = make(chan *transaction.Transaction) ntfCh = make(chan *state.ContainedNotificationEvent) aerCh = make(chan *state.AppExecResult) memCh = make(chan *result.MempoolEvent) bFlt *tutusrpc.BlockFilter txFlt *tutusrpc.TxFilter ntfFlt *tutusrpc.NotificationFilter aerFlt *tutusrpc.ExecutionFilter memFlt *tutusrpc.MempoolEventFilter err error ) if filtered { bFlt = &tutusrpc.BlockFilter{Primary: &primary} txFlt = &tutusrpc.TxFilter{Sender: &sender} ntfFlt = &tutusrpc.NotificationFilter{Name: &ntfName} aerFlt = &tutusrpc.ExecutionFilter{State: &st} memFlt = &tutusrpc.MempoolEventFilter{Type: &typ} } bID, err = c.ReceiveBlocks(bFlt, blockCh) require.NoError(t, err) txID, err = c.ReceiveTransactions(txFlt, txCh) require.NoError(t, err) ntfID, err = c.ReceiveExecutionNotifications(ntfFlt, ntfCh) require.NoError(t, err) aerID, err = c.ReceiveExecutions(aerFlt, aerCh) require.NoError(t, err) memID, err = c.ReceiveMempoolEvents(memFlt, memCh) require.NoError(t, err) var ( lock sync.RWMutex received byte exitCh = make(chan struct{}) ) go func() { dispatcher: for { select { case <-blockCh: lock.Lock() received |= 1 lock.Unlock() case <-txCh: lock.Lock() received |= 1 << 1 lock.Unlock() case <-ntfCh: lock.Lock() received |= 1 << 2 lock.Unlock() case <-aerCh: lock.Lock() received |= 1 << 3 lock.Unlock() case <-memCh: lock.Lock() received |= 1 << 4 lock.Unlock() case <-exitCh: break dispatcher } } drainLoop: for { select { case <-blockCh: case <-txCh: case <-ntfCh: case <-aerCh: case <-memCh: default: break drainLoop } } close(blockCh) close(txCh) close(ntfCh) close(aerCh) close(memCh) }() // Accept the next block and wait for events. require.NoError(t, chain.AddBlock(b)) // Blockchain's events channel is not buffered, and thus, by adding one more extra block // we're ensuring that the previous block event receiving was successfully handled by Blockchain's // notificationDispatcher loop. Once we're sure in that, we may start to check the actual notifications. require.NoError(t, chain.AddBlock(bNext)) // Generate mempool event. tx := &transaction.Transaction{ Signers: []transaction.Signer{{Account: sender}}, Script: []byte{byte(opcode.PUSH1)}, } require.NoError(t, chain.GetMemPool().Add(tx, &FeerStub{})) assert.Eventually(t, func() bool { lock.RLock() defer lock.RUnlock() return received == 1<<5-1 }, time.Second, 100*time.Millisecond) require.NoError(t, c.Unsubscribe(bID)) require.NoError(t, c.Unsubscribe(txID)) require.NoError(t, c.Unsubscribe(ntfID)) require.NoError(t, c.Unsubscribe(aerID)) require.NoError(t, c.Unsubscribe(memID)) exitCh <- struct{}{} } t.Run("relevant, filtered", func(t *testing.T) { checkRelevant(t, true) }) t.Run("relevant, non-filtered", func(t *testing.T) { checkRelevant(t, false) }) } func TestActor_CallWithNilParam(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) acc, err := wallet.NewAccount() require.NoError(t, err) act, err := actor.New(c, []actor.SignerAccount{ { Signer: transaction.Signer{ Account: acc.ScriptHash(), }, Account: acc, }, }) require.NoError(t, err) rubles, err := chain.GetContractScriptHash(basicchain.RublesContractID) require.NoError(t, err) // We don't have a suitable contract, thus use Rubles with simple put method, // it should fail at the moment of conversion Null value to ByteString (not earlier, // and that's the point of the test!). res, err := act.Call(rubles, "putValue", "123", (*util.Uint160)(nil)) require.NoError(t, err) require.True(t, strings.Contains(res.FaultException, "invalid conversion: Null/ByteString"), res.FaultException) } func TestClient_FindStorage(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) h, err := util.Uint160DecodeStringLE(testContractHashLE) require.NoError(t, err) prefix := []byte("aa") expected := result.FindStorage{ Results: []result.KeyValue{ { Key: []byte("aa"), Value: []byte("v1"), }, { Key: []byte("aa10"), Value: []byte("v2"), }, }, Next: 2, Truncated: true, } // By hash. actual, err := c.FindStorageByHash(h, prefix, nil) require.NoError(t, err) require.Equal(t, expected, actual) // By ID. actual, err = c.FindStorageByID(1, prefix, nil) // Rubles contract require.NoError(t, err) require.Equal(t, expected, actual) // Non-nil start. start := 1 actual, err = c.FindStorageByHash(h, prefix, &start) require.NoError(t, err) require.Equal(t, result.FindStorage{ Results: []result.KeyValue{ { Key: []byte("aa10"), Value: []byte("v2"), }, { Key: []byte("aa50"), Value: []byte("v3"), }, }, Next: 3, Truncated: false, }, actual) // Missing item. actual, err = c.FindStorageByHash(h, []byte("unknown prefix"), nil) require.NoError(t, err) require.Equal(t, result.FindStorage{ Results: []result.KeyValue{}, Next: 0, Truncated: false, }, actual) } func TestClient_FindStorageHistoric(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) root, err := util.Uint256DecodeStringLE(block21StateRootLE) require.NoError(t, err) h, err := util.Uint160DecodeStringLE(testContractHashLE) require.NoError(t, err) prefix := []byte("aa") expected := result.FindStorage{ Results: []result.KeyValue{ { Key: []byte("aa10"), Value: []byte("v2"), }, { Key: []byte("aa50"), Value: []byte("v3"), }, }, Next: 2, Truncated: true, } // By hash. actual, err := c.FindStorageByHashHistoric(root, h, prefix, nil) require.NoError(t, err) require.Equal(t, expected, actual) // By ID. actual, err = c.FindStorageByIDHistoric(root, 1, prefix, nil) // Rubles contract require.NoError(t, err) require.Equal(t, expected, actual) // Non-nil start. start := 1 actual, err = c.FindStorageByHashHistoric(root, h, prefix, &start) require.NoError(t, err) require.Equal(t, result.FindStorage{ Results: []result.KeyValue{ { Key: []byte("aa50"), Value: []byte("v3"), }, { Key: []byte("aa"), // order differs due to MPT traversal strategy. Value: []byte("v1"), }, }, Next: 3, Truncated: false, }, actual) // Missing item. earlyRoot, err := chain.GetStateRoot(15) // there's no `aa10` value in Rubles contract by the moment of block #15 require.NoError(t, err) actual, err = c.FindStorageByHashHistoric(earlyRoot.Root, h, prefix, nil) require.NoError(t, err) require.Equal(t, result.FindStorage{ Results: []result.KeyValue{}, Next: 0, Truncated: false, }, actual) } func TestClient_GetStorageHistoric(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) root, err := util.Uint256DecodeStringLE(block21StateRootLE) require.NoError(t, err) h, err := util.Uint160DecodeStringLE(testContractHashLE) require.NoError(t, err) key := []byte("aa10") expected := []byte("v2") // By hash. actual, err := c.GetStorageByHashHistoric(root, h, key) require.NoError(t, err) require.Equal(t, expected, actual) // By ID. actual, err = c.GetStorageByIDHistoric(root, 1, key) // Rubles contract require.NoError(t, err) require.Equal(t, expected, actual) // Missing item. earlyRoot, err := chain.GetStateRoot(15) // there's no `aa10` value in Rubles contract by the moment of block #15 require.NoError(t, err) _, err = c.GetStorageByHashHistoric(earlyRoot.Root, h, key) require.ErrorIs(t, tutusrpc.ErrUnknownStorageItem, err) } func TestClient_GetVersion_Hardforks(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) v, err := c.GetVersion() require.NoError(t, err) expected := map[config.Hardfork]uint32{ config.HFAspidochelone: 1, config.HFBasilisk: 2, config.HFCockatrice: 3, config.HFDomovoi: 4, config.HFEchidna: 5, } require.InDeltaMapValues(t, expected, v.Protocol.Hardforks, 0) } func TestClient_GetVersion(t *testing.T) { chain, orc, cfg, logger := getUnitTestChainWithCustomConfig(t, false, false, func(cfg *config.Config) { cfg.ApplicationConfiguration.KeepOnlyLatestState = true cfg.ApplicationConfiguration.SaveInvocations = true cfg.ApplicationConfiguration.RemoveUntraceableBlocks = true }) _, _, httpSrv := wrapUnitTestChain(t, chain, orc, cfg, logger) client, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(client.Close) require.NoError(t, client.Init()) version, err := client.GetVersion() require.NoError(t, err) require.True(t, version.Application.SaveInvocations) require.True(t, version.Application.KeepOnlyLatestState) require.True(t, version.Application.RemoveUntraceableBlocks) } func TestClient_NEP24(t *testing.T) { _, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) h, err := util.Uint160DecodeStringLE(nfsoContractHash) require.NoError(t, err) reader := nep24.NewRoyaltyReader(invoker.New(c, nil), h) t.Run("RoyaltyInfo", func(t *testing.T) { id, err := util.Uint160DecodeStringLE(nfsoToken1ID) require.NoError(t, err) info, err := reader.RoyaltyInfo(id.BytesLE(), h, big.NewInt(1000)) require.NoError(t, err) for _, r := range info { require.Equal(t, big.NewInt(50), r.Amount) } }) } func TestGetBlockNotifications(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) t.Run("nil filter", func(t *testing.T) { bn, err := c.GetBlockNotifications(chain.GetHeaderHash(1), nil) require.NoError(t, err) require.NotEmpty(t, bn) }) t.Run("empty filter", func(t *testing.T) { bn, err := c.GetBlockNotifications(chain.GetHeaderHash(1), &tutusrpc.NotificationFilter{}) require.NoError(t, err) require.NotEmpty(t, bn) }) t.Run("bad filter", func(t *testing.T) { badName := "Transfer1" bn, err := c.GetBlockNotifications(chain.GetHeaderHash(1), &tutusrpc.NotificationFilter{ Name: &badName, }) require.NoError(t, err) require.Empty(t, bn) }) t.Run("good", func(t *testing.T) { name := "Transfer" bn, err := c.GetBlockNotifications(chain.GetHeaderHash(1), &tutusrpc.NotificationFilter{ Name: &name, }) require.NoError(t, err) require.NotEmpty(t, bn) }) } func TestClient_SessionExpansionExtension(t *testing.T) { const ( // storageItemsCount is the amount of storage items stored in Storage contract, // it's hard-coded in the contract code. storageItemsCount = 255 expectedMaxIteratorResultItems = 50 batchSize = 33 ) expected := make([][]byte, storageItemsCount) for i := range storageItemsCount { expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes() } slices.SortFunc(expected, func(a, b []byte) int { if len(a) != len(b) { return len(a) - len(b) } return bytes.Compare(a, b) }) setup := func(t *testing.T, cfgMod func(*config.Config)) *rpcclient.Client { chain, rpcSrv, httpSrv := initClearServerWithCustomConfig(t, cfgMod) t.Cleanup(rpcSrv.Shutdown) t.Cleanup(httpSrv.Close) for _, b := range getTestBlocks(t) { require.NoError(t, chain.AddBlock(b)) } c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) require.NoError(t, c.Init()) t.Cleanup(c.Close) return c } prepareSession := func(t *testing.T, c *rpcclient.Client) (uuid.UUID, uuid.UUID, []stackitem.Item) { res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) require.NoError(t, err) 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, iterator.Values } t.Run("session disabled", func(t *testing.T) { c := setup(t, func(cfg *config.Config) { cfg.ApplicationConfiguration.RPC.Enabled = true cfg.ApplicationConfiguration.RPC.SessionEnabled = false cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = true cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems }) invokeRes, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) require.NoError(t, err) require.Equal(t, vmstate.Halt.String(), invokeRes.State) require.NotEmpty(t, invokeRes.Script) require.Empty(t, invokeRes.Session) require.Len(t, invokeRes.Stack, 1) stackItem := invokeRes.Stack[0] require.Equal(t, stackitem.InteropT, stackItem.Type()) iterVal, ok := stackItem.Value().(result.Iterator) require.True(t, ok) require.Equal(t, expectedMaxIteratorResultItems, len(iterVal.Values)) for i := range len(iterVal.Values) { require.Equal(t, expected[i], iterVal.Values[i].Value().([]byte), i) } require.True(t, iterVal.Truncated) require.Empty(t, iterVal.ID) }) t.Run("session disabled and session expansion disabled", func(t *testing.T) { c := setup(t, func(cfg *config.Config) { cfg.ApplicationConfiguration.RPC.Enabled = true cfg.ApplicationConfiguration.RPC.SessionEnabled = false cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = false cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems }) invokeRes, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) require.NoError(t, err) require.Equal(t, vmstate.Halt.String(), invokeRes.State) require.NotEmpty(t, invokeRes.Script) require.Empty(t, invokeRes.Session) require.Len(t, invokeRes.Stack, 1) stackItem := invokeRes.Stack[0] require.Equal(t, stackitem.InteropT, stackItem.Type()) iterVal, ok := stackItem.Value().(result.Iterator) require.True(t, ok) require.Equal(t, expectedMaxIteratorResultItems, len(iterVal.Values)) for i := range len(iterVal.Values) { require.Equal(t, expected[i], iterVal.Values[i].Value().([]byte), i) } require.True(t, iterVal.Truncated) require.Empty(t, iterVal.ID) }) t.Run("session enabled and SessionBackedByMPT", func(t *testing.T) { c := setup(t, func(cfg *config.Config) { cfg.ApplicationConfiguration.RPC.Enabled = true cfg.ApplicationConfiguration.RPC.SessionEnabled = true cfg.ApplicationConfiguration.RPC.SessionBackedByMPT = true cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = true cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems }) sID, iID, curr := prepareSession(t, c) require.Equal(t, len(curr), expectedMaxIteratorResultItems) for i := range curr { require.Equal(t, expected[i], curr[i].Value().([]byte), i) } var batchCount int for { batch, err := c.TraverseIterator(sID, iID, batchSize) require.NoError(t, err) if len(batch) == 0 { break } for i := range batch { require.Equal(t, expected[expectedMaxIteratorResultItems+batchCount*batchSize+i], batch[i].Value().([]byte), i) } batchCount++ } require.Equal(t, (storageItemsCount-expectedMaxIteratorResultItems)/batchSize+1, batchCount) }) t.Run("session enabled", func(t *testing.T) { c := setup(t, func(cfg *config.Config) { cfg.ApplicationConfiguration.RPC.Enabled = true cfg.ApplicationConfiguration.RPC.SessionEnabled = true cfg.ApplicationConfiguration.RPC.SessionExpansionEnabled = true cfg.ApplicationConfiguration.RPC.MaxIteratorResultItems = expectedMaxIteratorResultItems }) sID, iID, localItems := prepareSession(t, c) require.Equal(t, expectedMaxIteratorResultItems, len(localItems)) for i := range localItems { require.Equal(t, expected[i], localItems[i].Value().([]byte)) } firstBatch, err := c.TraverseIterator(sID, iID, 20) require.NoError(t, err) require.Len(t, firstBatch, 20) for i := range firstBatch { require.Equal(t, expected[expectedMaxIteratorResultItems+i], firstBatch[i].Value().([]byte)) } secondBatch, err := c.TraverseIterator(sID, iID, 20) require.NoError(t, err) require.Len(t, secondBatch, 20) for i := range secondBatch { require.Equal(t, expected[expectedMaxIteratorResultItems+20+i], secondBatch[i].Value().([]byte)) } thirdBatch, err := c.TraverseIterator(sID, iID, 25) require.NoError(t, err) require.Len(t, thirdBatch, 25) for i := range thirdBatch { require.Equal(t, expected[expectedMaxIteratorResultItems+20+20+i], thirdBatch[i].Value().([]byte)) } idx := expectedMaxIteratorResultItems + 20 + 20 + 25 for { batch, err := c.TraverseIterator(sID, iID, batchSize) require.NoError(t, err) if len(batch) == 0 { break } for i := range batch { require.Equal(t, expected[idx+i], batch[i].Value().([]byte)) } idx += len(batch) } require.Equal(t, storageItemsCount, idx) }) } func TestClient_InvokeContainedScript(t *testing.T) { chain, _, httpSrv := initServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) var ( trig = trigger.Application verbose = true 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, } txH = util.Uint256{9, 8, 7} txSize = 123 ) tx := transaction.NewFakeTX([]byte{1, 2, 3}, transaction.Signer{ Account: util.Uint160{1, 2, 3}, Scopes: transaction.CalledByEntry, }, txH, txSize) tx.Version = 1 tx.Nonce = 2 tx.SystemFee = 3 tx.NetworkFee = 4 tx.ValidUntilBlock = 5 type testCase struct { name string script []byte expected any } var ( testCases []testCase src = io.NewBufBinWriter() ) emit.AppCall(src.BinWriter, nativehashes.LedgerContract, "currentIndex", callflag.All) testCases = append(testCases, testCase{ name: "Ledger.currentIndex", script: slices.Clone(src.Bytes()), expected: h.Index - 1, // index of persisted block. }) src.Reset() emit.AppCall(src.BinWriter, nativehashes.LedgerContract, "currentHash", callflag.All) testCases = append(testCases, testCase{ name: "Ledger.currentHash", script: slices.Clone(src.Bytes()), expected: chain.GetHeaderHash(h.Index - 1), // hash of persisted block. }) src.Reset() emit.Syscall(src.BinWriter, interopnames.SystemRuntimeGetTrigger) testCases = append(testCases, testCase{ name: interopnames.SystemRuntimeGetTrigger, script: slices.Clone(src.Bytes()), expected: trig, }) src.Reset() emit.Syscall(src.BinWriter, interopnames.SystemRuntimeGetTime) testCases = append(testCases, testCase{ name: interopnames.SystemRuntimeGetTime, script: slices.Clone(src.Bytes()), expected: h.Timestamp, }) src.Reset() emit.Syscall(src.BinWriter, interopnames.SystemRuntimeGetScriptContainer) s := slices.Clone(src.Bytes()) testCases = append(testCases, testCase{ name: interopnames.SystemRuntimeGetScriptContainer, script: s, expected: stackitem.NewArray([]stackitem.Item{ stackitem.Make(txH), stackitem.Make(tx.Version), stackitem.Make(tx.Nonce), stackitem.Make(tx.Sender()), stackitem.Make(tx.SystemFee), stackitem.Make(tx.NetworkFee), stackitem.Make(tx.ValidUntilBlock), stackitem.Make(s), }), }) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tx.Script = tc.script res, err := c.InvokeContainedScript(tx, h, &trig, &verbose) require.NoError(t, err) require.Equal(t, stackitem.Make(tc.expected), res.Stack[0]) }) } } func TestClient_GetBlockHeader(t *testing.T) { chain, _, httpSrv := initClearServerWithInMemoryChain(t) c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) require.NoError(t, err) t.Cleanup(c.Close) require.NoError(t, c.Init()) expected, err := chain.GetHeader(chain.GetHeaderHash(0)) require.NoError(t, err) w := io.NewBufBinWriter() expected.EncodeBinary(w.BinWriter) require.NoError(t, w.Err) size := len(w.Bytes()) expectedRes := &result.Header{ Header: *expected, BlockMetadata: result.BlockMetadata{ Size: size, NextBlockHash: nil, Confirmations: 1, }, } t.Run("by hash", func(t *testing.T) { h, err := c.GetBlockHeaderByHash(expected.Hash()) require.NoError(t, err) require.Equal(t, expected, h) }) t.Run("by hash verbose", func(t *testing.T) { h, err := c.GetBlockHeaderByHashVerbose(expected.Hash()) require.NoError(t, err) require.Equal(t, expectedRes, h) }) t.Run("by index", func(t *testing.T) { h, err := c.GetBlockHeaderByIndex(0) require.NoError(t, err) require.Equal(t, expected, h) }) t.Run("by index verbose", func(t *testing.T) { h, err := c.GetBlockHeaderByIndexVerbose(0) require.NoError(t, err) require.Equal(t, expectedRes, h) }) }