Browse Source

offers: convert currency when they request an invoice.

Means a reshuffle of our logic: we want to multiply by quantity before
conversion for maximum accuracy.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
ppa
Rusty Russell 4 years ago
committed by Christian Decker
parent
commit
4bb05e46e9
  1. 6
      doc/lightning-offer.7
  2. 4
      doc/lightning-offer.7.md
  3. 250
      plugins/offers_invreq_hook.c
  4. 24
      tests/test_pay.py

6
doc/lightning-offer.7

@ -24,7 +24,9 @@ places ending in \fIbtc\fR\.
\fIamount\fR can also have an ISO 4217 postfix (i\.e\. USD), in which case
currency conversion will need to be done for the invoice itself\.
currency conversion will need to be done for the invoice itself\. A
plugin is needed which provides the "currencyconvert" API for this
currency, otherwise the offer creation will fail\.
The \fIdescription\fR is a short description of purpose of the offer,
@ -147,4 +149,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:54947f0571c064b5190b672f79dd8c4b4555aad3e93007c28deab37c9a0566c1
\" SHA256STAMP:60534030c8c7ebc34b521a5bb5d76bd1d59e99ac80d16f5b0a9a3ac3bd164b48

4
doc/lightning-offer.7.md

@ -23,7 +23,9 @@ three decimal places ending in *sat*, or a number with 1 to 11 decimal
places ending in *btc*.
*amount* can also have an ISO 4217 postfix (i.e. USD), in which case
currency conversion will need to be done for the invoice itself.
currency conversion will need to be done for the invoice itself. A
plugin is needed which provides the "currencyconvert" API for this
currency, otherwise the offer creation will fail.
The *description* is a short description of purpose of the offer,
e.g. *coffee*. This value is encoded into the resulting offer and is

250
plugins/offers_invreq_hook.c

