feat(walletconnect)_: support for session proposal for wc 2.0

This commit is contained in:
Sale Djenic 2023-11-24 16:27:05 +01:00 committed by saledjenic
parent e32c5546e1
commit d4ca8616fc
7 changed files with 389 additions and 132 deletions

View File

@ -1 +1 @@
0.171.17
0.171.18

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
@ -26,18 +27,19 @@ import (
// log.Debug("wallet.api.wc RESPONSE", "eventType", eventType, "error", err, "payload.len", len(payload), "sentCount", sentCount)
// }
func parseCaip2ChainID(str string) (uint64, error) {
// 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")
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 "", 0, fmt.Errorf("CAIP-2 second value not valid Chain ID: %w", err)
}
return chainID, nil
return caip2[0], chainID, nil
}
// JSONProxyType provides a generic way of changing the JSON value before unmarshalling it into the target.
@ -59,3 +61,11 @@ func (b *JSONProxyType) UnmarshalJSON(input []byte) error {
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

@ -1,60 +1,66 @@
package walletconnect
import "testing"
import (
"strings"
"testing"
)
func Test_parseCaip2ChainID(t *testing.T) {
func Test_parseCaip2(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want uint64
wantErr bool
name string
args args
wantChain uint64
wantErr bool
}{
{
name: "valid",
args: args{
str: "eip155:5",
},
want: 5,
wantErr: false,
wantChain: 5,
wantErr: false,
},
{
name: "invalid_number",
args: args{
str: "eip155:5a",
},
want: 0,
wantErr: true,
wantChain: 0,
wantErr: true,
},
{
name: "invalid_caip2_too_many",
args: args{
str: "eip155:1:5",
},
want: 0,
wantErr: true,
wantChain: 0,
wantErr: true,
},
{
name: "invalid_caip2_not_enough",
args: args{
str: "eip1551",
},
want: 0,
wantErr: true,
wantChain: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseCaip2ChainID(tt.args.str)
gotNamespaceName, gotChainID, 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)
if !tt.wantErr && !strings.Contains(tt.args.str, gotNamespaceName) {
t.Errorf("parseCaip2ChainID() = %v, doesn't match %v", gotNamespaceName, tt.args.str)
}
if gotChainID != tt.wantChain {
t.Errorf("parseCaip2ChainID() = %v, want %v", gotChainID, tt.wantChain)
}
})
}

View File

@ -85,7 +85,7 @@ func (s *Service) buildTransaction(request SessionRequest) (response *SessionReq
return nil, err
}
chainID, err := parseCaip2ChainID(request.Params.ChainID)
_, chainID, err := parseCaip2ChainID(request.Params.ChainID)
if err != nil {
return nil, err
}

View File

