status-go/services/wallet/walletconnect/walletconnect_test.go

610 lines
15 KiB
Go

package walletconnect
import (
"crypto/ecdsa"
"encoding/json"
"reflect"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
)
func getSessionJSONFor(chains []int, expiry int) string {
chainsStr := "["
for i, chain := range chains {
chainsStr += `"eip155:` + strconv.Itoa(chain) + `"`
if i != len(chains)-1 {
chainsStr += ","
}
}
chainsStr += "]"
expiryStr := strconv.Itoa(expiry)
return `{
"expiry": ` + expiryStr + `,
"namespaces": {
"eip155": {
"accounts": [
"eip155:1:0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240",
"eip155:10:0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240",
"eip155:42161:0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
],
"chains": ` + chainsStr + `,
"events": [
"accountsChanged",
"chainChanged"
],
"methods": [
"eth_sendTransaction",
"personal_sign"
]
}
},
"optionalNamespaces": {
"eip155": {
"chains": [],
"events": [],
"methods": [],
"rpcMap": {}
}
},
"pairingTopic": "50fba141cdb5c015493c2907c46bacf9f7cbd7c8e3d4e97df891f18dddcff69c",
"peer": {
"metadata": {
"description": "Test Dapp Description",
"icons": [ "https://test.org/test.png"],
"name": "Test Dapp",
"url": "https://dapp.test.org"
},
"publicKey": "1234567890aeb6081cabed26faf48919162fd70cc66d639f118a60507ae0463d"
},
"relay": { "protocol": "irn"},
"requiredNamespaces": {
"eip155": {
"chains": [
"eip155:1"
],
"events": [
"chainChanged",
"accountsChanged"
],
"methods": [
"eth_sendTransaction",
"personal_sign"
],
"rpcMap": {
"1": "https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1"
}
}
},
"self": {
"metadata": {
"description": "Test Wallet Description",
"icons": [
"https://wallet.test.org/test.svg"
],
"name": "Test Wallet",
"url": "http://localhost"
},
"publicKey": "da4a87d5f0f54951afe870ebf020cf03f8a3522fbd219398c3fa159a37e16d54"
},
"topic": "e39e1f435a46b5ee6b31484d1751cfbc35be1275653af2ea340974a7592f1a19"
}`
}
func Test_sessionProposalValidity(t *testing.T) {
tests := []struct {
name string
sessionProposalJSON string
expectedValidity bool
}{
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#11-proposal-namespaces-does-not-include-an-optional-namespace
{
name: "proposal-namespaces-does-not-include-an-optional-namespace",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155:10": {
"methods": ["personal_sign"],
"events": ["accountsChanged", "chainChanged"]
}
}
}
}`,
expectedValidity: true,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#12-proposal-namespaces-must-not-have-chains-empty
{
name: "proposal-namespaces-must-not-have-chains-empty",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"cosmos": {
"chains": [],
"methods": ["cosmos_signDirect"],
"events": ["someCosmosEvent"]
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index
{
name: "chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1", "eip155:137"],
"methods": ["eth_sendTransaction", "eth_signTransaction", "eth_sign"],
"events": ["accountsChanged", "chainChanged"]
},
"eip155:10": {
"methods": ["personal_sign"],
"events": ["accountsChanged", "chainChanged"]
}
}
}
}`,
expectedValidity: true,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#14-chains-must-be-caip-2-compliant
{
name: "chains-must-be-caip-2-compliant",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["42"],
"methods": ["eth_sign"],
"events": ["accountsChanged"]
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#15-proposal-namespace-methods-and-events-may-be-empty
{
name: "proposal-namespace-methods-and-events-may-be-empty",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1"],
"methods": [],
"events": []
}
}
}
}`,
expectedValidity: true,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#16-all-chains-in-the-namespace-must-contain-the-namespace-prefix
{
name: "all-chains-in-the-namespace-must-contain-the-namespace-prefix",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1", "eip155:137", "cosmos:cosmoshub-4"],
"methods": ["eth_sendTransaction"],
"events": ["accountsChanged", "chainChanged"]
}
},
"optionalNamespaces": {
"eip155:42161": {
"methods": ["personal_sign"],
"events": ["accountsChanged", "chainChanged"]
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#17-namespace-key-must-comply-with-caip-2-specification
{
name: "namespace-key-must-comply-with-caip-2-specification",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"": {
"chains": [":1"],
"methods": ["personalSign"],
"events": []
},
"**": {
"chains": ["**:1"],
"methods": ["personalSign"],
"events": []
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#18-all-namespaces-must-be-valid
{
name: "all-namespaces-must-be-valid",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1"],
"methods": ["personalSign"],
"events": []
},
"cosmos": {
"chains": [],
"methods": [],
"events": []
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#19-proposal-namespaces-may-be-empty
{
name: "proposal-namespaces-may-be-empty",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {}
}
}`,
expectedValidity: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var sessionProposal SessionProposal
err := json.Unmarshal([]byte(tt.sessionProposalJSON), &sessionProposal)
assert.NoError(t, err)
validRes := sessionProposal.ValidateProposal()
if tt.expectedValidity {
assert.True(t, validRes)
} else {
assert.False(t, validRes)
}
})
}
}
func Test_supportedChainInSession(t *testing.T) {
type args struct {
sessionProposal Session
}
tests := []struct {
name string
args args
expectedChains []uint64
}{
{
name: "supported_chain",
args: args{
sessionProposal: Session{
Namespaces: map[string]Namespace{
"eip155": {
Chains: []string{"eip155:1", "eip155:2", "eip155:3", "eip155:4", "eip155:5"},
},
},
},
},
expectedChains: []uint64{1, 2, 3, 4, 5},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotChains := supportedChainsInSession(tt.args.sessionProposal)
if !reflect.DeepEqual(gotChains, tt.expectedChains) {
t.Errorf("supportedChainInSessionProposal() gotChains = %v, want %v", gotChains, tt.expectedChains)
}
})
}
}
func Test_caip10Accounts(t *testing.T) {
type args struct {
accounts []*accounts.Account
chains []uint64
}
tests := []struct {
name string
args args
want []string
}{
{
name: "generate_caip10_accounts",
args: args{
accounts: []*accounts.Account{
{
Address: types.HexToAddress("0x1"),
Type: accounts.AccountTypeWatch,
},
{
Address: types.HexToAddress("0x2"),
Type: accounts.AccountTypeSeed,
},
},
chains: []uint64{1, 2},
},
want: []string{
"eip155:1:0x0000000000000000000000000000000000000001",
"eip155:2:0x0000000000000000000000000000000000000001",
"eip155:1:0x0000000000000000000000000000000000000002",
"eip155:2:0x0000000000000000000000000000000000000002",
},
},
{
name: "empty_addresses",
args: args{
accounts: []*accounts.Account{},
chains: []uint64{1, 2},
},
want: []string{},
},
{
name: "empty_chains",
args: args{
accounts: []*accounts.Account{
{
Address: types.HexToAddress("0x1"),
Type: accounts.AccountTypeWatch,
},
{
Address: types.HexToAddress("0x2"),
Type: accounts.AccountTypeSeed,
},
},
chains: []uint64{},
},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := caip10Accounts(tt.args.accounts, tt.args.chains); !reflect.DeepEqual(got, tt.want) {
t.Errorf("caip10Accounts() = %v, want %v", got, tt.want)
}
})
}
}
// Test_AddSession validates that the new added session is active (not expired and not disconnected)
func Test_AddSession(t *testing.T) {
db, close := SetupTestDB(t)
defer close()
// Add session for testnet
expiry := 1716581732
chainID := 11155111
sessionJSON := getSessionJSONFor([]int{chainID}, expiry)
networks := []params.Network{
{ChainID: 1, IsTest: false},
{ChainID: uint64(chainID), IsTest: true},
}
timestampBeforeAddSession := time.Now().Unix()
err := AddSession(db, networks, sessionJSON)
assert.NoError(t, err)
// Validate that session was written correctly to the database
sessions, err := GetSessions(db)
assert.NoError(t, err)
assert.Equal(t, 1, len(sessions))
sessJSONObj := map[string]interface{}{}
err = json.Unmarshal([]byte(sessionJSON), &sessJSONObj)
assert.NoError(t, err)
assert.Equal(t, false, sessions[0].Disconnected)
assert.Equal(t, sessionJSON, sessions[0].SessionJSON)
assert.Equal(t, int64(expiry), sessions[0].Expiry)
assert.GreaterOrEqual(t, sessions[0].CreatedTimestamp, timestampBeforeAddSession)
assert.Equal(t, sessJSONObj["pairingTopic"], string(sessions[0].PairingTopic))
assert.Equal(t, sessJSONObj["topic"], string(sessions[0].Topic))
assert.Equal(t, true, sessions[0].TestChains)
metadata := sessJSONObj["peer"].(map[string]interface{})["metadata"].(map[string]interface{})
assert.Equal(t, metadata["url"], sessions[0].URL)
assert.Equal(t, metadata["name"], sessions[0].Name)
assert.Equal(t, metadata["icons"].([]interface{})[0], sessions[0].IconURL)
dapps, err := GetActiveDapps(db, int64(expiry-1), true)
assert.NoError(t, err)
assert.Equal(t, 1, len(dapps))
assert.Equal(t, sessions[0].URL, dapps[0].URL)
assert.Equal(t, sessions[0].Name, dapps[0].Name)
assert.Equal(t, sessions[0].IconURL, dapps[0].IconURL)
}
type typedDataParams struct {
chainID int
skipField bool
excludeChainID bool
wrongContractType bool
}
func generateTypedDataJson(p typedDataParams) string {
optionalKeyValueField := ""
if !p.skipField {
if p.wrongContractType {
optionalKeyValueField = `,"verifyingContract": true`
} else {
optionalKeyValueField = `,"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"`
}
}
chainIDSchemeEntry := ""
chainIDDataEntry := ""
if !p.excludeChainID {
chainIDSchemeEntry = `{"name": "chainId", "type": "uint256"},`
chainIDDataEntry = `,"chainId": ` + strconv.Itoa(p.chainID)
}
typedData := `{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
` + chainIDSchemeEntry + `
{"name": "verifyingContract", "type": "address"}
],
"Person": [
{"name": "name", "type": "string"},
{"name": "wallet", "type": "address"}
],
"Mail": [
{"name": "from", "type": "Person"},
{"name": "to", "type": "Person"},
{"name": "contents", "type": "string"}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1"
` + chainIDDataEntry + `
` + optionalKeyValueField + `
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}`
return typedData
}
func TestSafeSignTypedDataForDApps(t *testing.T) {
// 0x4f1B9Ee595bF612480ADAF623Ec583f623ae802d
privateKey, err := crypto.HexToECDSA("efe79ae971aa8bb612de9de7c65b9224ab1b6a69e6ec733ec92110f100c7244a")
require.NoError(t, err)
type args struct {
typedJson string
privateKey *ecdsa.PrivateKey
chainID uint64
legacy bool
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "sign_typed_data",
args: args{
typedJson: generateTypedDataJson(typedDataParams{
chainID: 1,
}),
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: false,
},
{
name: "sign_typed_data_legacy",
args: args{
typedJson: generateTypedDataJson(typedDataParams{
chainID: 1,
}),
privateKey: privateKey,
chainID: 1,
legacy: true,
},
wantErr: false,
},
{
name: "sign_typed_data_invalid_json",
args: args{
typedJson: generateTypedDataJson(typedDataParams{
chainID: 1,
wrongContractType: true,
}),
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: true,
},
{
name: "sign_typed_data_invalid_json_legacy",
args: args{
typedJson: `{"invalid": "json"`,
privateKey: privateKey,
chainID: 1,
legacy: true,
},
wantErr: true,
},
{
name: "sign_typed_data_invalid_chain_id",
args: args{
typedJson: generateTypedDataJson(typedDataParams{
chainID: 1,
}),
privateKey: privateKey,
chainID: 2,
legacy: false,
},
wantErr: true,
},
{
name: "sign_typed_data_missing_field",
args: args{
typedJson: generateTypedDataJson(typedDataParams{
chainID: 1,
skipField: true,
}),
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: true,
},
{
name: "sign_typed_data_exclude_chain_id",
args: args{
typedJson: generateTypedDataJson(typedDataParams{
chainID: 1,
excludeChainID: true,
}),
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SafeSignTypedDataForDApps(tt.args.typedJson, tt.args.privateKey, tt.args.chainID, tt.args.legacy)
if (err != nil) != tt.wantErr {
t.Errorf("SafeSignTypedDataForDApps() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
require.NotEmpty(t, got)
require.Len(t, got, 65)
}
})
}
}