@ -4,6 +4,7 @@
#include <common/bech32_util.h>
#include <common/bolt12.h>
#include <common/bolt12_merkle.h>
#include <common/iso4217.h>
#include <common/json_stream.h>
#include <common/overflows.h>
#include <common/type_to_string.h>
@ -405,6 +406,170 @@ static bool check_recurrence_sig(const struct tlv_invoice_request *invreq,
sighash.u.u8, &payer_key->pubkey) == 1;
}
static struct command_result *invreq_amount_by_quantity(struct command *cmd,
const struct invreq *ir,
u64 *raw_amt)
{
struct command_result *err;
assert(ir->offer->amount);
/* BOLT-offers #12:
*
* - if the offer included `amount`:
* - MUST fail the request if it contains `amount`.
*/
err = invreq_must_not_have(cmd, ir, amount);
if (err)
return err;
*raw_amt = *ir->offer->amount;
/* BOLT-offers #12:
* - if request contains `quantity`, multiply by `quantity`.
*/
if (ir->invreq->quantity) {
if (mul_overflows_u64(*ir->invreq->quantity, *raw_amt)) {
return fail_invreq(cmd, ir,
"quantity %"PRIu64
" causes overflow",
*ir->invreq->quantity);
}
*raw_amt *= *ir->invreq->quantity;
}
return NULL;
}
/* The non-currency-converting case. */
static struct command_result *invreq_base_amount_simple(struct command *cmd,
const struct invreq *ir,
struct amount_msat *amt)
{
struct command_result *err;
if (ir->offer->amount) {
u64 raw_amount;
assert(!ir->offer->currency);
err = invreq_amount_by_quantity(cmd, ir, &raw_amount);
if (err)
return err;
*amt = amount_msat(raw_amount);
} else {
/* BOLT-offers #12:
*
* - otherwise:
* - MUST fail the request if it does not contain `amount`.
* - MUST use the request `amount` as the *base invoice amount*.
* (Note: invoice amount can be further modiifed by recurrence
* below)
*/
err = invreq_must_have(cmd, ir, amount);
if (err)
return err;
*amt = amount_msat(*ir->invreq->amount);
}
return NULL;
}
static struct command_result *handle_amount_and_recurrence(struct command *cmd,
struct invreq *ir,
struct amount_msat amount)
{
ir->inv->amount = tal_dup(ir->inv, u64,
&amount.millisatoshis); /* Raw: wire protocol */
/* Last of all, we handle recurrence details, which often requires
* further lookups. */
/* BOLT-offers #12:
* - MUST set (or not set) `recurrence_counter` exactly as the
* invoice_request did.
*/
if (ir->invreq->recurrence_counter) {
ir->inv->recurrence_counter = ir->invreq->recurrence_counter;
return check_previous_invoice(cmd, ir);
}
/* We're happy with 2 hours timeout (default): they can always
* request another. */
/* FIXME: Fallbacks? */
return create_invoicereq(cmd, ir);
}
static struct command_result *currency_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct invreq *ir)
{
const jsmntok_t *msat = json_get_member(buf, result, "msat");
struct amount_msat amount;
/* Fail in this case, forwarding warnings. */
if (!msat)
return fail_internalerr(cmd, ir,
"Cannot convert currency %.*s: %.*s",
(int)tal_bytelen(ir->offer->currency),
(const char *)ir->offer->currency,
json_tok_full_len(result),
json_tok_full(buf, result));
if (!json_to_msat(buf, msat, &amount))
return fail_internalerr(cmd, ir,
"Bad convert for currency %.*s: %.*s",
(int)tal_bytelen(ir->offer->currency),
(const char *)ir->offer->currency,
json_tok_full_len(msat),
json_tok_full(buf, msat));
return handle_amount_and_recurrence(cmd, ir, amount);
}
static struct command_result *convert_currency(struct command *cmd,
struct invreq *ir)
{
struct out_req *req;
u64 raw_amount;
double double_amount;
struct command_result *err;
const struct iso4217_name_and_divisor *iso4217;
assert(ir->offer->currency);
/* Multiply by quantity *first*, for best precision */
err = invreq_amount_by_quantity(cmd, ir, &raw_amount);
if (err)
return err;
/* BOLT-offers #12:
* - MUST calculate the *base invoice amount* using the offer
* `amount`:
* - if offer `currency` is not the invoice currency, convert
* to the invoice currency.
*/
iso4217 = find_iso4217(ir->offer->currency,
tal_bytelen(ir->offer->currency));
/* We should not create offer with unknown currency! */
if (!iso4217)
return fail_internalerr(cmd, ir,
"Unknown offer currency %.*s",
(int)tal_bytelen(ir->offer->currency),
ir->offer->currency);
double_amount = (double)raw_amount;
for (size_t i = 0; i < iso4217->minor_unit; i++)
double_amount /= 10;
req = jsonrpc_request_start(cmd->plugin, cmd, "currencyconvert",
currency_done, error, ir);
json_add_stringn(req->js, "currency",
(const char *)ir->offer->currency,
tal_bytelen(ir->offer->currency));
json_add_member(req->js, "amount", false, "%f", double_amount);
return send_outreq(cmd->plugin, req);
}
static struct command_result *listoffers_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
@ -413,9 +578,9 @@ static struct command_result *listoffers_done(struct command *cmd,
const jsmntok_t *arr = json_get_member(buf, result, "offers");
const jsmntok_t *offertok, *activetok, *b12tok;
bool active;
struct amount_msat amt;
char *fail;
struct command_result *err;
struct amount_msat amt;
/* BOLT-offers #12:
*
@ -498,64 +663,6 @@ static struct command_result *listoffers_done(struct command *cmd,
return err;
}
if (ir->offer->amount) {
u64 raw_amount;
/* BOLT-offers #12:
*
* - if the offer included `amount`:
* - MUST fail the request if it contains `amount`.
*/
err = invreq_must_not_have(cmd, ir, amount);
if (err)
return err;
/* BOLT-offers #12:
* - MUST calculate the *base invoice amount* using the offer
* `amount`:
* - if offer `currency` is not the invoice currency, convert
* to the invoice currency.
*/
if (ir->offer->currency) {
/* FIXME: Currency conversion! */
return fail_invreq(cmd, ir,
"FIXME: Request for currency %.*s",
(int)tal_bytelen(ir->offer->currency),
(char *)ir->offer->currency);
} else
raw_amount = *ir->offer->amount;
/* BOLT-offers #12:
* - if request contains `quantity`, multiply by `quantity`.
*/
if (ir->invreq->quantity) {
if (mul_overflows_u64(*ir->invreq->quantity, raw_amount)) {
return fail_invreq(cmd, ir,
"quantity %"PRIu64
" causes overflow",
*ir->invreq->quantity);
}
raw_amount *= *ir->invreq->quantity;
}
amt = amount_msat(raw_amount);
} else {
/* BOLT-offers #12:
*
* - otherwise:
* - MUST fail the request if it does not contain `amount`.
* - MUST use the request `amount` as the *base invoice amount*.
* (Note: invoice amount can be further modiifed by recurrence
* below)
*/
err = invreq_must_have(cmd, ir, amount);
if (err)
return err;
amt = amount_msat(*ir->invreq->amount);
}
if (ir->offer->recurrence) {
/* BOLT-offers #12:
*
@ -612,8 +719,6 @@ static struct command_result *listoffers_done(struct command *cmd,
/* Which is the same as the invreq */
ir->inv->offer_id = tal_dup(ir->inv, struct sha256,
ir->invreq->offer_id);
ir->inv->amount = tal_dup(ir->inv, u64,
&amt.millisatoshis); /* Raw: wire protocol */
ir->inv->description = tal_dup_talarr(ir->inv, char,
ir->offer->description);
ir->inv->features = tal_dup_talarr(ir->inv, u8,
@ -651,23 +756,14 @@ static struct command_result *listoffers_done(struct command *cmd,
ir->inv->timestamp = tal(ir->inv, u64);
*ir->inv->timestamp = time_now().ts.tv_sec;
/* Last of all, we handle recurrence details, which often requires
* further lookups. */
/* BOLT-offers #12:
* - MUST set (or not set) `recurrence_counter` exactly as the
* invoice_request did.
*/
if (ir->invreq->recurrence_counter) {
ir->inv->recurrence_counter = ir->invreq->recurrence_counter;
return check_previous_invoice(cmd, ir);
}
/* We're happy with 2 hours timeout (default): they can always
* request another. */
/* We may require currency lookup; if so, do it now. */
if (ir->offer->amount && ir->offer->currency)
return convert_currency(cmd, ir);
/* FIXME: Fallbacks? */
/* FIXME: refunds? */
return create_invoicereq(cmd, ir);
err = invreq_base_amount_simple(cmd, ir, &amt);
if (err)
return err;
return handle_amount_and_recurrence(cmd, ir, amt);
}
static struct command_result *handle_offerless_request(struct command *cmd,

24
tests/test_pay.py

@ -3853,7 +3853,10 @@ def test_offer(node_factory, bitcoind):
@unittest.skipIf(not EXPERIMENTAL_FEATURES, "offers are experimental")
def test_fetchinvoice(node_factory, bitcoind):
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
# We remove the conversion plugin on l3, causing it to get upset.
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,
opts=[{}, {},
{'allow_broken_log': True}])
# Simple offer first.
offer1 = l3.rpc.call('offer', {'amount': '1msat',
@ -3943,6 +3946,25 @@ def test_fetchinvoice(node_factory, bitcoind):
'recurrence_counter': 0,
'recurrence_label': 'test nochannel'})
# Now, test amount in different currency!
plugin = os.path.join(os.path.dirname(__file__), 'plugins/currencyUSDAUD5000.py')
l3.rpc.plugin_start(plugin)
offerusd = l3.rpc.call('offer', {'amount': '10.05USD',
'description': 'USD test'})['bolt12']
inv = l1.rpc.call('fetchinvoice', {'offer': offerusd})
assert inv['changes']['msat'] == Millisatoshi(int(10.05 * 5000))
# If we remove plugin, it can no longer give us an invoice.
l3.rpc.plugin_stop(plugin)
with pytest.raises(RpcError, match="Internal error"):
l1.rpc.call('fetchinvoice', {'offer': offerusd})
l3.daemon.wait_for_log("Unknown command 'currencyconvert'")
# But we can still pay the (already-converted) invoice.
l1.rpc.pay(inv['invoice'])
# Test timeout.
l3.stop()
with pytest.raises(RpcError, match='Timeout waiting for response'):

Loading…
Cancel
Save