offer: allow re-enabling a previously disabled offer

Sometimes, for various reasons, a user disables an offer
and then wants to re-enable it. This should be allowed because,
from the CLN point of view, it is just an internal state.

If a user has constraints on the description of the invoice
because they are using services that link some sort of user ID
to an offer, it is important for the user to be able to re-enable the
offer, not create a new one. Creating a new offer would
require a different description.

Link: https://github.com/ElementsProject/lightning/issues/7360
Co-Developed-by: Rusty Russell <rusty@rustcorp.com.au>
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
This commit is contained in:
Vincenzo Palazzo 2024-06-02 11:38:12 +02:00 committed by Rusty Russell
parent 47e7127b19
commit 1e1edfd073
7 changed files with 312 additions and 0 deletions

View File

@ -10757,6 +10757,7 @@
"Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible."
],
"see_also": [
"lightning-enableoffer(7)",
"lightning-offer(7)",
"lightning-listoffers(7)"
],
@ -10921,6 +10922,112 @@
}
]
},
"lightning-enableoffer.json": {
"$schema": "../rpc-schema-draft.json",
"type": "object",
"additionalProperties": false,
"rpc": "disableoffer",
"title": "Command for re-enabling an offer",
"warning": "experimental-offers only",
"description": [
"The **enableoffer** RPC command enables an offer, after it has been disabled."
],
"request": {
"required": [
"offer_id"
],
"properties": {
"offer_id": {
"type": "hash",
"description": [
"The id we use to identify this offer."
]
}
}
},
"response": {
"required": [
"offer_id",
"active",
"single_use",
"bolt12",
"used"
],
"properties": {
"offer_id": {
"type": "hash",
"description": [
"The merkle hash of the offer."
]
},
"active": {
"type": "boolean",
"enum": [
true
],
"description": [
"Whether the offer can produce invoices/payments."
]
},
"single_use": {
"type": "boolean",
"description": [
"Whether the offer is disabled after first successful use."
]
},
"bolt12": {
"type": "string",
"description": [
"The bolt12 string representing this offer."
]
},
"used": {
"type": "boolean",
"description": [
"Whether the offer has had an invoice paid / payment made."
]
},
"label": {
"type": "string",
"description": [
"The label provided when offer was created."
]
}
},
"pre_return_value_notes": [
"Note: the returned object is the same format as **listoffers**."
]
},
"author": [
"Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible."
],
"see_also": [
"lightning-offer(7)",
"lightning-disableoffer(7)",
"lightning-listoffers(7)"
],
"resources": [
"Main web site: <https://github.com/ElementsProject/lightning>"
],
"examples": [
{
"request": {
"id": "example:enableoffer#1",
"method": "enableoffer",
"params": {
"offer_id": "713a16ccd4eb10438bdcfbc2c8276be301020dd9d489c530773ba64f3b33307d"
}
},
"response": {
"offer_id": "053a5c566fbea2681a5ff9c05a913da23e45b95d09ef5bd25d7d408f23da7084",
"active": true,
"single_use": false,
"bolt12": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqvqcdgq2z9pk7enxv4jjqen0wgs8yatnw3ujz83qkc6rvp4j28rt3dtrn32zkvdy7efhnlrpr5rp5geqxs783wtlj550qs8czzku4nk3pqp6m593qxgunzuqcwkmgqkmp6ty0wyvjcqdguv3pnpukedwn6cr87m89t74h3auyaeg89xkvgzpac70z3m9rn5xzu28c",
"used": false
}
}
]
},
"lightning-feerates.json": {
"$schema": "../rpc-schema-draft.json",
"type": "object",

View File

@ -80,6 +80,7 @@
"Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible."
],
"see_also": [
"lightning-enableoffer(7)",
"lightning-offer(7)",
"lightning-listoffers(7)"
],

View File

@ -0,0 +1,106 @@
{
"$schema": "../rpc-schema-draft.json",
"type": "object",
"additionalProperties": false,
"rpc": "disableoffer",
"title": "Command for re-enabling an offer",
"warning": "experimental-offers only",
"description": [
"The **enableoffer** RPC command enables an offer, after it has been disabled."
],
"request": {
"required": [
"offer_id"
],
"properties": {
"offer_id": {
"type": "hash",
"description": [
"The id we use to identify this offer."
]
}
}
},
"response": {
"required": [
"offer_id",
"active",
"single_use",
"bolt12",
"used"
],
"properties": {
"offer_id": {
"type": "hash",
"description": [
"The merkle hash of the offer."
]
},
"active": {
"type": "boolean",
"enum": [
true
],
"description": [
"Whether the offer can produce invoices/payments."
]
},
"single_use": {
"type": "boolean",
"description": [
"Whether the offer is disabled after first successful use."
]
},
"bolt12": {
"type": "string",
"description": [
"The bolt12 string representing this offer."
]
},
"used": {
"type": "boolean",
"description": [
"Whether the offer has had an invoice paid / payment made."
]
},
"label": {
"type": "string",
"description": [
"The label provided when offer was created."
]
}
},
"pre_return_value_notes": [
"Note: the returned object is the same format as **listoffers**."
]
},
"author": [
"Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible."
],
"see_also": [
"lightning-offer(7)",
"lightning-disableoffer(7)",
"lightning-listoffers(7)"
],
"resources": [
"Main web site: <https://github.com/ElementsProject/lightning>"
],
"examples": [
{
"request": {
"id": "example:enableoffer#1",
"method": "enableoffer",
"params": {
"offer_id": "713a16ccd4eb10438bdcfbc2c8276be301020dd9d489c530773ba64f3b33307d"
}
},
"response": {
"offer_id": "053a5c566fbea2681a5ff9c05a913da23e45b95d09ef5bd25d7d408f23da7084",
"active": true,
"single_use": false,
"bolt12": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqvqcdgq2z9pk7enxv4jjqen0wgs8yatnw3ujz83qkc6rvp4j28rt3dtrn32zkvdy7efhnlrpr5rp5geqxs783wtlj550qs8czzku4nk3pqp6m593qxgunzuqcwkmgqkmp6ty0wyvjcqdguv3pnpukedwn6cr87m89t74h3auyaeg89xkvgzpac70z3m9rn5xzu28c",
"used": false
}
}
]
}

