diff --git a/Makefile b/Makefile index a38058e16..ca759685f 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,8 @@ test-all: @build/env.sh tail -n +2 coverage.out >> coverage-all.out build/env.sh go test -coverprofile=coverage.out -covermode=set ./extkeys @build/env.sh tail -n +2 coverage.out >> coverage-all.out + build/env.sh go test -coverprofile=coverage.out -covermode=set ./cmd/status + @build/env.sh tail -n +2 coverage.out >> coverage-all.out @build/env.sh go tool cover -html=coverage-all.out -o coverage.html @build/env.sh go tool cover -func=coverage-all.out diff --git a/cmd/status/library.go b/cmd/status/library.go index 482e3f525..48a4c698f 100644 --- a/cmd/status/library.go +++ b/cmd/status/library.go @@ -150,6 +150,7 @@ func CompleteTransaction(id, password *C.char) *C.char { } out := geth.CompleteTransactionResult{ + Id: C.GoString(id), Hash: txHash.Hex(), Error: errString, } @@ -158,6 +159,27 @@ func CompleteTransaction(id, password *C.char) *C.char { return C.CString(string(outBytes)) } +//export CompleteTransactions +func CompleteTransactions(ids, password *C.char) *C.char { + out := geth.CompleteTransactionsResult{} + out.Results = make(map[string]geth.CompleteTransactionResult) + + results := geth.CompleteTransactions(C.GoString(ids), C.GoString(password)) + for txId, result := range results { + txResult := geth.CompleteTransactionResult{ + Id: txId, + Hash: result.Hash.Hex(), + } + if result.Error != nil { + txResult.Error = result.Error.Error() + } + out.Results[txId] = txResult + } + outBytes, _ := json.Marshal(&out) + + return C.CString(string(outBytes)) +} + //export DiscardTransaction func DiscardTransaction(id *C.char) *C.char { err := geth.DiscardTransaction(C.GoString(id)) @@ -177,6 +199,26 @@ func DiscardTransaction(id *C.char) *C.char { return C.CString(string(outBytes)) } +//export DiscardTransactions +func DiscardTransactions(ids *C.char) *C.char { + out := geth.DiscardTransactionsResult{} + out.Results = make(map[string]geth.DiscardTransactionResult) + + results := geth.DiscardTransactions(C.GoString(ids)) + for txId, result := range results { + txResult := geth.DiscardTransactionResult{ + Id: txId, + } + if result.Error != nil { + txResult.Error = result.Error.Error() + } + out.Results[txId] = txResult + } + outBytes, _ := json.Marshal(&out) + + return C.CString(string(outBytes)) +} + //export StartNode func StartNode(datadir *C.char) *C.char { // This starts a geth node with the given datadir diff --git a/cmd/status/library_test.go b/cmd/status/library_test.go new file mode 100644 index 000000000..27ceb04de --- /dev/null +++ b/cmd/status/library_test.go @@ -0,0 +1,15 @@ +package main + +import ( + "testing" +) + + +// the actual test functions are in non-_test.go files (so that they can use cgo i.e. import "C") +// the only intent of these wrappers is for gotest can find what tests are exposed. +func TestExportedAPI(t *testing.T) { + allTestsDone := make(chan struct{}, 1) + go testExportedAPI(t, allTestsDone) + + <- allTestsDone +} diff --git a/cmd/status/utils.go b/cmd/status/utils.go new file mode 100644 index 000000000..cec78715c --- /dev/null +++ b/cmd/status/utils.go @@ -0,0 +1,384 @@ +package main + +import "C" +import ( + "encoding/json" + "math/big" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/les/status" + "github.com/ethereum/go-ethereum/rpc" + "github.com/status-im/status-go/geth" +) + +const ( + testDataDir = "../../.ethereumtest" + testNodeSyncSeconds = 120 + testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" + testAddressPassword = "asdf" + newAccountPassword = "badpassword" + testAddress1 = "0xf82da7547534045b4e00442bc89e16186cf8c272" +) + +func testExportedAPI(t *testing.T, done chan struct{}) { + <-startTestNode(t) + + tests := []struct { + name string + fn func(t *testing.T) bool + }{ + { + "test complete multiple queued transactions", + testCompleteMultipleQueuedTransactions, + }, + { + "test discard multiple queued transactions", + testDiscardMultipleQueuedTransactions, + }, + } + + for _, test := range tests { + if ok := test.fn(t); !ok { + break + } + } + + done <- struct{}{} +} + +func testCompleteMultipleQueuedTransactions(t *testing.T) bool { + // obtain reference to status backend + lightEthereum, err := geth.GetNodeManager().LightEthereumService() + if err != nil { + t.Errorf("Test failed: LES service is not running: %v", err) + return false + } + backend := lightEthereum.StatusBackend + + // reset queue + backend.TransactionQueue().Reset() + + // make sure you panic if transaction complete doesn't return + testTxCount := 3 + txIds := make(chan string, testTxCount) + allTestTxCompleted := make(chan struct{}, 1) + + // replace transaction notification handler + geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { + var txId string + var envelope geth.GethEvent + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) + return + } + if envelope.Type == geth.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + txId = event["id"].(string) + t.Logf("transaction queued (will be completed in a single call, once aggregated): {id: %s}\n", txId) + + txIds <- txId + } + }) + + // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called + sendTx := func() { + txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), + Value: rpc.NewHexNumber(big.NewInt(1000000000000)), + }) + if err != nil { + t.Errorf("unexpected error thrown: %v", err) + return + } + + if reflect.DeepEqual(txHashCheck, common.Hash{}) { + t.Error("transaction returned empty hash") + return + } + } + + // wait for transactions, and complete them in a single call + completeTxs := func(txIdStrings string) { + var parsedIds []string + json.Unmarshal([]byte(txIdStrings), &parsedIds) + + parsedIds = append(parsedIds, "invalid-tx-id") + updatedTxIdStrings, _ := json.Marshal(parsedIds) + + // complete + resultsString := CompleteTransactions(C.CString(string(updatedTxIdStrings)), C.CString(testAddressPassword)) + resultsStruct := geth.CompleteTransactionsResult{} + json.Unmarshal([]byte(C.GoString(resultsString)), &resultsStruct) + results := resultsStruct.Results + + if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error != "transaction hash not found" { + t.Errorf("cannot complete txs: %v", results) + return + } + for txId, txResult := range results { + if txId != txResult.Id { + t.Errorf("tx id not set in result: expected id is %s", txId) + return + } + if txResult.Error != "" && txId != "invalid-tx-id" { + t.Errorf("invalid error for %s", txId) + return + } + if txResult.Hash == "0x0000000000000000000000000000000000000000000000000000000000000000" && txId != "invalid-tx-id" { + t.Errorf("invalid hash (expected non empty hash): %s", txId) + return + } + + if txResult.Hash != "0x0000000000000000000000000000000000000000000000000000000000000000" { + t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txResult.Hash) + } + } + + time.Sleep(1 * time.Second) // make sure that tx complete signal propagates + for _, txId := range parsedIds { + if backend.TransactionQueue().Has(status.QueuedTxId(txId)) { + t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txId) + return + } + } + } + go func() { + var txIdStrings []string + for i := 0; i < testTxCount; i++ { + txIdStrings = append(txIdStrings, <-txIds) + } + + txIdJSON, _ := json.Marshal(txIdStrings) + completeTxs(string(txIdJSON)) + allTestTxCompleted <- struct{}{} + }() + + // send multiple transactions + for i := 0; i < testTxCount; i++ { + go sendTx() + } + + select { + case <-allTestTxCompleted: + // pass + case <-time.After(20 * time.Second): + t.Error("test timed out") + return false + } + + if backend.TransactionQueue().Count() != 0 { + t.Error("tx queue must be empty at this point") + return false + } + + return true +} + +func testDiscardMultipleQueuedTransactions(t *testing.T) bool { + // obtain reference to status backend + lightEthereum, err := geth.GetNodeManager().LightEthereumService() + if err != nil { + t.Errorf("Test failed: LES service is not running: %v", err) + return false + } + backend := lightEthereum.StatusBackend + + // reset queue + backend.TransactionQueue().Reset() + + // make sure you panic if transaction complete doesn't return + testTxCount := 3 + txIds := make(chan string, testTxCount) + allTestTxDiscarded := make(chan struct{}, 1) + + // replace transaction notification handler + txFailedEventCallCount := 0 + geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { + var txId string + var envelope geth.GethEvent + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) + return + } + if envelope.Type == geth.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + txId = event["id"].(string) + t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txId) + + if !backend.TransactionQueue().Has(status.QueuedTxId(txId)) { + t.Errorf("txqueue should still have test tx: %s", txId) + return + } + + txIds <- txId + } + + if envelope.Type == geth.EventTransactionFailed { + event := envelope.Event.(map[string]interface{}) + t.Logf("transaction return event received: {id: %s}\n", event["id"].(string)) + + receivedErrMessage := event["error_message"].(string) + expectedErrMessage := status.ErrQueuedTxDiscarded.Error() + if receivedErrMessage != expectedErrMessage { + t.Errorf("unexpected error message received: got %v", receivedErrMessage) + return + } + + receivedErrCode := event["error_code"].(string) + if receivedErrCode != geth.SendTransactionDiscardedErrorCode { + t.Errorf("unexpected error code received: got %v", receivedErrCode) + return + } + + txFailedEventCallCount++ + if txFailedEventCallCount == testTxCount { + allTestTxDiscarded <- struct{}{} + } + } + }) + + // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called + sendTx := func() { + txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), + Value: rpc.NewHexNumber(big.NewInt(1000000000000)), + }) + if err != status.ErrQueuedTxDiscarded { + t.Errorf("expected error not thrown: %v", err) + return + } + + if !reflect.DeepEqual(txHashCheck, common.Hash{}) { + t.Error("transaction returned hash, while it shouldn't") + return + } + } + + // wait for transactions, and discard immediately + discardTxs := func(txIdStrings string) { + var parsedIds []string + json.Unmarshal([]byte(txIdStrings), &parsedIds) + + parsedIds = append(parsedIds, "invalid-tx-id") + updatedTxIdStrings, _ := json.Marshal(parsedIds) + + // discard + discardResultsString := DiscardTransactions(C.CString(string(updatedTxIdStrings))) + discardResultsStruct := geth.DiscardTransactionsResult{} + json.Unmarshal([]byte(C.GoString(discardResultsString)), &discardResultsStruct) + discardResults := discardResultsStruct.Results + + if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error != "transaction hash not found" { + t.Errorf("cannot discard txs: %v", discardResults) + return + } + + // try completing discarded transaction + completeResultsString := CompleteTransactions(C.CString(string(updatedTxIdStrings)), C.CString(testAddressPassword)) + completeResultsStruct := geth.CompleteTransactionsResult{} + json.Unmarshal([]byte(C.GoString(completeResultsString)), &completeResultsStruct) + completeResults := completeResultsStruct.Results + + if len(completeResults) != (testTxCount + 1) { + t.Error("unexpected number of errors (call to CompleteTransaction should not succeed)") + } + for txId, txResult := range completeResults { + if txId != txResult.Id { + t.Errorf("tx id not set in result: expected id is %s", txId) + return + } + if txResult.Error != "transaction hash not found" { + t.Errorf("invalid error for %s", txResult.Hash) + return + } + if txResult.Hash != "0x0000000000000000000000000000000000000000000000000000000000000000" { + t.Errorf("invalid hash (expected zero): %s", txResult.Hash) + return + } + } + + time.Sleep(1 * time.Second) // make sure that tx complete signal propagates + for _, txId := range parsedIds { + if backend.TransactionQueue().Has(status.QueuedTxId(txId)) { + t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txId) + return + } + } + } + go func() { + var txIdStrings []string + for i := 0; i < testTxCount; i++ { + txIdStrings = append(txIdStrings, <-txIds) + } + + txIdJSON, _ := json.Marshal(txIdStrings) + discardTxs(string(txIdJSON)) + }() + + // send multiple transactions + for i := 0; i < testTxCount; i++ { + go sendTx() + } + + select { + case <-allTestTxDiscarded: + // pass + case <-time.After(20 * time.Second): + t.Error("test timed out") + return false + } + + if backend.TransactionQueue().Count() != 0 { + t.Error("tx queue must be empty at this point") + return false + } + + return true +} + +func startTestNode(t *testing.T) <-chan struct{} { + syncRequired := false + if _, err := os.Stat(filepath.Join(testDataDir, "testnet")); os.IsNotExist(err) { + syncRequired = true + } + + waitForNodeStart := make(chan struct{}, 1) + geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { + t.Log(jsonEvent) + var envelope geth.GethEvent + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) + return + } + if envelope.Type == geth.EventTransactionQueued { + } + if envelope.Type == geth.EventNodeStarted { + if syncRequired { + t.Logf("Sync is required, it will take %d seconds", testNodeSyncSeconds) + time.Sleep(testNodeSyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done + } else { + time.Sleep(5 * time.Second) + } + + // now we can proceed with tests + waitForNodeStart <- struct{}{} + } + }) + + response := StartNode(C.CString(testDataDir)) + err := geth.JSONError{} + + json.Unmarshal([]byte(C.GoString(response)), &err) + if err.Error != "" { + t.Error("cannot start node") + } + + return waitForNodeStart +} diff --git a/geth/node_test.go b/geth/node_test.go index 459c20a36..62a59cae9 100644 --- a/geth/node_test.go +++ b/geth/node_test.go @@ -13,6 +13,7 @@ const ( testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" testAddressPassword = "asdf" newAccountPassword = "badpassword" + testAddress1 = "0xf82da7547534045b4e00442bc89e16186cf8c272" whisperMessage1 = "test message 1 (K1 -> K1)" whisperMessage2 = "test message 2 (K1 -> '')" diff --git a/geth/txqueue.go b/geth/txqueue.go index 888a6cba8..72a93caae 100644 --- a/geth/txqueue.go +++ b/geth/txqueue.go @@ -102,6 +102,28 @@ func CompleteTransaction(id, password string) (common.Hash, error) { return backend.CompleteQueuedTransaction(status.QueuedTxId(id), password) } +func CompleteTransactions(ids, password string) map[string]RawCompleteTransactionResult { + results := make(map[string]RawCompleteTransactionResult) + + parsedIds, err := parseJSONArray(ids) + if err != nil { + results["none"] = RawCompleteTransactionResult{ + Error: err, + } + return results + } + + for _, txId := range parsedIds { + txHash, txErr := CompleteTransaction(txId, password) + results[txId] = RawCompleteTransactionResult{ + Hash: txHash, + Error: txErr, + } + } + + return results +} + func DiscardTransaction(id string) error { lightEthereum, err := GetNodeManager().LightEthereumService() if err != nil { @@ -113,6 +135,30 @@ func DiscardTransaction(id string) error { return backend.DiscardQueuedTransaction(status.QueuedTxId(id)) } +func DiscardTransactions(ids string) map[string]RawDiscardTransactionResult { + var parsedIds []string + results := make(map[string]RawDiscardTransactionResult) + + parsedIds, err := parseJSONArray(ids) + if err != nil { + results["none"] = RawDiscardTransactionResult{ + Error: err, + } + return results + } + + for _, txId := range parsedIds { + err := DiscardTransaction(txId) + if err != nil { + results[txId] = RawDiscardTransactionResult{ + Error: err, + } + } + } + + return results +} + func messageIdFromContext(ctx context.Context) string { if ctx == nil { return "" @@ -232,3 +278,13 @@ func sendTxArgsFromRPCCall(req RPCCall) status.SendTxArgs { Data: data, } } + +func parseJSONArray(items string) ([]string, error) { + var parsedItems []string + err := json.Unmarshal([]byte(items), &parsedItems) + if err != nil { + return nil, err + } + + return parsedItems, nil +} diff --git a/geth/txqueue_test.go b/geth/txqueue_test.go index af670bf82..b49c21eab 100644 --- a/geth/txqueue_test.go +++ b/geth/txqueue_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/les/status" "github.com/ethereum/go-ethereum/rpc" @@ -21,19 +20,6 @@ func TestQueuedTransactions(t *testing.T) { return } - accountManager, err := geth.GetNodeManager().AccountManager() - if err != nil { - t.Errorf(err.Error()) - return - } - - // create an account - address, _, _, err := geth.CreateAccount(newAccountPassword) - if err != nil { - t.Errorf("could not create account: %v", err) - return - } - // obtain reference to status backend lightEthereum, err := geth.GetNodeManager().LightEthereumService() if err != nil { @@ -69,23 +55,10 @@ func TestQueuedTransactions(t *testing.T) { } }) - // send from the same test account (which is guaranteed to have ether) - from, err := utils.MakeAddress(accountManager, testAddress) - if err != nil { - t.Errorf("could not retrieve account from address: %v", err) - return - } - - to, err := utils.MakeAddress(accountManager, address) - if err != nil { - t.Errorf("could not retrieve account from address: %v", err) - return - } - // this call blocks, up until Complete Transaction is called txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ - From: from.Address, - To: &to.Address, + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), Value: rpc.NewHexNumber(big.NewInt(1000000000000)), }) if err != nil { @@ -117,19 +90,6 @@ func TestDoubleCompleteQueuedTransactions(t *testing.T) { return } - accountManager, err := geth.GetNodeManager().AccountManager() - if err != nil { - t.Errorf(err.Error()) - return - } - - // create an account - address, _, _, err := geth.CreateAccount(newAccountPassword) - if err != nil { - t.Errorf("could not create account: %v", err) - return - } - // obtain reference to status backend lightEthereum, err := geth.GetNodeManager().LightEthereumService() if err != nil { @@ -209,23 +169,10 @@ func TestDoubleCompleteQueuedTransactions(t *testing.T) { } }) - // send from the same test account (which is guaranteed to have ether) - from, err := utils.MakeAddress(accountManager, testAddress) - if err != nil { - t.Errorf("could not retrieve account from address: %v", err) - return - } - - to, err := utils.MakeAddress(accountManager, address) - if err != nil { - t.Errorf("could not retrieve account from address: %v", err) - return - } - // this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password) txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ - From: from.Address, - To: &to.Address, + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), Value: rpc.NewHexNumber(big.NewInt(1000000000000)), }) if err != nil { @@ -264,19 +211,6 @@ func TestDiscardQueuedTransactions(t *testing.T) { return } - accountManager, err := geth.GetNodeManager().AccountManager() - if err != nil { - t.Errorf(err.Error()) - return - } - - // create an account - address, _, _, err := geth.CreateAccount(newAccountPassword) - if err != nil { - t.Errorf("could not create account: %v", err) - return - } - // obtain reference to status backend lightEthereum, err := geth.GetNodeManager().LightEthereumService() if err != nil { @@ -355,27 +289,14 @@ func TestDiscardQueuedTransactions(t *testing.T) { } }) - // send from the same test account (which is guaranteed to have ether) - from, err := utils.MakeAddress(accountManager, testAddress) - if err != nil { - t.Errorf("could not retrieve account from address: %v", err) - return - } - - to, err := utils.MakeAddress(accountManager, address) - if err != nil { - t.Errorf("could not retrieve account from address: %v", err) - return - } - // this call blocks, and should return when DiscardQueuedTransaction() is called txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ - From: from.Address, - To: &to.Address, + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), Value: rpc.NewHexNumber(big.NewInt(1000000000000)), }) if err != status.ErrQueuedTxDiscarded { - t.Errorf("expeced error not thrown: %v", err) + t.Errorf("expected error not thrown: %v", err) return } @@ -398,6 +319,286 @@ func TestDiscardQueuedTransactions(t *testing.T) { time.Sleep(5 * time.Second) } +func TestCompleteMultipleQueuedTransactions(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + // obtain reference to status backend + lightEthereum, err := geth.GetNodeManager().LightEthereumService() + if err != nil { + t.Errorf("Test failed: LES service is not running: %v", err) + return + } + backend := lightEthereum.StatusBackend + + // reset queue + backend.TransactionQueue().Reset() + + // make sure you panic if transaction complete doesn't return + testTxCount := 3 + txIds := make(chan string, testTxCount) + allTestTxCompleted := make(chan struct{}, 1) + + // replace transaction notification handler + geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { + var txId string + var envelope geth.GethEvent + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) + return + } + if envelope.Type == geth.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + txId = event["id"].(string) + t.Logf("transaction queued (will be completed in a single call, once aggregated): {id: %s}\n", txId) + + txIds <- txId + } + }) + + // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called + sendTx := func() { + txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), + Value: rpc.NewHexNumber(big.NewInt(1000000000000)), + }) + if err != nil { + t.Errorf("unexpected error thrown: %v", err) + return + } + + if reflect.DeepEqual(txHashCheck, common.Hash{}) { + t.Error("transaction returned empty hash") + return + } + } + + // wait for transactions, and complete them in a single call + completeTxs := func(txIdStrings string) { + var parsedIds []string + json.Unmarshal([]byte(txIdStrings), &parsedIds) + + parsedIds = append(parsedIds, "invalid-tx-id") + updatedTxIdStrings, _ := json.Marshal(parsedIds) + + // complete + results := geth.CompleteTransactions(string(updatedTxIdStrings), testAddressPassword) + if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error.Error() != "transaction hash not found" { + t.Errorf("cannot complete txs: %v", results) + return + } + for txId, txResult := range results { + if txResult.Error != nil && txId != "invalid-tx-id" { + t.Errorf("invalid error for %s", txId) + return + } + if txResult.Hash.Hex() == "0x0000000000000000000000000000000000000000000000000000000000000000" && txId != "invalid-tx-id" { + t.Errorf("invalid hash (expected non empty hash): %s", txId) + return + } + + if txResult.Hash.Hex() != "0x0000000000000000000000000000000000000000000000000000000000000000" { + t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txResult.Hash.Hex()) + } + } + + time.Sleep(1 * time.Second) // make sure that tx complete signal propagates + for _, txId := range parsedIds { + if backend.TransactionQueue().Has(status.QueuedTxId(txId)) { + t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txId) + return + } + } + } + go func() { + var txIdStrings []string + for i := 0; i < testTxCount; i++ { + txIdStrings = append(txIdStrings, <-txIds) + } + + txIdJSON, _ := json.Marshal(txIdStrings) + completeTxs(string(txIdJSON)) + allTestTxCompleted <- struct{}{} + }() + + // send multiple transactions + for i := 0; i < testTxCount; i++ { + go sendTx() + } + + select { + case <-allTestTxCompleted: + // pass + case <-time.After(20 * time.Second): + t.Error("test timed out") + return + } + + if backend.TransactionQueue().Count() != 0 { + t.Error("tx queue must be empty at this point") + return + } +} + +func TestDiscardMultipleQueuedTransactions(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + // obtain reference to status backend + lightEthereum, err := geth.GetNodeManager().LightEthereumService() + if err != nil { + t.Errorf("Test failed: LES service is not running: %v", err) + return + } + backend := lightEthereum.StatusBackend + + // reset queue + backend.TransactionQueue().Reset() + + // make sure you panic if transaction complete doesn't return + testTxCount := 3 + txIds := make(chan string, testTxCount) + allTestTxDiscarded := make(chan struct{}, 1) + + // replace transaction notification handler + txFailedEventCallCount := 0 + geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) { + var txId string + var envelope geth.GethEvent + if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil { + t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) + return + } + if envelope.Type == geth.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + txId = event["id"].(string) + t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txId) + + if !backend.TransactionQueue().Has(status.QueuedTxId(txId)) { + t.Errorf("txqueue should still have test tx: %s", txId) + return + } + + txIds <- txId + } + + if envelope.Type == geth.EventTransactionFailed { + event := envelope.Event.(map[string]interface{}) + t.Logf("transaction return event received: {id: %s}\n", event["id"].(string)) + + receivedErrMessage := event["error_message"].(string) + expectedErrMessage := status.ErrQueuedTxDiscarded.Error() + if receivedErrMessage != expectedErrMessage { + t.Errorf("unexpected error message received: got %v", receivedErrMessage) + return + } + + receivedErrCode := event["error_code"].(string) + if receivedErrCode != geth.SendTransactionDiscardedErrorCode { + t.Errorf("unexpected error code received: got %v", receivedErrCode) + return + } + + txFailedEventCallCount++ + if txFailedEventCallCount == testTxCount { + allTestTxDiscarded <- struct{}{} + } + } + }) + + // this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called + sendTx := func() { + txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ + From: geth.FromAddress(testAddress), + To: geth.ToAddress(testAddress1), + Value: rpc.NewHexNumber(big.NewInt(1000000000000)), + }) + if err != status.ErrQueuedTxDiscarded { + t.Errorf("expected error not thrown: %v", err) + return + } + + if !reflect.DeepEqual(txHashCheck, common.Hash{}) { + t.Error("transaction returned hash, while it shouldn't") + return + } + } + + // wait for transactions, and discard immediately + discardTxs := func(txIdStrings string) { + var parsedIds []string + json.Unmarshal([]byte(txIdStrings), &parsedIds) + + parsedIds = append(parsedIds, "invalid-tx-id") + updatedTxIdStrings, _ := json.Marshal(parsedIds) + + // discard + discardResults := geth.DiscardTransactions(string(updatedTxIdStrings)) + if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error.Error() != "transaction hash not found" { + t.Errorf("cannot discard txs: %v", discardResults) + return + } + + // try completing discarded transaction + completeResults := geth.CompleteTransactions(string(updatedTxIdStrings), testAddressPassword) + if len(completeResults) != (testTxCount + 1) { + t.Error("unexpected number of errors (call to CompleteTransaction should not succeed)") + } + for _, txResult := range completeResults { + if txResult.Error.Error() != "transaction hash not found" { + t.Errorf("invalid error for %s", txResult.Hash.Hex()) + return + } + if txResult.Hash.Hex() != "0x0000000000000000000000000000000000000000000000000000000000000000" { + t.Errorf("invalid hash (expected zero): %s", txResult.Hash.Hex()) + return + } + } + + time.Sleep(1 * time.Second) // make sure that tx complete signal propagates + for _, txId := range parsedIds { + if backend.TransactionQueue().Has(status.QueuedTxId(txId)) { + t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txId) + return + } + } + } + go func() { + var txIdStrings []string + for i := 0; i < testTxCount; i++ { + txIdStrings = append(txIdStrings, <-txIds) + } + + txIdJSON, _ := json.Marshal(txIdStrings) + discardTxs(string(txIdJSON)) + }() + + // send multiple transactions + for i := 0; i < testTxCount; i++ { + go sendTx() + } + + select { + case <-allTestTxDiscarded: + // pass + case <-time.After(20 * time.Second): + t.Error("test timed out") + return + } + + if backend.TransactionQueue().Count() != 0 { + t.Error("tx queue must be empty at this point") + return + } +} + func TestNonExistentQueuedTransactions(t *testing.T) { err := geth.PrepareTestNode() if err != nil { diff --git a/geth/types.go b/geth/types.go index fc039c8b2..9d782f0df 100644 --- a/geth/types.go +++ b/geth/types.go @@ -1,6 +1,7 @@ package geth import ( + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/les/status" ) @@ -49,15 +50,33 @@ type ReturnSendTransactionEvent struct { } type CompleteTransactionResult struct { + Id string `json:"id"` Hash string `json:"hash"` Error string `json:"error"` } +type RawCompleteTransactionResult struct { + Hash common.Hash + Error error +} + +type CompleteTransactionsResult struct { + Results map[string]CompleteTransactionResult `json:"results"` +} + +type RawDiscardTransactionResult struct { + Error error +} + type DiscardTransactionResult struct { Id string `json:"id"` Error string `json:"error"` } +type DiscardTransactionsResult struct { + Results map[string]DiscardTransactionResult `json:"results"` +} + type GethEvent struct { Type string `json:"type"` Event interface{} `json:"event"` diff --git a/geth/utils.go b/geth/utils.go index 935912a4d..6dfe9a4d7 100644 --- a/geth/utils.go +++ b/geth/utils.go @@ -15,6 +15,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" ) @@ -23,7 +25,7 @@ var muPrepareTestNode sync.Mutex const ( TestDataDir = "../.ethereumtest" - TestNodeSyncSeconds = 60 + TestNodeSyncSeconds = 120 ) type NodeNotificationHandler func(jsonEvent string) @@ -177,3 +179,31 @@ func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) { } }() } + +func FromAddress(accountAddress string) common.Address { + accountManager, err := GetNodeManager().AccountManager() + if err != nil { + return common.Address{} + } + + from, err := utils.MakeAddress(accountManager, accountAddress) + if err != nil { + return common.Address{} + } + + return from.Address +} + +func ToAddress(accountAddress string) *common.Address { + accountManager, err := GetNodeManager().AccountManager() + if err != nil { + return nil + } + + to, err := utils.MakeAddress(accountManager, accountAddress) + if err != nil { + return nil + } + + return &to.Address +}