mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 15:14:23 +01:00
bolt11: allow multiple fallback addresses.
We can have more than one; eg we might offer both bech32 and a p2sh address, and in future we might offer v1 segwit, etc. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
committed by
Christian Decker
parent
5b7fcab766
commit
09c4203767
@@ -313,13 +313,10 @@ static char *decode_n(struct bolt11 *b11,
|
|||||||
static char *decode_f(struct bolt11 *b11,
|
static char *decode_f(struct bolt11 *b11,
|
||||||
struct hash_u5 *hu5,
|
struct hash_u5 *hu5,
|
||||||
u5 **data, size_t *data_len,
|
u5 **data, size_t *data_len,
|
||||||
size_t data_length, bool *have_f)
|
size_t data_length)
|
||||||
{
|
{
|
||||||
u64 version;
|
u64 version;
|
||||||
|
u8 *fallback;
|
||||||
if (*have_f)
|
|
||||||
return unknown_field(b11, hu5, data, data_len, 'f',
|
|
||||||
data_length);
|
|
||||||
|
|
||||||
if (!pull_uint(hu5, data, data_len, &version, 5))
|
if (!pull_uint(hu5, data, data_len, &version, 5))
|
||||||
return tal_fmt(b11, "f: data_length %zu short", data_length);
|
return tal_fmt(b11, "f: data_length %zu short", data_length);
|
||||||
@@ -339,8 +336,7 @@ static char *decode_f(struct bolt11 *b11,
|
|||||||
|
|
||||||
pull_bits_certain(hu5, data, data_len, &pkhash, data_length*5,
|
pull_bits_certain(hu5, data, data_len, &pkhash, data_length*5,
|
||||||
false);
|
false);
|
||||||
b11->fallback = scriptpubkey_p2pkh(b11, &pkhash);
|
fallback = scriptpubkey_p2pkh(b11, &pkhash);
|
||||||
return NULL;
|
|
||||||
} else if (version == 18) {
|
} else if (version == 18) {
|
||||||
/* Pay to pubkey script hash (P2SH) */
|
/* Pay to pubkey script hash (P2SH) */
|
||||||
struct ripemd160 shash;
|
struct ripemd160 shash;
|
||||||
@@ -350,7 +346,7 @@ static char *decode_f(struct bolt11 *b11,
|
|||||||
|
|
||||||
pull_bits_certain(hu5, data, data_len, &shash, data_length*5,
|
pull_bits_certain(hu5, data, data_len, &shash, data_length*5,
|
||||||
false);
|
false);
|
||||||
b11->fallback = scriptpubkey_p2sh_hash(b11, &shash);
|
fallback = scriptpubkey_p2sh_hash(b11, &shash);
|
||||||
} else if (version < 17) {
|
} else if (version < 17) {
|
||||||
u8 *f = tal_arr(b11, u8, data_length * 5 / 8);
|
u8 *f = tal_arr(b11, u8, data_length * 5 / 8);
|
||||||
if (version == 0) {
|
if (version == 0) {
|
||||||
@@ -361,14 +357,20 @@ static char *decode_f(struct bolt11 *b11,
|
|||||||
}
|
}
|
||||||
pull_bits_certain(hu5, data, data_len, f, data_length * 5,
|
pull_bits_certain(hu5, data, data_len, f, data_length * 5,
|
||||||
false);
|
false);
|
||||||
b11->fallback = scriptpubkey_witness_raw(b11, version,
|
fallback = scriptpubkey_witness_raw(b11, version,
|
||||||
f, tal_len(f));
|
f, tal_len(f));
|
||||||
tal_free(f);
|
tal_free(f);
|
||||||
} else
|
} else
|
||||||
return unknown_field(b11, hu5, data, data_len, 'f',
|
return unknown_field(b11, hu5, data, data_len, 'f',
|
||||||
data_length);
|
data_length);
|
||||||
|
|
||||||
*have_f = true;
|
if (b11->fallbacks == NULL)
|
||||||
|
b11->fallbacks = tal_arr(b11, const u8 *, 1);
|
||||||
|
else
|
||||||
|
tal_resize(&b11->fallbacks, tal_count(b11->fallbacks) + 1);
|
||||||
|
|
||||||
|
b11->fallbacks[tal_count(b11->fallbacks)-1]
|
||||||
|
= tal_steal(b11->fallbacks, fallback);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +443,7 @@ struct bolt11 *new_bolt11(const tal_t *ctx, u64 *msatoshi)
|
|||||||
list_head_init(&b11->extra_fields);
|
list_head_init(&b11->extra_fields);
|
||||||
b11->description = NULL;
|
b11->description = NULL;
|
||||||
b11->description_hash = NULL;
|
b11->description_hash = NULL;
|
||||||
b11->fallback = NULL;
|
b11->fallbacks = NULL;
|
||||||
b11->routes = NULL;
|
b11->routes = NULL;
|
||||||
b11->msatoshi = NULL;
|
b11->msatoshi = NULL;
|
||||||
b11->expiry = DEFAULT_X;
|
b11->expiry = DEFAULT_X;
|
||||||
@@ -465,7 +467,7 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str,
|
|||||||
struct hash_u5 hu5;
|
struct hash_u5 hu5;
|
||||||
struct sha256 hash;
|
struct sha256 hash;
|
||||||
bool have_p = false, have_n = false, have_d = false, have_h = false,
|
bool have_p = false, have_n = false, have_d = false, have_h = false,
|
||||||
have_x = false, have_f = false, have_c = false;
|
have_x = false, have_c = false;
|
||||||
|
|
||||||
b11->routes = tal_arr(b11, struct route_info *, 0);
|
b11->routes = tal_arr(b11, struct route_info *, 0);
|
||||||
|
|
||||||
@@ -617,8 +619,7 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str,
|
|||||||
|
|
||||||
case 'f':
|
case 'f':
|
||||||
problem = decode_f(b11, &hu5, &data,
|
problem = decode_f(b11, &hu5, &data,
|
||||||
&data_len, data_length,
|
&data_len, data_length);
|
||||||
&have_f);
|
|
||||||
break;
|
break;
|
||||||
case 'r':
|
case 'r':
|
||||||
problem = decode_r(b11, &hu5, &data, &data_len,
|
problem = decode_r(b11, &hu5, &data, &data_len,
|
||||||
@@ -948,8 +949,8 @@ char *bolt11_encode_(const tal_t *ctx,
|
|||||||
if (b11->min_final_cltv_expiry != DEFAULT_C)
|
if (b11->min_final_cltv_expiry != DEFAULT_C)
|
||||||
encode_c(&data, b11->min_final_cltv_expiry);
|
encode_c(&data, b11->min_final_cltv_expiry);
|
||||||
|
|
||||||
if (b11->fallback)
|
for (size_t i = 0; i < tal_count(b11->fallbacks); i++)
|
||||||
encode_f(&data, b11->fallback);
|
encode_f(&data, b11->fallbacks[i]);
|
||||||
|
|
||||||
for (size_t i = 0; i < tal_count(b11->routes); i++)
|
for (size_t i = 0; i < tal_count(b11->routes); i++)
|
||||||
encode_r(&data, b11->routes[i]);
|
encode_r(&data, b11->routes[i]);
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ struct bolt11 {
|
|||||||
/* How many blocks final hop requires. */
|
/* How many blocks final hop requires. */
|
||||||
u32 min_final_cltv_expiry;
|
u32 min_final_cltv_expiry;
|
||||||
|
|
||||||
/* If non-NULL, indicates a fallback address to pay to. */
|
/* If non-NULL, indicates fallback addresses to pay to. */
|
||||||
const u8 *fallback;
|
const u8 **fallbacks;
|
||||||
|
|
||||||
/* If non-NULL: array of route arrays */
|
/* If non-NULL: array of route arrays */
|
||||||
struct route_info **routes;
|
struct route_info **routes;
|
||||||
|
|||||||
@@ -91,12 +91,11 @@ static void test_b11(const char *b11str,
|
|||||||
assert(b11->expiry == expect_b11->expiry);
|
assert(b11->expiry == expect_b11->expiry);
|
||||||
assert(b11->min_final_cltv_expiry == expect_b11->min_final_cltv_expiry);
|
assert(b11->min_final_cltv_expiry == expect_b11->min_final_cltv_expiry);
|
||||||
|
|
||||||
if (!b11->fallback)
|
assert(tal_count(b11->fallbacks) == tal_count(expect_b11->fallbacks));
|
||||||
assert(!expect_b11->fallback);
|
for (size_t i = 0; i < tal_count(b11->fallbacks); i++)
|
||||||
else
|
assert(memeq(b11->fallbacks[i], tal_len(b11->fallbacks[i]),
|
||||||
assert(memeq(b11->fallback, tal_len(b11->fallback),
|
expect_b11->fallbacks[i],
|
||||||
expect_b11->fallback,
|
tal_len(expect_b11->fallbacks[i])));
|
||||||
tal_len(expect_b11->fallback)));
|
|
||||||
|
|
||||||
/* FIXME: compare routes. */
|
/* FIXME: compare routes. */
|
||||||
assert(tal_count(b11->routes) == tal_count(expect_b11->routes));
|
assert(tal_count(b11->routes) == tal_count(expect_b11->routes));
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class LightningRpc(UnixDomainSocketRpc):
|
|||||||
}
|
}
|
||||||
return self.call("listchannels", payload)
|
return self.call("listchannels", payload)
|
||||||
|
|
||||||
def invoice(self, msatoshi, label, description, expiry=None, fallback=None):
|
def invoice(self, msatoshi, label, description, expiry=None, fallbacks=None):
|
||||||
"""
|
"""
|
||||||
Create an invoice for {msatoshi} with {label} and {description} with
|
Create an invoice for {msatoshi} with {label} and {description} with
|
||||||
optional {expiry} seconds (default 1 hour)
|
optional {expiry} seconds (default 1 hour)
|
||||||
@@ -160,7 +160,7 @@ class LightningRpc(UnixDomainSocketRpc):
|
|||||||
"label": label,
|
"label": label,
|
||||||
"description": description,
|
"description": description,
|
||||||
"expiry": expiry,
|
"expiry": expiry,
|
||||||
"fallback": fallback
|
"fallbacks": fallbacks
|
||||||
}
|
}
|
||||||
return self.call("invoice", payload)
|
return self.call("invoice", payload)
|
||||||
|
|
||||||
|
|||||||
@@ -111,27 +111,27 @@ int main(int argc, char *argv[])
|
|||||||
tal_hexstr(ctx, b11->description_hash,
|
tal_hexstr(ctx, b11->description_hash,
|
||||||
sizeof(*b11->description_hash)));
|
sizeof(*b11->description_hash)));
|
||||||
|
|
||||||
if (tal_len(b11->fallback)) {
|
for (i = 0; i < tal_count(b11->fallbacks); i++) {
|
||||||
struct bitcoin_address pkh;
|
struct bitcoin_address pkh;
|
||||||
struct ripemd160 sh;
|
struct ripemd160 sh;
|
||||||
struct sha256 wsh;
|
struct sha256 wsh;
|
||||||
|
|
||||||
printf("fallback: %s\n", tal_hex(ctx, b11->fallback));
|
printf("fallback: %s\n", tal_hex(ctx, b11->fallbacks[i]));
|
||||||
if (is_p2pkh(b11->fallback, &pkh)) {
|
if (is_p2pkh(b11->fallbacks[i], &pkh)) {
|
||||||
printf("fallback-P2PKH: %s\n",
|
printf("fallback-P2PKH: %s\n",
|
||||||
bitcoin_to_base58(ctx, b11->chain->testnet,
|
bitcoin_to_base58(ctx, b11->chain->testnet,
|
||||||
&pkh));
|
&pkh));
|
||||||
} else if (is_p2sh(b11->fallback, &sh)) {
|
} else if (is_p2sh(b11->fallbacks[i], &sh)) {
|
||||||
printf("fallback-P2SH: %s\n",
|
printf("fallback-P2SH: %s\n",
|
||||||
p2sh_to_base58(ctx,
|
p2sh_to_base58(ctx,
|
||||||
b11->chain->testnet,
|
b11->chain->testnet,
|
||||||
&sh));
|
&sh));
|
||||||
} else if (is_p2wpkh(b11->fallback, &pkh)) {
|
} else if (is_p2wpkh(b11->fallbacks[i], &pkh)) {
|
||||||
char out[73 + strlen(b11->chain->bip173_name)];
|
char out[73 + strlen(b11->chain->bip173_name)];
|
||||||
if (segwit_addr_encode(out, b11->chain->bip173_name, 0,
|
if (segwit_addr_encode(out, b11->chain->bip173_name, 0,
|
||||||
(const u8 *)&pkh, sizeof(pkh)))
|
(const u8 *)&pkh, sizeof(pkh)))
|
||||||
printf("fallback-P2WPKH: %s\n", out);
|
printf("fallback-P2WPKH: %s\n", out);
|
||||||
} else if (is_p2wsh(b11->fallback, &wsh)) {
|
} else if (is_p2wsh(b11->fallbacks[i], &wsh)) {
|
||||||
char out[73 + strlen(b11->chain->bip173_name)];
|
char out[73 + strlen(b11->chain->bip173_name)];
|
||||||
if (segwit_addr_encode(out, b11->chain->bip173_name, 0,
|
if (segwit_addr_encode(out, b11->chain->bip173_name, 0,
|
||||||
(const u8 *)&wsh, sizeof(wsh)))
|
(const u8 *)&wsh, sizeof(wsh)))
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
.\" Title: lightning-decodepay
|
.\" Title: lightning-decodepay
|
||||||
.\" Author: [see the "AUTHOR" section]
|
.\" Author: [see the "AUTHOR" section]
|
||||||
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
|
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
|
||||||
.\" Date: 01/13/2018
|
.\" Date: 04/05/2018
|
||||||
.\" Manual: \ \&
|
.\" Manual: \ \&
|
||||||
.\" Source: \ \&
|
.\" Source: \ \&
|
||||||
.\" Language: English
|
.\" Language: English
|
||||||
.\"
|
.\"
|
||||||
.TH "LIGHTNING\-DECODEPAY" "7" "01/13/2018" "\ \&" "\ \&"
|
.TH "LIGHTNING\-DECODEPAY" "7" "04/05/2018" "\ \&" "\ \&"
|
||||||
.\" -----------------------------------------------------------------
|
.\" -----------------------------------------------------------------
|
||||||
.\" * Define some portability stuff
|
.\" * Define some portability stuff
|
||||||
.\" -----------------------------------------------------------------
|
.\" -----------------------------------------------------------------
|
||||||
@@ -138,7 +138,7 @@ The following fields are optional:
|
|||||||
.sp -1
|
.sp -1
|
||||||
.IP \(bu 2.3
|
.IP \(bu 2.3
|
||||||
.\}
|
.\}
|
||||||
\fIfallback\fR: fallback address object containing a
|
\fIfallbacks\fR: array of fallback address object containing a
|
||||||
\fIhex\fR
|
\fIhex\fR
|
||||||
string, and both
|
string, and both
|
||||||
\fItype\fR
|
\fItype\fR
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ by BOLT11:
|
|||||||
The following fields are optional:
|
The following fields are optional:
|
||||||
|
|
||||||
- 'msatoshi': the number of millisatoshi requested (if any).
|
- 'msatoshi': the number of millisatoshi requested (if any).
|
||||||
- 'fallback': fallback address object containing a 'hex' string, and
|
- 'fallbacks': array of fallback address object containing a 'hex' string, and
|
||||||
both 'type' and 'addr' if it is recognized as one of 'P2PKH', 'P2SH', 'P2WPKH', or 'P2WSH'.
|
both 'type' and 'addr' if it is recognized as one of 'P2PKH', 'P2SH', 'P2WPKH', or 'P2WSH'.
|
||||||
- 'routes': an array of routes. Each route is an arrays of objects, each containing 'pubkey', 'short_channel_id', 'fee_base_msat', 'fee_proportional_millionths' and 'cltv_expiry_delta'.
|
- 'routes': an array of routes. Each route is an arrays of objects, each containing 'pubkey', 'short_channel_id', 'fee_base_msat', 'fee_proportional_millionths' and 'cltv_expiry_delta'.
|
||||||
- 'extra': an array of objects representing unknown fields, each with one-character 'tag' and a 'data' bech32 string.
|
- 'extra': an array of objects representing unknown fields, each with one-character 'tag' and a 'data' bech32 string.
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
.\" Title: lightning-invoice
|
.\" Title: lightning-invoice
|
||||||
.\" Author: [see the "AUTHOR" section]
|
.\" Author: [see the "AUTHOR" section]
|
||||||
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
|
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
|
||||||
.\" Date: 03/28/2018
|
.\" Date: 04/06/2018
|
||||||
.\" Manual: \ \&
|
.\" Manual: \ \&
|
||||||
.\" Source: \ \&
|
.\" Source: \ \&
|
||||||
.\" Language: English
|
.\" Language: English
|
||||||
.\"
|
.\"
|
||||||
.TH "LIGHTNING\-INVOICE" "7" "03/28/2018" "\ \&" "\ \&"
|
.TH "LIGHTNING\-INVOICE" "7" "04/06/2018" "\ \&" "\ \&"
|
||||||
.\" -----------------------------------------------------------------
|
.\" -----------------------------------------------------------------
|
||||||
.\" * Define some portability stuff
|
.\" * Define some portability stuff
|
||||||
.\" -----------------------------------------------------------------
|
.\" -----------------------------------------------------------------
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
lightning-invoice \- Protocol for accepting payments\&.
|
lightning-invoice \- Protocol for accepting payments\&.
|
||||||
.SH "SYNOPSIS"
|
.SH "SYNOPSIS"
|
||||||
.sp
|
.sp
|
||||||
\fBinvoice\fR \fImsatoshi\fR \fIlabel\fR \fIdescription\fR [\fIexpiry\fR]
|
\fBinvoice\fR \fImsatoshi\fR \fIlabel\fR \fIdescription\fR [\fIexpiry\fR] [\fIfallbacks\fR]
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
.sp
|
.sp
|
||||||
The \fBinvoice\fR RPC command creates the expectation of a payment of a given amount of milli\-satoshi: it returns a unique token which another lightning daemon can use to pay this invoice\&.
|
The \fBinvoice\fR RPC command creates the expectation of a payment of a given amount of milli\-satoshi: it returns a unique token which another lightning daemon can use to pay this invoice\&.
|
||||||
@@ -43,6 +43,8 @@ The \fIlabel\fR must be a unique string or number (which is treated as a string,
|
|||||||
The \fIdescription\fR is a short description of purpose of payment, e\&.g\&. \fI1 cup of coffee\fR\&. This value is encoded into the BOLT11 invoice and is viewable by any node you send this invoice to\&. It must be UTF\-8, and cannot use \fI\eu\fR JSON escape codes\&.
|
The \fIdescription\fR is a short description of purpose of payment, e\&.g\&. \fI1 cup of coffee\fR\&. This value is encoded into the BOLT11 invoice and is viewable by any node you send this invoice to\&. It must be UTF\-8, and cannot use \fI\eu\fR JSON escape codes\&.
|
||||||
.sp
|
.sp
|
||||||
The \fIexpiry\fR is optionally the number of seconds the invoice is valid for\&. If no value is provided the default of 3600 (1 Hour) is used\&.
|
The \fIexpiry\fR is optionally the number of seconds the invoice is valid for\&. If no value is provided the default of 3600 (1 Hour) is used\&.
|
||||||
|
.sp
|
||||||
|
The \fIfallbacks\fR array is one or more fallback addresses to include in the invoice (in order from most\-preferred to least): note that these arrays are not currently tracked to fulfill the invoice\&.
|
||||||
.SH "RETURN VALUE"
|
.SH "RETURN VALUE"
|
||||||
.sp
|
.sp
|
||||||
On success, a hash is returned as \fIpayment_hash\fR to be given to the payer, and the \fIexpiry_time\fR as a UNIX timestamp\&. It also returns a BOLT11 invoice as \fIbolt11\fR to be given to the payer\&. On failure, an error is returned and no invoice is created\&. If the lightning process fails before responding, the caller should use lightning\-listinvoice(7) to query whether this invoice was created or not\&.
|
On success, a hash is returned as \fIpayment_hash\fR to be given to the payer, and the \fIexpiry_time\fR as a UNIX timestamp\&. It also returns a BOLT11 invoice as \fIbolt11\fR to be given to the payer\&. On failure, an error is returned and no invoice is created\&. If the lightning process fails before responding, the caller should use lightning\-listinvoice(7) to query whether this invoice was created or not\&.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ lightning-invoice - Protocol for accepting payments.
|
|||||||
|
|
||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
--------
|
--------
|
||||||
*invoice* 'msatoshi' 'label' 'description' ['expiry']
|
*invoice* 'msatoshi' 'label' 'description' ['expiry'] ['fallbacks']
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
@@ -32,6 +32,10 @@ UTF-8, and cannot use '\u' JSON escape codes.
|
|||||||
The 'expiry' is optionally the number of seconds the invoice is valid for.
|
The 'expiry' is optionally the number of seconds the invoice is valid for.
|
||||||
If no value is provided the default of 3600 (1 Hour) is used.
|
If no value is provided the default of 3600 (1 Hour) is used.
|
||||||
|
|
||||||
|
The 'fallbacks' array is one or more fallback addresses to include in
|
||||||
|
the invoice (in order from most-preferred to least): note that these
|
||||||
|
arrays are not currently tracked to fulfill the invoice.
|
||||||
|
|
||||||
RETURN VALUE
|
RETURN VALUE
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
@@ -127,21 +127,43 @@ static struct json_escaped *json_tok_label(const tal_t *ctx,
|
|||||||
tok->end - tok->start);
|
tok->end - tok->start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool parse_fallback(struct command *cmd,
|
||||||
|
const char *buffer, const jsmntok_t *fallback,
|
||||||
|
const u8 **fallback_script)
|
||||||
|
|
||||||
|
{
|
||||||
|
enum address_parse_result fallback_parse;
|
||||||
|
|
||||||
|
fallback_parse
|
||||||
|
= json_tok_address_scriptpubkey(cmd,
|
||||||
|
get_chainparams(cmd->ld),
|
||||||
|
buffer, fallback,
|
||||||
|
fallback_script);
|
||||||
|
if (fallback_parse == ADDRESS_PARSE_UNRECOGNIZED) {
|
||||||
|
command_fail(cmd, "Fallback address not valid");
|
||||||
|
return false;
|
||||||
|
} else if (fallback_parse == ADDRESS_PARSE_WRONG_NETWORK) {
|
||||||
|
command_fail(cmd, "Fallback address does not match our network %s",
|
||||||
|
get_chainparams(cmd->ld)->network_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void json_invoice(struct command *cmd,
|
static void json_invoice(struct command *cmd,
|
||||||
const char *buffer, const jsmntok_t *params)
|
const char *buffer, const jsmntok_t *params)
|
||||||
{
|
{
|
||||||
struct invoice invoice;
|
struct invoice invoice;
|
||||||
struct invoice_details details;
|
struct invoice_details details;
|
||||||
jsmntok_t *msatoshi, *label, *desctok, *exp, *fallback;
|
jsmntok_t *msatoshi, *label, *desctok, *exp, *fallback, *fallbacks;
|
||||||
u64 *msatoshi_val;
|
u64 *msatoshi_val;
|
||||||
const struct json_escaped *label_val, *desc;
|
const struct json_escaped *label_val, *desc;
|
||||||
const char *desc_val;
|
const char *desc_val;
|
||||||
enum address_parse_result fallback_parse;
|
|
||||||
struct json_result *response = new_json_result(cmd);
|
struct json_result *response = new_json_result(cmd);
|
||||||
struct wallet *wallet = cmd->ld->wallet;
|
struct wallet *wallet = cmd->ld->wallet;
|
||||||
struct bolt11 *b11;
|
struct bolt11 *b11;
|
||||||
char *b11enc;
|
char *b11enc;
|
||||||
const u8 *fallback_script;
|
const u8 **fallback_scripts = NULL;
|
||||||
u64 expiry = 3600;
|
u64 expiry = 3600;
|
||||||
bool result;
|
bool result;
|
||||||
|
|
||||||
@@ -151,6 +173,7 @@ static void json_invoice(struct command *cmd,
|
|||||||
"description", &desctok,
|
"description", &desctok,
|
||||||
"?expiry", &exp,
|
"?expiry", &exp,
|
||||||
"?fallback", &fallback,
|
"?fallback", &fallback,
|
||||||
|
"?fallbacks", &fallbacks,
|
||||||
NULL)) {
|
NULL)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -219,21 +242,40 @@ static void json_invoice(struct command *cmd,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fallback address */
|
/* fallback addresses */
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
fallback_parse
|
if (deprecated_apis) {
|
||||||
= json_tok_address_scriptpubkey(cmd,
|
fallback_scripts = tal_arr(cmd, const u8 *, 1);
|
||||||
get_chainparams(cmd->ld),
|
if (!parse_fallback(cmd, buffer, fallback,
|
||||||
buffer, fallback,
|
&fallback_scripts[0]))
|
||||||
&fallback_script);
|
return;
|
||||||
if (fallback_parse == ADDRESS_PARSE_UNRECOGNIZED) {
|
} else {
|
||||||
command_fail(cmd, "Fallback address not valid");
|
command_fail(cmd, "fallback is deprecated: use fallbacks");
|
||||||
return;
|
return;
|
||||||
} else if (fallback_parse == ADDRESS_PARSE_WRONG_NETWORK) {
|
}
|
||||||
command_fail(cmd, "Fallback address does not match our network %s",
|
}
|
||||||
get_chainparams(cmd->ld)->network_name);
|
|
||||||
|
if (fallbacks) {
|
||||||
|
const jsmntok_t *i, *end = json_next(fallbacks);
|
||||||
|
size_t n = 0;
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
command_fail(cmd, "Cannot use fallback and fallbacks");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fallbacks->type != JSMN_ARRAY) {
|
||||||
|
command_fail(cmd, "fallback must be an array");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fallback_scripts = tal_arr(cmd, const u8 *, n);
|
||||||
|
for (i = fallbacks + 1; i < end; i = json_next(i)) {
|
||||||
|
tal_resize(&fallback_scripts, n+1);
|
||||||
|
if (!parse_fallback(cmd, buffer, i,
|
||||||
|
&fallback_scripts[n]))
|
||||||
|
return;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct preimage r;
|
struct preimage r;
|
||||||
@@ -253,8 +295,8 @@ static void json_invoice(struct command *cmd,
|
|||||||
b11->expiry = expiry;
|
b11->expiry = expiry;
|
||||||
b11->description = tal_steal(b11, desc_val);
|
b11->description = tal_steal(b11, desc_val);
|
||||||
b11->description_hash = NULL;
|
b11->description_hash = NULL;
|
||||||
if (fallback)
|
if (fallback_scripts)
|
||||||
b11->fallback = tal_steal(b11, fallback_script);
|
b11->fallbacks = tal_steal(b11, fallback_scripts);
|
||||||
|
|
||||||
/* FIXME: add private routes if necessary! */
|
/* FIXME: add private routes if necessary! */
|
||||||
b11enc = bolt11_encode(cmd, b11, false, hsm_sign_b11, cmd->ld);
|
b11enc = bolt11_encode(cmd, b11, false, hsm_sign_b11, cmd->ld);
|
||||||
@@ -623,6 +665,41 @@ static const struct json_command waitinvoice_command = {
|
|||||||
};
|
};
|
||||||
AUTODATA(json_command, &waitinvoice_command);
|
AUTODATA(json_command, &waitinvoice_command);
|
||||||
|
|
||||||
|
static void json_add_fallback(struct json_result *response,
|
||||||
|
const char *fieldname,
|
||||||
|
const u8 *fallback,
|
||||||
|
const struct chainparams *chain)
|
||||||
|
{
|
||||||
|
struct bitcoin_address pkh;
|
||||||
|
struct ripemd160 sh;
|
||||||
|
struct sha256 wsh;
|
||||||
|
|
||||||
|
json_object_start(response, fieldname);
|
||||||
|
if (is_p2pkh(fallback, &pkh)) {
|
||||||
|
json_add_string(response, "type", "P2PKH");
|
||||||
|
json_add_string(response, "addr",
|
||||||
|
bitcoin_to_base58(tmpctx, chain->testnet, &pkh));
|
||||||
|
} else if (is_p2sh(fallback, &sh)) {
|
||||||
|
json_add_string(response, "type", "P2SH");
|
||||||
|
json_add_string(response, "addr",
|
||||||
|
p2sh_to_base58(tmpctx, chain->testnet, &sh));
|
||||||
|
} else if (is_p2wpkh(fallback, &pkh)) {
|
||||||
|
char out[73 + strlen(chain->bip173_name)];
|
||||||
|
json_add_string(response, "type", "P2WPKH");
|
||||||
|
if (segwit_addr_encode(out, chain->bip173_name, 0,
|
||||||
|
(const u8 *)&pkh, sizeof(pkh)))
|
||||||
|
json_add_string(response, "addr", out);
|
||||||
|
} else if (is_p2wsh(fallback, &wsh)) {
|
||||||
|
char out[73 + strlen(chain->bip173_name)];
|
||||||
|
json_add_string(response, "type", "P2WSH");
|
||||||
|
if (segwit_addr_encode(out, chain->bip173_name, 0,
|
||||||
|
(const u8 *)&wsh, sizeof(wsh)))
|
||||||
|
json_add_string(response, "addr", out);
|
||||||
|
}
|
||||||
|
json_add_hex(response, "hex", fallback, tal_len(fallback));
|
||||||
|
json_object_end(response);
|
||||||
|
}
|
||||||
|
|
||||||
static void json_decodepay(struct command *cmd,
|
static void json_decodepay(struct command *cmd,
|
||||||
const char *buffer, const jsmntok_t *params)
|
const char *buffer, const jsmntok_t *params)
|
||||||
{
|
{
|
||||||
@@ -675,40 +752,15 @@ static void json_decodepay(struct command *cmd,
|
|||||||
sizeof(*b11->description_hash));
|
sizeof(*b11->description_hash));
|
||||||
json_add_num(response, "min_final_cltv_expiry",
|
json_add_num(response, "min_final_cltv_expiry",
|
||||||
b11->min_final_cltv_expiry);
|
b11->min_final_cltv_expiry);
|
||||||
if (tal_len(b11->fallback)) {
|
if (tal_count(b11->fallbacks)) {
|
||||||
struct bitcoin_address pkh;
|
if (deprecated_apis)
|
||||||
struct ripemd160 sh;
|
json_add_fallback(response, "fallback",
|
||||||
struct sha256 wsh;
|
b11->fallbacks[0], b11->chain);
|
||||||
|
json_array_start(response, "fallbacks");
|
||||||
json_object_start(response, "fallback");
|
for (size_t i = 0; i < tal_count(b11->fallbacks); i++)
|
||||||
if (is_p2pkh(b11->fallback, &pkh)) {
|
json_add_fallback(response, NULL,
|
||||||
json_add_string(response, "type", "P2PKH");
|
b11->fallbacks[i], b11->chain);
|
||||||
json_add_string(response, "addr",
|
json_array_end(response);
|
||||||
bitcoin_to_base58(cmd,
|
|
||||||
b11->chain->testnet,
|
|
||||||
&pkh));
|
|
||||||
} else if (is_p2sh(b11->fallback, &sh)) {
|
|
||||||
json_add_string(response, "type", "P2SH");
|
|
||||||
json_add_string(response, "addr",
|
|
||||||
p2sh_to_base58(cmd,
|
|
||||||
b11->chain->testnet,
|
|
||||||
&sh));
|
|
||||||
} else if (is_p2wpkh(b11->fallback, &pkh)) {
|
|
||||||
char out[73 + strlen(b11->chain->bip173_name)];
|
|
||||||
json_add_string(response, "type", "P2WPKH");
|
|
||||||
if (segwit_addr_encode(out, b11->chain->bip173_name, 0,
|
|
||||||
(const u8 *)&pkh, sizeof(pkh)))
|
|
||||||
json_add_string(response, "addr", out);
|
|
||||||
} else if (is_p2wsh(b11->fallback, &wsh)) {
|
|
||||||
char out[73 + strlen(b11->chain->bip173_name)];
|
|
||||||
json_add_string(response, "type", "P2WSH");
|
|
||||||
if (segwit_addr_encode(out, b11->chain->bip173_name, 0,
|
|
||||||
(const u8 *)&wsh, sizeof(wsh)))
|
|
||||||
json_add_string(response, "addr", out);
|
|
||||||
}
|
|
||||||
json_add_hex(response, "hex",
|
|
||||||
b11->fallback, tal_len(b11->fallback));
|
|
||||||
json_object_end(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tal_count(b11->routes)) {
|
if (tal_count(b11->routes)) {
|
||||||
|
|||||||
@@ -429,9 +429,10 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
l1 = self.node_factory.get_node()
|
l1 = self.node_factory.get_node()
|
||||||
l2 = self.node_factory.get_node()
|
l2 = self.node_factory.get_node()
|
||||||
|
|
||||||
addr = l2.rpc.newaddr('bech32')['address']
|
addr1 = l2.rpc.newaddr('bech32')['address']
|
||||||
|
addr2 = l2.rpc.newaddr('p2sh-segwit')['address']
|
||||||
before = int(time.time())
|
before = int(time.time())
|
||||||
inv = l1.rpc.invoice(123000, 'label', 'description', '3700', addr)
|
inv = l1.rpc.invoice(123000, 'label', 'description', '3700', [addr1, addr2])
|
||||||
after = int(time.time())
|
after = int(time.time())
|
||||||
b11 = l1.rpc.decodepay(inv['bolt11'])
|
b11 = l1.rpc.decodepay(inv['bolt11'])
|
||||||
assert b11['currency'] == 'bcrt'
|
assert b11['currency'] == 'bcrt'
|
||||||
@@ -441,8 +442,11 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
assert b11['description'] == 'description'
|
assert b11['description'] == 'description'
|
||||||
assert b11['expiry'] == 3700
|
assert b11['expiry'] == 3700
|
||||||
assert b11['payee'] == l1.info['id']
|
assert b11['payee'] == l1.info['id']
|
||||||
assert b11['fallback']['addr'] == addr
|
assert len(b11['fallbacks']) == 2
|
||||||
assert b11['fallback']['type'] == 'P2WPKH'
|
assert b11['fallbacks'][0]['addr'] == addr1
|
||||||
|
assert b11['fallbacks'][0]['type'] == 'P2WPKH'
|
||||||
|
assert b11['fallbacks'][1]['addr'] == addr2
|
||||||
|
assert b11['fallbacks'][1]['type'] == 'P2SH'
|
||||||
|
|
||||||
# Check pay_index is null
|
# Check pay_index is null
|
||||||
outputs = l1.db_query('SELECT pay_index IS NULL AS q FROM invoices WHERE label="label";')
|
outputs = l1.db_query('SELECT pay_index IS NULL AS q FROM invoices WHERE label="label";')
|
||||||
@@ -721,8 +725,8 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
||||||
assert b11['expiry'] == 3600
|
assert b11['expiry'] == 3600
|
||||||
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
||||||
assert b11['fallback']['type'] == 'P2PKH'
|
assert b11['fallbacks'][0]['type'] == 'P2PKH'
|
||||||
assert b11['fallback']['addr'] == 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'
|
assert b11['fallbacks'][0]['addr'] == 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'
|
||||||
|
|
||||||
# > ### On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255
|
# > ### On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255
|
||||||
# > lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj
|
# > lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj
|
||||||
@@ -751,8 +755,8 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
||||||
assert b11['expiry'] == 3600
|
assert b11['expiry'] == 3600
|
||||||
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
||||||
assert b11['fallback']['type'] == 'P2PKH'
|
assert b11['fallbacks'][0]['type'] == 'P2PKH'
|
||||||
assert b11['fallback']['addr'] == '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'
|
assert b11['fallbacks'][0]['addr'] == '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'
|
||||||
assert len(b11['routes']) == 1
|
assert len(b11['routes']) == 1
|
||||||
assert len(b11['routes'][0]) == 2
|
assert len(b11['routes'][0]) == 2
|
||||||
assert b11['routes'][0][0]['pubkey'] == '029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'
|
assert b11['routes'][0][0]['pubkey'] == '029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'
|
||||||
@@ -792,8 +796,8 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
||||||
assert b11['expiry'] == 3600
|
assert b11['expiry'] == 3600
|
||||||
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
||||||
assert b11['fallback']['type'] == 'P2SH'
|
assert b11['fallbacks'][0]['type'] == 'P2SH'
|
||||||
assert b11['fallback']['addr'] == '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'
|
assert b11['fallbacks'][0]['addr'] == '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'
|
||||||
|
|
||||||
# > ### On mainnet, with fallback (P2WPKH) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
|
# > ### On mainnet, with fallback (P2WPKH) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
|
||||||
# > lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7kepvrhrm9s57hejg0p662ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfuwzam7yr8e690nd2ypcq9hlkdwdvycqa0qza8
|
# > lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7kepvrhrm9s57hejg0p662ur5j5cr03890fa7k2pypgttmh4897d3raaq85a293e9jpuqwl0rnfuwzam7yr8e690nd2ypcq9hlkdwdvycqa0qza8
|
||||||
@@ -817,8 +821,8 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
||||||
assert b11['expiry'] == 3600
|
assert b11['expiry'] == 3600
|
||||||
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
||||||
assert b11['fallback']['type'] == 'P2WPKH'
|
assert b11['fallbacks'][0]['type'] == 'P2WPKH'
|
||||||
assert b11['fallback']['addr'] == 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
|
assert b11['fallbacks'][0]['addr'] == 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
|
||||||
|
|
||||||
# > ### On mainnet, with fallback (P2WSH) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
|
# > ### On mainnet, with fallback (P2WSH) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
|
||||||
# > lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q28j0v3rwgy9pvjnd48ee2pl8xrpxysd5g44td63g6xcjcu003j3qe8878hluqlvl3km8rm92f5stamd3jw763n3hck0ct7p8wwj463cql26ava
|
# > lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q28j0v3rwgy9pvjnd48ee2pl8xrpxysd5g44td63g6xcjcu003j3qe8878hluqlvl3km8rm92f5stamd3jw763n3hck0ct7p8wwj463cql26ava
|
||||||
@@ -842,8 +846,8 @@ class LightningDTests(BaseLightningDTests):
|
|||||||
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
assert b11['payment_hash'] == '0001020304050607080900010203040506070809000102030405060708090102'
|
||||||
assert b11['expiry'] == 3600
|
assert b11['expiry'] == 3600
|
||||||
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
assert b11['payee'] == '03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'
|
||||||
assert b11['fallback']['type'] == 'P2WSH'
|
assert b11['fallbacks'][0]['type'] == 'P2WSH'
|
||||||
assert b11['fallback']['addr'] == 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'
|
assert b11['fallbacks'][0]['addr'] == 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'
|
||||||
|
|
||||||
self.assertRaises(ValueError, l1.rpc.decodepay, '1111111')
|
self.assertRaises(ValueError, l1.rpc.decodepay, '1111111')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user