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

288 lines
8.5 KiB
Go

package walletconnect
import (
"crypto/ecdsa"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/services/wallet/walletevent"
)
const (
SupportedEip155Namespace = "eip155"
ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
)
var (
ErrorInvalidSessionProposal = errors.New("invalid session proposal")
ErrorNamespaceNotSupported = errors.New("namespace not supported")
ErrorChainsNotSupported = errors.New("chains not supported")
ErrorInvalidParamsCount = errors.New("invalid params count")
ErrorInvalidAddressMsgIndex = errors.New("invalid address and/or msg index (must be 0 or 1)")
ErrorMethodNotSupported = errors.New("method not supported")
)
type Topic string
type Namespace struct {
Methods []string `json:"methods"`
Chains []string `json:"chains"` // CAIP-2 format e.g. ["eip155:1"]
Events []string `json:"events"`
Accounts []string `json:"accounts,omitempty"` // CAIP-10 format e.g. ["eip155:1:0x453...228"]
}
type Metadata struct {
Description string `json:"description"`
URL string `json:"url"`
Icons []string `json:"icons"`
Name string `json:"name"`
VerifyURL string `json:"verifyUrl"`
}
type Proposer struct {
PublicKey string `json:"publicKey"`
Metadata Metadata `json:"metadata"`
}
type Verified struct {
VerifyURL string `json:"verifyUrl"`
Validation string `json:"validation"`
Origin string `json:"origin"`
IsScam bool `json:"isScam,omitempty"`
}
type VerifyContext struct {
Verified Verified `json:"verified"`
}
// Params has RequiredNamespaces entries if part of "proposal namespace" and Namespaces entries if part of "session namespace"
// see https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
type Params struct {
ID int64 `json:"id"`
PairingTopic Topic `json:"pairingTopic"`
Expiry int64 `json:"expiry"`
RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
Proposer Proposer `json:"proposer"`
Verify VerifyContext `json:"verifyContext"`
}
type SessionProposal struct {
ID int64 `json:"id"`
Params Params `json:"params"`
}
type PairSessionResponse struct {
SessionProposal SessionProposal `json:"sessionProposal"`
SupportedNamespaces map[string]Namespace `json:"supportedNamespaces"`
}
type RequestParams struct {
Request struct {
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
} `json:"request"`
ChainID string `json:"chainId"`
}
type SessionRequest struct {
ID int64 `json:"id"`
Topic Topic `json:"topic"`
Params RequestParams `json:"params"`
Verify VerifyContext `json:"verifyContext"`
}
type SessionDelete struct {
ID int64 `json:"id"`
Topic Topic `json:"topic"`
}
type Session struct {
Acknowledged bool `json:"acknowledged"`
Controller string `json:"controller"`
Expiry int64 `json:"expiry"`
Namespaces map[string]Namespace `json:"namespaces"`
OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
PairingTopic Topic `json:"pairingTopic"`
Peer Proposer `json:"peer"`
Relay json.RawMessage `json:"relay"`
RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
Self Proposer `json:"self"`
Topic Topic `json:"topic"`
}
// Valid namespace
func (n *Namespace) Valid(namespaceName string, chainID *uint64) bool {
if chainID == nil {
if len(n.Chains) == 0 {
log.Warn("namespace doesn't refer to any chain")
return false
}
for _, caip2Str := range n.Chains {
resolvedNamespaceName, _, err := parseCaip2ChainID(caip2Str)
if err != nil {
log.Warn("namespace chain not in caip2 format", "chain", caip2Str, "error", err)
return false
}
if resolvedNamespaceName != namespaceName {
log.Warn("namespace name doesn't match", "namespace", namespaceName, "chain", caip2Str)
return false
}
}
}
return true
}
// ValidateForProposal validates params part of the Proposal Namespace
func (p *Params) ValidateForProposal() bool {
for key, ns := range p.RequiredNamespaces {
var chainID *uint64
if strings.Contains(key, ":") {
resolvedNamespaceName, cID, err := parseCaip2ChainID(key)
if err != nil {
log.Warn("params validation failed CAIP-2", "str", key, "error", err)
return false
}
key = resolvedNamespaceName
chainID = &cID
}
if !isValidNamespaceName(key) {
log.Warn("invalid namespace name", "namespace", key)
return false
}
if !ns.Valid(key, chainID) {
return false
}
}
return true
}
// ValidateProposal validates params part of the Proposal Namespace
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
func (p *SessionProposal) ValidateProposal() bool {
return p.Params.ValidateForProposal()
}
// AddSession adds a new active session to the database
func AddSession(db *sql.DB, networks []params.Network, session_json string) error {
var session Session
err := json.Unmarshal([]byte(session_json), &session)
if err != nil {
return fmt.Errorf("unmarshal session: %v", err)
}
chains := supportedChainsInSession(session)
testChains, err := areTestChains(networks, chains)
if err != nil {
return fmt.Errorf("areTestChains: %v", err)
}
rowEntry := DBSession{
Topic: session.Topic,
Disconnected: false,
SessionJSON: session_json,
Expiry: session.Expiry,
CreatedTimestamp: time.Now().Unix(),
PairingTopic: session.PairingTopic,
TestChains: testChains,
DBDApp: DBDApp{
URL: session.Peer.Metadata.URL,
Name: session.Peer.Metadata.Name,
},
}
if len(session.Peer.Metadata.Icons) > 0 {
rowEntry.IconURL = session.Peer.Metadata.Icons[0]
}
return UpsertSession(db, rowEntry)
}
// areTestChains assumes chains to tests are all testnets or all mainnets
func areTestChains(networks []params.Network, chainIDs []uint64) (isTest bool, err error) {
for _, n := range networks {
for _, chainID := range chainIDs {
if n.ChainID == chainID {
return n.IsTest, nil
}
}
}
return false, fmt.Errorf("no network found for chainIDs %v", chainIDs)
}
func supportedChainsInSession(session Session) []uint64 {
caipChains := session.Namespaces[SupportedEip155Namespace].Chains
chains := make([]uint64, 0, len(caipChains))
for _, caip2Str := range caipChains {
_, chainID, err := parseCaip2ChainID(caip2Str)
if err != nil {
log.Warn("Failed parsing CAIP-2", "str", caip2Str, "error", err)
continue
}
chains = append(chains, chainID)
}
return chains
}
func caip10Accounts(accounts []*accounts.Account, chains []uint64) []string {
addresses := make([]string, 0, len(accounts)*len(chains))
for _, acc := range accounts {
for _, chainID := range chains {
addresses = append(addresses, fmt.Sprintf("%s:%s:%s", SupportedEip155Namespace, strconv.FormatUint(chainID, 10), acc.Address.Hex()))
}
}
return addresses
}
func SafeSignTypedDataForDApps(typedJson string, privateKey *ecdsa.PrivateKey, chainID uint64, legacy bool) (types.HexBytes, error) {
// Parse the data for both legacy and non-legacy cases to validate the chain
var typed typeddata.TypedData
err := json.Unmarshal([]byte(typedJson), &typed)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(chainID)
if err := typed.ValidateChainID(chain); err != nil {
return types.HexBytes{}, err
}
var sig hexutil.Bytes
if legacy {
sig, err = typeddata.Sign(typed, privateKey, chain)
} else {
var typedV4 signercore.TypedData
err = json.Unmarshal([]byte(typedJson), &typedV4)
if err != nil {
return types.HexBytes{}, err
}
sig, err = typeddata.SignTypedDataV4(typedV4, privateKey, chain)
}
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}