feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions

View File

@@ -0,0 +1,196 @@
package walletconnect
import (
"database/sql"
"errors"
)
type DbSession struct {
Topic Topic `json:"topic"`
PairingTopic Topic `json:"pairingTopic"`
Expiry int64 `json:"expiry"`
Active bool `json:"active"`
DappName string `json:"dappName"`
DappURL string `json:"dappUrl"`
DappDescription string `json:"dappDescription"`
DappIcon string `json:"dappIcon"`
DappVerifyURL string `json:"dappVerifyUrl"`
DappPublicKey string `json:"dappPublicKey"`
}
func UpsertSession(db *sql.DB, session DbSession) error {
insertSQL := `
INSERT OR IGNORE INTO
wallet_connect_sessions (topic, pairing_topic, expiry, active)
VALUES
(?, ?, ?, ?);
UPDATE
wallet_connect_sessions
SET
pairing_topic = ?,
expiry = ?,
active = ?,
dapp_name = ?,
dapp_url = ?,
dapp_description = ?,
dapp_icon = ?,
dapp_verify_url = ?,
dapp_publicKey = ?
WHERE
topic = ?;`
_, err := db.Exec(insertSQL,
session.Topic,
session.PairingTopic,
session.Expiry,
session.Active,
session.PairingTopic,
session.Expiry,
session.Active,
session.DappName,
session.DappURL,
session.DappDescription,
session.DappIcon,
session.DappVerifyURL,
session.DappPublicKey,
session.Topic,
)
return err
}
func ChangeSessionState(db *sql.DB, topic Topic, active bool) error {
stmt, err := db.Prepare("UPDATE wallet_connect_sessions SET active = ? WHERE topic = ?")
if err != nil {
return err
}
res, err := stmt.Exec(active, topic)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.New("unable to locate session for DB state change")
}
return nil
}
func GetSessionByTopic(db *sql.DB, topic Topic) (*DbSession, error) {
querySQL := `
SELECT *
FROM
wallet_connect_sessions
WHERE
topic = ?`
row := db.QueryRow(querySQL, topic)
var session DbSession
err := row.Scan(&session.Topic,
&session.PairingTopic,
&session.Expiry,
&session.Active,
&session.DappName,
&session.DappURL,
&session.DappDescription,
&session.DappIcon,
&session.DappVerifyURL,
&session.DappPublicKey)
if err != nil {
return nil, err
}
return &session, nil
}
func GetSessionsByPairingTopic(db *sql.DB, pairingTopic Topic) ([]DbSession, error) {
querySQL := `
SELECT *
FROM
wallet_connect_sessions
WHERE
pairing_topic = ?`
rows, err := db.Query(querySQL, pairingTopic)
if err != nil {
return nil, err
}
defer rows.Close()
sessions := make([]DbSession, 0, 2)
for rows.Next() {
var session DbSession
err := rows.Scan(&session.Topic,
&session.PairingTopic,
&session.Expiry,
&session.Active,
&session.DappName,
&session.DappURL,
&session.DappDescription,
&session.DappIcon,
&session.DappVerifyURL,
&session.DappPublicKey)
if err != nil {
return nil, err
}
sessions = append(sessions, session)
}
if err := rows.Err(); err != nil {
return nil, err
}
return sessions, nil
}
// GetActiveSessions returns all active sessions (active and not expired) that have an expiry timestamp newer or equal to the given timestamp.
func GetActiveSessions(db *sql.DB, expiryNotOlderThanTimestamp int64) ([]DbSession, error) {
querySQL := `
SELECT *
FROM
wallet_connect_sessions
WHERE
active != 0 AND
expiry >= ?
ORDER BY
expiry DESC`
rows, err := db.Query(querySQL, expiryNotOlderThanTimestamp)
if err != nil {
return nil, err
}
defer rows.Close()
sessions := make([]DbSession, 0, 2)
for rows.Next() {
var session DbSession
err := rows.Scan(&session.Topic,
&session.PairingTopic,
&session.Expiry,
&session.Active,
&session.DappName,
&session.DappURL,
&session.DappDescription,
&session.DappIcon,
&session.DappVerifyURL,
&session.DappPublicKey)
if err != nil {
return nil, err
}
sessions = append(sessions, session)
}
if err := rows.Err(); err != nil {
return nil, err
}
return sessions, nil
}

