diff --git a/geth/node.go b/geth/node.go index 19bdd4faa..499605fd8 100644 --- a/geth/node.go +++ b/geth/node.go @@ -50,6 +50,7 @@ var ( ErrInvalidWhisperService = errors.New("whisper service is unavailable") ErrInvalidLightEthereumService = errors.New("can not retrieve LES service") ErrInvalidClient = errors.New("RPC client is not properly initialized") + ErrInvalidJailedRequestQueue = errors.New("Jailed request queue is not properly initialized") ErrNodeStartFailure = errors.New("could not create the in-memory node object") ) @@ -60,14 +61,15 @@ type SelectedExtKey struct { } type NodeManager struct { - currentNode *node.Node // currently running geth node - ctx *cli.Context // the CLI context used to start the geth node - lightEthereum *les.LightEthereum // LES service - accountManager *accounts.Manager // the account manager attached to the currentNode - SelectedAccount *SelectedExtKey // account that was processed during the last call to SelectAccount() - whisperService *whisper.Whisper // Whisper service - client *rpc.ClientRestartWrapper // RPC client - nodeStarted chan struct{} // channel to wait for node to start + currentNode *node.Node // currently running geth node + ctx *cli.Context // the CLI context used to start the geth node + lightEthereum *les.LightEthereum // LES service + accountManager *accounts.Manager // the account manager attached to the currentNode + jailedRequestQueue *JailedRequestQueue // bridge via which jail notifies node of incoming requests + SelectedAccount *SelectedExtKey // account that was processed during the last call to SelectAccount() + whisperService *whisper.Whisper // Whisper service + client *rpc.ClientRestartWrapper // RPC client + nodeStarted chan struct{} // channel to wait for node to start } var ( @@ -77,7 +79,9 @@ var ( func NewNodeManager(datadir string, rpcport int) *NodeManager { createOnce.Do(func() { - nodeManagerInstance = &NodeManager{} + nodeManagerInstance = &NodeManager{ + jailedRequestQueue: NewJailedRequestsQueue(), + } nodeManagerInstance.MakeNode(datadir, rpcport) }) @@ -277,6 +281,22 @@ func (m *NodeManager) ClientRestartWrapper() (*rpc.ClientRestartWrapper, error) return m.client, nil } +func (m *NodeManager) HasJailedRequestQueue() bool { + return m.jailedRequestQueue != nil +} + +func (m *NodeManager) JailedRequestQueue() (*JailedRequestQueue, error) { + if m == nil || !m.HasNode() { + return nil, ErrInvalidGethNode + } + + if !m.HasJailedRequestQueue() { + return nil, ErrInvalidJailedRequestQueue + } + + return m.jailedRequestQueue, nil +} + func makeDefaultExtra() []byte { var clientInfo = struct { Version uint diff --git a/geth/node_test.go b/geth/node_test.go index 5ef437994..6ddcc5c58 100644 --- a/geth/node_test.go +++ b/geth/node_test.go @@ -2,9 +2,9 @@ package geth_test import ( "os" + "path/filepath" "testing" "time" - "path/filepath" "github.com/status-im/status-go/geth" ) diff --git a/geth/txqueue.go b/geth/txqueue.go index 090ba2816..8cd5d0292 100644 --- a/geth/txqueue.go +++ b/geth/txqueue.go @@ -8,22 +8,36 @@ extern bool StatusServiceSignalEvent( const char *jsonEvent ); import "C" import ( + "context" "encoding/json" + "fmt" + "bytes" + "github.com/cnf/structhash" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/les/status" + "github.com/robertkrimen/otto" ) const ( EventTransactionQueued = "transaction.queued" + SendTransactionRequest = "eth_sendTransaction" + MessageIdKey = "message_id" ) func onSendTransactionRequest(queuedTx status.QueuedTx) { + requestCtx := context.Background() + requestQueue, err := GetNodeManager().JailedRequestQueue() + if err == nil { + requestCtx = requestQueue.PopQueuedTxContext(&queuedTx) + } + event := GethEvent{ Type: EventTransactionQueued, Event: SendTransactionEvent{ - Id: string(queuedTx.Id), - Args: queuedTx.Args, + Id: string(queuedTx.Id), + Args: queuedTx.Args, + MessageId: fromContext(requestCtx, MessageIdKey), }, } @@ -41,3 +55,173 @@ func CompleteTransaction(id, password string) (common.Hash, error) { return backend.CompleteQueuedTransaction(status.QueuedTxId(id), password) } + +func fromContext(ctx context.Context, key string) string { + if ctx == nil { + return "" + } + if messageId, ok := ctx.Value(key).(string); ok { + return messageId + } + + return "" +} + +type JailedRequest struct { + method string + ctx context.Context + vm *otto.Otto +} + +type JailedRequestQueue struct { + requests map[string]*JailedRequest +} + +func NewJailedRequestsQueue() *JailedRequestQueue { + return &JailedRequestQueue{ + requests: make(map[string]*JailedRequest), + } +} + +func (q *JailedRequestQueue) PreProcessRequest(vm *otto.Otto, req RPCCall) { + messageId := currentMessageId(vm.Context()) + + // save request context for reuse (by request handlers, such as queued transaction signal sender) + ctx := context.Background() + ctx = context.WithValue(ctx, "method", req.Method) + if len(messageId) > 0 { + ctx = context.WithValue(ctx, MessageIdKey, messageId) + } + q.saveRequestContext(vm, ctx, req) +} + +func (q *JailedRequestQueue) PostProcessRequest(vm *otto.Otto, req RPCCall) { + // set message id (if present in context) + messageId := currentMessageId(vm.Context()) + if len(messageId) > 0 { + vm.Call("addContext", nil, MessageIdKey, messageId) + } + + // set extra markers for queued transaction requests + if req.Method == SendTransactionRequest { + vm.Call("addContext", nil, SendTransactionRequest, true) + } +} + +func (q *JailedRequestQueue) saveRequestContext(vm *otto.Otto, ctx context.Context, req RPCCall) { + hash := hashFromRPCCall(req) + + if len(hash) == 0 { // no need to persist empty hash + return + } + + q.requests[hash] = &JailedRequest{ + method: req.Method, + ctx: ctx, + vm: vm, + } +} + +func (q *JailedRequestQueue) GetQueuedTxContext(queuedTx *status.QueuedTx) context.Context { + hash := hashFromQueuedTx(queuedTx) + + req, ok := q.requests[hash] + if ok { + return req.ctx + } + + return context.Background() +} + +func (q *JailedRequestQueue) PopQueuedTxContext(queuedTx *status.QueuedTx) context.Context { + hash := hashFromQueuedTx(queuedTx) + + req, ok := q.requests[hash] + if ok { + delete(q.requests, hash) + return req.ctx + } + + return context.Background() +} + +// currentMessageId looks for `status.message_id` variable in current JS context +func currentMessageId(ctx otto.Context) string { + if statusObj, ok := ctx.Symbols["status"]; ok { + messageId, err := statusObj.Object().Get("message_id") + if err != nil { + return "" + } + if messageId, err := messageId.ToString(); err == nil { + return messageId + } + } + + return "" +} + +type HashableSendRequest struct { + method string + from string + to string + value string + data string +} + +func hashFromRPCCall(req RPCCall) string { + if req.Method != SendTransactionRequest { // no need to persist extra state for other requests + return "" + } + + params, ok := req.Params[0].(map[string]interface{}) + if !ok { + return "" + } + + from, ok := params["from"].(string) + if !ok { + from = "" + } + + to, ok := params["to"].(string) + if !ok { + to = "" + } + + value, ok := params["value"].(string) + if !ok { + value = "" + } + + data, ok := params["data"].(string) + if !ok { + data = "" + } + + s := HashableSendRequest{ + method: req.Method, + from: from, + to: to, + value: value, + data: data, + } + + return fmt.Sprintf("%x", structhash.Sha1(s, 1)) +} + +func hashFromQueuedTx(queuedTx *status.QueuedTx) string { + value, err := queuedTx.Args.Value.MarshalJSON() + if err != nil { + return "" + } + + s := HashableSendRequest{ + method: SendTransactionRequest, + from: queuedTx.Args.From.Hex(), + to: queuedTx.Args.To.Hex(), + value: string(bytes.Replace(value, []byte(`"`),[]byte("") , 2)), + data: queuedTx.Args.Data, + } + + return fmt.Sprintf("%x", structhash.Sha1(s, 1)) +} diff --git a/geth/types.go b/geth/types.go index 94a499d23..7b502057b 100644 --- a/geth/types.go +++ b/geth/types.go @@ -35,8 +35,9 @@ type WhisperMessageEvent struct { } type SendTransactionEvent struct { - Id string `json:"id"` - Args status.SendTxArgs `json:"args"` + Id string `json:"id"` + Args status.SendTxArgs `json:"args"` + MessageId string `json:"message_id"` } type CompleteTransactionResult struct { @@ -48,3 +49,9 @@ type GethEvent struct { Type string `json:"type"` Event interface{} `json:"event"` } + +type RPCCall struct { + Id int64 + Method string + Params []interface{} +} diff --git a/jail/jail.go b/jail/jail.go index 01ec53fdc..3cb2f5eb1 100644 --- a/jail/jail.go +++ b/jail/jail.go @@ -5,7 +5,9 @@ import ( "errors" "fmt" "sync" + "time" + "github.com/eapache/go-resiliency/semaphore" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/rpc" @@ -13,14 +15,25 @@ import ( "github.com/status-im/status-go/geth" ) +const ( + JailedRuntimeRequestTimeout = time.Second * 60 +) + var ( ErrInvalidJail = errors.New("jail environment is not properly initialized") ) type Jail struct { - client *rpc.ClientRestartWrapper // lazy inited on the first call to jail.ClientRestartWrapper() - VMs map[string]*otto.Otto - statusJS string + client *rpc.ClientRestartWrapper // lazy inited on the first call to jail.ClientRestartWrapper() + cells map[string]*JailedRuntime // jail supports running many isolated instances of jailed runtime + statusJS string + requestQueue *geth.JailedRequestQueue +} + +type JailedRuntime struct { + id string + vm *otto.Otto + sem *semaphore.Semaphore } var jailInstance *Jail @@ -29,7 +42,7 @@ var once sync.Once func New() *Jail { once.Do(func() { jailInstance = &Jail{ - VMs: make(map[string]*otto.Otto), + cells: make(map[string]*JailedRuntime), } }) @@ -47,14 +60,23 @@ func GetInstance() *Jail { return New() // singleton, we will always get the same reference } +func NewJailedRuntime(id string) *JailedRuntime { + return &JailedRuntime{ + id: id, + vm: otto.New(), + sem: semaphore.New(1, JailedRuntimeRequestTimeout), + } +} + func (jail *Jail) Parse(chatId string, js string) string { if jail == nil { return printError(ErrInvalidJail.Error()) } - vm := otto.New() + jail.cells[chatId] = NewJailedRuntime(chatId) + vm := jail.cells[chatId].vm + initJjs := jail.statusJS + ";" - jail.VMs[chatId] = vm _, err := vm.Run(initJjs) vm.Set("jeth", struct{}{}) @@ -83,12 +105,16 @@ func (jail *Jail) Call(chatId string, path string, args string) string { return printError(err.Error()) } - vm, ok := jail.VMs[chatId] + cell, ok := jail.cells[chatId] if !ok { - return printError(fmt.Sprintf("VM[%s] doesn't exist.", chatId)) + return printError(fmt.Sprintf("Cell[%s] doesn't exist.", chatId)) } - res, err := vm.Call("call", nil, path, args) + // serialize requests to VM + cell.sem.Acquire() + defer cell.sem.Release() + + res, err := cell.vm.Call("call", nil, path, args) return printResult(res.String(), err) } @@ -98,18 +124,12 @@ func (jail *Jail) GetVM(chatId string) (*otto.Otto, error) { return nil, ErrInvalidJail } - vm, ok := jail.VMs[chatId] + cell, ok := jail.cells[chatId] if !ok { - return nil, fmt.Errorf("VM[%s] doesn't exist.", chatId) + return nil, fmt.Errorf("Cell[%s] doesn't exist.", chatId) } - return vm, nil -} - -type jsonrpcCall struct { - Id int64 - Method string - Params []interface{} + return cell.vm, nil } // Send will serialize the first argument, send it to the node and returns the response. @@ -119,6 +139,11 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { return newErrorResponse(call, -32603, err.Error(), nil) } + requestQueue, err := jail.RequestQueue() + if err != nil { + return newErrorResponse(call, -32603, err.Error(), nil) + } + // Remarshal the request into a Go value. JSON, _ := call.Otto.Object("JSON") reqVal, err := JSON.Call("stringify", call.Argument(0)) @@ -127,7 +152,7 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { } var ( rawReq = []byte(reqVal.String()) - reqs []jsonrpcCall + reqs []geth.RPCCall batch bool ) if rawReq[0] == '[' { @@ -135,7 +160,7 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { json.Unmarshal(rawReq, &reqs) } else { batch = false - reqs = make([]jsonrpcCall, 1) + reqs = make([]geth.RPCCall, 1) json.Unmarshal(rawReq, &reqs[0]) } @@ -146,6 +171,10 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { resp.Set("id", req.Id) var result json.RawMessage + // do extra request pre and post processing (message id persisting, setting tx context) + requestQueue.PreProcessRequest(call.Otto, req) + defer requestQueue.PostProcessRequest(call.Otto, req) + client := clientFactory.Client() errc := make(chan error, 1) errc2 := make(chan error) @@ -218,6 +247,29 @@ func (jail *Jail) ClientRestartWrapper() (*rpc.ClientRestartWrapper, error) { return jail.client, nil } +func (jail *Jail) RequestQueue() (*geth.JailedRequestQueue, error) { + if jail == nil { + return nil, ErrInvalidJail + } + + if jail.requestQueue != nil { + return jail.requestQueue, nil + } + + nodeManager := geth.GetNodeManager() + if !nodeManager.HasNode() { + return nil, geth.ErrInvalidGethNode + } + + requestQueue, err := nodeManager.JailedRequestQueue() + if err != nil { + return nil, err + } + jail.requestQueue = requestQueue + + return jail.requestQueue, nil +} + func newErrorResponse(call otto.FunctionCall, code int, msg string, id interface{}) otto.Value { // Bundle the error into a JSON RPC call response m := map[string]interface{}{"version": "2.0", "id": id, "error": map[string]interface{}{"code": code, msg: msg}} diff --git a/jail/jail_test.go b/jail/jail_test.go index 58b74de3b..f29291b52 100644 --- a/jail/jail_test.go +++ b/jail/jail_test.go @@ -1,20 +1,27 @@ package jail_test import ( + "encoding/json" "reflect" + "strings" "testing" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/geth" "github.com/status-im/status-go/jail" ) const ( - TEST_ADDRESS = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" - CHAT_ID_INIT = "CHAT_ID_INIT_TEST" - CHAT_ID_CALL = "CHAT_ID_CALL_TEST" - CHAT_ID_NON_EXISTENT = "CHAT_IDNON_EXISTENT" + TEST_ADDRESS = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" + TEST_ADDRESS_PASSWORD = "asdf" + CHAT_ID_INIT = "CHAT_ID_INIT_TEST" + CHAT_ID_CALL = "CHAT_ID_CALL_TEST" + CHAT_ID_SEND = "CHAT_ID_CALL_SEND" + CHAT_ID_NON_EXISTENT = "CHAT_IDNON_EXISTENT" - TESTDATA_STATUS_JS = "testdata/status.js" + TESTDATA_STATUS_JS = "testdata/status.js" + TESTDATA_TX_SEND_JS = "testdata/tx-send/" ) func TestJailUnInited(t *testing.T) { @@ -128,7 +135,7 @@ func TestJailFunctionCall(t *testing.T) { // call with wrong chat id response := jailInstance.Call(CHAT_ID_NON_EXISTENT, "", "") - expectedError := `{"error":"VM[CHAT_IDNON_EXISTENT] doesn't exist."}` + expectedError := `{"error":"Cell[CHAT_IDNON_EXISTENT] doesn't exist."}` if response != expectedError { t.Errorf("expected error is not returned: expected %s, got %s", expectedError, response) return @@ -163,11 +170,11 @@ func TestJailRPCSend(t *testing.T) { return } + // internally (since we replaced `web3.send` with `jail.Send`) + // all requests to web3 are forwarded to `jail.Send` _, err = vm.Run(` - var data = {"jsonrpc":"2.0","method":"eth_getBalance","params":["` + TEST_ADDRESS + `", "latest"],"id":1}; - var sendResult = web3.currentProvider.send(data) - console.log(JSON.stringify(sendResult)) - var sendResult = web3.fromWei(sendResult.result, "ether") + var balance = web3.eth.getBalance("` + TEST_ADDRESS + `"); + var sendResult = web3.fromWei(balance, "ether") `) if err != nil { t.Errorf("cannot run custom code on VM: %v", err) @@ -194,6 +201,167 @@ func TestJailRPCSend(t *testing.T) { t.Logf("Balance of %.2f ETH found on '%s' account", balance, TEST_ADDRESS) } +func TestJailSendQueuedTransaction(t *testing.T) { + err := geth.PrepareTestNode() + if err != nil { + t.Error(err) + return + } + + txParams := `{ + "from": "` + TEST_ADDRESS + `", + "to": "0xf82da7547534045b4e00442bc89e16186cf8c272", + "value": "0.000001" + }` + + transactionCompletedSuccessfully := make(chan bool) + + // replace transaction notification handler + var txHash = common.Hash{} + requireMessageId := false + geth.SetDefaultNodeNotificationHandler(func(jsonEvent 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{}) + messageId, ok := event["message_id"].(string) + if !ok { + t.Error("Message id is required, but not found") + return + } + if requireMessageId { + if len(messageId) == 0 { + t.Error("Message id is required, but not provided") + return + } + } else { + if len(messageId) != 0 { + t.Error("Message id is not required, but provided") + return + } + } + t.Logf("Transaction queued (will be completed in 5 secs): {id: %s}\n", event["id"].(string)) + time.Sleep(5 * time.Second) + + if txHash, err = geth.CompleteTransaction(event["id"].(string), TEST_ADDRESS_PASSWORD); err != nil { + t.Errorf("cannot complete queued transation[%v]: %v", event["id"], err) + return + } + + t.Logf("Transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex()) + transactionCompletedSuccessfully <- true // so that timeout is aborted + } + }) + + type cmd struct { + command string + params string + expectedResponse string + } + + tests := []struct { + name string + file string + requireMessageId bool + commands []cmd + }{ + { + // no context or message id + name: "Case 1: no message id or context in inited JS", + file: "no-message-id-or-context.js", + requireMessageId: false, + commands: []cmd{ + { + `["commands", "send"]`, + txParams, + `{"result": {"transaction-hash":"TX_HASH"}}`, + }, + { + `["commands", "getBalance"]`, + `{"address": "` + TEST_ADDRESS + `"}`, + `{"result": {"balance":42}}`, + }, + }, + }, + { + // context is present in inited JS (but no message id is there) + name: "Case 2: context is present in inited JS (but no message id is there)", + file: "context-no-message-id.js", + requireMessageId: false, + commands: []cmd{ + { + `["commands", "send"]`, + txParams, + `{"result": {"context":{"` + geth.SendTransactionRequest + `":true},"result":{"transaction-hash":"TX_HASH"}}}`, + }, + { + `["commands", "getBalance"]`, + `{"address": "` + TEST_ADDRESS + `"}`, + `{"result": {"context":{},"result":{"balance":42}}}`, // note emtpy (but present) context! + }, + }, + }, + { + // message id is present in inited JS, but no context is there + name: "Case 3: message id is present, context is not present", + file: "message-id-no-context.js", + requireMessageId: true, + commands: []cmd{ + { + `["commands", "send"]`, + txParams, + `{"result": {"transaction-hash":"TX_HASH"}}`, + }, + { + `["commands", "getBalance"]`, + `{"address": "` + TEST_ADDRESS + `"}`, + `{"result": {"balance":42}}`, // note emtpy context! + }, + }, + }, + { + // both message id and context are present in inited JS (this UC is what we normally expect to see) + name: "Case 4: both message id and context are present", + file: "tx-send.js", + requireMessageId: true, + commands: []cmd{ + { + `["commands", "send"]`, + txParams, + `{"result": {"context":{"eth_sendTransaction":true,"message_id":"foobar"},"result":{"transaction-hash":"TX_HASH"}}}`, + }, + { + `["commands", "getBalance"]`, + `{"address": "` + TEST_ADDRESS + `"}`, + `{"result": {"context":{"message_id":"foobar"},"result":{"balance":42}}}`, // message id in context! + }, + }, + }, + } + + //var jailInstance *jail.Jail + for _, test := range tests { + jailInstance := jail.Init(geth.LoadFromFile(TESTDATA_TX_SEND_JS + test.file)) + geth.PanicAfter(20*time.Second, transactionCompletedSuccessfully, test.name) + jailInstance.Parse(CHAT_ID_SEND, ``) + + requireMessageId = test.requireMessageId + + for _, cmd := range test.commands { + t.Logf("%s: %s", test.name, cmd.command) + response := jailInstance.Call(CHAT_ID_SEND, cmd.command, cmd.params) + expectedResponse := strings.Replace(cmd.expectedResponse, "TX_HASH", txHash.Hex(), 1) + if response != expectedResponse { + t.Errorf("expected response is not returned: expected %s, got %s", expectedResponse, response) + return + } + } + } +} + func TestJailMultipleInitSingletonJail(t *testing.T) { err := geth.PrepareTestNode() if err != nil { @@ -226,7 +394,7 @@ func TestJailGetVM(t *testing.T) { jailInstance := jail.Init("") - expectedError := `VM[` + CHAT_ID_NON_EXISTENT + `] doesn't exist.` + expectedError := `Cell[` + CHAT_ID_NON_EXISTENT + `] doesn't exist.` _, err = jailInstance.GetVM(CHAT_ID_NON_EXISTENT) if err == nil || err.Error() != expectedError { t.Error("expected error, but call succeeded") diff --git a/jail/testdata/tx-send/context-no-message-id.js b/jail/testdata/tx-send/context-no-message-id.js new file mode 100644 index 000000000..1b78a3eb9 --- /dev/null +++ b/jail/testdata/tx-send/context-no-message-id.js @@ -0,0 +1,66 @@ +var _status_catalog = { + commands: {}, + responses: {} +}; + +var context = {}; +function addContext(key, value) { + context[key] = value; +} + +function call(pathStr, paramsStr) { + var params = JSON.parse(paramsStr), + path = JSON.parse(pathStr), + fn, res; + + context = {}; + + fn = path.reduce(function(catalog, name) { + if (catalog && catalog[name]) { + return catalog[name]; + } + }, _status_catalog); + + if (!fn) { + return null; + } + + // while fn wll be executed context will be populated + // by addContext calls from status-go + callResult = fn(params); + res = { + result: callResult, + // so context could contain + // {transaction-sent: true} + context: context + }; + + return JSON.stringify(res); +} + +function sendTransaction(params) { + var data = { + from: params.from, + to: params.to, + value: web3.toWei(params.value, "ether") + }; + + // Blocking call, it will return when transaction is complete. + // While call is executing, status-go will call up the application, + // allowing it to validate and complete transaction + var hash = web3.eth.sendTransaction(data); + + return {"transaction-hash": hash}; +} + +_status_catalog.commands['send'] = sendTransaction; +_status_catalog.commands['getBalance'] = function (params) { + var balance = web3.eth.getBalance(params.address); + balance = web3.fromWei(balance, "ether") + if (balance < 90) { + console.log("Unexpected balance (<90): ", balance) + } + // used in tx tests, to check that non-context, non-message-id requests work too, + // so actual balance is not important + return {"balance": 42} +}; diff --git a/jail/testdata/tx-send/message-id-no-context.js b/jail/testdata/tx-send/message-id-no-context.js new file mode 100644 index 000000000..b92fca4ee --- /dev/null +++ b/jail/testdata/tx-send/message-id-no-context.js @@ -0,0 +1,64 @@ +// jail.Send() expects to find the current message id in `status.message_id` +// (if not found message id will not be injected, and operation will proceed) +var status = { + message_id: '42' +}; + +var _status_catalog = { + commands: {}, + responses: {} +}; + +function call(pathStr, paramsStr) { + var params = JSON.parse(paramsStr), + path = JSON.parse(pathStr), + fn, res; + + fn = path.reduce(function(catalog, name) { + if (catalog && catalog[name]) { + return catalog[name]; + } + }, _status_catalog); + + if (!fn) { + return null; + } + + // while fn wll be executed context will be populated + // by addContext calls from status-go + res = fn(params); + + return JSON.stringify(res); +} + +function sendTransaction(params) { + var data = { + from: params.from, + to: params.to, + value: web3.toWei(params.value, "ether") + }; + + // message_id allows you to distinguish between !send invocations + // (when you receive transaction queued event, message_id will be + // attached along the queued transaction id) + status.message_id = 'foobar'; + + // Blocking call, it will return when transaction is complete. + // While call is executing, status-go will call up the application, + // allowing it to validate and complete transaction + var hash = web3.eth.sendTransaction(data); + + return {"transaction-hash": hash}; +} + +_status_catalog.commands['send'] = sendTransaction; +_status_catalog.commands['getBalance'] = function (params) { + var balance = web3.eth.getBalance(params.address); + balance = web3.fromWei(balance, "ether"); + if (balance < 90) { + console.log("Unexpected balance (<90): ", balance) + } + // used in tx tests, to check that non-context, non-message-is requests work too + // so actual balance is not important + return {"balance": 42} +}; diff --git a/jail/testdata/tx-send/no-message-id-or-context.js b/jail/testdata/tx-send/no-message-id-or-context.js new file mode 100644 index 000000000..866db7ba5 --- /dev/null +++ b/jail/testdata/tx-send/no-message-id-or-context.js @@ -0,0 +1,51 @@ + +var _status_catalog = { + commands: {}, + responses: {} +}; + +function call(pathStr, paramsStr) { + var params = JSON.parse(paramsStr), + path = JSON.parse(pathStr), + fn, res; + + fn = path.reduce(function(catalog, name) { + if (catalog && catalog[name]) { + return catalog[name]; + } + }, _status_catalog); + + if (!fn) { + return null; + } + + res = fn(params); + return JSON.stringify(res); +} + +function sendTransaction(params) { + var data = { + from: params.from, + to: params.to, + value: web3.toWei(params.value, "ether") + }; + + // Blocking call, it will return when transaction is complete. + // While call is executing, status-go will call up the application, + // allowing it to validate and complete transaction + var hash = web3.eth.sendTransaction(data); + + return {"transaction-hash": hash}; +} + +_status_catalog.commands['send'] = sendTransaction; +_status_catalog.commands['getBalance'] = function (params) { + var balance = web3.eth.getBalance(params.address); + balance = web3.fromWei(balance, "ether") + if (balance < 90) { + console.log("Unexpected balance (<90): ", balance) + } + // used in tx tests, to check that non-context, non-message-is requests work too + // so actual balance is not important + return {"balance": 42} +}; \ No newline at end of file diff --git a/jail/testdata/tx-send/tx-send.js b/jail/testdata/tx-send/tx-send.js new file mode 100644 index 000000000..833066c41 --- /dev/null +++ b/jail/testdata/tx-send/tx-send.js @@ -0,0 +1,77 @@ +// jail.Send() expects to find the current message id in `status.message_id` +// (if not found message id will not be injected, and operation will proceed) +var status = { + message_id: '42' // global message id, gets replaced in sendTransaction (or any other method) +}; + +var _status_catalog = { + commands: {}, + responses: {} +}; + +var context = {}; +function addContext(key, value) { // this function is expected to be present, as status-go uses it to set context + context[key] = value; +} + +function call(pathStr, paramsStr) { + var params = JSON.parse(paramsStr), + path = JSON.parse(pathStr), + fn, res; + + context = {}; + + fn = path.reduce(function(catalog, name) { + if (catalog && catalog[name]) { + return catalog[name]; + } + }, _status_catalog); + + if (!fn) { + return null; + } + + // while fn wll be executed context will be populated + // by addContext calls from status-go + callResult = fn(params); + res = { + result: callResult, + // so context could contain {eth_transactionSend: true} + // additionally, context gets `message_id` as well + context: context + }; + + return JSON.stringify(res); +} + +function sendTransaction(params) { + var data = { + from: params.from, + to: params.to, + value: web3.toWei(params.value, "ether") + }; + + // message_id allows you to distinguish between !send invocations + // (when you receive transaction queued event, message_id will be + // attached along the queued transaction id) + status.message_id = 'foobar'; + + // Blocking call, it will return when transaction is complete. + // While call is executing, status-go will call up the application, + // allowing it to validate and complete transaction + var hash = web3.eth.sendTransaction(data); + + return {"transaction-hash": hash}; +} + +_status_catalog.commands['send'] = sendTransaction; +_status_catalog.commands['getBalance'] = function (params) { + var balance = web3.eth.getBalance(params.address); + balance = web3.fromWei(balance, "ether"); + if (balance < 90) { + console.log("Unexpected balance (<90): ", balance) + } + // used in tx tests, to check that non-context, non-message-is requests work too + // so actual balance is not important + return {"balance": 42} +};