Skip to content

Commit

Permalink
rpc: support hex data output for createrawtransaction
Browse files Browse the repository at this point in the history
  • Loading branch information
roylee17 committed Sep 15, 2022
1 parent 8a80f06 commit c5193e7
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 50 deletions.
6 changes: 3 additions & 3 deletions btcjson/chainsvrcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -85,7 +85,7 @@ func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]fl
}
return &CreateRawTransactionCmd{
Inputs: inputs,
Amounts: amounts,
Outputs: outputs,
LockTime: lockTime,
}
}
Expand Down
37 changes: 28 additions & 9 deletions btcjson/chainsvrcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
},
{
Expand All @@ -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},
},
},
{
Expand All @@ -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) {
Expand Down
21 changes: 13 additions & 8 deletions rpcclient/rawtransactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,23 +291,28 @@ 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 {

convertedAmts := make(map[string]float64, len(amounts))
for addr, amount := range amounts {
convertedAmts[addr.String()] = amount.ToBTC()
outputs map[btcutil.Address]interface{}, lockTime *int64) FutureCreateRawTransactionResult {

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

// CreateRawTransaction returns a new transaction spending the provided inputs
// 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
Expand Down
94 changes: 68 additions & 26 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down
8 changes: 4 additions & 4 deletions rpcserverhelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down

0 comments on commit c5193e7

Please sign in to comment.