View File

@ -231,6 +231,48 @@ static const struct json_command disableoffer_command = {
};
AUTODATA(json_command, &disableoffer_command);
static struct command_result *json_enableoffer(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
const jsmntok_t *params)
{
struct json_stream *response;
struct sha256 *offer_id;
struct wallet *wallet = cmd->ld->wallet;
const char *b12;
const struct json_escape *label;
enum offer_status status;
if (!param_check(cmd, buffer, params,
p_req("offer_id", param_sha256, &offer_id),
NULL))
return command_param_failed();
b12 = wallet_offer_find(tmpctx, wallet, offer_id, &label, &status);
if (!b12)
return command_fail(cmd, LIGHTNINGD, "Unknown offer");
if (offer_status_active(status))
return command_fail(cmd, OFFER_ALREADY_DISABLED,
"offer already active");
if (command_check_only(cmd))
return command_check_done(cmd);
status = wallet_offer_enable(wallet, offer_id, status);
response = json_stream_success(cmd);
json_populate_offer(response, offer_id, b12, label, status);
return command_success(cmd, response);
}
static const struct json_command enableoffer_command = {
"enableoffer",
json_enableoffer,
};
AUTODATA(json_command, &enableoffer_command);
/* We do some sanity checks now, since we're looking up prev payment anyway,
* but our main purpose is to fill in prev_basetime tweak. */
static struct command_result *prev_payment(struct command *cmd,

View File

@ -5966,3 +5966,33 @@ def test_fetch_no_description_with_amount(node_factory):
err = r'description is required for the user to know what it was they paid for'
with pytest.raises(RpcError, match=err) as err:
_ = l2.rpc.call('offer', {'amount': '2msat'})
def test_enableoffer(node_factory):
l1, l2 = node_factory.line_graph(2, opts={'experimental-offers': None})
# Normal offer, works as expected
offer1 = l2.rpc.call('offer', {'amount': '2msat',
'description': 'test_disableoffer_reenable'})
assert offer1['created'] is True
l1.rpc.fetchinvoice(offer=offer1['bolt12'])
l2.rpc.disableoffer(offer_id=offer1['offer_id'])
with pytest.raises(RpcError, match="Offer no longer available"):
l1.rpc.fetchinvoice(offer=offer1['bolt12'])
with pytest.raises(RpcError, match="1000.*Already exists, but isn't active"):
l2.rpc.call('offer', {'amount': '2msat',
'description': 'test_disableoffer_reenable'})
l2.rpc.enableoffer(offer_id=offer1['offer_id'])
l1.rpc.fetchinvoice(offer=offer1['bolt12'])
# Can't enable twice.
with pytest.raises(RpcError, match="1001.*offer already active"):
l2.rpc.enableoffer(offer_id=offer1['offer_id'])
# Can't enable unknown.
with pytest.raises(RpcError, match="Unknown offer"):
l1.rpc.enableoffer(offer_id=offer1['offer_id'])

View File

@ -5569,6 +5569,20 @@ enum offer_status wallet_offer_disable(struct wallet *w,
return newstatus;
}
enum offer_status wallet_offer_enable(struct wallet *w,
const struct sha256 *offer_id,
enum offer_status s)
{
enum offer_status newstatus;
assert(!offer_status_active(s));
newstatus = offer_status_in_db(s | OFFER_STATUS_ACTIVE_F);
offer_status_update(w->db, offer_id, s, newstatus);
return newstatus;
}
void wallet_offer_mark_used(struct db *db, const struct sha256 *offer_id)
{
struct db_stmt *stmt;

View File

@ -1466,6 +1466,18 @@ enum offer_status wallet_offer_disable(struct wallet *w,
enum offer_status s)
NO_NULL_ARGS;
/**
* Enable an offer in the database.
* @w: the wallet
* @offer_id: the merkle root, as used for signing (must be unique)
* @s: the current status (must be active).
*
* Must exist. Returns new status. */
enum offer_status wallet_offer_enable(struct wallet *w,
const struct sha256 *offer_id,
enum offer_status s)
NO_NULL_ARGS;
/**
* Mark an offer in the database used.
* @w: the wallet