rpc: support hex data output for createrawtransaction

This commit is contained in:
Roy Lee 2022-09-14 18:27:18 -07:00
parent 8a80f0683a
commit c5193e74ac
5 changed files with 115 additions and 49 deletions

View file

@ -67,7 +67,7 @@ type TransactionInput struct {
// CreateRawTransactionCmd defines the createrawtransaction JSON-RPC command. // CreateRawTransactionCmd defines the createrawtransaction JSON-RPC command.
type CreateRawTransactionCmd struct { type CreateRawTransactionCmd struct {
Inputs []TransactionInput Inputs []TransactionInput
Amounts map[string]float64 `jsonrpcusage:"{\"address\":amount,...}"` // In BTC Outputs map[string]interface{} `jsonrpcusage:"{\"address\":amount, \"data\":\"hex\", ...}"`
LockTime *int64 LockTime *int64
} }
@ -76,7 +76,7 @@ type CreateRawTransactionCmd struct {
// //
// Amounts are in BTC. Passing in nil and the empty slice as inputs is equivalent, // Amounts are in BTC. Passing in nil and the empty slice as inputs is equivalent,
// both gets interpreted as the empty slice. // 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 { lockTime *int64) *CreateRawTransactionCmd {
// to make sure we're serializing this to the empty list and not null, we // to make sure we're serializing this to the empty list and not null, we
// explicitly initialize the list // explicitly initialize the list
@ -85,7 +85,7 @@ func NewCreateRawTransactionCmd(inputs []TransactionInput, amounts map[string]fl
} }
return &CreateRawTransactionCmd{ return &CreateRawTransactionCmd{
Inputs: inputs, Inputs: inputs,
Amounts: amounts, Outputs: outputs,
LockTime: lockTime, LockTime: lockTime,
} }
} }

View file

