Skip to content

Commit

Permalink
Merge pull request #47 from spacemeshos/ledger
Browse files Browse the repository at this point in the history
Add basic Ledger hardware wallet support
  • Loading branch information
lrettig authored May 13, 2023
2 parents 1566c28 + c7cbcd4 commit eec3c4f
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 108 deletions.
28 changes: 5 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ jobs:
uses: actions/setup-go@v4
with:
go-version: ${{ env.go-version }}
- name: install musl
uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: musl-tools # provides musl-gcc
version: 1.0
- name: fmt, tidy
run: |
make install
Expand All @@ -44,31 +39,23 @@ jobs:
uses: actions/setup-go@v4
with:
go-version: ${{ env.go-version }}
- name: install musl
uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: musl-tools # provides musl-gcc
version: 1.0
- name: setup env
run: make install
- name: lint
run: make lint-github-action
run: |
make install
make lint-github-action
build:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: checkout
uses: actions/checkout@v3
- name: install udev
run: sudo apt-get install -y libudev-dev
- name: set up go
uses: actions/setup-go@v4
with:
go-version: ${{ env.go-version }}
- name: install musl
uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: musl-tools # provides musl-gcc
version: 1.0
- name: build
run: make build

Expand All @@ -82,10 +69,5 @@ jobs:
uses: actions/setup-go@v4
with:
go-version: ${{ env.go-version }}
- name: install musl
uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: musl-tools # provides musl-gcc
version: 1.0
- name: go test
run: make test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ deps/
go.work

.idea

# Default build artifact
smcli
58 changes: 35 additions & 23 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# Based on https://gist.github.com/trosendal/d4646812a43920bfe94e

DEPTAG := 1.0.7
DEPLIBNAME := ed25519_bip32
DEPTAG := 0.0.1
DEPLIBNAME := spacemesh-sdk
DEPLOC := https://github.com/spacemeshos/$(DEPLIBNAME)/releases/download
DEPLIB := lib$(DEPLIBNAME)
# Exclude dylib files (we only need the static libs)
EXCLUDE_PATTERN := "LICENSE" "*.so" "*.dylib"
UNZIP_DEST := deps
REAL_DEST := $(shell realpath .)/$(UNZIP_DEST)
DOWNLOAD_DEST := $(UNZIP_DEST)/$(DEPLIB).tar.gz
EXTLDFLAGS := -L$(UNZIP_DEST) -l$(DEPLIBNAME)
DOWNLOAD_DEST := $(UNZIP_DEST)/$(DEPLIBNAME).tar.gz
STATICLDFLAGS := -L$(UNZIP_DEST) -led25519_bip32 -lspacemesh_remote_wallet

# Detect operating system
ifeq ($(OS),Windows_NT)
Expand Down Expand Up @@ -52,19 +49,18 @@ ifeq ($(GOOS),linux)
MACHINE = linux

# Linux specific settings
# We do a static build on Linux using musl toolchain
CPREFIX = CC=musl-gcc
LDFLAGS = -linkmode external -extldflags "-static $(EXTLDFLAGS)"
# We statically link our own libraries and dynamically link other required libraries
LDFLAGS = -linkmode external -extldflags "-Wl,-Bstatic $(STATICLDFLAGS) -Wl,-Bdynamic -ludev -lm"
else ifeq ($(GOOS),darwin)
MACHINE = macos

# macOS specific settings
# dynamic build using default toolchain
LDFLAGS = -extldflags "$(EXTLDFLAGS)"
LDFLAGS = -extldflags "$(STATICLDFLAGS)"
else ifeq ($(GOOS),windows)
# static build using default toolchain
# add a few extra required libs
LDFLAGS = -linkmode external -extldflags "-static $(EXTLDFLAGS) -lws2_32 -luserenv -lbcrypt"
LDFLAGS = -linkmode external -extldflags "-static $(STATICLDFLAGS) -lws2_32 -luserenv -lbcrypt"
else
$(error Unknown operating system: $(GOOS))
endif
Expand All @@ -77,18 +73,14 @@ ifeq ($(SYSTEM),windows)
RMDIR = rmdir /S /Q
MKDIR = mkdir

FN = $(DEPLIB)_windows-amd64.zip
DOWNLOAD_DEST = $(UNZIP_DEST)/$(DEPLIB).zip
FN = $(DEPLIBNAME)_windows-amd64.tar.gz
DOWNLOAD_DEST = $(UNZIP_DEST)/$(DEPLIBNAME).zip
EXTRACT = 7z x -y

# TODO: fix this, it doesn't seem to work as expected
#EXCLUDES = -x!$(EXCLUDE_PATTERN)
else
# Linux and macOS settings
RM = rm -f
RMDIR = rm -rf
MKDIR = mkdir -p
EXCLUDES = $(addprefix --exclude=,$(EXCLUDE_PATTERN))
EXTRACT = tar -xzf

