neo-go/pkg/services/rpcsrv/client_test.go
Ekaterina Pavlova b63c7aad73 rpcsrv: add NEP24 test
Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
2024-11-20 14:45:28 +03:00

2486 lines
78 KiB
Go

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/nspcc-dev/neo-go/internal/basicchain"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neorpc"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/network"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep24"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/oracle"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/policy"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/nspcc-dev/neo-go/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(testContractHash)
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 := 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())
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)
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)
txblock, err := polis.BlockAccountUnsigned(util.Uint160{1, 2, 3})
require.NoError(t, err)
for _, tx := range []*transaction.Transaction{txattr, txblock, txstorage, txnetfee, txexec} {
tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx)
}
bl := testchain.NewBlock(t, chain, 1, 0, txattr, txblock, 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)
}
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{testContractHash, 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 := neo.NewReader(invoker.New(c, nil))
sym, err := neoR.Symbol()
require.NoError(t, err)
require.Equal(t, "NEO", 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(10000), 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 := neo.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 := neo.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)
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 := neo.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 := neo.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, neorpc.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, neorpc.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 `neo.com` 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("neo.com"))
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("neo.com")}, 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("neo.com")}, items)
})
t.Run("Properties", func(t *testing.T) {
p, err := n11.Properties([]byte("neo.com"))
require.NoError(t, err)
blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(14)) // `neo.com` 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("neo.com")))
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("neo.com"), 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("neo.com")
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("neo.com", 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("neo.com")
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: "neo.com",
Type: nns.A,
Data: "1.2.3.4",
}, arr[0])
})
t.Run("GetAllRecordsExpanded, good", func(t *testing.T) {
rss, err := nnc.GetAllRecordsExpanded("neo.com", 42)
require.NoError(t, err)
require.Equal(t, []nns.RecordState{
{
Name: "neo.com",
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())
storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
require.NoError(t, err)
// 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, neorpc.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
}, time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second*3,
// Sessions list is updated once per SessionExpirationTime, thus, no need to ask for update more frequently than
// sessions cleaning occurs.
time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second/4)
ok, err := c.TerminateSession(sID)
require.Error(t, err)
require.ErrorIs(t, err, neorpc.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
storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
require.NoError(t, err)
// 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)
}
b, err := chain.GetBlock(chain.GetHeaderHash(1))
require.NoError(t, err)
require.True(t, len(b.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, h util.Uint256, vub uint32, errExpected bool) {
rcvr := make(chan struct{})
go func() {
aer, err := act.Wait(h, vub, nil)
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 transaction that has been persisted and VUB block has been persisted.
check(t, b.Transactions[0].Hash(), chain.BlockHeight()-1, false)
// Wait for transaction that has been persisted and VUB block hasn't yet been persisted.
check(t, b.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, 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(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 != neorpc.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 != neorpc.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(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(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 := neorpc.Notification{
JSONRPC: neorpc.JSONRPCVersion,
Event: neorpc.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)
var (
bID, txID, ntfID, aerID string
blockCh = make(chan *block.Block)
txCh = make(chan *transaction.Transaction)
ntfCh = make(chan *state.ContainedNotificationEvent)
aerCh = make(chan *state.AppExecResult)
bFlt *neorpc.BlockFilter
txFlt *neorpc.TxFilter
ntfFlt *neorpc.NotificationFilter
aerFlt *neorpc.ExecutionFilter
err error
)
if filtered {
bFlt = &neorpc.BlockFilter{Primary: &primary}
txFlt = &neorpc.TxFilter{Sender: &sender}
ntfFlt = &neorpc.NotificationFilter{Name: &ntfName}
aerFlt = &neorpc.ExecutionFilter{State: &st}
}
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)
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 <-exitCh:
break dispatcher
}
}
drainLoop:
for {
select {
case <-blockCh:
case <-txCh:
case <-ntfCh:
case <-aerCh:
default:
break drainLoop
}
}
close(blockCh)
close(txCh)
close(ntfCh)
close(aerCh)
}()
// 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))
assert.Eventually(t, func() bool {
lock.RLock()
defer lock.RUnlock()
return received == 1<<4-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))
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(testContractHash)
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(block20StateRootLE)
require.NoError(t, err)
h, err := util.Uint160DecodeStringLE(testContractHash)
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(block20StateRootLE)
require.NoError(t, err)
h, err := util.Uint160DecodeStringLE(testContractHash)
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, neorpc.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: 25,
}
require.InDeltaMapValues(t, expected, v.Protocol.Hardforks, 0)
}
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)
}
})
}