Skip to content

Commit

Permalink
feat: pay 0-amount invoices (#860)
Browse files Browse the repository at this point in the history
* feat: pay 0-amount invoices

* chore: update lightning-tools dependency

* feat: pay 0 amount invoices in LND

* chore: typo

* fix: lightning-tools dependency version

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
  • Loading branch information
rolznz and im-adithya authored Dec 17, 2024
1 parent 2d1327e commit 044e0c3
Show file tree
Hide file tree
Showing 26 changed files with 309 additions and 62 deletions.
6 changes: 5 additions & 1 deletion api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type API interface {
RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (*RedeemOnchainFundsResponse, error)
GetBalances(ctx context.Context) (*BalancesResponse, error)
ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error)
SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error)
SendPayment(ctx context.Context, invoice string, amountMsat *uint64) (*SendPaymentResponse, error)
CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error)
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
RequestMempoolApi(endpoint string) (interface{}, error)
Expand Down Expand Up @@ -290,6 +290,10 @@ type SignMessageResponse struct {
Signature string `json:"signature"`
}

type PayInvoiceRequest struct {
Amount *uint64 `json:"amount"`
}

type MakeInvoiceRequest struct {
Amount uint64 `json:"amount"`
Description string `json:"description"`
Expand Down
6 changes: 3 additions & 3 deletions api/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64,
return &apiTransactions, nil
}

