Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imp(types): Add required wallet types for integration #19

Merged
merged 3 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading