From e306a90554625132231f3d9bf580b119254d3789 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 4 Feb 2022 19:21:12 +0300 Subject: [PATCH] rpc: add NEP11-D tests to server and client Also refactor basic chain creation. --- pkg/core/helper_test.go | 178 ++++++++++----- pkg/core/statesync_test.go | 7 +- pkg/rpc/server/client_test.go | 93 ++++++-- pkg/rpc/server/server_helper_test.go | 2 +- pkg/rpc/server/server_test.go | 296 ++++++++++++++++++++----- pkg/rpc/server/testdata/testblocks.acc | Bin 24765 -> 33175 bytes 6 files changed, 444 insertions(+), 132 deletions(-) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index f915d9264..23ecede29 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -279,16 +279,34 @@ func initBasicChain(t *testing.T, bc *Blockchain) { priv0 := testchain.PrivateKeyByID(0) priv0ScriptHash := priv0.GetScriptHash() + priv1 := testchain.PrivateKeyByID(1) + priv1ScriptHash := priv1.GetScriptHash() + acc0 := wallet.NewAccountFromPrivateKey(priv0) + acc1 := wallet.NewAccountFromPrivateKey(priv1) + + deployContractFromPriv0 := func(t *testing.T, path, contractName string, configPath *string, expectedID int32) (util.Uint256, util.Uint256, util.Uint160) { + txDeploy, _ := newDeployTx(t, bc, priv0ScriptHash, path, contractName, configPath) + txDeploy.Nonce = getNextNonce() + txDeploy.ValidUntilBlock = validUntilBlock + require.NoError(t, addNetworkFee(bc, txDeploy, acc0)) + require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy)) + b := bc.newBlock(txDeploy) + require.NoError(t, bc.AddBlock(b)) // block #11 + checkTxHalt(t, bc, txDeploy.Hash()) + sh, err := bc.GetContractScriptHash(expectedID) + require.NoError(t, err) + return b.Hash(), txDeploy.Hash(), sh + } require.Equal(t, big.NewInt(5000_0000), bc.GetUtilityTokenBalance(priv0ScriptHash)) // gas bounty - // Move some NEO to one simple account. + + // Block #1: move 1000 GAS and neoAmount NEO to priv0. txMoveNeo, err := testchain.NewTransferFromOwner(bc, neoHash, priv0ScriptHash, neoAmount, getNextNonce(), validUntilBlock) require.NoError(t, err) // Move some GAS to one simple account. txMoveGas, err := testchain.NewTransferFromOwner(bc, gasHash, priv0ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), getNextNonce(), validUntilBlock) require.NoError(t, err) - b := bc.newBlock(txMoveNeo, txMoveGas) require.NoError(t, bc.AddBlock(b)) checkTxHalt(t, bc, txMoveGas.Hash()) @@ -322,25 +340,15 @@ func initBasicChain(t *testing.T, bc *Blockchain) { b.Header.EncodeBinary(buf.BinWriter) t.Logf("header: %s", hex.EncodeToString(buf.Bytes())) - acc0 := wallet.NewAccountFromPrivateKey(priv0) - - // Push some contract into the chain. + // Block #2: deploy test_contract. cfgPath := prefix + "test_contract.yml" - txDeploy, cHash := newDeployTx(t, bc, priv0ScriptHash, prefix+"test_contract.go", "Rubl", &cfgPath) - txDeploy.Nonce = getNextNonce() - txDeploy.ValidUntilBlock = validUntilBlock - require.NoError(t, addNetworkFee(bc, txDeploy, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy)) - b = bc.newBlock(txDeploy) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, txDeploy.Hash()) - t.Logf("txDeploy: %s", txDeploy.Hash().StringLE()) - t.Logf("Block2 hash: %s", b.Hash().StringLE()) + block2H, txDeployH, cHash := deployContractFromPriv0(t, prefix+"test_contract.go", "Rubl", &cfgPath, 1) + t.Logf("txDeploy: %s", txDeployH.StringLE()) + t.Logf("Block2 hash: %s", block2H.StringLE()) - // Now invoke this contract. + // Block #3: invoke `putValue` method on the test_contract. script := io.NewBufBinWriter() emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "testkey", "testvalue") - txInv := transaction.New(script.Bytes(), 1*native.GASFactor) txInv.Nonce = getNextNonce() txInv.ValidUntilBlock = validUntilBlock @@ -352,8 +360,8 @@ func initBasicChain(t *testing.T, bc *Blockchain) { checkTxHalt(t, bc, txInv.Hash()) t.Logf("txInv: %s", txInv.Hash().StringLE()) - priv1 := testchain.PrivateKeyByID(1) - txNeo0to1 := newNEP17Transfer(neoHash, priv0ScriptHash, priv1.GetScriptHash(), 1000) + // Block #4: transfer 0.0000_1 NEO from priv0 to priv1. + txNeo0to1 := newNEP17Transfer(neoHash, priv0ScriptHash, priv1ScriptHash, 1000) txNeo0to1.Nonce = getNextNonce() txNeo0to1.ValidUntilBlock = validUntilBlock txNeo0to1.Signers = []transaction.Signer{ @@ -370,6 +378,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, bc.AddBlock(b)) checkTxHalt(t, bc, txNeo0to1.Hash()) + // Block #5: initialize rubles contract and transfer 1000 rubles from the contract to priv0. w := io.NewBufBinWriter() emit.AppCall(w.BinWriter, cHash, "init", callflag.All) initTx := transaction.New(w.Bytes(), 1*native.GASFactor) @@ -378,7 +387,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { initTx.Signers = []transaction.Signer{{Account: priv0ScriptHash}} require.NoError(t, addNetworkFee(bc, initTx, acc0)) require.NoError(t, acc0.SignTx(testchain.Network(), initTx)) - transferTx := newNEP17Transfer(cHash, cHash, priv0.GetScriptHash(), 1000) + transferTx := newNEP17Transfer(cHash, cHash, priv0ScriptHash, 1000) transferTx.Nonce = getNextNonce() transferTx.ValidUntilBlock = validUntilBlock transferTx.Signers = []transaction.Signer{ @@ -392,14 +401,14 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, addNetworkFee(bc, transferTx, acc0)) transferTx.SystemFee += 1000000 require.NoError(t, acc0.SignTx(testchain.Network(), transferTx)) - b = bc.newBlock(initTx, transferTx) require.NoError(t, bc.AddBlock(b)) checkTxHalt(t, bc, initTx.Hash()) checkTxHalt(t, bc, transferTx.Hash()) t.Logf("recieveRublesTx: %v", transferTx.Hash().StringLE()) - transferTx = newNEP17Transfer(cHash, priv0.GetScriptHash(), priv1.GetScriptHash(), 123) + // Block #6: transfer 123 rubles from priv0 to priv1 + transferTx = newNEP17Transfer(cHash, priv0.GetScriptHash(), priv1ScriptHash, 123) transferTx.Nonce = getNextNonce() transferTx.ValidUntilBlock = validUntilBlock transferTx.Signers = []transaction.Signer{ @@ -413,24 +422,16 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, addNetworkFee(bc, transferTx, acc0)) transferTx.SystemFee += 1000000 require.NoError(t, acc0.SignTx(testchain.Network(), transferTx)) - b = bc.newBlock(transferTx) require.NoError(t, bc.AddBlock(b)) checkTxHalt(t, bc, transferTx.Hash()) t.Logf("sendRublesTx: %v", transferTx.Hash().StringLE()) - // Push verification contract into the chain. + // Block #7: push verification contract into the chain. verifyPath := filepath.Join(prefix, "verify", "verification_contract.go") - txDeploy2, _ := newDeployTx(t, bc, priv0ScriptHash, verifyPath, "Verify", nil) - txDeploy2.Nonce = getNextNonce() - txDeploy2.ValidUntilBlock = validUntilBlock - require.NoError(t, addNetworkFee(bc, txDeploy2, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy2)) - b = bc.newBlock(txDeploy2) - require.NoError(t, bc.AddBlock(b)) - checkTxHalt(t, bc, txDeploy2.Hash()) + _, _, _ = deployContractFromPriv0(t, verifyPath, "Verify", nil, 2) - // Deposit some GAS to notary contract for priv0 + // Block #8: deposit some GAS to notary contract for priv0. transferTx = newNEP17Transfer(gasHash, priv0.GetScriptHash(), notaryHash, 10_0000_0000, priv0.GetScriptHash(), int64(bc.BlockHeight()+1000)) transferTx.Nonce = getNextNonce() transferTx.ValidUntilBlock = validUntilBlock @@ -443,68 +444,59 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, addNetworkFee(bc, transferTx, acc0)) transferTx.SystemFee += 10_0000 require.NoError(t, acc0.SignTx(testchain.Network(), transferTx)) - b = bc.newBlock(transferTx) require.NoError(t, bc.AddBlock(b)) checkTxHalt(t, bc, transferTx.Hash()) t.Logf("notaryDepositTxPriv0: %v", transferTx.Hash().StringLE()) - // Designate new Notary node + // Block #9: designate new Notary node. ntr, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, "./testdata/notary1.json")) require.NoError(t, err) require.NoError(t, ntr.Accounts[0].Decrypt("one", ntr.Scrypt)) bc.setNodesByRole(t, true, noderoles.P2PNotary, keys.PublicKeys{ntr.Accounts[0].PrivateKey().PublicKey()}) t.Logf("Designated Notary node: %s", hex.EncodeToString(ntr.Accounts[0].PrivateKey().PublicKey().Bytes())) - // Push verification contract with arguments into the chain. + // Block #10: push verification contract with arguments into the chain. verifyPath = filepath.Join(prefix, "verify_args", "verification_with_args_contract.go") - txDeploy3, _ := newDeployTx(t, bc, priv0ScriptHash, verifyPath, "VerifyWithArgs", nil) - txDeploy3.Nonce = getNextNonce() - txDeploy3.ValidUntilBlock = validUntilBlock - require.NoError(t, addNetworkFee(bc, txDeploy3, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy3)) - b = bc.newBlock(txDeploy3) - require.NoError(t, bc.AddBlock(b)) // block #10 - checkTxHalt(t, bc, txDeploy3.Hash()) + _, _, _ = deployContractFromPriv0(t, verifyPath, "VerifyWithArgs", nil, 3) // block #10 - // Push NameService contract into the chain. + // Block #11: push NameService contract into the chain. nsPath := examplesPrefix + "nft-nd-nns/" nsConfigPath := nsPath + "nns.yml" - txDeploy4, _ := newDeployTx(t, bc, priv0ScriptHash, nsPath, nsPath, &nsConfigPath) - txDeploy4.Nonce = getNextNonce() - txDeploy4.ValidUntilBlock = validUntilBlock - require.NoError(t, addNetworkFee(bc, txDeploy4, acc0)) - require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy4)) - b = bc.newBlock(txDeploy4) - require.NoError(t, bc.AddBlock(b)) // block #11 - checkTxHalt(t, bc, txDeploy4.Hash()) - nsHash, err := bc.GetContractScriptHash(4) - require.NoError(t, err) - t.Logf("contract (%s): \n\tHash: %s\n", nsPath, nsHash.StringLE()) + _, _, nsHash := deployContractFromPriv0(t, nsPath, nsPath, &nsConfigPath, 4) // block #11 - // register `neo.com` with A record type and priv0 owner via NS + // Block #12: transfer funds to committee for futher NS record registration. transferFundsToCommittee(t, bc) // block #12 + + // Block #13: add `.com` root to NNS. res, err := invokeContractMethodGeneric(bc, -1, nsHash, "addRoot", true, "com") // block #13 require.NoError(t, err) checkResult(t, res, stackitem.Null{}) + + // Block #14: register `neo.com` via NNS. res, err = invokeContractMethodGeneric(bc, -1, nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14 require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) + require.Equal(t, 1, len(res.Events)) // transfer + tokenID, err := res.Events[0].Item.Value().([]stackitem.Item)[3].TryBytes() + require.NoError(t, err) + t.Logf("NNS token #1 ID (hex): %s", hex.EncodeToString(tokenID)) + + // Block #15: set A record type with priv0 owner via NNS. res, err = invokeContractMethodGeneric(bc, -1, nsHash, "setRecord", acc0, "neo.com", int64(nns.A), "1.2.3.4") // block #15 require.NoError(t, err) checkResult(t, res, stackitem.Null{}) - // Invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call + // Block #16: invoke `test_contract.go`: put new value with the same key to check `getstate` RPC call script.Reset() emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "testkey", "newtestvalue") // Invoke `test_contract.go`: put values to check `findstates` RPC call emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa", "v1") emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa10", "v2") emit.AppCall(script.BinWriter, cHash, "putValue", callflag.All, "aa50", "v3") - txInv = transaction.New(script.Bytes(), 1*native.GASFactor) txInv.Nonce = getNextNonce() txInv.ValidUntilBlock = validUntilBlock @@ -512,9 +504,75 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, addNetworkFee(bc, txInv, acc0)) require.NoError(t, acc0.SignTx(testchain.Network(), txInv)) b = bc.newBlock(txInv) - require.NoError(t, bc.AddBlock(b)) + require.NoError(t, bc.AddBlock(b)) // block #16 checkTxHalt(t, bc, txInv.Hash()) + // Block #17: deploy NeoFS Object contract (NEP11-Divisible). + nfsPath := examplesPrefix + "nft-d/" + nfsConfigPath := nfsPath + "nft.yml" + _, _, nfsHash := deployContractFromPriv0(t, nfsPath, nfsPath, &nfsConfigPath, 5) // block #17 + + // Block #18: mint 1.00 NFSO token by transferring 10 GAS to NFSO contract. + containerID := util.Uint256{1, 2, 3} + objectID := util.Uint256{4, 5, 6} + txGas0toNFS := newNEP17Transfer(gasHash, priv0ScriptHash, nfsHash, 10_0000_0000, containerID.BytesBE(), objectID.BytesBE()) + txGas0toNFS.SystemFee += 4000_0000 + txGas0toNFS.Nonce = getNextNonce() + txGas0toNFS.ValidUntilBlock = validUntilBlock + txGas0toNFS.Signers = []transaction.Signer{ + { + Account: priv0ScriptHash, + Scopes: transaction.CalledByEntry, + }, + } + require.NoError(t, addNetworkFee(bc, txGas0toNFS, acc0)) + require.NoError(t, acc0.SignTx(testchain.Network(), txGas0toNFS)) + b = bc.newBlock(txGas0toNFS) + require.NoError(t, bc.AddBlock(b)) // block #18 + checkTxHalt(t, bc, txGas0toNFS.Hash()) + aer, _ := bc.GetAppExecResults(txGas0toNFS.Hash(), trigger.Application) + require.Equal(t, 2, len(aer[0].Events)) // GAS transfer + NFSO transfer + tokenID, err = aer[0].Events[1].Item.Value().([]stackitem.Item)[3].TryBytes() + require.NoError(t, err) + t.Logf("NFSO token #1 ID (hex): %s", hex.EncodeToString(tokenID)) + + // Block #19: transfer 0.25 NFSO from priv0 to priv1. + script.Reset() + emit.AppCall(script.BinWriter, nfsHash, "transfer", callflag.All, priv0ScriptHash, priv1ScriptHash, 25, tokenID, nil) + emit.Opcodes(script.BinWriter, opcode.ASSERT) + require.NoError(t, script.Err) + txNFS0to1 := transaction.New(script.Bytes(), 1*native.GASFactor) + txNFS0to1.Nonce = getNextNonce() + txNFS0to1.ValidUntilBlock = validUntilBlock + txNFS0to1.Signers = []transaction.Signer{{Account: priv0ScriptHash, Scopes: transaction.CalledByEntry}} + require.NoError(t, addNetworkFee(bc, txNFS0to1, acc0)) + require.NoError(t, acc0.SignTx(testchain.Network(), txNFS0to1)) + b = bc.newBlock(txNFS0to1) + require.NoError(t, bc.AddBlock(b)) // block #19 + checkTxHalt(t, bc, txNFS0to1.Hash()) + + // Block #20: transfer 1000 GAS to priv1. + txMoveGas, err = testchain.NewTransferFromOwner(bc, gasHash, priv1ScriptHash, int64(fixedn.Fixed8FromInt64(1000)), + getNextNonce(), validUntilBlock) + require.NoError(t, err) + require.NoError(t, bc.AddBlock(bc.newBlock(txMoveGas))) + checkTxHalt(t, bc, txMoveGas.Hash()) // block #20 + + // Block #21: transfer 0.05 NFSO from priv1 back to priv0. + script.Reset() + emit.AppCall(script.BinWriter, nfsHash, "transfer", callflag.All, priv1ScriptHash, priv0.GetScriptHash(), 5, tokenID, nil) + emit.Opcodes(script.BinWriter, opcode.ASSERT) + require.NoError(t, script.Err) + txNFS1to0 := transaction.New(script.Bytes(), 1*native.GASFactor) + txNFS1to0.Nonce = getNextNonce() + txNFS1to0.ValidUntilBlock = validUntilBlock + txNFS1to0.Signers = []transaction.Signer{{Account: priv1ScriptHash, Scopes: transaction.CalledByEntry}} + require.NoError(t, addNetworkFee(bc, txNFS1to0, acc0)) + require.NoError(t, acc1.SignTx(testchain.Network(), txNFS1to0)) + b = bc.newBlock(txNFS1to0) + require.NoError(t, bc.AddBlock(b)) // block #21 + checkTxHalt(t, bc, txNFS1to0.Hash()) + // Compile contract to test `invokescript` RPC call invokePath := filepath.Join(prefix, "invoke", "invokescript_contract.go") invokeCfg := filepath.Join(prefix, "invoke", "invoke.yml") diff --git a/pkg/core/statesync_test.go b/pkg/core/statesync_test.go index 9d62088a0..9da8e5f2b 100644 --- a/pkg/core/statesync_test.go +++ b/pkg/core/statesync_test.go @@ -280,7 +280,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { var ( stateSyncInterval = 4 maxTraceable uint32 = 6 - stateSyncPoint = 16 + stateSyncPoint = 20 ) spoutCfg := func(c *config.Config) { c.ProtocolConfiguration.StateRootInHeader = true @@ -291,10 +291,9 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { bcSpout := newTestChainWithCustomCfg(t, spoutCfg) initBasicChain(t, bcSpout) - // make spout chain higher that latest state sync point + // make spout chain higher that latest state sync point (add several blocks up to stateSyncPoint+2) require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) - require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock())) - require.Equal(t, uint32(stateSyncPoint+2), bcSpout.BlockHeight()) + require.Equal(t, stateSyncPoint+2, int(bcSpout.BlockHeight())) boltCfg := func(c *config.Config) { spoutCfg(c) diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index 2baf7b00e..18c2b418d 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -3,6 +3,7 @@ package server import ( "context" "encoding/base64" + "encoding/hex" "testing" "github.com/nspcc-dev/neo-go/internal/testchain" @@ -107,7 +108,7 @@ func TestAddNetworkFeeCalculateNetworkFee(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 = 20 + tx.ValidUntilBlock = 25 tx.Signers = []transaction.Signer{{ Account: acc0.PrivateKey().GetScriptHash(), Scopes: transaction.CalledByEntry, @@ -165,7 +166,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { require.NoError(t, err) check := func(t *testing.T, extraFee int64) { tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) - tx.ValidUntilBlock = 20 + tx.ValidUntilBlock = 25 tx.Signers = []transaction.Signer{ { Account: acc0.PrivateKey().GetScriptHash(), @@ -801,7 +802,7 @@ func TestClient_GetNativeContracts(t *testing.T) { require.Equal(t, chain.GetNatives(), cs) } -func TestClient_NEP11(t *testing.T) { +func TestClient_NEP11_ND(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close() defer func() { _ = rpcSrv.Shutdown() }() @@ -810,7 +811,7 @@ func TestClient_NEP11(t *testing.T) { require.NoError(t, err) require.NoError(t, c.Init()) - h, err := util.Uint160DecodeStringLE(nameServiceContractHash) + h, err := util.Uint160DecodeStringLE(nnsContractHash) require.NoError(t, err) acc := testchain.PrivateKeyByID(0).GetScriptHash() @@ -867,6 +868,73 @@ func TestClient_NEP11(t *testing.T) { }) } +func TestClient_NEP11_D(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) + defer chain.Close() + defer func() { _ = rpcSrv.Shutdown() }() + + c, err := client.New(context.Background(), httpSrv.URL, client.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + + priv0 := testchain.PrivateKeyByID(0).GetScriptHash() + priv1 := testchain.PrivateKeyByID(1).GetScriptHash() + token1ID, err := hex.DecodeString(nfsoToken1ID) + require.NoError(t, err) + + t.Run("Decimals", func(t *testing.T) { + d, err := c.NEP11Decimals(nfsoHash) + require.NoError(t, err) + require.EqualValues(t, 2, d) // Divisible. + }) + t.Run("TotalSupply", func(t *testing.T) { + s, err := c.NEP11TotalSupply(nfsoHash) + require.NoError(t, err) + require.EqualValues(t, 1, s) // the only NFSO of acc0 + }) + t.Run("Symbol", func(t *testing.T) { + sym, err := c.NEP11Symbol(nfsoHash) + require.NoError(t, err) + require.Equal(t, "NFSO", sym) + }) + t.Run("TokenInfo", func(t *testing.T) { + tok, err := c.NEP11TokenInfo(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 := c.NEP11BalanceOf(nfsoHash, priv0) + require.NoError(t, err) + require.EqualValues(t, 80, b) + }) + t.Run("OwnerOf", func(t *testing.T) { + b, err := c.NEP11DOwnerOf(nfsoHash, token1ID) + require.NoError(t, err) + require.Equal(t, []util.Uint160{priv1, priv0}, b) + }) + t.Run("Properties", func(t *testing.T) { + p, err := c.NEP11Properties(nfsoHash, 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 := c.TransferNEP11D(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), + testchain.PrivateKeyByID(1).GetScriptHash(), + nfsoHash, 20, token1ID, nil, 0, nil) + require.NoError(t, err) + }) +} + func TestClient_NNS(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close() @@ -876,34 +944,31 @@ func TestClient_NNS(t *testing.T) { require.NoError(t, err) require.NoError(t, c.Init()) - nsHash, err := util.Uint160DecodeStringLE(nameServiceContractHash) - require.NoError(t, err) - t.Run("NNSIsAvailable, false", func(t *testing.T) { - b, err := c.NNSIsAvailable(nsHash, "neo.com") + b, err := c.NNSIsAvailable(nnsHash, "neo.com") require.NoError(t, err) require.Equal(t, false, b) }) t.Run("NNSIsAvailable, true", func(t *testing.T) { - b, err := c.NNSIsAvailable(nsHash, "neogo.com") + b, err := c.NNSIsAvailable(nnsHash, "neogo.com") require.NoError(t, err) require.Equal(t, true, b) }) t.Run("NNSResolve, good", func(t *testing.T) { - b, err := c.NNSResolve(nsHash, "neo.com", nns.A) + b, err := c.NNSResolve(nnsHash, "neo.com", nns.A) require.NoError(t, err) require.Equal(t, "1.2.3.4", b) }) t.Run("NNSResolve, bad", func(t *testing.T) { - _, err := c.NNSResolve(nsHash, "neogo.com", nns.A) + _, err := c.NNSResolve(nnsHash, "neogo.com", nns.A) require.Error(t, err) }) t.Run("NNSResolve, forbidden", func(t *testing.T) { - _, err := c.NNSResolve(nsHash, "neogo.com", nns.CNAME) + _, err := c.NNSResolve(nnsHash, "neogo.com", nns.CNAME) require.Error(t, err) }) t.Run("NNSGetAllRecords, good", func(t *testing.T) { - rss, err := c.NNSGetAllRecords(nsHash, "neo.com") + rss, err := c.NNSGetAllRecords(nnsHash, "neo.com") require.NoError(t, err) require.Equal(t, []nns.RecordState{ { @@ -914,7 +979,7 @@ func TestClient_NNS(t *testing.T) { }, rss) }) t.Run("NNSGetAllRecords, bad", func(t *testing.T) { - _, err := c.NNSGetAllRecords(nsHash, "neopython.com") + _, err := c.NNSGetAllRecords(nnsHash, "neopython.com") require.Error(t, err) }) } diff --git a/pkg/rpc/server/server_helper_test.go b/pkg/rpc/server/server_helper_test.go index 0baa86411..0c535ca71 100644 --- a/pkg/rpc/server/server_helper_test.go +++ b/pkg/rpc/server/server_helper_test.go @@ -75,7 +75,7 @@ func getUnitTestChain(t testing.TB, enableOracle bool, enableNotary bool) (*core } func getTestBlocks(t *testing.T) []*block.Block { - // File "./testdata/testblocks.acc" was generated by function core._ + // File "./testdata/testblocks.acc" was generated by function core.TestCreateBasicChain // ("neo-go/pkg/core/helper_test.go"). // To generate new "./testdata/testblocks.acc", follow the steps: // 1. Rename the function diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index cebad1bb7..6bc84ee8b 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -61,30 +61,27 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } -const testContractHash = "5c9e40a12055c6b9e3f72271c9779958c842135d" -const deploymentTxHash = "8de63ea12ca8a9c5233ebf8664a442c881ae1bb83708d82da7fa1da2305ecf14" -const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" +const genesisBlockHash = "a4ae00f6ac7496cac14e709fbf8b8ecb4c9831d8a6ee396056af9350fcf22671" +const testContractHash = "1ab08f5508edafa6f28e3db3227442a9e70aac52" +const deploymentTxHash = "017c9edb217477aeb3e0c35462361209fdb7bf104dc8e285e2385af8713926b4" -const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0" -const verifyContractAVM = "VwIAQS1RCDAhcAwU7p6iLCfjS9AUj8QQjgj3To9QSLLbMHFoE87bKGnbKJdA" -const verifyWithArgsContractHash = "947c780f45b2a3d32e946355ee5cb57faf4decb7" -const invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGhB+CfsjCGqJgQRQAwUDQ8DAgkAAgEDBwMEBQIBAA4GDAnbMHFpQfgn7IwhqiYEEkATQA==" +const ( + verifyContractHash = "7deef31e5c616e157cdf02a5446f36d0a4eead52" + verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" + verifyWithArgsContractHash = "6df009754ce475a6a5730c9e488f80e8e47bc1f1" + nnsContractHash = "1a7530a4c6cfdd40ffed40775aa5453febab24c0" + nnsToken1ID = "6e656f2e636f6d" + nfsoContractHash = "aaf8913c501e25c42877e79f04cb7c2c1ab47e57" + nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" + invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" +) -const nameServiceContractHash = "3a602b3e7cfd760850bfac44f4a9bb0ebad3e2dc" - -var NNSHash = util.Uint160{0xdc, 0xe2, 0xd3, 0xba, 0x0e, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x08, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a} - -var nep11Reg = &result.NEP11Balances{ - Address: "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn", - Balances: []result.NEP11AssetBalance{{ - Asset: NNSHash, - Tokens: []result.NEP11TokenBalance{{ - ID: "6e656f2e636f6d", - Amount: "1", - LastUpdated: 14, - }}, - }}, -} +var ( + nnsHash, _ = util.Uint160DecodeStringLE(nnsContractHash) + nfsoHash, _ = util.Uint160DecodeStringLE(nfsoContractHash) + nfsoToken1ContainerID = util.Uint256{1, 2, 3} + nfsoToken1ObjectID = util.Uint256{4, 5, 6} +) var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { @@ -245,12 +242,14 @@ var rpcTestCases = map[string][]rpcTestCase{ { name: "positive", params: `["` + testchain.PrivateKeyByID(0).GetScriptHash().StringLE() + `"]`, - result: func(e *executor) interface{} { return nep11Reg }, + result: func(e *executor) interface{} { return &result.NEP11Balances{} }, + check: checkNep11Balances, }, { name: "positive_address", params: `["` + address.Uint160ToString(testchain.PrivateKeyByID(0).GetScriptHash()) + `"]`, - result: func(e *executor) interface{} { return nep11Reg }, + result: func(e *executor) interface{} { return &result.NEP11Balances{} }, + check: checkNep11Balances, }, }, "getnep11properties": { @@ -266,21 +265,21 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { name: "no token", - params: `["` + NNSHash.StringLE() + `"]`, + params: `["` + nnsContractHash + `"]`, fail: true, }, { name: "bad token", - params: `["` + NNSHash.StringLE() + `", "abcdef"]`, + params: `["` + nnsContractHash + `", "abcdef"]`, fail: true, }, { name: "positive", - params: `["` + NNSHash.StringLE() + `", "6e656f2e636f6d"]`, + params: `["` + nnsContractHash + `", "6e656f2e636f6d"]`, result: func(e *executor) interface{} { return &map[string]interface{}{ "name": "neo.com", - "expiration": "bhORxoMB", + "expiration": "HrL+G4YB", } }, }, @@ -309,9 +308,8 @@ var rpcTestCases = map[string][]rpcTestCase{ { name: "positive", params: `["` + testchain.PrivateKeyByID(0).Address() + `", 0]`, - result: func(e *executor) interface{} { - return &result.NEP11Transfers{Sent: []result.NEP11Transfer{}, Received: []result.NEP11Transfer{{Timestamp: 0x17c6edfe76e, Asset: util.Uint160{0xdc, 0xe2, 0xd3, 0xba, 0xe, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x8, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a}, Address: "", ID: "6e656f2e636f6d", Amount: "1", Index: 0xe, NotifyIndex: 0x0, TxHash: util.Uint256{0x5b, 0x5a, 0x5b, 0xae, 0xf2, 0xc5, 0x63, 0x8a, 0x2e, 0xcc, 0x77, 0x27, 0xd9, 0x6b, 0xb9, 0xda, 0x3a, 0x7f, 0x30, 0xaa, 0xcf, 0xda, 0x7f, 0x8a, 0x10, 0xd3, 0x23, 0xbf, 0xd, 0x1f, 0x28, 0x69}}}, Address: "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn"} - }, + result: func(e *executor) interface{} { return &result.NEP11Transfers{} }, + check: checkNep11Transfers, }, }, "getnep17balances": { @@ -812,7 +810,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) expected := result.UnclaimedGas{ Address: testchain.MultisigScriptHash(), - Unclaimed: *big.NewInt(8000), + Unclaimed: *big.NewInt(10500), } assert.Equal(t, expected, *actual) }, @@ -882,16 +880,16 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { name: "positive, with notifications", - params: `["` + NNSHash.StringLE() + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, + params: `["` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, result: func(e *executor) interface{} { - script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b, 0x13, 0xc0, 0x1f, 0x0c, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x0c, 0x14, 0xdc, 0xe2, 0xd3, 0xba, 0x0e, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x08, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a, 0x41, 0x62, 0x7d, 0x5b, 0x52} + script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b, 0x13, 0xc0, 0x1f, 0x0c, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x0c, 0x14, 0xc0, 0x24, 0xab, 0xeb, 0x3f, 0x45, 0xa5, 0x5a, 0x77, 0x40, 0xed, 0xff, 0x40, 0xdd, 0xcf, 0xc6, 0xa4, 0x30, 0x75, 0x1a, 0x41, 0x62, 0x7d, 0x5b, 0x52} return &result.Invoke{ State: "HALT", - GasConsumed: 33767940, + GasConsumed: 32167260, Script: script, Stack: []stackitem.Item{stackitem.Make(true)}, Notifications: []state.NotificationEvent{{ - ScriptHash: NNSHash, + ScriptHash: nnsHash, Name: "Transfer", Item: stackitem.NewArray([]stackitem.Item{ stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}), @@ -917,19 +915,19 @@ var rpcTestCases = map[string][]rpcTestCase{ chg := []storage.Operation{{ State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, - Value: []byte{0xbc, 0xf8, 0x8b, 0xa, 0x56, 0x79, 0x12}, + Value: []byte{0xe8, 0x80, 0x64, 0xcb, 0x53, 0x79, 0x12}, }, { State: "Added", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, - Value: []byte{0x41, 0x3, 0x21, 0x1, 0x1, 0x21, 0x1, 0x11, 0x0}, + Value: []byte{0x41, 0x3, 0x21, 0x1, 0x1, 0x21, 0x1, 0x16, 0}, }, { State: "Changed", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x3, 0x21, 0x4, 0x2f, 0xd9, 0xf5, 0x5, 0x21, 0x1, 0x11, 0x0}, + Value: []byte{0x41, 0x3, 0x21, 0x4, 0x2f, 0xd9, 0xf5, 0x5, 0x21, 0x1, 0x16, 0}, }, { State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x1, 0x21, 0x5, 0x4, 0xfa, 0xb2, 0x9b, 0xd}, + Value: []byte{0x41, 0x01, 0x21, 0x05, 0x9e, 0x0b, 0x0b, 0x18, 0x0b}, }} // Can be returned in any order. assert.ElementsMatch(t, chg, res.Diagnostics.Changes) @@ -937,14 +935,14 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { name: "positive, verbose", - params: `["` + NNSHash.StringLE() + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, + params: `["` + nnsContractHash + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, result: func(e *executor) interface{} { - script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0xdc, 0xe2, 0xd3, 0xba, 0xe, 0xbb, 0xa9, 0xf4, 0x44, 0xac, 0xbf, 0x50, 0x8, 0x76, 0xfd, 0x7c, 0x3e, 0x2b, 0x60, 0x3a, 0x41, 0x62, 0x7d, 0x5b, 0x52} + script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0xc0, 0x24, 0xab, 0xeb, 0x3f, 0x45, 0xa5, 0x5a, 0x77, 0x40, 0xed, 0xff, 0x40, 0xdd, 0xcf, 0xc6, 0xa4, 0x30, 0x75, 0x1a, 0x41, 0x62, 0x7d, 0x5b, 0x52} stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 17958510, + GasConsumed: 15928320, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -954,7 +952,7 @@ var rpcTestCases = map[string][]rpcTestCase{ Current: hash.Hash160(script), Calls: []*vm.InvocationTree{ { - Current: NNSHash, + Current: nnsHash, Calls: []*vm.InvocationTree{ { Current: stdHash, @@ -1834,7 +1832,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoErrorf(t, err, "could not parse response: %s", txOut) assert.Equal(t, *block.Transactions[0], actual.Transaction) - assert.Equal(t, 17, actual.Confirmations) + assert.Equal(t, 22, actual.Confirmations) assert.Equal(t, TXHash, actual.Transaction.Hash()) }) @@ -1947,12 +1945,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoError(t, json.Unmarshal(res, actual)) checkNep17TransfersAux(t, e, actual, sent, rcvd) } - t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{10, 11, 12, 13}, []int{2, 3}) }) + t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{14, 15, 16, 17}, []int{3, 4}) }) t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) }) - t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{7, 8}, []int{1}) }) - t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{10}, []int{2}) }) - t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{9, 10}, []int{2}) }) - t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{11, 12}, []int{3}) }) + t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{11, 12}, []int{2}) }) + t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{14}, []int{3}) }) + t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{13, 14}, []int{3}) }) + t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{15, 16}, []int{4}) }) }) } @@ -2036,6 +2034,39 @@ func doRPCCallOverHTTP(rpcCall string, url string, t *testing.T) []byte { return bytes.TrimSpace(body) } +func checkNep11Balances(t *testing.T, e *executor, acc interface{}) { + res, ok := acc.(*result.NEP11Balances) + require.True(t, ok) + + expected := result.NEP11Balances{ + Balances: []result.NEP11AssetBalance{ + { + Asset: nnsHash, + Tokens: []result.NEP11TokenBalance{ + { + ID: nnsToken1ID, + Amount: "1", + LastUpdated: 14, + }, + }, + }, + { + Asset: nfsoHash, + Tokens: []result.NEP11TokenBalance{ + { + ID: nfsoToken1ID, + Amount: "80", + LastUpdated: 21, + }, + }, + }, + }, + Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), + } + require.Equal(t, testchain.PrivateKeyByID(0).Address(), res.Address) + require.ElementsMatch(t, expected.Balances, res.Balances) +} + func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { res, ok := acc.(*result.NEP17Balances) require.True(t, ok) @@ -2055,8 +2086,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "57796785740", - LastUpdated: 16, + Amount: "46748035310", + LastUpdated: 19, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), } @@ -2064,8 +2095,110 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { require.ElementsMatch(t, expected.Balances, res.Balances) } +func checkNep11Transfers(t *testing.T, e *executor, acc interface{}) { + checkNep11TransfersAux(t, e, acc, []int{0}, []int{0, 1, 2}) +} + +func checkNep11TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { + res, ok := acc.(*result.NEP11Transfers) + require.True(t, ok) + + blockReceiveNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(21)) // transfer 0.05 NFSO from priv1 back to priv0. + require.NoError(t, err) + require.Equal(t, 1, len(blockReceiveNFSO.Transactions)) + txReceiveNFSO := blockReceiveNFSO.Transactions[0] + + blockSendNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(19)) // transfer 0.25 NFSO from priv0 to priv1. + require.NoError(t, err) + require.Equal(t, 1, len(blockSendNFSO.Transactions)) + txSendNFSO := blockSendNFSO.Transactions[0] + + blockMintNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(18)) // mint 1.00 NFSO token by transferring 10 GAS to NFSO contract. + require.NoError(t, err) + require.Equal(t, 1, len(blockMintNFSO.Transactions)) + txMintNFSO := blockMintNFSO.Transactions[0] + + blockRegisterNSRecordA, err := e.chain.GetBlock(e.chain.GetHeaderHash(14)) // register `neo.com` with A record type and priv0 owner via NS + require.NoError(t, err) + require.Equal(t, 1, len(blockRegisterNSRecordA.Transactions)) + txRegisterNSRecordA := blockRegisterNSRecordA.Transactions[0] + + // These are laid out here explicitly for 2 purposes: + // * to be able to reference any particular event for paging + // * to check chain events consistency + // Technically these could be retrieved from application log, but that would almost + // duplicate the Server method. + expected := result.NEP11Transfers{ + Sent: []result.NEP11Transfer{ + { + Timestamp: blockSendNFSO.Timestamp, + Asset: nfsoHash, + Address: testchain.PrivateKeyByID(1).Address(), // to priv1 + ID: nfsoToken1ID, // NFSO ID + Amount: big.NewInt(25).String(), + Index: 19, + TxHash: txSendNFSO.Hash(), + }, + }, + Received: []result.NEP11Transfer{ + { + Timestamp: blockReceiveNFSO.Timestamp, + Asset: nfsoHash, + ID: nfsoToken1ID, + Address: testchain.PrivateKeyByID(1).Address(), // from priv1 + Amount: "5", + Index: 21, + TxHash: txReceiveNFSO.Hash(), + }, + { + Timestamp: blockMintNFSO.Timestamp, + Asset: nfsoHash, + ID: nfsoToken1ID, + Address: "", // minting + Amount: "100", + Index: 18, + TxHash: txMintNFSO.Hash(), + }, + { + Timestamp: blockRegisterNSRecordA.Timestamp, + Asset: nnsHash, + ID: nnsToken1ID, + Address: "", // minting + Amount: "1", + Index: 14, + TxHash: txRegisterNSRecordA.Hash(), + }, + }, + Address: testchain.PrivateKeyByID(0).Address(), + } + + require.Equal(t, expected.Address, res.Address) + + arr := make([]result.NEP11Transfer, 0, len(expected.Sent)) + for i := range expected.Sent { + for _, j := range sent { + if i == j { + arr = append(arr, expected.Sent[i]) + break + } + } + } + require.Equal(t, arr, res.Sent) + + arr = arr[:0] + for i := range expected.Received { + for _, j := range rcvd { + if i == j { + arr = append(arr, expected.Received[i]) + break + } + } + } + require.Equal(t, arr, res.Received) +} + func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) { - checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, []int{0, 1, 2, 3, 4, 5, 6, 7}) + checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) } func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { @@ -2074,6 +2207,21 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc rublesHash, err := util.Uint160DecodeStringLE(testContractHash) require.NoError(t, err) + blockTransferNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(19)) // transfer 0.25 NFSO from priv0 to priv1. + require.NoError(t, err) + require.Equal(t, 1, len(blockTransferNFSO.Transactions)) + txTransferNFSO := blockTransferNFSO.Transactions[0] + + blockMintNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(18)) // mint 1.00 NFSO token for priv0 by transferring 10 GAS to NFSO contract. + require.NoError(t, err) + require.Equal(t, 1, len(blockMintNFSO.Transactions)) + txMintNFSO := blockMintNFSO.Transactions[0] + + blockDeploy5, err := e.chain.GetBlock(e.chain.GetHeaderHash(17)) // deploy NeoFS Object contract (NEP11-Divisible) + require.NoError(t, err) + require.Equal(t, 1, len(blockDeploy5.Transactions)) + txDeploy5 := blockDeploy5.Transactions[0] + blockPutNewTestValue, err := e.chain.GetBlock(e.chain.GetHeaderHash(16)) // invoke `put` method of `test_contract.go` with `testkey`, `newtestvalue` args require.NoError(t, err) require.Equal(t, 1, len(blockPutNewTestValue.Transactions)) @@ -2155,6 +2303,39 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc // duplicate the Server method. expected := result.NEP17Transfers{ Sent: []result.NEP17Transfer{ + { + Timestamp: blockTransferNFSO.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txTransferNFSO.SystemFee + txTransferNFSO.NetworkFee).String(), + Index: 19, + TxHash: blockTransferNFSO.Hash(), + }, + { + Timestamp: blockMintNFSO.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: address.Uint160ToString(nfsoHash), + Amount: "1000000000", + Index: 18, + NotifyIndex: 0, + TxHash: txMintNFSO.Hash(), + }, + { + Timestamp: blockMintNFSO.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txMintNFSO.SystemFee + txMintNFSO.NetworkFee).String(), + Index: 18, + TxHash: blockMintNFSO.Hash(), + }, + { + Timestamp: blockDeploy5.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txDeploy5.SystemFee + txDeploy5.NetworkFee).String(), + Index: 17, + TxHash: blockDeploy5.Hash(), + }, { Timestamp: blockPutNewTestValue.Timestamp, Asset: e.chain.UtilityTokenHash(), @@ -2288,6 +2469,15 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc }, }, Received: []result.NEP17Transfer{ + { + Timestamp: blockMintNFSO.Timestamp, // GAS bounty + Asset: e.chain.UtilityTokenHash(), + Address: "", + Amount: "50000000", + Index: 18, + NotifyIndex: 0, + TxHash: blockMintNFSO.Hash(), + }, { Timestamp: blockGASBounty2.Timestamp, Asset: e.chain.UtilityTokenHash(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index 7ce922f592c084939459ac961f13d141e4efb313..f004f9cf5d6862b9662098837dc7f761b9a6a9c1 100644 GIT binary patch literal 33175 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 delta 11247 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 z