offer / offerout: return existing if its still active.

As requested by @shesek: it's weird to fail if they ask for the exact
same thing (which is quite possible, since offers don't expire by
default).

And add a new "created" field so they can tell if they have an old
one.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2021-07-21 15:07:49 +09:30 committed by neil saitug
parent 44c469d52b
commit cb9e0268a7
9 changed files with 95 additions and 26 deletions

19
doc/lightning-offer.7 generated
View file

@ -10,9 +10,10 @@ lightning-offer - Command for accepting payments
.SH DESCRIPTION .SH DESCRIPTION
The \fBoffer\fR RPC command creates an offer, which is a precursor to The \fBoffer\fR RPC command creates an offer (or returns an existing
creating one or more invoices\. It automatically enables the processing of one), which is a precursor to creating one or more invoices\. It
an incoming invoice_request, and issuing of invoices\. automatically enables the processing of an incoming invoice_request,
and issuing of invoices\.
Note that it creates two variants of the offer: a signed and an Note that it creates two variants of the offer: a signed and an
@ -119,7 +120,9 @@ On success, an object is returned, containing:
.IP \[bu] .IP \[bu]
\fBbolt12_unsigned\fR (string): the bolt12 encoding of the offer, without a signature \fBbolt12_unsigned\fR (string): the bolt12 encoding of the offer, without a signature
.IP \[bu] .IP \[bu]
\fBused\fR (boolean): True if an associated invoice has been paid (always \fIfalse\fR) \fBused\fR (boolean): True if an associated invoice has been paid
.IP \[bu]
\fBcreated\fR (boolean): false if the offer already existed
.IP \[bu] .IP \[bu]
\fBlabel\fR (string, optional): the (optional) user-specified label \fBlabel\fR (string, optional): the (optional) user-specified label
@ -131,13 +134,17 @@ lightning process fails before responding, the caller should use
not\. not\.
If the offer already existed, and is still active, that is returned;
if it's not active then this call fails\.
The following error codes may occur: The following error codes may occur:
.RS .RS
.IP \[bu] .IP \[bu]
-1: Catchall nonspecific error\. -1: Catchall nonspecific error\.
.IP \[bu] .IP \[bu]
1000: Offer with this offer_id already exists\. 1000: Offer with this offer_id already exists (but is not active)\.
.RE .RE
.SH AUTHOR .SH AUTHOR
@ -152,4 +159,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:f7faf86c7cb052200c3b4d6e3d6209afa54600cc7a33e1082583a2766d02a275 \" SHA256STAMP:bd1bae6f6b56d4efa95e17b7aff042b897acacdd783cef7df0dd729f6c9b5d8f

View file

@ -11,9 +11,10 @@ SYNOPSIS
DESCRIPTION DESCRIPTION
----------- -----------
The **offer** RPC command creates an offer, which is a precursor to The **offer** RPC command creates an offer (or returns an existing
creating one or more invoices. It automatically enables the processing of one), which is a precursor to creating one or more invoices. It
an incoming invoice_request, and issuing of invoices. automatically enables the processing of an incoming invoice_request,
and issuing of invoices.
Note that it creates two variants of the offer: a signed and an Note that it creates two variants of the offer: a signed and an
unsigned one (which is smaller). Wallets should accept both: the unsigned one (which is smaller). Wallets should accept both: the
@ -100,7 +101,8 @@ On success, an object is returned, containing:
- **single_use** (boolean): whether this expires as soon as it's paid (reflects the *single_use* parameter) - **single_use** (boolean): whether this expires as soon as it's paid (reflects the *single_use* parameter)
- **bolt12** (string): the bolt12 encoding of the offer - **bolt12** (string): the bolt12 encoding of the offer
- **bolt12_unsigned** (string): the bolt12 encoding of the offer, without a signature - **bolt12_unsigned** (string): the bolt12 encoding of the offer, without a signature
- **used** (boolean): True if an associated invoice has been paid (always *false*) - **used** (boolean): True if an associated invoice has been paid
- **created** (boolean): false if the offer already existed
- **label** (string, optional): the (optional) user-specified label - **label** (string, optional): the (optional) user-specified label
[comment]: # (GENERATE-FROM-SCHEMA-END) [comment]: # (GENERATE-FROM-SCHEMA-END)
@ -109,9 +111,12 @@ lightning process fails before responding, the caller should use
lightning-listoffers(7) to query whether this offer was created or lightning-listoffers(7) to query whether this offer was created or
not. not.
If the offer already existed, and is still active, that is returned;
if it's not active then this call fails.
The following error codes may occur: The following error codes may occur:
- -1: Catchall nonspecific error. - -1: Catchall nonspecific error.
- 1000: Offer with this offer_id already exists. - 1000: Offer with this offer_id already exists (but is not active).
AUTHOR AUTHOR
------ ------
@ -128,4 +133,4 @@ RESOURCES
Main web site: <https://github.com/ElementsProject/lightning> Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:0f9cfd3cc68aaba20af0eee763c93b475016619d960e3f5bbc0b762a809f0fef) [comment]: # ( SHA256STAMP:53f1460e28d45129b2c00b7116c87eb47a5b717f7912f499e864f4aa28b320fa)

