Skip to content

Commit

Permalink
imp(types): Add required wallet types for integration (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
MalteHerrmann committed Aug 5, 2024
1 parent 9c637f2 commit 579bac9
Show file tree
Hide file tree
Showing 13 changed files with 2,071 additions and 6 deletions.
3 changes: 2 additions & 1 deletion .clconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"proto",
"rpc",
"staking-precompile",
"tests"
"tests",
"types"
],
"change_types": {
"API Breaking": "api\\s*breaking",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This changelog was created using the `clu` binary

### Improvements

- (types) [#19](https://github.com/evmos/os/pull/19) Add required wallet types for integration.
- (all) [#15](https://github.com/evmos/os/pull/15) Add general types and utils.
- (proto) [#14](https://github.com/evmos/os/pull/14) Add Protobufs and adjust scripts.
- (eip-712) [#13](https://github.com/evmos/os/pull/13) Add EIP-712 package.
Expand Down
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ require (
cosmossdk.io/simapp v0.0.0-20230608160436-666c345ad23d
github.com/cometbft/cometbft v0.37.9
github.com/cosmos/cosmos-sdk v0.47.12
github.com/cosmos/gogoproto v1.4.10
github.com/cosmos/ibc-go/v7 v7.6.0
github.com/ethereum/go-ethereum v1.11.5
github.com/evmos/evmos/v19 v19.0.0-20240731212153-b36241652b57
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.3
github.com/tidwall/sjson v1.2.5
github.com/zondax/hid v0.9.2
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/text v0.16.0
)

Expand Down Expand Up @@ -56,9 +60,7 @@ require (
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.4.10 // indirect
github.com/cosmos/iavl v0.21.0-alpha.1.0.20230904092046-df3db2d96583 // indirect
github.com/cosmos/ibc-go/v7 v7.6.0 // indirect
github.com/cosmos/ics23/go v0.10.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect
github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect
Expand Down Expand Up @@ -171,6 +173,7 @@ require (
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
Expand All @@ -183,7 +186,6 @@ require (
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/zbiljic/go-filelock v0.0.0-20170914061330-1dbf7103ab7d // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v0.14.3 // indirect
go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect
go.opencensus.io v0.24.0 // indirect
Expand All @@ -194,7 +196,6 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sync v0.7.0 // indirect
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,6 @@ github.com/evmos/cosmos-sdk v0.47.12-evmos.2 h1:NODyhYKCqu8JNLeR6b6ff0+TS3KYdcBi
github.com/evmos/cosmos-sdk v0.47.12-evmos.2/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0=
github.com/evmos/evmos/v19 v19.0.0-20240731212153-b36241652b57 h1:C+JOScyVYgoASWrdIBmCYPBRbOpQgvGMCc8URWdq+xQ=
github.com/evmos/evmos/v19 v19.0.0-20240731212153-b36241652b57/go.mod h1:HEPvi70nAyQyzYaDqtB2x33lwQ80wKVIyTRNnufjTg8=
github.com/evmos/evmos/v19 v19.0.0/go.mod h1:0BtH6AsIRvAaNmSIfIYGH3AaXgWtq8ZBTdmYV08VZjE=
github.com/evmos/go-ethereum v1.10.26-evmos-rc4 h1:vwDVMScuB2KSu8ze5oWUuxm6v3bMUp6dL3PWvJNJY+I=
github.com/evmos/go-ethereum v1.10.26-evmos-rc4/go.mod h1:/6CsT5Ceen2WPLI/oCA3xMcZ5sWMF/D46SjM/ayY0Oo=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
Expand Down
80 changes: 80 additions & 0 deletions wallets/accounts/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
package accounts

import (
"crypto/ecdsa"

gethaccounts "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)

// Account represents an Ethereum account located at a specific location defined
// by the optional URL field.
type Account struct {
Address common.Address `json:"address"` // Ethereum account address derived from the key
PublicKey *ecdsa.PublicKey `json:"publicKey"` // Public key corresponding to the account address
}

// Wallet represents a software or hardware wallet that might contain one or more
// accounts (derived from the same seed).
type Wallet interface {
// URL retrieves the canonical path under which this wallet is reachable. It is
// used by upper layers to define a sorting order over all wallets from multiple
// backends.
URL() gethaccounts.URL

// Status returns a textual status to aid the user in the current state of the
// wallet. It also returns an error indicating any failure the wallet might have
// encountered.
Status() (string, error)

// Open initializes access to a wallet instance. It is not meant to unlock or
// decrypt account keys, rather simply to establish a connection to hardware
// wallets and/or to access derivation seeds.
//
// The passphrase parameter may or may not be used by the implementation of a
// particular wallet instance. The reason there is no password-less open method
// is to strive towards a uniform wallet handling, oblivious to the different
// backend providers.
//
// Please note, if you open a wallet, you must close it to release any allocated
// resources (especially important when working with hardware wallets).
Open(passphrase string) error

// Close releases any resources held by an open wallet instance.
Close() error

// Accounts retrieves the list of signing accounts the wallet is currently aware
// of. For hierarchical deterministic wallets, the list will not be exhaustive,
// rather only contain the accounts explicitly pinned during account derivation.
Accounts() []Account

// Contains returns whether an account is part of this particular wallet or not.
Contains(account Account) bool

// Derive attempts to explicitly derive a hierarchical deterministic account at
// the specified derivation path. If requested, the derived account will be added
// to the wallet's tracked account list.
Derive(path gethaccounts.DerivationPath, pin bool) (Account, error)

// SignTypedData signs a TypedData object using EIP-712 encoding
SignTypedData(account Account, typedData apitypes.TypedData) ([]byte, error)
}

// Backend is a "wallet provider" that may contain a batch of accounts they can
// sign transactions with and upon request, do so.
type Backend interface {
// Wallets retrieves the list of wallets the backend is currently aware of.
//
// The returned wallets are not opened by default. For software HD wallets this
// means that no base seeds are decrypted, and for hardware wallets that no actual
// connection is established.
//
// The resulting wallet list will be sorted alphabetically based on its internal
// URL assigned by the backend. Since wallets (especially hardware) may come and
// go, the same wallet might appear at a different positions in the list during
// subsequent retrievals.
Wallets() []Wallet
}
190 changes: 190 additions & 0 deletions wallets/ledger/ledger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)

package ledger

import (
"encoding/hex"
"errors"
"fmt"
"strings"

sdkledger "github.com/cosmos/cosmos-sdk/crypto/ledger"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core/apitypes"

"github.com/evmos/os/ethereum/eip712"
"github.com/evmos/os/wallets/accounts"
"github.com/evmos/os/wallets/usbwallet"
)

// Secp256k1DerivationFn defines the derivation function used on the Cosmos SDK Keyring.
type Secp256k1DerivationFn func() (sdkledger.SECP256K1, error)

func EvmosLedgerDerivation() Secp256k1DerivationFn {
evmosSECP256K1 := new(EvmosSECP256K1)

return func() (sdkledger.SECP256K1, error) {
return evmosSECP256K1.connectToLedgerApp()
}
}

var _ sdkledger.SECP256K1 = &EvmosSECP256K1{}

// EvmosSECP256K1 defines a wrapper of the Ethereum App to
// for compatibility with Cosmos SDK chains.
type EvmosSECP256K1 struct {
*usbwallet.Hub
PrimaryWallet accounts.Wallet
}

// Close closes the associated primary wallet. Any requests on
// the object after a successful Close() should not work
func (e EvmosSECP256K1) Close() error {
if e.PrimaryWallet == nil {
return errors.New("could not close Ledger: no wallet found")
}

return e.PrimaryWallet.Close()
}

// GetPublicKeySECP256K1 returns the public key associated with the address derived from
// the provided hdPath using the primary wallet
func (e EvmosSECP256K1) GetPublicKeySECP256K1(hdPath []uint32) ([]byte, error) {
if e.PrimaryWallet == nil {
return nil, errors.New("could not get Ledger public key: no wallet found")
}

// Re-open wallet in case it was closed. Do not handle the error here (see SignSECP256K1)
_ = e.PrimaryWallet.Open("")

account, err := e.PrimaryWallet.Derive(hdPath, true)
if err != nil {
return nil, errors.New("unable to derive public key, please retry")
}

pubkeyBz := crypto.FromECDSAPub(account.PublicKey)

return pubkeyBz, nil
}

// GetAddressPubKeySECP256K1 takes in the HD path as well as a "Human Readable Prefix" (HRP, e.g. "evmos")
// to return the public key bytes in secp256k1 format as well as the account address.
func (e EvmosSECP256K1) GetAddressPubKeySECP256K1(hdPath []uint32, hrp string) ([]byte, string, error) {
if e.PrimaryWallet == nil {
return nil, "", errors.New("could not get Ledger address: no wallet found")
}

// Re-open wallet in case it was closed. Ignore the error here (see SignSECP256K1)
_ = e.PrimaryWallet.Open("")

account, err := e.PrimaryWallet.Derive(hdPath, true)
if err != nil {
return nil, "", errors.New("unable to derive Ledger address, please open the Ethereum app and retry")
}

address, err := sdk.Bech32ifyAddressBytes(hrp, account.Address.Bytes())
if err != nil {
return nil, "", err
}

pubkeyBz := crypto.FromECDSAPub(account.PublicKey)

return pubkeyBz, address, nil
}

// SignSECP256K1 returns the signature bytes generated from signing a transaction
// using the EIP712 signature.
func (e EvmosSECP256K1) SignSECP256K1(hdPath []uint32, signDocBytes []byte) ([]byte, error) {
fmt.Printf("Generating payload, please check your Ledger...\n")

if e.PrimaryWallet == nil {
return nil, errors.New("unable to sign with Ledger: no wallet found")
}

// Re-open wallet in case it was closed. Since an error occurs if the wallet is already open,
// ignore the error. Any errors due to the wallet being closed will surface later on.
_ = e.PrimaryWallet.Open("")

// Derive requested account
account, err := e.PrimaryWallet.Derive(hdPath, true)
if err != nil {
return nil, errors.New("unable to derive Ledger address, please open the Ethereum app and retry")
}

typedData, err := eip712.GetEIP712TypedDataForMsg(signDocBytes)
if err != nil {
return nil, err
}

// Display EIP-712 message hash for user to verify
if err := e.displayEIP712Hash(typedData); err != nil {
return nil, fmt.Errorf("unable to generate EIP-712 hash for object: %w", err)
}

// Sign with EIP712 signature
signature, err := e.PrimaryWallet.SignTypedData(account, typedData)
if err != nil {
return nil, fmt.Errorf("error generating signature, please retry: %w", err)
}

return signature, nil
}

// displayEIP712Hash is a helper function to display the EIP-712 hashes.
// This allows users to verify the hashed message they are signing via Ledger.
func (e EvmosSECP256K1) displayEIP712Hash(typedData apitypes.TypedData) error {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
return err
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return err
}

fmt.Printf("Signing the following payload with EIP-712:\n")
fmt.Printf("- Domain: %s\n", bytesToHexString(domainSeparator))
fmt.Printf("- Message: %s\n", bytesToHexString(typedDataHash))

return nil
}

func (e *EvmosSECP256K1) connectToLedgerApp() (sdkledger.SECP256K1, error) {
// Instantiate new Ledger object
ledger, err := usbwallet.NewLedgerHub()
if err != nil {
return nil, err
}

if ledger == nil {
return nil, errors.New("no hardware wallets detected")
}

e.Hub = ledger
wallets := e.Wallets()

// No wallets detected; throw an error
if len(wallets) == 0 {
return nil, errors.New("no hardware wallets detected")
}

// Default to use first wallet found
primaryWallet := wallets[0]

// Open wallet for the first time. Unlike with other cases, we want to handle the error here.
if err := primaryWallet.Open(""); err != nil {
return nil, err
}

e.PrimaryWallet = primaryWallet

return e, nil
}

// bytesToHexString is a helper function to convert a slice of bytes to a
// string in hex-format.
func bytesToHexString(bytes []byte) string {
return "0x" + strings.ToUpper(hex.EncodeToString(bytes))
}
Loading

0 comments on commit 579bac9

Please sign in to comment.