code.vegaprotocol.io/vega@v0.79.0/wallet/api/client_connect_wallet.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package api
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  
    23  	"code.vegaprotocol.io/vega/libs/jsonrpc"
    24  	"code.vegaprotocol.io/vega/wallet/preferences"
    25  	"code.vegaprotocol.io/vega/wallet/wallet"
    26  )
    27  
    28  const (
    29  	WalletConnectionSuccessfullyEstablished = "The connection to the wallet has been successfully established."
    30  	PassphraseRequestReasonUnlockWallet     = "The application wants to unlock the wallet."
    31  )
    32  
    33  type ClientConnectWallet struct {
    34  	walletStore WalletStore
    35  	interactor  Interactor
    36  }
    37  
    38  // Handle initiates the wallet connection between the API and a third-party
    39  // application.
    40  //
    41  // It triggers a selection of the wallet the client wants to use for this
    42  // connection. The wallet is then loaded in memory. All changes done to that wallet
    43  // will start in-memory, and then, be saved in the wallet file. Any changes done
    44  // to the wallet outside the JSON-RPC session (via the command-line for example)
    45  // will be overridden. For the effects to be taken into account, the wallet has
    46  // to be disconnected first, and then re-connected.
    47  //
    48  // All sessions have to be initialized by using this handler. Otherwise, a call
    49  // to any other handlers will be rejected.
    50  func (h *ClientConnectWallet) Handle(ctx context.Context, hostname string) (wallet.Wallet, *jsonrpc.ErrorDetails) {
    51  	traceID := jsonrpc.TraceIDFromContext(ctx)
    52  
    53  	if err := h.interactor.NotifyInteractionSessionBegan(ctx, traceID, WalletConnectionWorkflow, 4); err != nil {
    54  		return nil, RequestNotPermittedError(err)
    55  	}
    56  	defer h.interactor.NotifyInteractionSessionEnded(ctx, traceID)
    57  
    58  	availableWallets, err := h.walletStore.ListWallets(ctx)
    59  	if err != nil {
    60  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not list the available wallets: %w", err))
    61  		return nil, InternalError(ErrCouldNotConnectToWallet)
    62  	}
    63  	if len(availableWallets) == 0 {
    64  		h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, ErrNoWalletToConnectTo)
    65  		return nil, ApplicationCancellationError(ErrApplicationCancelledTheRequest)
    66  	}
    67  
    68  	var approval preferences.ConnectionApproval
    69  	for {
    70  		rawApproval, err := h.interactor.RequestWalletConnectionReview(ctx, traceID, 1, hostname)
    71  		if err != nil {
    72  			if errDetails := HandleRequestFlowError(ctx, traceID, h.interactor, err); errDetails != nil {
    73  				return nil, errDetails
    74  			}
    75  			h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("reviewing the wallet connection failed: %w", err))
    76  			return nil, InternalError(ErrCouldNotConnectToWallet)
    77  		}
    78  
    79  		a, err := preferences.ParseConnectionApproval(rawApproval)
    80  		if err != nil {
    81  			h.interactor.NotifyError(ctx, traceID, UserErrorType, err)
    82  			continue
    83  		}
    84  		approval = a
    85  		break
    86  	}
    87  
    88  	if isConnectionRejected(approval) {
    89  		return nil, UserRejectionError(ErrUserRejectedWalletConnection)
    90  	}
    91  
    92  	var walletName string
    93  	if len(availableWallets) > 1 {
    94  		for {
    95  			if ctx.Err() != nil {
    96  				h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, ErrRequestInterrupted)
    97  				return nil, RequestInterruptedError(ErrRequestInterrupted)
    98  			}
    99  
   100  			selectedWallet, err := h.interactor.RequestWalletSelection(ctx, traceID, 2, hostname, availableWallets)
   101  			if err != nil {
   102  				if errDetails := HandleRequestFlowError(ctx, traceID, h.interactor, err); errDetails != nil {
   103  					return nil, errDetails
   104  				}
   105  				h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("requesting the wallet selection failed: %w", err))
   106  				return nil, InternalError(ErrCouldNotConnectToWallet)
   107  			}
   108  
   109  			if exist, err := h.walletStore.WalletExists(ctx, selectedWallet); err != nil {
   110  				h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not verify the wallet existence: %w", err))
   111  				return nil, InternalError(ErrCouldNotConnectToWallet)
   112  			} else if !exist {
   113  				h.interactor.NotifyError(ctx, traceID, UserErrorType, ErrWalletDoesNotExist)
   114  				continue
   115  			}
   116  
   117  			walletName = selectedWallet
   118  			break
   119  		}
   120  	} else {
   121  		// There is single wallet available, it doesn't make sense to ask which
   122  		// wallet to use.
   123  		walletName = availableWallets[0]
   124  	}
   125  
   126  	alreadyUnlocked, err := h.walletStore.IsWalletAlreadyUnlocked(ctx, walletName)
   127  	if err != nil {
   128  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not verify whether the wallet is already unlock or not: %w", err))
   129  		return nil, InternalError(ErrCouldNotConnectToWallet)
   130  	}
   131  
   132  	if !alreadyUnlocked {
   133  		for {
   134  			if ctx.Err() != nil {
   135  				h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, ErrRequestInterrupted)
   136  				return nil, RequestInterruptedError(ErrRequestInterrupted)
   137  			}
   138  
   139  			walletPassphrase, err := h.interactor.RequestPassphrase(ctx, traceID, 3, walletName, PassphraseRequestReasonUnlockWallet)
   140  			if err != nil {
   141  				if errDetails := HandleRequestFlowError(ctx, traceID, h.interactor, err); errDetails != nil {
   142  					return nil, errDetails
   143  				}
   144  				h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("requesting the wallet passphrase failed: %w", err))
   145  				return nil, InternalError(ErrCouldNotConnectToWallet)
   146  			}
   147  
   148  			if err := h.walletStore.UnlockWallet(ctx, walletName, walletPassphrase); err != nil {
   149  				if errors.Is(err, wallet.ErrWrongPassphrase) {
   150  					h.interactor.NotifyError(ctx, traceID, UserErrorType, wallet.ErrWrongPassphrase)
   151  					continue
   152  				}
   153  				if errDetails := HandleRequestFlowError(ctx, traceID, h.interactor, err); errDetails != nil {
   154  					return nil, errDetails
   155  				}
   156  				h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not unlock the wallet: %w", err))
   157  				return nil, InternalError(ErrCouldNotConnectToWallet)
   158  			}
   159  			break
   160  		}
   161  	}
   162  
   163  	w, err := h.walletStore.GetWallet(ctx, walletName)
   164  	if err != nil {
   165  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not retrieve the wallet: %w", err))
   166  		return nil, InternalError(ErrCouldNotConnectToWallet)
   167  	}
   168  
   169  	h.interactor.NotifySuccessfulRequest(ctx, traceID, 4, WalletConnectionSuccessfullyEstablished)
   170  
   171  	return w, nil
   172  }
   173  
   174  func isConnectionRejected(approval preferences.ConnectionApproval) bool {
   175  	return approval != preferences.ApprovedOnlyThisTime
   176  }
   177  
   178  func NewConnectWallet(walletStore WalletStore, interactor Interactor) *ClientConnectWallet {
   179  	return &ClientConnectWallet{
   180  		walletStore: walletStore,
   181  		interactor:  interactor,
   182  	}
   183  }