From c585f227118bccdc77cb9914199abcecf1fa9262 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 12 Jun 2019 10:08:54 +0930 Subject: [PATCH] libplugin: update API to use json_out. We now hand around struct json_out members, rather than using formatted strings, so plugins need to construct them properly. There's no automatic conversion between ' and " any more, so those are eliminated too. pay still uses some manual construction of elements. Signed-off-by: Rusty Russell --- plugins/autoclean.c | 22 ++- plugins/libplugin.c | 100 +++++++------ plugins/libplugin.h | 34 +++-- plugins/pay.c | 342 +++++++++++++++++++++++--------------------- 4 files changed, 268 insertions(+), 230 deletions(-) diff --git a/plugins/autoclean.c b/plugins/autoclean.c index 3e45df829..8ef8b8ab5 100644 --- a/plugins/autoclean.c +++ b/plugins/autoclean.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -22,10 +23,16 @@ static struct command_result *ignore(struct command *timer, static struct command_result *do_clean(void) { - u64 age = time_now().ts.tv_sec - expired_by; + struct json_out *params = json_out_new(NULL); + json_out_start(params, NULL, '{'); + json_out_add(params, "maxexpirytime", false, "%"PRIu64, + time_now().ts.tv_sec - expired_by); + json_out_end(params, '}'); + json_out_finished(params); + /* FIXME: delexpiredinvoice should be in our plugin too! */ return send_outreq(NULL, "delexpiredinvoice", ignore, ignore, NULL, - "'maxexpirytime': %"PRIu64, age); + take(params)); } static struct command_result *json_autocleaninvoice(struct command *cmd, @@ -46,15 +53,16 @@ static struct command_result *json_autocleaninvoice(struct command *cmd, if (cycle_seconds == 0) { tal_free(cleantimer); - return command_success(cmd, "'Autoclean timer disabled'"); + return command_success_str(cmd, "Autoclean timer disabled"); } tal_free(cleantimer); cleantimer = plugin_timer(rpc, time_from_sec(cycle_seconds), do_clean); - return command_success(cmd, tal_fmt(cmd, "'Autocleaning %"PRIu64 - "-second old invoices every %"PRIu64 - " seconds'", - expired_by, cycle_seconds)); + return command_success_str(cmd, + tal_fmt(cmd, "Autocleaning %"PRIu64 + "-second old invoices every %"PRIu64 + " seconds", + expired_by, cycle_seconds)); } static void init(struct plugin_conn *prpc) diff --git a/plugins/libplugin.c b/plugins/libplugin.c index 85bf21c67..e365e28b1 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -84,6 +84,20 @@ struct command_result *command_param_failed(void) return &complete; } +struct json_out *json_out_obj(const tal_t *ctx, + const char *fieldname, + const char *str) +{ + struct json_out *jout = json_out_new(ctx); + json_out_start(jout, NULL, '{'); + if (str) + json_out_addstr(jout, fieldname, str); + json_out_end(jout, '}'); + json_out_finished(jout); + + return jout; +} + /* Realloc helper for tal membufs */ static void *membuf_tal_realloc(struct membuf *mb, void *rawelems, size_t newsize) @@ -190,17 +204,7 @@ static struct command_result *WARN_UNUSED_RESULT end_cmd(struct command *cmd) return &complete; } -/* FIXME: We promised callers we'd turn ' into ". */ -static void copy_with_quote_sub(char *dst, const char *src, size_t len) -{ - for (size_t i = 0; i < len; i++) { - if (src[i] == '\'') - dst[i] = '"'; - else - dst[i] = src[i]; - } -} - +/* str is raw JSON from RPC output. */ static struct command_result *WARN_UNUSED_RESULT command_done_raw(struct command *cmd, const char *label, @@ -208,23 +212,42 @@ command_done_raw(struct command *cmd, { struct json_out *jout = start_json_rpc(cmd, cmd->id); - copy_with_quote_sub(json_out_member_direct(jout, label, size), - str, size); + memcpy(json_out_member_direct(jout, label, size), str, size); finish_and_send_json(STDOUT_FILENO, jout); return end_cmd(cmd); } -static struct command_result *WARN_UNUSED_RESULT -command_done_ok(struct command *cmd, const char *result) +struct command_result *WARN_UNUSED_RESULT +command_success(struct command *cmd, const struct json_out *result) { - return command_done_raw(cmd, "result", result, strlen(result)); + struct json_out *jout = start_json_rpc(cmd, cmd->id); + + json_out_add_splice(jout, "result", result); + finish_and_send_json(STDOUT_FILENO, jout); + return end_cmd(cmd); +} + +struct command_result *WARN_UNUSED_RESULT +command_success_str(struct command *cmd, const char *str) +{ + struct json_out *jout = start_json_rpc(cmd, cmd->id); + + if (str) + json_out_addstr(jout, "result", str); + else { + /* Use an empty object if they don't want anything. */ + json_out_start(jout, "result", '{'); + json_out_end(jout, '}'); + } + finish_and_send_json(STDOUT_FILENO, jout); + return end_cmd(cmd); } struct command_result *command_done_err(struct command *cmd, int code, const char *errmsg, - const char *data) + const struct json_out *data) { struct json_out *jout = start_json_rpc(cmd, cmd->id); @@ -232,11 +255,8 @@ struct command_result *command_done_err(struct command *cmd, json_out_add(jout, "code", false, "%d", code); json_out_addstr(jout, "message", errmsg); - if (data) { - char *p; - p = json_out_member_direct(jout, "data", strlen(data)); - copy_with_quote_sub(p, data, strlen(data)); - } + if (data) + json_out_add_splice(jout, "data", data); json_out_end(jout, '}'); finish_and_send_json(STDOUT_FILENO, jout); @@ -250,11 +270,6 @@ struct command_result *timer_complete(void) return &complete; } -struct command_result *command_success(struct command *cmd, const char *result) -{ - return command_done_raw(cmd, "result", result, strlen(result)); -} - struct command_result *forward_error(struct command *cmd, const char *buf, const jsmntok_t *error, @@ -342,23 +357,23 @@ static const jsmntok_t *read_rpc_reply(const tal_t *ctx, static struct json_out *start_json_request(const tal_t *ctx, u64 id, const char *method, - const char *params) + const struct json_out *params TAKES) { struct json_out *jout; jout = start_json_rpc(tmpctx, id); json_out_addstr(jout, "method", method); - json_out_start(jout, "params", '{'); - copy_with_quote_sub(json_out_direct(jout, strlen(params)), - params, strlen(params)); - json_out_end(jout, '}'); + json_out_add_splice(jout, "params", params); + if (taken(params)) + tal_free(params); return jout; } /* Synchronous routine to send command and extract single field from response */ const char *rpc_delve(const tal_t *ctx, - const char *method, const char *params, + const char *method, + const struct json_out *params TAKES, struct plugin_conn *rpc, const char *guide) { bool error; @@ -435,12 +450,10 @@ send_outreq_(struct command *cmd, const jsmntok_t *result, void *arg), void *arg, - const char *paramfmt_single_ticks, ...) + const struct json_out *params TAKES) { - va_list ap; struct json_out *jout; struct out_req *out; - char *params; out = tal(cmd, struct out_req); out->id = next_outreq_id++; @@ -450,10 +463,6 @@ send_outreq_(struct command *cmd, out->arg = arg; uintmap_add(&out_reqs, out->id, out); - va_start(ap, paramfmt_single_ticks); - params = tal_vfmt(tmpctx, paramfmt_single_ticks, ap); - va_end(ap); - jout = start_json_request(tmpctx, out->id, method, params); finish_and_send_json(rpc_conn.fd, jout); @@ -467,8 +476,6 @@ handle_getmanifest(struct command *getmanifest_cmd, const struct plugin_option *opts) { struct json_out *params = json_out_new(tmpctx); - size_t len; - const char *p; json_out_start(params, NULL, '{'); json_out_start(params, "options", '['); @@ -497,8 +504,7 @@ handle_getmanifest(struct command *getmanifest_cmd, json_out_end(params, '}'); json_out_finished(params); - p = json_out_contents(params, &len); - return command_done_raw(getmanifest_cmd, "result", p, len); + return command_success(getmanifest_cmd, params); } static struct command_result *handle_init(struct command *init_cmd, @@ -511,6 +517,7 @@ static struct command_result *handle_init(struct command *init_cmd, struct sockaddr_un addr; size_t i; char *dir; + struct json_out *param_obj; /* Move into lightning directory: other files are relative */ dirtok = json_delve(buf, params, ".configuration.lightning-dir"); @@ -533,8 +540,9 @@ static struct command_result *handle_init(struct command *init_cmd, rpctok->end - rpctok->start, buf + rpctok->start, strerror(errno)); + param_obj = json_out_obj(NULL, "config", "allow-deprecated-apis"); deprecated_apis = streq(rpc_delve(tmpctx, "listconfigs", - "'config': 'allow-deprecated-apis'", + take(param_obj), &rpc_conn, ".allow-deprecated-apis"), "true"); @@ -558,7 +566,7 @@ static struct command_result *handle_init(struct command *init_cmd, if (init) init(&rpc_conn); - return command_done_ok(init_cmd, "{}"); + return command_success_str(init_cmd, NULL); } char *u64_option(const char *arg, u64 *i) diff --git a/plugins/libplugin.h b/plugins/libplugin.h index dc3ce911f..3be2ff319 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -11,6 +11,7 @@ #include struct command; +struct json_out; struct plugin_conn; extern bool deprecated_apis; @@ -35,6 +36,12 @@ struct plugin_option { void *arg; }; +/* Helper to create a zero or single-value JSON object; if @str is NULL, + * object is empty. */ +struct json_out *json_out_obj(const tal_t *ctx, + const char *fieldname, + const char *str); + /* Return this iff the param() call failed in your handler. */ struct command_result *command_param_failed(void); @@ -42,27 +49,34 @@ struct command_result *command_param_failed(void); void NORETURN plugin_err(const char *fmt, ...); /* This command is finished, here's a detailed error; @cmd cannot be - * NULL, data can be NULL. */ + * NULL, data can be NULL; otherwise it must be a JSON object. */ struct command_result *WARN_UNUSED_RESULT command_done_err(struct command *cmd, int code, const char *errmsg, - const char *data); + const struct json_out *data); + +/* This command is finished, here's the result object; @cmd cannot be NULL. */ +struct command_result *WARN_UNUSED_RESULT +command_success(struct command *cmd, const struct json_out *result); -/* This command is finished, here's the success msg; @cmd cannot be NULL. */ +/* Simple version where we just want to send a string, or NULL means an empty + * result object. @cmd cannot be NULL. */ struct command_result *WARN_UNUSED_RESULT -command_success(struct command *cmd, const char *result); +command_success_str(struct command *cmd, const char *str); /* Synchronous helper to send command and extract single field from * response; can only be used in init callback. */ const char *rpc_delve(const tal_t *ctx, - const char *method, const char *params, + const char *method, + const struct json_out *params TAKES, struct plugin_conn *rpc, const char *guide); -/* Async rpc request. For convenience, and single ' are turned into ". +/* Async rpc request. * @cmd can be NULL if we're coming from a timer callback. + * @params can be NULL, otherwise it's an array or object. */ -PRINTF_FMT(6,7) struct command_result * +struct command_result * send_outreq_(struct command *cmd, const char *method, struct command_result *(*cb)(struct command *command, @@ -74,9 +88,9 @@ send_outreq_(struct command *cmd, const jsmntok_t *result, void *arg), void *arg, - const char *paramfmt_single_ticks, ...); + const struct json_out *params TAKES); -#define send_outreq(cmd, method, cb, errcb, arg, ...) \ +#define send_outreq(cmd, method, cb, errcb, arg, params) \ send_outreq_((cmd), (method), \ typesafe_cb_preargs(struct command_result *, void *, \ (cb), (arg), \ @@ -88,7 +102,7 @@ send_outreq_(struct command *cmd, struct command *command, \ const char *buf, \ const jsmntok_t *result), \ - (arg), __VA_ARGS__) + (arg), (params)) /* Callback to just forward error and close request; @cmd cannot be NULL */ struct command_result *forward_error(struct command *cmd, diff --git a/plugins/pay.c b/plugins/pay.c index 1591560ec..f6effa6ce 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -136,12 +137,38 @@ static void attempt_failed_tok(struct pay_command *pc, const char *method, buf + msg->start); else attempt_failed_fmt(pc, - "{ 'message': 'Call to %s failed', %.*s", + "{ \"message\": \"Call to %s failed\", %.*s", method, errtok->end - errtok->start - 1, buf + errtok->start + 1); } +/* Helper to copy JSON object directly into a json_out */ +static void json_out_add_raw_len(struct json_out *jout, + const char *fieldname, + const char *jsonstr, size_t len) +{ + char *p; + + p = json_out_member_direct(jout, fieldname, len); + memcpy(p, jsonstr, len); +} + +static void json_out_add_raw(struct json_out *jout, + const char *fieldname, + const char *jsonstr) +{ + json_out_add_raw_len(jout, fieldname, jsonstr, strlen(jsonstr)); +} + +/* Helper to add a u64. */ +static void json_out_add_u64(struct json_out *jout, + const char *fieldname, + u64 val) +{ + json_out_add(jout, fieldname, false, "%"PRIu64, val); +} + static struct command_result *start_pay_attempt(struct command *cmd, struct pay_command *pc, const char *fmt, ...); @@ -178,24 +205,23 @@ static size_t count_sendpays(const struct pay_attempt *attempts) static struct command_result *waitsendpay_expired(struct command *cmd, struct pay_command *pc) { - char *errmsg, *data; + char *errmsg; + struct json_out *data; size_t num_attempts = count_sendpays(pc->ps->attempts); errmsg = tal_fmt(pc, "Gave up after %zu attempt%s: see paystatus", num_attempts, num_attempts == 1 ? "" : "s"); - data = tal_strdup(pc, "{ 'attempts': [ "); + data = json_out_new(NULL); + json_out_start(data, NULL, '{'); + json_out_start(data, "attempts", '['); for (size_t i = 0; i < tal_count(pc->ps->attempts); i++) { if (pc->ps->attempts[i].route) - tal_append_fmt(&data, "%s { 'route': %s,\n 'failure': %s\n }", - i == 0 ? "" : ",", - pc->ps->attempts[i].route, - pc->ps->attempts[i].failure); - else - tal_append_fmt(&data, "%s { 'failure': %s\n }", - i == 0 ? "" : ",", - pc->ps->attempts[i].failure); + json_out_add_raw(data, "route", + pc->ps->attempts[i].route); + json_out_add_raw(data, "failure", pc->ps->attempts[i].failure); } - tal_append_fmt(&data, "] }"); + json_out_end(data, ']'); + json_out_end(data, '}'); return command_done_err(cmd, PAY_STOPPED_RETRYING, errmsg, data); } @@ -315,8 +341,8 @@ static struct command_result *sendpay_done(struct command *cmd, { return send_outreq(cmd, "waitsendpay", waitsendpay_done, waitsendpay_error, pc, - "'payment_hash': '%s'", - pc->payment_hash); + take(json_out_obj(NULL, "payment_hash", + pc->payment_hash))); } /* Calculate how many millisatoshi we need at the start of this route @@ -375,10 +401,10 @@ static const char *join_routehint(const tal_t *ctx, return tal_free(ret); tal_append_fmt(&ret, ", {" - " 'id': '%s'," - " 'channel': '%s'," - " 'msatoshi': '%s'," - " 'delay': %u }", + " \"id\": \"%s\"," + " \"channel\": \"%s\"," + " \"msatoshi\": \"%s\"," + " \"delay\": %u }", /* pubkey of *destination* */ route_pubkey(tmpctx, pc, routehint, i + 1), type_to_string(tmpctx, struct short_channel_id, @@ -455,10 +481,10 @@ static struct command_result *getroute_done(struct command *cmd, { struct pay_attempt *attempt = current_attempt(pc); const jsmntok_t *t = json_get_member(buf, result, "route"); - char *json_desc; struct amount_msat fee; u32 delay; double feepercent; + struct json_out *params; if (!t) plugin_err("getroute gave no 'route'? '%.*s'", @@ -469,7 +495,7 @@ static struct command_result *getroute_done(struct command *cmd, pc, pc->current_routehint); if (!attempt->route) { attempt_failed_fmt(pc, - "{ 'message': 'Joining routehint gave absurd fee' }"); + "{ \"message\": \"Joining routehint gave absurd fee\" }"); return next_routehint(cmd, pc); } } else @@ -497,7 +523,7 @@ static struct command_result *getroute_done(struct command *cmd, && feepercent > pc->maxfeepercent) { const jsmntok_t *charger; - attempt_failed_fmt(pc, "{ 'message': 'Route wanted fee of %s' }", + attempt_failed_fmt(pc, "{ \"message\": \"Route wanted fee of %s\" }", type_to_string(tmpctx, struct amount_msat, &fee)); @@ -526,7 +552,7 @@ static struct command_result *getroute_done(struct command *cmd, const jsmntok_t *delayer; attempt_failed_fmt(pc, - "{ 'message': 'Route wanted delay of %u blocks' }", + "{ \"message\": \"Route wanted delay of %u blocks\" }", delay); /* Remember this if we eliminating this causes us to have no @@ -549,18 +575,18 @@ static struct command_result *getroute_done(struct command *cmd, return next_routehint(cmd, pc); } + attempt->sendpay = true; + params = json_out_new(NULL); + json_out_start(params, NULL, '{'); + json_out_add_raw(params, "route", attempt->route); + json_out_add(params, "payment_hash", true, "%s", pc->payment_hash); + json_out_add(params, "bolt11", true, "%s", pc->ps->bolt11); if (pc->label) - json_desc = tal_fmt(pc, ", 'label': '%s'", pc->label); - else - json_desc = ""; + json_out_add(params, "label", true, "%s", pc->label); + json_out_end(params, '}'); - attempt->sendpay = true; return send_outreq(cmd, "sendpay", sendpay_done, sendpay_error, pc, - "'route': %s, 'payment_hash': '%s', 'bolt11': '%s'%s", - attempt->route, - pc->payment_hash, - pc->ps->bolt11, - json_desc); + take(params)); } @@ -600,7 +626,6 @@ static struct command_result *start_pay_attempt(struct command *cmd, struct pay_command *pc, const char *fmt, ...) { - char *exclude; struct amount_msat msat; const char *dest; size_t max_hops = ROUTING_MAX_HOPS; @@ -608,6 +633,7 @@ static struct command_result *start_pay_attempt(struct command *cmd, struct pay_attempt *attempt; va_list ap; size_t n; + struct json_out *params; n = tal_count(pc->ps->attempts); tal_resize(&pc->ps->attempts, n+1); @@ -627,17 +653,6 @@ static struct command_result *start_pay_attempt(struct command *cmd, /* routehint set below. */ - if (tal_count(pc->excludes) != 0) { - exclude = tal_strdup(tmpctx, ",'exclude': ["); - for (size_t i = 0; i < tal_count(pc->excludes); i++) - /* JSON.org grammar doesn't allow trailing , */ - tal_append_fmt(&exclude, "%s %s", - i == 0 ? "" : ",", - pc->excludes[i]); - tal_append_fmt(&exclude, "]"); - } else - exclude = ""; - /* If we have a routehint, try that first; we need to do extra * checks that it meets our criteria though. */ if (pc->current_routehint) { @@ -646,7 +661,7 @@ static struct command_result *start_pay_attempt(struct command *cmd, attempt->routehint, tal_count(attempt->routehint))) { attempt_failed_fmt(pc, - "{ 'message': 'Routehint absurd fee' }"); + "{ \"message\": \"Routehint absurd fee\" }"); return next_routehint(cmd, pc); } dest = type_to_string(tmpctx, struct node_id, @@ -663,15 +678,24 @@ static struct command_result *start_pay_attempt(struct command *cmd, } /* OK, ask for route to destination */ + params = json_out_new(NULL); + json_out_start(params, NULL, '{'); + json_out_addstr(params, "id", dest); + json_out_addstr(params, "msatoshi", + type_to_string(tmpctx, struct amount_msat, &msat)); + json_out_add_u64(params, "cltv", cltv); + json_out_add_u64(params, "maxhops", max_hops); + json_out_add(params, "riskfactor", false, "%f", pc->riskfactor); + if (tal_count(pc->excludes) != 0) { + json_out_start(params, "exclude", '['); + for (size_t i = 0; i < tal_count(pc->excludes); i++) + json_out_addstr(params, NULL, pc->excludes[i]); + json_out_end(params, ']'); + } + json_out_end(params, '}'); + return send_outreq(cmd, "getroute", getroute_done, getroute_error, pc, - "'id': '%s'," - "'msatoshi': '%s'," - "'cltv': %u," - "'maxhops': %zu," - "'riskfactor': %f%s", - dest, - type_to_string(tmpctx, struct amount_msat, &msat), - cltv, max_hops, pc->riskfactor, exclude); + take(params)); } /* BOLT #7: @@ -751,7 +775,7 @@ static struct command_result *shadow_route(struct command *cmd, return send_outreq(cmd, "listchannels", add_shadow_route, forward_error, pc, - "'source' : '%s'", pc->shadow_dest); + take(json_out_obj(NULL, "source", pc->shadow_dest))); } /* gossipd doesn't know much about the current state of channels; here we @@ -996,10 +1020,9 @@ static struct command_result *json_pay(struct command *cmd, pc->routehints = filter_routehints(pc, b11->routes); pc->expensive_route = NULL; - /* Get capacities of local channels. */ + /* Get capacities of local channels (no parameters) */ return send_outreq(cmd, "listpeers", listpeers_done, forward_error, pc, - /* gcc doesn't like zero-length format strings! */ - " "); + take(json_out_obj(NULL, NULL, NULL))); } /* FIXME: Add this to ccan/time? */ @@ -1014,7 +1037,7 @@ static void utc_timestring(const struct timeabs *time, char str[UTC_TIMELEN]) (int) time->ts.tv_nsec / 1000000); } -static void add_attempt(char **ret, +static void add_attempt(struct json_out *ret, const struct pay_status *ps, const struct pay_attempt *attempt) { @@ -1022,62 +1045,56 @@ static void add_attempt(char **ret, utc_timestring(&attempt->start, timestr); - tal_append_fmt(ret, "{ 'strategy': '%s'," - " 'start_time': '%s'," - " 'age_in_seconds': %"PRIu64, - attempt->why, - timestr, - time_to_sec(time_between(time_now(), attempt->start))); + json_out_start(ret, NULL, '{'); + json_out_addstr(ret, "strategy", attempt->why); + json_out_addstr(ret, "start_time", timestr); + json_out_add_u64(ret, "age_in_seconds", + time_to_sec(time_between(time_now(), attempt->start))); if (attempt->result || attempt->failure) { utc_timestring(&attempt->end, timestr); - tal_append_fmt(ret, ", 'end_time': '%s'" - ", 'duration_in_seconds': %"PRIu64, - timestr, - time_to_sec(time_between(attempt->end, - attempt->start))); + json_out_addstr(ret, "end_time", timestr); + json_out_add_u64(ret, "duration_in_seconds", + time_to_sec(time_between(attempt->end, + attempt->start))); } if (tal_count(attempt->routehint)) { - tal_append_fmt(ret, ", 'routehint': ["); + json_out_start(ret, "routehint", '['); for (size_t i = 0; i < tal_count(attempt->routehint); i++) { - tal_append_fmt(ret, "%s{" - " 'id': '%s'," - " 'channel': '%s'," - " 'fee_base_msat': %u," - " 'fee_proportional_millionths': %u," - " 'cltv_expiry_delta': %u }", - i == 0 ? "" : ", ", - type_to_string(tmpctx, struct node_id, - &attempt->routehint[i].pubkey), - type_to_string(tmpctx, - struct short_channel_id, - &attempt->routehint[i].short_channel_id), - attempt->routehint[i].fee_base_msat, - attempt->routehint[i].fee_proportional_millionths, - attempt->routehint[i].cltv_expiry_delta); + json_out_start(ret, NULL, '{'); + json_out_addstr(ret, "id", + type_to_string(tmpctx, struct node_id, + &attempt->routehint[i].pubkey)); + json_out_addstr(ret, "channel", + type_to_string(tmpctx, + struct short_channel_id, + &attempt->routehint[i].short_channel_id)); + json_out_add_u64(ret, "fee_base_msat", + attempt->routehint[i].fee_base_msat); + json_out_add_u64(ret, "fee_proportional_millionths", + attempt->routehint[i].fee_proportional_millionths); + json_out_add_u64(ret, "cltv_expiry_delta", + attempt->routehint[i].cltv_expiry_delta); + json_out_end(ret, '}'); } - tal_append_fmt(ret, "]"); + json_out_end(ret, ']'); } if (tal_count(attempt->excludes)) { - for (size_t i = 0; i < tal_count(attempt->excludes); i++) { - if (i == 0) - tal_append_fmt(ret, ", 'excluded_channels': ["); - else - tal_append_fmt(ret, ", "); - tal_append_fmt(ret, "'%s'", attempt->excludes[i]); - } - tal_append_fmt(ret, "]"); + json_out_start(ret, "excluded_channels", '['); + for (size_t i = 0; i < tal_count(attempt->excludes); i++) + json_out_addstr(ret, NULL, attempt->excludes[i]); + json_out_end(ret, ']'); } if (attempt->route) - tal_append_fmt(ret, ", 'route': %s", attempt->route); + json_out_add_raw(ret, "route", attempt->route); if (attempt->failure) - tal_append_fmt(ret, ", 'failure': %s", attempt->failure); + json_out_add_raw(ret, "failure", attempt->failure); if (attempt->result) - tal_append_fmt(ret, ", 'success': %s", attempt->result); + json_out_add_raw(ret, "success", attempt->result); - tal_append_fmt(ret, "}"); + json_out_end(ret, '}'); } static struct command_result *json_paystatus(struct command *cmd, @@ -1086,71 +1103,65 @@ static struct command_result *json_paystatus(struct command *cmd, { struct pay_status *ps; const char *b11str; - char *ret; - bool some = false; + struct json_out *ret; if (!param(cmd, buf, params, p_opt("bolt11", param_string, &b11str), NULL)) return NULL; - ret = tal_fmt(cmd, "{ 'pay': ["); + ret = json_out_new(NULL); + json_out_start(ret, NULL, '{'); + json_out_start(ret, "pay", '['); + /* FIXME: Index by bolt11 string! */ list_for_each(&pay_status, ps, list) { if (b11str && !streq(b11str, ps->bolt11)) continue; - if (some) - tal_append_fmt(&ret, ",\n"); - some = true; - - tal_append_fmt(&ret, "{ 'bolt11': '%s'," - " 'msatoshi': %"PRIu64", " - " 'amount_msat': '%s', " - " 'destination': '%s'", - ps->bolt11, - ps->msat.millisatoshis, /* Raw: JSON */ + json_out_start(ret, NULL, '{'); + json_out_addstr(ret, "bolt11", ps->bolt11); + json_out_add_u64(ret, "msatoshi", + ps->msat.millisatoshis); /* Raw: JSON */ + json_out_addstr(ret, "amount_msat", type_to_string(tmpctx, struct amount_msat, - &ps->msat), ps->dest); + &ps->msat)); + json_out_addstr(ret, "destination", ps->dest); if (ps->label) - tal_append_fmt(&ret, ", 'label': '%s'", ps->label); + json_out_addstr(ret, "label", ps->label); if (ps->routehint_modifications) - tal_append_fmt(&ret, ", 'routehint_modifications': '%s'", - ps->routehint_modifications); + json_out_addstr(ret, "routehint_modifications", + ps->routehint_modifications); if (ps->shadow && !streq(ps->shadow, "")) - tal_append_fmt(&ret, ", 'shadow': '%s'", ps->shadow); + json_out_addstr(ret, "shadow", ps->shadow); if (ps->exclusions) - tal_append_fmt(&ret, ", 'local_exclusions': '%s'", - ps->exclusions); + json_out_addstr(ret, "local_exclusions", ps->exclusions); assert(tal_count(ps->attempts)); - for (size_t i = 0; i < tal_count(ps->attempts); i++) { - if (i == 0) - tal_append_fmt(&ret, ", 'attempts': ["); - else - tal_append_fmt(&ret, ","); - - add_attempt(&ret, ps, &ps->attempts[i]); - } - tal_append_fmt(&ret, "] }"); + json_out_start(ret, "attempts", '['); + for (size_t i = 0; i < tal_count(ps->attempts); i++) + add_attempt(ret, ps, &ps->attempts[i]); + json_out_end(ret, ']'); + json_out_end(ret, '}'); } - tal_append_fmt(&ret, "] }"); + json_out_end(ret, ']'); + json_out_end(ret, '}'); return command_success(cmd, ret); } -static const jsmntok_t *copy_member(char **ret, - const char *buf, - const jsmntok_t *result, - const char *membername, - const char *term) +/* Copy field and member to output, if it exists: return member */ +static const jsmntok_t *copy_member(struct json_out *ret, + const char *buf, const jsmntok_t *obj, + const char *membername) { - const jsmntok_t *m = json_get_member(buf, result, membername); - if (m) - tal_append_fmt(ret, "'%s': %.*s%s", - membername, - json_tok_full_len(m), json_tok_full(buf, m), - term); + const jsmntok_t *m = json_get_member(buf, obj, membername); + if (!m) + return NULL; + + /* Literal copy: it's already JSON escaped, and may be a string. */ + json_out_add_raw_len(ret, membername, + json_tok_full(buf, m), json_tok_full_len(m)); return m; } @@ -1175,33 +1186,30 @@ static struct command_result *listsendpays_done(struct command *cmd, { size_t i; const jsmntok_t *t, *arr; - char *ret; - bool some = false; + struct json_out *ret; arr = json_get_member(buf, result, "payments"); if (!arr || arr->type != JSMN_ARRAY) return command_fail(cmd, LIGHTNINGD, "Unexpected non-array result from listsendpays"); - ret = tal_fmt(cmd, "{ 'pays': ["); + ret = json_out_new(NULL); + json_out_start(ret, NULL, '{'); + json_out_start(ret, "pays", '['); json_for_each_arr(i, t, arr) { const jsmntok_t *status, *b11; - if (some) - tal_append_fmt(&ret, ",\n"); - some = true; - - tal_append_fmt(&ret, "{"); + json_out_start(ret, NULL, '{'); /* Old payments didn't have bolt11 field */ - b11 = copy_member(&ret, buf, t, "bolt11", ","); + b11 = copy_member(ret, buf, t, "bolt11"); if (!b11) { if (b11str) { /* If it's a single query, we can fake it */ - tal_append_fmt(&ret, "'bolt11': '%s',", b11str); + json_out_addstr(ret, "bolt11", b11str); } else { - copy_member(&ret, buf, t, "payment_hash", ","); - copy_member(&ret, buf, t, "destination", ","); - copy_member(&ret, buf, t, "amount_msat", ","); + copy_member(ret, buf, t, "payment_hash"); + copy_member(ret, buf, t, "destination"); + copy_member(ret, buf, t, "amount_msat"); } } @@ -1210,19 +1218,20 @@ static struct command_result *listsendpays_done(struct command *cmd, if (status) { if (json_tok_streq(buf, status, "failed") && attempt_ongoing(buf, b11)) { - tal_append_fmt(&ret, "'status': 'pending',"); + json_out_addstr(ret, "status", "pending"); } else { - copy_member(&ret, buf, t, "status", ","); + copy_member(ret, buf, t, "status"); if (json_tok_streq(buf, status, "complete")) - copy_member(&ret, buf, t, - "payment_preimage", ","); + copy_member(ret, buf, t, + "payment_preimage"); } } - copy_member(&ret, buf, t, "label", ","); - copy_member(&ret, buf, t, "amount_sent_msat", ""); - tal_append_fmt(&ret, "}"); + copy_member(ret, buf, t, "label"); + copy_member(ret, buf, t, "amount_sent_msat"); + json_out_end(ret, '}'); } - tal_append_fmt(&ret, "] }"); + json_out_end(ret, ']'); + json_out_end(ret, '}'); return command_success(cmd, ret); } @@ -1230,7 +1239,7 @@ static struct command_result *json_listpays(struct command *cmd, const char *buf, const jsmntok_t *params) { - const char *b11str, *paramstr; + const char *b11str; /* FIXME: would be nice to parse as a bolt11 so check worked in future */ if (!param(cmd, buf, params, @@ -1238,26 +1247,25 @@ static struct command_result *json_listpays(struct command *cmd, NULL)) return NULL; - if (b11str) - paramstr = tal_fmt(tmpctx, "'bolt11' : '%s'", b11str); - else - paramstr = ""; return send_outreq(cmd, "listsendpays", listsendpays_done, forward_error, cast_const(char *, b11str), - "%s", paramstr); + /* Neatly returns empty object if b11str is NULL */ + take(json_out_obj(NULL, "bolt11", b11str))); } static void init(struct plugin_conn *rpc) { const char *field; - field = rpc_delve(tmpctx, "getinfo", "", rpc, ".id"); + field = rpc_delve(tmpctx, "getinfo", + take(json_out_obj(NULL, NULL, NULL)), rpc, ".id"); if (!node_id_from_hexstr(field, strlen(field), &my_id)) plugin_err("getinfo didn't contain valid id: '%s'", field); field = rpc_delve(tmpctx, "listconfigs", - "'config': 'max-locktime-blocks'", + take(json_out_obj(NULL, + "config", "max-locktime-blocks")), rpc, ".max-locktime-blocks"); maxdelay_default = atoi(field); }