Merge pull request #60 from farazdagi/feature/tx-multi-actions

DiscardTransactions() + CompleteTransactions()
This commit is contained in:
Roman Volosovskyi 2016-11-18 15:27:29 +02:00 committed by GitHub
commit bc8ec4500e
9 changed files with 837 additions and 87 deletions

View File

@ -59,6 +59,8 @@ test-all:
@build/env.sh tail -n +2 coverage.out >> coverage-all.out @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 go test -coverprofile=coverage.out -covermode=set ./extkeys
@build/env.sh tail -n +2 coverage.out >> coverage-all.out @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 -html=coverage-all.out -o coverage.html
@build/env.sh go tool cover -func=coverage-all.out @build/env.sh go tool cover -func=coverage-all.out

View File

@ -150,6 +150,7 @@ func CompleteTransaction(id, password *C.char) *C.char {
} }
out := geth.CompleteTransactionResult{ out := geth.CompleteTransactionResult{
Id: C.GoString(id),
Hash: txHash.Hex(), Hash: txHash.Hex(),
Error: errString, Error: errString,
} }
@ -158,6 +159,27 @@ func CompleteTransaction(id, password *C.char) *C.char {
return C.CString(string(outBytes)) 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 //export DiscardTransaction
func DiscardTransaction(id *C.char) *C.char { func DiscardTransaction(id *C.char) *C.char {
err := geth.DiscardTransaction(C.GoString(id)) err := geth.DiscardTransaction(C.GoString(id))
@ -177,6 +199,26 @@ func DiscardTransaction(id *C.char) *C.char {
return C.CString(string(outBytes)) 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 //export StartNode
func StartNode(datadir *C.char) *C.char { func StartNode(datadir *C.char) *C.char {
// This starts a geth node with the given datadir // This starts a geth node with the given datadir

View File

@ -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
}

384
cmd/status/utils.go Normal file
View File

@ -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
}

View File

@ -13,6 +13,7 @@ const (
testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4"
testAddressPassword = "asdf" testAddressPassword = "asdf"
newAccountPassword = "badpassword" newAccountPassword = "badpassword"
testAddress1 = "0xf82da7547534045b4e00442bc89e16186cf8c272"
whisperMessage1 = "test message 1 (K1 -> K1)" whisperMessage1 = "test message 1 (K1 -> K1)"
whisperMessage2 = "test message 2 (K1 -> '')" whisperMessage2 = "test message 2 (K1 -> '')"

View File

@ -102,6 +102,28 @@ func CompleteTransaction(id, password string) (common.Hash, error) {
return backend.CompleteQueuedTransaction(status.QueuedTxId(id), password) 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 { func DiscardTransaction(id string) error {
lightEthereum, err := GetNodeManager().LightEthereumService() lightEthereum, err := GetNodeManager().LightEthereumService()
if err != nil { if err != nil {
@ -113,6 +135,30 @@ func DiscardTransaction(id string) error {
return backend.DiscardQueuedTransaction(status.QueuedTxId(id)) 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 { func messageIdFromContext(ctx context.Context) string {
if ctx == nil { if ctx == nil {
return "" return ""
@ -232,3 +278,13 @@ func sendTxArgsFromRPCCall(req RPCCall) status.SendTxArgs {
Data: data, 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
}

View File

@ -7,7 +7,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les/status" "github.com/ethereum/go-ethereum/les/status"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
@ -21,19 +20,6 @@ func TestQueuedTransactions(t *testing.T) {
return 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 // obtain reference to status backend
lightEthereum, err := geth.GetNodeManager().LightEthereumService() lightEthereum, err := geth.GetNodeManager().LightEthereumService()
if err != nil { 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 // this call blocks, up until Complete Transaction is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: from.Address, From: geth.FromAddress(testAddress),
To: &to.Address, To: geth.ToAddress(testAddress1),
Value: rpc.NewHexNumber(big.NewInt(1000000000000)), Value: rpc.NewHexNumber(big.NewInt(1000000000000)),
}) })
if err != nil { if err != nil {
@ -117,19 +90,6 @@ func TestDoubleCompleteQueuedTransactions(t *testing.T) {
return 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 // obtain reference to status backend
lightEthereum, err := geth.GetNodeManager().LightEthereumService() lightEthereum, err := geth.GetNodeManager().LightEthereumService()
if err != nil { 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) // this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password)
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: from.Address, From: geth.FromAddress(testAddress),
To: &to.Address, To: geth.ToAddress(testAddress1),
Value: rpc.NewHexNumber(big.NewInt(1000000000000)), Value: rpc.NewHexNumber(big.NewInt(1000000000000)),
}) })
if err != nil { if err != nil {
@ -264,19 +211,6 @@ func TestDiscardQueuedTransactions(t *testing.T) {
return 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 // obtain reference to status backend
lightEthereum, err := geth.GetNodeManager().LightEthereumService() lightEthereum, err := geth.GetNodeManager().LightEthereumService()
if err != nil { 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 // this call blocks, and should return when DiscardQueuedTransaction() is called
txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{ txHashCheck, err := backend.SendTransaction(nil, status.SendTxArgs{
From: from.Address, From: geth.FromAddress(testAddress),
To: &to.Address, To: geth.ToAddress(testAddress1),
Value: rpc.NewHexNumber(big.NewInt(1000000000000)), Value: rpc.NewHexNumber(big.NewInt(1000000000000)),
}) })
if err != status.ErrQueuedTxDiscarded { if err != status.ErrQueuedTxDiscarded {
t.Errorf("expeced error not thrown: %v", err) t.Errorf("expected error not thrown: %v", err)
return return
} }
@ -398,6 +319,286 @@ func TestDiscardQueuedTransactions(t *testing.T) {
time.Sleep(5 * time.Second) 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) { func TestNonExistentQueuedTransactions(t *testing.T) {
err := geth.PrepareTestNode() err := geth.PrepareTestNode()
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package geth package geth
import ( import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les/status" "github.com/ethereum/go-ethereum/les/status"
) )
@ -49,15 +50,33 @@ type ReturnSendTransactionEvent struct {
} }
type CompleteTransactionResult struct { type CompleteTransactionResult struct {
Id string `json:"id"`
Hash string `json:"hash"` Hash string `json:"hash"`
Error string `json:"error"` 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 { type DiscardTransactionResult struct {
Id string `json:"id"` Id string `json:"id"`
Error string `json:"error"` Error string `json:"error"`
} }
type DiscardTransactionsResult struct {
Results map[string]DiscardTransactionResult `json:"results"`
}
type GethEvent struct { type GethEvent struct {
Type string `json:"type"` Type string `json:"type"`
Event interface{} `json:"event"` Event interface{} `json:"event"`

View File

@ -15,6 +15,8 @@ import (
"sync" "sync"
"time" "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"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
) )
@ -23,7 +25,7 @@ var muPrepareTestNode sync.Mutex
const ( const (
TestDataDir = "../.ethereumtest" TestDataDir = "../.ethereumtest"
TestNodeSyncSeconds = 60 TestNodeSyncSeconds = 120
) )
type NodeNotificationHandler func(jsonEvent string) 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
}