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 }