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"
|
||||
],
|
||||
"go.testTags": "gowaku_skip_migrations,gowaku_no_rln",
|
||||
"cSpell.words": [
|
||||
"unmarshalling"
|
||||
],
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ type Account struct {
|
|||
Hidden bool `json:"hidden"`
|
||||
Clock uint64 `json:"clock,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"`
|
||||
Position int64 `json:"position"`
|
||||
ProdPreferredChainIDs string `json:"prodPreferredChainIds"`
|
||||
|
|
|
@ -608,6 +608,7 @@ func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int
|
|||
return client.ChainID(ctx)
|
||||
}
|
||||
|
||||
// WCPairSessionProposal responds to "session_proposal" event
|
||||
func (api *API) WCPairSessionProposal(ctx context.Context, sessionProposalJSON string) (*wc.PairSessionResponse, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, feed)
|
||||
walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, transactor, gethManager, feed)
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package walletconnect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
@ -39,3 +40,23 @@ func parseCaip2ChainID(str string) (uint64, error) {
|
|||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/params"
|
||||
"github.com/status-im/status-go/rpc/network"
|
||||
"github.com/status-im/status-go/transactions"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
networkManager *network.Manager
|
||||
accountsDB *accounts.Database
|
||||
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{
|
||||
networkManager: networkManager,
|
||||
accountsDB: accountsDB,
|
||||
eventFeed: eventFeed,
|
||||
transactor: transactor,
|
||||
gethManager: gethManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) {
|
||||
namespace := Namespace{
|
||||
Methods: []string{"eth_sendTransaction", "personal_sign"},
|
||||
Methods: []string{params.SendTransactionMethodName, params.PersonalSignMethodName},
|
||||
Events: []string{"accountsChanged", "chainChanged"},
|
||||
}
|
||||
|
||||
|
@ -31,16 +42,26 @@ func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionRes
|
|||
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, 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)
|
||||
|
||||
// TODO #12434: respond async
|
||||
|
@ -51,3 +72,17 @@ func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionRes
|
|||
},
|
||||
}, 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
|
@ -14,6 +15,8 @@ import (
|
|||
const ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
|
||||
|
||||
var ErrorChainsNotSupported = errors.New("chains not supported")
|
||||
var ErrorInvalidParamsCount = errors.New("invalid params count")
|
||||
var ErrorMethodNotSupported = errors.New("method not supported")
|
||||
|
||||
type Namespace struct {
|
||||
Methods []string `json:"methods"`
|
||||
|
@ -40,14 +43,13 @@ type Namespaces struct {
|
|||
// We ignore non ethereum namespaces
|
||||
}
|
||||
|
||||
type Verified struct {
|
||||
type VerifyContext struct {
|
||||
Verified struct {
|
||||
VerifyURL string `json:"verifyUrl"`
|
||||
Validation string `json:"validation"`
|
||||
Origin string `json:"origin"`
|
||||
}
|
||||
|
||||
type VerifyContext struct {
|
||||
Verified Verified `json:"verified"`
|
||||
IsScam bool `json:"isScam,omitempty"`
|
||||
} `json:"verified"`
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
|
@ -57,7 +59,7 @@ type Params struct {
|
|||
RequiredNamespaces Namespaces `json:"requiredNamespaces"`
|
||||
OptionalNamespaces Namespaces `json:"optionalNamespaces"`
|
||||
Proposer Proposer `json:"proposer"`
|
||||
VerifyContext VerifyContext `json:"verifyContext"`
|
||||
Verify VerifyContext `json:"verifyContext"`
|
||||
}
|
||||
|
||||
type SessionProposal struct {
|
||||
|
@ -70,6 +72,26 @@ type PairSessionResponse struct {
|
|||
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) {
|
||||
chains = make([]uint64, 0, 1)
|
||||
eipChains = make([]string, 0, 1)
|
||||
|
|
|
@ -219,7 +219,6 @@ func (api *API) web3AccResponse(request Web3SendAsyncReadOnlyRequest) (*Web3Send
|
|||
result = dappsAddress
|
||||
} else {
|
||||
result = []types.Address{dappsAddress}
|
||||
|
||||
}
|
||||
|
||||
return &Web3SendAsyncReadOnlyResponse{
|
||||
|
|
Loading…
Reference in New Issue