Browse Source
Changelog-Changed: protocol: `fundchannel` now shuffles inputs and outputs, and no longer follows BIP69.bump-pyln-proto
ZmnSCPxj jxPCSnmZ
4 years ago
committed by
Rusty Russell
8 changed files with 147 additions and 466 deletions
@ -1,6 +1,5 @@ |
|||
autoclean |
|||
bcli |
|||
fundchannel |
|||
pay |
|||
spenderp |
|||
multifundchannel |
|||
|
@ -1,452 +0,0 @@ |
|||
#include <bitcoin/chainparams.c> |
|||
#include <bitcoin/script.h> |
|||
#include <ccan/array_size/array_size.h> |
|||
#include <ccan/json_out/json_out.h> |
|||
#include <ccan/tal/str/str.h> |
|||
#include <common/addr.h> |
|||
#include <common/amount.h> |
|||
#include <common/features.h> |
|||
#include <common/json_stream.h> |
|||
#include <common/json_tok.h> |
|||
#include <common/type_to_string.h> |
|||
#include <common/utils.h> |
|||
#include <plugins/libplugin.h> |
|||
|
|||
const char *placeholder_script = "0020b95810f824f843934fa042acd0becba52087813e260edaeebc42b5cb9abe1464"; |
|||
const char *placeholder_funding_addr; |
|||
|
|||
/* Populated by libplugin */ |
|||
extern const struct chainparams *chainparams; |
|||
|
|||
struct funding_req { |
|||
struct node_id *id; |
|||
const char *feerate_str; |
|||
const char *funding_str; |
|||
const char *utxo_str; |
|||
bool funding_all; |
|||
struct amount_msat *push_msat; |
|||
|
|||
/* Features offered by this peer. */ |
|||
const u8 *their_features; |
|||
|
|||
bool *announce_channel; |
|||
u32 *minconf; |
|||
|
|||
/* The prepared tx id */ |
|||
struct bitcoin_txid tx_id; |
|||
u32 outnum; |
|||
|
|||
const char *chanstr; |
|||
const u8 *out_script; |
|||
const char *funding_addr; |
|||
|
|||
/* Failing result (NULL on success) */ |
|||
/* Raw JSON from RPC output */ |
|||
const char *error; |
|||
}; |
|||
|
|||
static struct command_result *send_prior(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *error, |
|||
struct funding_req *fr) |
|||
{ |
|||
return command_err_raw(cmd, fr->error); |
|||
} |
|||
|
|||
static struct command_result *tx_abort(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *error, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req; |
|||
|
|||
/* We stash the error so we can return it after we've cleaned up */ |
|||
fr->error = json_strdup(fr, buf, error); |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "txdiscard", |
|||
send_prior, send_prior, fr); |
|||
json_add_string(req->js, "txid", |
|||
type_to_string(tmpctx, struct bitcoin_txid, &fr->tx_id)); |
|||
|
|||
/* We need to call txdiscard, and forward the actual cause for the
|
|||
* error after we've cleaned up. We swallow any errors returned by |
|||
* this call, as we don't really care if it succeeds or not */ |
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
/* We're basically done, we just need to format the output to match
|
|||
* what the original `fundchannel` returned */ |
|||
static struct command_result *finish(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct json_stream *out; |
|||
|
|||
out = jsonrpc_stream_success(cmd); |
|||
json_add_tok(out, "tx", json_get_member(buf, result, "tx"), buf); |
|||
json_add_string(out, "txid", |
|||
type_to_string(tmpctx, struct bitcoin_txid, &fr->tx_id)); |
|||
json_add_u32(out, "outnum", fr->outnum); |
|||
json_add_string(out, "channel_id", fr->chanstr); |
|||
|
|||
return command_finished(cmd, out); |
|||
} |
|||
|
|||
/* We're ready to broadcast the transaction */ |
|||
static struct command_result *send_tx(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
|
|||
struct out_req *req; |
|||
const jsmntok_t *tok; |
|||
bool commitments_secured; |
|||
|
|||
/* For sanity's sake, let's check that it's secured */ |
|||
tok = json_get_member(buf, result, "commitments_secured"); |
|||
if (!json_to_bool(buf, tok, &commitments_secured) || !commitments_secured) |
|||
/* TODO: better failure path? this should never fail though. */ |
|||
plugin_err(cmd->plugin, "Commitment not secured."); |
|||
|
|||
/* Stash the channel_id so we can return it when finalized */ |
|||
tok = json_get_member(buf, result, "channel_id"); |
|||
fr->chanstr = json_strdup(fr, buf, tok); |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "txsend", |
|||
finish, tx_abort, fr); |
|||
json_add_string(req->js, "txid", |
|||
type_to_string(tmpctx, struct bitcoin_txid, &fr->tx_id)); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result *tx_prepare_done(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
const jsmntok_t *txid_tok; |
|||
const jsmntok_t *tx_tok; |
|||
struct out_req *req; |
|||
const struct bitcoin_tx *tx; |
|||
const char *hex; |
|||
bool outnum_found; |
|||
|
|||
txid_tok = json_get_member(buf, result, "txid"); |
|||
if (!txid_tok) |
|||
plugin_err(cmd->plugin, "txprepare missing 'txid' field"); |
|||
|
|||
tx_tok = json_get_member(buf, result, "unsigned_tx"); |
|||
if (!tx_tok) |
|||
plugin_err(cmd->plugin, "txprepare missing 'unsigned_tx' field"); |
|||
|
|||
hex = json_strdup(tmpctx, buf, tx_tok); |
|||
tx = bitcoin_tx_from_hex(fr, hex, strlen(hex)); |
|||
if (!tx) |
|||
plugin_err(cmd->plugin, "Unable to parse tx %s", hex); |
|||
|
|||
/* Find the txout */ |
|||
outnum_found = false; |
|||
for (size_t i = 0; i < tx->wtx->num_outputs; i++) { |
|||
const u8 *output_script = bitcoin_tx_output_get_script(fr, tx, i); |
|||
if (scripteq(output_script, fr->out_script)) { |
|||
fr->outnum = i; |
|||
outnum_found = true; |
|||
break; |
|||
} |
|||
} |
|||
if (!outnum_found) |
|||
plugin_err(cmd->plugin, "txprepare doesn't include our funding output. " |
|||
"tx: %s, output: %s", |
|||
type_to_string(tmpctx, struct bitcoin_tx, tx), |
|||
tal_hex(tmpctx, fr->out_script)); |
|||
|
|||
hex = json_strdup(tmpctx, buf, txid_tok); |
|||
if (!bitcoin_txid_from_hex(hex, strlen(hex), &fr->tx_id)) |
|||
plugin_err(cmd->plugin, "Unable to parse txid %s", hex); |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "fundchannel_complete", |
|||
send_tx, tx_abort, fr); |
|||
json_add_string(req->js, "id", node_id_to_hexstr(tmpctx, fr->id)); |
|||
/* Note that hex is reused from above */ |
|||
json_add_string(req->js, "txid", hex); |
|||
json_add_u32(req->js, "txout", fr->outnum); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result *cancel_start(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *error, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req; |
|||
|
|||
/* We stash the error so we can return it after we've cleaned up */ |
|||
fr->error = json_strdup(fr, buf, error); |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "fundchannel_cancel", |
|||
send_prior, send_prior, fr); |
|||
json_add_string(req->js, "id", node_id_to_hexstr(tmpctx, fr->id)); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static void txprepare(struct json_stream *js, |
|||
struct funding_req *fr, |
|||
const char *destination) |
|||
{ |
|||
/* Add the 'outputs' */ |
|||
json_array_start(js, "outputs"); |
|||
json_object_start(js, NULL); |
|||
json_add_string(js, destination, fr->funding_str); |
|||
json_object_end(js); |
|||
json_array_end(js); |
|||
|
|||
if (fr->feerate_str) |
|||
json_add_string(js, "feerate", fr->feerate_str); |
|||
if (fr->minconf) |
|||
json_add_u32(js, "minconf", *fr->minconf); |
|||
if (fr->utxo_str) |
|||
json_add_jsonstr(js, "utxos", fr->utxo_str); |
|||
} |
|||
|
|||
static struct command_result *prepare_actual(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req; |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "txprepare", |
|||
tx_prepare_done, cancel_start, |
|||
fr); |
|||
txprepare(req->js, fr, fr->funding_addr); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result *fundchannel_start_done(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req; |
|||
|
|||
/* Save the outscript so we can fund the outnum later */ |
|||
fr->out_script = json_tok_bin_from_hex(fr, buf, |
|||
json_get_member(buf, result, "scriptpubkey")); |
|||
|
|||
/* Save the funding address, we'll need it later */ |
|||
fr->funding_addr = json_strdup(cmd, buf, |
|||
json_get_member(buf, result, "funding_address")); |
|||
|
|||
/* Now that we're ready to go, cancel the reserved tx */ |
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "txdiscard", |
|||
prepare_actual, cancel_start, |
|||
fr); |
|||
json_add_string(req->js, "txid", |
|||
type_to_string(tmpctx, struct bitcoin_txid, &fr->tx_id)); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result *fundchannel_start(struct command *cmd, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req = jsonrpc_request_start(cmd->plugin, cmd, |
|||
"fundchannel_start", |
|||
fundchannel_start_done, |
|||
tx_abort, fr); |
|||
|
|||
json_add_string(req->js, "id", node_id_to_hexstr(tmpctx, fr->id)); |
|||
|
|||
json_add_string(req->js, "amount", fr->funding_str); |
|||
|
|||
if (fr->feerate_str) |
|||
json_add_string(req->js, "feerate", fr->feerate_str); |
|||
if (fr->announce_channel) |
|||
json_add_bool(req->js, "announce", *fr->announce_channel); |
|||
if (fr->push_msat) |
|||
json_add_string(req->js, "push_msat", |
|||
type_to_string(tmpctx, struct amount_msat, fr->push_msat)); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result *post_dryrun(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct bitcoin_tx *tx; |
|||
const char *hex; |
|||
struct amount_sat funding; |
|||
bool funding_found; |
|||
u8 *placeholder = tal_hexdata(tmpctx, placeholder_script, strlen(placeholder_script)); |
|||
struct amount_asset asset; |
|||
|
|||
/* Stash the 'reserved' txid to unreserve later */ |
|||
hex = json_strdup(tmpctx, buf, json_get_member(buf, result, "txid")); |
|||
if (!bitcoin_txid_from_hex(hex, strlen(hex), &fr->tx_id)) |
|||
plugin_err(cmd->plugin, "Unable to parse reserved txid %s", hex); |
|||
|
|||
|
|||
hex = json_strdup(tmpctx, buf, json_get_member(buf, result, "unsigned_tx")); |
|||
tx = bitcoin_tx_from_hex(fr, hex, strlen(hex)); |
|||
tx->chainparams = chainparams; |
|||
|
|||
/* Find the funding amount */ |
|||
funding_found = false; |
|||
for (size_t i = 0; i < tx->wtx->num_outputs; i++) { |
|||
const u8 *output_script = bitcoin_tx_output_get_script(tmpctx, tx, i); |
|||
asset = bitcoin_tx_output_get_amount(tx, i); |
|||
|
|||
/* We do not support funding a channel with anything but the
|
|||
* main asset, for now. */ |
|||
if (!amount_asset_is_main(&asset)) |
|||
continue; |
|||
|
|||
if (scripteq(output_script, placeholder)) { |
|||
funding = amount_asset_to_sat(&asset); |
|||
funding_found = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!funding_found) |
|||
plugin_err(cmd->plugin, "Error creating placebo funding tx, funding_out not found. %s", hex); |
|||
|
|||
/* Update funding to actual amount */ |
|||
if (fr->funding_all |
|||
&& !feature_negotiated(plugin_feature_set(cmd->plugin), |
|||
fr->their_features, OPT_LARGE_CHANNELS) |
|||
&& amount_sat_greater(funding, chainparams->max_funding)) |
|||
funding = chainparams->max_funding; |
|||
|
|||
fr->funding_str = type_to_string(fr, struct amount_sat, &funding); |
|||
return fundchannel_start(cmd, fr); |
|||
} |
|||
|
|||
static struct command_result *exec_dryrun(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req; |
|||
const jsmntok_t *t; |
|||
|
|||
/* Stash features so we can wumbo. */ |
|||
t = json_get_member(buf, result, "features"); |
|||
if (!t) |
|||
plugin_err(cmd->plugin, "No features found in connect response?"); |
|||
fr->their_features = json_tok_bin_from_hex(fr, buf, t); |
|||
if (!fr->their_features) |
|||
plugin_err(cmd->plugin, "Bad features '%.*s' in connect response?", |
|||
t->end - t->start, buf + t->start); |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "txprepare", |
|||
post_dryrun, forward_error, |
|||
fr); |
|||
|
|||
/* Now that we've tried connecting, we do a 'dry-run' of txprepare,
|
|||
* so we can get an accurate idea of the funding amount */ |
|||
txprepare(req->js, fr, placeholder_funding_addr); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
|
|||
} |
|||
|
|||
static struct command_result *connect_to_peer(struct command *cmd, |
|||
struct funding_req *fr) |
|||
{ |
|||
struct out_req *req = jsonrpc_request_start(cmd->plugin, cmd, "connect", |
|||
exec_dryrun, forward_error, |
|||
fr); |
|||
|
|||
json_add_string(req->js, "id", node_id_to_hexstr(tmpctx, fr->id)); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
/* We will use 'id' and 'amount' to build a output: {id: amount}.
|
|||
* For array type, if we miss 'amount', next parameter will be |
|||
* mistaken for 'amount'. |
|||
* Note the check for 'output' in 'txprepare' is behind of the checks |
|||
* for other parameter, so doing a simply check for 'amount' here can |
|||
* help us locate error correctly. |
|||
*/ |
|||
static struct command_result *param_string_check_sat(struct command *cmd, const char *name, |
|||
const char * buffer, const jsmntok_t *tok, |
|||
const char **str) |
|||
{ |
|||
struct command_result *res; |
|||
struct amount_sat *amount; |
|||
|
|||
res = param_sat_or_all(cmd, name, buffer, tok, &amount); |
|||
if (res) |
|||
return res; |
|||
|
|||
return param_string(cmd, name, buffer, tok, str); |
|||
} |
|||
|
|||
static struct command_result *json_fundchannel(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *params) |
|||
{ |
|||
struct funding_req *fr = tal(cmd, struct funding_req); |
|||
|
|||
if (!param(cmd, buf, params, |
|||
p_req("id", param_node_id, &fr->id), |
|||
p_req("amount", param_string_check_sat, &fr->funding_str), |
|||
p_opt("feerate", param_string, &fr->feerate_str), |
|||
p_opt_def("announce", param_bool, &fr->announce_channel, true), |
|||
p_opt_def("minconf", param_number, &fr->minconf, 1), |
|||
p_opt("utxos", param_string, &fr->utxo_str), |
|||
p_opt("push_msat", param_msat, &fr->push_msat), |
|||
NULL)) |
|||
return command_param_failed(); |
|||
|
|||
fr->funding_all = streq(fr->funding_str, "all"); |
|||
|
|||
return connect_to_peer(cmd, fr); |
|||
} |
|||
|
|||
static void init(struct plugin *p, |
|||
const char *buf UNUSED, const jsmntok_t *config UNUSED) |
|||
{ |
|||
/* Figure out what the 'placeholder' addr is */ |
|||
const char *network_name; |
|||
u8 *placeholder = tal_hexdata(tmpctx, placeholder_script, strlen(placeholder_script)); |
|||
|
|||
network_name = rpc_delve(tmpctx, p, "listconfigs", |
|||
take(json_out_obj(NULL, "config", |
|||
"network")), |
|||
".network"); |
|||
chainparams = chainparams_for_network(network_name); |
|||
placeholder_funding_addr = encode_scriptpubkey_to_addr(NULL, chainparams, |
|||
placeholder); |
|||
} |
|||
|
|||
|
|||
static const struct plugin_command commands[] = { { |
|||
"fundchannel", |
|||
"channels", |
|||
"Fund channel with {id} using {amount} (or 'all'), at optional {feerate}. " |
|||
"Only use outputs that have {minconf} confirmations.", |
|||
"Initiaties a channel open with node 'id'. Must " |
|||
"be connected to the node and have enough funds available at the requested minimum confirmation " |
|||
"depth (minconf)", |
|||
json_fundchannel |
|||
} |
|||
}; |
|||
|
|||
|
|||
int main(int argc, char *argv[]) |
|||
{ |
|||
setup_locale(); |
|||
plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, commands, |
|||
ARRAY_SIZE(commands), NULL, 0, NULL, 0, NULL); |
|||
} |
@ -0,0 +1,124 @@ |
|||
#include <ccan/array_size/array_size.h> |
|||
#include <ccan/compiler/compiler.h> |
|||
#include <common/json.h> |
|||
#include <common/json_stream.h> |
|||
#include <plugins/spender/fundchannel.h> |
|||
|
|||
static struct command_result * |
|||
json_fundchannel(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *params); |
|||
|
|||
const struct plugin_command fundchannel_commands[] = { { |
|||
"fundchannel", |
|||
"channels", |
|||
"Fund channel with {id} using {amount} (or 'all'), at optional {feerate}. " |
|||
"Only use outputs that have {minconf} confirmations.", |
|||
"Initiaties a channel open with node 'id'. Must " |
|||
"be connected to the node and have enough funds available at the requested minimum confirmation " |
|||
"depth (minconf)", |
|||
json_fundchannel |
|||
} |
|||
}; |
|||
const size_t num_fundchannel_commands = ARRAY_SIZE(fundchannel_commands); |
|||
|
|||
static struct command_result * |
|||
fundchannel_get_result(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
void *nothing UNUSED); |
|||
|
|||
/* Thin wrapper aroud multifundchannel. */ |
|||
static struct command_result * |
|||
json_fundchannel(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *params) |
|||
{ |
|||
const char *id; |
|||
const jsmntok_t *amount; |
|||
const jsmntok_t *feerate; |
|||
const jsmntok_t *announce; |
|||
const jsmntok_t *minconf; |
|||
const jsmntok_t *utxos; |
|||
const jsmntok_t *push_msat; |
|||
|
|||
struct out_req *req; |
|||
|
|||
if (!param(cmd, buf, params, |
|||
p_req("id", param_string, &id), |
|||
p_req("amount", param_tok, &amount), |
|||
p_opt("feerate", param_tok, &feerate), |
|||
p_opt("announce", param_tok, &announce), |
|||
p_opt("minconf", param_tok, &minconf), |
|||
p_opt("utxos", param_tok, &utxos), |
|||
p_opt("push_msat", param_tok, &push_msat), |
|||
NULL)) |
|||
return command_param_failed(); |
|||
|
|||
req = jsonrpc_request_start(cmd->plugin, cmd, "multifundchannel", |
|||
&fundchannel_get_result, &forward_error, |
|||
NULL); |
|||
|
|||
json_array_start(req->js, "destinations"); |
|||
json_object_start(req->js, NULL); |
|||
json_add_string(req->js, "id", id); |
|||
json_add_tok(req->js, "amount", amount, buf); |
|||
if (announce) |
|||
json_add_tok(req->js, "announce", announce, buf); |
|||
if (push_msat) |
|||
json_add_tok(req->js, "push_msat", push_msat, buf); |
|||
json_object_end(req->js); |
|||
json_array_end(req->js); |
|||
if (feerate) |
|||
json_add_tok(req->js, "feerate", feerate, buf); |
|||
if (minconf) |
|||
json_add_tok(req->js, "minconf", minconf, buf); |
|||
if (utxos) |
|||
json_add_tok(req->js, "utxos", utxos, buf); |
|||
|
|||
return send_outreq(cmd->plugin, req); |
|||
} |
|||
|
|||
static struct command_result * |
|||
fundchannel_get_result(struct command *cmd, |
|||
const char *buf, |
|||
const jsmntok_t *result, |
|||
void *nothing UNUSED) |
|||
{ |
|||
bool ok; |
|||
const jsmntok_t *tx; |
|||
const jsmntok_t *txid; |
|||
const jsmntok_t *channel_ids_array; |
|||
const jsmntok_t *channel_ids_obj; |
|||
const jsmntok_t *channel_id; |
|||
const jsmntok_t *outnum; |
|||
|
|||
struct json_stream *out; |
|||
|
|||
ok = true; |
|||
tx = ok ? json_get_member(buf, result, "tx") : NULL; |
|||
ok = ok && tx; |
|||
txid = ok ? json_get_member(buf, result, "txid") : NULL; |
|||
ok = ok && txid; |
|||
channel_ids_array = ok ? json_get_member(buf, result, "channel_ids") : NULL; |
|||
ok = ok && channel_ids_array; |
|||
channel_ids_obj = ok ? json_get_arr(channel_ids_array, 0) : NULL; |
|||
ok = ok && channel_ids_obj; |
|||
channel_id = ok ? json_get_member(buf, channel_ids_obj, "channel_id") : NULL; |
|||
ok = ok && channel_id; |
|||
outnum = ok ? json_get_member(buf, channel_ids_obj, "outnum") : NULL; |
|||
ok = ok && outnum; |
|||
|
|||
if (!ok) |
|||
plugin_err(cmd->plugin, |
|||
"Unexpected result from nultifundchannel: %.*s", |
|||
json_tok_full_len(result), |
|||
json_tok_full(buf, result)); |
|||
|
|||
out = jsonrpc_stream_success(cmd); |
|||
json_add_tok(out, "tx", tx, buf); |
|||
json_add_tok(out, "txid", txid, buf); |
|||
json_add_tok(out, "channel_id", channel_id, buf); |
|||
json_add_tok(out, "outnum", outnum, buf); |
|||
return command_finished(cmd, out); |
|||
} |
@ -0,0 +1,10 @@ |
|||
#ifndef LIGHTNING_PLUGINS_SPENDER_FUNDCHANNEL_H |
|||
#define LIGHTNING_PLUGINS_SPENDER_FUNDCHANNEL_H |
|||
#include "config.h" |
|||
|
|||
#include <plugins/libplugin.h> |
|||
|
|||
extern const struct plugin_command fundchannel_commands[]; |
|||
extern const size_t num_fundchannel_commands; |
|||
|
|||
#endif /* LIGHTNING_PLUGINS_SPENDER_FUNDCHANNEL_H */ |
Loading…
Reference in new issue