func (api *api) SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error) {
func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uint64) (*SendPaymentResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(ctx, invoice, nil, api.svc.GetLNClient(), nil, nil)
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(ctx, invoice, amountMsat, nil, api.svc.GetLNClient(), nil, nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -131,7 +131,7 @@ func (api *api) TopupIsolatedApp(ctx context.Context, userApp *db.App, amountMsa
return err
}

_, err = api.svc.GetTransactionsService().SendPaymentSync(ctx, transaction.PaymentRequest, nil, api.svc.GetLNClient(), nil, nil)
_, err = api.svc.GetTransactionsService().SendPaymentSync(ctx, transaction.PaymentRequest, nil, nil, api.svc.GetLNClient(), nil, nil)
return err
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"prepare": "cd .. && husky frontend/.husky"
},
"dependencies": {
"@getalby/lightning-tools": "^5.1.1",
"@getalby/lightning-tools": "^5.1.2",
"@getalby/sdk": "^3.8.2",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import ReceiveInvoice from "src/screens/wallet/receive/ReceiveInvoice";
import ConfirmPayment from "src/screens/wallet/send/ConfirmPayment";
import LnurlPay from "src/screens/wallet/send/LnurlPay";
import PaymentSuccess from "src/screens/wallet/send/PaymentSuccess";
import ZeroAmount from "src/screens/wallet/send/ZeroAmount";

const routes = [
{
Expand Down Expand Up @@ -128,6 +129,10 @@ const routes = [
path: "lnurl-pay",
element: <LnurlPay />,
},
{
path: "0-amount",
element: <ZeroAmount />,
},
{
path: "confirm-payment",
element: <ConfirmPayment />,
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/screens/wallet/Send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ export default function Send() {
}

const invoice = new Invoice({ pr: recipient });
if (invoice.satoshi === 0) {
navigate(`/wallet/send/0-amount`, {
state: {
args: { paymentRequest: invoice },
},
});
return;
}

navigate(`/wallet/send/confirm-payment`, {
state: {
args: { paymentRequest: invoice },
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/screens/wallet/send/ConfirmPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function ConfirmPayment() {
const { toast } = useToast();

const invoice = state?.args?.paymentRequest as Invoice;
const amount = state?.args?.amount as number | undefined;
const [isLoading, setLoading] = React.useState(false);

const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
Expand All @@ -26,6 +27,9 @@ export default function ConfirmPayment() {
`/api/payments/${invoice.paymentRequest}`,
{
method: "POST",
body: JSON.stringify({
amount: amount ? amount * 1000 : undefined,
}),
headers: {
"Content-Type": "application/json",
},
Expand Down Expand Up @@ -71,7 +75,7 @@ export default function ConfirmPayment() {
<div>
<Label>Amount</Label>
<p className="font-bold slashed-zero">
{new Intl.NumberFormat().format(invoice.satoshi)} sats
{new Intl.NumberFormat().format(amount || invoice.satoshi)} sats
</p>
</div>
{invoice.description && (
Expand Down
93 changes: 93 additions & 0 deletions frontend/src/screens/wallet/send/ZeroAmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from "react";
import { Button } from "src/components/ui/button";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { LoadingButton } from "src/components/ui/loading-button";
import { useToast } from "src/components/ui/use-toast";

import { Invoice } from "@getalby/lightning-tools";
import { Link, useLocation, useNavigate } from "react-router-dom";
import Loading from "src/components/Loading";

export default function ZeroAmount() {
const { state } = useLocation();
const navigate = useNavigate();
const { toast } = useToast();

const paymentRequest = state?.args?.paymentRequest as Invoice;
const [amount, setAmount] = React.useState("");
const [isLoading, setLoading] = React.useState(false);

const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
if (!paymentRequest) {
throw new Error("no invoice set");
}
setLoading(true);

navigate(`/wallet/send/confirm-payment`, {
state: {
args: { paymentRequest, amount: parseInt(amount) },
},
});
} catch (e) {
toast({
variant: "destructive",
title: "Failed to send payment",
description: "" + e,
});
console.error(e);
} finally {
setLoading(false);
}
};

React.useEffect(() => {
if (!paymentRequest) {
navigate("/wallet/send");
}
}, [navigate, paymentRequest]);

if (!paymentRequest) {
return <Loading />;
}

return (
<form onSubmit={onSubmit} className="grid gap-4">
<div>
{paymentRequest.description && (
<div className="mt-2">
<Label>Description</Label>
<p className="text-muted-foreground">
{paymentRequest.description}
</p>
</div>
)}
<div className="mb-2">
<Label htmlFor="amount">Amount</Label>
<Input
id="amount"
type="number"
value={amount}
placeholder="Amount in Satoshi..."
onChange={(e) => {
setAmount(e.target.value.trim());
}}
min={1}
required
autoFocus
/>
</div>
</div>
<div className="flex gap-4">
<LoadingButton loading={isLoading} type="submit">
Continue
</LoadingButton>
<Link to="/wallet/send">
<Button variant="secondary">Back</Button>
</Link>
</div>
</form>
);
}
8 changes: 4 additions & 4 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1337,10 +1337,10 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==

"@getalby/lightning-tools@^5.1.1":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-5.1.1.tgz#51125b2c58ef9372ae9efa93d0808d2205914b91"
integrity sha512-qiGWY7AMnQXywNlpEUTm/2u7Qx0C0qV0i3vlAV5ip8xV2quo4hkesHuAh6dBg/p3VC7t1fa9YUe9677hvQ3fVA==
"@getalby/lightning-tools@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-5.1.2.tgz#8a018e98d5c13097dd98d93192cf5e4e455f4c20"
integrity sha512-BwGm8eGbPh59BVa1gI5yJMantBl/Fdps6X4p1ZACnmxz9vDINX8/3aFoOnDlF7yyA2boXWCsReVQSr26Q2yjiQ==

"@getalby/sdk@^3.8.2":
version "3.8.2"
Expand Down
9 changes: 8 additions & 1 deletion http/http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,14 @@ func (httpSvc *HttpService) balancesHandler(c echo.Context) error {
func (httpSvc *HttpService) sendPaymentHandler(c echo.Context) error {
ctx := c.Request().Context()

paymentResponse, err := httpSvc.api.SendPayment(ctx, c.Param("invoice"))
var payInvoiceRequest api.PayInvoiceRequest
if err := c.Bind(&payInvoiceRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}

paymentResponse, err := httpSvc.api.SendPayment(ctx, c.Param("invoice"), payInvoiceRequest.Amount)

if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Expand Down
5 changes: 4 additions & 1 deletion lnclient/breez/breez.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ func (bs *BreezService) Shutdown() error {
return bs.svc.Disconnect()
}

func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) {
func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
if amount != nil {
return nil, errors.New("0-amount invoices not supported")
}
sendPaymentRequest := breez_sdk.SendPaymentRequest{
Bolt11: payReq,
}
Expand Down
7 changes: 6 additions & 1 deletion lnclient/cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ func (cs *CashuService) Shutdown() error {
return nil
}

func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (response *lnclient.PayInvoiceResponse, err error) {
func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64) (response *lnclient.PayInvoiceResponse, err error) {
// TODO: support 0-amount invoices
if amount != nil {
return nil, errors.New("0-amount invoices not supported")
}

meltResponse, err := cs.wallet.Melt(invoice, cs.wallet.CurrentMint())
if err != nil {
logger.Logger.WithError(err).Error("Failed to melt invoice")
Expand Down
5 changes: 4 additions & 1 deletion lnclient/greenlight/greenlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ func (gs *GreenlightService) Shutdown() error {
return nil
}

func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) {
func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
if amount != nil {
return nil, errors.New("0-amount invoices not supported")
}
response, err := gs.client.Pay(glalby.PayRequest{
Bolt11: payReq,
})
Expand Down
24 changes: 17 additions & 7 deletions lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ func (ls *LDKService) resetRouterInternal() {
}
}

func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnclient.PayInvoiceResponse, error) {
func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
Expand All @@ -482,8 +482,13 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc
return nil, err
}

paymentAmount := uint64(paymentRequest.MSatoshi)
if amount != nil {
paymentAmount = *amount
}

maxSpendable := ls.getMaxSpendable()
if paymentRequest.MSatoshi > maxSpendable {
if paymentAmount > maxSpendable {
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_outgoing_liquidity_required",
Properties: map[string]interface{}{
Expand All @@ -499,7 +504,12 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc
ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)

paymentHash, err := ls.node.Bolt11Payment().Send(invoice, nil)
var paymentHash string
if amount == nil {
paymentHash, err = ls.node.Bolt11Payment().Send(invoice, nil)
} else {
paymentHash, err = ls.node.Bolt11Payment().SendUsingAmount(invoice, *amount, nil)
}
if err != nil {
logger.Logger.WithError(err).Error("SendPayment failed")
return nil, err
Expand Down Expand Up @@ -649,15 +659,15 @@ func (ls *LDKService) getMaxReceivable() int64 {
return int64(receivable)
}

func (ls *LDKService) getMaxSpendable() int64 {
var spendable int64 = 0
func (ls *LDKService) getMaxSpendable() uint64 {
var spendable uint64 = 0
channels := ls.node.ListChannels()
for _, channel := range channels {
if channel.IsUsable {
spendable += min(int64(channel.OutboundCapacityMsat), int64(*channel.CounterpartyOutboundHtlcMaximumMsat))
spendable += min(channel.OutboundCapacityMsat, *channel.CounterpartyOutboundHtlcMaximumMsat)
}
}
return int64(spendable)
return spendable
}

func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
Expand Down
10 changes: 8 additions & 2 deletions lnclient/lnd/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,14 @@ func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (t
return transaction, nil
}

func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) {
resp, err := svc.client.SendPaymentSync(ctx, &lnrpc.SendRequest{PaymentRequest: payReq})
func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
sendRequest := &lnrpc.SendRequest{PaymentRequest: payReq}

if amount != nil {
sendRequest.AmtMsat = int64(*amount)
}

resp, err := svc.client.SendPaymentSync(ctx, sendRequest)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion lnclient/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type NodeConnectionInfo struct {
}

type LNClient interface {
SendPaymentSync(ctx context.Context, payReq string) (*PayInvoiceResponse, error)
SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*PayInvoiceResponse, error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord, preimage string) (*PayKeysendResponse, error)
GetPubkey() string
GetInfo(ctx context.Context) (info *NodeInfo, err error)
Expand Down
6 changes: 5 additions & 1 deletion lnclient/phoenixd/phoenixd.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,11 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string
return transaction, nil
}

func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) {
func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
// TODO: support 0-amount invoices
if amount != nil {
return nil, errors.New("0-amount invoices not supported")
}
form := url.Values{}
form.Add("invoice", payReq)
req, err := http.NewRequest(http.MethodPost, svc.Address+"/payinvoice", strings.NewReader(form.Encode()))
Expand Down
2 changes: 1 addition & 1 deletion nip47/controllers/multi_pay_invoice_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (controller *nip47Controller) HandleMultiPayInvoiceEvent(ctx context.Contex
dTag := []string{"d", invoiceDTagValue}

controller.
pay(ctx, bolt11, metadata, &paymentRequest, nip47Request, requestEventId, app, publishResponse, nostr.Tags{dTag})
pay(ctx, bolt11, invoiceInfo.Amount, metadata, &paymentRequest, nip47Request, requestEventId, app, publishResponse, nostr.Tags{dTag})
}(invoiceInfo)
}

Expand Down
Loading

0 comments on commit 044e0c3

Please sign in to comment.