2021-12-04 21:53:56 +10:30
# include "config.h"
2020-12-16 13:48:00 +10:30
# include <bitcoin/chainparams.h>
# include <ccan/array_size/array_size.h>
2021-10-01 06:49:36 +09:30
# include <ccan/cast/cast.h>
2020-12-16 13:48:00 +10:30
# include <ccan/json_out/json_out.h>
2020-12-16 13:48:20 +10:30
# include <ccan/mem/mem.h>
2020-12-16 13:48:42 +10:30
# include <ccan/str/hex/hex.h>
2020-12-16 13:48:00 +10:30
# include <ccan/tal/str/str.h>
# include <common/blindedpath.h>
# include <common/bolt12_merkle.h>
# include <common/dijkstra.h>
# include <common/gossmap.h>
2023-12-13 16:04:32 +10:30
# include <common/gossmods_listpeerchannels.h>
2022-07-04 13:19:38 +09:30
# include <common/json_param.h>
2020-12-16 13:48:00 +10:30
# include <common/json_stream.h>
# include <common/memleak.h>
2020-12-16 13:48:20 +10:30
# include <common/overflows.h>
2020-12-16 13:48:00 +10:30
# include <common/route.h>
# include <errno.h>
2024-05-13 18:10:48 +09:30
# include <plugins/establish_onion_path.h>
2020-12-16 13:48:00 +10:30
# include <plugins/libplugin.h>
2020-12-16 13:48:20 +10:30
# include <secp256k1_schnorrsig.h>
2021-09-16 14:30:42 +09:30
# include <sodium.h>
2020-12-16 13:48:00 +10:30
static struct gossmap * global_gossmap ;
2021-09-21 14:53:45 +09:30
static struct pubkey local_id ;
2021-07-01 13:58:57 +09:30
static bool disable_connect = false ;
2020-12-16 13:48:20 +10:30
static LIST_HEAD ( sent_list ) ;
2020-12-16 13:48:00 +10:30
struct sent {
2020-12-16 13:48:20 +10:30
/* We're in sent_invreqs, awaiting reply. */
struct list_node list ;
2022-11-09 12:00:10 +10:30
/* The secret used by reply */
struct secret * reply_secret ;
2020-12-16 13:48:20 +10:30
/* The command which sent us. */
struct command * cmd ;
2021-01-08 05:08:47 +10:30
/* The offer we are trying to get an invoice/payment for. */
2020-12-16 13:48:00 +10:30
struct tlv_offer * offer ;
2021-09-21 14:53:45 +09:30
/* Path to use (including self) */
2024-05-13 18:10:48 +09:30
const struct pubkey * path ;
2021-01-08 05:08:47 +10:30
2024-05-09 14:12:38 +09:30
/* When creating blinded return path, use scid not pubkey for intro node. */
struct short_channel_id_dir * dev_path_use_scidd ;
2024-05-14 14:01:26 +09:30
/* Force reply path, for testing. */
struct pubkey * dev_reply_path ;
2021-01-08 05:08:47 +10:30
/* The invreq we sent, OR the invoice we sent */
2020-12-16 13:48:00 +10:30
struct tlv_invoice_request * invreq ;
2021-01-08 05:08:47 +10:30
struct tlv_invoice * inv ;
struct preimage inv_preimage ;
struct json_escape * inv_label ;
2021-01-08 05:09:47 +10:30
/* How long to wait for response before giving up. */
2021-01-08 05:10:47 +10:30
u32 wait_timeout ;
2020-12-16 13:48:00 +10:30
} ;
2022-11-09 12:00:10 +10:30
static struct sent * find_sent_by_secret ( const struct secret * pathsecret )
2021-10-01 06:49:36 +09:30
{
struct sent * i ;
list_for_each ( & sent_list , i , list ) {
2022-11-09 12:00:10 +10:30
if ( i - > reply_secret & & secret_eq_consttime ( i - > reply_secret , pathsecret ) )
2021-10-01 06:49:36 +09:30
return i ;
}
return NULL ;
}
2020-12-16 13:48:20 +10:30
/* Hack to suppress warnings when we finish a different command */
static void discard_result ( struct command_result * ret )
{
}
2021-01-08 05:08:47 +10:30
/* Returns NULL if it wasn't an error. */
static struct command_result * handle_error ( struct command * cmd ,
struct sent * sent ,
const char * buf ,
const jsmntok_t * om )
{
const u8 * data ;
size_t dlen ;
struct tlv_invoice_error * err ;
struct json_out * details ;
const jsmntok_t * errtok ;
errtok = json_get_member ( buf , om , " invoice_error " ) ;
if ( ! errtok )
return NULL ;
data = json_tok_bin_from_hex ( cmd , buf , errtok ) ;
dlen = tal_bytelen ( data ) ;
details = json_out_new ( cmd ) ;
plugin_log ( cmd - > plugin , LOG_DBG , " errtok = %.*s " ,
json_tok_full_len ( errtok ) ,
json_tok_full ( buf , errtok ) ) ;
json_out_start ( details , NULL , ' { ' ) ;
2022-03-23 10:01:14 +10:30
err = fromwire_tlv_invoice_error ( cmd , & data , & dlen ) ;
if ( ! err ) {
2021-01-08 05:08:47 +10:30
plugin_log ( cmd - > plugin , LOG_DBG ,
" Invalid invoice_error %.*s " ,
json_tok_full_len ( errtok ) ,
json_tok_full ( buf , errtok ) ) ;
json_out_addstr ( details , " invoice_error_hex " ,
tal_strndup ( tmpctx ,
buf + errtok - > start ,
errtok - > end - errtok - > start ) ) ;
} else {
char * failstr ;
/* FIXME: with a bit more generate-wire.py support,
* we could have fieldnames and even types . */
if ( err - > erroneous_field )
json_out_add ( details , " erroneous_field " , false ,
" % " PRIu64 , * err - > erroneous_field ) ;
if ( err - > suggested_value )
json_out_addstr ( details , " suggested_value " ,
tal_hex ( tmpctx ,
err - > suggested_value ) ) ;
/* If they don't include this, it'll be empty */
failstr = tal_strndup ( tmpctx ,
err - > error ,
tal_bytelen ( err - > error ) ) ;
json_out_addstr ( details , " error " , failstr ) ;
}
json_out_end ( details , ' } ' ) ;
discard_result ( command_done_err ( sent - > cmd ,
OFFER_BAD_INVREQ_REPLY ,
" Remote node sent failure message " ,
details ) ) ;
return command_hook_success ( cmd ) ;
}
2022-11-09 13:02:00 +10:30
/* BOLT-offers #12:
* - if the invoice is a response to an ` invoice_request ` :
* - MUST reject the invoice if all fields less than type 160 do not
* exactly match the ` invoice_request ` .
*/
static bool invoice_matches_request ( struct command * cmd ,
const u8 * invbin ,
const struct tlv_invoice_request * invreq )
{
size_t len1 , len2 ;
u8 * wire ;
/* We linearize then strip signature. This is dumb! */
wire = tal_arr ( tmpctx , u8 , 0 ) ;
towire_tlv_invoice_request ( & wire , invreq ) ;
len1 = tlv_span ( wire , 0 , 159 , NULL ) ;
len2 = tlv_span ( invbin , 0 , 159 , NULL ) ;
return memeq ( wire , len1 , invbin , len2 ) ;
}
2021-01-08 05:08:47 +10:30
static struct command_result * handle_invreq_response ( struct command * cmd ,
struct sent * sent ,
const char * buf ,
const jsmntok_t * om )
2020-12-16 13:48:20 +10:30
{
2022-11-09 13:02:00 +10:30
const u8 * invbin , * cursor ;
2021-01-08 05:08:47 +10:30
const jsmntok_t * invtok ;
2020-12-16 13:48:20 +10:30
size_t len ;
struct tlv_invoice * inv ;
struct sha256 merkle , sighash ;
struct json_stream * out ;
const char * badfield ;
u64 * expected_amount ;
2020-12-16 13:48:42 +10:30
invtok = json_get_member ( buf , om , " invoice " ) ;
if ( ! invtok ) {
plugin_log ( cmd - > plugin , LOG_UNUSUAL ,
" Neither invoice nor invoice_request_failed in reply %.*s " ,
json_tok_full_len ( om ) ,
json_tok_full ( buf , om ) ) ;
discard_result ( command_fail ( sent - > cmd ,
OFFER_BAD_INVREQ_REPLY ,
" Neither invoice nor invoice_request_failed in reply %.*s " ,
json_tok_full_len ( om ) ,
json_tok_full ( buf , om ) ) ) ;
return command_hook_success ( cmd ) ;
}
2020-12-16 13:48:20 +10:30
invbin = json_tok_bin_from_hex ( cmd , buf , invtok ) ;
2022-11-09 13:02:00 +10:30
cursor = invbin ;
2020-12-16 13:48:20 +10:30
len = tal_bytelen ( invbin ) ;
2022-11-09 13:02:00 +10:30
inv = fromwire_tlv_invoice ( cmd , & cursor , & len ) ;
2022-03-23 10:01:14 +10:30
if ( ! inv ) {
2020-12-16 13:48:20 +10:30
badfield = " invoice " ;
goto badinv ;
}
2021-08-19 14:23:04 +09:30
/* Raw send? Just fwd reply. */
2023-09-21 15:06:27 +09:30
if ( plugin_developer_mode ( cmd - > plugin ) & & ! sent - > offer ) {
2021-08-19 14:23:04 +09:30
out = jsonrpc_stream_success ( sent - > cmd ) ;
json_add_string ( out , " invoice " , invoice_encode ( tmpctx , inv ) ) ;
discard_result ( command_finished ( sent - > cmd , out ) ) ;
return command_hook_success ( cmd ) ;
}
2020-12-16 13:48:20 +10:30
/* BOLT-offers #12:
2022-11-09 13:02:00 +10:30
* - if the invoice is a response to an ` invoice_request ` :
* - MUST reject the invoice if all fields less than type 160 do not
* exactly match the ` invoice_request ` .
2020-12-16 13:48:20 +10:30
*/
2022-11-09 13:02:00 +10:30
if ( ! invoice_matches_request ( cmd , invbin , sent - > invreq ) ) {
badfield = " invoice_request match " ;
goto badinv ;
}
/* BOLT-offers #12:
* - if ` offer_node_id ` is present ( invoice_request for an offer ) :
* - MUST reject the invoice if ` invoice_node_id ` is not equal to ` offer_node_id ` .
*/
if ( ! inv - > invoice_node_id | | ! pubkey_eq ( inv - > offer_node_id , inv - > invoice_node_id ) ) {
badfield = " invoice_node_id " ;
2020-12-16 13:48:20 +10:30
goto badinv ;
}
/* BOLT-offers #12:
* - MUST reject the invoice if ` signature ` is not a valid signature
2022-11-09 13:02:00 +10:30
* using ` invoice_node_id ` as described in [ Signature Calculation ]
2020-12-16 13:48:20 +10:30
*/
merkle_tlv ( inv - > fields , & merkle ) ;
sighash_from_merkle ( " invoice " , " signature " , & merkle , & sighash ) ;
if ( ! inv - > signature
2022-11-09 13:02:00 +10:30
| | ! check_schnorr_sig ( & sighash , & inv - > invoice_node_id - > pubkey , inv - > signature ) ) {
2020-12-16 13:48:20 +10:30
badfield = " signature " ;
goto badinv ;
}
/* BOLT-offers #12:
2022-11-09 13:02:00 +10:30
* A reader of an invoice :
* - MUST reject the invoice if ` invoice_amount ` is not present .
2020-12-16 13:48:20 +10:30
*/
2022-11-09 13:02:00 +10:30
if ( ! inv - > invoice_amount ) {
badfield = " invoice_amount " ;
2020-12-16 13:48:20 +10:30
goto badinv ;
}
2021-07-21 15:07:39 +09:30
/* Get the amount we expected: firstly, if that's what we sent,
* secondly , if specified in the invoice . */
2022-11-09 13:02:01 +10:30
if ( inv - > invreq_amount ) {
expected_amount = tal_dup ( tmpctx , u64 , inv - > invreq_amount ) ;
} else if ( inv - > offer_amount & & ! inv - > offer_currency ) {
2020-12-16 13:48:20 +10:30
expected_amount = tal ( tmpctx , u64 ) ;
2022-11-09 13:02:01 +10:30
* expected_amount = * inv - > offer_amount ;
if ( inv - > invreq_quantity ) {
2020-12-16 13:48:20 +10:30
/* We should never have sent this! */
if ( mul_overflows_u64 ( * expected_amount ,
2022-11-09 13:02:01 +10:30
* inv - > invreq_quantity ) ) {
2020-12-16 13:48:20 +10:30
badfield = " quantity overflow " ;
goto badinv ;
}
2022-11-09 13:02:01 +10:30
* expected_amount * = * inv - > invreq_quantity ;
2020-12-16 13:48:20 +10:30
}
} else
expected_amount = NULL ;
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2020-12-16 13:48:42 +10:30
* - if the offer contained ` recurrence ` :
* - MUST reject the invoice if ` recurrence_basetime ` is not set .
*/
2022-11-09 13:02:01 +10:30
if ( inv - > invreq_recurrence_counter & & ! inv - > invoice_recurrence_basetime ) {
2020-12-16 13:48:42 +10:30
badfield = " recurrence_basetime " ;
goto badinv ;
}
2020-12-16 13:48:20 +10:30
out = jsonrpc_stream_success ( sent - > cmd ) ;
json_add_string ( out , " invoice " , invoice_encode ( tmpctx , inv ) ) ;
json_object_start ( out , " changes " ) ;
/* BOLT-offers #12:
2022-11-09 13:02:01 +10:30
* - SHOULD confirm authorization if ` invoice_amount ` . ` msat ` is not within
* the amount range authorized .
2020-12-16 13:48:20 +10:30
*/
/* We always tell them this unless it's trivial to calc and
* exactly as expected . */
2022-11-09 13:02:00 +10:30
if ( ! expected_amount | | * inv - > invoice_amount ! = * expected_amount ) {
2023-03-14 15:51:50 +10:30
json_add_amount_msat ( out , " amount_msat " ,
amount_msat ( * inv - > invoice_amount ) ) ;
2022-06-20 19:52:09 +09:30
}
2020-12-16 13:48:20 +10:30
json_object_end ( out ) ;
2020-12-16 13:48:42 +10:30
/* We tell them about next period at this point, if any. */
2022-11-09 13:02:01 +10:30
if ( inv - > offer_recurrence ) {
2020-12-16 13:48:42 +10:30
u64 next_counter , next_period_idx ;
u64 paywindow_start , paywindow_end ;
2022-11-09 13:02:01 +10:30
next_counter = * inv - > invreq_recurrence_counter + 1 ;
if ( inv - > invreq_recurrence_start )
next_period_idx = * inv - > invreq_recurrence_start
2020-12-16 13:48:42 +10:30
+ next_counter ;
else
next_period_idx = next_counter ;
/* If this was the last, don't tell them about a next! */
2022-11-09 13:02:01 +10:30
if ( ! inv - > offer_recurrence_limit
| | next_period_idx < = * inv - > offer_recurrence_limit ) {
2020-12-16 13:48:42 +10:30
json_object_start ( out , " next_period " ) ;
json_add_u64 ( out , " counter " , next_counter ) ;
json_add_u64 ( out , " starttime " ,
2022-11-09 13:02:00 +10:30
offer_period_start ( * inv - > invoice_recurrence_basetime ,
2020-12-16 13:48:42 +10:30
next_period_idx ,
2022-11-09 13:02:01 +10:30
inv - > offer_recurrence ) ) ;
2020-12-16 13:48:42 +10:30
json_add_u64 ( out , " endtime " ,
2022-11-09 13:02:00 +10:30
offer_period_start ( * inv - > invoice_recurrence_basetime ,
2020-12-16 13:48:42 +10:30
next_period_idx + 1 ,
2022-11-09 13:02:01 +10:30
inv - > offer_recurrence ) - 1 ) ;
2020-12-16 13:48:42 +10:30
2022-11-09 13:02:01 +10:30
offer_period_paywindow ( inv - > offer_recurrence ,
inv - > offer_recurrence_paywindow ,
inv - > offer_recurrence_base ,
2022-11-09 13:02:00 +10:30
* inv - > invoice_recurrence_basetime ,
2020-12-16 13:48:42 +10:30
next_period_idx ,
& paywindow_start , & paywindow_end ) ;
json_add_u64 ( out , " paywindow_start " , paywindow_start ) ;
json_add_u64 ( out , " paywindow_end " , paywindow_end ) ;
json_object_end ( out ) ;
}
}
2020-12-16 13:48:20 +10:30
discard_result ( command_finished ( sent - > cmd , out ) ) ;
return command_hook_success ( cmd ) ;
badinv :
plugin_log ( cmd - > plugin , LOG_DBG , " Failed invoice due to %s " , badfield ) ;
discard_result ( command_fail ( sent - > cmd ,
OFFER_BAD_INVREQ_REPLY ,
" Incorrect %s field in %.*s " ,
badfield ,
json_tok_full_len ( invtok ) ,
json_tok_full ( buf , invtok ) ) ) ;
return command_hook_success ( cmd ) ;
}
2021-10-01 06:49:36 +09:30
static struct command_result * recv_modern_onion_message ( struct command * cmd ,
const char * buf ,
const jsmntok_t * params )
{
2022-11-09 12:00:10 +10:30
const jsmntok_t * om , * secrettok ;
2021-10-01 06:49:36 +09:30
struct sent * sent ;
2022-11-09 12:00:10 +10:30
struct secret pathsecret ;
2021-10-01 06:49:36 +09:30
struct command_result * err ;
om = json_get_member ( buf , params , " onion_message " ) ;
2022-11-09 12:00:10 +10:30
secrettok = json_get_member ( buf , om , " pathsecret " ) ;
json_to_secret ( buf , secrettok , & pathsecret ) ;
sent = find_sent_by_secret ( & pathsecret ) ;
2021-10-01 06:49:36 +09:30
if ( ! sent ) {
plugin_log ( cmd - > plugin , LOG_DBG ,
" No match for modern onion %.*s " ,
json_tok_full_len ( om ) ,
json_tok_full ( buf , om ) ) ;
return command_hook_success ( cmd ) ;
}
plugin_log ( cmd - > plugin , LOG_DBG , " Received modern onion message: %.*s " ,
json_tok_full_len ( params ) ,
json_tok_full ( buf , params ) ) ;
err = handle_error ( cmd , sent , buf , om ) ;
if ( err )
return err ;
if ( sent - > invreq )
return handle_invreq_response ( cmd , sent , buf , om ) ;
return command_hook_success ( cmd ) ;
}
2020-12-16 13:48:20 +10:30
static void destroy_sent ( struct sent * sent )
{
list_del ( & sent - > list ) ;
}
2021-01-08 05:10:47 +10:30
/* We've received neither a reply nor a payment; return failure. */
static void timeout_sent_invreq ( struct sent * sent )
{
/* This will free sent! */
discard_result ( command_fail ( sent - > cmd , OFFER_TIMEOUT ,
" Timeout waiting for response " ) ) ;
}
2020-12-16 13:48:00 +10:30
static struct command_result * sendonionmsg_done ( struct command * cmd ,
const char * buf UNUSED ,
const jsmntok_t * result UNUSED ,
struct sent * sent )
{
2021-01-08 05:10:47 +10:30
tal_steal ( cmd , plugin_timer ( cmd - > plugin ,
time_from_sec ( sent - > wait_timeout ) ,
timeout_sent_invreq , sent ) ) ;
2020-12-16 13:48:20 +10:30
sent - > cmd = cmd ;
list_add_tail ( & sent_list , & sent - > list ) ;
tal_add_destructor ( sent , destroy_sent ) ;
2020-12-16 13:48:00 +10:30
return command_still_pending ( cmd ) ;
}
static void init_gossmap ( struct plugin * plugin )
{
2021-08-20 13:42:27 +09:30
size_t num_cupdates_rejected ;
2020-12-16 13:48:00 +10:30
global_gossmap
= notleak_with_children ( gossmap_load ( NULL ,
2021-08-20 13:42:27 +09:30
GOSSIP_STORE_FILENAME ,
& num_cupdates_rejected ) ) ;
2020-12-16 13:48:00 +10:30
if ( ! global_gossmap )
plugin_err ( plugin , " Could not load gossmap %s: %s " ,
GOSSIP_STORE_FILENAME , strerror ( errno ) ) ;
2021-08-20 13:42:27 +09:30
if ( num_cupdates_rejected )
plugin_log ( plugin , LOG_DBG ,
" gossmap ignored %zu channel updates " ,
num_cupdates_rejected ) ;
2020-12-16 13:48:00 +10:30
}
static struct gossmap * get_gossmap ( struct plugin * plugin )
{
if ( ! global_gossmap )
init_gossmap ( plugin ) ;
else
2021-08-20 13:42:27 +09:30
gossmap_refresh ( global_gossmap , NULL ) ;
2020-12-16 13:48:00 +10:30
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 ;
* 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 ) ) ;
return NULL ;
}
2021-11-30 13:36:04 +10:30
/* Marshal arguments for sending onion messages */
2021-10-01 06:49:36 +09:30
struct sending {
struct sent * sent ;
2022-10-17 11:14:39 +10:30
struct tlv_onionmsg_tlv * payload ;
2021-10-01 06:49:36 +09:30
struct command_result * ( * done ) ( struct command * cmd ,
const char * buf UNUSED ,
const jsmntok_t * result UNUSED ,
struct sent * sent ) ;
} ;
2021-11-30 13:36:05 +10:30
static struct command_result *
send_modern_message ( struct command * cmd ,
2022-10-17 11:14:39 +10:30
struct blinded_path * reply_path ,
2021-11-30 13:36:05 +10:30
struct sending * sending )
{
struct sent * sent = sending - > sent ;
struct privkey blinding_iter ;
struct pubkey fwd_blinding , * node_alias ;
size_t nhops = tal_count ( sent - > path ) ;
2022-10-17 11:14:39 +10:30
struct tlv_onionmsg_tlv * * payloads ;
2021-11-30 13:36:05 +10:30
struct out_req * req ;
2022-11-09 12:00:10 +10:30
struct tlv_encrypted_data_tlv * tlv ;
2021-11-30 13:36:05 +10:30
/* Now create enctlvs for *forward* path. */
randombytes_buf ( & blinding_iter , sizeof ( blinding_iter ) ) ;
if ( ! pubkey_from_privkey ( & blinding_iter , & fwd_blinding ) )
return command_fail ( cmd , LIGHTNINGD ,
" Could not convert blinding %s to pubkey! " ,
2024-03-20 11:17:52 +10:30
fmt_privkey ( tmpctx , & blinding_iter ) ) ;
2021-11-30 13:36:05 +10:30
/* We overallocate: this node (0) doesn't have payload or alias */
2022-10-17 11:14:39 +10:30
payloads = tal_arr ( cmd , struct tlv_onionmsg_tlv * , nhops ) ;
2021-11-30 13:36:05 +10:30
node_alias = tal_arr ( cmd , struct pubkey , nhops ) ;
for ( size_t i = 1 ; i < nhops - 1 ; i + + ) {
2022-10-17 11:14:39 +10:30
payloads [ i ] = tlv_onionmsg_tlv_new ( payloads ) ;
2022-11-09 12:00:10 +10:30
tlv = tlv_encrypted_data_tlv_new ( tmpctx ) ;
2024-05-13 18:10:48 +09:30
tlv - > next_node_id = cast_const ( struct pubkey * ,
& sent - > path [ i + 1 ] ) ;
2022-11-09 12:00:10 +10:30
/* FIXME: Pad? */
payloads [ i ] - > encrypted_recipient_data
= encrypt_tlv_encrypted_data ( payloads [ i ] ,
& blinding_iter ,
& sent - > path [ i ] ,
tlv ,
& blinding_iter ,
& node_alias [ i ] ) ;
2021-11-30 13:36:05 +10:30
}
/* Final payload contains the actual data. */
2022-09-29 13:19:03 +09:30
payloads [ nhops - 1 ] = sending - > payload ;
2021-11-30 13:36:05 +10:30
/* We don't include enctlv in final, but it gives us final alias */
2022-11-09 12:00:10 +10:30
tlv = tlv_encrypted_data_tlv_new ( tmpctx ) ;
if ( ! encrypt_tlv_encrypted_data ( tmpctx ,
& blinding_iter ,
& sent - > path [ nhops - 1 ] ,
tlv ,
NULL ,
& node_alias [ nhops - 1 ] ) ) {
2021-11-30 13:36:05 +10:30
/* Should not happen! */
return command_fail ( cmd , LIGHTNINGD ,
" Could create final enctlv " ) ;
}
payloads [ nhops - 1 ] - > reply_path = reply_path ;
req = jsonrpc_request_start ( cmd - > plugin , cmd , " sendonionmessage " ,
2022-09-29 13:19:03 +09:30
sending - > done ,
2021-11-30 13:36:05 +10:30
forward_error ,
2022-09-29 13:19:03 +09:30
sending - > sent ) ;
2021-11-30 13:36:05 +10:30
json_add_pubkey ( req - > js , " first_id " , & sent - > path [ 1 ] ) ;
json_add_pubkey ( req - > js , " blinding " , & fwd_blinding ) ;
json_array_start ( req - > js , " hops " ) ;
for ( size_t i = 1 ; i < nhops ; i + + ) {
2022-11-09 12:00:10 +10:30
u8 * tlvbin ;
2021-11-30 13:36:05 +10:30
json_object_start ( req - > js , NULL ) ;
json_add_pubkey ( req - > js , " id " , & node_alias [ i ] ) ;
2022-11-09 12:00:10 +10:30
tlvbin = tal_arr ( tmpctx , u8 , 0 ) ;
towire_tlv_onionmsg_tlv ( & tlvbin , payloads [ i ] ) ;
json_add_hex_talarr ( req - > js , " tlv " , tlvbin ) ;
2021-11-30 13:36:05 +10:30
json_object_end ( req - > js ) ;
}
json_array_end ( req - > js ) ;
return send_outreq ( cmd - > plugin , req ) ;
}
2024-05-09 13:05:14 +09:30
static struct blinded_path * blinded_path ( const tal_t * ctx ,
struct command * cmd ,
const struct pubkey * ids ,
2024-05-09 14:12:38 +09:30
const struct short_channel_id_dir * first_scidd ,
2024-05-09 13:05:14 +09:30
const struct secret * pathsecret )
2024-05-09 13:05:13 +09:30
{
struct privkey first_blinding , blinding_iter ;
struct blinded_path * path ;
size_t nhops ;
struct tlv_encrypted_data_tlv * tlv ;
2024-05-09 13:05:14 +09:30
path = tal ( ctx , struct blinded_path ) ;
2024-05-09 13:05:13 +09:30
nhops = tal_count ( ids ) ;
2024-05-09 13:05:14 +09:30
assert ( nhops > 0 ) ;
2024-05-09 14:12:38 +09:30
if ( first_scidd )
sciddir_or_pubkey_from_scidd ( & path - > first_node_id , first_scidd ) ;
else
sciddir_or_pubkey_from_pubkey ( & path - > first_node_id , & ids [ 0 ] ) ;
2024-05-09 13:05:14 +09:30
assert ( pubkey_eq ( & ids [ nhops - 1 ] , & local_id ) ) ;
2024-05-09 13:05:13 +09:30
randombytes_buf ( & first_blinding , sizeof ( first_blinding ) ) ;
if ( ! pubkey_from_privkey ( & first_blinding , & path - > blinding ) )
2024-05-09 13:05:14 +09:30
plugin_err ( cmd - > plugin , " Could not convert blinding to pubkey! " ) ;
2024-05-09 13:05:13 +09:30
/* We convert ids into aliases as we go. */
path - > path = tal_arr ( cmd , struct onionmsg_hop * , nhops ) ;
blinding_iter = first_blinding ;
for ( size_t i = 0 ; i < nhops - 1 ; i + + ) {
path - > path [ i ] = tal ( path - > path , struct onionmsg_hop ) ;
tlv = tlv_encrypted_data_tlv_new ( tmpctx ) ;
2024-05-09 13:05:14 +09:30
tlv - > next_node_id = cast_const ( struct pubkey * , & ids [ i + 1 ] ) ;
2024-05-09 13:05:13 +09:30
/* FIXME: Pad? */
path - > path [ i ] - > encrypted_recipient_data
= encrypt_tlv_encrypted_data ( path - > path [ i ] ,
& blinding_iter ,
& ids [ i ] ,
tlv ,
& blinding_iter ,
& path - > path [ i ] - > blinded_node_id ) ;
}
/* FIXME: Add padding! */
path - > path [ nhops - 1 ] = tal ( path - > path , struct onionmsg_hop ) ;
tlv = tlv_encrypted_data_tlv_new ( tmpctx ) ;
tlv - > path_id = ( u8 * ) tal_dup ( tlv , struct secret , pathsecret ) ;
path - > path [ nhops - 1 ] - > encrypted_recipient_data
= encrypt_tlv_encrypted_data ( path - > path [ nhops - 1 ] ,
& blinding_iter ,
& ids [ nhops - 1 ] ,
tlv ,
NULL ,
& path - > path [ nhops - 1 ] - > blinded_node_id ) ;
2024-05-09 13:05:14 +09:30
return path ;
2021-10-01 06:49:36 +09:30
}
static struct command_result * make_reply_path ( struct command * cmd ,
struct sending * sending )
{
size_t nhops = tal_count ( sending - > sent - > path ) ;
2024-05-09 13:05:14 +09:30
struct blinded_path * rpath ;
struct pubkey * ids ;
2021-10-01 06:49:36 +09:30
/* FIXME: Maybe we should allow this? */
if ( tal_count ( sending - > sent - > path ) = = 1 )
return command_fail ( cmd , PAY_ROUTE_NOT_FOUND ,
" Refusing to talk to ourselves " ) ;
2022-11-09 12:00:10 +10:30
/* Create transient secret so we can validate reply! */
sending - > sent - > reply_secret = tal ( sending - > sent , struct secret ) ;
randombytes_buf ( sending - > sent - > reply_secret , sizeof ( struct secret ) ) ;
2024-05-14 14:01:26 +09:30
if ( sending - > sent - > dev_reply_path ) {
ids = sending - > sent - > dev_reply_path ;
} else {
/* FIXME: Could create an independent reply path, not just
* reverse existing . */
ids = tal_arr ( tmpctx , struct pubkey , nhops - 1 ) ;
for ( int i = nhops - 2 ; i > = 0 ; i - - )
ids [ nhops - 2 - i ] = sending - > sent - > path [ i ] ;
}
2024-05-09 13:05:14 +09:30
2024-05-09 14:12:38 +09:30
rpath = blinded_path ( cmd , cmd , ids , sending - > sent - > dev_path_use_scidd ,
sending - > sent - > reply_secret ) ;
2024-05-09 13:05:14 +09:30
return send_modern_message ( cmd , rpath , sending ) ;
2021-10-01 06:49:36 +09:30
}
static struct command_result * send_message ( struct command * cmd ,
struct sent * sent ,
2022-10-17 11:14:39 +10:30
struct tlv_onionmsg_tlv * payload STEALS ,
2021-10-01 06:49:36 +09:30
struct command_result * ( * done )
( struct command * cmd ,
const char * buf UNUSED ,
const jsmntok_t * result UNUSED ,
struct sent * sent ) )
{
struct sending * sending = tal ( cmd , struct sending ) ;
sending - > sent = sent ;
2022-09-29 13:19:03 +09:30
sending - > payload = tal_steal ( sending , payload ) ;
2021-10-01 06:49:36 +09:30
sending - > done = done ;
return make_reply_path ( cmd , sending ) ;
}
2022-11-09 13:02:01 +10:30
/* We've received neither a reply nor a payment; return failure. */
static void timeout_sent_inv ( struct sent * sent )
{
struct json_out * details = json_out_new ( sent ) ;
json_out_start ( details , NULL , ' { ' ) ;
json_out_addstr ( details , " invstring " , invoice_encode ( tmpctx , sent - > inv ) ) ;
json_out_end ( details , ' } ' ) ;
/* This will free sent! */
discard_result ( command_done_err ( sent - > cmd , OFFER_TIMEOUT ,
" Failed: timeout waiting for response " ,
details ) ) ;
}
static struct command_result * prepare_inv_timeout ( struct command * cmd ,
const char * buf UNUSED ,
const jsmntok_t * result UNUSED ,
struct sent * sent )
{
tal_steal ( cmd , plugin_timer ( cmd - > plugin ,
time_from_sec ( sent - > wait_timeout ) ,
timeout_sent_inv , sent ) ) ;
return sendonionmsg_done ( cmd , buf , result , sent ) ;
}
2024-05-13 18:10:48 +09:30
static struct command_result * fetchinvoice_path_done ( struct command * cmd ,
const struct pubkey * path ,
struct sent * sent )
2021-07-01 13:58:57 +09:30
{
2022-10-17 11:14:39 +10:30
struct tlv_onionmsg_tlv * payload = tlv_onionmsg_tlv_new ( sent ) ;
2022-09-29 13:19:03 +09:30
payload - > invoice_request = tal_arr ( payload , u8 , 0 ) ;
towire_tlv_invoice_request ( & payload - > invoice_request , sent - > invreq ) ;
2024-05-13 18:10:48 +09:30
sent - > path = tal_steal ( sent , path ) ;
2022-03-23 10:01:14 +10:30
2022-09-29 13:19:03 +09:30
return send_message ( cmd , sent , payload , sendonionmsg_done ) ;
2021-07-01 13:58:57 +09:30
}
2024-05-13 18:10:48 +09:30
static struct command_result * fetchinvoice_path_fail ( struct command * cmd ,
const char * why ,
struct sent * sent )
2023-12-13 16:04:32 +10:30
{
2024-05-13 18:10:48 +09:30
return command_fail ( cmd , OFFER_ROUTE_NOT_FOUND ,
" Failed: could not route, could not connect: %s " ,
why ) ;
2023-12-13 16:04:32 +10:30
}
2020-12-16 13:48:00 +10:30
static struct command_result * invreq_done ( struct command * cmd ,
const char * buf ,
const jsmntok_t * result ,
2021-01-08 05:10:47 +10:30
struct sent * sent )
2020-12-16 13:48:00 +10:30
{
const jsmntok_t * t ;
char * fail ;
/* 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 ) ) ;
2021-01-08 05:08:47 +10:30
sent - > inv = NULL ;
2020-12-16 13:48:00 +10:30
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 ) ;
2021-01-08 05:11:47 +10:30
/* Now that's given us the previous base, check this is an OK time
* to request an invoice . */
2022-11-09 13:02:00 +10:30
if ( sent - > invreq - > invreq_recurrence_counter ) {
2021-01-08 05:11:47 +10:30
u64 * base ;
const jsmntok_t * pbtok ;
2022-11-09 13:02:00 +10:30
u64 period_idx = * sent - > invreq - > invreq_recurrence_counter ;
2021-01-08 05:11:47 +10:30
2022-11-09 13:02:00 +10:30
if ( sent - > invreq - > invreq_recurrence_start )
period_idx + = * sent - > invreq - > invreq_recurrence_start ;
2021-01-08 05:11:47 +10:30
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2021-01-08 05:11:47 +10:30
* - if the offer contained ` recurrence_limit ` :
* - MUST NOT send an ` invoice_request ` for a period greater
* than ` max_period `
*/
2022-11-09 13:02:01 +10:30
if ( sent - > invreq - > offer_recurrence_limit
& & period_idx > * sent - > invreq - > offer_recurrence_limit )
2021-01-08 05:11:47 +10:30
return command_fail ( cmd , LIGHTNINGD ,
" Can't send invreq for period % "
PRIu64 " (limit %u) " ,
period_idx ,
2022-11-09 13:02:01 +10:30
* sent - > invreq - > offer_recurrence_limit ) ;
2021-01-08 05:11:47 +10:30
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2021-01-08 05:11:47 +10:30
* - SHOULD NOT send an ` invoice_request ` for a period which has
* already passed .
*/
/* If there's no recurrence_base, we need a previous payment
* for this : fortunately createinvoicerequest does that
* lookup . */
pbtok = json_get_member ( buf , result , " previous_basetime " ) ;
if ( pbtok ) {
base = tal ( tmpctx , u64 ) ;
json_to_u64 ( buf , pbtok , base ) ;
2022-11-09 13:02:01 +10:30
} else if ( sent - > invreq - > offer_recurrence_base )
base = & sent - > invreq - > offer_recurrence_base - > basetime ;
2021-01-08 05:11:47 +10:30
else {
/* happens with *recurrence_base == 0 */
2022-11-09 13:02:00 +10:30
assert ( * sent - > invreq - > invreq_recurrence_counter = = 0 ) ;
2021-01-08 05:11:47 +10:30
base = NULL ;
}
if ( base ) {
u64 period_start , period_end , now = time_now ( ) . ts . tv_sec ;
2022-11-09 13:02:01 +10:30
offer_period_paywindow ( sent - > invreq - > offer_recurrence ,
sent - > invreq - > offer_recurrence_paywindow ,
sent - > invreq - > offer_recurrence_base ,
2021-01-08 05:11:47 +10:30
* base , period_idx ,
& period_start , & period_end ) ;
if ( now < period_start )
return command_fail ( cmd , LIGHTNINGD ,
" Too early: can't send until time % "
PRIu64 " (in % " PRIu64 " secs) " ,
period_start ,
period_start - now ) ;
if ( now > period_end )
return command_fail ( cmd , LIGHTNINGD ,
" Too late: expired time % "
PRIu64 " (% " PRIu64 " secs ago) " ,
period_end ,
now - period_end ) ;
}
}
2024-05-13 18:10:48 +09:30
return establish_onion_path ( cmd , get_gossmap ( cmd - > plugin ) , & local_id ,
sent - > invreq - > offer_node_id ,
2024-07-09 18:12:44 +09:30
disable_connect ,
2024-05-13 18:10:48 +09:30
fetchinvoice_path_done ,
fetchinvoice_path_fail ,
2023-12-13 16:04:32 +10:30
sent ) ;
2020-12-16 13:48:00 +10:30
}
2024-05-09 14:12:38 +09:30
static struct command_result * param_dev_scidd ( struct command * cmd , const char * name ,
const char * buffer , const jsmntok_t * tok ,
struct short_channel_id_dir * * scidd )
{
if ( ! plugin_developer_mode ( cmd - > plugin ) )
return command_fail_badparam ( cmd , name , buffer , tok ,
" not available outside --developer mode " ) ;
* scidd = tal ( cmd , struct short_channel_id_dir ) ;
if ( short_channel_id_dir_from_str ( buffer + tok - > start , tok - > end - tok - > start , * scidd ) )
return NULL ;
return command_fail_badparam ( cmd , name , buffer , tok ,
" should be a short_channel_id of form NxNxN/dir " ) ;
}
2024-05-14 14:01:26 +09:30
static struct command_result * param_dev_reply_path ( struct command * cmd , const char * name ,
const char * buffer , const jsmntok_t * tok ,
struct pubkey * * path )
{
size_t i ;
const jsmntok_t * t ;
if ( ! plugin_developer_mode ( cmd - > plugin ) )
return command_fail_badparam ( cmd , name , buffer , tok ,
" not available outside --developer mode " ) ;
if ( tok - > type ! = JSMN_ARRAY )
return command_fail_badparam ( cmd , name , buffer , tok , " Must be array " ) ;
* path = tal_arr ( cmd , struct pubkey , tok - > size ) ;
json_for_each_arr ( i , t , tok ) {
if ( ! json_to_pubkey ( buffer , t , & ( * path ) [ i ] ) )
return command_fail_badparam ( cmd , name , buffer , t , " invalid pubkey " ) ;
}
return NULL ;
}
2020-12-16 13:48:00 +10:30
/* 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 amount_msat * msat ;
2021-07-02 09:41:35 +09:30
const char * rec_label , * payer_note ;
2020-12-16 13:48:00 +10:30
struct out_req * req ;
struct tlv_invoice_request * invreq ;
2021-01-08 05:10:47 +10:30
struct sent * sent = tal ( cmd , struct sent ) ;
u32 * timeout ;
2022-11-09 13:02:00 +10:30
u64 * quantity ;
u32 * recurrence_counter , * recurrence_start ;
2020-12-16 13:48:00 +10:30
if ( ! param ( cmd , buffer , params ,
2021-01-08 05:10:47 +10:30
p_req ( " offer " , param_offer , & sent - > offer ) ,
2024-01-25 10:58:48 +10:30
p_opt ( " amount_msat " , param_msat , & msat ) ,
2022-11-09 13:02:00 +10:30
p_opt ( " quantity " , param_u64 , & quantity ) ,
p_opt ( " recurrence_counter " , param_number , & recurrence_counter ) ,
p_opt ( " recurrence_start " , param_number , & recurrence_start ) ,
2020-12-16 13:48:00 +10:30
p_opt ( " recurrence_label " , param_string , & rec_label ) ,
2021-01-08 05:10:47 +10:30
p_opt_def ( " timeout " , param_number , & timeout , 60 ) ,
2021-07-02 09:41:35 +09:30
p_opt ( " payer_note " , param_string , & payer_note ) ,
2024-05-09 14:12:38 +09:30
p_opt ( " dev_path_use_scidd " , param_dev_scidd , & sent - > dev_path_use_scidd ) ,
2024-05-14 14:01:26 +09:30
p_opt ( " dev_reply_path " , param_dev_reply_path , & sent - > dev_reply_path ) ,
2020-12-16 13:48:00 +10:30
NULL ) )
return command_param_failed ( ) ;
2021-01-08 05:10:47 +10:30
sent - > wait_timeout = * timeout ;
2020-12-16 13:48:00 +10:30
/* BOLT-offers #12:
* - SHOULD not respond to an offer if the current time is after
2022-11-09 13:02:01 +10:30
* ` offer_absolute_expiry ` .
2020-12-16 13:48:00 +10:30
*/
2022-11-09 13:02:00 +10:30
if ( sent - > offer - > offer_absolute_expiry
& & time_now ( ) . ts . tv_sec > * sent - > offer - > offer_absolute_expiry )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , OFFER_EXPIRED , " Offer expired " ) ;
2022-11-09 13:02:00 +10:30
/* BOLT-offers #12:
* The writer :
* - if it is responding to an offer :
* - MUST copy all fields from the offer ( including unknown fields ) .
*/
invreq = invoice_request_for_offer ( sent , sent - > offer ) ;
invreq - > invreq_recurrence_counter = tal_steal ( invreq , recurrence_counter ) ;
invreq - > invreq_recurrence_start = tal_steal ( invreq , recurrence_start ) ;
2023-03-13 12:53:14 +01:00
invreq - > invreq_quantity = tal_steal ( invreq , quantity ) ;
2022-11-09 13:02:00 +10:30
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2022-11-09 13:02:00 +10:30
* - if ` offer_amount ` is not present :
* - MUST specify ` invreq_amount ` .
* - otherwise :
* - MAY omit ` invreq_amount ` .
* - if it sets ` invreq_amount ` :
* - MUST specify ` invreq_amount ` . ` msat ` as greater or equal to
* amount expected by ` offer_amount ` ( and , if present ,
* ` offer_currency ` and ` invreq_quantity ` ) .
2020-12-16 13:48:00 +10:30
*/
2022-11-09 13:02:01 +10:30
if ( invreq - > offer_amount ) {
2021-01-09 14:55:46 +10:30
/* FIXME: Check after quantity? */
if ( msat ) {
2022-11-09 13:02:00 +10:30
invreq - > invreq_amount = tal_dup ( invreq , u64 ,
& msat - > millisatoshis ) ; /* Raw: tu64 */
2021-01-09 14:55:46 +10:30
}
2020-12-16 13:48:00 +10:30
} else {
if ( ! msat )
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" msatoshi parameter required " ) ;
2022-11-09 13:02:00 +10:30
invreq - > invreq_amount = tal_dup ( invreq , u64 ,
& msat - > millisatoshis ) ; /* Raw: tu64 */
2020-12-16 13:48:00 +10:30
}
/* BOLT-offers #12:
2022-11-09 13:02:00 +10:30
* - if ` offer_quantity_max ` is present :
2022-11-09 13:02:01 +10:30
* - MUST set ` invreq_quantity ` to greater than zero .
2022-11-09 13:02:00 +10:30
* - if ` offer_quantity_max ` is non - zero :
* - MUST set ` invreq_quantity ` less than or equal to
* ` offer_quantity_max ` .
2020-12-16 13:48:00 +10:30
*/
2022-11-09 13:02:01 +10:30
if ( invreq - > offer_quantity_max ) {
2022-11-09 13:02:00 +10:30
if ( ! invreq - > invreq_quantity )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" quantity parameter required " ) ;
2022-11-09 13:02:01 +10:30
if ( * invreq - > invreq_quantity = = 0 )
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" quantity parameter must be non-zero " ) ;
2022-11-09 13:02:01 +10:30
if ( * invreq - > offer_quantity_max
& & * invreq - > invreq_quantity > * invreq - > offer_quantity_max )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" quantity must be <= % " PRIu64 ,
2022-11-09 13:02:01 +10:30
* invreq - > offer_quantity_max ) ;
2020-12-16 13:48:00 +10:30
} else {
2022-11-09 13:02:00 +10:30
if ( invreq - > invreq_quantity )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" quantity parameter unnecessary " ) ;
}
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2020-12-16 13:48:00 +10:30
* - if the offer contained ` recurrence ` :
*/
2022-11-09 13:02:01 +10:30
if ( invreq - > offer_recurrence ) {
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2020-12-16 13:48:00 +10:30
* - for the initial request :
* . . .
* - MUST set ` recurrence_counter ` ` counter ` to 0.
*/
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2020-12-16 13:48:00 +10:30
* - for any successive requests :
* . . .
* - MUST set ` recurrence_counter ` ` counter ` to one greater
* than the highest - paid invoice .
*/
2022-11-09 13:02:00 +10:30
if ( ! invreq - > invreq_recurrence_counter )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" needs recurrence_counter " ) ;
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2020-12-16 13:48:00 +10:30
* - if the offer contained ` recurrence_base ` with
* ` start_any_period ` non - zero :
* - MUST include ` recurrence_start `
* . . .
* - otherwise :
* - MUST NOT include ` recurrence_start `
*/
2022-11-09 13:02:01 +10:30
if ( invreq - > offer_recurrence_base
& & invreq - > offer_recurrence_base - > start_any_period ) {
2022-11-09 13:02:00 +10:30
if ( ! invreq - > invreq_recurrence_start )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" needs recurrence_start " ) ;
} else {
2022-11-09 13:02:00 +10:30
if ( invreq - > invreq_recurrence_start )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" unnecessary recurrence_start " ) ;
}
/* recurrence_label uniquely identifies this series of
2023-09-21 15:06:27 +09:30
* payments */
if ( ! rec_label )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" needs recurrence_label " ) ;
} else {
2021-11-30 13:36:05 +10:30
/* BOLT-offers-recurrence #12:
2020-12-16 13:48:00 +10:30
* - otherwise :
* - MUST NOT set ` recurrence_counter ` .
* - MUST NOT set ` recurrence_start `
*/
2022-11-09 13:02:00 +10:30
if ( invreq - > invreq_recurrence_counter )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" unnecessary recurrence_counter " ) ;
2022-11-09 13:02:00 +10:30
if ( invreq - > invreq_recurrence_start )
2020-12-16 13:48:00 +10:30
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" unnecessary recurrence_start " ) ;
}
/* BOLT-offers #12:
*
2022-11-09 13:02:00 +10:30
* - if ` offer_chains ` is set :
* - MUST set ` invreq_chain ` to one of ` offer_chains ` unless that
* chain is bitcoin , in which case it MAY omit ` invreq_chain ` .
2020-12-16 13:48:00 +10:30
* - otherwise :
2022-11-09 13:02:00 +10:30
* - if it sets ` invreq_chain ` it MUST set it to bitcoin .
2020-12-16 13:48:00 +10:30
*/
2022-11-09 13:02:00 +10:30
/* We already checked that we're compatible chain, in param_offer */
2020-12-16 13:48:00 +10:30
if ( ! streq ( chainparams - > network_name , " bitcoin " ) ) {
2022-11-09 13:02:00 +10:30
invreq - > invreq_chain = tal_dup ( invreq , struct bitcoin_blkid ,
& chainparams - > genesis_blockhash ) ;
2020-12-16 13:48:00 +10:30
}
2022-11-09 13:02:00 +10:30
/* BOLT-offers #12:
* - if it supports bolt12 invoice request features :
* - MUST set ` invreq_features ` . ` features ` to the bitmap of features .
*/
invreq - > invreq_features
= plugin_feature_set ( cmd - > plugin ) - > bits [ BOLT12_OFFER_FEATURE ] ;
2020-12-16 13:48:00 +10:30
2022-11-09 13:02:00 +10:30
/* invreq->invreq_payer_note is not a nul-terminated string! */
2021-07-02 09:41:35 +09:30
if ( payer_note )
2022-11-09 13:02:00 +10:30
invreq - > invreq_payer_note = tal_dup_arr ( invreq , utf8 ,
payer_note ,
strlen ( payer_note ) ,
0 ) ;
2021-07-02 09:41:35 +09:30
2020-12-16 13:48:00 +10:30
/* Make the invoice request (fills in payer_key and payer_info) */
req = jsonrpc_request_start ( cmd - > plugin , cmd , " createinvoicerequest " ,
& invreq_done ,
& forward_error ,
2021-01-08 05:10:47 +10:30
sent ) ;
2022-11-09 13:02:01 +10:30
/* We don't want this is the database: that's only for ones we publish */
2020-12-16 13:48:00 +10:30
json_add_string ( req - > js , " bolt12 " , invrequest_encode ( tmpctx , invreq ) ) ;
2022-11-09 13:02:01 +10:30
json_add_bool ( req - > js , " savetodb " , false ) ;
2020-12-16 13:48:00 +10:30
if ( rec_label )
json_add_string ( req - > js , " recurrence_label " , rec_label ) ;
return send_outreq ( cmd - > plugin , req ) ;
}
2021-01-08 05:09:47 +10:30
/* FIXME: Using a hook here is not ideal: technically it doesn't mean
* it ' s actually hit the db ! But using waitinvoice is also suboptimal
* because we don ' t have libplugin infra to cancel a pending req ( and I
* want to rewrite our wait * API anyway ) */
static struct command_result * invoice_payment ( struct command * cmd ,
const char * buf ,
const jsmntok_t * params )
{
struct sent * i ;
const jsmntok_t * ptok , * preimagetok , * msattok ;
struct preimage preimage ;
struct amount_msat msat ;
ptok = json_get_member ( buf , params , " payment " ) ;
preimagetok = json_get_member ( buf , ptok , " preimage " ) ;
msattok = json_get_member ( buf , ptok , " msat " ) ;
if ( ! preimagetok | | ! msattok )
plugin_err ( cmd - > plugin ,
" Invalid invoice_payment %.*s " ,
json_tok_full_len ( params ) ,
json_tok_full ( buf , params ) ) ;
hex_decode ( buf + preimagetok - > start ,
preimagetok - > end - preimagetok - > start ,
& preimage , sizeof ( preimage ) ) ;
json_to_msat ( buf , msattok , & msat ) ;
list_for_each ( & sent_list , i , list ) {
2021-01-08 05:20:47 +10:30
struct out_req * req ;
2021-01-08 05:09:47 +10:30
if ( ! i - > inv )
continue ;
if ( ! preimage_eq ( & preimage , & i - > inv_preimage ) )
continue ;
2021-01-08 05:20:47 +10:30
/* It was paid! Success. Return as per waitinvoice. */
req = jsonrpc_request_start ( cmd - > plugin , i - > cmd , " waitinvoice " ,
& forward_result ,
& forward_error ,
i ) ;
json_add_escaped_string ( req - > js , " label " , i - > inv_label ) ;
discard_result ( send_outreq ( cmd - > plugin , req ) ) ;
2021-01-08 05:09:47 +10:30
break ;
}
return command_hook_success ( cmd ) ;
}
2024-05-13 18:10:48 +09:30
static struct command_result * sendinvoice_path_done ( struct command * cmd ,
const struct pubkey * path ,
struct sent * sent )
2022-11-09 13:02:01 +10:30
{
struct tlv_onionmsg_tlv * payload = tlv_onionmsg_tlv_new ( sent ) ;
payload - > invoice = tal_arr ( payload , u8 , 0 ) ;
towire_tlv_invoice ( & payload - > invoice , sent - > inv ) ;
2024-05-13 18:10:48 +09:30
sent - > path = tal_steal ( sent , path ) ;
2022-11-09 13:02:01 +10:30
return send_message ( cmd , sent , payload , prepare_inv_timeout ) ;
}
2024-05-13 18:10:48 +09:30
static struct command_result * sendinvoice_path_fail ( struct command * cmd ,
const char * why ,
struct sent * sent )
2023-12-13 16:04:32 +10:30
{
2024-05-13 18:10:48 +09:30
return command_fail ( cmd , OFFER_ROUTE_NOT_FOUND ,
" Failed: could not route, could not connect: %s " ,
why ) ;
2023-12-13 16:04:32 +10:30
}
2022-11-09 13:02:01 +10:30
static struct command_result * createinvoice_done ( struct command * cmd ,
const char * buf ,
const jsmntok_t * result ,
struct sent * sent )
{
const jsmntok_t * invtok = json_get_member ( buf , result , " bolt12 " ) ;
char * fail ;
/* Replace invoice with signed one */
tal_free ( sent - > inv ) ;
sent - > inv = invoice_decode ( sent ,
buf + invtok - > start ,
invtok - > end - invtok - > start ,
plugin_feature_set ( cmd - > plugin ) ,
chainparams ,
& fail ) ;
if ( ! sent - > inv ) {
plugin_log ( cmd - > plugin , LOG_BROKEN ,
" Bad createinvoice %.*s: %s " ,
json_tok_full_len ( invtok ) ,
json_tok_full ( buf , invtok ) ,
fail ) ;
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" Bad createinvoice response %s " , fail ) ;
}
/* BOLT-offers #12:
* - if it sends an invoice in response :
* - MUST use ` offer_paths ` if present , otherwise MUST use
* ` invreq_payer_id ` as the node id to send to .
*/
/* FIXME! */
if ( sent - > invreq - > offer_paths ) {
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" FIXME: support blinded paths! " ) ;
}
2024-05-13 18:10:48 +09:30
return establish_onion_path ( cmd , get_gossmap ( cmd - > plugin ) , & local_id ,
sent - > invreq - > invreq_payer_id ,
2024-07-09 18:12:44 +09:30
disable_connect ,
2024-05-13 18:10:48 +09:30
sendinvoice_path_done ,
sendinvoice_path_fail ,
2023-12-13 16:04:32 +10:30
sent ) ;
2022-11-09 13:02:01 +10:30
}
static struct command_result * sign_invoice ( struct command * cmd ,
struct sent * sent )
{
struct out_req * req ;
/* Get invoice signature and put in db so we can receive payment */
req = jsonrpc_request_start ( cmd - > plugin , cmd , " createinvoice " ,
& createinvoice_done ,
& forward_error ,
sent ) ;
json_add_string ( req - > js , " invstring " , invoice_encode ( tmpctx , sent - > inv ) ) ;
json_add_preimage ( req - > js , " preimage " , & sent - > inv_preimage ) ;
json_add_escaped_string ( req - > js , " label " , sent - > inv_label ) ;
return send_outreq ( cmd - > plugin , req ) ;
}
2021-08-19 14:23:04 +09:30
static struct command_result * param_invreq ( struct command * cmd ,
const char * name ,
const char * buffer ,
const jsmntok_t * tok ,
struct tlv_invoice_request * * invreq )
{
char * fail ;
2022-11-09 13:02:01 +10:30
int badf ;
u8 * wire ;
struct sha256 merkle , sighash ;
/* BOLT-offers #12:
* - if ` invreq_chain ` is not present :
* - MUST fail the request if bitcoin is not a supported chain .
* - otherwise :
* - MUST fail the request if ` invreq_chain ` . ` chain ` is not a
* supported chain .
*/
* invreq = invrequest_decode ( cmd ,
buffer + tok - > start , tok - > end - tok - > start ,
plugin_feature_set ( cmd - > plugin ) ,
chainparams ,
& fail ) ;
if ( ! * invreq )
return command_fail_badparam ( cmd , name , buffer , tok ,
tal_fmt ( cmd ,
" Unparsable invoice_request: %s " ,
fail ) ) ;
/* BOLT-offers #12:
* The reader :
* - MUST fail the request if ` invreq_payer_id ` or ` invreq_metadata `
* are not present .
* - MUST fail the request if any non - signature TLV fields greater or
* equal to 160.
* - if ` invreq_features ` contains unknown _odd_ bits that are
* non - zero :
* - MUST ignore the bit .
* - if ` invreq_features ` contains unknown _even_ bits that are
* non - zero :
* - MUST fail the request .
*/
if ( ! ( * invreq ) - > invreq_payer_id )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Missing invreq_payer_id " ) ;
if ( ! ( * invreq ) - > invreq_metadata )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Missing invreq_metadata " ) ;
wire = tal_arr ( tmpctx , u8 , 0 ) ;
towire_tlv_invoice_request ( & wire , * invreq ) ;
if ( tlv_span ( wire , 160 , 239 , NULL ) ! = 0
| | tlv_span ( wire , 1001 , UINT64_MAX , NULL ) ! = 0 ) {
return command_fail_badparam ( cmd , name , buffer , tok ,
" Invalid high-numbered fields " ) ;
}
badf = features_unsupported ( plugin_feature_set ( cmd - > plugin ) ,
( * invreq ) - > invreq_features ,
BOLT12_INVREQ_FEATURE ) ;
if ( badf ! = - 1 ) {
return command_fail_badparam ( cmd , name , buffer , tok ,
tal_fmt ( tmpctx ,
" unknown feature %i " ,
badf ) ) ;
}
/* BOLT-offers #12:
* - MUST fail the request if ` signature ` is not correct as detailed in [ Signature
* Calculation ] ( # signature - calculation ) using the ` invreq_payer_id ` .
*/
merkle_tlv ( ( * invreq ) - > fields , & merkle ) ;
sighash_from_merkle ( " invoice_request " , " signature " , & merkle , & sighash ) ;
if ( ! ( * invreq ) - > signature )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Missing signature " ) ;
if ( ! check_schnorr_sig ( & sighash ,
& ( * invreq ) - > invreq_payer_id - > pubkey ,
( * invreq ) - > signature ) )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Invalid signature " ) ;
/* Plugin handles these automatically, you shouldn't send one
* manually . */
if ( ( * invreq ) - > offer_node_id ) {
return command_fail_badparam ( cmd , name , buffer , tok ,
" This is based on an offer? " ) ;
}
/* BOLT-offers #12:
* - otherwise ( no ` offer_node_id ` , not a response to our offer ) :
* - MUST fail the request if any of the following are present :
* - ` offer_chains ` , ` offer_features ` or ` offer_quantity_max ` .
* - MUST fail the request if ` invreq_amount ` is not present .
*/
if ( ( * invreq ) - > offer_chains )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Unexpected offer_chains " ) ;
if ( ( * invreq ) - > offer_features )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Unexpected offer_features " ) ;
if ( ( * invreq ) - > offer_quantity_max )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Unexpected offer_quantity_max " ) ;
if ( ! ( * invreq ) - > invreq_amount )
return command_fail_badparam ( cmd , name , buffer , tok ,
" Missing invreq_amount " ) ;
/* BOLT-offers #12:
* - otherwise ( no ` offer_node_id ` , not a response to our offer ) :
* . . .
* - MAY use ` offer_amount ` ( or ` offer_currency ` ) for informational display to user .
*/
if ( ( * invreq ) - > offer_amount & & ( * invreq ) - > offer_currency ) {
plugin_notify_message ( cmd , LOG_INFORM ,
" invoice_request offers %.*s% " PRIu64 " as %s " ,
( int ) tal_bytelen ( ( * invreq ) - > offer_currency ) ,
( * invreq ) - > offer_currency ,
* ( * invreq ) - > offer_amount ,
fmt_amount_msat ( tmpctx ,
amount_msat ( * ( * invreq ) - > invreq_amount ) ) ) ;
}
return NULL ;
}
static struct command_result * json_sendinvoice ( struct command * cmd ,
const char * buffer ,
const jsmntok_t * params )
{
struct amount_msat * msat ;
u32 * timeout ;
struct sent * sent = tal ( cmd , struct sent ) ;
sent - > offer = NULL ;
sent - > cmd = cmd ;
/* FIXME: Support recurring invoice_requests? */
if ( ! param ( cmd , buffer , params ,
p_req ( " invreq " , param_invreq , & sent - > invreq ) ,
p_req ( " label " , param_label , & sent - > inv_label ) ,
p_opt ( " amount_msat " , param_msat , & msat ) ,
p_opt_def ( " timeout " , param_number , & timeout , 90 ) ,
NULL ) )
return command_param_failed ( ) ;
2024-05-09 14:12:38 +09:30
sent - > dev_path_use_scidd = NULL ;
2024-05-14 14:01:26 +09:30
sent - > dev_reply_path = NULL ;
2024-05-09 14:12:38 +09:30
2022-11-09 13:02:01 +10:30
/* BOLT-offers #12:
2022-11-09 13:02:01 +10:30
* - if the invoice is in response to an ` invoice_request ` :
* - MUST copy all non - signature fields from the ` invoice_request `
* ( including unknown fields ) .
2022-11-09 13:02:01 +10:30
*/
sent - > inv = invoice_for_invreq ( sent , sent - > invreq ) ;
/* This is how long we'll wait for a reply for. */
sent - > wait_timeout = * timeout ;
/* BOLT-offers #12:
* - if ` invreq_amount ` is present :
* - MUST set ` invoice_amount ` to ` invreq_amount `
* - otherwise :
* - MUST set ` invoice_amount ` to the * expected amount * .
*/
if ( ! msat )
sent - > inv - > invoice_amount = tal_dup ( sent - > inv , u64 ,
sent - > invreq - > invreq_amount ) ;
else
sent - > inv - > invoice_amount = tal_dup ( sent - > inv , u64 ,
& msat - > millisatoshis ) ; /* Raw: tlv */
/* BOLT-offers #12:
* - MUST set ` invoice_created_at ` to the number of seconds since Midnight 1
2023-01-12 14:26:38 +10:30
* January 1970 , UTC when the invoice was created .
2022-11-09 13:02:01 +10:30
* - MUST set ` invoice_amount ` to the minimum amount it will accept , in units of
* the minimal lightning - payable unit ( e . g . milli - satoshis for bitcoin ) for
* ` invreq_chain ` .
*/
sent - > inv - > invoice_created_at = tal ( sent - > inv , u64 ) ;
* sent - > inv - > invoice_created_at = time_now ( ) . ts . tv_sec ;
/* FIXME: Support blinded paths, in which case use fake nodeid */
/* BOLT-offers #12:
* - MUST set ` invoice_payment_hash ` to the SHA256 hash of the
* ` payment_preimage ` that will be given in return for payment .
*/
randombytes_buf ( & sent - > inv_preimage , sizeof ( sent - > inv_preimage ) ) ;
sent - > inv - > invoice_payment_hash = tal ( sent - > inv , struct sha256 ) ;
sha256 ( sent - > inv - > invoice_payment_hash ,
& sent - > inv_preimage , sizeof ( sent - > inv_preimage ) ) ;
/* BOLT-offers #12:
* - if ` offer_node_id ` is present :
* - MUST set ` invoice_node_id ` to ` offer_node_id ` .
* - otherwise :
* - MUST set ` invoice_node_id ` to a valid public key .
*/
/* FIXME: Use transitory id! */
sent - > inv - > invoice_node_id = tal ( sent - > inv , struct pubkey ) ;
sent - > inv - > invoice_node_id - > pubkey = local_id . pubkey ;
/* BOLT-offers #12:
* - if the expiry for accepting payment is not 7200 seconds
* after ` invoice_created_at ` :
* - MUST set ` invoice_relative_expiry ` . ` seconds_from_creation `
* to the number of seconds after ` invoice_created_at ` that
* payment of this invoice should not be attempted .
*/
if ( sent - > wait_timeout ! = 7200 ) {
sent - > inv - > invoice_relative_expiry = tal ( sent - > inv , u32 ) ;
* sent - > inv - > invoice_relative_expiry = sent - > wait_timeout ;
}
/* FIXME: recurrence? */
if ( sent - > inv - > offer_recurrence )
return command_fail ( cmd , JSONRPC2_INVALID_PARAMS ,
" FIXME: handle recurring invreq? " ) ;
sent - > inv - > invoice_features
= plugin_feature_set ( cmd - > plugin ) - > bits [ BOLT12_INVOICE_FEATURE ] ;
return sign_invoice ( cmd , sent ) ;
}
/* This version doesn't do sanity checks! */
static struct command_result * param_raw_invreq ( struct command * cmd ,
const char * name ,
const char * buffer ,
const jsmntok_t * tok ,
struct tlv_invoice_request * * invreq )
{
char * fail ;
2021-08-19 14:23:04 +09:30
* invreq = invrequest_decode ( cmd , buffer + tok - > start , tok - > end - tok - > start ,
plugin_feature_set ( cmd - > plugin ) , chainparams ,
& fail ) ;
if ( ! * invreq )
return command_fail_badparam ( cmd , name , buffer , tok ,
tal_fmt ( cmd ,
" Unparsable invreq: %s " ,
fail ) ) ;
return NULL ;
}
2023-09-21 15:06:27 +09:30
static struct command_result * json_dev_rawrequest ( struct command * cmd ,
const char * buffer ,
const jsmntok_t * params )
2021-08-19 14:23:04 +09:30
{
struct sent * sent = tal ( cmd , struct sent ) ;
u32 * timeout ;
2022-10-17 11:07:05 +10:30
struct pubkey * node_id ;
2021-08-19 14:23:04 +09:30
if ( ! param ( cmd , buffer , params ,
2022-11-09 13:02:01 +10:30
p_req ( " invreq " , param_raw_invreq , & sent - > invreq ) ,
2022-10-17 11:07:05 +10:30
p_req ( " nodeid " , param_pubkey , & node_id ) ,
2021-08-19 14:23:04 +09:30
p_opt_def ( " timeout " , param_number , & timeout , 60 ) ,
NULL ) )
return command_param_failed ( ) ;
/* This is how long we'll wait for a reply for. */
sent - > wait_timeout = * timeout ;
sent - > cmd = cmd ;
sent - > offer = NULL ;
2024-05-09 14:12:38 +09:30
sent - > dev_path_use_scidd = NULL ;
2024-05-14 14:01:26 +09:30
sent - > dev_reply_path = NULL ;
2021-08-19 14:23:04 +09:30
2024-05-13 18:10:48 +09:30
return establish_onion_path ( cmd , get_gossmap ( cmd - > plugin ) , & local_id ,
node_id ,
2024-07-09 18:12:44 +09:30
disable_connect ,
2024-05-13 18:10:48 +09:30
fetchinvoice_path_done ,
fetchinvoice_path_fail ,
2023-12-13 16:04:32 +10:30
sent ) ;
2021-08-19 14:23:04 +09:30
}
2021-01-08 05:08:47 +10:30
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 ,
} ,
2022-11-09 13:02:01 +10:30
{
" sendinvoice " ,
" payment " ,
" Request remote node for to pay this {invreq}, with {label}, optional {amount_msat}, and {timeout} (default 90 seconds). " ,
NULL ,
json_sendinvoice ,
} ,
2021-08-19 14:23:04 +09:30
{
" dev-rawrequest " ,
" util " ,
" Send {invreq} to {nodeid}, wait {timeout} (60 seconds by default) " ,
NULL ,
2023-09-21 15:06:27 +09:30
json_dev_rawrequest ,
. dev_only = true ,
2021-08-19 14:23:04 +09:30
} ,
2020-12-16 13:48:00 +10:30
} ;
2021-01-13 13:30:24 +10:30
static const char * init ( struct plugin * p , const char * buf UNUSED ,
const jsmntok_t * config UNUSED )
2020-12-16 13:48:00 +10:30
{
2021-01-13 19:28:38 +10:30
bool exp_offers ;
2021-01-06 16:11:20 +10:30
rpc_scan ( p , " getinfo " ,
take ( json_out_obj ( NULL , NULL , NULL ) ) ,
2021-09-21 14:53:45 +09:30
" {id:%} " , JSON_SCAN ( json_to_pubkey , & local_id ) ) ;
2021-01-13 13:30:24 +10:30
2021-01-13 19:28:38 +10:30
rpc_scan ( p , " listconfigs " ,
take ( json_out_obj ( NULL , " config " , " experimental-offers " ) ) ,
2023-06-02 12:06:04 +09:30
" {configs:{experimental-offers:{set:%}}} " ,
2021-01-13 19:28:38 +10:30
JSON_SCAN ( json_to_bool , & exp_offers ) ) ;
if ( ! exp_offers )
return " offers not enabled in config " ;
2021-01-13 13:30:24 +10:30
return NULL ;
2020-12-16 13:48:00 +10:30
}
2020-12-16 13:48:20 +10:30
static const struct plugin_hook hooks [ ] = {
2021-10-01 06:49:36 +09:30
{
2022-11-09 12:00:10 +10:30
" onion_message_recv_secret " ,
2021-10-01 06:49:36 +09:30
recv_modern_onion_message
2020-12-16 13:48:20 +10:30
} ,
2021-01-08 05:09:47 +10:30
{
" invoice_payment " ,
invoice_payment ,
} ,
2020-12-16 13:48:20 +10:30
} ;
2020-12-16 13:48:00 +10:30
int main ( int argc , char * argv [ ] )
{
setup_locale ( ) ;
plugin_main ( argv , init , PLUGIN_RESTARTABLE , true , NULL ,
commands , ARRAY_SIZE ( commands ) ,
/* No notifications */
NULL , 0 ,
2020-12-16 13:48:20 +10:30
hooks , ARRAY_SIZE ( hooks ) ,
2021-04-28 17:28:27 +02:00
NULL , 0 ,
2021-07-01 13:58:57 +09:30
plugin_option ( " fetchinvoice-noconnect " , " flag " ,
" Don't try to connect directly to fetch an invoice. " ,
2024-05-14 12:54:01 +09:30
flag_option , flag_jsonfmt , & disable_connect ) ,
2020-12-16 13:48:00 +10:30
NULL ) ;
}