diff --git a/btcjson/cmdparse.go b/btcjson/cmdparse.go index 43d28f13df..cf8f6dc453 100644 --- a/btcjson/cmdparse.go +++ b/btcjson/cmdparse.go @@ -480,7 +480,7 @@ func assignField(paramNum int, fieldName string, dest reflect.Value, src reflect // String -> float of varying size. case reflect.Float32, reflect.Float64: - srcFloat, err := strconv.ParseFloat(src.String(), 0) + srcFloat, err := strconv.ParseFloat(src.String(), 64) if err != nil { str := fmt.Sprintf("parameter #%d '%s' must "+ "parse to a %v", paramNum, fieldName, diff --git a/btcjson/dashevocmds.go b/btcjson/dashevocmds.go index e80817db12..71e08d342a 100644 --- a/btcjson/dashevocmds.go +++ b/btcjson/dashevocmds.go @@ -93,9 +93,10 @@ type QuorumCmdSubCmd string // Quorum commands https://dashcore.readme.io/docs/core-api-ref-remote-procedure-calls-evo#quorum const ( - QuorumSign QuorumCmdSubCmd = "sign" - QuorumVerify QuorumCmdSubCmd = "verify" - QuorumInfo QuorumCmdSubCmd = "info" + QuorumSign QuorumCmdSubCmd = "sign" + QuorumSignPlatform QuorumCmdSubCmd = "platformsign" + QuorumVerify QuorumCmdSubCmd = "verify" + QuorumInfo QuorumCmdSubCmd = "info" // QuorumList lists all quorums QuorumList QuorumCmdSubCmd = "list" @@ -197,7 +198,7 @@ func (t LLMQType) Validate() error { // QuorumCmd defines the quorum JSON-RPC command. type QuorumCmd struct { - SubCmd QuorumCmdSubCmd `jsonrpcusage:"\"list|info|dkgstatus|sign|getrecsig|hasrecsig|isconflicting|memberof|selectquorum\""` + SubCmd QuorumCmdSubCmd `jsonrpcusage:"\"list|info|dkgstatus|sign|platformsign|getrecsig|hasrecsig|isconflicting|memberof|selectquorum\""` LLMQType *LLMQType `json:",omitempty"` RequestID *string `json:",omitempty"` @@ -229,6 +230,22 @@ func NewQuorumSignCmd(quorumType LLMQType, requestID, messageHash, quorumHash st return cmd } +// NewQuorumPlatformSignCmd returns a new instance which can be used to issue a quorum +// JSON-RPC command. +func NewQuorumPlatformSignCmd(requestID, messageHash, quorumHash string, submit bool) *QuorumCmd { + cmd := &QuorumCmd{ + SubCmd: QuorumSignPlatform, + RequestID: &requestID, + MessageHash: &messageHash, + } + if quorumHash == "" { + return cmd + } + cmd.QuorumHash = &quorumHash + cmd.Submit = &submit + return cmd +} + // NewQuorumVerifyCmd returns a new instance which can be used to issue a quorum // JSON-RPC command. func NewQuorumVerifyCmd(quorumType LLMQType, requestID string, messageHash string, signature string, quorumHash string) *QuorumCmd { @@ -517,9 +534,10 @@ func (q *QuorumCmd) UnmarshalArgs(args []interface{}) error { type unmarshalQuorumCmdFunc func(*QuorumCmd, []interface{}) error var quorumCmdUnmarshalers = map[string]unmarshalQuorumCmdFunc{ - "info": withQuorumUnmarshaler(quorumInfoUnmarshaler, validateQuorumArgs(3), unmarshalQuorumLLMQType), - "sign": withQuorumUnmarshaler(quorumSignUnmarshaler, validateQuorumArgs(5), unmarshalQuorumLLMQType), - "verify": withQuorumUnmarshaler(quorumVerifyUnmarshaler, validateQuorumArgs(5), unmarshalQuorumLLMQType), + string(QuorumInfo): withQuorumUnmarshaler(quorumInfoUnmarshaler, validateQuorumArgs(3), unmarshalQuorumLLMQType), + string(QuorumSign): withQuorumUnmarshaler(quorumSignUnmarshaler, validateQuorumArgs(5), unmarshalQuorumLLMQType), + string(QuorumSignPlatform): withQuorumUnmarshaler(quorumPlatformSignUnmarshaler, validateQuorumArgs(4)), + string(QuorumVerify): withQuorumUnmarshaler(quorumVerifyUnmarshaler, validateQuorumArgs(5), unmarshalQuorumLLMQType), } func unmarshalLLMQType(val interface{}) (LLMQType, error) { @@ -551,6 +569,14 @@ func quorumSignUnmarshaler(q *QuorumCmd, args []interface{}) error { return nil } +func quorumPlatformSignUnmarshaler(q *QuorumCmd, args []interface{}) error { + q.RequestID = strPtr(args[0].(string)) + q.MessageHash = strPtr(args[1].(string)) + q.QuorumHash = strPtr(args[2].(string)) + q.Submit = boolPtr(args[3].(bool)) + return nil +} + func unmarshalQuorumLLMQType(next unmarshalQuorumCmdFunc) unmarshalQuorumCmdFunc { return func(q *QuorumCmd, args []interface{}) error { val, err := unmarshalLLMQType(args[0]) diff --git a/btcjson/dashevocmds_test.go b/btcjson/dashevocmds_test.go index 0e0c5a542a..5588fbf836 100644 --- a/btcjson/dashevocmds_test.go +++ b/btcjson/dashevocmds_test.go @@ -24,7 +24,7 @@ func pLLMQType(l btcjson.LLMQType) *btcjson.LLMQType { return &l } // into valid results include handling of optional fields being omitted in the // marshalled command, while optional fields with defaults have the default // assigned on unmarshalled commands. -func TestdashpayCmds(t *testing.T) { +func TestDashpayCmds(t *testing.T) { t.Parallel() testID := 1 @@ -61,6 +61,31 @@ func TestdashpayCmds(t *testing.T) { Submit: pBool(false), }, }, + { + name: "quorum platformsign", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("quorum", "platformsign", + "0067c4fd779a195a95b267e263c631f71f83f8d5e6191091289d114012b373a1", + "ce490ca26cad6f1749ff9b977fe0fe4ece4391166f69be75c4619bc94b184dbc", + "6f1018f54507606069303fd16257434073c6f374729b0090bb9dbbe629241236", + false) + }, + staticCmd: func() interface{} { + return btcjson.NewQuorumPlatformSignCmd( + "0067c4fd779a195a95b267e263c631f71f83f8d5e6191091289d114012b373a1", + "ce490ca26cad6f1749ff9b977fe0fe4ece4391166f69be75c4619bc94b184dbc", + "6f1018f54507606069303fd16257434073c6f374729b0090bb9dbbe629241236", + false) + }, + marshalled: `{"jsonrpc":"1.0","method":"quorum","params":["platformsign","0067c4fd779a195a95b267e263c631f71f83f8d5e6191091289d114012b373a1","ce490ca26cad6f1749ff9b977fe0fe4ece4391166f69be75c4619bc94b184dbc","6f1018f54507606069303fd16257434073c6f374729b0090bb9dbbe629241236",false],"id":1}`, + unmarshalled: &btcjson.QuorumCmd{ + SubCmd: "platformsign", + RequestID: pString("0067c4fd779a195a95b267e263c631f71f83f8d5e6191091289d114012b373a1"), + MessageHash: pString("ce490ca26cad6f1749ff9b977fe0fe4ece4391166f69be75c4619bc94b184dbc"), + QuorumHash: pString("6f1018f54507606069303fd16257434073c6f374729b0090bb9dbbe629241236"), + Submit: pBool(false), + }, + }, { name: "quorum info", newCmd: func() (interface{}, error) { diff --git a/btcjson/help.go b/btcjson/help.go index f502d09fd8..bad104436d 100644 --- a/btcjson/help.go +++ b/btcjson/help.go @@ -269,7 +269,7 @@ func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) str w.Init(&formatted, 0, 4, 1, ' ', 0) for i, text := range results { if i == len(results)-1 { - fmt.Fprintf(w, text) + fmt.Fprint(w, text) } else { fmt.Fprintln(w, text) } @@ -476,11 +476,12 @@ func isValidResultType(kind reflect.Kind) bool { // an error will use the key in place of the description. // // The following outlines the required keys: -// "--synopsis" Synopsis for the command -// "-" Description for each command argument -// "-" Description for each object field -// "--condition<#>" Description for each result condition -// "--result<#>" Description for each primitive result num +// +// "--synopsis" Synopsis for the command +// "-" Description for each command argument +// "-" Description for each object field +// "--condition<#>" Description for each result condition +// "--result<#>" Description for each primitive result num // // Notice that the "special" keys synopsis, condition<#>, and result<#> are // preceded by a double dash to ensure they don't conflict with field names. @@ -492,16 +493,17 @@ func isValidResultType(kind reflect.Kind) bool { // For example, consider the 'help' command itself. There are two possible // returns depending on the provided parameters. So, the help would be // generated by calling the function as follows: -// GenerateHelp("help", descs, (*string)(nil), (*string)(nil)). +// +// GenerateHelp("help", descs, (*string)(nil), (*string)(nil)). // // The following keys would then be required in the provided descriptions map: // -// "help--synopsis": "Returns a list of all commands or help for ...." -// "help-command": "The command to retrieve help for", -// "help--condition0": "no command provided" -// "help--condition1": "command specified" -// "help--result0": "List of commands" -// "help--result1": "Help for specified command" +// "help--synopsis": "Returns a list of all commands or help for ...." +// "help-command": "The command to retrieve help for", +// "help--condition0": "no command provided" +// "help--condition1": "command specified" +// "help--result0": "List of commands" +// "help--result1": "Help for specified command" func GenerateHelp(method string, descs map[string]string, resultTypes ...interface{}) (string, error) { // Look up details about the provided method and error out if not // registered. diff --git a/btcjson/help_test.go b/btcjson/help_test.go index 6c55ea9a87..ddf2b0d61e 100644 --- a/btcjson/help_test.go +++ b/btcjson/help_test.go @@ -173,7 +173,7 @@ func TestHelpReflectInternals(t *testing.T) { name: "array of struct indent level 0", reflectType: func() reflect.Type { type s struct { - field int + field int //nolint:unused } return reflect.TypeOf([]s{}) }(), @@ -192,7 +192,7 @@ func TestHelpReflectInternals(t *testing.T) { name: "array of struct indent level 1", reflectType: func() reflect.Type { type s struct { - field int + field int //nolint:unused } return reflect.TypeOf([]s{}) }(), @@ -309,7 +309,7 @@ func TestResultStructHelp(t *testing.T) { name: "struct with primitive field", reflectType: func() reflect.Type { type s struct { - field int + field int //nolint:unused } return reflect.TypeOf(s{}) }(), @@ -333,7 +333,7 @@ func TestResultStructHelp(t *testing.T) { name: "struct with array of primitive field", reflectType: func() reflect.Type { type s struct { - field []int + field []int //nolint:unused } return reflect.TypeOf(s{}) }(), @@ -344,9 +344,11 @@ func TestResultStructHelp(t *testing.T) { { name: "struct with sub-struct field", reflectType: func() reflect.Type { + //nolint:unused type s2 struct { subField int } + //nolint:unused type s struct { field s2 } @@ -362,9 +364,11 @@ func TestResultStructHelp(t *testing.T) { { name: "struct with sub-struct field pointer", reflectType: func() reflect.Type { + //nolint:unused type s2 struct { subField int } + //nolint:unused type s struct { field *s2 } @@ -380,9 +384,11 @@ func TestResultStructHelp(t *testing.T) { { name: "struct with array of structs field", reflectType: func() reflect.Type { + //nolint:unused type s2 struct { subField int } + //nolint:unused type s struct { field []s2 } diff --git a/btcjson/register_test.go b/btcjson/register_test.go index 9e43c33d71..36cb3ad6c0 100644 --- a/btcjson/register_test.go +++ b/btcjson/register_test.go @@ -103,7 +103,9 @@ func TestRegisterCmdErrors(t *testing.T) { name: "embedded field", method: "registertestcmd", cmdFunc: func() interface{} { - type test struct{ int } + type test struct { + int //nolint:unused + } return (*test)(nil) }, err: btcjson.Error{ErrorCode: btcjson.ErrEmbeddedType}, @@ -112,7 +114,9 @@ func TestRegisterCmdErrors(t *testing.T) { name: "unexported field", method: "registertestcmd", cmdFunc: func() interface{} { - type test struct{ a int } + type test struct { + a int //nolint:unused + } return (*test)(nil) }, err: btcjson.Error{ErrorCode: btcjson.ErrUnexportedField}, diff --git a/rpcclient/evo.go b/rpcclient/evo.go index a0926c0a12..a2ce47d85b 100644 --- a/rpcclient/evo.go +++ b/rpcclient/evo.go @@ -116,6 +116,28 @@ func (c *Client) QuorumSign(quorumType btcjson.LLMQType, requestID, messageHash, return c.QuorumSignAsync(quorumType, requestID, messageHash, quorumHash, submit).Receive() } +// QuorumPlatformSign returns a future that can be used to get the result of the RPC at some future time by invoking the Receive function on the returned instance. +// It uses `quorum platformsign` RPC command. +func (c *Client) QuorumPlatformSignAsync(requestID, messageHash, quorumHash string, submit bool) FutureGetQuorumSignResult { + cmd := btcjson.NewQuorumPlatformSignCmd(requestID, messageHash, quorumHash, submit) + + return FutureGetQuorumSignResult{ + client: c, + Response: c.SendCmd(cmd), + } +} + +// QuorumPlatformSign a quorum sign result containing a signature signed by the quorum in question. +// It uses `quorum platformsign` RPC command. +func (c *Client) QuorumPlatformSign(requestID, messageHash, quorumHash string, submit bool) (*btcjson.QuorumSignResultWithBool, error) { + cmd := btcjson.NewQuorumPlatformSignCmd(requestID, messageHash, quorumHash, submit) + + return FutureGetQuorumSignResult{ + client: c, + Response: c.SendCmd(cmd), + }.Receive() +} + // QuorumSignSubmit calls QuorumSign but only returns a boolean to match dash-cli func (c *Client) QuorumSignSubmit(quorumType btcjson.LLMQType, requestID, messageHash, quorumHash string) (bool, error) { r, err := c.QuorumSignAsync(quorumType, requestID, messageHash, quorumHash, true).Receive() diff --git a/rpcclient/evo_test.go b/rpcclient/evo_test.go index 95d4430ba6..626957378a 100644 --- a/rpcclient/evo_test.go +++ b/rpcclient/evo_test.go @@ -278,6 +278,63 @@ func TestQuorumSign(t *testing.T) { t.Log("bool response:", bl) } +func TestQuorumPlatformSign(t *testing.T) { + requestID := "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" + messageHash := "51c11d287dfa85aef3eebb5420834c8e443e01d15c0b0a8e397d67e2e51aa239" + proTxHash := "ec21749595a34d868cc366c0feefbd1cfaeb659c6acbc1e2e96fd1e714affa56" + submit := false + + client, err := New(connCfg, nil) + if err != nil { + t.Fatal(err) + } + defer client.Shutdown() + + client.httpClient.Transport = mockRoundTripperFunc( + []btcjson.QuorumMemberOfResult{ + { + Height: 264072, + Type: "llmq_400_60", + QuorumHash: "000004bfc56646880bfeb80a0b89ad955e557ead7b0f09bcc61e56c8473eaea9", + MinedBlock: "000006113a77b35a0ed606b08ecb8e37f1ac7e2d773c365bd07064a72ae9a61d", + QuorumPublicKey: "0644ff153b9b92c6a59e2adf4ef0b9836f7f6af05fe432ffdcb69bc9e300a2a70af4a8d9fc61323f6b81074d740033d2", + IsValidMember: false, + MemberIndex: 10, + }, + }, + expectBody(`{"jsonrpc":"1.0","method":"quorum","params":["memberof","ec21749595a34d868cc366c0feefbd1cfaeb659c6acbc1e2e96fd1e714affa56"],"id":1}`), + ) + mo, err := client.QuorumMemberOf(proTxHash, 0) + if err != nil { + t.Fatal(err) + } + + if len(mo) == 0 { + t.Fatal("not a member of any quorums") + } + quorumHash := mo[0].QuorumHash + quorumType := btcjson.GetLLMQType(mo[0].Type) + if quorumType == 0 { + t.Fatal("unknown quorum type", mo[0].Type) + } + + client.httpClient.Transport = mockRoundTripperFunc( + btcjson.QuorumSignResultWithBool{Result: true}, + expectBody(`{"jsonrpc":"1.0","method":"quorum","params":["platformsign","abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234","51c11d287dfa85aef3eebb5420834c8e443e01d15c0b0a8e397d67e2e51aa239","000004bfc56646880bfeb80a0b89ad955e557ead7b0f09bcc61e56c8473eaea9",false],"id":2}`), + ) + result, err := client.QuorumPlatformSign(requestID, messageHash, quorumHash, submit) + if err != nil { + t.Fatal(err) + } + + cli := &btcjson.QuorumSignResultWithBool{} + compareWithCliCommand(t, result, cli, "quorum", "platformsign", requestID, messageHash, quorumHash, strconv.FormatBool(submit)) + + client.httpClient.Transport = mockRoundTripperFunc( + btcjson.QuorumSignResultWithBool{Result: true}, + expectBody(`{"jsonrpc":"1.0","method":"quorum","params":["platformsign",2,"abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234","51c11d287dfa85aef3eebb5420834c8e443e01d15c0b0a8e397d67e2e51aa239","000004bfc56646880bfeb80a0b89ad955e557ead7b0f09bcc61e56c8473eaea9",true],"id":3}`), + ) +} func TestQuorumGetRecSig(t *testing.T) { client, err := New(connCfg, nil) if err != nil {