-
Notifications
You must be signed in to change notification settings - Fork 30
/
utils.go
378 lines (336 loc) · 12.6 KB
/
utils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
package test
import (
"bytes"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/crypto"
"github.com/go-kit/kit/transport/http/jsonrpc"
"github.com/gorilla/websocket"
"github.com/ten-protocol/go-ten/go/common/log"
"github.com/ten-protocol/go-ten/go/common/viewingkey"
"github.com/ten-protocol/go-ten/tools/walletextension/common"
"github.com/ten-protocol/go-ten/tools/walletextension/config"
"github.com/ten-protocol/go-ten/tools/walletextension/container"
gethcommon "github.com/ethereum/go-ethereum/common"
gethlog "github.com/ethereum/go-ethereum/log"
gethnode "github.com/ethereum/go-ethereum/node"
gethrpc "github.com/ethereum/go-ethereum/rpc"
hostcontainer "github.com/ten-protocol/go-ten/go/host/container"
)
const jsonID = "1"
func createWalExtCfg(connectPort, wallHTTPPort, wallWSPort int) *config.Config { //nolint: unparam
testDBPath, err := os.CreateTemp("", "")
if err != nil {
panic("could not create persistence file for wallet extension tests")
}
return &config.Config{
NodeRPCWebsocketAddress: fmt.Sprintf("localhost:%d", connectPort),
DBPathOverride: testDBPath.Name(),
WalletExtensionPortHTTP: wallHTTPPort,
WalletExtensionPortWS: wallWSPort,
DBType: "sqlite",
}
}
func createWalExt(t *testing.T, walExtCfg *config.Config) func() error {
// todo (@ziga) - log somewhere else?
logger := log.New(log.WalletExtCmp, int(gethlog.LvlInfo), log.SysOut)
wallExtContainer := container.NewWalletExtensionContainerFromConfig(*walExtCfg, logger)
go wallExtContainer.Start() //nolint: errcheck
err := waitForEndpoint(fmt.Sprintf("http://%s:%d%s", walExtCfg.WalletExtensionHost, walExtCfg.WalletExtensionPortHTTP, common.PathReady))
if err != nil {
t.Fatalf(err.Error())
}
return wallExtContainer.Stop
}
// Creates an RPC layer that the wallet extension can connect to. Returns a handle to shut down the host.
func createDummyHost(t *testing.T, wsRPCPort int) (*DummyAPI, func() error) { //nolint: unparam
dummyAPI := NewDummyAPI()
cfg := gethnode.Config{
WSHost: common.Localhost,
WSPort: wsRPCPort,
WSOrigins: []string{"*"},
}
rpcServerNode, err := gethnode.New(&cfg)
rpcServerNode.RegisterAPIs([]gethrpc.API{
{
Namespace: hostcontainer.APINamespaceObscuro,
Version: hostcontainer.APIVersion1,
Service: dummyAPI,
Public: true,
},
{
Namespace: hostcontainer.APINamespaceEth,
Version: hostcontainer.APIVersion1,
Service: dummyAPI,
Public: true,
},
})
if err != nil {
t.Fatalf(fmt.Sprintf("could not create new client server. Cause: %s", err))
}
t.Cleanup(func() { rpcServerNode.Close() })
err = rpcServerNode.Start()
if err != nil {
t.Fatalf(fmt.Sprintf("could not create new client server. Cause: %s", err))
}
return dummyAPI, rpcServerNode.Close
}
// Waits for the endpoint to be available. Times out after three seconds.
func waitForEndpoint(addr string) error {
retries := 30
for i := 0; i < retries; i++ {
resp, err := http.Get(addr) //nolint:noctx,gosec
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if err == nil {
return nil
}
time.Sleep(300 * time.Millisecond)
}
return fmt.Errorf("could not establish connection to wallet extension")
}
// Makes an Ethereum JSON RPC request over HTTP and returns the response body.
func makeHTTPEthJSONReq(port int, method string, params interface{}) []byte {
reqBody := prepareRequestBody(method, params)
return makeRequestHTTP(fmt.Sprintf("http://%s:%d/v1/", common.Localhost, port), reqBody)
}
// Makes an Ethereum JSON RPC request over HTTP to specific endpoint and returns the response body.
func makeHTTPEthJSONReqWithPath(port int, path string) []byte {
reqBody := prepareRequestBody("", "")
return makeRequestHTTP(fmt.Sprintf("http://%s:%d/%s", common.Localhost, port, path), reqBody)
}
// Makes an Ethereum JSON RPC request over HTTP and returns the response body with userID query paremeter.
func makeHTTPEthJSONReqWithUserID(port int, method string, params interface{}, userID string) []byte { //nolint: unparam
reqBody := prepareRequestBody(method, params)
return makeRequestHTTP(fmt.Sprintf("http://%s:%d/v1/?token=%s", common.Localhost, port, userID), reqBody)
}
// Makes an Ethereum JSON RPC request over websockets and returns the response body.
func makeWSEthJSONReq(port int, method string, params interface{}) ([]byte, *websocket.Conn) {
reqBody := prepareRequestBody(method, params)
return makeRequestWS(fmt.Sprintf("ws://%s:%d", common.Localhost, port), reqBody)
}
func makeWSEthJSONReqWithConn(conn *websocket.Conn, method string, params interface{}) []byte {
reqBody := prepareRequestBody(method, params)
return issueRequestWS(conn, reqBody)
}
func openWSConn(port int) (*websocket.Conn, error) {
conn, dialResp, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://%s:%d", common.Localhost, port), nil)
if dialResp != nil && dialResp.Body != nil {
defer dialResp.Body.Close()
}
if err != nil {
if conn != nil {
conn.Close()
}
panic(fmt.Errorf("received error response from wallet extension: %w", err))
}
return conn, err
}
// Formats a method and its parameters as a Ethereum JSON RPC request.
func prepareRequestBody(method string, params interface{}) []byte {
reqBodyBytes, err := json.Marshal(map[string]interface{}{
common.JSONKeyRPCVersion: jsonrpc.Version,
common.JSONKeyMethod: method,
common.JSONKeyParams: params,
common.JSONKeyID: jsonID,
})
if err != nil {
panic(fmt.Errorf("failed to prepare request body. Cause: %w", err))
}
return reqBodyBytes
}
// Generates a new account and registers it with the node.
func simulateViewingKeyRegister(t *testing.T, walletHTTPPort, walletWSPort int, useWS bool) (*gethcommon.Address, []byte, []byte) {
accountPrivateKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
accountAddr := crypto.PubkeyToAddress(accountPrivateKey.PublicKey)
compressedHexVKBytes := generateViewingKey(walletHTTPPort, walletWSPort, accountAddr.String(), useWS)
mmSignature := signViewingKey(accountPrivateKey, compressedHexVKBytes)
submitViewingKey(accountAddr.String(), walletHTTPPort, walletWSPort, mmSignature, useWS)
// transform the metamask signature to the geth compatible one
sigStr := hex.EncodeToString(mmSignature)
// and then we extract the signature bytes in the same way as the wallet extension
outputSig, err := hex.DecodeString(sigStr[2:])
if err != nil {
panic(fmt.Errorf("failed to decode signature string: %w", err))
}
// This same change is made in geth internals, for legacy reasons to be able to recover the address:
// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L452-L459
outputSig[64] -= 27
// keys are expected to be a []byte of hex string
vkPubKeyBytes, err := hex.DecodeString(string(compressedHexVKBytes))
if err != nil {
panic(fmt.Errorf("unexpected hex string"))
}
return &accountAddr, vkPubKeyBytes, outputSig
}
// Generates a viewing key.
func generateViewingKey(wallHTTPPort, wallWSPort int, accountAddress string, useWS bool) []byte {
generateViewingKeyBodyBytes, err := json.Marshal(map[string]interface{}{
common.JSONKeyAddress: accountAddress,
})
if err != nil {
panic(err)
}
if useWS {
viewingKeyBytes, _ := makeRequestWS(fmt.Sprintf("ws://%s:%d%s", common.Localhost, wallWSPort, common.PathGenerateViewingKey), generateViewingKeyBodyBytes)
return viewingKeyBytes
}
return makeRequestHTTP(fmt.Sprintf("http://%s:%d%s", common.Localhost, wallHTTPPort, common.PathGenerateViewingKey), generateViewingKeyBodyBytes)
}
// Signs a viewing key like metamask
func signViewingKey(privateKey *ecdsa.PrivateKey, compressedHexVKBytes []byte) []byte {
// compressedHexVKBytes already has the key in the hex format
// it should be decoded back into raw bytes
viewingKey, err := hex.DecodeString(string(compressedHexVKBytes))
if err != nil {
panic(err)
}
msgToSign := viewingkey.GenerateSignMessage(viewingKey)
signature, err := crypto.Sign(accounts.TextHash([]byte(msgToSign)), privateKey)
if err != nil {
panic(err)
}
// We have to transform the V from 0/1 to 27/28, and add the leading "0".
signature[64] += 27
signatureWithLeadBytes := append([]byte("0"), signature...)
return signatureWithLeadBytes
}
// Submits a viewing key.
func submitViewingKey(accountAddr string, wallHTTPPort, wallWSPort int, signature []byte, useWS bool) {
submitViewingKeyBodyBytes, err := json.Marshal(map[string]interface{}{
common.JSONKeySignature: hex.EncodeToString(signature),
common.JSONKeyAddress: accountAddr,
})
if err != nil {
panic(err)
}
if useWS {
makeRequestWS(fmt.Sprintf("ws://%s:%d%s", common.Localhost, wallWSPort, common.PathSubmitViewingKey), submitViewingKeyBodyBytes)
} else {
makeRequestHTTP(fmt.Sprintf("http://%s:%d%s", common.Localhost, wallHTTPPort, common.PathSubmitViewingKey), submitViewingKeyBodyBytes)
}
}
// Sends the body to the URL over HTTP, and returns the result.
func makeRequestHTTP(url string, body []byte) []byte {
generateViewingKeyBody := bytes.NewBuffer(body)
resp, err := http.Post(url, "application/json", generateViewingKeyBody) //nolint:noctx,gosec
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
panic(err)
}
viewingKey, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
return viewingKey
}
// Sends the body to the URL over a websocket connection, and returns the result.
func makeRequestWS(url string, body []byte) ([]byte, *websocket.Conn) {
conn, dialResp, err := websocket.DefaultDialer.Dial(url, nil)
if dialResp != nil && dialResp.Body != nil {
defer dialResp.Body.Close()
}
if err != nil {
if conn != nil {
conn.Close()
}
panic(fmt.Errorf("received error response from wallet extension: %w", err))
}
return issueRequestWS(conn, body), conn
}
// issues request on an existing ws connection
func issueRequestWS(conn *websocket.Conn, body []byte) []byte {
err := conn.WriteMessage(websocket.TextMessage, body)
if err != nil {
panic(err)
}
_, reqResp, err := conn.ReadMessage()
if err != nil {
panic(err)
}
return reqResp
}
// Reads messages from the connection for the provided duration, and returns the read messages.
//func readMessagesForDuration(t *testing.T, conn *websocket.Conn, duration time.Duration) [][]byte {
// // We set a timeout to kill the test, in case we never receive a log.
// timeout := time.AfterFunc(duration*3, func() {
// t.Fatalf("timed out waiting to receive a log via the subscription")
// })
// defer timeout.Stop()
//
// var msgs [][]byte
// endTime := time.Now().Add(duration)
// for {
// _, msg, err := conn.ReadMessage()
// if err != nil {
// t.Fatalf("could not read message from websocket. Cause: %s", err)
// }
// msgs = append(msgs, msg)
// if time.Now().After(endTime) {
// return msgs
// }
// }
//}
// Asserts that there are no duplicate logs in the provided list.
//func assertNoDupeLogs(t *testing.T, logsJSON [][]byte) {
// logCount := make(map[string]int)
//
// for _, logJSON := range logsJSON {
// // Check if the log is already in the logCount map.
// _, exist := logCount[string(logJSON)]
// if exist {
// logCount[string(logJSON)]++ // If it is, increase the count for that log by one.
// } else {
// logCount[string(logJSON)] = 1 // Otherwise, start a count for that log starting at one.
// }
// }
//
// for logJSON, count := range logCount {
// if count > 1 {
// t.Errorf("received duplicate log with body %s", logJSON)
// }
// }
//}
// Checks that the response to a request is correctly formatted, and returns the result field.
func validateJSONResponse(t *testing.T, resp []byte) {
var respJSON map[string]interface{}
err := json.Unmarshal(resp, &respJSON)
if err != nil {
t.Fatalf("could not unmarshal response to JSON")
}
id := respJSON[common.JSONKeyID]
jsonRPCVersion := respJSON[common.JSONKeyRPCVersion]
result := respJSON[common.JSONKeyResult]
if id != jsonID {
t.Fatalf("response did not contain expected ID. Expected 1, got %s", id)
}
if jsonRPCVersion != jsonrpc.Version {
t.Fatalf("response did not contain expected RPC version. Expected 2.0, got %s", jsonRPCVersion)
}
if result == nil {
t.Fatalf("response did not contain `result` field")
}
}
// Checks that the response to a subscription request is correctly formatted.
//func validateSubscriptionResponse(t *testing.T, resp []byte) {
// result := validateJSONResponse(t, resp)
// pattern := "0x.*"
// resultString, ok := result.(string)
// if !ok || !regexp.MustCompile(pattern).MatchString(resultString) {
// t.Fatalf("subscription response did not contain expected result. Expected pattern matching %s, got %s", pattern, resultString)
// }
//}