btcjson: turn warnings into StringOrArray type

Fixes #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.
This commit is contained in:
Oliver Gugger 2024-09-02 09:30:25 +02:00
parent 2b53ed1989
commit ed879eac20
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
2 changed files with 93 additions and 1 deletions

View File

@ -8,6 +8,7 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@ -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 {
@ -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

View File

@ -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
}
}
}