View file

@ -73,6 +73,8 @@ On success, an object is returned, containing:
.IP \[bu] .IP \[bu]
\fBused\fR (boolean): True if an incoming invoice has been paid (always \fIfalse\fR) \fBused\fR (boolean): True if an incoming invoice has been paid (always \fIfalse\fR)
.IP \[bu] .IP \[bu]
\fBcreated\fR (boolean): false if the offer already existed
.IP \[bu]
\fBlabel\fR (string, optional): the (optional) user-specified label \fBlabel\fR (string, optional): the (optional) user-specified label
.RE .RE
@ -113,4 +115,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:3de11c6de7905322d9ef748981fc1d4f9ca91f4be46d76af6e9124572853047d \" SHA256STAMP:4998ef3e06f37823c969a524fc3deb96b58eb2babee560df27e3f006fc535186

View file

@ -61,6 +61,7 @@ On success, an object is returned, containing:
- **bolt12** (string): the bolt12 encoding of the offer - **bolt12** (string): the bolt12 encoding of the offer
- **bolt12_unsigned** (string): the bolt12 encoding of the offer, without a signature - **bolt12_unsigned** (string): the bolt12 encoding of the offer, without a signature
- **used** (boolean): True if an incoming invoice has been paid (always *false*) - **used** (boolean): True if an incoming invoice has been paid (always *false*)
- **created** (boolean): false if the offer already existed
- **label** (string, optional): the (optional) user-specified label - **label** (string, optional): the (optional) user-specified label
[comment]: # (GENERATE-FROM-SCHEMA-END) [comment]: # (GENERATE-FROM-SCHEMA-END)
@ -97,4 +98,4 @@ RESOURCES
Main web site: <https://github.com/ElementsProject/lightning> Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:2b7e7b543a88a10dbfbca2508e034af79f43ed0845abdb9df1fdf7e28ee33c26) [comment]: # ( SHA256STAMP:14fada9336956a08b6d55c4ce01fcb62726cbdef9a065f1966335f61c4e91ce5)

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used" ], "required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used", "created" ],
"properties": { "properties": {
"offer_id": { "offer_id": {
"type": "hex", "type": "hex",
@ -29,9 +29,12 @@
}, },
"used": { "used": {
"type": "boolean", "type": "boolean",
"enum": [ false ],
"description": "True if an associated invoice has been paid" "description": "True if an associated invoice has been paid"
}, },
"created": {
"type": "boolean",
"description": "false if the offer already existed"
},
"label": { "label": {
"type": "string", "type": "string",
"description": "the (optional) user-specified label" "description": "the (optional) user-specified label"

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used" ], "required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used", "created" ],
"properties": { "properties": {
"offer_id": { "offer_id": {
"type": "hex", "type": "hex",
@ -33,6 +33,10 @@
"enum": [ false ], "enum": [ false ],
"description": "True if an incoming invoice has been paid" "description": "True if an incoming invoice has been paid"
}, },
"created": {
"type": "boolean",
"description": "false if the offer already existed"
},
"label": { "label": {
"type": "string", "type": "string",
"description": "the (optional) user-specified label" "description": "the (optional) user-specified label"

View file

@ -1,3 +1,4 @@
#include <ccan/cast/cast.h>
#include <common/bolt12.h> #include <common/bolt12.h>
#include <common/bolt12_merkle.h> #include <common/bolt12_merkle.h>
#include <common/configdir.h> #include <common/configdir.h>
@ -91,6 +92,7 @@ static struct command_result *json_createoffer(struct command *cmd,
bool *single_use; bool *single_use;
enum offer_status status; enum offer_status status;
struct pubkey32 key; struct pubkey32 key;
bool created;
if (!param(cmd, buffer, params, if (!param(cmd, buffer, params,
p_req("bolt12", param_b12_offer, &offer), p_req("bolt12", param_b12_offer, &offer),
@ -110,17 +112,28 @@ static struct command_result *json_createoffer(struct command *cmd,
hsm_sign_b12(cmd->ld, "offer", "signature", &merkle, NULL, &key, hsm_sign_b12(cmd->ld, "offer", "signature", &merkle, NULL, &key,
offer->signature); offer->signature);
b12str = offer_encode(cmd, offer); b12str = offer_encode(cmd, offer);
if (!wallet_offer_create(cmd->ld->wallet, &merkle, b12str, label,
status)) { /* If it already exists, we use that one instead (and then
return command_fail(cmd, * the offer plugin will complain if it's inactive or expired) */
OFFER_ALREADY_EXISTS, if (!wallet_offer_create(cmd->ld->wallet, &merkle,
"Duplicate offer"); b12str, label, status)) {
} if (!wallet_offer_find(cmd, cmd->ld->wallet, &merkle,
cast_const2(const struct json_escape **,
&label),
&status)) {
return command_fail(cmd, LIGHTNINGD,
"Could not create, nor find offer");
}
created = false;
} else
created = true;
offer->signature = tal_free(offer->signature); offer->signature = tal_free(offer->signature);
b12str_nosig = offer_encode(cmd, offer); b12str_nosig = offer_encode(cmd, offer);
response = json_stream_success(cmd); response = json_stream_success(cmd);
json_populate_offer(response, &merkle, b12str, b12str_nosig, label, status); json_populate_offer(response, &merkle, b12str, b12str_nosig, label, status);
json_add_bool(response, "created", created);
return command_success(cmd, response); return command_success(cmd, response);
} }

View file

@ -255,6 +255,31 @@ struct offer_info {
bool *single_use; bool *single_use;
}; };
static struct command_result *check_result(struct command *cmd,
const char *buf,
const jsmntok_t *result,
void *arg UNNEEDED)
{
bool active;
/* If it's inactive, we can't return it, */
if (!json_to_bool(buf, json_get_member(buf, result, "active"),
&active)) {
return command_fail(cmd,
LIGHTNINGD,
"Bad creaoffer status reply %.*s",
json_tok_full_len(result),
json_tok_full(buf, result));
}
if (!active)
return command_fail(cmd,
OFFER_ALREADY_EXISTS,
"Offer already exists, but isn't active");
/* Otherwise, push through the result. */
return forward_result(cmd, buf, result, arg);
}
static struct command_result *create_offer(struct command *cmd, static struct command_result *create_offer(struct command *cmd,
struct offer_info *offinfo) struct offer_info *offinfo)
{ {
@ -262,7 +287,7 @@ static struct command_result *create_offer(struct command *cmd,
/* We simply pass this through. */ /* We simply pass this through. */
req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer", req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer",
forward_result, forward_error, check_result, forward_error,
offinfo); offinfo);
json_add_string(req->js, "bolt12", json_add_string(req->js, "bolt12",
offer_encode(tmpctx, offinfo->offer)); offer_encode(tmpctx, offinfo->offer));
@ -436,9 +461,8 @@ struct command_result *json_offerout(struct command *cmd,
offer->node_id = tal_dup(offer, struct pubkey32, &id); offer->node_id = tal_dup(offer, struct pubkey32, &id);
/* We simply pass this through. */
req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer", req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer",
forward_result, forward_error, check_result, forward_error,
offer); offer);
json_add_string(req->js, "bolt12", offer_encode(tmpctx, offer)); json_add_string(req->js, "bolt12", offer_encode(tmpctx, offer));
if (label) if (label)

View file

@ -4122,6 +4122,7 @@ def test_fetchinvoice(node_factory, bitcoind):
# Simple offer first. # Simple offer first.
offer1 = l3.rpc.call('offer', {'amount': '2msat', offer1 = l3.rpc.call('offer', {'amount': '2msat',
'description': 'simple test'}) 'description': 'simple test'})
assert offer1['created'] is True
inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12']}) inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12']})
inv2 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12_unsigned'], inv2 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12_unsigned'],
@ -4274,6 +4275,15 @@ def test_fetchinvoice(node_factory, bitcoind):
# But we can still pay the (already-converted) invoice. # But we can still pay the (already-converted) invoice.
l1.rpc.pay(inv['invoice']) l1.rpc.pay(inv['invoice'])
# Identical creation gives it again, just with created false.
offer1 = l3.rpc.call('offer', {'amount': '2msat',
'description': 'simple test'})
assert offer1['created'] is False
l3.rpc.call('disableoffer', {'offer_id': offer1['offer_id']})
with pytest.raises(RpcError, match="1000.*Offer already exists, but isn't active"):
l3.rpc.call('offer', {'amount': '2msat',
'description': 'simple test'})
# Test timeout. # Test timeout.
l3.stop() l3.stop()
with pytest.raises(RpcError, match='Timeout waiting for response'): with pytest.raises(RpcError, match='Timeout waiting for response'):