diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 1345f8f7c8..466e1e57a5 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -8,6 +8,7 @@ package btcjson import ( + "encoding/hex" "encoding/json" "fmt" @@ -79,6 +80,37 @@ func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]fl } } +// FundRawTransactionOpts are the different options that can be passed to rawtransaction +type FundRawTransactionOpts struct { + ChangeAddress *string `json:"changeAddress,omitempty"` + ChangePosition *int `json:"changePosition,omitempty"` + ChangeType *string `json:"change_type,omitempty"` + IncludeWatching *bool `json:"includeWatching,omitempty"` + LockUnspents *bool `json:"lockUnspents,omitempty"` + FeeRate *float64 `json:"feeRate,omitempty"` // BTC/kB + SubtractFeeFromOutputs []int `json:"subtractFeeFromOutputs,omitempty"` + Replaceable *bool `json:"replaceable,omitempty"` + ConfTarget *int `json:"conf_target,omitempty"` + EstimateMode *EstimateSmartFeeMode `json:"estimate_mode,omitempty"` +} + +// FundRawTransactionCmd defines the fundrawtransaction JSON-RPC command +type FundRawTransactionCmd struct { + HexTx string + Options FundRawTransactionOpts + IsWitness *bool +} + +// NewFundRawTransactionCmd returns a new instance which can be used to issue +// a fundrawtransaction JSON-RPC command +func NewFundRawTransactionCmd(serializedTx []byte, opts FundRawTransactionOpts, isWitness *bool) *FundRawTransactionCmd { + return &FundRawTransactionCmd{ + HexTx: hex.EncodeToString(serializedTx), + Options: opts, + IsWitness: isWitness, + } +} + // DecodeRawTransactionCmd defines the decoderawtransaction JSON-RPC command. type DecodeRawTransactionCmd struct { HexTx string @@ -838,6 +870,7 @@ func init() { MustRegisterCmd("addnode", (*AddNodeCmd)(nil), flags) MustRegisterCmd("createrawtransaction", (*CreateRawTransactionCmd)(nil), flags) + MustRegisterCmd("fundrawtransaction", (*FundRawTransactionCmd)(nil), flags) MustRegisterCmd("decoderawtransaction", (*DecodeRawTransactionCmd)(nil), flags) MustRegisterCmd("decodescript", (*DecodeScriptCmd)(nil), flags) MustRegisterCmd("getaddednodeinfo", (*GetAddedNodeInfoCmd)(nil), flags) diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index dd997ce5e9..f9cd1abe9f 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -6,6 +6,7 @@ package btcjson_test import ( "bytes" + "encoding/hex" "encoding/json" "fmt" "reflect" @@ -95,7 +96,108 @@ func TestChainSvrCmds(t *testing.T) { LockTime: btcjson.Int64(12312333333), }, }, + { + name: "fundrawtransaction - empty opts", + newCmd: func() (i interface{}, e error) { + return btcjson.NewCmd("fundrawtransaction", "deadbeef", "{}") + }, + staticCmd: func() interface{} { + deadbeef, err := hex.DecodeString("deadbeef") + if err != nil { + panic(err) + } + return btcjson.NewFundRawTransactionCmd(deadbeef, btcjson.FundRawTransactionOpts{}, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"fundrawtransaction","params":["deadbeef",{}],"id":1}`, + unmarshalled: &btcjson.FundRawTransactionCmd{ + HexTx: "deadbeef", + Options: btcjson.FundRawTransactionOpts{}, + IsWitness: nil, + }, + }, + { + name: "fundrawtransaction - full opts", + newCmd: func() (i interface{}, e error) { + return btcjson.NewCmd("fundrawtransaction", "deadbeef", `{"changeAddress":"bcrt1qeeuctq9wutlcl5zatge7rjgx0k45228cxez655","changePosition":1,"change_type":"legacy","includeWatching":true,"lockUnspents":true,"feeRate":0.7,"subtractFeeFromOutputs":[0],"replaceable":true,"conf_target":8,"estimate_mode":"ECONOMICAL"}`) + }, + staticCmd: func() interface{} { + deadbeef, err := hex.DecodeString("deadbeef") + if err != nil { + panic(err) + } + changeAddress := "bcrt1qeeuctq9wutlcl5zatge7rjgx0k45228cxez655" + change := 1 + changeType := "legacy" + watching := true + lockUnspents := true + feeRate := 0.7 + replaceable := true + confTarget := 8 + return btcjson.NewFundRawTransactionCmd(deadbeef, btcjson.FundRawTransactionOpts{ + ChangeAddress: &changeAddress, + ChangePosition: &change, + ChangeType: &changeType, + IncludeWatching: &watching, + LockUnspents: &lockUnspents, + FeeRate: &feeRate, + SubtractFeeFromOutputs: []int{0}, + Replaceable: &replaceable, + ConfTarget: &confTarget, + EstimateMode: &btcjson.EstimateModeEconomical, + }, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"fundrawtransaction","params":["deadbeef",{"changeAddress":"bcrt1qeeuctq9wutlcl5zatge7rjgx0k45228cxez655","changePosition":1,"change_type":"legacy","includeWatching":true,"lockUnspents":true,"feeRate":0.7,"subtractFeeFromOutputs":[0],"replaceable":true,"conf_target":8,"estimate_mode":"ECONOMICAL"}],"id":1}`, + unmarshalled: func() interface{} { + changeAddress := "bcrt1qeeuctq9wutlcl5zatge7rjgx0k45228cxez655" + change := 1 + changeType := "legacy" + watching := true + lockUnspents := true + feeRate := 0.7 + replaceable := true + confTarget := 8 + return &btcjson.FundRawTransactionCmd{ + HexTx: "deadbeef", + Options: btcjson.FundRawTransactionOpts{ + ChangeAddress: &changeAddress, + ChangePosition: &change, + ChangeType: &changeType, + IncludeWatching: &watching, + LockUnspents: &lockUnspents, + FeeRate: &feeRate, + SubtractFeeFromOutputs: []int{0}, + Replaceable: &replaceable, + ConfTarget: &confTarget, + EstimateMode: &btcjson.EstimateModeEconomical, + }, + IsWitness: nil, + } + }(), + }, + { + name: "fundrawtransaction - iswitness", + newCmd: func() (i interface{}, e error) { + return btcjson.NewCmd("fundrawtransaction", "deadbeef", "{}", true) + }, + staticCmd: func() interface{} { + deadbeef, err := hex.DecodeString("deadbeef") + if err != nil { + panic(err) + } + t := true + return btcjson.NewFundRawTransactionCmd(deadbeef, btcjson.FundRawTransactionOpts{}, &t) + }, + marshalled: `{"jsonrpc":"1.0","method":"fundrawtransaction","params":["deadbeef",{},true],"id":1}`, + unmarshalled: &btcjson.FundRawTransactionCmd{ + HexTx: "deadbeef", + Options: btcjson.FundRawTransactionOpts{}, + IsWitness: func() *bool { + t := true + return &t + }(), + }, + }, { name: "decoderawtransaction", newCmd: func() (interface{}, error) { diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index d076b74691..c0ad2f5cd8 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -4,7 +4,14 @@ package btcjson -import "encoding/json" +import ( + "bytes" + "encoding/hex" + "encoding/json" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) // GetBlockHeaderVerboseResult models the data from the getblockheader command when // the verbose flag is set. When the verbose flag is not set, getblockheader @@ -664,3 +671,50 @@ type EstimateSmartFeeResult struct { Errors []string `json:"errors,omitempty"` Blocks int64 `json:"blocks"` } + +var _ json.Unmarshaler = &FundRawTransactionResult{} + +type rawFundRawTransactionResult struct { + Transaction string `json:"hex"` + Fee float64 `json:"fee"` + ChangePosition int `json:"changepos"` +} + +// FundRawTransactionResult is the result of the fundrawtransaction JSON-RPC call +type FundRawTransactionResult struct { + Transaction *wire.MsgTx + Fee btcutil.Amount + ChangePosition int // the position of the added change output, or -1 +} + +// UnmarshalJSON unmarshals the result of the fundrawtransaction JSON-RPC call +func (f *FundRawTransactionResult) UnmarshalJSON(data []byte) error { + var rawRes rawFundRawTransactionResult + if err := json.Unmarshal(data, &rawRes); err != nil { + return err + } + + txBytes, err := hex.DecodeString(rawRes.Transaction) + if err != nil { + return err + } + + var msgTx wire.MsgTx + witnessErr := msgTx.Deserialize(bytes.NewReader(txBytes)) + if witnessErr != nil { + legacyErr := msgTx.DeserializeNoWitness(bytes.NewReader(txBytes)) + if legacyErr != nil { + return legacyErr + } + } + + fee, err := btcutil.NewAmount(rawRes.Fee) + if err != nil { + return err + } + + f.Transaction = &msgTx + f.Fee = fee + f.ChangePosition = rawRes.ChangePosition + return nil +} diff --git a/rpcclient/rawtransactions.go b/rpcclient/rawtransactions.go index 63a086642a..4e8d4e4d9c 100644 --- a/rpcclient/rawtransactions.go +++ b/rpcclient/rawtransactions.go @@ -205,6 +205,47 @@ func (c *Client) DecodeRawTransaction(serializedTx []byte) (*btcjson.TxRawResult return c.DecodeRawTransactionAsync(serializedTx).Receive() } +// FutureFundRawTransactionResult is a future promise to deliver the result +// of a FutureFundRawTransactionAsync RPC invocation (or an applicable error). +type FutureFundRawTransactionResult chan *response + +// Receive waits for the response promised by the future and returns information +// about a funding attempt +func (r FutureFundRawTransactionResult) Receive() (*btcjson.FundRawTransactionResult, error) { + res, err := receiveFuture(r) + if err != nil { + return nil, err + } + + var marshalled btcjson.FundRawTransactionResult + if err := json.Unmarshal(res, &marshalled); err != nil { + return nil, err + } + + return &marshalled, nil +} + +// FundRawTransactionAsync returns an instance of a type that can be used to +// get the result of the RPC at some future time by invoking the Receive +// function on the returned instance. +// +// See FundRawTransaction for the blocking version and more details. +func (c *Client) FundRawTransactionAsync(tx *wire.MsgTx, opts btcjson.FundRawTransactionOpts, isWitness *bool) FutureFundRawTransactionResult { + var txBuf bytes.Buffer + if err := tx.Serialize(&txBuf); err != nil { + return newFutureError(err) + } + + cmd := btcjson.NewFundRawTransactionCmd(txBuf.Bytes(), opts, isWitness) + return c.sendCmd(cmd) +} + +// FundRawTransaction returns the result of trying to fund the given transaction with +// funds from the node wallet +func (c *Client) FundRawTransaction(tx *wire.MsgTx, opts btcjson.FundRawTransactionOpts, isWitness *bool) (*btcjson.FundRawTransactionResult, error) { + return c.FundRawTransactionAsync(tx, opts, isWitness).Receive() +} + // FutureCreateRawTransactionResult is a future promise to deliver the result // of a CreateRawTransactionAsync RPC invocation (or an applicable error). type FutureCreateRawTransactionResult chan *response