github.com/status-im/status-go@v1.1.0/services/wallet/walletconnect/walletconnect_test.go (about) 1 package walletconnect 2 3 import ( 4 "crypto/ecdsa" 5 "encoding/json" 6 "reflect" 7 "strconv" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 14 "github.com/status-im/status-go/eth-node/crypto" 15 "github.com/status-im/status-go/eth-node/types" 16 "github.com/status-im/status-go/multiaccounts/accounts" 17 "github.com/status-im/status-go/params" 18 ) 19 20 func getSessionJSONFor(chains []int, expiry int) string { 21 chainsStr := "[" 22 for i, chain := range chains { 23 chainsStr += `"eip155:` + strconv.Itoa(chain) + `"` 24 if i != len(chains)-1 { 25 chainsStr += "," 26 } 27 } 28 chainsStr += "]" 29 expiryStr := strconv.Itoa(expiry) 30 31 return `{ 32 "expiry": ` + expiryStr + `, 33 "namespaces": { 34 "eip155": { 35 "accounts": [ 36 "eip155:1:0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", 37 "eip155:10:0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", 38 "eip155:42161:0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" 39 ], 40 "chains": ` + chainsStr + `, 41 "events": [ 42 "accountsChanged", 43 "chainChanged" 44 ], 45 "methods": [ 46 "eth_sendTransaction", 47 "personal_sign" 48 ] 49 } 50 }, 51 "optionalNamespaces": { 52 "eip155": { 53 "chains": [], 54 "events": [], 55 "methods": [], 56 "rpcMap": {} 57 } 58 }, 59 "pairingTopic": "50fba141cdb5c015493c2907c46bacf9f7cbd7c8e3d4e97df891f18dddcff69c", 60 "peer": { 61 "metadata": { 62 "description": "Test Dapp Description", 63 "icons": [ "https://test.org/test.png"], 64 "name": "Test Dapp", 65 "url": "https://dapp.test.org" 66 }, 67 "publicKey": "1234567890aeb6081cabed26faf48919162fd70cc66d639f118a60507ae0463d" 68 }, 69 "relay": { "protocol": "irn"}, 70 "requiredNamespaces": { 71 "eip155": { 72 "chains": [ 73 "eip155:1" 74 ], 75 "events": [ 76 "chainChanged", 77 "accountsChanged" 78 ], 79 "methods": [ 80 "eth_sendTransaction", 81 "personal_sign" 82 ], 83 "rpcMap": { 84 "1": "https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1" 85 } 86 } 87 }, 88 "self": { 89 "metadata": { 90 "description": "Test Wallet Description", 91 "icons": [ 92 "https://wallet.test.org/test.svg" 93 ], 94 "name": "Test Wallet", 95 "url": "http://localhost" 96 }, 97 "publicKey": "da4a87d5f0f54951afe870ebf020cf03f8a3522fbd219398c3fa159a37e16d54" 98 }, 99 "topic": "e39e1f435a46b5ee6b31484d1751cfbc35be1275653af2ea340974a7592f1a19" 100 }` 101 } 102 103 func Test_sessionProposalValidity(t *testing.T) { 104 tests := []struct { 105 name string 106 sessionProposalJSON string 107 expectedValidity bool 108 }{ 109 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#11-proposal-namespaces-does-not-include-an-optional-namespace 110 { 111 name: "proposal-namespaces-does-not-include-an-optional-namespace", 112 sessionProposalJSON: `{ 113 "params": { 114 "requiredNamespaces": { 115 "eip155:10": { 116 "methods": ["personal_sign"], 117 "events": ["accountsChanged", "chainChanged"] 118 } 119 } 120 } 121 }`, 122 expectedValidity: true, 123 }, 124 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#12-proposal-namespaces-must-not-have-chains-empty 125 { 126 name: "proposal-namespaces-must-not-have-chains-empty", 127 sessionProposalJSON: `{ 128 "params": { 129 "requiredNamespaces": { 130 "cosmos": { 131 "chains": [], 132 "methods": ["cosmos_signDirect"], 133 "events": ["someCosmosEvent"] 134 } 135 } 136 } 137 }`, 138 expectedValidity: false, 139 }, 140 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index 141 { 142 name: "chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index", 143 sessionProposalJSON: `{ 144 "params": { 145 "requiredNamespaces": { 146 "eip155": { 147 "chains": ["eip155:1", "eip155:137"], 148 "methods": ["eth_sendTransaction", "eth_signTransaction", "eth_sign"], 149 "events": ["accountsChanged", "chainChanged"] 150 }, 151 "eip155:10": { 152 "methods": ["personal_sign"], 153 "events": ["accountsChanged", "chainChanged"] 154 } 155 } 156 } 157 }`, 158 expectedValidity: true, 159 }, 160 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#14-chains-must-be-caip-2-compliant 161 { 162 name: "chains-must-be-caip-2-compliant", 163 sessionProposalJSON: `{ 164 "params": { 165 "requiredNamespaces": { 166 "eip155": { 167 "chains": ["42"], 168 "methods": ["eth_sign"], 169 "events": ["accountsChanged"] 170 } 171 } 172 } 173 }`, 174 expectedValidity: false, 175 }, 176 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#15-proposal-namespace-methods-and-events-may-be-empty 177 { 178 name: "proposal-namespace-methods-and-events-may-be-empty", 179 sessionProposalJSON: `{ 180 "params": { 181 "requiredNamespaces": { 182 "eip155": { 183 "chains": ["eip155:1"], 184 "methods": [], 185 "events": [] 186 } 187 } 188 } 189 }`, 190 expectedValidity: true, 191 }, 192 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#16-all-chains-in-the-namespace-must-contain-the-namespace-prefix 193 { 194 name: "all-chains-in-the-namespace-must-contain-the-namespace-prefix", 195 sessionProposalJSON: `{ 196 "params": { 197 "requiredNamespaces": { 198 "eip155": { 199 "chains": ["eip155:1", "eip155:137", "cosmos:cosmoshub-4"], 200 "methods": ["eth_sendTransaction"], 201 "events": ["accountsChanged", "chainChanged"] 202 } 203 }, 204 "optionalNamespaces": { 205 "eip155:42161": { 206 "methods": ["personal_sign"], 207 "events": ["accountsChanged", "chainChanged"] 208 } 209 } 210 } 211 }`, 212 expectedValidity: false, 213 }, 214 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#17-namespace-key-must-comply-with-caip-2-specification 215 { 216 name: "namespace-key-must-comply-with-caip-2-specification", 217 sessionProposalJSON: `{ 218 "params": { 219 "requiredNamespaces": { 220 "": { 221 "chains": [":1"], 222 "methods": ["personalSign"], 223 "events": [] 224 }, 225 "**": { 226 "chains": ["**:1"], 227 "methods": ["personalSign"], 228 "events": [] 229 } 230 } 231 } 232 }`, 233 expectedValidity: false, 234 }, 235 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#18-all-namespaces-must-be-valid 236 { 237 name: "all-namespaces-must-be-valid", 238 sessionProposalJSON: `{ 239 "params": { 240 "requiredNamespaces": { 241 "eip155": { 242 "chains": ["eip155:1"], 243 "methods": ["personalSign"], 244 "events": [] 245 }, 246 "cosmos": { 247 "chains": [], 248 "methods": [], 249 "events": [] 250 } 251 } 252 } 253 }`, 254 expectedValidity: false, 255 }, 256 // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#19-proposal-namespaces-may-be-empty 257 { 258 name: "proposal-namespaces-may-be-empty", 259 sessionProposalJSON: `{ 260 "params": { 261 "requiredNamespaces": {} 262 } 263 }`, 264 expectedValidity: true, 265 }, 266 } 267 268 for _, tt := range tests { 269 t.Run(tt.name, func(t *testing.T) { 270 var sessionProposal SessionProposal 271 err := json.Unmarshal([]byte(tt.sessionProposalJSON), &sessionProposal) 272 assert.NoError(t, err) 273 274 validRes := sessionProposal.ValidateProposal() 275 if tt.expectedValidity { 276 assert.True(t, validRes) 277 } else { 278 assert.False(t, validRes) 279 } 280 }) 281 } 282 } 283 284 func Test_supportedChainInSession(t *testing.T) { 285 type args struct { 286 sessionProposal Session 287 } 288 tests := []struct { 289 name string 290 args args 291 expectedChains []uint64 292 }{ 293 { 294 name: "supported_chain", 295 args: args{ 296 sessionProposal: Session{ 297 Namespaces: map[string]Namespace{ 298 "eip155": { 299 Chains: []string{"eip155:1", "eip155:2", "eip155:3", "eip155:4", "eip155:5"}, 300 }, 301 }, 302 }, 303 }, 304 expectedChains: []uint64{1, 2, 3, 4, 5}, 305 }, 306 } 307 for _, tt := range tests { 308 t.Run(tt.name, func(t *testing.T) { 309 gotChains := supportedChainsInSession(tt.args.sessionProposal) 310 if !reflect.DeepEqual(gotChains, tt.expectedChains) { 311 t.Errorf("supportedChainInSessionProposal() gotChains = %v, want %v", gotChains, tt.expectedChains) 312 } 313 }) 314 } 315 } 316 317 func Test_caip10Accounts(t *testing.T) { 318 type args struct { 319 accounts []*accounts.Account 320 chains []uint64 321 } 322 tests := []struct { 323 name string 324 args args 325 want []string 326 }{ 327 { 328 name: "generate_caip10_accounts", 329 args: args{ 330 accounts: []*accounts.Account{ 331 { 332 Address: types.HexToAddress("0x1"), 333 Type: accounts.AccountTypeWatch, 334 }, 335 { 336 Address: types.HexToAddress("0x2"), 337 Type: accounts.AccountTypeSeed, 338 }, 339 }, 340 chains: []uint64{1, 2}, 341 }, 342 want: []string{ 343 "eip155:1:0x0000000000000000000000000000000000000001", 344 "eip155:2:0x0000000000000000000000000000000000000001", 345 "eip155:1:0x0000000000000000000000000000000000000002", 346 "eip155:2:0x0000000000000000000000000000000000000002", 347 }, 348 }, 349 { 350 name: "empty_addresses", 351 args: args{ 352 accounts: []*accounts.Account{}, 353 chains: []uint64{1, 2}, 354 }, 355 want: []string{}, 356 }, 357 { 358 name: "empty_chains", 359 args: args{ 360 accounts: []*accounts.Account{ 361 { 362 Address: types.HexToAddress("0x1"), 363 Type: accounts.AccountTypeWatch, 364 }, 365 { 366 Address: types.HexToAddress("0x2"), 367 Type: accounts.AccountTypeSeed, 368 }, 369 }, 370 chains: []uint64{}, 371 }, 372 want: []string{}, 373 }, 374 } 375 for _, tt := range tests { 376 t.Run(tt.name, func(t *testing.T) { 377 if got := caip10Accounts(tt.args.accounts, tt.args.chains); !reflect.DeepEqual(got, tt.want) { 378 t.Errorf("caip10Accounts() = %v, want %v", got, tt.want) 379 } 380 }) 381 } 382 } 383 384 // Test_AddSession validates that the new added session is active (not expired and not disconnected) 385 func Test_AddSession(t *testing.T) { 386 db, close := SetupTestDB(t) 387 defer close() 388 389 // Add session for testnet 390 expiry := 1716581732 391 chainID := 11155111 392 sessionJSON := getSessionJSONFor([]int{chainID}, expiry) 393 networks := []params.Network{ 394 {ChainID: 1, IsTest: false}, 395 {ChainID: uint64(chainID), IsTest: true}, 396 } 397 timestampBeforeAddSession := time.Now().Unix() 398 err := AddSession(db, networks, sessionJSON) 399 assert.NoError(t, err) 400 401 // Validate that session was written correctly to the database 402 sessions, err := GetSessions(db) 403 assert.NoError(t, err) 404 assert.Equal(t, 1, len(sessions)) 405 406 sessJSONObj := map[string]interface{}{} 407 err = json.Unmarshal([]byte(sessionJSON), &sessJSONObj) 408 assert.NoError(t, err) 409 410 assert.Equal(t, false, sessions[0].Disconnected) 411 assert.Equal(t, sessionJSON, sessions[0].SessionJSON) 412 assert.Equal(t, int64(expiry), sessions[0].Expiry) 413 assert.GreaterOrEqual(t, sessions[0].CreatedTimestamp, timestampBeforeAddSession) 414 assert.Equal(t, sessJSONObj["pairingTopic"], string(sessions[0].PairingTopic)) 415 assert.Equal(t, sessJSONObj["topic"], string(sessions[0].Topic)) 416 assert.Equal(t, true, sessions[0].TestChains) 417 418 metadata := sessJSONObj["peer"].(map[string]interface{})["metadata"].(map[string]interface{}) 419 assert.Equal(t, metadata["url"], sessions[0].URL) 420 assert.Equal(t, metadata["name"], sessions[0].Name) 421 assert.Equal(t, metadata["icons"].([]interface{})[0], sessions[0].IconURL) 422 423 dapps, err := GetActiveDapps(db, int64(expiry-1), true) 424 assert.NoError(t, err) 425 assert.Equal(t, 1, len(dapps)) 426 assert.Equal(t, sessions[0].URL, dapps[0].URL) 427 assert.Equal(t, sessions[0].Name, dapps[0].Name) 428 assert.Equal(t, sessions[0].IconURL, dapps[0].IconURL) 429 } 430 431 type typedDataParams struct { 432 chainID int 433 skipField bool 434 excludeChainID bool 435 wrongContractType bool 436 } 437 438 func generateTypedDataJson(p typedDataParams) string { 439 optionalKeyValueField := "" 440 if !p.skipField { 441 if p.wrongContractType { 442 optionalKeyValueField = `,"verifyingContract": true` 443 } else { 444 optionalKeyValueField = `,"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"` 445 } 446 } 447 448 chainIDSchemeEntry := "" 449 chainIDDataEntry := "" 450 if !p.excludeChainID { 451 chainIDSchemeEntry = `{"name": "chainId", "type": "uint256"},` 452 chainIDDataEntry = `,"chainId": ` + strconv.Itoa(p.chainID) 453 } 454 455 typedData := `{ 456 "types": { 457 "EIP712Domain": [ 458 {"name": "name", "type": "string"}, 459 {"name": "version", "type": "string"}, 460 ` + chainIDSchemeEntry + ` 461 {"name": "verifyingContract", "type": "address"} 462 ], 463 "Person": [ 464 {"name": "name", "type": "string"}, 465 {"name": "wallet", "type": "address"} 466 ], 467 "Mail": [ 468 {"name": "from", "type": "Person"}, 469 {"name": "to", "type": "Person"}, 470 {"name": "contents", "type": "string"} 471 ] 472 }, 473 "primaryType": "Mail", 474 "domain": { 475 "name": "Ether Mail", 476 "version": "1" 477 ` + chainIDDataEntry + ` 478 ` + optionalKeyValueField + ` 479 }, 480 "message": { 481 "from": { 482 "name": "Cow", 483 "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 484 }, 485 "to": { 486 "name": "Bob", 487 "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 488 }, 489 "contents": "Hello, Bob!" 490 } 491 }` 492 return typedData 493 } 494 495 func TestSafeSignTypedDataForDApps(t *testing.T) { 496 // 0x4f1B9Ee595bF612480ADAF623Ec583f623ae802d 497 privateKey, err := crypto.HexToECDSA("efe79ae971aa8bb612de9de7c65b9224ab1b6a69e6ec733ec92110f100c7244a") 498 require.NoError(t, err) 499 type args struct { 500 typedJson string 501 privateKey *ecdsa.PrivateKey 502 chainID uint64 503 legacy bool 504 } 505 tests := []struct { 506 name string 507 args args 508 wantErr bool 509 }{ 510 { 511 name: "sign_typed_data", 512 args: args{ 513 typedJson: generateTypedDataJson(typedDataParams{ 514 chainID: 1, 515 }), 516 privateKey: privateKey, 517 chainID: 1, 518 legacy: false, 519 }, 520 wantErr: false, 521 }, 522 { 523 name: "sign_typed_data_legacy", 524 args: args{ 525 typedJson: generateTypedDataJson(typedDataParams{ 526 chainID: 1, 527 }), 528 privateKey: privateKey, 529 chainID: 1, 530 legacy: true, 531 }, 532 wantErr: false, 533 }, 534 { 535 name: "sign_typed_data_invalid_json", 536 args: args{ 537 typedJson: generateTypedDataJson(typedDataParams{ 538 chainID: 1, 539 wrongContractType: true, 540 }), 541 privateKey: privateKey, 542 chainID: 1, 543 legacy: false, 544 }, 545 wantErr: true, 546 }, 547 { 548 name: "sign_typed_data_invalid_json_legacy", 549 args: args{ 550 typedJson: `{"invalid": "json"`, 551 privateKey: privateKey, 552 chainID: 1, 553 legacy: true, 554 }, 555 wantErr: true, 556 }, 557 { 558 name: "sign_typed_data_invalid_chain_id", 559 args: args{ 560 typedJson: generateTypedDataJson(typedDataParams{ 561 chainID: 1, 562 }), 563 privateKey: privateKey, 564 chainID: 2, 565 legacy: false, 566 }, 567 wantErr: true, 568 }, 569 { 570 name: "sign_typed_data_missing_field", 571 args: args{ 572 typedJson: generateTypedDataJson(typedDataParams{ 573 chainID: 1, 574 skipField: true, 575 }), 576 privateKey: privateKey, 577 chainID: 1, 578 legacy: false, 579 }, 580 wantErr: true, 581 }, 582 { 583 name: "sign_typed_data_exclude_chain_id", 584 args: args{ 585 typedJson: generateTypedDataJson(typedDataParams{ 586 chainID: 1, 587 excludeChainID: true, 588 }), 589 privateKey: privateKey, 590 chainID: 1, 591 legacy: false, 592 }, 593 wantErr: false, 594 }, 595 } 596 for _, tt := range tests { 597 t.Run(tt.name, func(t *testing.T) { 598 got, err := SafeSignTypedDataForDApps(tt.args.typedJson, tt.args.privateKey, tt.args.chainID, tt.args.legacy) 599 if (err != nil) != tt.wantErr { 600 t.Errorf("SafeSignTypedDataForDApps() error = %v, wantErr %v", err, tt.wantErr) 601 return 602 } 603 if !tt.wantErr { 604 require.NotEmpty(t, got) 605 require.Len(t, got, 65) 606 } 607 }) 608 } 609 }