parent
c0b0bdc8fe
commit
36da204282
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
"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/token"
|
||||||
"github.com/status-im/status-go/services/wallet/transfer"
|
"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/services/wallet/walletevent"
|
||||||
"github.com/status-im/status-go/transactions"
|
"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) {
|
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)
|
rpcClient, err := gethrpc.Dial(rpcURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -635,3 +636,15 @@ func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int
|
||||||
client := ethclient.NewClient(rpcClient)
|
client := ethclient.NewClient(rpcClient)
|
||||||
return client.ChainID(ctx)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
|
"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/token"
|
||||||
"github.com/status-im/status-go/services/wallet/transfer"
|
"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/services/wallet/walletevent"
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
)
|
)
|
||||||
|
@ -136,6 +137,8 @@ func NewService(
|
||||||
|
|
||||||
activity := activity.NewService(db, tokenManager, collectiblesManager, feed)
|
activity := activity.NewService(db, tokenManager, collectiblesManager, feed)
|
||||||
|
|
||||||
|
walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, feed)
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
accountsDB: accountsDB,
|
accountsDB: accountsDB,
|
||||||
|
@ -163,6 +166,7 @@ func NewService(
|
||||||
decoder: NewDecoder(),
|
decoder: NewDecoder(),
|
||||||
blockChainState: blockChainState,
|
blockChainState: blockChainState,
|
||||||
keycardPairings: NewKeycardPairings(),
|
keycardPairings: NewKeycardPairings(),
|
||||||
|
walletConnect: walletconnect,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +199,7 @@ type Service struct {
|
||||||
decoder *Decoder
|
decoder *Decoder
|
||||||
blockChainState *BlockChainState
|
blockChainState *BlockChainState
|
||||||
keycardPairings *KeycardPairings
|
keycardPairings *KeycardPairings
|
||||||
|
walletConnect *walletconnect.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start signals transmitter.
|
// Start signals transmitter.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue