cmd/lncli+walletrpc: add createwatchonly command
This commit is contained in:
4 files changed
+276
No files matched your search
@@ -0,0 +1,76 @@
|
||||
package walletrpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
)
|
||||
|
||||
// AccountsToWatchOnly converts the accounts returned by the walletkit's
|
||||
// ListAccounts RPC into a struct that can be used to create a watch-only
|
||||
// wallet.
|
||||
func AccountsToWatchOnly(exported []*Account) ([]*lnrpc.WatchOnlyAccount,
|
||||
error) {
|
||||
|
||||
result := make([]*lnrpc.WatchOnlyAccount, len(exported))
|
||||
for idx, acct := range exported {
|
||||
parsedPath, err := parseDerivationPath(acct.DerivationPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing derivation path "+
|
||||
"of account %d: %v", idx, err)
|
||||
}
|
||||
if len(parsedPath) < 3 {
|
||||
return nil, fmt.Errorf("derivation path of account %d "+
|
||||
"has invalid derivation path, need at least "+
|
||||
"path of depth 3, instead has depth %d", idx,
|
||||
len(parsedPath))
|
||||
}
|
||||
|
||||
result[idx] = &lnrpc.WatchOnlyAccount{
|
||||
Purpose: parsedPath[0],
|
||||
CoinType: parsedPath[1],
|
||||
Account: parsedPath[2],
|
||||
Xpub: acct.ExtendedPublicKey,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseDerivationPath parses a path in the form of m/x'/y'/z'/a/b into a slice
|
||||
// of [x, y, z, a, b], meaning that the apostrophe is ignored and 2^31 is _not_
|
||||
// added to the numbers.
|
||||
func parseDerivationPath(path string) ([]uint32, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if len(path) == 0 {
|
||||
return nil, fmt.Errorf("path cannot be empty")
|
||||
}
|
||||
if !strings.HasPrefix(path, "m/") {
|
||||
return nil, fmt.Errorf("path must start with m/")
|
||||
}
|
||||
|
||||
// Just the root key, no path was provided. This is valid but not useful
|
||||
// in most cases.
|
||||
rest := strings.ReplaceAll(path, "m/", "")
|
||||
if rest == "" {
|
||||
return []uint32{}, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(rest, "/")
|
||||
indices := make([]uint32, len(parts))
|
||||
for i := 0; i < len(parts); i++ {
|
||||
part := parts[i]
|
||||
if strings.Contains(parts[i], "'") {
|
||||
part = strings.TrimRight(parts[i], "'")
|
||||
}
|
||||
parsed, err := strconv.ParseInt(part, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse part \"%s\": "+
|
||||
"%v", part, err)
|
||||
}
|
||||
indices[i] = uint32(parsed)
|
||||
}
|
||||
return indices, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package walletrpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDerivationPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedErr string
|
||||
expectedResult []uint32
|
||||
}{{
|
||||
name: "empty path",
|
||||
path: "",
|
||||
expectedErr: "path cannot be empty",
|
||||
}, {
|
||||
name: "just whitespace",
|
||||
path: " \n\t\r",
|
||||
expectedErr: "path cannot be empty",
|
||||
}, {
|
||||
name: "incorrect prefix",
|
||||
path: "0/0",
|
||||
expectedErr: "path must start with m/",
|
||||
}, {
|
||||
name: "invalid number",
|
||||
path: "m/a'/0'",
|
||||
expectedErr: "could not parse part \"a\": strconv.ParseInt",
|
||||
}, {
|
||||
name: "double slash",
|
||||
path: "m/0'//",
|
||||
expectedErr: "could not parse part \"\": strconv.ParseInt",
|
||||
}, {
|
||||
name: "number too large",
|
||||
path: "m/99999999999999",
|
||||
expectedErr: "could not parse part \"99999999999999\": strconv",
|
||||
}, {
|
||||
name: "empty path",
|
||||
path: "m/",
|
||||
expectedResult: []uint32{},
|
||||
}, {
|
||||
name: "mixed path",
|
||||
path: "m/0'/1'/2'/3/4/5/6'/7'",
|
||||
expectedResult: []uint32{0, 1, 2, 3, 4, 5, 6, 7},
|
||||
}, {
|
||||
name: "short path",
|
||||
path: "m/0'",
|
||||
expectedResult: []uint32{0},
|
||||
}, {
|
||||
name: "plain path",
|
||||
path: "m/0/1/2",
|
||||
expectedResult: []uint32{0, 1, 2},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
result, err := parseDerivationPath(tc.path)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
require.Error(tt, err)
|
||||
require.Contains(
|
||||
tt, err.Error(), tc.expectedErr,
|
||||
)
|
||||
} else {
|
||||
require.NoError(tt, err)
|
||||
require.Equal(tt, tc.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user