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  }