feat(wallet) add WalletConnect pair API

Updates #12551
This commit is contained in:
Stefan 2023-10-30 18:58:57 +02:00 committed by Stefan Dunca
parent c0b0bdc8fe
commit 36da204282
7 changed files with 449 additions and 1 deletions

View File

@ -26,6 +26,7 @@ import (
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
wc "github.com/status-im/status-go/services/wallet/walletconnect"
"github.com/status-im/status-go/services/wallet/walletevent"
"github.com/status-im/status-go/transactions"
)
@ -626,7 +627,7 @@ func (api *API) GetActivityCollectiblesAsync(requestID int32, chainIDs []wcommon
}
func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int, error) {
log.Debug("wallet.api.VerifyURL", rpcURL)
log.Debug("wallet.api.VerifyURL", "rpcURL", rpcURL)
rpcClient, err := gethrpc.Dial(rpcURL)
if err != nil {
@ -635,3 +636,15 @@ func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int
client := ethclient.NewClient(rpcClient)
return client.ChainID(ctx)
}
func (api *API) WCPairSessionProposal(ctx context.Context, sessionProposalJSON string) (*wc.PairSessionResponse, error) {
log.Debug("wallet.api.wc.PairSessionProposal", "proposal.len", len(sessionProposalJSON))
var data wc.SessionProposal
err := json.Unmarshal([]byte(sessionProposalJSON), &data)
if err != nil {
return nil, err
}
return api.s.walletConnect.PairSessionProposal(data)
}

View File

@ -31,6 +31,7 @@ import (
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletconnect"
"github.com/status-im/status-go/services/wallet/walletevent"
"github.com/status-im/status-go/transactions"
)
@ -136,6 +137,8 @@ func NewService(
activity := activity.NewService(db, tokenManager, collectiblesManager, feed)
walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, feed)
return &Service{
db: db,
accountsDB: accountsDB,
@ -163,6 +166,7 @@ func NewService(
decoder: NewDecoder(),
blockChainState: blockChainState,
keycardPairings: NewKeycardPairings(),
walletConnect: walletconnect,
}
}
@ -195,6 +199,7 @@ type Service struct {
decoder *Decoder
blockChainState *BlockChainState
keycardPairings *KeycardPairings
walletConnect *walletconnect.Service
}
// Start signals transmitter.

View File

@ -0,0 +1,41 @@
package walletconnect
import (
"errors"
"fmt"
"strconv"
"strings"
)
// TODO #12434: respond async
// 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
// }
// log.Debug("wallet.api.wc RESPONSE", "eventType", eventType, "error", err, "payload.len", len(payload))
// event := walletevent.Event{
// Type: eventType,
// Message: string(payload),
// }
// eventFeed.Send(event)
// }
func parseCaip2ChainID(str 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 chainID, nil
}

View File

@ -0,0 +1,61 @@
package walletconnect
import "testing"
func Test_parseCaip2ChainID(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want uint64
wantErr bool
}{
{
name: "valid",
args: args{
str: "eip155:5",
},
want: 5,
wantErr: false,
},
{
name: "invalid_number",
args: args{
str: "eip155:5a",
},
want: 0,
wantErr: true,
},
{
name: "invalid_caip2_too_many",
args: args{
str: "eip155:1:5",
},
want: 0,
wantErr: true,
},
{
name: "invalid_caip2_not_enough",
args: args{
str: "eip1551",
},
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseCaip2ChainID(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("parseCaip2ChainID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseCaip2ChainID() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,53 @@
package walletconnect
import (
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/rpc/network"
)
type Service struct {
networkManager *network.Manager
accountsDB *accounts.Database
eventFeed *event.Feed
}
func NewService(networkManager *network.Manager, accountsDB *accounts.Database, eventFeed *event.Feed) *Service {
return &Service{
networkManager: networkManager,
accountsDB: accountsDB,
eventFeed: eventFeed,
}
}
func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) {
namespace := Namespace{
Methods: []string{"eth_sendTransaction", "personal_sign"},
Events: []string{"accountsChanged", "chainChanged"},
}
proposedChains := proposal.Params.RequiredNamespaces.Eip155.Chains
chains, eipChains := sessionProposalToSupportedChain(proposedChains, func(chainID uint64) bool {
return s.networkManager.Find(chainID) != nil
})
if len(chains) != len(proposedChains) {
return nil, ErrorChainsNotSupported
}
namespace.Chains = eipChains
activeAccounts, err := s.accountsDB.GetActiveAccounts()
if err != nil {
return nil, ErrorChainsNotSupported
}
addresses := activeToOwnedAccounts(activeAccounts)
namespace.Accounts = caip10Accounts(addresses, chains)
// TODO #12434: respond async
return &PairSessionResponse{
SessionProposal: proposal,
SupportedNamespaces: Namespaces{
Eip155: namespace,
},
}, nil
}

View File

@ -0,0 +1,111 @@
package walletconnect
import (
"errors"
"strconv"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/services/wallet/walletevent"
)
const ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
var ErrorChainsNotSupported = errors.New("chains not supported")
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 Namespaces struct {
Eip155 Namespace `json:"eip155"`
// We ignore non ethereum namespaces
}
type Verified struct {
VerifyURL string `json:"verifyUrl"`
Validation string `json:"validation"`
Origin string `json:"origin"`
}
type VerifyContext struct {
Verified Verified `json:"verified"`
}
type Params struct {
ID int64 `json:"id"`
PairingTopic string `json:"pairingTopic"`
Expiry int64 `json:"expiry"`
RequiredNamespaces Namespaces `json:"requiredNamespaces"`
OptionalNamespaces Namespaces `json:"optionalNamespaces"`
Proposer Proposer `json:"proposer"`
VerifyContext VerifyContext `json:"verifyContext"`
}
type SessionProposal struct {
ID uint64 `json:"id"`
Params Params `json:"params"`
}
type PairSessionResponse struct {
SessionProposal SessionProposal `json:"sessionProposal"`
SupportedNamespaces Namespaces `json:"supportedNamespaces"`
}
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 activeToOwnedAccounts(activeAccounts []*accounts.Account) []types.Address {
addresses := make([]types.Address, 0, 1)
for _, account := range activeAccounts {
if account.Type != accounts.AccountTypeWatch {
addresses = append(addresses, account.Address)
}
}
return addresses
}
func caip10Accounts(addresses []types.Address, chains []uint64) []string {
accounts := make([]string, 0, len(addresses)*len(chains))
for _, address := range addresses {
for _, chainID := range chains {
accounts = append(accounts, "eip155:"+strconv.FormatUint(chainID, 10)+":"+address.Hex())
}
}
return accounts
}

View File

@ -0,0 +1,164 @@
package walletconnect
import (
"reflect"
"testing"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
)
func Test_sessionProposalToSupportedChain(t *testing.T) {
type args struct {
chains []string
supportsChain func(uint64) bool
}
tests := []struct {
name string
args args
wantChains []uint64
wantEipChains []string
}{
{
name: "filter_out_unsupported_chains_and_invalid_chains",
args: args{
chains: []string{"eip155:1", "eip155:3", "eip155:invalid"},
supportsChain: func(chainID uint64) bool {
return chainID == 1
},
},
wantChains: []uint64{1},
wantEipChains: []string{"eip155:1"},
},
{
name: "no_supported_chains",
args: args{
chains: []string{"eip155:3", "eip155:5"},
supportsChain: func(chainID uint64) bool {
return false
},
},
wantChains: []uint64{},
wantEipChains: []string{},
},
{
name: "empty_proposal",
args: args{
chains: []string{},
supportsChain: func(chainID uint64) bool {
return true
},
},
wantChains: []uint64{},
wantEipChains: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotChains, gotEipChains := sessionProposalToSupportedChain(tt.args.chains, tt.args.supportsChain)
if !reflect.DeepEqual(gotChains, tt.wantChains) {
t.Errorf("sessionProposalToSupportedChain() gotChains = %v, want %v", gotChains, tt.wantChains)
}
if !reflect.DeepEqual(gotEipChains, tt.wantEipChains) {
t.Errorf("sessionProposalToSupportedChain() gotEipChains = %v, want %v", gotEipChains, tt.wantEipChains)
}
})
}
}
func Test_activeToOwnedAccounts(t *testing.T) {
type args struct {
activeAccounts []*accounts.Account
}
tests := []struct {
name string
args args
want []types.Address
}{
{
name: "filter_out_watch_accounts",
args: args{
activeAccounts: []*accounts.Account{
{
Address: types.HexToAddress("0x1"),
Type: accounts.AccountTypeWatch,
},
{
Address: types.HexToAddress("0x2"),
Type: accounts.AccountTypeSeed,
},
{
Address: types.HexToAddress("0x3"),
Type: accounts.AccountTypeSeed,
},
},
},
want: []types.Address{
types.HexToAddress("0x2"),
types.HexToAddress("0x3"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := activeToOwnedAccounts(tt.args.activeAccounts); !reflect.DeepEqual(got, tt.want) {
t.Errorf("activeToOwnedAccounts() = %v, want %v", got, tt.want)
}
})
}
}
func Test_caip10Accounts(t *testing.T) {
type args struct {
addresses []types.Address
chains []uint64
}
tests := []struct {
name string
args args
want []string
}{
{
name: "generate_caip10_accounts",
args: args{
addresses: []types.Address{
types.HexToAddress("0x1"),
types.HexToAddress("0x2"),
},
chains: []uint64{1, 2},
},
want: []string{
"eip155:1:0x0000000000000000000000000000000000000001",
"eip155:2:0x0000000000000000000000000000000000000001",
"eip155:1:0x0000000000000000000000000000000000000002",
"eip155:2:0x0000000000000000000000000000000000000002",
},
},
{
name: "empty_addresses",
args: args{
addresses: []types.Address{},
chains: []uint64{1, 2},
},
want: []string{},
},
{
name: "empty_chains",
args: args{
addresses: []types.Address{
types.HexToAddress("0x1"),
types.HexToAddress("0x2"),
},
chains: []uint64{},
},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := caip10Accounts(tt.args.addresses, tt.args.chains); !reflect.DeepEqual(got, tt.want) {
t.Errorf("caip10Accounts() = %v, want %v", got, tt.want)
}
})
}
}