2020-12-16 04:13:23 +01:00
/* This plugin covers both sending and receiving offers */
2021-12-04 12:23:56 +01:00
# include "config.h"
2021-01-07 19:48:47 +01:00
# include <bitcoin/chainparams.h>
2020-12-16 04:13:23 +01:00
# include <ccan/array_size/array_size.h>
2021-09-30 23:19:37 +02:00
# include <ccan/cast/cast.h>
2022-07-16 15:18:27 +02:00
# include <ccan/rune/rune.h>
2021-01-07 19:48:47 +01:00
# include <ccan/tal/str/str.h>
# include <common/bech32.h>
# include <common/bolt11.h>
# include <common/bolt11_json.h>
# include <common/bolt12_merkle.h>
# include <common/iso4217.h>
2022-07-04 05:49:38 +02:00
# include <common/json_param.h>
2021-01-07 19:35:47 +01:00
# include <common/json_stream.h>
# include <plugins/offers.h>
2021-01-07 19:36:47 +01:00
# include <plugins/offers_inv_hook.h>
2020-12-16 04:13:32 +01:00
# include <plugins/offers_invreq_hook.h>
2020-12-16 04:13:28 +01:00
# include <plugins/offers_offer.h>
2021-10-08 00:54:42 +02:00
struct point32 id ;
2022-03-22 09:50:13 +01:00
u16 cltv_final ;
2021-08-02 01:56:12 +02:00
bool offers_enabled ;
2020-12-16 04:13:32 +01:00
2021-01-07 19:35:47 +01:00
static struct command_result * finished ( struct command * cmd ,
const char * buf ,
const jsmntok_t * result ,
void * unused )
{
return command_hook_success ( cmd ) ;
}
static struct command_result * sendonionmessage_error ( struct command * cmd ,
const char * buf ,
const jsmntok_t * err ,
void * unused )
{
2021-10-13 02:27:11 +02:00
/* This can happen if the peer goes offline or wasn't directly
* connected : " Unknown first peer " */
plugin_log ( cmd - > plugin , LOG_DBG ,
" sendonionmessage gave JSON error: %.*s " ,
2021-01-07 19:35:47 +01:00
json_tok_full_len ( err ) ,
json_tok_full ( buf , err ) ) ;
return command_hook_success ( cmd ) ;
}
2022-03-28 01:10:54 +02:00
/* FIXME: replyfield string interface is to accomodate obsolete API */
static struct command_result *
send_obs2_onion_reply ( struct command * cmd ,
struct tlv_obs2_onionmsg_payload_reply_path * reply_path ,
const char * replyfield ,
const u8 * replydata )
{
struct out_req * req ;
size_t nhops = tal_count ( reply_path - > path ) ;
req = jsonrpc_request_start ( cmd - > plugin , cmd , " sendobs2onionmessage " ,
finished , sendonionmessage_error , NULL ) ;
json_add_pubkey ( req - > js , " first_id " , & reply_path - > first_node_id ) ;
json_add_pubkey ( req - > js , " blinding " , & reply_path - > blinding ) ;
json_array_start ( req - > js , " hops " ) ;
for ( size_t i = 0 ; i < nhops ; i + + ) {
struct tlv_obs2_onionmsg_payload * omp ;
u8 * tlv ;
json_object_start ( req - > js , NULL ) ;
json_add_pubkey ( req - > js , " id " , & reply_path - > path [ i ] - > node_id ) ;
omp = tlv_obs2_onionmsg_payload_new ( tmpctx ) ;
omp - > enctlv = reply_path - > path [ i ] - > encrypted_recipient_data ;
/* Put payload in last hop. */
if ( i = = nhops - 1 ) {
if ( streq ( replyfield , " invoice " ) ) {
omp - > invoice = cast_const ( u8 * , replydata ) ;
} else {
assert ( streq ( replyfield , " invoice_error " ) ) ;
omp - > invoice_error = cast_const ( u8 * , replydata ) ;
}
}
tlv = tal_arr ( tmpctx , u8 , 0 ) ;
towire_tlv_obs2_onionmsg_payload ( & tlv , omp ) ;
json_add_hex_talarr ( req - > js , " tlv " , tlv ) ;
json_object_end ( req - > js ) ;
}
json_array_end ( req - > js ) ;
return send_outreq ( cmd - > plugin , req ) ;
}
2021-11-30 04:06:05 +01:00
struct command_result *
send_onion_reply ( struct command * cmd ,
struct tlv_onionmsg_payload_reply_path * reply_path ,
2022-03-28 01:10:54 +02:00
struct tlv_obs2_onionmsg_payload_reply_path * obs2_reply_path ,
const char * replyfield ,
const u8 * replydata )
2021-11-30 04:06:05 +01:00
{
struct out_req * req ;
size_t nhops ;
2022-03-28 01:10:54 +02:00
/* Exactly one must be set! */
assert ( ! reply_path ! = ! obs2_reply_path ) ;
if ( obs2_reply_path )
return send_obs2_onion_reply ( cmd , obs2_reply_path , replyfield , replydata ) ;
2021-11-30 04:06:05 +01:00
req = jsonrpc_request_start ( cmd - > plugin , cmd , " sendonionmessage " ,
finished , sendonionmessage_error , NULL ) ;
json_add_pubkey ( req - > js , " first_id " , & reply_path - > first_node_id ) ;
json_add_pubkey ( req - > js , " blinding " , & reply_path - > blinding ) ;
json_array_start ( req - > js , " hops " ) ;
nhops = tal_count ( reply_path - > path ) ;
for ( size_t i = 0 ; i < nhops ; i + + ) {
struct tlv_onionmsg_payload * omp ;
u8 * tlv ;
json_object_start ( req - > js , NULL ) ;
json_add_pubkey ( req - > js , " id " , & reply_path - > path [ i ] - > node_id ) ;
2022-03-28 01:10:54 +02:00
omp = tlv_onionmsg_payload_new ( tmpctx ) ;
2021-11-30 04:06:05 +01:00
omp - > encrypted_data_tlv = reply_path - > path [ i ] - > encrypted_recipient_data ;
2022-03-28 01:10:54 +02:00
/* Put payload in last hop. */
if ( i = = nhops - 1 ) {
if ( streq ( replyfield , " invoice " ) ) {
omp - > invoice = cast_const ( u8 * , replydata ) ;
} else {
assert ( streq ( replyfield , " invoice_error " ) ) ;
omp - > invoice_error = cast_const ( u8 * , replydata ) ;
}
}
2021-11-30 04:06:05 +01:00
tlv = tal_arr ( tmpctx , u8 , 0 ) ;
2022-03-23 00:31:14 +01:00
towire_tlv_onionmsg_payload ( & tlv , omp ) ;
2021-11-30 04:06:05 +01:00
json_add_hex_talarr ( req - > js , " tlv " , tlv ) ;
json_object_end ( req - > js ) ;
}
json_array_end ( req - > js ) ;
return send_outreq ( cmd - > plugin , req ) ;
2021-09-30 23:19:37 +02:00
}
static struct command_result * onion_message_modern_call ( struct command * cmd ,
const char * buf ,
const jsmntok_t * params )
{
const jsmntok_t * om , * replytok , * invreqtok , * invtok ;
2022-03-28 01:10:54 +02:00
struct tlv_obs2_onionmsg_payload_reply_path * obs2_reply_path = NULL ;
2021-11-30 04:06:05 +01:00
struct tlv_onionmsg_payload_reply_path * reply_path = NULL ;
2021-09-30 23:19:37 +02:00
if ( ! offers_enabled )
return command_hook_success ( cmd ) ;
om = json_get_member ( buf , params , " onion_message " ) ;
replytok = json_get_member ( buf , om , " reply_blindedpath " ) ;
if ( replytok ) {
2022-03-28 01:10:54 +02:00
bool obs2 ;
json_to_bool ( buf , json_get_member ( buf , om , " obs2 " ) , & obs2 ) ;
if ( obs2 ) {
obs2_reply_path = json_to_obs2_reply_path ( cmd , buf , replytok ) ;
if ( ! obs2_reply_path )
plugin_err ( cmd - > plugin , " Invalid obs2 reply path %.*s? " ,
json_tok_full_len ( replytok ) ,
json_tok_full ( buf , replytok ) ) ;
} else {
reply_path = json_to_reply_path ( cmd , buf , replytok ) ;
if ( ! reply_path )
plugin_err ( cmd - > plugin , " Invalid reply path %.*s? " ,
json_tok_full_len ( replytok ) ,
json_tok_full ( buf , replytok ) ) ;
}
2021-11-30 04:06:05 +01:00
}
2021-09-30 23:19:37 +02:00
invreqtok = json_get_member ( buf , om , " invoice_request " ) ;
if ( invreqtok ) {
2021-11-30 04:06:04 +01:00
const u8 * invreqbin = json_tok_bin_from_hex ( tmpctx , buf , invreqtok ) ;
2022-03-28 01:10:54 +02:00
if ( reply_path | | obs2_reply_path )
2021-11-30 04:06:04 +01:00
return handle_invoice_request ( cmd ,
invreqbin ,
2022-03-28 01:10:54 +02:00
reply_path ,
obs2_reply_path ) ;
2021-09-30 23:19:37 +02:00
else
plugin_log ( cmd - > plugin , LOG_DBG ,
" invoice_request without reply_path " ) ;
}
invtok = json_get_member ( buf , om , " invoice " ) ;
if ( invtok ) {
2021-11-30 04:06:04 +01:00
const u8 * invbin = json_tok_bin_from_hex ( tmpctx , buf , invtok ) ;
if ( invbin )
2022-03-28 01:10:54 +02:00
return handle_invoice ( cmd , invbin , reply_path , obs2_reply_path ) ;
2021-01-07 19:36:47 +01:00
}
2020-12-16 04:13:32 +01:00
return command_hook_success ( cmd ) ;
}
2020-12-16 04:13:23 +01:00
static const struct plugin_hook hooks [ ] = {
2021-09-30 23:19:37 +02:00
{
" onion_message_blinded " ,
onion_message_modern_call
} ,
2020-12-16 04:13:23 +01:00
} ;
2021-01-07 19:48:47 +01:00
struct decodable {
const char * type ;
struct bolt11 * b11 ;
struct tlv_offer * offer ;
struct tlv_invoice * invoice ;
struct tlv_invoice_request * invreq ;
2022-07-16 15:18:27 +02:00
struct rune * rune ;
2021-01-07 19:48:47 +01:00
} ;
static struct command_result * param_decodable ( struct command * cmd ,
const char * name ,
const char * buffer ,
const jsmntok_t * token ,
struct decodable * decodable )
{
char * likely_fail = NULL , * fail ;
jsmntok_t tok ;
/* BOLT #11:
*
* If a URI scheme is desired , the current recommendation is to either
* use ' lightning : ' as a prefix before the BOLT - 11 encoding
*/
tok = * token ;
if ( json_tok_startswith ( buffer , & tok , " lightning: " )
| | json_tok_startswith ( buffer , & tok , " LIGHTNING: " ) )
tok . start + = strlen ( " lightning: " ) ;
decodable - > offer = offer_decode ( cmd , buffer + tok . start ,
tok . end - tok . start ,
plugin_feature_set ( cmd - > plugin ) , NULL ,
json_tok_startswith ( buffer , & tok , " lno1 " )
? & likely_fail : & fail ) ;
if ( decodable - > offer ) {
decodable - > type = " bolt12 offer " ;
return NULL ;
}
decodable - > invoice = invoice_decode ( cmd , buffer + tok . start ,
tok . end - tok . start ,
plugin_feature_set ( cmd - > plugin ) ,
NULL ,
json_tok_startswith ( buffer , & tok ,
" lni1 " )
? & likely_fail : & fail ) ;
if ( decodable - > invoice ) {
decodable - > type = " bolt12 invoice " ;
return NULL ;
}
decodable - > invreq = invrequest_decode ( cmd , buffer + tok . start ,
tok . end - tok . start ,
plugin_feature_set ( cmd - > plugin ) ,
NULL ,
json_tok_startswith ( buffer , & tok ,
" lnr1 " )
? & likely_fail : & fail ) ;
if ( decodable - > invreq ) {
decodable - > type = " bolt12 invoice_request " ;
return NULL ;
}
/* If no other was likely, bolt11 decoder gives us failure string. */
decodable - > b11 = bolt11_decode ( cmd ,
tal_strndup ( tmpctx , buffer + tok . start ,
tok . end - tok . start ) ,
plugin_feature_set ( cmd - > plugin ) ,
NULL , NULL ,
likely_fail ? & fail : & likely_fail ) ;
if ( decodable - > b11 ) {
decodable - > type = " bolt11 invoice " ;
return NULL ;
}
2022-07-16 15:18:27 +02:00
decodable - > rune = rune_from_base64n ( decodable , buffer + tok . start ,
tok . end - tok . start ) ;
if ( decodable - > rune ) {
decodable - > type = " rune " ;
return NULL ;
}
2021-01-07 19:48:47 +01:00
/* Return failure message from most likely parsing candidate */
return command_fail_badparam ( cmd , name , buffer , & tok , likely_fail ) ;
}
static void json_add_chains ( struct json_stream * js ,
const struct bitcoin_blkid * chains )
{
json_array_start ( js , " chains " ) ;
for ( size_t i = 0 ; i < tal_count ( chains ) ; i + + )
json_add_sha256 ( js , NULL , & chains [ i ] . shad . sha ) ;
json_array_end ( js ) ;
}
static void json_add_onionmsg_path ( struct json_stream * js ,
const char * fieldname ,
const struct onionmsg_path * path ,
const struct blinded_payinfo * payinfo )
{
json_object_start ( js , fieldname ) ;
json_add_pubkey ( js , " node_id " , & path - > node_id ) ;
2021-11-30 04:06:04 +01:00
json_add_hex_talarr ( js , " encrypted_recipient_data " , path - > encrypted_recipient_data ) ;
2021-01-07 19:48:47 +01:00
if ( payinfo ) {
json_add_u32 ( js , " fee_base_msat " , payinfo - > fee_base_msat ) ;
json_add_u32 ( js , " fee_proportional_millionths " ,
payinfo - > fee_proportional_millionths ) ;
json_add_u32 ( js , " cltv_expiry_delta " ,
payinfo - > cltv_expiry_delta ) ;
json_add_hex_talarr ( js , " features " , payinfo - > features ) ;
}
json_object_end ( js ) ;
}
2021-05-26 07:46:01 +02:00
/* Returns true if valid */
static bool json_add_blinded_paths ( struct json_stream * js ,
2021-01-07 19:48:47 +01:00
struct blinded_path * * paths ,
struct blinded_payinfo * * blindedpay )
{
size_t n = 0 ;
json_array_start ( js , " paths " ) ;
for ( size_t i = 0 ; i < tal_count ( paths ) ; i + + ) {
json_object_start ( js , NULL ) ;
json_add_pubkey ( js , " blinding " , & paths [ i ] - > blinding ) ;
json_array_start ( js , " path " ) ;
for ( size_t j = 0 ; j < tal_count ( paths [ i ] - > path ) ; j + + ) {
json_add_onionmsg_path ( js , NULL , paths [ i ] - > path [ j ] ,
n < tal_count ( blindedpay )
? blindedpay [ n ] : NULL ) ;
n + + ;
}
json_array_end ( js ) ;
json_object_end ( js ) ;
}
json_array_end ( js ) ;
/* BOLT-offers #12:
* - MUST reject the invoice if ` blinded_payinfo ` does not contain
* exactly as many ` payinfo ` as total ` onionmsg_path ` in
* ` blinded_path ` .
*/
2021-05-26 07:46:01 +02:00
if ( blindedpay & & n ! = tal_count ( blindedpay ) ) {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_invalid_blinded_payinfo " ,
" invoice does not have correct number of blinded_payinfo " ) ;
2021-05-26 07:46:01 +02:00
return false ;
}
return true ;
2021-01-07 19:48:47 +01:00
}
static const char * recurrence_time_unit_name ( u8 time_unit )
{
2021-11-30 04:06:05 +01:00
/* BOLT-offers-recurrence #12:
2021-01-07 19:48:47 +01:00
* ` time_unit ` defining 0 ( seconds ) , 1 ( days ) , 2 ( months ) , 3 ( years ) .
*/
switch ( time_unit ) {
case 0 :
return " seconds " ;
case 1 :
return " days " ;
case 2 :
return " months " ;
case 3 :
return " years " ;
}
return NULL ;
}
static void json_add_offer ( struct json_stream * js , const struct tlv_offer * offer )
{
struct sha256 offer_id ;
2021-05-26 07:46:01 +02:00
bool valid = true ;
2021-01-07 19:48:47 +01:00
merkle_tlv ( offer - > fields , & offer_id ) ;
json_add_sha256 ( js , " offer_id " , & offer_id ) ;
if ( offer - > chains )
json_add_chains ( js , offer - > chains ) ;
if ( offer - > currency ) {
const struct iso4217_name_and_divisor * iso4217 ;
json_add_stringn ( js , " currency " ,
offer - > currency , tal_bytelen ( offer - > currency ) ) ;
if ( offer - > amount )
json_add_u64 ( js , " amount " , * offer - > amount ) ;
iso4217 = find_iso4217 ( offer - > currency ,
tal_bytelen ( offer - > currency ) ) ;
if ( iso4217 )
json_add_num ( js , " minor_unit " , iso4217 - > minor_unit ) ;
else
json_add_string ( js , " warning_offer_unknown_currency " ,
" unknown currency code " ) ;
} else if ( offer - > amount )
json_add_amount_msat_only ( js , " amount_msat " ,
amount_msat ( * offer - > amount ) ) ;
if ( offer - > send_invoice )
json_add_bool ( js , " send_invoice " , true ) ;
if ( offer - > refund_for )
json_add_sha256 ( js , " refund_for " , offer - > refund_for ) ;
/* BOLT-offers #12:
* A reader of an offer :
* . . .
2021-07-05 13:27:39 +02:00
* - if ` node_id ` or ` description ` is not set :
2021-01-07 19:48:47 +01:00
* - MUST NOT respond to the offer .
*/
if ( offer - > description )
json_add_stringn ( js , " description " ,
offer - > description ,
tal_bytelen ( offer - > description ) ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_offer_missing_description " ,
" offers without a description are invalid " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
2021-10-08 02:52:34 +02:00
if ( offer - > issuer ) {
json_add_stringn ( js , " issuer " , offer - > issuer ,
tal_bytelen ( offer - > issuer ) ) ;
if ( deprecated_apis ) {
json_add_stringn ( js , " vendor " , offer - > issuer ,
tal_bytelen ( offer - > issuer ) ) ;
}
}
2021-01-07 19:48:47 +01:00
if ( offer - > features )
json_add_hex_talarr ( js , " features " , offer - > features ) ;
if ( offer - > absolute_expiry )
json_add_u64 ( js , " absolute_expiry " ,
* offer - > absolute_expiry ) ;
if ( offer - > paths )
2021-05-26 07:46:01 +02:00
valid & = json_add_blinded_paths ( js , offer - > paths , NULL ) ;
2021-01-07 19:48:47 +01:00
if ( offer - > quantity_min )
json_add_u64 ( js , " quantity_min " , * offer - > quantity_min ) ;
if ( offer - > quantity_max )
json_add_u64 ( js , " quantity_max " , * offer - > quantity_max ) ;
if ( offer - > recurrence ) {
const char * name ;
json_object_start ( js , " recurrence " ) ;
json_add_num ( js , " time_unit " , offer - > recurrence - > time_unit ) ;
name = recurrence_time_unit_name ( offer - > recurrence - > time_unit ) ;
if ( name )
json_add_string ( js , " time_unit_name " , name ) ;
json_add_num ( js , " period " , offer - > recurrence - > period ) ;
if ( offer - > recurrence_base ) {
json_add_u64 ( js , " basetime " ,
offer - > recurrence_base - > basetime ) ;
if ( offer - > recurrence_base - > start_any_period )
json_add_bool ( js , " start_any_period " , true ) ;
}
if ( offer - > recurrence_limit )
json_add_u32 ( js , " limit " , * offer - > recurrence_limit ) ;
if ( offer - > recurrence_paywindow ) {
json_object_start ( js , " paywindow " ) ;
json_add_u32 ( js , " seconds_before " ,
offer - > recurrence_paywindow - > seconds_before ) ;
json_add_u32 ( js , " seconds_after " ,
offer - > recurrence_paywindow - > seconds_after ) ;
if ( offer - > recurrence_paywindow - > proportional_amount )
json_add_bool ( js , " proportional_amount " , true ) ;
json_object_end ( js ) ;
}
json_object_end ( js ) ;
}
2021-07-05 08:23:24 +02:00
if ( offer - > node_id )
2021-10-08 00:54:42 +02:00
json_add_point32 ( js , " node_id " , offer - > node_id ) ;
2021-07-05 08:23:24 +02:00
else
valid = false ;
/* If it's present, offer_decode checked it was valid */
if ( offer - > signature )
json_add_bip340sig ( js , " signature " , offer - > signature ) ;
2021-05-26 07:46:01 +02:00
json_add_bool ( js , " valid " , valid ) ;
2021-01-07 19:48:47 +01:00
}
2021-05-26 07:46:01 +02:00
/* Returns true if valid */
static bool json_add_fallback_address ( struct json_stream * js ,
2021-01-07 19:48:47 +01:00
const struct chainparams * chain ,
u8 version , const u8 * address )
{
2021-11-29 08:10:06 +01:00
char out [ 73 + strlen ( chain - > onchain_hrp ) ] ;
2021-01-07 19:48:47 +01:00
/* Does extra checks, in particular checks v0 sizes */
2021-11-29 08:10:06 +01:00
if ( segwit_addr_encode ( out , chain - > onchain_hrp , version ,
2021-05-26 07:46:01 +02:00
address , tal_bytelen ( address ) ) ) {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " address " , out ) ;
2021-05-26 07:46:01 +02:00
return true ;
}
json_add_string ( js ,
" warning_invoice_fallbacks_address_invalid " ,
" invalid fallback address for this version " ) ;
return false ;
2021-01-07 19:48:47 +01:00
}
2021-05-26 07:46:01 +02:00
/* Returns true if valid */
static bool json_add_fallbacks ( struct json_stream * js ,
2021-01-07 19:48:47 +01:00
const struct bitcoin_blkid * chains ,
struct fallback_address * * fallbacks )
{
const struct chainparams * chain ;
2021-05-26 07:46:01 +02:00
bool valid = true ;
2021-01-07 19:48:47 +01:00
/* Present address as first chain mentioned. */
if ( tal_count ( chains ) ! = 0 )
chain = chainparams_by_chainhash ( & chains [ 0 ] ) ;
else
chain = chainparams_for_network ( " bitcoin " ) ;
json_array_start ( js , " fallbacks " ) ;
for ( size_t i = 0 ; i < tal_count ( fallbacks ) ; i + + ) {
size_t addrlen = tal_bytelen ( fallbacks [ i ] - > address ) ;
json_object_start ( js , NULL ) ;
json_add_u32 ( js , " version " , fallbacks [ i ] - > version ) ;
json_add_hex_talarr ( js , " hex " , fallbacks [ i ] - > address ) ;
/* BOLT-offers #12:
* - for the bitcoin chain , if the invoice specifies ` fallbacks ` :
* - MUST ignore any ` fallback_address ` for which ` version ` is
* greater than 16.
* - MUST ignore any ` fallback_address ` for which ` address ` is
* less than 2 or greater than 40 bytes .
* - MUST ignore any ` fallback_address ` for which ` address ` does
* not meet known requirements for the given ` version `
*/
if ( fallbacks [ i ] - > version > 16 ) {
json_add_string ( js ,
" warning_invoice_fallbacks_version_invalid " ,
" invoice fallback version > 16 " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
2021-01-07 19:48:47 +01:00
} else if ( addrlen < 2 | | addrlen > 40 ) {
json_add_string ( js ,
" warning_invoice_fallbacks_address_invalid " ,
" invoice fallback address bad length " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
2021-01-07 19:48:47 +01:00
} else if ( chain ) {
2021-05-26 07:46:01 +02:00
valid & = json_add_fallback_address ( js , chain ,
fallbacks [ i ] - > version ,
fallbacks [ i ] - > address ) ;
2021-01-07 19:48:47 +01:00
}
json_object_end ( js ) ;
}
json_array_end ( js ) ;
2021-05-26 07:46:01 +02:00
return valid ;
2021-01-07 19:48:47 +01:00
}
static void json_add_b12_invoice ( struct json_stream * js ,
const struct tlv_invoice * invoice )
{
2021-05-26 07:46:01 +02:00
bool valid = true ;
2021-10-08 05:27:29 +02:00
if ( invoice - > chain )
json_add_sha256 ( js , " chain " , & invoice - > chain - > shad . sha ) ;
2021-01-07 19:48:47 +01:00
if ( invoice - > offer_id )
json_add_sha256 ( js , " offer_id " , invoice - > offer_id ) ;
/* BOLT-offers #12:
* - MUST reject the invoice if ` msat ` is not present .
*/
if ( invoice - > amount )
json_add_amount_msat_only ( js , " amount_msat " ,
amount_msat ( * invoice - > amount ) ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_missing_amount " ,
" invoices without an amount are invalid " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
* - MUST reject the invoice if ` description ` is not present .
*/
if ( invoice - > description )
json_add_stringn ( js , " description " , invoice - > description ,
tal_bytelen ( invoice - > description ) ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_missing_description " ,
" invoices without a description are invalid " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-10-08 02:52:34 +02:00
if ( invoice - > issuer ) {
json_add_stringn ( js , " issuer " , invoice - > issuer ,
tal_bytelen ( invoice - > issuer ) ) ;
if ( deprecated_apis ) {
json_add_stringn ( js , " vendor " , invoice - > issuer ,
tal_bytelen ( invoice - > issuer ) ) ;
}
}
2021-01-07 19:48:47 +01:00
if ( invoice - > features )
json_add_hex_talarr ( js , " features " , invoice - > features ) ;
if ( invoice - > paths ) {
/* BOLT-offers #12:
* - if ` blinded_path ` is present :
* - MUST reject the invoice if ` blinded_payinfo ` is not
* present .
* - MUST reject the invoice if ` blinded_payinfo ` does not
* contain exactly as many ` payinfo ` as total ` onionmsg_path `
* in ` blinded_path ` .
*/
2021-05-26 07:46:01 +02:00
if ( ! invoice - > blindedpay ) {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_missing_blinded_payinfo " ,
2021-05-26 07:46:01 +02:00
" invoices with blinded_path without blinded_payinfo are invalid " ) ;
valid = false ;
}
valid & = json_add_blinded_paths ( js , invoice - > paths , invoice - > blindedpay ) ;
2021-01-07 19:48:47 +01:00
}
if ( invoice - > quantity )
json_add_u64 ( js , " quantity " , * invoice - > quantity ) ;
if ( invoice - > send_invoice )
json_add_bool ( js , " send_invoice " , true ) ;
if ( invoice - > refund_for )
json_add_sha256 ( js , " refund_for " , invoice - > refund_for ) ;
if ( invoice - > recurrence_counter ) {
json_add_u32 ( js , " recurrence_counter " ,
* invoice - > recurrence_counter ) ;
if ( invoice - > recurrence_start )
json_add_u32 ( js , " recurrence_start " ,
* invoice - > recurrence_start ) ;
2021-11-30 04:06:05 +01:00
/* BOLT-offers-recurrence #12:
2021-01-07 19:48:47 +01:00
* - if the offer contained ` recurrence ` :
* - MUST reject the invoice if ` recurrence_basetime ` is not
* set .
*/
if ( invoice - > recurrence_basetime )
json_add_u64 ( js , " recurrence_basetime " ,
* invoice - > recurrence_basetime ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_missing_recurrence_basetime " ,
" recurring invoices without a recurrence_basetime are invalid " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
}
if ( invoice - > payer_key )
2021-10-08 00:54:42 +02:00
json_add_point32 ( js , " payer_key " , invoice - > payer_key ) ;
2021-01-07 19:48:47 +01:00
if ( invoice - > payer_info )
json_add_hex_talarr ( js , " payer_info " , invoice - > payer_info ) ;
2021-07-02 02:11:35 +02:00
if ( invoice - > payer_note )
json_add_stringn ( js , " payer_note " , invoice - > payer_note ,
tal_bytelen ( invoice - > payer_note ) ) ;
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
2021-07-21 03:31:39 +02:00
* - MUST reject the invoice if ` created_at ` is not present .
2021-01-07 19:48:47 +01:00
*/
2021-07-21 03:31:39 +02:00
if ( invoice - > created_at ) {
/* FIXME: Remove soon! */
if ( deprecated_apis )
json_add_u64 ( js , " timestamp " , * invoice - > created_at ) ;
json_add_u64 ( js , " created_at " , * invoice - > created_at ) ;
} else {
json_add_string ( js , " warning_invoice_missing_created_at " ,
" invoices without created_at are invalid " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
* - MUST reject the invoice if ` payment_hash ` is not present .
*/
if ( invoice - > payment_hash )
json_add_sha256 ( js , " payment_hash " , invoice - > payment_hash ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_missing_payment_hash " ,
" invoices without a payment_hash are invalid " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
*
* - if the expiry for accepting payment is not 7200 seconds after
2021-07-21 03:31:39 +02:00
* ` created_at ` :
2021-01-07 19:48:47 +01:00
* - MUST set ` relative_expiry `
*/
if ( invoice - > relative_expiry )
json_add_u32 ( js , " relative_expiry " , * invoice - > relative_expiry ) ;
else
json_add_u32 ( js , " relative_expiry " , 7200 ) ;
/* BOLT-offers #12:
* - if the ` min_final_cltv_expiry ` for the last HTLC in the route is
* not 18 :
* - MUST set ` min_final_cltv_expiry ` .
*/
if ( invoice - > cltv )
json_add_u32 ( js , " min_final_cltv_expiry " , * invoice - > cltv ) ;
else
json_add_u32 ( js , " min_final_cltv_expiry " , 18 ) ;
if ( invoice - > fallbacks )
2021-10-08 05:27:29 +02:00
valid & = json_add_fallbacks ( js ,
2021-11-30 04:06:05 +01:00
invoice - > chain ,
2022-03-22 09:50:13 +01:00
invoice - > fallbacks ) ;
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
* - if the offer contained ` refund_for ` :
* - MUST reject the invoice if ` payer_key ` does not match the invoice
* whose ` payment_hash ` is equal to ` refund_for `
* ` refunded_payment_hash `
* - MUST reject the invoice if ` refund_signature ` is not set .
* - MUST reject the invoice if ` refund_signature ` is not a valid
* signature using ` payer_key ` as described in
* [ Signature Calculation ] ( # signature - calculation ) .
*/
if ( invoice - > refund_signature ) {
json_add_bip340sig ( js , " refund_signature " ,
invoice - > refund_signature ) ;
2021-05-26 07:46:01 +02:00
if ( ! invoice - > payer_key ) {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_refund_signature_missing_payer_key " ,
" Can't have refund_signature without payer key " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
} else if ( ! bolt12_check_signature ( invoice - > fields ,
" invoice " ,
" refund_signature " ,
invoice - > payer_key ,
invoice - > refund_signature ) ) {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_refund_signature_invalid " ,
" refund_signature does not match " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
} else if ( invoice - > refund_for ) {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_refund_missing_signature " ,
" refund_for requires refund_signature " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
/* invoice_decode checked these */
2021-10-08 00:54:42 +02:00
json_add_point32 ( js , " node_id " , invoice - > node_id ) ;
2021-01-07 19:48:47 +01:00
json_add_bip340sig ( js , " signature " , invoice - > signature ) ;
2021-05-26 07:46:01 +02:00
json_add_bool ( js , " valid " , valid ) ;
2021-01-07 19:48:47 +01:00
}
static void json_add_invoice_request ( struct json_stream * js ,
const struct tlv_invoice_request * invreq )
{
2021-05-26 07:46:01 +02:00
bool valid = true ;
2021-10-08 05:27:29 +02:00
if ( invreq - > chain )
json_add_sha256 ( js , " chain " , & invreq - > chain - > shad . sha ) ;
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
* - MUST fail the request if ` payer_key ` is not present .
2021-11-30 04:06:05 +01:00
* . . .
2021-01-07 19:48:47 +01:00
* - MUST fail the request if ` features ` contains unknown even bits .
* - MUST fail the request if ` offer_id ` is not present .
*/
if ( invreq - > offer_id )
json_add_sha256 ( js , " offer_id " , invreq - > offer_id ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_request_missing_offer_id " ,
" invoice_request requires offer_id " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
if ( invreq - > amount )
json_add_amount_msat_only ( js , " amount_msat " ,
amount_msat ( * invreq - > amount ) ) ;
if ( invreq - > features )
json_add_hex_talarr ( js , " features " , invreq - > features ) ;
if ( invreq - > quantity )
json_add_u64 ( js , " quantity " , * invreq - > quantity ) ;
if ( invreq - > recurrence_counter )
json_add_u32 ( js , " recurrence_counter " ,
* invreq - > recurrence_counter ) ;
if ( invreq - > recurrence_start )
json_add_u32 ( js , " recurrence_start " ,
* invreq - > recurrence_start ) ;
if ( invreq - > payer_key )
2021-10-08 00:54:42 +02:00
json_add_point32 ( js , " payer_key " , invreq - > payer_key ) ;
2021-05-26 07:46:01 +02:00
else {
2021-01-07 19:48:47 +01:00
json_add_string ( js , " warning_invoice_request_missing_payer_key " ,
" invoice_request requires payer_key " ) ;
2021-05-26 07:46:01 +02:00
valid = false ;
}
2021-01-07 19:48:47 +01:00
if ( invreq - > payer_info )
json_add_hex_talarr ( js , " payer_info " , invreq - > payer_info ) ;
2021-07-02 02:11:35 +02:00
if ( invreq - > payer_note )
json_add_stringn ( js , " payer_note " , invreq - > payer_note ,
tal_bytelen ( invreq - > payer_note ) ) ;
2021-01-07 19:48:47 +01:00
/* BOLT-offers #12:
2022-03-22 09:50:13 +01:00
* - MUST fail the request if there is no ` signature ` field .
* - MUST fail the request if ` signature ` is not correct .
2021-01-07 19:48:47 +01:00
*/
2022-03-22 09:50:13 +01:00
if ( invreq - > signature ) {
2021-07-02 02:11:35 +02:00
if ( invreq - > payer_key
& & ! bolt12_check_signature ( invreq - > fields ,
" invoice_request " ,
2022-03-22 09:50:13 +01:00
" signature " ,
2021-07-02 02:11:35 +02:00
invreq - > payer_key ,
2022-03-22 09:50:13 +01:00
invreq - > signature ) ) {
bool sig_valid ;
if ( deprecated_apis ) {
/* The old name? */
sig_valid = bolt12_check_signature ( invreq - > fields ,
" invoice_request " ,
" payer_signature " ,
invreq - > payer_key ,
invreq - > signature ) ;
} else {
sig_valid = false ;
}
if ( ! sig_valid ) {
json_add_string ( js , " warning_invoice_request_invalid_signature " ,
" Bad signature " ) ;
valid = false ;
}
2021-07-02 02:11:35 +02:00
}
} else {
2022-03-22 09:50:13 +01:00
json_add_string ( js , " warning_invoice_request_missing_signature " ,
" Missing signature " ) ;
2021-07-05 08:23:13 +02:00
valid = false ;
2021-01-07 19:48:47 +01:00
}
2021-05-26 07:46:01 +02:00
json_add_bool ( js , " valid " , valid ) ;
2021-01-07 19:48:47 +01:00
}
2022-07-16 15:18:27 +02:00
static void json_add_rune ( struct command * cmd , struct json_stream * js , const struct rune * rune )
{
2022-07-25 03:23:30 +02:00
const char * string ;
/* Simplest to check everything for UTF-8 compliance at once.
* Since separators are | and & ( which cannot appear inside
* UTF - 8 multichars ) , if the entire thing is valid UTF - 8 then
* each part is . */
string = rune_to_string ( tmpctx , rune ) ;
if ( ! utf8_check ( string , strlen ( string ) ) ) {
json_add_hex ( js , " hex " , string , strlen ( string ) ) ;
json_add_string ( js , " warning_rune_invalid_utf8 " ,
" Rune contains invalid UTF-8 strings " ) ;
json_add_bool ( js , " valid " , false ) ;
return ;
}
2022-07-16 15:18:27 +02:00
if ( rune - > unique_id )
json_add_string ( js , " unique_id " , rune - > unique_id ) ;
if ( rune - > version )
json_add_string ( js , " version " , rune - > version ) ;
2022-07-25 03:23:30 +02:00
json_add_string ( js , " string " , take ( string ) ) ;
2022-07-16 15:18:27 +02:00
json_array_start ( js , " restrictions " ) ;
for ( size_t i = rune - > unique_id ? 1 : 0 ; i < tal_count ( rune - > restrs ) ; i + + ) {
const struct rune_restr * restr = rune - > restrs [ i ] ;
char * summary = tal_strdup ( tmpctx , " " ) ;
const char * sep = " " ;
json_object_start ( js , NULL ) ;
json_array_start ( js , " alternatives " ) ;
for ( size_t j = 0 ; j < tal_count ( restr - > alterns ) ; j + + ) {
const struct rune_altern * alt = restr - > alterns [ j ] ;
const char * annotation , * value ;
bool int_val = false , time_val = false ;
if ( streq ( alt - > fieldname , " time " ) ) {
annotation = " in seconds since 1970 " ;
time_val = true ;
} else if ( streq ( alt - > fieldname , " id " ) )
annotation = " of commanding peer " ;
else if ( streq ( alt - > fieldname , " method " ) )
annotation = " of command " ;
else if ( streq ( alt - > fieldname , " pnum " ) ) {
annotation = " number of command parameters " ;
int_val = true ;
} else if ( streq ( alt - > fieldname , " rate " ) ) {
annotation = " max per minute " ;
int_val = true ;
} else if ( strstarts ( alt - > fieldname , " parr " ) ) {
annotation = tal_fmt ( tmpctx , " array parameter #%s " , alt - > fieldname + 4 ) ;
} else if ( strstarts ( alt - > fieldname , " pname " ) )
annotation = tal_fmt ( tmpctx , " object parameter '%s' " , alt - > fieldname + 5 ) ;
else
annotation = " unknown condition? " ;
tal_append_fmt ( & summary , " %s " , sep ) ;
/* Where it's ambiguous, quote if it's not treated as an int */
if ( int_val )
value = alt - > value ;
else if ( time_val ) {
u64 t = atol ( alt - > value ) ;
if ( t ) {
u64 diff , now = time_now ( ) . ts . tv_sec ;
/* Need a non-const during construction */
char * v ;
if ( now > t )
diff = now - t ;
else
diff = t - now ;
if ( diff < 60 )
v = tal_fmt ( tmpctx , " % " PRIu64 " seconds " , diff ) ;
else if ( diff < 60 * 60 )
v = tal_fmt ( tmpctx , " % " PRIu64 " minutes % " PRIu64 " seconds " ,
diff / 60 , diff % 60 ) ;
else {
v = tal_strdup ( tmpctx , " approximately " ) ;
/* diff is in minutes */
diff / = 60 ;
if ( diff < 48 * 60 )
tal_append_fmt ( & v , " % " PRIu64 " hours % " PRIu64 " minutes " ,
diff / 60 , diff % 60 ) ;
else {
/* hours */
diff / = 60 ;
if ( diff < 60 * 24 )
tal_append_fmt ( & v , " % " PRIu64 " days % " PRIu64 " hours " ,
diff / 24 , diff % 24 ) ;
else {
/* days */
diff / = 24 ;
if ( diff < 365 * 2 )
tal_append_fmt ( & v , " % " PRIu64 " months % " PRIu64 " days " ,
diff / 30 , diff % 30 ) ;
else {
/* months */
diff / = 30 ;
tal_append_fmt ( & v , " % " PRIu64 " years % " PRIu64 " months " ,
diff / 12 , diff % 12 ) ;
}
}
}
}
if ( now > t )
tal_append_fmt ( & v , " ago " ) ;
else
tal_append_fmt ( & v , " from now " ) ;
value = tal_fmt ( tmpctx , " %s (%s) " , alt - > value , v ) ;
} else
value = alt - > value ;
} else
value = tal_fmt ( tmpctx , " '%s' " , alt - > value ) ;
switch ( alt - > condition ) {
case RUNE_COND_IF_MISSING :
tal_append_fmt ( & summary , " %s (%s) is missing " , alt - > fieldname , annotation ) ;
break ;
case RUNE_COND_EQUAL :
tal_append_fmt ( & summary , " %s (%s) equal to %s " , alt - > fieldname , annotation , value ) ;
break ;
case RUNE_COND_NOT_EQUAL :
tal_append_fmt ( & summary , " %s (%s) unequal to %s " , alt - > fieldname , annotation , value ) ;
break ;
case RUNE_COND_BEGINS :
tal_append_fmt ( & summary , " %s (%s) starts with '%s' " , alt - > fieldname , annotation , alt - > value ) ;
break ;
case RUNE_COND_ENDS :
tal_append_fmt ( & summary , " %s (%s) ends with '%s' " , alt - > fieldname , annotation , alt - > value ) ;
break ;
case RUNE_COND_CONTAINS :
tal_append_fmt ( & summary , " %s (%s) contains '%s' " , alt - > fieldname , annotation , alt - > value ) ;
break ;
case RUNE_COND_INT_LESS :
tal_append_fmt ( & summary , " %s (%s) less than %s " , alt - > fieldname , annotation ,
time_val ? value : alt - > value ) ;
break ;
case RUNE_COND_INT_GREATER :
tal_append_fmt ( & summary , " %s (%s) greater than %s " , alt - > fieldname , annotation ,
time_val ? value : alt - > value ) ;
break ;
case RUNE_COND_LEXO_BEFORE :
tal_append_fmt ( & summary , " %s (%s) sorts before '%s' " , alt - > fieldname , annotation , alt - > value ) ;
break ;
case RUNE_COND_LEXO_AFTER :
tal_append_fmt ( & summary , " %s (%s) sorts after '%s' " , alt - > fieldname , annotation , alt - > value ) ;
break ;
case RUNE_COND_COMMENT :
tal_append_fmt ( & summary , " [comment: %s%s] " , alt - > fieldname , alt - > value ) ;
break ;
}
sep = " OR " ;
json_add_str_fmt ( js , NULL , " %s%c%s " , alt - > fieldname , alt - > condition , alt - > value ) ;
}
json_array_end ( js ) ;
json_add_string ( js , " summary " , summary ) ;
json_object_end ( js ) ;
}
json_array_end ( js ) ;
/* FIXME: do some sanity checks? */
json_add_bool ( js , " valid " , true ) ;
}
2021-01-07 19:48:47 +01:00
static struct command_result * json_decode ( struct command * cmd ,
const char * buffer ,
const jsmntok_t * params )
{
struct decodable * decodable = talz ( cmd , struct decodable ) ;
struct json_stream * response ;
if ( ! param ( cmd , buffer , params ,
p_req ( " string " , param_decodable , decodable ) ,
NULL ) )
return command_param_failed ( ) ;
response = jsonrpc_stream_success ( cmd ) ;
json_add_string ( response , " type " , decodable - > type ) ;
if ( decodable - > offer )
json_add_offer ( response , decodable - > offer ) ;
if ( decodable - > invreq )
json_add_invoice_request ( response , decodable - > invreq ) ;
if ( decodable - > invoice )
json_add_b12_invoice ( response , decodable - > invoice ) ;
2021-05-26 07:46:01 +02:00
if ( decodable - > b11 ) {
/* The bolt11 decoder simply refuses to decode bad invs. */
2021-01-07 19:48:47 +01:00
json_add_bolt11 ( response , decodable - > b11 ) ;
2021-05-26 07:46:01 +02:00
json_add_bool ( response , " valid " , true ) ;
}
2022-07-16 15:18:27 +02:00
if ( decodable - > rune )
json_add_rune ( cmd , response , decodable - > rune ) ;
2021-01-07 19:48:47 +01:00
return command_finished ( cmd , response ) ;
}
2021-01-13 04:00:24 +01:00
static const char * init ( struct plugin * p ,
const char * buf UNUSED ,
const jsmntok_t * config UNUSED )
2020-12-16 04:13:23 +01:00
{
2020-12-16 04:13:28 +01:00
struct pubkey k ;
2021-01-06 06:41:20 +01:00
rpc_scan ( p , " getinfo " ,
take ( json_out_obj ( NULL , NULL , NULL ) ) ,
" {id:%} " , JSON_SCAN ( json_to_pubkey , & k ) ) ;
2020-12-16 04:13:28 +01:00
if ( secp256k1_xonly_pubkey_from_pubkey ( secp256k1_ctx , & id . pubkey ,
NULL , & k . pubkey ) ! = 1 )
abort ( ) ;
2020-12-16 04:13:32 +01:00
2021-01-06 06:41:20 +01:00
rpc_scan ( p , " listconfigs " ,
2021-01-13 09:58:38 +01:00
take ( json_out_obj ( NULL , NULL , NULL ) ) ,
" {cltv-final:%,experimental-offers:%} " ,
2022-03-22 09:50:13 +01:00
JSON_SCAN ( json_to_u16 , & cltv_final ) ,
2021-08-02 01:56:12 +02:00
JSON_SCAN ( json_to_bool , & offers_enabled ) ) ;
2021-01-13 04:00:24 +01:00
return NULL ;
2020-12-16 04:13:23 +01:00
}
static const struct plugin_command commands [ ] = {
2020-12-16 04:13:28 +01:00
{
" offer " ,
" payment " ,
2021-01-07 19:46:47 +01:00
" Create an offer to accept money " ,
2021-10-08 02:52:34 +02:00
" Create an offer for invoices of {amount} with {description}, optional {issuer}, internal {label}, {quantity_min}, {quantity_max}, {absolute_expiry}, {recurrence}, {recurrence_base}, {recurrence_paywindow}, {recurrence_limit} and {single_use} " ,
2020-12-16 04:13:28 +01:00
json_offer
} ,
2021-01-07 19:46:47 +01:00
{
" offerout " ,
" payment " ,
" Create an offer to send money " ,
2021-10-08 02:52:34 +02:00
" Create an offer to pay invoices of {amount} with {description}, optional {issuer}, internal {label}, {absolute_expiry} and {refund_for} " ,
2021-01-07 19:46:47 +01:00
json_offerout
} ,
2021-01-07 19:48:47 +01:00
{
" decode " ,
" utility " ,
" Decode {string} message, returning {type} and information. " ,
NULL ,
json_decode ,
} ,
2020-12-16 04:13:23 +01:00
} ;
int main ( int argc , char * argv [ ] )
{
setup_locale ( ) ;
2020-12-16 04:13:32 +01:00
/* We deal in UTC; mktime() uses local time */
setenv ( " TZ " , " " , 1 ) ;
2020-12-16 04:13:23 +01:00
plugin_main ( argv , init , PLUGIN_RESTARTABLE , true , NULL , commands ,
ARRAY_SIZE ( commands ) , NULL , 0 , hooks , ARRAY_SIZE ( hooks ) ,
2021-04-28 17:28:27 +02:00
NULL , 0 , NULL ) ;
2020-12-16 04:13:23 +01:00
}