From 103a194e5c19c1b6b563e1abcf58c4365a2bfd23 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Wed, 15 Jan 2025 17:58:06 +0530 Subject: [PATCH] cmd/lncli: update listchannels output fields Added scid_str as a string representation of chan_id, replacing chan_id with scid, and including chan_id(BOLT02) in lncli listchannels output. Signed-off-by: Nishant Bansal --- cmd/commands/commands.go | 106 ++++++++++++++++++++++ cmd/commands/commands_test.go | 163 +++++++++++++++++++++++++++++++--- 2 files changed, 256 insertions(+), 13 deletions(-) diff --git a/cmd/commands/commands.go b/cmd/commands/commands.go index cde907119..60579a70a 100644 --- a/cmd/commands/commands.go +++ b/cmd/commands/commands.go @@ -21,6 +21,7 @@ import ( "github.com/jessevdk/go-flags" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/signal" @@ -50,6 +51,14 @@ var ( customDataPattern = regexp.MustCompile( `"custom_channel_data":\s*"([0-9a-f]+)"`, ) + + chanIDPattern = regexp.MustCompile( + `"chan_id":\s*"(\d+)"`, + ) + + channelPointPattern = regexp.MustCompile( + `"channel_point":\s*"([0-9a-fA-F]+:[0-9]+)"`, + ) ) // replaceCustomData replaces the custom channel data hex string with the @@ -86,6 +95,96 @@ func replaceCustomData(jsonBytes []byte) []byte { return buf.Bytes() } +// replaceAndAppendScid replaces the chan_id with scid and appends the human +// readable string representation of scid. +func replaceAndAppendScid(jsonBytes []byte) []byte { + // If there's nothing to replace, return the original JSON. + if !chanIDPattern.Match(jsonBytes) { + return jsonBytes + } + + replacedBytes := chanIDPattern.ReplaceAllFunc( + jsonBytes, func(match []byte) []byte { + // Extract the captured scid group from the match. + chanID := chanIDPattern.FindStringSubmatch( + string(match), + )[1] + + scid, err := strconv.ParseUint(chanID, 10, 64) + if err != nil { + return match + } + + // Format a new JSON field for the scid (chan_id), + // including both its numeric representation and its + // string representation (scid_str). + scidStr := lnwire.NewShortChanIDFromInt(scid). + AltString() + updatedField := fmt.Sprintf( + `"scid": "%d", "scid_str": "%s"`, scid, scidStr, + ) + + // Replace the entire match with the new structure. + return []byte(updatedField) + }, + ) + + var buf bytes.Buffer + err := json.Indent(&buf, replacedBytes, "", " ") + if err != nil { + // If we can't indent the JSON, it likely means the replacement + // data wasn't correct, so we return the original JSON. + return jsonBytes + } + + return buf.Bytes() +} + +// appendChanID appends the chan_id which is computed using the outpoint +// of the funding transaction (the txid, and output index). +func appendChanID(jsonBytes []byte) []byte { + // If there's nothing to replace, return the original JSON. + if !channelPointPattern.Match(jsonBytes) { + return jsonBytes + } + + replacedBytes := channelPointPattern.ReplaceAllFunc( + jsonBytes, func(match []byte) []byte { + chanPoint := channelPointPattern.FindStringSubmatch( + string(match), + )[1] + + chanOutpoint, err := wire.NewOutPointFromString( + chanPoint, + ) + if err != nil { + return match + } + + // Format a new JSON field computed from the + // channel_point (chan_id). + chanID := lnwire.NewChanIDFromOutPoint(*chanOutpoint) + updatedField := fmt.Sprintf( + `"channel_point": "%s", "chan_id": "%s"`, + chanPoint, chanID.String(), + ) + + // Replace the entire match with the new structure. + return []byte(updatedField) + }, + ) + + var buf bytes.Buffer + err := json.Indent(&buf, replacedBytes, "", " ") + if err != nil { + // If we can't indent the JSON, it likely means the replacement + // data wasn't correct, so we return the original JSON. + return jsonBytes + } + + return buf.Bytes() +} + func getContext() context.Context { shutdownInterceptor, err := signal.Intercept() if err != nil { @@ -120,8 +219,15 @@ func printRespJSON(resp proto.Message) { return } + // Replace custom_channel_data in the JSON. jsonBytesReplaced := replaceCustomData(jsonBytes) + // Replace chan_id with scid, and append scid_str and scid fields. + jsonBytesReplaced = replaceAndAppendScid(jsonBytesReplaced) + + // Append the chan_id field to the JSON. + jsonBytesReplaced = appendChanID(jsonBytesReplaced) + fmt.Printf("%s\n", jsonBytesReplaced) } diff --git a/cmd/commands/commands_test.go b/cmd/commands/commands_test.go index cb9cbe0db..61bd12920 100644 --- a/cmd/commands/commands_test.go +++ b/cmd/commands/commands_test.go @@ -127,10 +127,9 @@ func TestReplaceCustomData(t *testing.T) { t.Parallel() testCases := []struct { - name string - data string - replaceData string - expected string + name string + data string + expected string }{ { name: "no replacement necessary", @@ -139,10 +138,10 @@ func TestReplaceCustomData(t *testing.T) { }, { name: "valid json with replacement", - data: "{\"foo\":\"bar\",\"custom_channel_data\":\"" + + data: `{"foo":"bar","custom_channel_data":"` + hex.EncodeToString([]byte( - "{\"bar\":\"baz\"}", - )) + "\"}", + `{"bar":"baz"}`, + )) + `"}`, expected: `{ "foo": "bar", "custom_channel_data": { @@ -152,10 +151,10 @@ func TestReplaceCustomData(t *testing.T) { }, { name: "valid json with replacement and space", - data: "{\"foo\":\"bar\",\"custom_channel_data\": \"" + + data: `{"foo":"bar","custom_channel_data": "` + hex.EncodeToString([]byte( - "{\"bar\":\"baz\"}", - )) + "\"}", + `{"bar":"baz"}`, + )) + `"}`, expected: `{ "foo": "bar", "custom_channel_data": { @@ -178,9 +177,11 @@ func TestReplaceCustomData(t *testing.T) { "\"custom_channel_data\":\"a\"", }, { - name: "valid json, invalid hex, just formatted", - data: "{\"custom_channel_data\":\"f\"}", - expected: "{\n \"custom_channel_data\": \"f\"\n}", + name: "valid json, invalid hex, just formatted", + data: `{"custom_channel_data":"f"}`, + expected: `{ + "custom_channel_data": "f" +}`, }, } @@ -191,3 +192,139 @@ func TestReplaceCustomData(t *testing.T) { }) } } + +// TestReplaceAndAppendScid tests whether chan_id is replaced with scid and +// scid_str in the JSON console output. +func TestReplaceAndAppendScid(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + expected string + }{ + { + name: "no replacement necessary", + data: "foo", + expected: "foo", + }, + { + name: "valid json with replacement", + data: `{"foo":"bar","chan_id":"829031767408640"}`, + expected: `{ + "foo": "bar", + "scid": "829031767408640", + "scid_str": "754x1x0" +}`, + }, + { + name: "valid json with replacement and space", + data: `{"foo":"bar","chan_id": "829031767408640"}`, + expected: `{ + "foo": "bar", + "scid": "829031767408640", + "scid_str": "754x1x0" +}`, + }, + { + name: "doesn't match pattern, returned identical", + data: "this ain't even json, and no chan_id " + + "either", + expected: "this ain't even json, and no chan_id " + + "either", + }, + { + name: "invalid json", + data: "this ain't json, " + + "\"chan_id\":\"18446744073709551616\"", + expected: "this ain't json, " + + "\"chan_id\":\"18446744073709551616\"", + }, + { + name: "valid json, invalid uint, just formatted", + data: `{"chan_id":"18446744073709551616"}`, + expected: `{ + "chan_id": "18446744073709551616" +}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := replaceAndAppendScid([]byte(tc.data)) + require.Equal(t, tc.expected, string(result)) + }) + } +} + +// TestAppendChanID tests whether chan_id (BOLT02) is appended +// to the JSON console output. +func TestAppendChanID(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + expected string + }{ + { + name: "no amendment necessary", + data: "foo", + expected: "foo", + }, + { + name: "valid json with amendment", + data: `{"foo":"bar","channel_point":"6ab312e3b744e` + + `1b80a33a6541697df88766515c31c08e839bf11dc` + + `9fcc036a19:0"}`, + expected: `{ + "foo": "bar", + "channel_point": "6ab312e3b744e1b80a33a6541697df88766515c31c` + + `08e839bf11dc9fcc036a19:0", + "chan_id": "196a03cc9fdc11bf39e8081cc315657688df971654a` + + `6330ab8e144b7e312b36a" +}`, + }, + { + name: "valid json with amendment and space", + data: `{"foo":"bar","channel_point": "6ab312e3b744e` + + `1b80a33a6541697df88766515c31c08e839bf11dc` + + `9fcc036a19:0"}`, + expected: `{ + "foo": "bar", + "channel_point": "6ab312e3b744e1b80a33a6541697df88766515c31c` + + `08e839bf11dc9fcc036a19:0", + "chan_id": "196a03cc9fdc11bf39e8081cc315657688df971654a` + + `6330ab8e144b7e312b36a" +}`, + }, + { + name: "doesn't match pattern, returned identical", + data: "this ain't even json, and no channel_point " + + "either", + expected: "this ain't even json, and no channel_point" + + " either", + }, + { + name: "invalid json", + data: "this ain't json, " + + "\"channel_point\":\"f:0\"", + expected: "this ain't json, " + + "\"channel_point\":\"f:0\"", + }, + { + name: "valid json with invalid outpoint, formatted", + data: `{"channel_point":"f:0"}`, + expected: `{ + "channel_point": "f:0" +}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := appendChanID([]byte(tc.data)) + require.Equal(t, tc.expected, string(result)) + }) + } +}