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

203 lines
5.7 KiB
Go

package walletconnect
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/multiaccounts/accounts"
"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"`
}
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
}
// Valid params
func (p *Params) Valid() 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
}
// Valid session propsal
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
func (p *SessionProposal) Valid() bool {
return p.Params.Valid()
}
func sessionProposalToSupportedChain(caipChains []string, supportsChain func(uint64) bool) (chains []uint64, eipChains []string) {
chains = make([]uint64, 0, 1)
eipChains = make([]string, 0, 1)
for _, caip2Str := range caipChains {
_, chainID, err := parseCaip2ChainID(caip2Str)
if err != nil {
log.Warn("Failed parsing CAIP-2", "str", caip2Str, "error", err)
continue
}
if !supportsChain(chainID) {
continue
}
eipChains = append(eipChains, caip2Str)
chains = append(chains, chainID)
}
return
}
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
}