diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 30024dda..d6263fff 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -67,7 +67,7 @@ type TransactionInput struct { // CreateRawTransactionCmd defines the createrawtransaction JSON-RPC command. type CreateRawTransactionCmd struct { Inputs []TransactionInput - Amounts map[string]float64 `jsonrpcusage:"{\"address\":amount,...}"` // In BTC + Outputs map[string]interface{} `jsonrpcusage:"{\"address\":amount, \"data\":\"hex\", ...}"` LockTime *int64 } @@ -76,7 +76,7 @@ type CreateRawTransactionCmd struct { // // Amounts are in BTC. Passing in nil and the empty slice as inputs is equivalent, // both gets interpreted as the empty slice. -func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]float64, +func NewCreateRawTransactionCmd(inputs []TransactionInput, outputs map[string]interface{}, lockTime *int64) *CreateRawTransactionCmd { // to make sure we're serializing this to the empty list and not null, we // explicitly initialize the list @@ -85,7 +85,7 @@ func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]fl } return &CreateRawTransactionCmd{ Inputs: inputs, - Amounts: amounts, + Outputs: outputs, LockTime: lockTime, } } diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index 824e87d7..4cdcb131 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -52,13 +52,13 @@ func TestChainSvrCmds(t *testing.T) { txInputs := []btcjson.TransactionInput{ {Txid: "123", Vout: 1}, } - amounts := map[string]float64{"456": .0123} - return btcjson.NewCreateRawTransactionCmd(txInputs, amounts, nil) + txOutputs := map[string]interface{}{"456": .0123} + return btcjson.NewCreateRawTransactionCmd(txInputs, txOutputs, nil) }, marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123}],"id":1}`, unmarshalled: &btcjson.CreateRawTransactionCmd{ Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}}, - Amounts: map[string]float64{"456": .0123}, + Outputs: map[string]interface{}{"456": .0123}, }, }, { @@ -67,13 +67,13 @@ func TestChainSvrCmds(t *testing.T) { return btcjson.NewCmd("createrawtransaction", `[]`, `{"456":0.0123}`) }, staticCmd: func() interface{} { - amounts := map[string]float64{"456": .0123} - return btcjson.NewCreateRawTransactionCmd(nil, amounts, nil) + txOutputs := map[string]interface{}{"456": .0123} + return btcjson.NewCreateRawTransactionCmd(nil, txOutputs, nil) }, marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[],{"456":0.0123}],"id":1}`, unmarshalled: &btcjson.CreateRawTransactionCmd{ Inputs: []btcjson.TransactionInput{}, - Amounts: map[string]float64{"456": .0123}, + Outputs: map[string]interface{}{"456": .0123}, }, }, { @@ -86,16 +86,35 @@ func TestChainSvrCmds(t *testing.T) { txInputs := []btcjson.TransactionInput{ {Txid: "123", Vout: 1}, } - amounts := map[string]float64{"456": .0123} - return btcjson.NewCreateRawTransactionCmd(txInputs, amounts, btcjson.Int64(12312333333)) + txOutputs := map[string]interface{}{"456": .0123} + return btcjson.NewCreateRawTransactionCmd(txInputs, txOutputs, btcjson.Int64(12312333333)) }, marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123},12312333333],"id":1}`, unmarshalled: &btcjson.CreateRawTransactionCmd{ Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}}, - Amounts: map[string]float64{"456": .0123}, + Outputs: map[string]interface{}{"456": .0123}, LockTime: btcjson.Int64(12312333333), }, }, + { + name: "createrawtransaction with data", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("createrawtransaction", `[{"txid":"123","vout":1}]`, + `{"data":"6a134920616d204672616374616c456e6372797074"}`) + }, + staticCmd: func() interface{} { + txInputs := []btcjson.TransactionInput{ + {Txid: "123", Vout: 1}, + } + txOutputs := map[string]interface{}{"data": "6a134920616d204672616374616c456e6372797074"} + return btcjson.NewCreateRawTransactionCmd(txInputs, txOutputs, nil) + }, + marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"data":"6a134920616d204672616374616c456e6372797074"}],"id":1}`, + unmarshalled: &btcjson.CreateRawTransactionCmd{ + Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}}, + Outputs: map[string]interface{}{"data": "6a134920616d204672616374616c456e6372797074"}, + }, + }, { name: "fundrawtransaction - empty opts", newCmd: func() (i interface{}, e error) { diff --git a/rpcclient/rawtransactions.go b/rpcclient/rawtransactions.go index e4402f1d..6d14d228 100644 --- a/rpcclient/rawtransactions.go +++ b/rpcclient/rawtransactions.go @@ -291,13 +291,18 @@ func (r FutureCreateRawTransactionResult) Receive() (*wire.MsgTx, error) { // // See CreateRawTransaction for the blocking version and more details. func (c *Client) CreateRawTransactionAsync(inputs []btcjson.TransactionInput, - amounts map[btcutil.Address]btcutil.Amount, lockTime *int64) FutureCreateRawTransactionResult { + outputs map[btcutil.Address]interface{}, lockTime *int64) FutureCreateRawTransactionResult { - convertedAmts := make(map[string]float64, len(amounts)) - for addr, amount := range amounts { - convertedAmts[addr.String()] = amount.ToBTC() + convertedData := make(map[string]interface{}, len(outputs)) + for key, value := range outputs { + switch val := value.(type) { + case btcutil.Amount: + convertedData[key.String()] = val.ToBTC() + case string: + convertedData[key.String()] = val + } } - cmd := btcjson.NewCreateRawTransactionCmd(inputs, convertedAmts, lockTime) + cmd := btcjson.NewCreateRawTransactionCmd(inputs, convertedData, lockTime) return c.SendCmd(cmd) } @@ -305,9 +310,9 @@ func (c *Client) CreateRawTransactionAsync(inputs []btcjson.TransactionInput, // and sending to the provided addresses. If the inputs are either nil or an // empty slice, it is interpreted as an empty slice. func (c *Client) CreateRawTransaction(inputs []btcjson.TransactionInput, - amounts map[btcutil.Address]btcutil.Amount, lockTime *int64) (*wire.MsgTx, error) { + outputs map[btcutil.Address]interface{}, lockTime *int64) (*wire.MsgTx, error) { - return c.CreateRawTransactionAsync(inputs, amounts, lockTime).Receive() + return c.CreateRawTransactionAsync(inputs, outputs, lockTime).Receive() } // FutureSendRawTransactionResult is a future promise to deliver the result diff --git a/rpcserver.go b/rpcserver.go index ab052bc4..742f65ab 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -327,6 +327,15 @@ func rpcDecodeHexError(gotHex string) *btcjson.RPCError { gotHex)) } +// rpcInvalidAddressOrKey is a convenience function for returning a nicely +// formatted RPC error which indicates the address or key is invalid. +func rpcInvalidAddressOrKeyError(addr string, msg string) *btcjson.RPCError { + return &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: msg, + } +} + // rpcNoTxInfoError is a convenience function for returning a nicely formatted // RPC error which indicates there is no information available for the provided // transaction hash. @@ -568,59 +577,92 @@ func handleCreateRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan // Add all transaction outputs to the transaction after performing // some validity checks. params := s.cfg.ChainParams - for encodedAddr, amount := range c.Amounts { - // Ensure amount is in the valid range for monetary amounts. - if amount <= 0 || amount*btcutil.SatoshiPerBitcoin > btcutil.MaxSatoshi { + + // Ensure amount is in the valid range for monetary amounts. + // Decode the provided address. + // Ensure the address is one of the supported types and that + // the network encoded with the address matches the network the + // server is currently on. + // Create a new script which pays to the provided address. + // Convert the amount to satoshi. + handleAmountFn := func(amount float64, encodedAddr string) (*wire.TxOut, + error) { + + if amount <= 0 || + amount*btcutil.SatoshiPerBitcoin > btcutil.MaxSatoshi { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCType, - Message: "Invalid amount", + Message: "invalid amount", } } - // Decode the provided address. addr, err := btcutil.DecodeAddress(encodedAddr, params) if err != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: "Invalid address or key: " + err.Error(), - } + return nil, rpcInvalidAddressOrKeyError(encodedAddr, + "invalid address or key") } - // Ensure the address is one of the supported types and that - // the network encoded with the address matches the network the - // server is currently on. switch addr.(type) { case *btcutil.AddressPubKeyHash: case *btcutil.AddressScriptHash: default: - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: "Invalid address or key: " + addr.String(), - } + return nil, rpcInvalidAddressOrKeyError(addr.String(), + "invalid address or key") } if !addr.IsForNet(params) { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: "Invalid address: " + encodedAddr + - " is for the wrong network", - } + return nil, rpcInvalidAddressOrKeyError(addr.String(), + "wrong network") } - // Create a new script which pays to the provided address. pkScript, err := txscript.PayToAddrScript(addr) if err != nil { - context := "Failed to generate pay-to-address script" + context := "failed to generate pay-to-address script" return nil, internalRPCError(err.Error(), context) } - // Convert the amount to satoshi. satoshi, err := btcutil.NewAmount(amount) if err != nil { - context := "Failed to convert amount" + context := "failed to convert amount" return nil, internalRPCError(err.Error(), context) } - txOut := wire.NewTxOut(int64(satoshi), pkScript) + return wire.NewTxOut(int64(satoshi), pkScript), nil + } + + handleDataFn := func(key string, value string) (*wire.TxOut, error) { + if key != "data" { + context := "output key must be an address or \"data\"" + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: context, + } + } + var data []byte + data, err := hex.DecodeString(value) + if err != nil { + return nil, rpcDecodeHexError(value) + } + return wire.NewTxOut(0, data), nil + } + + for key, value := range c.Outputs { + var err error + var txOut *wire.TxOut + switch value := value.(type) { + case float64: + txOut, err = handleAmountFn(value, key) + case string: + txOut, err = handleDataFn(key, value) + default: + context := "output value must be a string or float" + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCType, + Message: context, + } + } + if err != nil { + return nil, err + } mtx.AddTxOut(txOut) } diff --git a/rpcserverhelp.go b/rpcserverhelp.go index b7680783..44d92683 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -49,10 +49,10 @@ var helpDescsEnUS = map[string]string{ "The transaction inputs are not signed in the created transaction.\n" + "The signrawtransaction RPC command provided by wallet must be used to sign the resulting transaction.", "createrawtransaction-inputs": "The inputs to the transaction", - "createrawtransaction-amounts": "JSON object with the destination addresses as keys and amounts as values", - "createrawtransaction-amounts--key": "address", - "createrawtransaction-amounts--value": "n.nnn", - "createrawtransaction-amounts--desc": "The destination address as the key and the amount in LBC as the value", + "createrawtransaction-outputs": "JSON object with the destination addresses as keys and amounts as values", + "createrawtransaction-outputs--key": "address or \"data\"", + "createrawtransaction-outputs--value": "value in BTC as floating point number or hex-encoded data for \"data\"", + "createrawtransaction-outputs--desc": "The destination address as the key and the amount in LBC as the value", "createrawtransaction-locktime": "Locktime value; a non-zero value will also locktime-activate the inputs", "createrawtransaction--result0": "Hex-encoded bytes of the serialized transaction",