@ -3,6 +3,7 @@ package walletconnect
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
@ -66,45 +67,79 @@ func (s *Service) SendTransaction(signature string) (response *SessionRequestRes
}
func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) {
namespace := Namespace{
Methods: []string{params.SendTransactionMethodName, params.PersonalSignMethodName},
Events: []string{"accountsChanged", "chainChanged"},
if !proposal.Valid() {
return nil, ErrorInvalidSessionProposal
}
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) {
log.Warn("Some chains are not supported; wanted: ", proposedChains, "; supported: ", chains)
return nil, ErrorChainsNotSupported
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
}
}
namespace.Chains = eipChains
activeAccounts, err := s.accountsDB.GetActiveAccounts()
if err != nil {
return nil, fmt.Errorf("failed to get active accounts: %w", err)
}
// Filter out non-own accounts
usableAccounts := make([]*accounts.Account, 0, 1)
allWalletAccountsReadyForTransaction := make([]*accounts.Account, 0, 1)
for _, acc := range activeAccounts {
if !acc.IsWalletAccountReadyForTransaction() {
continue
}
usableAccounts = append(usableAccounts, acc)
allWalletAccountsReadyForTransaction = append(allWalletAccountsReadyForTransaction, acc)
}
addresses := activeToOwnedAccounts(usableAccounts)
namespace.Accounts = caip10Accounts(addresses, chains)
result := &PairSessionResponse{
SessionProposal: proposal,
SupportedNamespaces: map[string]Namespace{
SupportedEip155Namespace: Namespace{
Methods: []string{params.SendTransactionMethodName,
params.PersonalSignMethodName,
},
Events: []string{"accountsChanged", "chainChanged"},
Chains: eipChains,
Accounts: caip10Accounts(allWalletAccountsReadyForTransaction, chains),
},
},
}
// TODO #12434: respond async
return &PairSessionResponse{
SessionProposal: proposal,
SupportedNamespaces: Namespaces{
Eip155: namespace,
},
}, nil
return result, nil
}
func (s *Service) RecordSuccessfulPairing(proposal SessionProposal) error {

View File

@ -3,7 +3,9 @@ package walletconnect
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/log"
@ -12,11 +14,20 @@ import (
"github.com/status-im/status-go/services/wallet/walletevent"
)
const ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
const (
SupportedEip155Namespace = "eip155"
var ErrorChainsNotSupported = errors.New("chains not supported")
var ErrorInvalidParamsCount = errors.New("invalid params count")
var ErrorMethodNotSupported = errors.New("method not supported")
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
@ -40,11 +51,6 @@ type Proposer struct {
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"`
@ -57,13 +63,13 @@ type VerifyContext struct {
}
type Params struct {
ID int64 `json:"id"`
PairingTopic Topic `json:"pairingTopic"`
Expiry int64 `json:"expiry"`
RequiredNamespaces Namespaces `json:"requiredNamespaces"`
OptionalNamespaces Namespaces `json:"optionalNamespaces"`
Proposer Proposer `json:"proposer"`
Verify VerifyContext `json:"verifyContext"`
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 {
@ -72,8 +78,8 @@ type SessionProposal struct {
}
type PairSessionResponse struct {
SessionProposal SessionProposal `json:"sessionProposal"`
SupportedNamespaces Namespaces `json:"supportedNamespaces"`
SessionProposal SessionProposal `json:"sessionProposal"`
SupportedNamespaces map[string]Namespace `json:"supportedNamespaces"`
}
type RequestParams struct {
@ -105,11 +111,67 @@ type SessionRequestResponse struct {
SignedMessage interface{} `json:"signedMessage,omitempty"`
}
// 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)
_, chainID, err := parseCaip2ChainID(caip2Str)
if err != nil {
log.Warn("Failed parsing CAIP-2", "str", caip2Str, "error", err)
continue
@ -125,22 +187,12 @@ func sessionProposalToSupportedChain(caipChains []string, supportsChain func(uin
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)
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 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

@ -4,10 +4,194 @@ import (
"reflect"
"testing"
"encoding/json"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/stretchr/testify/assert"
)
func Test_sessionProposalValidity(t *testing.T) {
tests := []struct {
name string
sessionProposalJSON string
expectedValidity bool
}{
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#11-proposal-namespaces-does-not-include-an-optional-namespace
{
name: "proposal-namespaces-does-not-include-an-optional-namespace",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155:10": {
"methods": ["personal_sign"],
"events": ["accountsChanged", "chainChanged"]
}
}
}
}`,
expectedValidity: true,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#12-proposal-namespaces-must-not-have-chains-empty
{
name: "proposal-namespaces-must-not-have-chains-empty",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"cosmos": {
"chains": [],
"methods": ["cosmos_signDirect"],
"events": ["someCosmosEvent"]
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index
{
name: "chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1", "eip155:137"],
"methods": ["eth_sendTransaction", "eth_signTransaction", "eth_sign"],
"events": ["accountsChanged", "chainChanged"]
},
"eip155:10": {
"methods": ["personal_sign"],
"events": ["accountsChanged", "chainChanged"]
}
}
}
}`,
expectedValidity: true,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#14-chains-must-be-caip-2-compliant
{
name: "chains-must-be-caip-2-compliant",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["42"],
"methods": ["eth_sign"],
"events": ["accountsChanged"]
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#15-proposal-namespace-methods-and-events-may-be-empty
{
name: "proposal-namespace-methods-and-events-may-be-empty",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1"],
"methods": [],
"events": []
}
}
}
}`,
expectedValidity: true,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#16-all-chains-in-the-namespace-must-contain-the-namespace-prefix
{
name: "all-chains-in-the-namespace-must-contain-the-namespace-prefix",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1", "eip155:137", "cosmos:cosmoshub-4"],
"methods": ["eth_sendTransaction"],
"events": ["accountsChanged", "chainChanged"]
}
},
"optionalNamespaces": {
"eip155:42161": {
"methods": ["personal_sign"],
"events": ["accountsChanged", "chainChanged"]
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#17-namespace-key-must-comply-with-caip-2-specification
{
name: "namespace-key-must-comply-with-caip-2-specification",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"": {
"chains": [":1"],
"methods": ["personalSign"],
"events": []
},
"**": {
"chains": ["**:1"],
"methods": ["personalSign"],
"events": []
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#18-all-namespaces-must-be-valid
{
name: "all-namespaces-must-be-valid",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {
"eip155": {
"chains": ["eip155:1"],
"methods": ["personalSign"],
"events": []
},
"cosmos": {
"chains": [],
"methods": [],
"events": []
}
}
}
}`,
expectedValidity: false,
},
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#19-proposal-namespaces-may-be-empty
{
name: "proposal-namespaces-may-be-empty",
sessionProposalJSON: `{
"params": {
"requiredNamespaces": {}
}
}`,
expectedValidity: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var sessionProposal SessionProposal
err := json.Unmarshal([]byte(tt.sessionProposalJSON), &sessionProposal)
assert.NoError(t, err)
if tt.expectedValidity {
assert.True(t, sessionProposal.Valid())
} else {
assert.False(t, sessionProposal.Valid())
}
})
}
}
func Test_sessionProposalToSupportedChain(t *testing.T) {
type args struct {
chains []string
@ -66,52 +250,10 @@ func Test_sessionProposalToSupportedChain(t *testing.T) {
}
}
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
accounts []*accounts.Account
chains []uint64
}
tests := []struct {
name string
@ -121,9 +263,15 @@ func Test_caip10Accounts(t *testing.T) {
{
name: "generate_caip10_accounts",
args: args{
addresses: []types.Address{
types.HexToAddress("0x1"),
types.HexToAddress("0x2"),
accounts: []*accounts.Account{
{
Address: types.HexToAddress("0x1"),
Type: accounts.AccountTypeWatch,
},
{
Address: types.HexToAddress("0x2"),
Type: accounts.AccountTypeSeed,
},
},
chains: []uint64{1, 2},
},
@ -137,17 +285,23 @@ func Test_caip10Accounts(t *testing.T) {
{
name: "empty_addresses",
args: args{
addresses: []types.Address{},
chains: []uint64{1, 2},
accounts: []*accounts.Account{},
chains: []uint64{1, 2},
},
want: []string{},
},
{
name: "empty_chains",
args: args{
addresses: []types.Address{
types.HexToAddress("0x1"),
types.HexToAddress("0x2"),
accounts: []*accounts.Account{
{
Address: types.HexToAddress("0x1"),
Type: accounts.AccountTypeWatch,
},
{
Address: types.HexToAddress("0x2"),
Type: accounts.AccountTypeSeed,
},
},
chains: []uint64{},
},
@ -156,7 +310,7 @@ func Test_caip10Accounts(t *testing.T) {
}
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) {
if got := caip10Accounts(tt.args.accounts, tt.args.chains); !reflect.DeepEqual(got, tt.want) {
t.Errorf("caip10Accounts() = %v, want %v", got, tt.want)
}
})