306 lines
8.8 KiB
Go
306 lines
8.8 KiB
Go
package walletconnect
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
|
|
|
|
"github.com/status-im/status-go/eth-node/types"
|
|
"github.com/status-im/status-go/logutils"
|
|
"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 {
|
|
logutils.ZapLogger().Warn("namespace doesn't refer to any chain")
|
|
return false
|
|
}
|
|
for _, caip2Str := range n.Chains {
|
|
resolvedNamespaceName, _, err := parseCaip2ChainID(caip2Str)
|
|
if err != nil {
|
|
logutils.ZapLogger().Warn("namespace chain not in caip2 format",
|
|
zap.String("chain", caip2Str),
|
|
zap.Error(err),
|
|
)
|
|
return false
|
|
}
|
|
|
|
if resolvedNamespaceName != namespaceName {
|
|
logutils.ZapLogger().Warn("namespace name doesn't match",
|
|
zap.String("namespace", namespaceName),
|
|
zap.String("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 {
|
|
logutils.ZapLogger().Warn("params validation failed CAIP-2",
|
|
zap.String("str", key),
|
|
zap.Error(err),
|
|
)
|
|
return false
|
|
}
|
|
key = resolvedNamespaceName
|
|
chainID = &cID
|
|
}
|
|
|
|
if !isValidNamespaceName(key) {
|
|
logutils.ZapLogger().Warn("invalid namespace name", zap.String("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 {
|
|
logutils.ZapLogger().Warn("Failed parsing CAIP-2",
|
|
zap.String("str", caip2Str),
|
|
zap.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)
|
|
|
|
var sig hexutil.Bytes
|
|
if legacy {
|
|
sig, err = typeddata.Sign(typed, privateKey, chain)
|
|
} else {
|
|
// Validate chainID if part of the typed data
|
|
if _, exist := typed.Domain[typeddata.ChainIDKey]; exist {
|
|
if err := typed.ValidateChainID(chain); err != nil {
|
|
return types.HexBytes{}, err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|