From 69eafc8c4e1eae6aa06585ab0e72ebf9c81c56ef Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 6 Jan 2025 11:37:27 +0000 Subject: [PATCH] itest: add harness for invoking chantools binary Add a test harness to execute the chantools binary via the command line and parse its output. --- itest/chantools_harness.go | 160 +++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 itest/chantools_harness.go diff --git a/itest/chantools_harness.go b/itest/chantools_harness.go new file mode 100644 index 000000000..139ae6e34 --- /dev/null +++ b/itest/chantools_harness.go @@ -0,0 +1,160 @@ +package itest + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/stretchr/testify/require" +) + +// chantoolsCmdBuilder is a helper function that creates a new exec.Cmd instance +// for the chantools binary. +func chantoolsCmdBuilder(binPath string, workingDir string, + args ...string) exec.Cmd { + + cmd := exec.Command(binPath, args...) + + // Set the working directory so that any log files are handled + // gracefully. + cmd.Dir = workingDir + + // Set the wallet password. + cmd.Env = append(os.Environ(), "WALLET_PASSWORD=-") + + // Set the seed generation passphrase. + cmd.Env = append(cmd.Env, "AEZEED_PASSPHRASE=-") + + return *cmd +} + +// ChantoolsHarness is a helper struct that provides a way to interact with +// the chantools binary. +type ChantoolsHarness struct { + // path is the path to the chantools binary. + path string + + // workingDir is the chantools harness working directory. + workingDir string + + // walletDbPath is the path to the wallet.db file created by chantools. + walletDbPath string +} + +// NewChantoolsHarness creates a new instance of the ChantoolsHarness struct. +func NewChantoolsHarness(t *testing.T) ChantoolsHarness { + path, err := exec.LookPath("chantools") + require.NoError(t, err, "chantools is not installed or not in PATH") + + t.Logf("Using chantools binary at: %v", path) + + // Create a temporary directory to store the log file and wallet.db + // file. + workingDir, err := os.MkdirTemp("", "itest-tapd-chantools") + require.NoError(t, err, "failed to create chantools working directory") + + // Assert that the version of chantools is as expected. + cmd := chantoolsCmdBuilder(path, workingDir, "--version") + versionOut, err := cmd.CombinedOutput() + require.NoError(t, err, "failed to get chantools version") + + versionOutStr := string(versionOut) + if !strings.Contains(versionOutStr, "chantools version v0.13.5") { + t.Fatalf("unexpected chantools version: %v", versionOutStr) + } + + return ChantoolsHarness{ + path: path, + workingDir: workingDir, + walletDbPath: filepath.Join(workingDir, "wallet.db"), + } +} + +// buildCmd is a helper method that creates a new exec.Cmd instance for the +// chantools binary. +func (c *ChantoolsHarness) buildCmd(args ...string) exec.Cmd { + return chantoolsCmdBuilder(c.path, c.workingDir, args...) +} + +// CreateWallet creates a new wallet using the chantools binary. +func (c *ChantoolsHarness) CreateWallet(t *testing.T) { + cmd := c.buildCmd( + "--regtest", "createwallet", "--bip39", + "--generateseed", "--walletdbdir", c.workingDir, + ) + + cmdOut, err := cmd.CombinedOutput() + require.NoError(t, err) + + t.Logf("Chantools createwallet output: %v", string(cmdOut)) +} + +// DeriveKey derives a new key using the chantools binary. +func (c *ChantoolsHarness) DeriveKey(t *testing.T) (string, string) { + cmd := c.buildCmd( + "--regtest", "derivekey", "--walletdb", c.walletDbPath, + "--path", "m/86'/1'/0'", "--neuter", + ) + + cmdOut, err := cmd.CombinedOutput() + require.NoError(t, err) + + cmdOutStr := string(cmdOut) + t.Logf("Chantools derivekey output: %v", cmdOutStr) + + // Parsing for xpub. + xpubPattern := `Extended public key \(xpub\):\s+([a-zA-Z0-9]+)` + xpubRegex := regexp.MustCompile(xpubPattern) + matches := xpubRegex.FindStringSubmatch(cmdOutStr) + require.Len(t, matches, 2) + + xpub := matches[1] + require.NotEmpty(t, xpub) + + // Parsing for master fingerprint. + fingerprintPattern := `Master Fingerprint:\s+([a-f0-9]+)` + fingerprintRegex := regexp.MustCompile(fingerprintPattern) + matches = fingerprintRegex.FindStringSubmatch(cmdOutStr) + require.Len(t, matches, 2) + + masterFingerprint := matches[1] + require.Len(t, masterFingerprint, 8) + + return xpub, masterFingerprint +} + +// SignPsbt signs a PSBT using the chantools binary. +func (c *ChantoolsHarness) SignPsbt(t *testing.T, psbtStr string) psbt.Packet { + cmd := c.buildCmd( + "--regtest", "signpsbt", "--walletdb", c.walletDbPath, + "--psbt", psbtStr, + ) + + cmdOut, err := cmd.CombinedOutput() + require.NoError(t, err) + + cmdOutStr := string(cmdOut) + t.Logf("Chantools signpsbt output: %v", cmdOutStr) + + // Extract the signed PSBT. + psbtPattern := `Successfully signed PSBT:\n\n([a-zA-Z0-9+/=]+)` + psbtRegex := regexp.MustCompile(psbtPattern) + matches := psbtRegex.FindStringSubmatch(cmdOutStr) + require.Len(t, matches, 2) + + signedPsbtStr := matches[1] + require.NotEmpty(t, signedPsbtStr) + + // Parse the signed PSBT. + signedPsbt, err := psbt.NewFromRawBytes(bytes.NewReader( + []byte(signedPsbtStr)), true, + ) + require.NoError(t, err) + + return *signedPsbt +}