diff --git a/VERSION b/VERSION index 0274a8c22..6751d8d8a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.171.15 \ No newline at end of file +0.171.16 \ No newline at end of file diff --git a/multiaccounts/accounts/account_test.go b/multiaccounts/accounts/account_test.go index 7bdf31fc5..ffa2a9264 100644 --- a/multiaccounts/accounts/account_test.go +++ b/multiaccounts/accounts/account_test.go @@ -10,7 +10,7 @@ import ( ) func TestIsOwnAccount(t *testing.T) { - account := Account{Wallet: true} + account := Account{Wallet: true, Type: AccountTypeGenerated} require.True(t, account.IsWalletNonWatchOnlyAccount()) account = Account{ diff --git a/multiaccounts/accounts/database.go b/multiaccounts/accounts/database.go index e9447302f..14564a957 100644 --- a/multiaccounts/accounts/database.go +++ b/multiaccounts/accounts/database.go @@ -116,7 +116,7 @@ const ( // Returns true if an account is a wallet account that logged in user has a control over, otherwise returns false. func (a *Account) IsWalletNonWatchOnlyAccount() bool { - return !a.Chat && a.Type != AccountTypeWatch + return !a.Chat && len(a.Type) > 0 && a.Type != AccountTypeWatch } // Returns true if an account is a wallet account that is ready for sending transactions, otherwise returns false. diff --git a/services/wallet/api.go b/services/wallet/api.go index f3bb8d57d..cd184267c 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -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" + "github.com/status-im/status-go/services/wallet/walletconnect" 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" @@ -608,6 +609,18 @@ func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int return client.ChainID(ctx) } +func (api *API) WCSignMessage(ctx context.Context, message types.HexBytes, address common.Address, password string) (string, error) { + log.Debug("wallet.api.wc.SignMessage", "message", message, "address", address, "password", password) + + return api.s.walletConnect.SignMessage(message, address, password) +} + +func (api *API) WCSendTransaction(signature string) (response *walletconnect.SessionRequestResponse, err error) { + log.Debug("wallet.api.wc.SendTransaction", "signature", signature) + + return api.s.walletConnect.SendTransaction(signature) +} + // 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)) @@ -622,8 +635,8 @@ func (api *API) WCPairSessionProposal(ctx context.Context, sessionProposalJSON s } // 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)) +func (api *API) WCSessionRequest(ctx context.Context, sessionRequestJSON string) (response *wc.SessionRequestResponse, err error) { + log.Debug("wallet.api.wc.SessionRequest", "request.len", len(sessionRequestJSON)) var request wc.SessionRequest err = json.Unmarshal([]byte(sessionRequestJSON), &request) @@ -631,5 +644,5 @@ func (api *API) WCSessionRequest(ctx context.Context, sessionRequestJSON string, return nil, err } - return api.s.walletConnect.SessionRequest(request, hashedPassword) + return api.s.walletConnect.SessionRequest(request) } diff --git a/services/wallet/service.go b/services/wallet/service.go index 8960ceac1..2299d8354 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -137,7 +137,7 @@ func NewService( activity := activity.NewService(db, tokenManager, collectiblesManager, feed) - walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, transactor, gethManager, feed) + walletconnect := walletconnect.NewService(rpcClient.NetworkManager, accountsDB, transactor, gethManager, feed, config) return &Service{ db: db, diff --git a/services/wallet/walletconnect/rpc.go b/services/wallet/walletconnect/rpc.go index d6ecc7e46..176d154a6 100644 --- a/services/wallet/walletconnect/rpc.go +++ b/services/wallet/walletconnect/rpc.go @@ -1,9 +1,15 @@ package walletconnect import ( + "encoding/hex" "encoding/json" + "errors" + "fmt" + "math/big" "strings" + "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" "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" @@ -58,39 +64,75 @@ func (n *sendTransactionParams) MarshalJSON() ([]byte, error) { return json.Marshal(n.SendTxArgs) } -func (s *Service) sendTransaction(request SessionRequest, hashedPassword string) (response *SessionRequestResponse, err error) { +func (s *Service) buildTransaction(request SessionRequest) (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 { + 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) + account, err := s.accountsDB.GetAccountByAddress(params.From) + if err != nil { + return nil, fmt.Errorf("failed to get active account: %w", err) + } + + kp, err := s.accountsDB.GetKeypairByKeyUID(account.KeyUID) 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) + // In this case we can ignore `unlock` function received from `ValidateAndBuildTransaction` cause `Nonce` + // will be always set by the initiator of this transaction (by the dapp). + // Though we will need sort out completely that part since Nonce kept in the local cache is not the most recent one, + // instead of that we should always ask network what's the most recent known Nonce for the account. + // Logged issue to handle that: https://github.com/status-im/status-go/issues/4335 + txBeingSigned, _, err := s.transactor.ValidateAndBuildTransaction(chainID, params.SendTxArgs) + if err != nil { + return nil, err + } + + s.txSignDetails = &txSigningDetails{ + from: common.Address(account.Address), + chainID: chainID, + txBeingSigned: txBeingSigned, + } + + signer := ethTypes.NewLondonSigner(new(big.Int).SetUint64(s.txSignDetails.chainID)) + return &SessionRequestResponse{ + KeyUID: account.KeyUID, + Address: account.Address, + AddressPath: account.Path, + SignOnKeycard: kp.MigratedToKeycard(), + MesageToSign: signer.Hash(s.txSignDetails.txBeingSigned), + }, nil +} + +func (s *Service) sendTransaction(signature string) (response *SessionRequestResponse, err error) { + if s.txSignDetails.txBeingSigned == nil { + return response, errors.New("no tx to sign") + } + + signatureBytes, _ := hex.DecodeString(signature) + + hash, err := s.transactor.SendBuiltTransactionWithSignature(s.txSignDetails.chainID, s.txSignDetails.txBeingSigned, signatureBytes) if err != nil { return nil, err } return &SessionRequestResponse{ - SessionRequest: request, - Signed: hash.Bytes(), + SignedMessage: hash, }, nil } -func (s *Service) personalSign(request SessionRequest, hashedPassword string) (response *SessionRequestResponse, err error) { +func (s *Service) buildPersonalSingMessage(request SessionRequest) (response *SessionRequestResponse, err error) { if len(request.Params.Request.Params) != 2 { return nil, ErrorInvalidParamsCount } @@ -100,7 +142,12 @@ func (s *Service) personalSign(request SessionRequest, hashedPassword string) (r return nil, err } - acc, err := s.gethManager.GetVerifiedWalletAccount(s.accountsDB, address.Hex(), hashedPassword) + account, err := s.accountsDB.GetAccountByAddress(address) + if err != nil { + return nil, fmt.Errorf("failed to get active account: %w", err) + } + + kp, err := s.accountsDB.GetKeypairByKeyUID(account.KeyUID) if err != nil { return nil, err } @@ -112,15 +159,11 @@ func (s *Service) personalSign(request SessionRequest, hashedPassword string) (r 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), + KeyUID: account.KeyUID, + Address: account.Address, + AddressPath: account.Path, + SignOnKeycard: kp.MigratedToKeycard(), + MesageToSign: types.HexBytes(hash), }, nil } diff --git a/services/wallet/walletconnect/service.go b/services/wallet/walletconnect/service.go index 1fe5542be..eb79efc16 100644 --- a/services/wallet/walletconnect/service.go +++ b/services/wallet/walletconnect/service.go @@ -3,15 +3,25 @@ package walletconnect import ( "fmt" + "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" "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/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" "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 txSigningDetails struct { + chainID uint64 + from common.Address + txBeingSigned *ethTypes.Transaction +} + type Service struct { networkManager *network.Manager accountsDB *accounts.Database @@ -19,18 +29,38 @@ type Service struct { transactor *transactions.Transactor gethManager *account.GethManager + + config *params.NodeConfig + txSignDetails *txSigningDetails } -func NewService(networkManager *network.Manager, accountsDB *accounts.Database, transactor *transactions.Transactor, gethManager *account.GethManager, eventFeed *event.Feed) *Service { +func NewService(networkManager *network.Manager, accountsDB *accounts.Database, transactor *transactions.Transactor, + gethManager *account.GethManager, eventFeed *event.Feed, config *params.NodeConfig) *Service { return &Service{ networkManager: networkManager, accountsDB: accountsDB, eventFeed: eventFeed, transactor: transactor, gethManager: gethManager, + config: config, } } +func (s *Service) SignMessage(message types.HexBytes, address common.Address, password string) (string, error) { + selectedAccount, err := s.gethManager.VerifyAccountPassword(s.config.KeyStoreDir, address.Hex(), password) + if err != nil { + return "", err + } + + signature, err := crypto.Sign(message[:], selectedAccount.PrivateKey) + + return types.EncodeHex(signature), err +} + +func (s *Service) SendTransaction(signature string) (response *SessionRequestResponse, err error) { + return s.sendTransaction(signature) +} + func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionResponse, error) { namespace := Namespace{ Methods: []string{params.SendTransactionMethodName, params.PersonalSignMethodName}, @@ -55,7 +85,7 @@ func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionRes // Filter out non-own accounts usableAccounts := make([]*accounts.Account, 0, 1) for _, acc := range activeAccounts { - if !acc.IsOwnAccount() || acc.Operable != accounts.AccountFullyOperable { + if !acc.IsWalletAccountReadyForTransaction() { continue } usableAccounts = append(usableAccounts, acc) @@ -73,14 +103,14 @@ func (s *Service) PairSessionProposal(proposal SessionProposal) (*PairSessionRes }, nil } -func (s *Service) SessionRequest(request SessionRequest, hashedPassword string) (response *SessionRequestResponse, err error) { +func (s *Service) SessionRequest(request SessionRequest) (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) + return s.buildTransaction(request) } else if request.Params.Request.Method == params.PersonalSignMethodName { - return s.personalSign(request, hashedPassword) + return s.buildPersonalSingMessage(request) } // TODO #12434: respond async diff --git a/services/wallet/walletconnect/walletconnect.go b/services/wallet/walletconnect/walletconnect.go index bae7670fe..9d2065d79 100644 --- a/services/wallet/walletconnect/walletconnect.go +++ b/services/wallet/walletconnect/walletconnect.go @@ -88,8 +88,12 @@ type SessionRequest struct { } type SessionRequestResponse struct { - SessionRequest SessionRequest `json:"sessionRequest"` - Signed types.HexBytes `json:"signed"` + KeyUID string `json:"keyUid,omitempty"` + Address types.Address `json:"address,omitempty"` + AddressPath string `json:"addressPath,omitempty"` + SignOnKeycard bool `json:"signOnKeycard,omitempty"` + MesageToSign interface{} `json:"messageToSign,omitempty"` + SignedMessage interface{} `json:"signedMessage,omitempty"` } func sessionProposalToSupportedChain(caipChains []string, supportsChain func(uint64) bool) (chains []uint64, eipChains []string) {