Browse Source
Doesn't catch the reply yet, but prepares the invoice request based on the offer and sends it. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>ppa
committed by
Christian Decker
3 changed files with 488 additions and 1 deletions
@ -0,0 +1,478 @@ |
|||||
|
#include <bitcoin/chainparams.h> |
||||
|
#include <ccan/array_size/array_size.h> |
||||
|
#include <ccan/json_out/json_out.h> |
||||
|
#include <ccan/tal/str/str.h> |
||||
|
#include <ccan/time/time.h> |
||||
|
#include <common/blindedpath.h> |
||||
|
#include <common/bolt11.h> |
||||
|
#include <common/bolt12.h> |
||||
|
#include <common/bolt12_merkle.h> |
||||
|
#include <common/dijkstra.h> |
||||
|
#include <common/gossmap.h> |
||||
|
#include <common/json_stream.h> |
||||
|
#include <common/memleak.h> |
||||
|
#include <common/route.h> |
||||
|
#include <common/type_to_string.h> |
||||
|
#include <common/utils.h> |
||||
|
#include <errno.h> |
||||
|
#include <inttypes.h> |
||||
|
#include <plugins/libplugin.h> |
||||
|
|
||||
|
static struct gossmap *global_gossmap; |
||||
|
static struct node_id local_id; |
||||
|
|
||||
|
struct sent { |
||||
|
/* The offer we are trying to get an invoice for. */ |
||||
|
struct tlv_offer *offer; |
||||
|
/* The invreq we sent. */ |
||||
|
struct tlv_invoice_request *invreq; |
||||
|
}; |
||||
|
|
||||
|
static struct command_result *sendonionmsg_done(struct command *cmd, |
||||
|
const char *buf UNUSED, |
||||
|
const jsmntok_t *result UNUSED, |
||||
|
struct sent *sent) |
||||
|
{ |
||||
|
/* FIXME: Now wait for reply. */ |
||||
|
return command_still_pending(cmd); |
||||
|
} |
||||
|
|
||||
|
static void init_gossmap(struct plugin *plugin) |
||||
|
{ |
||||
|
global_gossmap |
||||
|
= notleak_with_children(gossmap_load(NULL, |
||||
|
GOSSIP_STORE_FILENAME)); |
||||
|
if (!global_gossmap) |
||||
|
plugin_err(plugin, "Could not load gossmap %s: %s", |
||||
|
GOSSIP_STORE_FILENAME, strerror(errno)); |
||||
|
} |
||||
|
|
||||
|
static struct gossmap *get_gossmap(struct plugin *plugin) |
||||
|
{ |
||||
|
if (!global_gossmap) |
||||
|
init_gossmap(plugin); |
||||
|
else |
||||
|
gossmap_refresh(global_gossmap); |
||||
|
return global_gossmap; |
||||
|
} |
||||
|
|
||||
|
static struct command_result *param_offer(struct command *cmd, |
||||
|
const char *name, |
||||
|
const char *buffer, |
||||
|
const jsmntok_t *tok, |
||||
|
struct tlv_offer **offer) |
||||
|
{ |
||||
|
char *fail; |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - if `features` contains unknown _odd_ bits that are non-zero: |
||||
|
* - MUST ignore the bit. |
||||
|
* - if `features` contains unknown _even_ bits that are non-zero: |
||||
|
* - MUST NOT respond to the offer. |
||||
|
* - SHOULD indicate the unknown bit to the user. |
||||
|
*/ |
||||
|
/* BOLT-offers #12:
|
||||
|
* - MUST NOT set or imply any `chain_hash` not set or implied by |
||||
|
* the offer. |
||||
|
*/ |
||||
|
*offer = offer_decode(cmd, buffer + tok->start, tok->end - tok->start, |
||||
|
plugin_feature_set(cmd->plugin), chainparams, |
||||
|
&fail); |
||||
|
if (!*offer) |
||||
|
return command_fail_badparam(cmd, name, buffer, tok, |
||||
|
tal_fmt(cmd, |
||||
|
"Unparsable offer: %s", |
||||
|
fail)); |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* |
||||
|
* - if `node_id`, `description` or `signature` is not set: |
||||
|
* - MUST NOT respond to the offer. |
||||
|
*/ |
||||
|
/* Note: offer_decode checks `signature` */ |
||||
|
if (!(*offer)->node_id) |
||||
|
return command_fail_badparam(cmd, name, buffer, tok, |
||||
|
"Offer does not contain a node_id"); |
||||
|
|
||||
|
if (!(*offer)->description) |
||||
|
return command_fail_badparam(cmd, name, buffer, tok, |
||||
|
"Offer does not contain a description"); |
||||
|
return NULL; |
||||
|
} |
||||
|
|
||||
|
static bool can_carry_onionmsg(const struct gossmap *map, |
||||
|
const struct gossmap_chan *c, |
||||
|
int dir, |
||||
|
struct amount_msat amount UNUSED, |
||||
|
void *arg UNUSED) |
||||
|
{ |
||||
|
const struct gossmap_node *n; |
||||
|
/* Don't use it if either side says it's disabled */ |
||||
|
if (!c->half[dir].enabled || !c->half[!dir].enabled) |
||||
|
return false; |
||||
|
|
||||
|
/* Check features of recipient */ |
||||
|
n = gossmap_nth_node(map, c, !dir); |
||||
|
return n && gossmap_node_get_feature(map, n, OPT_ONION_MESSAGES) != -1; |
||||
|
} |
||||
|
|
||||
|
/* make_blindedpath only needs pubkeys */ |
||||
|
static const struct pubkey *route_backwards(const tal_t *ctx, |
||||
|
const struct gossmap *gossmap, |
||||
|
struct route **r) |
||||
|
{ |
||||
|
struct pubkey *rarr; |
||||
|
|
||||
|
rarr = tal_arr(ctx, struct pubkey, tal_count(r)); |
||||
|
for (size_t i = 0; i < tal_count(r); i++) { |
||||
|
const struct gossmap_node *dst; |
||||
|
struct node_id id; |
||||
|
|
||||
|
dst = gossmap_nth_node(gossmap, r[i]->c, r[i]->dir); |
||||
|
gossmap_node_get_id(gossmap, dst, &id); |
||||
|
/* We're going backwards */ |
||||
|
if (!pubkey_from_node_id(&rarr[tal_count(rarr) - 1 - i], &id)) |
||||
|
abort(); |
||||
|
} |
||||
|
|
||||
|
return rarr; |
||||
|
} |
||||
|
|
||||
|
static struct command_result *send_message(struct command *cmd, |
||||
|
struct sent *sent, |
||||
|
const char *msgfield, |
||||
|
const u8 *msgval) |
||||
|
{ |
||||
|
const struct dijkstra *dij; |
||||
|
const struct gossmap_node *dst, *src; |
||||
|
struct route **r; |
||||
|
struct gossmap *gossmap = get_gossmap(cmd->plugin); |
||||
|
const struct pubkey *backwards; |
||||
|
struct onionmsg_path **path; |
||||
|
struct pubkey blinding, reply_blinding; |
||||
|
struct out_req *req; |
||||
|
struct node_id dstid; |
||||
|
|
||||
|
/* FIXME: Use blinded path if avail. */ |
||||
|
gossmap_guess_node_id(gossmap, sent->offer->node_id, &dstid); |
||||
|
dst = gossmap_find_node(gossmap, &dstid); |
||||
|
if (!dst) |
||||
|
return command_fail(cmd, LIGHTNINGD, |
||||
|
"Unknown destination %s", |
||||
|
type_to_string(tmpctx, struct node_id, |
||||
|
&dstid)); |
||||
|
|
||||
|
/* If we don't exist in gossip, routing can't happen. */ |
||||
|
src = gossmap_find_node(gossmap, &local_id); |
||||
|
if (!src) |
||||
|
return command_fail(cmd, PAY_ROUTE_NOT_FOUND, |
||||
|
"We don't have any channels"); |
||||
|
|
||||
|
dij = dijkstra(tmpctx, gossmap, dst, AMOUNT_MSAT(0), 0, |
||||
|
can_carry_onionmsg, route_score_shorter, NULL); |
||||
|
|
||||
|
r = route_from_dijkstra(tmpctx, gossmap, dij, src); |
||||
|
if (!r) |
||||
|
/* FIXME: We need to retry kind of like keysend here... */ |
||||
|
return command_fail(cmd, OFFER_ROUTE_NOT_FOUND, |
||||
|
"Can't find route"); |
||||
|
|
||||
|
/* Ok, now make reply for onion_message */ |
||||
|
backwards = route_backwards(tmpctx, gossmap, r); |
||||
|
path = make_blindedpath(tmpctx, backwards, &blinding, &reply_blinding); |
||||
|
|
||||
|
req = jsonrpc_request_start(cmd->plugin, cmd, "sendonionmessage", |
||||
|
&sendonionmsg_done, |
||||
|
&forward_error, |
||||
|
sent); |
||||
|
json_array_start(req->js, "hops"); |
||||
|
for (size_t i = 0; i < tal_count(r); i++) { |
||||
|
struct node_id id; |
||||
|
|
||||
|
json_object_start(req->js, NULL); |
||||
|
gossmap_node_get_id(gossmap, |
||||
|
gossmap_nth_node(gossmap, r[i]->c, !r[i]->dir), |
||||
|
&id); |
||||
|
json_add_node_id(req->js, "id", &id); |
||||
|
if (i == tal_count(r) - 1) |
||||
|
json_add_hex_talarr(req->js, msgfield, msgval); |
||||
|
json_object_end(req->js); |
||||
|
} |
||||
|
json_array_end(req->js); |
||||
|
|
||||
|
json_object_start(req->js, "reply_path"); |
||||
|
json_add_pubkey(req->js, "blinding", &blinding); |
||||
|
json_array_start(req->js, "path"); |
||||
|
for (size_t i = 0; i < tal_count(path); i++) { |
||||
|
json_object_start(req->js, NULL); |
||||
|
json_add_pubkey(req->js, "id", &path[i]->node_id); |
||||
|
if (path[i]->enctlv) |
||||
|
json_add_hex_talarr(req->js, "enctlv", path[i]->enctlv); |
||||
|
json_object_end(req->js); |
||||
|
} |
||||
|
json_array_end(req->js); |
||||
|
json_object_end(req->js); |
||||
|
return send_outreq(cmd->plugin, req); |
||||
|
} |
||||
|
|
||||
|
static struct command_result *invreq_done(struct command *cmd, |
||||
|
const char *buf, |
||||
|
const jsmntok_t *result, |
||||
|
struct tlv_offer *offer) |
||||
|
{ |
||||
|
const jsmntok_t *t; |
||||
|
struct sent *sent; |
||||
|
char *fail; |
||||
|
u8 *rawinvreq; |
||||
|
|
||||
|
/* We need to remember both offer and invreq to check reply. */ |
||||
|
sent = tal(cmd, struct sent); |
||||
|
sent->offer = tal_steal(sent, offer); |
||||
|
|
||||
|
/* Get invoice request */ |
||||
|
t = json_get_member(buf, result, "bolt12"); |
||||
|
if (!t) |
||||
|
return command_fail(cmd, LIGHTNINGD, |
||||
|
"Missing bolt12 %.*s", |
||||
|
json_tok_full_len(result), |
||||
|
json_tok_full(buf, result)); |
||||
|
|
||||
|
plugin_log(cmd->plugin, LOG_DBG, |
||||
|
"invoice_request: %.*s", |
||||
|
json_tok_full_len(t), |
||||
|
json_tok_full(buf, t)); |
||||
|
|
||||
|
sent->invreq = invrequest_decode(sent, |
||||
|
buf + t->start, |
||||
|
t->end - t->start, |
||||
|
plugin_feature_set(cmd->plugin), |
||||
|
chainparams, |
||||
|
&fail); |
||||
|
if (!sent->invreq) |
||||
|
return command_fail(cmd, LIGHTNINGD, |
||||
|
"Invalid invoice_request %.*s: %s", |
||||
|
json_tok_full_len(t), |
||||
|
json_tok_full(buf, t), |
||||
|
fail); |
||||
|
|
||||
|
rawinvreq = tal_arr(tmpctx, u8, 0); |
||||
|
towire_invoice_request(&rawinvreq, sent->invreq); |
||||
|
return send_message(cmd, sent, "invoice_request", rawinvreq); |
||||
|
} |
||||
|
|
||||
|
/* Fetches an invoice for this offer, and makes sure it corresponds. */ |
||||
|
static struct command_result *json_fetchinvoice(struct command *cmd, |
||||
|
const char *buffer, |
||||
|
const jsmntok_t *params) |
||||
|
{ |
||||
|
struct tlv_offer *offer; |
||||
|
struct amount_msat *msat; |
||||
|
const char *rec_label; |
||||
|
struct out_req *req; |
||||
|
struct tlv_invoice_request *invreq; |
||||
|
|
||||
|
invreq = tlv_invoice_request_new(cmd); |
||||
|
|
||||
|
if (!param(cmd, buffer, params, |
||||
|
p_req("offer", param_offer, &offer), |
||||
|
p_opt("msatoshi", param_msat, &msat), |
||||
|
p_opt("quantity", param_u64, &invreq->quantity), |
||||
|
p_opt("recurrence_counter", param_number, |
||||
|
&invreq->recurrence_counter), |
||||
|
p_opt("recurrence_start", param_number, |
||||
|
&invreq->recurrence_start), |
||||
|
p_opt("recurrence_label", param_string, &rec_label), |
||||
|
NULL)) |
||||
|
return command_param_failed(); |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - MUST set `offer_id` to the merkle root of the offer as described |
||||
|
* in [Signature Calculation](#signature-calculation). |
||||
|
*/ |
||||
|
invreq->offer_id = tal(invreq, struct sha256); |
||||
|
merkle_tlv(offer->fields, invreq->offer_id); |
||||
|
|
||||
|
/* Check if they are trying to send us money. */ |
||||
|
if (offer->send_invoice) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"Offer wants an invoice, not invoice_request"); |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - SHOULD not respond to an offer if the current time is after |
||||
|
* `absolute_expiry`. |
||||
|
*/ |
||||
|
if (offer->absolute_expiry |
||||
|
&& time_now().ts.tv_sec > *offer->absolute_expiry) |
||||
|
return command_fail(cmd, OFFER_EXPIRED, "Offer expired"); |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - if the offer did not specify `amount`: |
||||
|
* - MUST specify `amount`.`msat` in multiples of the minimum |
||||
|
* lightning-payable unit (e.g. milli-satoshis for bitcoin) for the |
||||
|
* first `chains` entry. |
||||
|
* - otherwise: |
||||
|
* - MUST NOT set `amount` |
||||
|
*/ |
||||
|
if (offer->amount) { |
||||
|
if (msat) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"msatoshi parameter unnecessary"); |
||||
|
} else { |
||||
|
if (!msat) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"msatoshi parameter required"); |
||||
|
invreq->amount = tal_dup(invreq, u64, |
||||
|
&msat->millisatoshis); /* Raw: tu64 */ |
||||
|
} |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - if the offer had a `quantity_min` or `quantity_max` field: |
||||
|
* - MUST set `quantity` |
||||
|
* - MUST set it within that (inclusive) range. |
||||
|
* - otherwise: |
||||
|
* - MUST NOT set `quantity` |
||||
|
*/ |
||||
|
if (offer->quantity_min || offer->quantity_max) { |
||||
|
if (!invreq->quantity) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"quantity parameter required"); |
||||
|
if (offer->quantity_min |
||||
|
&& *invreq->quantity < *offer->quantity_min) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"quantity must be >= %"PRIu64, |
||||
|
*offer->quantity_min); |
||||
|
if (offer->quantity_max |
||||
|
&& *invreq->quantity > *offer->quantity_max) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"quantity must be <= %"PRIu64, |
||||
|
*offer->quantity_max); |
||||
|
} else { |
||||
|
if (invreq->quantity) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"quantity parameter unnecessary"); |
||||
|
} |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - if the offer contained `recurrence`: |
||||
|
*/ |
||||
|
if (offer->recurrence) { |
||||
|
/* BOLT-offers #12:
|
||||
|
* - for the initial request: |
||||
|
*... |
||||
|
* - MUST set `recurrence_counter` `counter` to 0. |
||||
|
*/ |
||||
|
/* BOLT-offers #12:
|
||||
|
* - for any successive requests: |
||||
|
*... |
||||
|
* - MUST set `recurrence_counter` `counter` to one greater |
||||
|
* than the highest-paid invoice. |
||||
|
*/ |
||||
|
if (!invreq->recurrence_counter) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"needs recurrence_counter"); |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* - if the offer contained `recurrence_base` with |
||||
|
* `start_any_period` non-zero: |
||||
|
* - MUST include `recurrence_start` |
||||
|
*... |
||||
|
* - otherwise: |
||||
|
* - MUST NOT include `recurrence_start` |
||||
|
*/ |
||||
|
if (offer->recurrence_base |
||||
|
&& offer->recurrence_base->start_any_period) { |
||||
|
if (!invreq->recurrence_start) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"needs recurrence_start"); |
||||
|
} else { |
||||
|
if (invreq->recurrence_start) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"unnecessary recurrence_start"); |
||||
|
} |
||||
|
|
||||
|
/* recurrence_label uniquely identifies this series of
|
||||
|
* payments. */ |
||||
|
if (!rec_label) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"needs recurrence_label"); |
||||
|
|
||||
|
/* FIXME! */ |
||||
|
/* BOLT-offers #12:
|
||||
|
* - SHOULD NOT send an `invoice_request` for a period which has |
||||
|
* already passed. |
||||
|
*/ |
||||
|
/* If there's no recurrence_base, we need the initial payment
|
||||
|
* for this... */ |
||||
|
} else { |
||||
|
/* BOLT-offers #12:
|
||||
|
* - otherwise: |
||||
|
* - MUST NOT set `recurrence_counter`. |
||||
|
*... |
||||
|
* - MUST NOT set `recurrence_start` |
||||
|
*/ |
||||
|
if (invreq->recurrence_counter) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"unnecessary recurrence_counter"); |
||||
|
if (invreq->recurrence_start) |
||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, |
||||
|
"unnecessary recurrence_start"); |
||||
|
} |
||||
|
|
||||
|
/* BOLT-offers #12:
|
||||
|
* |
||||
|
* - if the chain for the invoice is not solely bitcoin: |
||||
|
* - MUST specify `chains` the offer is valid for. |
||||
|
* - otherwise: |
||||
|
* - the bitcoin chain is implied as the first and only entry. |
||||
|
*/ |
||||
|
if (!streq(chainparams->network_name, "bitcoin")) { |
||||
|
invreq->chains = tal_arr(invreq, struct bitcoin_blkid, 1); |
||||
|
invreq->chains[0] = chainparams->genesis_blockhash; |
||||
|
} |
||||
|
|
||||
|
invreq->features |
||||
|
= plugin_feature_set(cmd->plugin)->bits[BOLT11_FEATURE]; |
||||
|
|
||||
|
/* Make the invoice request (fills in payer_key and payer_info) */ |
||||
|
req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest", |
||||
|
&invreq_done, |
||||
|
&forward_error, |
||||
|
offer); |
||||
|
json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq)); |
||||
|
if (rec_label) |
||||
|
json_add_string(req->js, "recurrence_label", rec_label); |
||||
|
return send_outreq(cmd->plugin, req); |
||||
|
} |
||||
|
|
||||
|
static const struct plugin_command commands[] = { { |
||||
|
"fetchinvoice", |
||||
|
"payment", |
||||
|
"Request remote node for an invoice for this {offer}, with {amount}, {quanitity}, {recurrence_counter}, {recurrence_start} and {recurrence_label} iff required.", |
||||
|
NULL, |
||||
|
json_fetchinvoice, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
static void init(struct plugin *p, const char *buf UNUSED, |
||||
|
const jsmntok_t *config UNUSED) |
||||
|
{ |
||||
|
const char *field; |
||||
|
|
||||
|
field = rpc_delve(tmpctx, p, "getinfo", |
||||
|
take(json_out_obj(NULL, NULL, NULL)), ".id"); |
||||
|
if (!node_id_from_hexstr(field, strlen(field), &local_id)) |
||||
|
plugin_err(p, "getinfo didn't contain valid id: '%s'", field); |
||||
|
} |
||||
|
|
||||
|
int main(int argc, char *argv[]) |
||||
|
{ |
||||
|
setup_locale(); |
||||
|
plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, |
||||
|
commands, ARRAY_SIZE(commands), |
||||
|
/* No notifications */ |
||||
|
NULL, 0, |
||||
|
/* No hooks */ |
||||
|
NULL, 0, |
||||
|
/* No options */ |
||||
|
NULL); |
||||
|
} |
Loading…
Reference in new issue