code.vegaprotocol.io/vega@v0.79.0/wallet/service/v2/connections/manager.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 connections
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"sort"
    23  	"sync"
    24  	"time"
    25  
    26  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    27  	"code.vegaprotocol.io/vega/libs/jsonrpc"
    28  	"code.vegaprotocol.io/vega/wallet/api"
    29  	"code.vegaprotocol.io/vega/wallet/wallet"
    30  )
    31  
    32  // Manager holds the opened connections between the third-party
    33  // applications and the wallets.
    34  type Manager struct {
    35  	// tokenToConnection maps the token to the connection. It is the base
    36  	// registry for all opened connections.
    37  	tokenToConnection map[Token]*walletConnection
    38  
    39  	// sessionFingerprintToToken maps the session fingerprint (wallet + hostname
    40  	// on a session connection). This is used to determine whether a session
    41  	// connection already exists for a given wallet and hostname.
    42  	// It only holds sessions fingerprints.
    43  	// Long-living connections are not tracked.
    44  	sessionFingerprintToToken map[string]Token
    45  
    46  	// timeService is used to resolve the current time to update the last activity
    47  	// time on the token, and figure out their expiration.
    48  	timeService TimeService
    49  
    50  	walletStore  WalletStore
    51  	sessionStore SessionStore
    52  	tokenStore   TokenStore
    53  
    54  	interactor api.Interactor
    55  
    56  	mu sync.Mutex
    57  }
    58  
    59  type walletConnection struct {
    60  	// connectedWallet is the projection of the wallet through the permissions
    61  	// and authentication system. On a regular wallet, there is no restriction
    62  	// on what we can call, which doesn't fit the model of having allowed
    63  	// access, so we wrap the "regular wallet" behind the "connected wallet".
    64  	connectedWallet api.ConnectedWallet
    65  
    66  	policy connectionPolicy
    67  }
    68  
    69  // StartSession initializes a connection between a wallet and a third-party
    70  // application.
    71  // If a connection already exists, it's disconnected and a new token is
    72  // generated.
    73  func (m *Manager) StartSession(hostname string, w wallet.Wallet) (Token, error) {
    74  	m.mu.Lock()
    75  	defer m.mu.Unlock()
    76  
    77  	cw, err := api.NewConnectedWallet(hostname, w)
    78  	if err != nil {
    79  		return "", fmt.Errorf("could not instantiate the connected wallet for a session connection: %w", err)
    80  	}
    81  
    82  	newToken := m.generateToken()
    83  
    84  	sessionFingerprint := asSessionFingerprint(hostname, w.Name())
    85  	if previousToken, sessionAlreadyExists := m.sessionFingerprintToToken[sessionFingerprint]; sessionAlreadyExists {
    86  		m.destroySessionToken(previousToken)
    87  	}
    88  	m.sessionFingerprintToToken[sessionFingerprint] = newToken
    89  
    90  	now := m.timeService.Now()
    91  	m.tokenToConnection[newToken] = &walletConnection{
    92  		connectedWallet: cw,
    93  		policy: &sessionPolicy{
    94  			expiryDate: now.Add(1 * time.Hour),
    95  		},
    96  	}
    97  
    98  	// We ignore this error as tracking the session a nice-to-have feature to
    99  	// ease reconnection after a software reboot. We don't want to prevent the
   100  	// connection because an error on that layer.
   101  	_ = m.sessionStore.TrackSession(Session{
   102  		Token:    newToken,
   103  		Hostname: hostname,
   104  		Wallet:   w.Name(),
   105  	})
   106  
   107  	return newToken, nil
   108  }
   109  
   110  func (m *Manager) EndSessionConnectionWithToken(token Token) {
   111  	m.mu.Lock()
   112  	defer m.mu.Unlock()
   113  
   114  	m.destroySessionToken(token)
   115  }
   116  
   117  func (m *Manager) EndSessionConnection(hostname, walletName string) {
   118  	m.mu.Lock()
   119  	defer m.mu.Unlock()
   120  
   121  	fingerprint := asSessionFingerprint(hostname, walletName)
   122  
   123  	token, exists := m.sessionFingerprintToToken[fingerprint]
   124  	if !exists {
   125  		return
   126  	}
   127  
   128  	m.destroySessionToken(token)
   129  }
   130  
   131  func (m *Manager) EndAllSessionConnections() {
   132  	m.mu.Lock()
   133  	defer m.mu.Unlock()
   134  
   135  	for token := range m.tokenToConnection {
   136  		m.destroySessionToken(token)
   137  	}
   138  }
   139  
   140  // ConnectedWallet retrieves the wallet associated to the specified token.
   141  func (m *Manager) ConnectedWallet(ctx context.Context, hostname string, token Token) (api.ConnectedWallet, *jsonrpc.ErrorDetails) {
   142  	m.mu.Lock()
   143  	defer m.mu.Unlock()
   144  
   145  	connection, exists := m.tokenToConnection[token]
   146  	if !exists {
   147  		return api.ConnectedWallet{}, serverErrorAuthenticationFailure(ErrNoConnectionAssociatedThisAuthenticationToken)
   148  	}
   149  
   150  	now := m.timeService.Now()
   151  
   152  	hasExpired := connection.policy.HasConnectionExpired(now)
   153  
   154  	if connection.policy.IsLongLivingConnection() {
   155  		if hasExpired {
   156  			return api.ConnectedWallet{}, serverErrorAuthenticationFailure(ErrTokenHasExpired)
   157  		}
   158  	} else {
   159  		traceID := jsonrpc.TraceIDFromContext(ctx)
   160  
   161  		if connection.connectedWallet.Hostname() != hostname {
   162  			return api.ConnectedWallet{}, serverErrorAuthenticationFailure(ErrHostnamesMismatchForThisToken)
   163  		}
   164  
   165  		isClosed := connection.policy.IsClosed()
   166  
   167  		if hasExpired || isClosed {
   168  			if err := m.interactor.NotifyInteractionSessionBegan(ctx, traceID, api.WalletUnlockingWorkflow, 2); err != nil {
   169  				return api.ConnectedWallet{}, api.RequestNotPermittedError(err)
   170  			}
   171  			defer m.interactor.NotifyInteractionSessionEnded(ctx, traceID)
   172  
   173  			for {
   174  				if ctx.Err() != nil {
   175  					m.interactor.NotifyError(ctx, traceID, api.ApplicationErrorType, api.ErrRequestInterrupted)
   176  					return api.ConnectedWallet{}, api.RequestInterruptedError(api.ErrRequestInterrupted)
   177  				}
   178  
   179  				unlockingReason := fmt.Sprintf("The third-party application %q is attempting access to the locked wallet %q. To unlock this wallet and allow access to all connected apps associated to it, enter its passphrase.", hostname, connection.connectedWallet.Name())
   180  
   181  				walletPassphrase, err := m.interactor.RequestPassphrase(ctx, traceID, 1, connection.connectedWallet.Name(), unlockingReason)
   182  				if err != nil {
   183  					if errDetails := api.HandleRequestFlowError(ctx, traceID, m.interactor, err); errDetails != nil {
   184  						return api.ConnectedWallet{}, errDetails
   185  					}
   186  					m.interactor.NotifyError(ctx, traceID, api.InternalErrorType, fmt.Errorf("requesting the wallet passphrase failed: %w", err))
   187  					return api.ConnectedWallet{}, api.InternalError(api.ErrCouldNotConnectToWallet)
   188  				}
   189  
   190  				if err := m.walletStore.UnlockWallet(ctx, connection.connectedWallet.Name(), walletPassphrase); err != nil {
   191  					if errors.Is(err, wallet.ErrWrongPassphrase) {
   192  						m.interactor.NotifyError(ctx, traceID, api.UserErrorType, wallet.ErrWrongPassphrase)
   193  						continue
   194  					}
   195  					if errDetails := api.HandleRequestFlowError(ctx, traceID, m.interactor, err); errDetails != nil {
   196  						return api.ConnectedWallet{}, errDetails
   197  					}
   198  					m.interactor.NotifyError(ctx, traceID, api.InternalErrorType, fmt.Errorf("could not unlock the wallet: %w", err))
   199  					return api.ConnectedWallet{}, api.InternalError(api.ErrCouldNotConnectToWallet)
   200  				}
   201  				break
   202  			}
   203  
   204  			w, err := m.walletStore.GetWallet(ctx, connection.connectedWallet.Name())
   205  			if err != nil {
   206  				m.interactor.NotifyError(ctx, traceID, api.InternalErrorType, fmt.Errorf("could not retrieve the wallet: %w", err))
   207  				return api.ConnectedWallet{}, api.InternalError(api.ErrCouldNotConnectToWallet)
   208  			}
   209  
   210  			// Update the connected wallet for all connections referencing the
   211  			// newly unlocked wallet.
   212  			for _, otherConnection := range m.tokenToConnection {
   213  				if w.Name() != otherConnection.connectedWallet.Name() {
   214  					continue
   215  				}
   216  
   217  				cw, err := api.NewConnectedWallet(otherConnection.connectedWallet.Hostname(), w)
   218  				if err != nil {
   219  					m.interactor.NotifyError(ctx, traceID, api.InternalErrorType, fmt.Errorf("could not instantiate the connected wallet for a session connection: %w", err))
   220  					return api.ConnectedWallet{}, api.InternalError(api.ErrCouldNotConnectToWallet)
   221  				}
   222  
   223  				otherConnection.connectedWallet = cw
   224  				otherConnection.policy = &sessionPolicy{
   225  					expiryDate: now.Add(1 * time.Hour),
   226  					closed:     false,
   227  				}
   228  			}
   229  
   230  			m.interactor.NotifySuccessfulRequest(ctx, traceID, 2, fmt.Sprintf("The wallet %q has been successfully unlocked.", w.Name()))
   231  		}
   232  	}
   233  
   234  	return connection.connectedWallet, nil
   235  }
   236  
   237  // ListSessionConnections lists all the session connections as a list of pairs of
   238  // hostname/wallet name.
   239  // The list is sorted, first, by hostname, and, then, by wallet name.
   240  func (m *Manager) ListSessionConnections() []api.Connection {
   241  	m.mu.Lock()
   242  	defer m.mu.Unlock()
   243  
   244  	connections := make([]api.Connection, 0, len(m.tokenToConnection))
   245  	connectionsCount := 0
   246  	for _, connection := range m.tokenToConnection {
   247  		if connection.policy.IsLongLivingConnection() {
   248  			continue
   249  		}
   250  
   251  		connections = append(connections, api.Connection{
   252  			Hostname: connection.connectedWallet.Hostname(),
   253  			Wallet:   connection.connectedWallet.Name(),
   254  		})
   255  
   256  		connectionsCount++
   257  	}
   258  	connections = connections[0:connectionsCount]
   259  
   260  	sort.SliceStable(connections, func(i, j int) bool {
   261  		if connections[i].Hostname == connections[j].Hostname {
   262  			return connections[i].Wallet < connections[j].Wallet
   263  		}
   264  
   265  		return connections[i].Hostname < connections[j].Hostname
   266  	})
   267  
   268  	return connections
   269  }
   270  
   271  // generateToken generates a new token and ensure it is not already in use to
   272  // avoid collisions.
   273  func (m *Manager) generateToken() Token {
   274  	for {
   275  		token := GenerateToken()
   276  		if _, alreadyExistingToken := m.tokenToConnection[token]; !alreadyExistingToken {
   277  			return token
   278  		}
   279  	}
   280  }
   281  
   282  func (m *Manager) destroySessionToken(tokenToDestroy Token) {
   283  	connection, exists := m.tokenToConnection[tokenToDestroy]
   284  	if !exists || connection.policy.IsLongLivingConnection() {
   285  		return
   286  	}
   287  
   288  	// Remove the session fingerprint associated to the session token.
   289  	for sessionFingerprint, t := range m.sessionFingerprintToToken {
   290  		if t == tokenToDestroy {
   291  			delete(m.sessionFingerprintToToken, sessionFingerprint)
   292  			break
   293  		}
   294  	}
   295  
   296  	// Break the link between a token and its associated wallet.
   297  	m.tokenToConnection[tokenToDestroy] = nil
   298  	delete(m.tokenToConnection, tokenToDestroy)
   299  }
   300  
   301  func (m *Manager) loadLongLivingConnections(ctx context.Context) error {
   302  	tokenSummaries, err := m.tokenStore.ListTokens()
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	for _, tokenSummary := range tokenSummaries {
   308  		tokenDescription, err := m.tokenStore.DescribeToken(tokenSummary.Token)
   309  		if err != nil {
   310  			return fmt.Errorf("could not get information associated to the token %q: %w", tokenDescription.Token.Short(), err)
   311  		}
   312  
   313  		if err := m.loadLongLivingConnection(ctx, tokenDescription); err != nil {
   314  			return err
   315  		}
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  func (m *Manager) loadLongLivingConnection(ctx context.Context, tokenDescription TokenDescription) error {
   322  	if err := m.walletStore.UnlockWallet(ctx, tokenDescription.Wallet.Name, tokenDescription.Wallet.Passphrase); err != nil {
   323  		// We don't properly handle wallets renaming, nor wallets passphrase
   324  		// update in the token file automatically. We only support a direct
   325  		// update of the token file.
   326  		return fmt.Errorf("could not unlock the wallet %q associated to the token %q: %w",
   327  			tokenDescription.Wallet.Name,
   328  			tokenDescription.Token.Short(),
   329  			err)
   330  	}
   331  
   332  	w, err := m.walletStore.GetWallet(ctx, tokenDescription.Wallet.Name)
   333  	if err != nil {
   334  		// This should not happen because we just unlocked the wallet.
   335  		return fmt.Errorf("could not get the information for the wallet %q associated to the token %q: %w",
   336  			tokenDescription.Wallet.Name,
   337  			tokenDescription.Token.Short(),
   338  			err)
   339  	}
   340  
   341  	m.tokenToConnection[tokenDescription.Token] = &walletConnection{
   342  		connectedWallet: api.NewLongLivingConnectedWallet(w),
   343  		policy: &longLivingConnectionPolicy{
   344  			expirationDate: tokenDescription.ExpirationDate,
   345  		},
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func (m *Manager) loadPreviousSessionConnections(ctx context.Context) error {
   352  	sessions, err := m.sessionStore.ListSessions(ctx)
   353  	if err != nil {
   354  		return fmt.Errorf("could not list the sessions: %w", err)
   355  	}
   356  
   357  	now := m.timeService.Now()
   358  
   359  	for _, session := range sessions {
   360  		sessionFingerprint := asSessionFingerprint(session.Hostname, session.Wallet)
   361  		m.sessionFingerprintToToken[sessionFingerprint] = session.Token
   362  
   363  		// If the wallet in the session store doesn't exist, we destroy that session
   364  		// and move onto the next.
   365  		walletExists, err := m.walletStore.WalletExists(ctx, session.Wallet)
   366  		if err != nil {
   367  			return fmt.Errorf("could not verify if the wallet %q exists: %w", session.Wallet, err)
   368  		}
   369  
   370  		if !walletExists {
   371  			err := m.sessionStore.DeleteSession(ctx, session.Token)
   372  			if err != nil {
   373  				return fmt.Errorf("could not delete the session with token %q: %w", session.Token, err)
   374  			}
   375  			continue
   376  		}
   377  
   378  		// If the wallet is already unlocked, we fully restore the connection.
   379  		isAlreadyUnlocked, err := m.walletStore.IsWalletAlreadyUnlocked(ctx, session.Wallet)
   380  		if err != nil {
   381  			return fmt.Errorf("could not verify wether the wallet %q is locked or not: %w", session.Wallet, err)
   382  		}
   383  
   384  		var connectedWallet api.ConnectedWallet
   385  		isClosed := true
   386  		if isAlreadyUnlocked {
   387  			w, err := m.walletStore.GetWallet(ctx, session.Wallet)
   388  			if err != nil {
   389  				return fmt.Errorf("could not retrieve the wallet %q: %w", session.Wallet, err)
   390  			}
   391  			cw, err := api.NewConnectedWallet(session.Hostname, w)
   392  			if err != nil {
   393  				return fmt.Errorf("could not instantiate the connected wallet for a session connection: %w", err)
   394  			}
   395  			connectedWallet = cw
   396  			isClosed = false
   397  		} else {
   398  			connectedWallet = api.NewDisconnectedWallet(session.Hostname, session.Wallet)
   399  		}
   400  
   401  		m.tokenToConnection[session.Token] = &walletConnection{
   402  			connectedWallet: connectedWallet,
   403  			policy: &sessionPolicy{
   404  				// Since this session is being reloaded, we consider it to be
   405  				// expired.
   406  				expiryDate: now,
   407  				closed:     isClosed,
   408  			},
   409  		}
   410  	}
   411  
   412  	return nil
   413  }
   414  
   415  // refreshConnections is called when the wallet store notices a change in
   416  // the wallets. This way the connection manager is able to reload the connected
   417  // wallets.
   418  func (m *Manager) refreshConnections(_ context.Context, event wallet.Event) {
   419  	m.mu.Lock()
   420  	defer m.mu.Unlock()
   421  
   422  	switch event.Type {
   423  	case wallet.WalletRemovedEventType:
   424  		m.destroyConnectionsUsingThisWallet(event.Data.(wallet.WalletRemovedEventData).Name)
   425  	case wallet.UnlockedWalletUpdatedEventType:
   426  		m.updateConnectionsUsingThisWallet(event.Data.(wallet.UnlockedWalletUpdatedEventData).UpdatedWallet)
   427  	case wallet.WalletHasBeenLockedEventType:
   428  		m.closeConnectionsUsingThisWallet(event.Data.(wallet.WalletHasBeenLockedEventData).Name)
   429  	case wallet.WalletRenamedEventType:
   430  		data := event.Data.(wallet.WalletRenamedEventData)
   431  		m.updateConnectionsUsingThisRenamedWallet(data.PreviousName, data.NewName)
   432  	}
   433  }
   434  
   435  // destroyConnectionUsingThisWallet close the connection, dereference it, and
   436  // remove it from the session store.
   437  func (m *Manager) destroyConnectionsUsingThisWallet(walletName string) {
   438  	ctx := context.Background()
   439  
   440  	for token, connection := range m.tokenToConnection {
   441  		if connection.connectedWallet.Name() != walletName {
   442  			continue
   443  		}
   444  
   445  		connection.policy.SetAsForcefullyClose()
   446  
   447  		delete(m.tokenToConnection, token)
   448  
   449  		// We ignore the error in a best-effort to have the session store clean
   450  		// up.
   451  		_ = m.sessionStore.DeleteSession(ctx, token)
   452  	}
   453  }
   454  
   455  func (m *Manager) updateConnectionsUsingThisWallet(w wallet.Wallet) {
   456  	for _, connection := range m.tokenToConnection {
   457  		if connection.connectedWallet.Name() != w.Name() {
   458  			continue
   459  		}
   460  
   461  		var updatedConnectedWallet api.ConnectedWallet
   462  		if connection.policy.IsLongLivingConnection() {
   463  			updatedConnectedWallet = api.NewLongLivingConnectedWallet(w)
   464  		} else {
   465  			updatedConnectedWallet, _ = api.NewConnectedWallet(connection.connectedWallet.Hostname(), w)
   466  		}
   467  
   468  		connection.connectedWallet = updatedConnectedWallet
   469  	}
   470  }
   471  
   472  func (m *Manager) refreshLongLivingTokens(ctx context.Context, activeTokensDescriptions ...TokenDescription) {
   473  	m.mu.Lock()
   474  	defer m.mu.Unlock()
   475  
   476  	// We need to find the new tokens among the active ones, so, we build a
   477  	// registry with all the active tokens. Then, we remove the tracked token
   478  	// when found. We will end up with the new tokens, only.
   479  	activeTokens := make(map[Token]TokenDescription, len(activeTokensDescriptions))
   480  	for _, tokenDescription := range activeTokensDescriptions {
   481  		activeTokens[tokenDescription.Token] = tokenDescription
   482  	}
   483  
   484  	isActiveToken := func(token Token) (TokenDescription, bool) {
   485  		activeTokenDescription, isTracked := activeTokens[token]
   486  		if isTracked {
   487  			delete(activeTokens, token)
   488  		}
   489  		return activeTokenDescription, isTracked
   490  	}
   491  
   492  	// First, we update of the tokens we already track.
   493  	for token, connection := range m.tokenToConnection {
   494  		if !connection.policy.IsLongLivingConnection() {
   495  			continue
   496  		}
   497  
   498  		activeToken, isActive := isActiveToken(token)
   499  		if !isActive {
   500  			// If the token could not be found in the active tokens, this means
   501  			// the token has been deleted from the token store. Thus, we close the
   502  			// connection.
   503  			delete(m.tokenToConnection, token)
   504  			continue
   505  		}
   506  
   507  		_ = m.loadLongLivingConnection(ctx, activeToken)
   508  	}
   509  
   510  	// Then, we load the new tokens.
   511  	for _, tokenDescription := range activeTokens {
   512  		_ = m.loadLongLivingConnection(ctx, tokenDescription)
   513  	}
   514  }
   515  
   516  // closeConnectionsUsingThisWallet defines the connection as closed. It keeps track of it
   517  // but next time it will be used the application will request for the passphrase
   518  // to reinstate it.
   519  func (m *Manager) closeConnectionsUsingThisWallet(walletName string) {
   520  	for _, connection := range m.tokenToConnection {
   521  		if connection.connectedWallet.Name() != walletName {
   522  			continue
   523  		}
   524  
   525  		connection.policy.SetAsForcefullyClose()
   526  	}
   527  }
   528  
   529  func (m *Manager) updateConnectionsUsingThisRenamedWallet(previousWalletName, newWalletName string) {
   530  	var _updatedWallet wallet.Wallet
   531  
   532  	// This acts as a cached getter, to avoid multiple or useless fetch.
   533  	getUpdatedWallet := func() wallet.Wallet {
   534  		if _updatedWallet == nil {
   535  			w, _ := m.walletStore.GetWallet(context.Background(), newWalletName)
   536  			_updatedWallet = w
   537  		}
   538  		return _updatedWallet
   539  	}
   540  
   541  	for _, connection := range m.tokenToConnection {
   542  		if connection.connectedWallet.Name() != previousWalletName {
   543  			continue
   544  		}
   545  
   546  		if connection.policy.IsLongLivingConnection() {
   547  			connection.connectedWallet = api.NewLongLivingConnectedWallet(
   548  				getUpdatedWallet(),
   549  			)
   550  		}
   551  
   552  		connection.connectedWallet, _ = api.NewConnectedWallet(
   553  			connection.connectedWallet.Hostname(),
   554  			getUpdatedWallet(),
   555  		)
   556  	}
   557  }
   558  
   559  func NewManager(timeService TimeService, walletStore WalletStore, tokenStore TokenStore, sessionStore SessionStore, interactor api.Interactor) (*Manager, error) {
   560  	m := &Manager{
   561  		tokenToConnection:         map[Token]*walletConnection{},
   562  		sessionFingerprintToToken: map[string]Token{},
   563  		timeService:               timeService,
   564  		walletStore:               walletStore,
   565  		sessionStore:              sessionStore,
   566  		tokenStore:                tokenStore,
   567  		interactor:                interactor,
   568  	}
   569  
   570  	ctx := context.Background()
   571  
   572  	if err := m.loadLongLivingConnections(ctx); err != nil {
   573  		return nil, fmt.Errorf("could not load the long-living connections: %w", err)
   574  	}
   575  
   576  	if err := m.loadPreviousSessionConnections(ctx); err != nil {
   577  		return nil, fmt.Errorf("could not load the previous session connections: %w", err)
   578  	}
   579  
   580  	walletStore.OnUpdate(m.refreshConnections)
   581  	tokenStore.OnUpdate(m.refreshLongLivingTokens)
   582  
   583  	return m, nil
   584  }
   585  
   586  func asSessionFingerprint(hostname string, walletName string) string {
   587  	return vgcrypto.HashStrToHex(hostname + "::" + walletName)
   588  }
   589  
   590  func serverErrorAuthenticationFailure(err error) *jsonrpc.ErrorDetails {
   591  	return jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
   592  }