ifeq ($(GOARCH),amd64)
Expand All @@ -98,11 +90,11 @@ else
else
$(error Unknown processor architecture: $(GOARCH))
endif
FN = $(DEPLIB)_$(PLATFORM).tar.gz
FN = $(DEPLIBNAME)_$(PLATFORM).tar.gz
endif

$(UNZIP_DEST): $(DOWNLOAD_DEST)
cd $(UNZIP_DEST) && $(EXTRACT) ../$(DOWNLOAD_DEST) $(EXCLUDES)
cd $(UNZIP_DEST) && $(EXTRACT) ../$(DOWNLOAD_DEST)

$(DOWNLOAD_DEST):
$(MKDIR) $(UNZIP_DEST)
Expand All @@ -121,11 +113,19 @@ tidy:

.PHONY: build
build: $(UNZIP_DEST)
$(CPREFIX) GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=1 go build -ldflags '$(LDFLAGS)'
CGO_CFLAGS="-I$(REAL_DEST)" \
CGO_LDFLAGS="-L$(REAL_DEST)" \
GOOS=$(GOOS) \
GOARCH=$(GOARCH) \
CGO_ENABLED=1 \
go build -ldflags '$(LDFLAGS)'

.PHONY: test
test: $(UNZIP_DEST)
LD_LIBRARY_PATH=$(REAL_DEST) go test -v -ldflags "-extldflags \"-L$(REAL_DEST) -led25519_bip32\"" ./...
CGO_CFLAGS="-I$(REAL_DEST)" \
CGO_LDFLAGS="-L$(REAL_DEST)" \
LD_LIBRARY_PATH=$(REAL_DEST) \
go test -v -count 1 -ldflags "-extldflags \"$(STATICLDFLAGS)\"" ./...

.PHONY: test-tidy
test-tidy:
Expand All @@ -144,19 +144,31 @@ test-fmt:

.PHONY: lint
lint:
CGO_CFLAGS="-I$(REAL_DEST)" \
CGO_LDFLAGS="-L$(REAL_DEST)" \
LD_LIBRARY_PATH=$(REAL_DEST) \
./bin/golangci-lint run --config .golangci.yml

# Auto-fixes golangci-lint issues where possible.
.PHONY: lint-fix
lint-fix:
CGO_CFLAGS="-I$(REAL_DEST)" \
CGO_LDFLAGS="-L$(REAL_DEST)" \
LD_LIBRARY_PATH=$(REAL_DEST) \
./bin/golangci-lint run --config .golangci.yml --fix

.PHONY: lint-github-action
lint-github-action:
CGO_CFLAGS="-I$(REAL_DEST)" \
CGO_LDFLAGS="-L$(REAL_DEST)" \
LD_LIBRARY_PATH=$(REAL_DEST) \
./bin/golangci-lint run --config .golangci.yml --out-format=github-actions

.PHONY: staticcheck
staticcheck:
staticcheck: $(UNZIP_DEST)
CGO_CFLAGS="-I$(REAL_DEST)" \
CGO_LDFLAGS="-L$(REAL_DEST)" \
LD_LIBRARY_PATH=$(REAL_DEST) \
staticcheck ./...

clean:
Expand Down
125 changes: 80 additions & 45 deletions cmd/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ var (

// printBase58 indicates that keys should be printed in base58 format.
printBase58 bool

// printParent indicates that the parent key should be printed.
printParent bool

// useLedger indicates that the Ledger device should be used.
useLedger bool
)

// walletCmd represents the wallet command.
Expand All @@ -46,10 +52,14 @@ to quickly create a Cobra application.`,

// createCmd represents the create command.
var createCmd = &cobra.Command{
Use: "create [numaccounts]",
Short: "Generate a new wallet file from a BIP-39-compatible mnemonic",
Long: `Create a new wallet file containing one or more accounts using a BIP-39-compatible mnemonic.
You can choose to use an existing mnemonic or generate a new, random mnemonic.`,
Use: "create [--ledger] [numaccounts]",
Short: "Generate a new wallet file from a BIP-39-compatible mnemonic or Ledger device",
Long: `Create a new wallet file containing one or more accounts using a BIP-39-compatible mnemonic
or a Ledger hardware wallet. If using a mnemonic you can choose to use an existing mnemonic or generate
a new, random mnemonic.
Add --ledger to instead read the public key from a Ledger device. If using a Ledger device please make
sure the device is connected, unlocked, and the Spacemesh app is open.`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// get the number of accounts to create
Expand All @@ -60,31 +70,41 @@ You can choose to use an existing mnemonic or generate a new, random mnemonic.`,
n = int(tmpN)
}

// get or generate the mnemonic
fmt.Print("Enter a BIP-39-compatible mnemonic (or leave blank to generate a new one): ")
text, err := password.Read(os.Stdin)
fmt.Println()
cobra.CheckErr(err)
fmt.Println("Note: This application does not yet support BIP-39-compatible optional passwords. Support will be added soon.")

// It's critical that we trim whitespace, including CRLF. Otherwise it will get included in the mnemonic.
text = strings.TrimSpace(text)

