github.com/status-im/status-go@v1.1.0/services/wallet/walletconnect/walletconnect.go (about)

     1  package walletconnect
     2  
     3  import (
     4  	"crypto/ecdsa"
     5  	"database/sql"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"math/big"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/ethereum/go-ethereum/common/hexutil"
    15  	"github.com/ethereum/go-ethereum/log"
    16  	signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
    17  
    18  	"github.com/status-im/status-go/eth-node/types"
    19  	"github.com/status-im/status-go/multiaccounts/accounts"
    20  	"github.com/status-im/status-go/params"
    21  	"github.com/status-im/status-go/services/typeddata"
    22  	"github.com/status-im/status-go/services/wallet/walletevent"
    23  )
    24  
    25  const (
    26  	SupportedEip155Namespace = "eip155"
    27  
    28  	ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
    29  )
    30  
    31  var (
    32  	ErrorInvalidSessionProposal = errors.New("invalid session proposal")
    33  	ErrorNamespaceNotSupported  = errors.New("namespace not supported")
    34  	ErrorChainsNotSupported     = errors.New("chains not supported")
    35  	ErrorInvalidParamsCount     = errors.New("invalid params count")
    36  	ErrorInvalidAddressMsgIndex = errors.New("invalid address and/or msg index (must be 0 or 1)")
    37  	ErrorMethodNotSupported     = errors.New("method not supported")
    38  )
    39  
    40  type Topic string
    41  
    42  type Namespace struct {
    43  	Methods  []string `json:"methods"`
    44  	Chains   []string `json:"chains"` // CAIP-2 format e.g. ["eip155:1"]
    45  	Events   []string `json:"events"`
    46  	Accounts []string `json:"accounts,omitempty"` // CAIP-10 format e.g. ["eip155:1:0x453...228"]
    47  }
    48  
    49  type Metadata struct {
    50  	Description string   `json:"description"`
    51  	URL         string   `json:"url"`
    52  	Icons       []string `json:"icons"`
    53  	Name        string   `json:"name"`
    54  	VerifyURL   string   `json:"verifyUrl"`
    55  }
    56  
    57  type Proposer struct {
    58  	PublicKey string   `json:"publicKey"`
    59  	Metadata  Metadata `json:"metadata"`
    60  }
    61  
    62  type Verified struct {
    63  	VerifyURL  string `json:"verifyUrl"`
    64  	Validation string `json:"validation"`
    65  	Origin     string `json:"origin"`
    66  	IsScam     bool   `json:"isScam,omitempty"`
    67  }
    68  
    69  type VerifyContext struct {
    70  	Verified Verified `json:"verified"`
    71  }
    72  
    73  // Params has RequiredNamespaces entries if part of "proposal namespace" and Namespaces entries if part of "session namespace"
    74  // see https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
    75  type Params struct {
    76  	ID                 int64                `json:"id"`
    77  	PairingTopic       Topic                `json:"pairingTopic"`
    78  	Expiry             int64                `json:"expiry"`
    79  	RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
    80  	OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
    81  	Proposer           Proposer             `json:"proposer"`
    82  	Verify             VerifyContext        `json:"verifyContext"`
    83  }
    84  
    85  type SessionProposal struct {
    86  	ID     int64  `json:"id"`
    87  	Params Params `json:"params"`
    88  }
    89  
    90  type PairSessionResponse struct {
    91  	SessionProposal     SessionProposal      `json:"sessionProposal"`
    92  	SupportedNamespaces map[string]Namespace `json:"supportedNamespaces"`
    93  }
    94  
    95  type RequestParams struct {
    96  	Request struct {
    97  		Method string            `json:"method"`
    98  		Params []json.RawMessage `json:"params"`
    99  	} `json:"request"`
   100  	ChainID string `json:"chainId"`
   101  }
   102  
   103  type SessionRequest struct {
   104  	ID     int64         `json:"id"`
   105  	Topic  Topic         `json:"topic"`
   106  	Params RequestParams `json:"params"`
   107  	Verify VerifyContext `json:"verifyContext"`
   108  }
   109  
   110  type SessionDelete struct {
   111  	ID    int64 `json:"id"`
   112  	Topic Topic `json:"topic"`
   113  }
   114  
   115  type Session struct {
   116  	Acknowledged       bool                 `json:"acknowledged"`
   117  	Controller         string               `json:"controller"`
   118  	Expiry             int64                `json:"expiry"`
   119  	Namespaces         map[string]Namespace `json:"namespaces"`
   120  	OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
   121  	PairingTopic       Topic                `json:"pairingTopic"`
   122  	Peer               Proposer             `json:"peer"`
   123  	Relay              json.RawMessage      `json:"relay"`
   124  	RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
   125  	Self               Proposer             `json:"self"`
   126  	Topic              Topic                `json:"topic"`
   127  }
   128  
   129  // Valid namespace
   130  func (n *Namespace) Valid(namespaceName string, chainID *uint64) bool {
   131  	if chainID == nil {
   132  		if len(n.Chains) == 0 {
   133  			log.Warn("namespace doesn't refer to any chain")
   134  			return false
   135  		}
   136  		for _, caip2Str := range n.Chains {
   137  			resolvedNamespaceName, _, err := parseCaip2ChainID(caip2Str)
   138  			if err != nil {
   139  				log.Warn("namespace chain not in caip2 format", "chain", caip2Str, "error", err)
   140  				return false
   141  			}
   142  
   143  			if resolvedNamespaceName != namespaceName {
   144  				log.Warn("namespace name doesn't match", "namespace", namespaceName, "chain", caip2Str)
   145  				return false
   146  			}
   147  		}
   148  	}
   149  	return true
   150  }
   151  
   152  // ValidateForProposal validates params part of the Proposal Namespace
   153  func (p *Params) ValidateForProposal() bool {
   154  	for key, ns := range p.RequiredNamespaces {
   155  		var chainID *uint64
   156  		if strings.Contains(key, ":") {
   157  			resolvedNamespaceName, cID, err := parseCaip2ChainID(key)
   158  			if err != nil {
   159  				log.Warn("params validation failed CAIP-2", "str", key, "error", err)
   160  				return false
   161  			}
   162  			key = resolvedNamespaceName
   163  			chainID = &cID
   164  		}
   165  
   166  		if !isValidNamespaceName(key) {
   167  			log.Warn("invalid namespace name", "namespace", key)
   168  			return false
   169  		}
   170  
   171  		if !ns.Valid(key, chainID) {
   172  			return false
   173  		}
   174  	}
   175  
   176  	return true
   177  }
   178  
   179  // ValidateProposal validates params part of the Proposal Namespace
   180  // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
   181  func (p *SessionProposal) ValidateProposal() bool {
   182  	return p.Params.ValidateForProposal()
   183  }
   184  
   185  // AddSession adds a new active session to the database
   186  func AddSession(db *sql.DB, networks []params.Network, session_json string) error {
   187  	var session Session
   188  	err := json.Unmarshal([]byte(session_json), &session)
   189  	if err != nil {
   190  		return fmt.Errorf("unmarshal session: %v", err)
   191  	}
   192  
   193  	chains := supportedChainsInSession(session)
   194  	testChains, err := areTestChains(networks, chains)
   195  	if err != nil {
   196  		return fmt.Errorf("areTestChains: %v", err)
   197  	}
   198  
   199  	rowEntry := DBSession{
   200  		Topic:            session.Topic,
   201  		Disconnected:     false,
   202  		SessionJSON:      session_json,
   203  		Expiry:           session.Expiry,
   204  		CreatedTimestamp: time.Now().Unix(),
   205  		PairingTopic:     session.PairingTopic,
   206  		TestChains:       testChains,
   207  		DBDApp: DBDApp{
   208  			URL:  session.Peer.Metadata.URL,
   209  			Name: session.Peer.Metadata.Name,
   210  		},
   211  	}
   212  	if len(session.Peer.Metadata.Icons) > 0 {
   213  		rowEntry.IconURL = session.Peer.Metadata.Icons[0]
   214  	}
   215  
   216  	return UpsertSession(db, rowEntry)
   217  }
   218  
   219  // areTestChains assumes chains to tests are all testnets or all mainnets
   220  func areTestChains(networks []params.Network, chainIDs []uint64) (isTest bool, err error) {
   221  	for _, n := range networks {
   222  		for _, chainID := range chainIDs {
   223  			if n.ChainID == chainID {
   224  				return n.IsTest, nil
   225  			}
   226  		}
   227  	}
   228  
   229  	return false, fmt.Errorf("no network found for chainIDs %v", chainIDs)
   230  }
   231  
   232  func supportedChainsInSession(session Session) []uint64 {
   233  	caipChains := session.Namespaces[SupportedEip155Namespace].Chains
   234  	chains := make([]uint64, 0, len(caipChains))
   235  	for _, caip2Str := range caipChains {
   236  		_, chainID, err := parseCaip2ChainID(caip2Str)
   237  		if err != nil {
   238  			log.Warn("Failed parsing CAIP-2", "str", caip2Str, "error", err)
   239  			continue
   240  		}
   241  
   242  		chains = append(chains, chainID)
   243  	}
   244  	return chains
   245  }
   246  
   247  func caip10Accounts(accounts []*accounts.Account, chains []uint64) []string {
   248  	addresses := make([]string, 0, len(accounts)*len(chains))
   249  	for _, acc := range accounts {
   250  		for _, chainID := range chains {
   251  			addresses = append(addresses, fmt.Sprintf("%s:%s:%s", SupportedEip155Namespace, strconv.FormatUint(chainID, 10), acc.Address.Hex()))
   252  		}
   253  	}
   254  	return addresses
   255  }
   256  
   257  func SafeSignTypedDataForDApps(typedJson string, privateKey *ecdsa.PrivateKey, chainID uint64, legacy bool) (types.HexBytes, error) {
   258  	// Parse the data for both legacy and non-legacy cases to validate the chain
   259  	var typed typeddata.TypedData
   260  	err := json.Unmarshal([]byte(typedJson), &typed)
   261  	if err != nil {
   262  		return types.HexBytes{}, err
   263  	}
   264  
   265  	chain := new(big.Int).SetUint64(chainID)
   266  
   267  	var sig hexutil.Bytes
   268  	if legacy {
   269  		sig, err = typeddata.Sign(typed, privateKey, chain)
   270  	} else {
   271  		// Validate chainID if part of the typed data
   272  		if _, exist := typed.Domain[typeddata.ChainIDKey]; exist {
   273  			if err := typed.ValidateChainID(chain); err != nil {
   274  				return types.HexBytes{}, err
   275  			}
   276  		}
   277  
   278  		var typedV4 signercore.TypedData
   279  		err = json.Unmarshal([]byte(typedJson), &typedV4)
   280  		if err != nil {
   281  			return types.HexBytes{}, err
   282  		}
   283  
   284  		sig, err = typeddata.SignTypedDataV4(typedV4, privateKey, chain)
   285  	}
   286  	if err != nil {
   287  		return types.HexBytes{}, err
   288  	}
   289  
   290  	return types.HexBytes(sig), err
   291  }