Skip to content

Commit

Permalink
btcjson: turn warnings into StringOrArray type
Browse files Browse the repository at this point in the history
Fixes btcsuite#2224 and lightningnetwork/lnd#9053.

Depending on the version of Bitcoin Core, the "warnings" field in the
response to getnetworkinfo is either a single string value or an array
of strings.
We can easily parse those two variants with a custom type that
implements an UnmarshalJSON method.
  • Loading branch information
guggero committed Sep 2, 2024
1 parent 2b53ed1 commit ed879ea
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 1 deletion.
46 changes: 45 additions & 1 deletion btcjson/chainsvrresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"

"github.com/btcsuite/btcd/chaincfg/chainhash"

Expand Down Expand Up @@ -363,6 +364,49 @@ type LocalAddressesResult struct {
Score int32 `json:"score"`
}

// StringOrArray defines a type that can be used as type that is either a single
// string value or a string array in JSON-RPC commands, depending on the version
// of the chain backend.
type StringOrArray []string

// MarshalJSON implements the json.Marshaler interface.
func (h StringOrArray) MarshalJSON() ([]byte, error) {
return json.Marshal(h)
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (h *StringOrArray) UnmarshalJSON(data []byte) error {
var unmarshalled interface{}
if err := json.Unmarshal(data, &unmarshalled); err != nil {
return err
}

switch v := unmarshalled.(type) {
case string:
*h = []string{v}

case []interface{}:
s := make([]string, len(v))
for i, e := range v {
str, ok := e.(string)
if !ok {
return fmt.Errorf("invalid string_or_array "+
"value: %v", unmarshalled)
}

s[i] = str
}

*h = s

default:
return fmt.Errorf("invalid string_or_array value: %v",
unmarshalled)
}

return nil
}

// GetNetworkInfoResult models the data returned from the getnetworkinfo
// command.
type GetNetworkInfoResult struct {
Expand All @@ -380,7 +424,7 @@ type GetNetworkInfoResult struct {
RelayFee float64 `json:"relayfee"`
IncrementalFee float64 `json:"incrementalfee"`
LocalAddresses []LocalAddressesResult `json:"localaddresses"`
Warnings string `json:"warnings"`
Warnings StringOrArray `json:"warnings"`
}

// GetNodeAddressesResult models the data returned from the getnodeaddresses
Expand Down
48 changes: 48 additions & 0 deletions btcjson/chainsvrresults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,51 @@ func TestChainSvrMiningInfoResults(t *testing.T) {
}
}
}

// TestGetNetworkInfoWarnings tests that we can use both a single string value
// and an array of string values for the warnings field in GetNetworkInfoResult.
func TestGetNetworkInfoWarnings(t *testing.T) {
t.Parallel()

tests := []struct {
name string
result string
expected btcjson.GetNetworkInfoResult
}{
{
name: "network info with single warning",
result: `{"warnings": "this is a warning"}`,
expected: btcjson.GetNetworkInfoResult{
Warnings: btcjson.StringOrArray{
"this is a warning",
},
},
},
{
name: "network info with array of warnings",
result: `{"warnings": ["a", "or", "b"]}`,
expected: btcjson.GetNetworkInfoResult{
Warnings: btcjson.StringOrArray{
"a", "or", "b",
},
},
},
}

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
var infoResult btcjson.GetNetworkInfoResult
err := json.Unmarshal([]byte(test.result), &infoResult)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
if !reflect.DeepEqual(infoResult, test.expected) {
t.Errorf("Test #%d (%s) unexpected marhsalled data - "+
"got %+v, want %+v", i, test.name, infoResult,
test.expected)
continue
}
}
}

0 comments on commit ed879ea

Please sign in to comment.