var w *wallet.Wallet
if text == "" {
w, err = wallet.NewMultiWalletRandomMnemonic(n)
var err error

// Short-circuit and check for a ledger device
if useLedger {
w, err = wallet.NewMultiWalletFromLedger(n)
cobra.CheckErr(err)
fmt.Println("\nThis is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to restore your wallet.")
fmt.Println("Neither Spacemesh nor anyone else can help you restore your wallet without this mnemonic.")
fmt.Println("\n***********************************\nSAVE THIS MNEMONIC IN A SAFE PLACE!\n***********************************")
fmt.Println()
fmt.Println(w.Mnemonic())
fmt.Println("\nPress enter when you have securely saved your mnemonic.")
_, _ = fmt.Scanln()
fmt.Println("Note that, when using a hardware wallet, the wallet file I'm about to produce won't " +
"contain any private keys or mnemonics, but you may still choose to encrypt it to protect privacy.")
} else {
// try to use as a mnemonic
w, err = wallet.NewMultiWalletFromMnemonic(text, n)
// get or generate the mnemonic
fmt.Print("Enter a BIP-39-compatible mnemonic (or leave blank to generate a new one): ")
text, err := password.Read(os.Stdin)
fmt.Println()
cobra.CheckErr(err)
fmt.Println("Note: This application does not yet support BIP-39-compatible optional passwords. Support will be added soon.")

// It's critical that we trim whitespace, including CRLF. Otherwise it will get included in the mnemonic.
text = strings.TrimSpace(text)

if text == "" {
w, err = wallet.NewMultiWalletRandomMnemonic(n)
cobra.CheckErr(err)
fmt.Println("\nThis is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to restore your wallet.")
fmt.Println("Neither Spacemesh nor anyone else can help you restore your wallet without this mnemonic.")
fmt.Println("\n***********************************\nSAVE THIS MNEMONIC IN A SAFE PLACE!\n***********************************")
fmt.Println()
fmt.Println(w.Mnemonic())
fmt.Println("\nPress enter when you have securely saved your mnemonic.")
_, _ = fmt.Scanln()
} else {
// try to use as a mnemonic
w, err = wallet.NewMultiWalletFromMnemonic(text, n)
cobra.CheckErr(err)
}
}

fmt.Print("Enter a secure password used to encrypt the wallet file (optional but strongly recommended): ")
Expand Down Expand Up @@ -125,7 +145,8 @@ var readCmd = &cobra.Command{
successfully read and decrypted, whether the password to open the file is correct, etc.
It prints the accounts from the wallet file. By default it does not print private keys.
Add --private to print private keys. Add --full to print full keys. Add --base58 to print
keys in base58 format rather than hexadecimal.`,
keys in base58 format rather than hexadecimal. Add --parent to print parent key (and not
only child keys).`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
walletFn := args[0]
Expand Down Expand Up @@ -201,36 +222,48 @@ keys in base58 format rather than hexadecimal.`,
})
}

// print the master keypair
master := w.Secrets.MasterKeypair
// set the encoder
encoder := hex.EncodeToString
if printBase58 {
encoder = base58.Encode
}
if master != nil {
if printPrivate {
t.AppendRow(table.Row{
encoder(master.Public),
encoder(master.Private),
master.Path.String(),
master.DisplayName,
master.Created,
})
} else {
t.AppendRow(table.Row{
encoder(master.Public),
master.Path.String(),
master.DisplayName,
master.Created,
})

privKeyEncoder := func(privKey []byte) string {
if len(privKey) == 0 {
return "(none)"
}
return encoder(privKey)
}

// print the master account
if printParent {
master := w.Secrets.MasterKeypair
if master != nil {
if printPrivate {
t.AppendRow(table.Row{
encoder(master.Public),
privKeyEncoder(master.Private),
master.Path.String(),
master.DisplayName,
master.Created,
})
} else {
t.AppendRow(table.Row{
encoder(master.Public),
master.Path.String(),
master.DisplayName,
master.Created,
})
}
}
}

// print child accounts
for _, a := range w.Secrets.Accounts {
if printPrivate {
t.AppendRow(table.Row{
encoder(a.Public),
encoder(a.Private),
privKeyEncoder(a.Private),
a.Path.String(),
a.DisplayName,
a.Created,
Expand All @@ -255,5 +288,7 @@ func init() {
readCmd.Flags().BoolVarP(&printPrivate, "private", "p", false, "Print private keys")
readCmd.Flags().BoolVarP(&printFull, "full", "f", false, "Print full keys (no abbreviation)")
readCmd.Flags().BoolVar(&printBase58, "base58", false, "Print keys in base58 (rather than hex)")
readCmd.Flags().BoolVar(&printParent, "parent", false, "Print parent key (not only child keys)")
readCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug mode")
createCmd.Flags().BoolVarP(&useLedger, "ledger", "l", false, "Create a wallet using a Ledger device")
}
Loading

0 comments on commit eec3c4f

Please sign in to comment.