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 }