@ -52,13 +52,13 @@ func TestChainSvrCmds(t *testing.T) {
txInputs := []btcjson.TransactionInput{ txInputs := []btcjson.TransactionInput{
{Txid: "123", Vout: 1}, {Txid: "123", Vout: 1},
} }
amounts := map[string]float64{"456": .0123} txOutputs := map[string]interface{}{"456": .0123}
return btcjson.NewCreateRawTransactionCmd(txInputs, amounts, nil) return btcjson.NewCreateRawTransactionCmd(txInputs, txOutputs, nil)
}, },
marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123}],"id":1}`, marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123}],"id":1}`,
unmarshalled: &btcjson.CreateRawTransactionCmd{ unmarshalled: &btcjson.CreateRawTransactionCmd{
Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}}, 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}`) return btcjson.NewCmd("createrawtransaction", `[]`, `{"456":0.0123}`)
}, },
staticCmd: func() interface{} { staticCmd: func() interface{} {
amounts := map[string]float64{"456": .0123} txOutputs := map[string]interface{}{"456": .0123}
return btcjson.NewCreateRawTransactionCmd(nil, amounts, nil) return btcjson.NewCreateRawTransactionCmd(nil, txOutputs, nil)
}, },
marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[],{"456":0.0123}],"id":1}`, marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[],{"456":0.0123}],"id":1}`,
unmarshalled: &btcjson.CreateRawTransactionCmd{ unmarshalled: &btcjson.CreateRawTransactionCmd{
Inputs: []btcjson.TransactionInput{}, 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{ txInputs := []btcjson.TransactionInput{
{Txid: "123", Vout: 1}, {Txid: "123", Vout: 1},
} }
amounts := map[string]float64{"456": .0123} txOutputs := map[string]interface{}{"456": .0123}
return btcjson.NewCreateRawTransactionCmd(txInputs, amounts, btcjson.Int64(12312333333)) 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}`, marshalled: `{"jsonrpc":"1.0","method":"createrawtransaction","params":[[{"txid":"123","vout":1}],{"456":0.0123},12312333333],"id":1}`,
unmarshalled: &btcjson.CreateRawTransactionCmd{ unmarshalled: &btcjson.CreateRawTransactionCmd{
Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}}, Inputs: []btcjson.TransactionInput{{Txid: "123", Vout: 1}},
Amounts: map[string]float64{"456": .0123}, Outputs: map[string]interface{}{"456": .0123},
LockTime: btcjson.Int64(12312333333), 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", name: "fundrawtransaction - empty opts",
newCmd: func() (i interface{}, e error) { newCmd: func() (i interface{}, e error) {

View file

@ -291,13 +291,18 @@ func (r FutureCreateRawTransactionResult) Receive() (*wire.MsgTx, error) {
// //
// See CreateRawTransaction for the blocking version and more details. // See CreateRawTransaction for the blocking version and more details.
func (c *Client) CreateRawTransactionAsync(inputs []btcjson.TransactionInput, 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)) convertedData := make(map[string]interface{}, len(outputs))
for addr, amount := range amounts { for key, value := range outputs {
convertedAmts[addr.String()] = amount.ToBTC() 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) 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 // and sending to the provided addresses. If the inputs are either nil or an
// empty slice, it is interpreted as an empty slice. // empty slice, it is interpreted as an empty slice.
func (c *Client) CreateRawTransaction(inputs []btcjson.TransactionInput, 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 // FutureSendRawTransactionResult is a future promise to deliver the result

View file

@ -327,6 +327,15 @@ func rpcDecodeHexError(gotHex string) *btcjson.RPCError {
gotHex)) 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 // rpcNoTxInfoError is a convenience function for returning a nicely formatted
// RPC error which indicates there is no information available for the provided // RPC error which indicates there is no information available for the provided
// transaction hash. // transaction hash.
@ -568,59 +577,92 @@ func handleCreateRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan
// Add all transaction outputs to the transaction after performing // Add all transaction outputs to the transaction after performing
// some validity checks. // some validity checks.
params := s.cfg.ChainParams params := s.cfg.ChainParams
for encodedAddr, amount := range c.Amounts {
// Ensure amount is in the valid range for monetary amounts. // Ensure amount is in the valid range for monetary amounts.
if amount <= 0 || amount*btcutil.SatoshiPerBitcoin > btcutil.MaxSatoshi { // 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{ return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCType, Code: btcjson.ErrRPCType,
Message: "Invalid amount", Message: "invalid amount",
} }
} }
// Decode the provided address.
addr, err := btcutil.DecodeAddress(encodedAddr, params) addr, err := btcutil.DecodeAddress(encodedAddr, params)
if err != nil { if err != nil {
return nil, &btcjson.RPCError{ return nil, rpcInvalidAddressOrKeyError(encodedAddr,
Code: btcjson.ErrRPCInvalidAddressOrKey, "invalid address or key")
Message: "Invalid address or key: " + err.Error(),
}
} }
// 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) { switch addr.(type) {
case *btcutil.AddressPubKeyHash: case *btcutil.AddressPubKeyHash:
case *btcutil.AddressScriptHash: case *btcutil.AddressScriptHash:
default: default:
return nil, &btcjson.RPCError{ return nil, rpcInvalidAddressOrKeyError(addr.String(),
Code: btcjson.ErrRPCInvalidAddressOrKey, "invalid address or key")
Message: "Invalid address or key: " + addr.String(),
}
} }
if !addr.IsForNet(params) { if !addr.IsForNet(params) {
return nil, &btcjson.RPCError{ return nil, rpcInvalidAddressOrKeyError(addr.String(),
Code: btcjson.ErrRPCInvalidAddressOrKey, "wrong network")
Message: "Invalid address: " + encodedAddr +
" is for the wrong network",
}
} }
// Create a new script which pays to the provided address.
pkScript, err := txscript.PayToAddrScript(addr) pkScript, err := txscript.PayToAddrScript(addr)
if err != nil { 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) return nil, internalRPCError(err.Error(), context)
} }
// Convert the amount to satoshi.
satoshi, err := btcutil.NewAmount(amount) satoshi, err := btcutil.NewAmount(amount)
if err != nil { if err != nil {
context := "Failed to convert amount" context := "failed to convert amount"
return nil, internalRPCError(err.Error(), context) 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) mtx.AddTxOut(txOut)
} }

View file

@ -49,10 +49,10 @@ var helpDescsEnUS = map[string]string{
"The transaction inputs are not signed in the created transaction.\n" + "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.", "The signrawtransaction RPC command provided by wallet must be used to sign the resulting transaction.",
"createrawtransaction-inputs": "The inputs to the transaction", "createrawtransaction-inputs": "The inputs to the transaction",
"createrawtransaction-amounts": "JSON object with the destination addresses as keys and amounts as values", "createrawtransaction-outputs": "JSON object with the destination addresses as keys and amounts as values",
"createrawtransaction-amounts--key": "address", "createrawtransaction-outputs--key": "address or \"data\"",
"createrawtransaction-amounts--value": "n.nnn", "createrawtransaction-outputs--value": "value in BTC as floating point number or hex-encoded data for \"data\"",
"createrawtransaction-amounts--desc": "The destination address as the key and the amount in LBC as the value", "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-locktime": "Locktime value; a non-zero value will also locktime-activate the inputs",
"createrawtransaction--result0": "Hex-encoded bytes of the serialized transaction", "createrawtransaction--result0": "Hex-encoded bytes of the serialized transaction",