github.com/Evanesco-Labs/go-evanesco@v1.0.1/signer/core/api.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package core 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "math/big" 25 "os" 26 "reflect" 27 28 "github.com/Evanesco-Labs/go-evanesco/accounts" 29 "github.com/Evanesco-Labs/go-evanesco/accounts/keystore" 30 "github.com/Evanesco-Labs/go-evanesco/accounts/scwallet" 31 "github.com/Evanesco-Labs/go-evanesco/accounts/usbwallet" 32 "github.com/Evanesco-Labs/go-evanesco/common" 33 "github.com/Evanesco-Labs/go-evanesco/common/hexutil" 34 "github.com/Evanesco-Labs/go-evanesco/internal/ethapi" 35 "github.com/Evanesco-Labs/go-evanesco/log" 36 "github.com/Evanesco-Labs/go-evanesco/signer/storage" 37 ) 38 39 const ( 40 // numberOfAccountsToDerive For hardware wallets, the number of accounts to derive 41 numberOfAccountsToDerive = 10 42 // ExternalAPIVersion -- see extapi_changelog.md 43 ExternalAPIVersion = "6.1.0" 44 // InternalAPIVersion -- see intapi_changelog.md 45 InternalAPIVersion = "7.0.1" 46 ) 47 48 // ExternalAPI defines the external API through which signing requests are made. 49 type ExternalAPI interface { 50 // List available accounts 51 List(ctx context.Context) ([]common.Address, error) 52 // New request to create a new account 53 New(ctx context.Context) (common.Address, error) 54 // SignTransaction request to sign the specified transaction 55 SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) 56 // SignData - request to sign the given data (plus prefix) 57 SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) 58 // SignTypedData - request to sign the given structured data (plus prefix) 59 SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) 60 // EcRecover - recover public key from given message and signature 61 EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) 62 // Version info about the APIs 63 Version(ctx context.Context) (string, error) 64 // SignGnosisSafeTransaction signs/confirms a gnosis-safe multisig transaction 65 SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) 66 } 67 68 // UIClientAPI specifies what method a UI needs to implement to be able to be used as a 69 // UI for the signer 70 type UIClientAPI interface { 71 // ApproveTx prompt the user for confirmation to request to sign Transaction 72 ApproveTx(request *SignTxRequest) (SignTxResponse, error) 73 // ApproveSignData prompt the user for confirmation to request to sign data 74 ApproveSignData(request *SignDataRequest) (SignDataResponse, error) 75 // ApproveListing prompt the user for confirmation to list accounts 76 // the list of accounts to list can be modified by the UI 77 ApproveListing(request *ListRequest) (ListResponse, error) 78 // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller 79 ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) 80 // ShowError displays error message to user 81 ShowError(message string) 82 // ShowInfo displays info message to user 83 ShowInfo(message string) 84 // OnApprovedTx notifies the UI about a transaction having been successfully signed. 85 // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. 86 OnApprovedTx(tx ethapi.SignTransactionResult) 87 // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version 88 // information 89 OnSignerStartup(info StartupInfo) 90 // OnInputRequired is invoked when clef requires user input, for example master password or 91 // pin-code for unlocking hardware wallets 92 OnInputRequired(info UserInputRequest) (UserInputResponse, error) 93 // RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication 94 RegisterUIServer(api *UIServerAPI) 95 } 96 97 // Validator defines the methods required to validate a transaction against some 98 // sanity defaults as well as any underlying 4byte method database. 99 // 100 // Use fourbyte.Database as an implementation. It is separated out of this package 101 // to allow pieces of the signer package to be used without having to load the 102 // 7MB embedded 4byte dump. 103 type Validator interface { 104 // ValidateTransaction does a number of checks on the supplied transaction, and 105 // returns either a list of warnings, or an error (indicating that the transaction 106 // should be immediately rejected). 107 ValidateTransaction(selector *string, tx *SendTxArgs) (*ValidationMessages, error) 108 } 109 110 // SignerAPI defines the actual implementation of ExternalAPI 111 type SignerAPI struct { 112 chainID *big.Int 113 am *accounts.Manager 114 UI UIClientAPI 115 validator Validator 116 rejectMode bool 117 credentials storage.Storage 118 } 119 120 // Metadata about a request 121 type Metadata struct { 122 Remote string `json:"remote"` 123 Local string `json:"local"` 124 Scheme string `json:"scheme"` 125 UserAgent string `json:"User-Agent"` 126 Origin string `json:"Origin"` 127 } 128 129 func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, scpath string) *accounts.Manager { 130 var ( 131 backends []accounts.Backend 132 n, p = keystore.StandardScryptN, keystore.StandardScryptP 133 ) 134 if lightKDF { 135 n, p = keystore.LightScryptN, keystore.LightScryptP 136 } 137 // support password based accounts 138 if len(ksLocation) > 0 { 139 backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) 140 } 141 if !nousb { 142 // Start a USB hub for Ledger hardware wallets 143 if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { 144 log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) 145 } else { 146 backends = append(backends, ledgerhub) 147 log.Debug("Ledger support enabled") 148 } 149 // Start a USB hub for Trezor hardware wallets (HID version) 150 if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil { 151 log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err)) 152 } else { 153 backends = append(backends, trezorhub) 154 log.Debug("Trezor support enabled via HID") 155 } 156 // Start a USB hub for Trezor hardware wallets (WebUSB version) 157 if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil { 158 log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err)) 159 } else { 160 backends = append(backends, trezorhub) 161 log.Debug("Trezor support enabled via WebUSB") 162 } 163 } 164 165 // Start a smart card hub 166 if len(scpath) > 0 { 167 // Sanity check that the smartcard path is valid 168 fi, err := os.Stat(scpath) 169 if err != nil { 170 log.Info("Smartcard socket file missing, disabling", "err", err) 171 } else { 172 if fi.Mode()&os.ModeType != os.ModeSocket { 173 log.Error("Invalid smartcard socket file type", "path", scpath, "type", fi.Mode().String()) 174 } else { 175 if schub, err := scwallet.NewHub(scpath, scwallet.Scheme, ksLocation); err != nil { 176 log.Warn(fmt.Sprintf("Failed to start smart card hub, disabling: %v", err)) 177 } else { 178 backends = append(backends, schub) 179 } 180 } 181 } 182 } 183 184 // Clef doesn't allow insecure http account unlock. 185 return accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: false}, backends...) 186 } 187 188 // MetadataFromContext extracts Metadata from a given context.Context 189 func MetadataFromContext(ctx context.Context) Metadata { 190 m := Metadata{"NA", "NA", "NA", "", ""} // batman 191 192 if v := ctx.Value("remote"); v != nil { 193 m.Remote = v.(string) 194 } 195 if v := ctx.Value("scheme"); v != nil { 196 m.Scheme = v.(string) 197 } 198 if v := ctx.Value("local"); v != nil { 199 m.Local = v.(string) 200 } 201 if v := ctx.Value("Origin"); v != nil { 202 m.Origin = v.(string) 203 } 204 if v := ctx.Value("User-Agent"); v != nil { 205 m.UserAgent = v.(string) 206 } 207 return m 208 } 209 210 // String implements Stringer interface 211 func (m Metadata) String() string { 212 s, err := json.Marshal(m) 213 if err == nil { 214 return string(s) 215 } 216 return err.Error() 217 } 218 219 // types for the requests/response types between signer and UI 220 type ( 221 // SignTxRequest contains info about a Transaction to sign 222 SignTxRequest struct { 223 Transaction SendTxArgs `json:"transaction"` 224 Callinfo []ValidationInfo `json:"call_info"` 225 Meta Metadata `json:"meta"` 226 } 227 // SignTxResponse result from SignTxRequest 228 SignTxResponse struct { 229 //The UI may make changes to the TX 230 Transaction SendTxArgs `json:"transaction"` 231 Approved bool `json:"approved"` 232 } 233 SignDataRequest struct { 234 ContentType string `json:"content_type"` 235 Address common.MixedcaseAddress `json:"address"` 236 Rawdata []byte `json:"raw_data"` 237 Messages []*NameValueType `json:"messages"` 238 Callinfo []ValidationInfo `json:"call_info"` 239 Hash hexutil.Bytes `json:"hash"` 240 Meta Metadata `json:"meta"` 241 } 242 SignDataResponse struct { 243 Approved bool `json:"approved"` 244 } 245 NewAccountRequest struct { 246 Meta Metadata `json:"meta"` 247 } 248 NewAccountResponse struct { 249 Approved bool `json:"approved"` 250 } 251 ListRequest struct { 252 Accounts []accounts.Account `json:"accounts"` 253 Meta Metadata `json:"meta"` 254 } 255 ListResponse struct { 256 Accounts []accounts.Account `json:"accounts"` 257 } 258 Message struct { 259 Text string `json:"text"` 260 } 261 StartupInfo struct { 262 Info map[string]interface{} `json:"info"` 263 } 264 UserInputRequest struct { 265 Title string `json:"title"` 266 Prompt string `json:"prompt"` 267 IsPassword bool `json:"isPassword"` 268 } 269 UserInputResponse struct { 270 Text string `json:"text"` 271 } 272 ) 273 274 var ErrRequestDenied = errors.New("request denied") 275 276 // NewSignerAPI creates a new API that can be used for Account management. 277 // ksLocation specifies the directory where to store the password protected private 278 // key that is generated when a new Account is created. 279 // noUSB disables USB support that is required to support hardware devices such as 280 // ledger and trezor. 281 func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI { 282 if advancedMode { 283 log.Info("Clef is in advanced mode: will warn instead of reject") 284 } 285 signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials} 286 if !noUSB { 287 signer.startUSBListener() 288 } 289 return signer 290 } 291 func (api *SignerAPI) openTrezor(url accounts.URL) { 292 resp, err := api.UI.OnInputRequired(UserInputRequest{ 293 Prompt: "Pin required to open Trezor wallet\n" + 294 "Look at the device for number positions\n\n" + 295 "7 | 8 | 9\n" + 296 "--+---+--\n" + 297 "4 | 5 | 6\n" + 298 "--+---+--\n" + 299 "1 | 2 | 3\n\n", 300 IsPassword: true, 301 Title: "Trezor unlock", 302 }) 303 if err != nil { 304 log.Warn("failed getting trezor pin", "err", err) 305 return 306 } 307 // We're using the URL instead of the pointer to the 308 // Wallet -- perhaps it is not actually present anymore 309 w, err := api.am.Wallet(url.String()) 310 if err != nil { 311 log.Warn("wallet unavailable", "url", url) 312 return 313 } 314 err = w.Open(resp.Text) 315 if err != nil { 316 log.Warn("failed to open wallet", "wallet", url, "err", err) 317 return 318 } 319 320 } 321 322 // startUSBListener starts a listener for USB events, for hardware wallet interaction 323 func (api *SignerAPI) startUSBListener() { 324 eventCh := make(chan accounts.WalletEvent, 16) 325 am := api.am 326 am.Subscribe(eventCh) 327 // Open any wallets already attached 328 for _, wallet := range am.Wallets() { 329 if err := wallet.Open(""); err != nil { 330 log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err) 331 if err == usbwallet.ErrTrezorPINNeeded { 332 go api.openTrezor(wallet.URL()) 333 } 334 } 335 } 336 go api.derivationLoop(eventCh) 337 } 338 339 // derivationLoop listens for wallet events 340 func (api *SignerAPI) derivationLoop(events chan accounts.WalletEvent) { 341 // Listen for wallet event till termination 342 for event := range events { 343 switch event.Kind { 344 case accounts.WalletArrived: 345 if err := event.Wallet.Open(""); err != nil { 346 log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err) 347 if err == usbwallet.ErrTrezorPINNeeded { 348 go api.openTrezor(event.Wallet.URL()) 349 } 350 } 351 case accounts.WalletOpened: 352 status, _ := event.Wallet.Status() 353 log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status) 354 var derive = func(limit int, next func() accounts.DerivationPath) { 355 // Derive first N accounts, hardcoded for now 356 for i := 0; i < limit; i++ { 357 path := next() 358 if acc, err := event.Wallet.Derive(path, true); err != nil { 359 log.Warn("Account derivation failed", "error", err) 360 } else { 361 log.Info("Derived account", "address", acc.Address, "path", path) 362 } 363 } 364 } 365 log.Info("Deriving default paths") 366 derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.DefaultBaseDerivationPath)) 367 if event.Wallet.URL().Scheme == "ledger" { 368 log.Info("Deriving ledger legacy paths") 369 derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.LegacyLedgerBaseDerivationPath)) 370 log.Info("Deriving ledger live paths") 371 // For ledger live, since it's based off the same (DefaultBaseDerivationPath) 372 // as one we've already used, we need to step it forward one step to avoid 373 // hitting the same path again 374 nextFn := accounts.LedgerLiveIterator(accounts.DefaultBaseDerivationPath) 375 nextFn() 376 derive(numberOfAccountsToDerive, nextFn) 377 } 378 case accounts.WalletDropped: 379 log.Info("Old wallet dropped", "url", event.Wallet.URL()) 380 event.Wallet.Close() 381 } 382 } 383 } 384 385 // List returns the set of wallet this signer manages. Each wallet can contain 386 // multiple accounts. 387 func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) { 388 var accs = make([]accounts.Account, 0) 389 // accs is initialized as empty list, not nil. We use 'nil' to signal 390 // rejection, as opposed to an empty list. 391 for _, wallet := range api.am.Wallets() { 392 accs = append(accs, wallet.Accounts()...) 393 } 394 result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) 395 if err != nil { 396 return nil, err 397 } 398 if result.Accounts == nil { 399 return nil, ErrRequestDenied 400 } 401 addresses := make([]common.Address, 0) 402 for _, acc := range result.Accounts { 403 addresses = append(addresses, acc.Address) 404 } 405 return addresses, nil 406 } 407 408 // New creates a new password protected Account. The private key is protected with 409 // the given password. Users are responsible to backup the private key that is stored 410 // in the keystore location thas was specified when this API was created. 411 func (api *SignerAPI) New(ctx context.Context) (common.Address, error) { 412 if be := api.am.Backends(keystore.KeyStoreType); len(be) == 0 { 413 return common.Address{}, errors.New("password based accounts not supported") 414 } 415 if resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}); err != nil { 416 return common.Address{}, err 417 } else if !resp.Approved { 418 return common.Address{}, ErrRequestDenied 419 } 420 return api.newAccount() 421 } 422 423 // newAccount is the internal method to create a new account. It should be used 424 // _after_ user-approval has been obtained 425 func (api *SignerAPI) newAccount() (common.Address, error) { 426 be := api.am.Backends(keystore.KeyStoreType) 427 if len(be) == 0 { 428 return common.Address{}, errors.New("password based accounts not supported") 429 } 430 // Three retries to get a valid password 431 for i := 0; i < 3; i++ { 432 resp, err := api.UI.OnInputRequired(UserInputRequest{ 433 "New account password", 434 fmt.Sprintf("Please enter a password for the new account to be created (attempt %d of 3)", i), 435 true}) 436 if err != nil { 437 log.Warn("error obtaining password", "attempt", i, "error", err) 438 continue 439 } 440 if pwErr := ValidatePasswordFormat(resp.Text); pwErr != nil { 441 api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", i+1, pwErr)) 442 } else { 443 // No error 444 acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Text) 445 log.Info("Your new key was generated", "address", acc.Address) 446 log.Warn("Please backup your key file!", "path", acc.URL.Path) 447 log.Warn("Please remember your password!") 448 return acc.Address, err 449 } 450 } 451 // Otherwise fail, with generic error message 452 return common.Address{}, errors.New("account creation failed") 453 } 454 455 // logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. 456 // it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow 457 // UI-modifications to requests 458 func logDiff(original *SignTxRequest, new *SignTxResponse) bool { 459 var intPtrModified = func(a, b *hexutil.Big) bool { 460 aBig := (*big.Int)(a) 461 bBig := (*big.Int)(b) 462 if aBig != nil && bBig != nil { 463 return aBig.Cmp(bBig) != 0 464 } 465 // One or both of them are nil 466 return a != b 467 } 468 469 modified := false 470 if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { 471 log.Info("Sender-account changed by UI", "was", f0, "is", f1) 472 modified = true 473 } 474 if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { 475 log.Info("Recipient-account changed by UI", "was", t0, "is", t1) 476 modified = true 477 } 478 if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 { 479 modified = true 480 log.Info("Gas changed by UI", "was", g0, "is", g1) 481 } 482 if a, b := original.Transaction.GasPrice, new.Transaction.GasPrice; intPtrModified(a, b) { 483 log.Info("GasPrice changed by UI", "was", a, "is", b) 484 modified = true 485 } 486 if a, b := original.Transaction.MaxPriorityFeePerGas, new.Transaction.MaxPriorityFeePerGas; intPtrModified(a, b) { 487 log.Info("maxPriorityFeePerGas changed by UI", "was", a, "is", b) 488 modified = true 489 } 490 if a, b := original.Transaction.MaxFeePerGas, new.Transaction.MaxFeePerGas; intPtrModified(a, b) { 491 log.Info("maxFeePerGas changed by UI", "was", a, "is", b) 492 modified = true 493 } 494 if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { 495 modified = true 496 log.Info("Value changed by UI", "was", v0, "is", v1) 497 } 498 if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { 499 d0s := "" 500 d1s := "" 501 if d0 != nil { 502 d0s = hexutil.Encode(*d0) 503 } 504 if d1 != nil { 505 d1s = hexutil.Encode(*d1) 506 } 507 if d1s != d0s { 508 modified = true 509 log.Info("Data changed by UI", "was", d0s, "is", d1s) 510 } 511 } 512 if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { 513 modified = true 514 log.Info("Nonce changed by UI", "was", n0, "is", n1) 515 } 516 return modified 517 } 518 519 func (api *SignerAPI) lookupPassword(address common.Address) (string, error) { 520 return api.credentials.Get(address.Hex()) 521 } 522 523 func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) { 524 // Look up the password and return if available 525 if pw, err := api.lookupPassword(address); err == nil { 526 return pw, nil 527 } 528 // Password unavailable, request it from the user 529 pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true}) 530 if err != nil { 531 log.Warn("error obtaining password", "error", err) 532 // We'll not forward the error here, in case the error contains info about the response from the UI, 533 // which could leak the password if it was malformed json or something 534 return "", errors.New("internal error") 535 } 536 return pwResp.Text, nil 537 } 538 539 // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form 540 func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { 541 var ( 542 err error 543 result SignTxResponse 544 ) 545 msgs, err := api.validator.ValidateTransaction(methodSelector, &args) 546 if err != nil { 547 return nil, err 548 } 549 // If we are in 'rejectMode', then reject rather than show the user warnings 550 if api.rejectMode { 551 if err := msgs.getWarnings(); err != nil { 552 return nil, err 553 } 554 } 555 if args.ChainID != nil { 556 requestedChainId := (*big.Int)(args.ChainID) 557 if api.chainID.Cmp(requestedChainId) != 0 { 558 log.Error("Signing request with wrong chain id", "requested", requestedChainId, "configured", api.chainID) 559 return nil, fmt.Errorf("requested chainid %d does not match the configuration of the signer", 560 requestedChainId) 561 } 562 } 563 req := SignTxRequest{ 564 Transaction: args, 565 Meta: MetadataFromContext(ctx), 566 Callinfo: msgs.Messages, 567 } 568 // Process approval 569 result, err = api.UI.ApproveTx(&req) 570 if err != nil { 571 return nil, err 572 } 573 if !result.Approved { 574 return nil, ErrRequestDenied 575 } 576 // Log changes made by the UI to the signing-request 577 logDiff(&req, &result) 578 var ( 579 acc accounts.Account 580 wallet accounts.Wallet 581 ) 582 acc = accounts.Account{Address: result.Transaction.From.Address()} 583 wallet, err = api.am.Find(acc) 584 if err != nil { 585 return nil, err 586 } 587 // Convert fields into a real transaction 588 var unsignedTx = result.Transaction.toTransaction() 589 // Get the password for the transaction 590 pw, err := api.lookupOrQueryPassword(acc.Address, "Account password", 591 fmt.Sprintf("Please enter the password for account %s", acc.Address.String())) 592 if err != nil { 593 return nil, err 594 } 595 // The one to sign is the one that was returned from the UI 596 signedTx, err := wallet.SignTxWithPassphrase(acc, pw, unsignedTx, api.chainID) 597 if err != nil { 598 api.UI.ShowError(err.Error()) 599 return nil, err 600 } 601 602 data, err := signedTx.MarshalBinary() 603 if err != nil { 604 return nil, err 605 } 606 response := ethapi.SignTransactionResult{Raw: data, Tx: signedTx} 607 608 // Finally, send the signed tx to the UI 609 api.UI.OnApprovedTx(response) 610 // ...and to the external caller 611 return &response, nil 612 613 } 614 615 func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { 616 // Do the usual validations, but on the last-stage transaction 617 args := gnosisTx.ArgsForValidation() 618 msgs, err := api.validator.ValidateTransaction(methodSelector, args) 619 if err != nil { 620 return nil, err 621 } 622 // If we are in 'rejectMode', then reject rather than show the user warnings 623 if api.rejectMode { 624 if err := msgs.getWarnings(); err != nil { 625 return nil, err 626 } 627 } 628 typedData := gnosisTx.ToTypedData() 629 signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs) 630 if err != nil { 631 return nil, err 632 } 633 checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex()) 634 635 gnosisTx.Signature = signature 636 gnosisTx.SafeTxHash = common.BytesToHash(preimage) 637 gnosisTx.Sender = *checkSummedSender // Must be checksumed to be accepted by relay 638 639 return &gnosisTx, nil 640 } 641 642 // Returns the external api version. This method does not require user acceptance. Available methods are 643 // available via enumeration anyway, and this info does not contain user-specific data 644 func (api *SignerAPI) Version(ctx context.Context) (string, error) { 645 return ExternalAPIVersion, nil 646 }