feat(wallet) implement Wallet Connect sign APIs
add WalletConnect support for signing session events - implement `eth_sendTransaction` - implement `personal_sign` Also fix exposing unusable accounts Updates status-desktop #12637
This commit is contained in:
parent
f1d31d6339
commit
b994cedfc3
|
@ -5,4 +5,7 @@
|
||||||
"-w"
|
"-w"
|
||||||
],
|
],
|
||||||
"go.testTags": "gowaku_skip_migrations,gowaku_no_rln",
|
"go.testTags": "gowaku_skip_migrations,gowaku_no_rln",
|
||||||
|
"cSpell.words": [
|
||||||
|
"unmarshalling"
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ type Account struct {
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
Clock uint64 `json:"clock,omitempty"`
|
Clock uint64 `json:"clock,omitempty"`
|
||||||
Removed bool `json:"removed,omitempty"`
|
Removed bool `json:"removed,omitempty"`
|
||||||
Operable AccountOperable `json:"operable"` // describes an account's operability (read an explanation at the top of this file)
|
Operable AccountOperable `json:"operable"` // describes an account's operability (check AccountOperable type constants for details)
|
||||||
CreatedAt int64 `json:"createdAt"`
|
CreatedAt int64 `json:"createdAt"`
|
||||||
Position int64 `json:"position"`
|
Position int64 `json:"position"`
|
||||||
ProdPreferredChainIDs string `json:"prodPreferredChainIds"`
|
ProdPreferredChainIDs string `json:"prodPreferredChainIds"`
|
||||||
|
|
|
@ -608,6 +608,7 @@ func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int
|
||||||
return client.ChainID(ctx)
|
return client.ChainID(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WCPairSessionProposal responds to "session_proposal" event
|
||||||
func (api *API) WCPairSessionProposal(ctx context.Context, sessionProposalJSON string) (*wc.PairSessionResponse, error) {
|
func (api *API) WCPairSessionProposal(ctx context.Context, sessionProposalJSON string) (*wc.PairSessionResponse, error) {
|
||||||
log.Debug("wallet.api.wc.PairSessionProposal", "proposal.len", len(sessionProposalJSON))
|
log.Debug("wallet.api.wc.PairSessionProposal", "proposal.len", len(sessionProposalJSON))
|
||||||
|
|
||||||
|
@ -619,3 +620,16 @@ func (api *API) WCPairSessionProposal(ctx context.Context, sessionProposalJSON s
|
||||||
|
|
||||||
return api.s.walletConnect.PairSessionProposal(data)
|
return api.s.walletConnect.PairSessionProposal(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WCSessionRequest responds to "session_request" event
|
||||||
|
func (api *API) WCSessionRequest(ctx context.Context, sessionRequestJSON string, hashedPassword string) (response *wc.SessionRequestResponse, err error) {
|
||||||
|
log.Debug("wallet.api.wc.SessionRequest", "request.len", len(sessionRequestJSON), "hashedPassword.len", len(hashedPassword))
|
||||||
|
|
||||||
|
var request wc.SessionRequest
|
||||||
|
err = json.Unmarshal([]byte(sessionRequestJSON), &request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.s.walletConnect.SessionRequest(request, hashedPassword)
|
||||||
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ func NewService(
|
||||||
|
|
||||||
activity := activity.NewService(db, tokenManager, collectiblesManager, feed)
|
activity := activity.NewService(db, tokenManager, collectiblesManager, feed)
|
||||||
|
|
||||||
walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, feed)
|
walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, transactor, gethManager, feed)
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package walletconnect
|
package walletconnect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -39,3 +40,23 @@ func parseCaip2ChainID(str string) (uint64, error) {
|
||||||
}
|
}
|
||||||
return chainID, nil
|
return 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)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package walletconnect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/eth-node/crypto"
|
||||||
|
"github.com/status-im/status-go/eth-node/types"
|
||||||
|
"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) sendTransaction(request SessionRequest, hashedPassword string) (response *SessionRequestResponse, err error) {
|
||||||
|
if len(request.Params.Request.Params) != 1 {
|
||||||
|
return nil, ErrorInvalidParamsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
var params sendTransactionParams
|
||||||
|
if err := json.Unmarshal(request.Params.Request.Params[0], ¶ms); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := s.gethManager.GetVerifiedWalletAccount(s.accountsDB, params.From.Hex(), hashedPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: export it as a JSON parsable type
|
||||||
|
chainID, err := parseCaip2ChainID(request.Params.ChainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := s.transactor.SendTransactionWithChainID(chainID, params.SendTxArgs, acc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SessionRequestResponse{
|
||||||
|
SessionRequest: request,
|
||||||
|
Signed: hash.Bytes(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) personalSign(request SessionRequest, hashedPassword string) (response *SessionRequestResponse, err error) {
|
||||||
|
if len(request.Params.Request.Params) != 2 {
|
||||||
|
return nil, ErrorInvalidParamsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
var address types.Address
|
||||||
|
if err := json.Unmarshal(request.Params.Request.Params[1], &address); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := s.gethManager.GetVerifiedWalletAccount(s.accountsDB, address.Hex(), hashedPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dBytes types.HexBytes
|
||||||
|
if err := json.Unmarshal(request.Params.Request.Params[0], &dBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := crypto.TextHash(dBytes)
|
||||||
|
|
||||||
|
sig, err := crypto.Sign(hash, acc.AccountKey.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||||
|
|
||||||
|
return &SessionRequestResponse{
|
||||||
|
SessionRequest: request,
|
||||||
|
Signed: types.HexBytes(sig),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -1,28 +1,39 @@
|
||||||
package walletconnect
|
package walletconnect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"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/multiaccounts/accounts"
|
"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/rpc/network"
|
||||||
|
"github.com/status-im/status-go/transactions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
networkManager *network.Manager
|
networkManager *network.Manager
|
||||||
accountsDB *accounts.Database
|
accountsDB *accounts.Database
|
||||||
eventFeed *event.Feed
|
eventFeed *event.Feed
|
||||||
|
|
||||||
|
transactor *transactions.Transactor
|
||||||
|
gethManager *account.GethManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(networkManager *network.Manager, accountsDB *accounts.Database, eventFeed *event.Feed) *Service {
|
func NewService(networkManager *network.Manager, accountsDB *accounts.Database, transactor *transactions.Transactor, gethManager *account.GethManager, eventFeed *event.Feed) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
networkManager: networkManager,
|
networkManager: networkManager,
|
||||||
accountsDB: accountsDB,
|
accountsDB: accountsDB,
|
||||||
eventFeed: eventFeed,
|
eventFeed: eventFeed,
|
||||||
|
transactor: transactor,
|
||||||
|
gethManager: gethManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) {
|
func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) {
|
||||||
namespace := Namespace{
|
namespace := Namespace{
|
||||||
Methods: []string{"eth_sendTransaction", "personal_sign"},
|
Methods: []string{params.SendTransactionMethodName, params.PersonalSignMethodName},
|
||||||
Events: []string{"accountsChanged", "chainChanged"},
|
Events: []string{"accountsChanged", "chainChanged"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,16 +42,26 @@ func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionRes
|
||||||
return s.networkManager.Find(chainID) != nil
|
return s.networkManager.Find(chainID) != nil
|
||||||
})
|
})
|
||||||
if len(chains) != len(proposedChains) {
|
if len(chains) != len(proposedChains) {
|
||||||
|
log.Warn("Some chains are not supported; wanted: ", proposedChains, "; supported: ", chains)
|
||||||
return nil, ErrorChainsNotSupported
|
return nil, ErrorChainsNotSupported
|
||||||
}
|
}
|
||||||
namespace.Chains = eipChains
|
namespace.Chains = eipChains
|
||||||
|
|
||||||
activeAccounts, err := s.accountsDB.GetActiveAccounts()
|
activeAccounts, err := s.accountsDB.GetActiveAccounts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrorChainsNotSupported
|
return nil, fmt.Errorf("failed to get active accounts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses := activeToOwnedAccounts(activeAccounts)
|
// Filter out non-own accounts
|
||||||
|
usableAccounts := make([]*accounts.Account, 0, 1)
|
||||||
|
for _, acc := range activeAccounts {
|
||||||
|
if !acc.IsOwnAccount() || acc.Operable != accounts.AccountFullyOperable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usableAccounts = append(usableAccounts, acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses := activeToOwnedAccounts(usableAccounts)
|
||||||
namespace.Accounts = caip10Accounts(addresses, chains)
|
namespace.Accounts = caip10Accounts(addresses, chains)
|
||||||
|
|
||||||
// TODO #12434: respond async
|
// TODO #12434: respond async
|
||||||
|
@ -51,3 +72,17 @@ func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionRes
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SessionRequest(request SessionRequest, hashedPassword string) (response *SessionRequestResponse, 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.sendTransaction(request, hashedPassword)
|
||||||
|
} else if request.Params.Request.Method == params.PersonalSignMethodName {
|
||||||
|
return s.personalSign(request, hashedPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO #12434: respond async
|
||||||
|
return nil, ErrorMethodNotSupported
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package walletconnect
|
package walletconnect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -14,6 +15,8 @@ import (
|
||||||
const ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
|
const ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
|
||||||
|
|
||||||
var ErrorChainsNotSupported = errors.New("chains not supported")
|
var ErrorChainsNotSupported = errors.New("chains not supported")
|
||||||
|
var ErrorInvalidParamsCount = errors.New("invalid params count")
|
||||||
|
var ErrorMethodNotSupported = errors.New("method not supported")
|
||||||
|
|
||||||
type Namespace struct {
|
type Namespace struct {
|
||||||
Methods []string `json:"methods"`
|
Methods []string `json:"methods"`
|
||||||
|
@ -40,14 +43,13 @@ type Namespaces struct {
|
||||||
// We ignore non ethereum namespaces
|
// We ignore non ethereum namespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
type Verified struct {
|
|
||||||
VerifyURL string `json:"verifyUrl"`
|
|
||||||
Validation string `json:"validation"`
|
|
||||||
Origin string `json:"origin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VerifyContext struct {
|
type VerifyContext struct {
|
||||||
Verified Verified `json:"verified"`
|
Verified struct {
|
||||||
|
VerifyURL string `json:"verifyUrl"`
|
||||||
|
Validation string `json:"validation"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
IsScam bool `json:"isScam,omitempty"`
|
||||||
|
} `json:"verified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Params struct {
|
type Params struct {
|
||||||
|
@ -57,7 +59,7 @@ type Params struct {
|
||||||
RequiredNamespaces Namespaces `json:"requiredNamespaces"`
|
RequiredNamespaces Namespaces `json:"requiredNamespaces"`
|
||||||
OptionalNamespaces Namespaces `json:"optionalNamespaces"`
|
OptionalNamespaces Namespaces `json:"optionalNamespaces"`
|
||||||
Proposer Proposer `json:"proposer"`
|
Proposer Proposer `json:"proposer"`
|
||||||
VerifyContext VerifyContext `json:"verifyContext"`
|
Verify VerifyContext `json:"verifyContext"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionProposal struct {
|
type SessionProposal struct {
|
||||||
|
@ -70,6 +72,26 @@ type PairSessionResponse struct {
|
||||||
SupportedNamespaces Namespaces `json:"supportedNamespaces"`
|
SupportedNamespaces Namespaces `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 string `json:"topic"`
|
||||||
|
Params RequestParams `json:"params"`
|
||||||
|
Verify VerifyContext `json:"verifyContext"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionRequestResponse struct {
|
||||||
|
SessionRequest SessionRequest `json:"sessionRequest"`
|
||||||
|
Signed types.HexBytes `json:"signed"`
|
||||||
|
}
|
||||||
|
|
||||||
func sessionProposalToSupportedChain(caipChains []string, supportsChain func(uint64) bool) (chains []uint64, eipChains []string) {
|
func sessionProposalToSupportedChain(caipChains []string, supportsChain func(uint64) bool) (chains []uint64, eipChains []string) {
|
||||||
chains = make([]uint64, 0, 1)
|
chains = make([]uint64, 0, 1)
|
||||||
eipChains = make([]string, 0, 1)
|
eipChains = make([]string, 0, 1)
|
||||||
|
|
|
@ -219,7 +219,6 @@ func (api *API) web3AccResponse(request Web3SendAsyncReadOnlyRequest) (*Web3Send
|
||||||
result = dappsAddress
|
result = dappsAddress
|
||||||
} else {
|
} else {
|
||||||
result = []types.Address{dappsAddress}
|
result = []types.Address{dappsAddress}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Web3SendAsyncReadOnlyResponse{
|
return &Web3SendAsyncReadOnlyResponse{
|
||||||
|
|
Loading…
Reference in New Issue