View File

@@ -0,0 +1,71 @@
package walletconnect
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// func sendResponseEvent(eventFeed *event.Feed, eventType walletevent.EventType, payloadObj interface{}, resErr error) {
// payload, err := json.Marshal(payloadObj)
// if err != nil {
// log.Error("Error marshaling WC response: %v; result error: %w", err, resErr)
// } else {
// err = resErr
// }
// event := walletevent.Event{
// Type: eventType,
// Message: string(payload),
// }
// sentCount := eventFeed.Send(event)
// log.Debug("wallet.api.wc RESPONSE", "eventType", eventType, "error", err, "payload.len", len(payload), "sentCount", sentCount)
// }
// Returns namspace name, chainID and error
func parseCaip2ChainID(str string) (string, uint64, error) {
caip2 := strings.Split(str, ":")
if len(caip2) != 2 {
return "", 0, errors.New("CAIP-2 string is not valid")
}
chainIDStr := caip2[1]
chainID, err := strconv.ParseUint(chainIDStr, 10, 64)
if err != nil {
return "", 0, fmt.Errorf("CAIP-2 second value not valid Chain ID: %w", err)
}
return caip2[0], chainID, nil
}
// JSONProxyType provides a generic way of changing the JSON value before unmarshalling it into the target.
// transform function is called before unmarshalling.
type JSONProxyType struct {
target interface{}
transform func([]byte) ([]byte, error)
}
func (b *JSONProxyType) UnmarshalJSON(input []byte) error {
if b.transform == nil {
return errors.New("transform function is not set")
}
output, err := b.transform(input)
if err != nil {
return err
}
return json.Unmarshal(output, b.target)
}
func isValidNamespaceName(namespaceName string) bool {
pattern := "^[a-z0-9-]{3,8}$"
regex := regexp.MustCompile(pattern)
return regex.MatchString(namespaceName)
}

View File

