Skip to content

Commit

Permalink
offer: allow re-enabling a previously disabled offer
Browse files Browse the repository at this point in the history
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: #7360
Co-Developed-by: Rusty Russell <[email protected]>
Signed-off-by: Vincenzo Palazzo <[email protected]>
  • Loading branch information
vincenzopalazzo authored and rustyrussell committed Aug 11, 2024
1 parent 47e7127 commit 1e1edfd
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 0 deletions.
107 changes: 107 additions & 0 deletions contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10757,6 +10757,7 @@
"Rusty Russell <<[email protected]>> is mainly responsible."
],
"see_also": [
"lightning-enableoffer(7)",
"lightning-offer(7)",
"lightning-listoffers(7)"
],
Expand Down Expand Up @@ -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 <<[email protected]>> 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",
Expand Down
1 change: 1 addition & 0 deletions doc/schemas/lightning-disableoffer.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"Rusty Russell <<[email protected]>> is mainly responsible."
],
"see_also": [
"lightning-enableoffer(7)",
"lightning-offer(7)",
"lightning-listoffers(7)"
],
Expand Down
106 changes: 106 additions & 0 deletions doc/schemas/lightning-enableoffer.json
Original file line number Diff line number Diff line change
@@ -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 <<[email protected]>> 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
}
}
]
}
42 changes: 42 additions & 0 deletions lightningd/offer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions tests/test_pay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
14 changes: 14 additions & 0 deletions wallet/wallet.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1e1edfd

Please sign in to comment.