code.vegaprotocol.io/vega@v0.79.0/core/nodewallets/eth/clef/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 clef
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sync"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/nodewallets/registry"
    25  	"code.vegaprotocol.io/vega/libs/crypto"
    26  
    27  	"github.com/ethereum/go-ethereum/accounts"
    28  	ethcommon "github.com/ethereum/go-ethereum/common"
    29  	"github.com/ethereum/go-ethereum/common/hexutil"
    30  )
    31  
    32  const (
    33  	requestTimeout          = time.Second * 10
    34  	signDataTextRawMimeType = "text/raw"
    35  	ClefAlgoType            = "clef"
    36  )
    37  
    38  //go:generate go run github.com/golang/mock/mockgen -destination mocks/rpc_client_mock.go -package mocks code.vegaprotocol.io/vega/core/nodewallets/eth/clef Client
    39  type Client interface {
    40  	CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error
    41  	Close()
    42  }
    43  
    44  type Wallet struct {
    45  	client   Client
    46  	endpoint string
    47  	name     string
    48  	account  *accounts.Account
    49  	mut      sync.Mutex
    50  }
    51  
    52  func newAccount(accountAddr ethcommon.Address, endpoint string) *accounts.Account {
    53  	return &accounts.Account{
    54  		URL: accounts.URL{
    55  			Scheme: "clef",
    56  			Path:   endpoint,
    57  		},
    58  		Address: accountAddr,
    59  	}
    60  }
    61  
    62  func NewWallet(client Client, endpoint string, accountAddr ethcommon.Address) (*Wallet, error) {
    63  	w := &Wallet{
    64  		name:     fmt.Sprintf("clef-%s", endpoint),
    65  		client:   client,
    66  		endpoint: endpoint,
    67  	}
    68  
    69  	if err := w.contains(accountAddr); err != nil {
    70  		return nil, fmt.Errorf("account not found: %w", err)
    71  	}
    72  
    73  	w.account = newAccount(accountAddr, w.endpoint)
    74  
    75  	return w, nil
    76  }
    77  
    78  // GenerateNewWallet new wallet will create new account in Clef and returns wallet.
    79  // Caveat: generating new wallet in Clef has to be manually approved and only key store backend is supported.
    80  func GenerateNewWallet(client Client, endpoint string) (*Wallet, error) {
    81  	w := &Wallet{
    82  		name:     fmt.Sprintf("clef-%s", endpoint),
    83  		client:   client,
    84  		endpoint: endpoint,
    85  	}
    86  
    87  	acc, err := w.generateAccount()
    88  	if err != nil {
    89  		return nil, fmt.Errorf("failed to generate account: %w", err)
    90  	}
    91  
    92  	w.account = acc
    93  
    94  	return w, nil
    95  }
    96  
    97  func (w *Wallet) generateAccount() (*accounts.Account, error) {
    98  	// increase timeout here as generating new account has to be manually approved in Clef
    99  	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout*20)
   100  	defer cancel()
   101  
   102  	var res string
   103  	if err := w.client.CallContext(ctx, &res, "account_new"); err != nil {
   104  		return nil, fmt.Errorf("failed to call client: %w", err)
   105  	}
   106  
   107  	return newAccount(ethcommon.HexToAddress(res), w.endpoint), nil
   108  }
   109  
   110  // contains returns nil if account is found, otherwise returns an error.
   111  func (w *Wallet) contains(testAddr ethcommon.Address) error {
   112  	addresses, err := w.listAccounts()
   113  	if err != nil {
   114  		return fmt.Errorf("failed to list accounts: %w", err)
   115  	}
   116  
   117  	for _, addr := range addresses {
   118  		if testAddr == addr {
   119  			return nil
   120  		}
   121  	}
   122  
   123  	return fmt.Errorf("wallet does not contain account %q", testAddr)
   124  }
   125  
   126  func (w *Wallet) listAccounts() ([]ethcommon.Address, error) {
   127  	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
   128  	defer cancel()
   129  
   130  	var res []ethcommon.Address
   131  	if err := w.client.CallContext(ctx, &res, "account_list"); err != nil {
   132  		return nil, fmt.Errorf("failed to call client: %w", err)
   133  	}
   134  	return res, nil
   135  }
   136  
   137  func (w *Wallet) Cleanup() error {
   138  	w.client.Close()
   139  	return nil
   140  }
   141  
   142  func (w *Wallet) Name() string {
   143  	return w.name
   144  }
   145  
   146  func (w *Wallet) Chain() string {
   147  	return "ethereum"
   148  }
   149  
   150  func (w *Wallet) Sign(data []byte) ([]byte, error) {
   151  	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
   152  	defer cancel()
   153  
   154  	var res hexutil.Bytes
   155  	signAddress := ethcommon.NewMixedcaseAddress(w.account.Address)
   156  
   157  	if err := w.client.CallContext(
   158  		ctx,
   159  		&res,
   160  		"account_signData",
   161  		signDataTextRawMimeType,
   162  		&signAddress, // Need to use the pointer here, because of how MarshalJSON is defined
   163  		hexutil.Encode(data),
   164  	); err != nil {
   165  		return nil, fmt.Errorf("failed to call client: %w", err)
   166  	}
   167  
   168  	return res, nil
   169  }
   170  
   171  func (w *Wallet) Algo() string {
   172  	return ClefAlgoType
   173  }
   174  
   175  func (w *Wallet) Version() (string, error) {
   176  	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
   177  	defer cancel()
   178  
   179  	var v string
   180  	if err := w.client.CallContext(ctx, &v, "account_version"); err != nil {
   181  		return "", fmt.Errorf("failed to call client: %w", err)
   182  	}
   183  
   184  	return v, nil
   185  }
   186  
   187  func (w *Wallet) PubKey() crypto.PublicKey {
   188  	return crypto.NewPublicKey(w.account.Address.Hex(), w.account.Address.Bytes())
   189  }
   190  
   191  func (w *Wallet) Reload(details registry.EthereumWalletDetails) error {
   192  	d, ok := details.(registry.EthereumClefWallet)
   193  	if !ok {
   194  		// this would mean an implementation error
   195  		panic(fmt.Errorf("failed to get EthereumClefWallet"))
   196  	}
   197  
   198  	accountAddr := ethcommon.HexToAddress(d.AccountAddress)
   199  	if err := w.contains(accountAddr); err != nil {
   200  		return fmt.Errorf("account not found: %w", err)
   201  	}
   202  
   203  	w.mut.Lock()
   204  	defer w.mut.Unlock()
   205  
   206  	w.account = newAccount(accountAddr, w.endpoint)
   207  
   208  	return nil
   209  }