@@ -0,0 +1,138 @@
package walletconnect
import (
"encoding/json"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/transactions"
)
// sendTransactionParams instead of transactions.SendTxArgs to allow parsing of hex Uint64 with leading 0 ("0x01") and empty hex value ("0x")
type sendTransactionParams struct {
transactions.SendTxArgs
Nonce JSONProxyType `json:"nonce"`
Gas JSONProxyType `json:"gas"`
GasPrice JSONProxyType `json:"gasPrice"`
Value JSONProxyType `json:"value"`
MaxFeePerGas JSONProxyType `json:"maxFeePerGas"`
MaxPriorityFeePerGas JSONProxyType `json:"maxPriorityFeePerGas"`
}
func (n *sendTransactionParams) UnmarshalJSON(data []byte) error {
// Avoid recursion
type Alias sendTransactionParams
var alias Alias
// Fix hex values with leading 0 or empty
fixWCHexValues := func(input []byte) ([]byte, error) {
hexStr := string(input)
if !strings.HasPrefix(hexStr, "\"0x") {
return input, nil
}
trimmedStr := strings.TrimPrefix(hexStr, "\"0x")
fixedStrNoPrefix := strings.TrimLeft(trimmedStr, "0")
fixedStr := "\"0x" + fixedStrNoPrefix
if fixedStr == "\"0x\"" {
fixedStr = "\"0x0\""
}
return []byte(fixedStr), nil
}
alias.Nonce = JSONProxyType{target: &alias.SendTxArgs.Nonce, transform: fixWCHexValues}
alias.Gas = JSONProxyType{target: &alias.SendTxArgs.Gas, transform: fixWCHexValues}
alias.GasPrice = JSONProxyType{target: &alias.SendTxArgs.GasPrice, transform: fixWCHexValues}
alias.Value = JSONProxyType{target: &alias.SendTxArgs.Value, transform: fixWCHexValues}
alias.MaxFeePerGas = JSONProxyType{target: &alias.SendTxArgs.MaxFeePerGas, transform: fixWCHexValues}
alias.MaxPriorityFeePerGas = JSONProxyType{target: &alias.SendTxArgs.MaxPriorityFeePerGas, transform: fixWCHexValues}
if err := json.Unmarshal(data, &alias); err != nil {
return err
}
*n = sendTransactionParams(alias)
return nil
}
func (n *sendTransactionParams) MarshalJSON() ([]byte, error) {
return json.Marshal(n.SendTxArgs)
}
func (s *Service) buildTransaction(request SessionRequest) (response *transfer.TxResponse, err error) {
if len(request.Params.Request.Params) != 1 {
return nil, ErrorInvalidParamsCount
}
var params sendTransactionParams
if err = json.Unmarshal(request.Params.Request.Params[0], &params); err != nil {
return nil, err
}
_, chainID, err := parseCaip2ChainID(request.Params.ChainID)
if err != nil {
return nil, err
}
return s.transactionManager.BuildTransaction(chainID, params.SendTxArgs)
}
func (s *Service) buildMessage(request SessionRequest, addressIndex int, messageIndex int,
handleTypedData bool) (response *transfer.TxResponse, err error) {
if len(request.Params.Request.Params) != 2 {
return nil, ErrorInvalidParamsCount
}
if addressIndex > 1 || addressIndex < 0 || messageIndex > 1 || messageIndex < 0 {
return nil, ErrorInvalidAddressMsgIndex
}
var address types.Address
if err := json.Unmarshal(request.Params.Request.Params[addressIndex], &address); err != nil {
return nil, err
}
account, err := s.accountsDB.GetAccountByAddress(address)
if err != nil {
return nil, fmt.Errorf("failed to get active account: %w", err)
}
kp, err := s.accountsDB.GetKeypairByKeyUID(account.KeyUID)
if err != nil {
return nil, err
}
var hash []byte
if !handleTypedData {
var dBytes types.HexBytes
if err := json.Unmarshal(request.Params.Request.Params[messageIndex], &dBytes); err != nil {
return nil, err
}
hash = crypto.TextHash(dBytes)
} else {
var typedDataJSON string
if err := json.Unmarshal(request.Params.Request.Params[messageIndex], &typedDataJSON); err != nil {
return nil, err
}
var typedData apitypes.TypedData
if err := json.Unmarshal([]byte(typedDataJSON), &typedData); err != nil {
return nil, err
}
hash, _, err = apitypes.TypedDataAndHash(typedData)
if err != nil {
return nil, err
}
}
return &transfer.TxResponse{
KeyUID: account.KeyUID,
Address: account.Address,
AddressPath: account.Path,
SignOnKeycard: kp.MigratedToKeycard(),
MessageToSign: types.HexBytes(hash),
}, nil
}

View File

