diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4468b7a..a76b964 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -44,15 +39,10 @@ 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 @@ -60,15 +50,12 @@ jobs: 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 @@ -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 diff --git a/.gitignore b/.gitignore index 3383004..304de18 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ deps/ go.work .idea + +# Default build artifact +smcli \ No newline at end of file diff --git a/Makefile b/Makefile index 0e0680d..3284a83 100644 --- a/Makefile +++ b/Makefile @@ -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) @@ -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 @@ -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) @@ -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) @@ -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: @@ -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: diff --git a/cmd/wallet.go b/cmd/wallet.go index db5aa58..a19d22b 100644 --- a/cmd/wallet.go +++ b/cmd/wallet.go @@ -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. @@ -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 @@ -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): ") @@ -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] @@ -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, @@ -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") } diff --git a/go.mod b/go.mod index b435aaa..50a3f88 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/btcsuite/btcutil v1.0.2 github.com/jedib0t/go-pretty/v6 v6.4.6 - github.com/spacemeshos/smkeys v1.0.2 + github.com/spacemeshos/smkeys v1.0.3 github.com/stretchr/testify v1.8.2 ) @@ -17,11 +17,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.7.0 @@ -31,9 +31,9 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/tyler-smith/go-bip39 v1.1.0 github.com/xdg-go/pbkdf2 v1.0.0 - golang.org/x/crypto v0.8.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/sys v0.8.0 // indirect - golang.org/x/term v0.7.0 // indirect + golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 3f09e27..f30086c 100644 --- a/go.sum +++ b/go.sum @@ -161,8 +161,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -177,14 +178,15 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spacemeshos/smkeys v1.0.2 h1:6bD2+CsLkd+gNUojyvjyu/5dvxMKktkGndtuFqTbK+s= -github.com/spacemeshos/smkeys v1.0.2/go.mod h1:gj9yv0Zek5D9p6zWmVV/2d0WdhPwyKXDMQm2MpmxIow= +github.com/spacemeshos/smkeys v1.0.3 h1:v1O8NgRtSTCMBClvBM/MqxWJ35moKYURadFDwZRQki4= +github.com/spacemeshos/smkeys v1.0.3/go.mod h1:gj9yv0Zek5D9p6zWmVV/2d0WdhPwyKXDMQm2MpmxIow= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -237,8 +239,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -378,8 +381,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/wallet/bip32.go b/wallet/bip32.go index 262262e..10d7c53 100644 --- a/wallet/bip32.go +++ b/wallet/bip32.go @@ -7,6 +7,7 @@ import ( "fmt" smbip32 "github.com/spacemeshos/smkeys/bip32" + ledger "github.com/spacemeshos/smkeys/remote-wallet" "github.com/spacemeshos/smcli/common" ) @@ -16,6 +17,13 @@ import ( type PublicKey ed25519.PublicKey +type keyType int + +const ( + typeSoftware keyType = iota + typeLedger +) + func (k *PublicKey) MarshalJSON() ([]byte, error) { return json.Marshal(hex.EncodeToString(*k)) } @@ -50,6 +58,7 @@ type EDKeyPair struct { Path HDPath `json:"path"` Public PublicKey `json:"publicKey"` Private PrivateKey `json:"secretKey"` + KeyType keyType `json:"keyType"` } func NewMasterKeyPair(seed []byte) (*EDKeyPair, error) { @@ -70,15 +79,50 @@ func NewMasterKeyPair(seed []byte) (*EDKeyPair, error) { func (kp *EDKeyPair) NewChildKeyPair(seed []byte, childIdx int) (*EDKeyPair, error) { path := kp.Path.Extend(BIP44HardenedAccountIndex(uint32(childIdx))) - key, err := smbip32.Derive(HDPathToString(path), seed) + switch kp.KeyType { + case typeLedger: + return pubkeyFromLedger(path, false) + case typeSoftware: + key, err := smbip32.Derive(HDPathToString(path), seed) + if err != nil { + return nil, err + } + return &EDKeyPair{ + DisplayName: fmt.Sprintf("Child Key %d", childIdx), + Created: common.NowTimeString(), + Private: key[:], + Public: PublicKey(ed25519.PrivateKey(key).Public().(ed25519.PublicKey)), + Path: path, + }, nil + default: + return nil, fmt.Errorf("unknown key type") + } +} + +func NewMasterKeyPairFromLedger() (*EDKeyPair, error) { + return pubkeyFromLedger(DefaultPath(), true) +} + +func pubkeyFromLedger(path HDPath, master bool) (*EDKeyPair, error) { + // TODO: support multiple ledger devices (https://github.com/spacemeshos/smcli/issues/46) + // don't bother confirming the master key; we only want the user to have to confirm a single key, + // the one they really care about, which is the first child key. + key, err := ledger.ReadPubkeyFromLedger("", HDPathToString(path), !master) if err != nil { - return nil, err + return nil, fmt.Errorf("error reading pubkey from ledger. Are you sure it's connected, unlocked, and the Spacemesh app is open? err: %w", err) + } + + name := "Ledger Master Key" + if !master { + name = "Ledger Child Key" } + return &EDKeyPair{ - DisplayName: fmt.Sprintf("Child Key %d", childIdx), + DisplayName: name, Created: common.NowTimeString(), - Private: key[:], - Public: PublicKey(ed25519.PrivateKey(key[:]).Public().(ed25519.PublicKey)), - Path: path, + // note: we do not set a Private key here (it lives on the device) + Public: key[:], + Path: path, + KeyType: typeLedger, }, nil } diff --git a/wallet/wallet.go b/wallet/wallet.go index ed7fd61..338b53c 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -108,6 +108,22 @@ func NewMultiWalletFromMnemonic(m string, n int) (*Wallet, error) { return walletFromMnemonicAndAccounts(m, masterKeyPair, accounts) } +func NewMultiWalletFromLedger(n int) (*Wallet, error) { + if n < 0 || n > common.MaxAccountsPerWallet { + return nil, fmt.Errorf("invalid number of accounts") + } + masterKeyPair, err := NewMasterKeyPairFromLedger() + if err != nil { + return nil, err + } + // seed is not used in case of ledger + accounts, err := accountsFromMaster(masterKeyPair, []byte{}, n) + if err != nil { + return nil, err + } + return walletFromMnemonicAndAccounts("(none)", masterKeyPair, accounts) +} + func walletFromMnemonicAndAccounts(m string, masterKp *EDKeyPair, kp []*EDKeyPair) (*Wallet, error) { w := &Wallet{ Meta: walletMetadata{