@@ -0,0 +1,197 @@
package walletconnect
import (
"database/sql"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"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/rpc/network"
"github.com/status-im/status-go/services/wallet/transfer"
)
type Service struct {
db *sql.DB
networkManager *network.Manager
accountsDB *accounts.Database
eventFeed *event.Feed
transactionManager *transfer.TransactionManager
gethManager *account.GethManager
config *params.NodeConfig
}
func NewService(db *sql.DB, networkManager *network.Manager, accountsDB *accounts.Database,
transactionManager *transfer.TransactionManager, gethManager *account.GethManager, eventFeed *event.Feed,
config *params.NodeConfig) *Service {
return &Service{
db: db,
networkManager: networkManager,
accountsDB: accountsDB,
eventFeed: eventFeed,
transactionManager: transactionManager,
gethManager: gethManager,
config: config,
}
}
func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) {
if !proposal.Valid() {
return nil, ErrorInvalidSessionProposal
}
var (
chains []uint64
eipChains []string
)
if len(proposal.Params.RequiredNamespaces) == 0 {
// return all we support
allChains, err := s.networkManager.GetAll()
if err != nil {
return nil, fmt.Errorf("failed to get all chains: %w", err)
}
for _, chain := range allChains {
chains = append(chains, chain.ChainID)
eipChains = append(eipChains, fmt.Sprintf("%s:%d", SupportedEip155Namespace, chain.ChainID))
}
} else {
var proposedChains []string
for key, ns := range proposal.Params.RequiredNamespaces {
if !strings.Contains(key, SupportedEip155Namespace) {
log.Warn("Some namespaces are not supported; wanted: ", key, "; supported: ", SupportedEip155Namespace)
return nil, ErrorNamespaceNotSupported
}
if strings.Contains(key, ":") {
proposedChains = append(proposedChains, key)
} else {
proposedChains = append(proposedChains, ns.Chains...)
}
}
chains, eipChains = sessionProposalToSupportedChain(proposedChains, func(chainID uint64) bool {
return s.networkManager.Find(chainID) != nil
})
if len(chains) != len(proposedChains) {
log.Warn("Some chains are not supported; wanted: ", proposedChains, "; supported: ", chains)
return nil, ErrorChainsNotSupported
}
}
activeAccounts, err := s.accountsDB.GetActiveAccounts()
if err != nil {
return nil, fmt.Errorf("failed to get active accounts: %w", err)
}
allWalletAccountsReadyForTransaction := make([]*accounts.Account, 0, 1)
for _, acc := range activeAccounts {
if !acc.IsWalletAccountReadyForTransaction() {
continue
}
allWalletAccountsReadyForTransaction = append(allWalletAccountsReadyForTransaction, acc)
}
result := &PairSessionResponse{
SessionProposal: proposal,
SupportedNamespaces: map[string]Namespace{
SupportedEip155Namespace: Namespace{
Methods: []string{params.SendTransactionMethodName,
params.SendRawTransactionMethodName,
params.PersonalSignMethodName,
params.SignMethodName,
params.SignTransactionMethodName,
params.SignTypedDataMethodName,
params.SignTypedDataV3MethodName,
params.SignTypedDataV4MethodName,
params.WalletSwitchEthereumChainMethodName,
},
Events: []string{"accountsChanged", "chainChanged"},
Chains: eipChains,
Accounts: caip10Accounts(allWalletAccountsReadyForTransaction, chains),
},
},
}
// TODO #12434: respond async
return result, nil
}
func (s *Service) SaveOrUpdateSession(session Session) error {
var icon string
if len(session.Peer.Metadata.Icons) > 0 {
icon = session.Peer.Metadata.Icons[0]
}
return UpsertSession(s.db, DbSession{
Topic: session.Topic,
PairingTopic: session.PairingTopic,
Expiry: session.Expiry,
Active: true,
DappName: session.Peer.Metadata.Name,
DappURL: session.Peer.Metadata.URL,
DappDescription: session.Peer.Metadata.Description,
DappIcon: icon,
DappVerifyURL: session.Peer.Metadata.VerifyURL,
DappPublicKey: session.Peer.PublicKey,
})
}
func (s *Service) ChangeSessionState(topic Topic, active bool) error {
return ChangeSessionState(s.db, topic, active)
}
func (s *Service) SessionRequest(request SessionRequest) (response *transfer.TxResponse, err error) {
// TODO #12434: should we check topic for validity? It might make sense if we
// want to cache the paired sessions
if request.Params.Request.Method == params.SendTransactionMethodName {
return s.buildTransaction(request)
} else if request.Params.Request.Method == params.SignTransactionMethodName {
return s.buildTransaction(request)
} else if request.Params.Request.Method == params.PersonalSignMethodName {
return s.buildMessage(request, 1, 0, false)
} else if request.Params.Request.Method == params.SignMethodName {
return s.buildMessage(request, 0, 1, false)
} else if request.Params.Request.Method == params.SignTypedDataMethodName ||
request.Params.Request.Method == params.SignTypedDataV3MethodName ||
request.Params.Request.Method == params.SignTypedDataV4MethodName {
return s.buildMessage(request, 0, 1, true)
}
// TODO #12434: respond async
return nil, ErrorMethodNotSupported
}
func (s *Service) AuthRequest(address common.Address, authMessage string) (*transfer.TxResponse, error) {
account, err := s.accountsDB.GetAccountByAddress(types.Address(address))
if err != nil {
return nil, fmt.Errorf("failed to get active account: %w", err)
}
kp, err := s.accountsDB.GetKeypairByKeyUID(account.KeyUID)
if err != nil {
return nil, err
}
byteArray := []byte(authMessage)
hash := crypto.TextHash(byteArray)
return &transfer.TxResponse{
KeyUID: account.KeyUID,
Address: account.Address,
AddressPath: account.Path,
SignOnKeycard: kp.MigratedToKeycard(),
MessageToSign: types.HexBytes(hash),
}, nil
}

View File

@@ -0,0 +1,202 @@
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
}