feat: add BOLT-12 offers (#1242)

* feat: add BOLT-12 offers

* chore: display incoming bolt 12 payments in tx list

* chore: show bolt 12 button for users without alby account

* fix: check if bolt12payment fields are nil before assigning

* chore: do not pass in expiry

* fix: use checkLDKerr for error handling

* chore: add wails support

* chore: ui improvements

* chore: show bolt-12 for LDK backend type only

* chore: add offer info to tx metadata

* chore: use bolt12Offer

* fix: make offer description optional

* fix: remove incorrect log line

* chore: minor updates to node payBOLT12Offer command

* chore: remove unnecessary BOLT-12 offer copy

* chore: remove unused quantity

* chore: cleanup bolt12Offer usage in TransactionItem

* chore: prioritize lightning address, make bolt-12 copy consistent

* chore: rename generate offer to make offer

* fix: error handling

* chore: move payment hash into pay offer response

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
Adithya Vardhan 2025-06-04 20:58:19 +05:30 committed by GitHub
parent 4ab91202b7
commit c653ae9090
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 757 additions and 680 deletions

View file

@ -685,6 +685,18 @@ func (api *api) UpdateChannel(ctx context.Context, updateChannelRequest *UpdateC
return api.svc.GetLNClient().UpdateChannel(ctx, updateChannelRequest)
}
func (api *api) MakeOffer(ctx context.Context, description string) (string, error) {
if api.svc.GetLNClient() == nil {
return "", errors.New("LNClient not started")
}
offer, err := api.svc.GetLNClient().MakeOffer(ctx, description)
if err != nil {
return "", err
}
return offer, nil
}
func (api *api) GetNewOnchainAddress(ctx context.Context) (string, error) {
if api.svc.GetLNClient() == nil {
return "", errors.New("LNClient not started")

View file

@ -31,6 +31,7 @@ type API interface {
OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error)
CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*CloseChannelResponse, error)
UpdateChannel(ctx context.Context, updateChannelRequest *UpdateChannelRequest) error
MakeOffer(ctx context.Context, description string) (string, error)
GetNewOnchainAddress(ctx context.Context) (string, error)
GetUnusedOnchainAddress(ctx context.Context) (string, error)
SignMessage(ctx context.Context, message string) (*SignMessageResponse, error)
@ -346,6 +347,10 @@ type PayInvoiceRequest struct {
Metadata Metadata `json:"metadata"`
}
type MakeOfferRequest struct {
Description string `json:"description"`
}
type MakeInvoiceRequest struct {
Amount uint64 `json:"amount"`
Description string `json:"description"`

View file

@ -88,7 +88,10 @@ function TransactionItem({ tx }: Props) {
const eventId = tx.metadata?.nostr?.tags?.find((t) => t[0] === "e")?.[1];
const description = tx.description || tx.metadata?.comment;
const bolt12Offer = tx.metadata?.offer;
const description =
tx.description || tx.metadata?.comment || bolt12Offer?.payer_note;
const copy = (text: string) => {
copyToClipboard(text, toast);
@ -266,7 +269,15 @@ function TransactionItem({ tx }: Props) {
<div className="mt-6">
<p>Comment</p>
<p className="text-muted-foreground break-all">
{tx.metadata?.comment}
{tx.metadata.comment}
</p>
</div>
)}
{bolt12Offer?.payer_note && (
<div className="mt-6">
<p>Payer Note</p>
<p className="text-muted-foreground break-all">
{bolt12Offer.payer_note}
</p>
</div>
)}
@ -304,6 +315,22 @@ function TransactionItem({ tx }: Props) {
{showDetails && (
<>
{tx.boostagram && <PodcastingInfo boost={tx.boostagram} />}
{bolt12Offer && (
<div className="mt-6">
<p>BOLT-12 Offer Id</p>
<div className="flex items-center gap-4">
<p className="text-muted-foreground break-all">
{bolt12Offer.id}
</p>
<CopyIcon
className="cursor-pointer text-muted-foreground w-4 h-4 flex-shrink-0"
onClick={() => {
copy(bolt12Offer.id as string);
}}
/>
</div>
</div>
)}
{tx.preimage && (
<div className="mt-6">
<p>Preimage</p>

View file

@ -21,7 +21,7 @@ export default function ReceiveLayout() {
title="Receive"
contentRight={
hasChannelManagement && (
<div className="flex items-center gap-4">
<div className="md:flex md:items-center md:gap-4">
<span className="text-muted-foreground">Receive Limit:</span>
<div className="balance sensitive slashed-zero">
{new Intl.NumberFormat().format(

View file

@ -78,6 +78,7 @@ import Send from "src/screens/wallet/Send";
import SignMessage from "src/screens/wallet/SignMessage";
import WithdrawOnchainFunds from "src/screens/wallet/WithdrawOnchainFunds";
import ReceiveInvoice from "src/screens/wallet/receive/ReceiveInvoice";
import ReceiveOffer from "src/screens/wallet/receive/ReceiveOffer";
import ConfirmPayment from "src/screens/wallet/send/ConfirmPayment";
import LnurlPay from "src/screens/wallet/send/LnurlPay";
import PaymentSuccess from "src/screens/wallet/send/PaymentSuccess";
@ -127,6 +128,11 @@ const routes = [
path: "invoice",
element: <ReceiveInvoice />,
},
{
handle: { crumb: () => "BOLT-12 Offer" },
path: "offer",
element: <ReceiveOffer />,
},
],
},
{

View file

@ -1,9 +1,10 @@
import { CopyIcon, PencilIcon } from "lucide-react";
import { CopyIcon, PencilIcon, ReceiptTextIcon } from "lucide-react";
import React from "react";
import { useNavigate } from "react-router-dom";
import Loading from "src/components/Loading";
import QRCode from "src/components/QRCode";
import { Button, LinkButton } from "src/components/ui/button";
import { Card, CardContent } from "src/components/ui/card";
import { useToast } from "src/components/ui/use-toast";
import UserAvatar from "src/components/UserAvatar";
import { useAlbyMe } from "src/hooks/useAlbyMe";
@ -35,36 +36,54 @@ export default function Receive() {
}
return (
<div className="w-full max-w-lg">
<div className="w-full md:max-w-lg">
<div className="grid gap-5">
{info?.albyAccountConnected && me?.lightning_address && (
<div className="flex flex-col items-center justify-center gap-6 border rounded-xl w-full md:max-w-xs p-4 md:p-6">
<div className="relative flex flex-col items-center justify-center">
<QRCode value={me.lightning_address} className="w-full h-auto" />
<UserAvatar className="w-14 h-14 absolute border-4 border-white bg-white" />
</div>
<p className="text-center font-semibold break-all">
{me.lightning_address}
</p>
<div className="flex gap-4 w-full">
<LinkButton
to="invoice"
variant="outline"
className="flex-1 flex gap-2 items-center justify-center"
>
<PencilIcon className="w-4 h-4" /> Amount
</LinkButton>
<Button
variant="secondary"
onClick={() => {
copyToClipboard(me.lightning_address, toast);
}}
className="flex-1 flex gap-2 items-center justify-center"
>
<CopyIcon className="w-4 h-4" /> Copy
</Button>
</div>
</div>
<Card className="w-full md:max-w-xs">
<CardContent className="flex flex-col items-center gap-6 pt-6">
<div className="relative flex items-center justify-center">
<QRCode
value={me.lightning_address}
className="w-full h-auto"
/>
<UserAvatar className="w-14 h-14 absolute border-4 border-white bg-white" />
</div>
<p className="text-center font-semibold break-all">
{me.lightning_address}
</p>
<div className="flex gap-4 w-full">
<Button
variant="secondary"
onClick={() => {
copyToClipboard(me.lightning_address, toast);
}}
className="flex-1 flex gap-2 items-center justify-center"
>
<CopyIcon className="w-4 h-4" /> Copy Lightning Address
</Button>
</div>
<div className="flex gap-4 w-full border-t pt-6">
<LinkButton
to="invoice"
variant="outline"
className="flex-1 flex gap-2 items-center justify-center"
>
<PencilIcon className="w-4 h-4" /> Amount
</LinkButton>
{info.backendType === "LDK" && (
<LinkButton
to="/wallet/receive/offer"
variant="outline"
className="flex-1"
>
<ReceiptTextIcon className="h-4 w-4 shrink-0 mr-2" />
BOLT-12
</LinkButton>
)}
</div>
</CardContent>
</Card>
)}
</div>
</div>

View file

@ -1,4 +1,9 @@
import { AlertTriangleIcon, CircleCheckIcon, CopyIcon } from "lucide-react";
import {
AlertTriangleIcon,
CircleCheckIcon,
CopyIcon,
ReceiptTextIcon,
} from "lucide-react";
import React from "react";
import { Link } from "react-router-dom";
import ExternalLink from "src/components/ExternalLink";
@ -99,7 +104,7 @@ export default function ReceiveInvoice() {
return (
<div className="flex flex-col md:flex-row gap-6">
<div className="w-full md:max-w-xl">
<div className="w-full md:max-w-lg">
<div className="grid gap-5">
{hasChannelManagement &&
parseInt(amount || "0") * 1000 >=
@ -117,57 +122,63 @@ export default function ReceiveInvoice() {
)}
<div>
{transaction ? (
<div className="flex flex-col items-center justify-center gap-6 border rounded-xl w-full md:max-w-xs p-4 md:p-6">
<Card className="w-full md:max-w-xs">
{!paymentDone ? (
<>
<div className="flex flex-row items-center gap-2 font-medium">
<Loading className="w-4 h-4" />
<p>Waiting for payment</p>
</div>
<div className="relative flex flex-col items-center justify-center">
<CardHeader>
<CardTitle className="flex justify-center">
<Loading className="w-4 h-4 mr-2" />
<p>Waiting for payment</p>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<QRCode value={transaction.invoice} className="w-full" />
</div>
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-semibold slashed-zero">
{new Intl.NumberFormat().format(parseInt(amount))} sats
</p>
<FormattedFiatAmount amount={parseInt(amount)} />
</div>
<div>
<Button onClick={copy} variant="outline">
<CopyIcon className="w-4 h-4 mr-2" />
Copy Invoice
</Button>
</div>
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-semibold slashed-zero">
{new Intl.NumberFormat().format(parseInt(amount))}{" "}
sats
</p>
<FormattedFiatAmount amount={parseInt(amount)} />
</div>
<div>
<Button onClick={copy} variant="outline">
<CopyIcon className="w-4 h-4 mr-2" />
Copy Invoice
</Button>
</div>
</CardContent>
</>
) : (
<>
<div className="text-center font-medium">
Payment Received!
</div>
<div className="relative flex flex-col items-center justify-center">
<CircleCheckIcon className="w-64 h-64 mb-1" />
</div>
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-semibold slashed-zero">
{new Intl.NumberFormat().format(parseInt(amount))} sats
</p>
<FormattedFiatAmount amount={parseInt(amount)} />
</div>
<div>
<Button
variant="outline"
onClick={() => {
setPaymentDone(false);
setTransaction(null);
}}
>
Receive Another Payment
</Button>
</div>
<CardHeader>
<CardTitle className="text-center">
Payment Received!
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<CircleCheckIcon className="w-72 h-72 p-2" />
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-semibold slashed-zero">
{new Intl.NumberFormat().format(parseInt(amount))}{" "}
sats
</p>
<FormattedFiatAmount amount={parseInt(amount)} />
</div>
<div>
<Button
variant="outline"
onClick={() => {
setPaymentDone(false);
setTransaction(null);
}}
>
Receive Another Payment
</Button>
</div>
</CardContent>
</>
)}
</div>
</Card>
) : (
<form onSubmit={handleSubmit} className="grid gap-5">
<div>
@ -197,7 +208,7 @@ export default function ReceiveInvoice() {
}}
/>
</div>
<div>
<div className="flex flex-col md:flex-row gap-4">
<LoadingButton
className="w-full md:w-auto"
loading={isLoading}
@ -206,6 +217,15 @@ export default function ReceiveInvoice() {
>
Create Invoice
</LoadingButton>
{!info?.albyAccountConnected &&
info.backendType === "LDK" && (
<Link to="/wallet/receive/offer">
<Button variant="outline" className="w-full">
<ReceiptTextIcon className="h-4 w-4 shrink-0 mr-2" />
BOLT-12 Offer
</Button>
</Link>
)}
</div>
</form>
)}

View file

@ -0,0 +1,137 @@
import { CircleAlertIcon, CopyIcon, RefreshCwIcon } from "lucide-react";
import React from "react";
import QRCode from "src/components/QRCode";
import {
Alert,
AlertDescription,
AlertTitle,
} from "src/components/ui/alert.tsx";
import { Button } from "src/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "src/components/ui/card";
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 { copyToClipboard } from "src/lib/clipboard";
import { CreateOfferRequest } from "src/types";
import { request } from "src/utils/request";
export default function ReceiveOffer() {
const { toast } = useToast();
const [isLoading, setLoading] = React.useState(false);
const [description, setDescription] = React.useState<string>("");
const [offer, setOffer] = React.useState<string | null>(null);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
setLoading(true);
const offer = await request<string>("/api/offers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
description,
} as CreateOfferRequest),
});
if (offer) {
setOffer(offer);
toast({
title: "Successfully created BOLT-12 offer",
});
}
} catch (e) {
toast({
variant: "destructive",
title: "Failed to create offer: " + e,
});
console.error(e);
} finally {
setLoading(false);
}
};
const copy = () => {
copyToClipboard(offer as string, toast);
};
return (
<div className="grid gap-5 md:max-w-lg">
{!offer && (
<Alert>
<CircleAlertIcon className="h-4 w-4" />
<AlertTitle>BOLT-12 Offers are in beta</AlertTitle>
<AlertDescription>
BOLT-12 is not supported by all wallets and nodes in the lightning
network. This feature will work only if you have a channel with a
node that supports onion message forwarding, and are paid by a
lightning wallet that supports paying BOLT-12 offers.
</AlertDescription>
</Alert>
)}
{offer ? (
<>
<Card className="w-full md:max-w-xs">
<CardHeader>
<CardTitle className="text-center">Lightning Offer</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<QRCode value={offer} className="w-full" />
<div className="flex flex-col md:flex-row gap-4 w-full">
<Button
className="flex-1"
variant="outline"
onClick={() => {
setDescription("");
setOffer(null);
}}
>
<RefreshCwIcon className="h-4 w-4 shrink-0 mr-2" />
New Offer
</Button>
<Button className="flex-1" onClick={copy} variant="secondary">
<CopyIcon className="w-4 h-4 mr-2" />
Copy
</Button>
</div>
</CardContent>
</Card>
</>
) : (
<form onSubmit={handleSubmit} className="grid gap-5">
<div>
<Label htmlFor="description">Description</Label>
<Input
id="description"
type="text"
value={description}
placeholder="For e.g. what is this payment for?"
onChange={(e) => {
setDescription(e.target.value);
}}
/>
</div>
<div>
<LoadingButton
className="w-full md:w-auto"
loading={isLoading}
type="submit"
>
Create Offer
</LoadingButton>
</div>
</form>
)}
</div>
);
}

View file

@ -299,6 +299,10 @@ export type PayInvoiceResponse = {
fee: number;
};
export type CreateOfferRequest = {
description: string;
};
export type CreateInvoiceRequest = {
amount: number;
description: string;
@ -492,6 +496,10 @@ export type TransactionMetadata = {
pubkey: string;
tags: string[][];
}; // NIP-57
offer?: {
id: string;
payer_note: string;
}; // BOLT-12
} & Record<string, unknown>;
export type Boostagram = {

View file

@ -148,6 +148,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
restrictedApiGroup.GET("/wallet/capabilities", httpSvc.capabilitiesHandler)
restrictedApiGroup.POST("/payments/:invoice", httpSvc.sendPaymentHandler)
restrictedApiGroup.POST("/invoices", httpSvc.makeInvoiceHandler)
restrictedApiGroup.POST("/offers", httpSvc.makeOfferHandler)
restrictedApiGroup.GET("/transactions", httpSvc.listTransactionsHandler)
restrictedApiGroup.GET("/transactions/:paymentHash", httpSvc.lookupTransactionHandler)
restrictedApiGroup.GET("/balances", httpSvc.balancesHandler)
@ -543,6 +544,27 @@ func (httpSvc *HttpService) sendPaymentHandler(c echo.Context) error {
return c.JSON(http.StatusOK, paymentResponse)
}
func (httpSvc *HttpService) makeOfferHandler(c echo.Context) error {
ctx := c.Request().Context()
var makeOfferRequest api.MakeOfferRequest
if err := c.Bind(&makeOfferRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}
offer, err := httpSvc.api.MakeOffer(ctx, makeOfferRequest.Description)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to generate BOLT-12 offer: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, offer)
}
func (httpSvc *HttpService) makeInvoiceHandler(c echo.Context) error {
var makeInvoiceRequest api.MakeInvoiceRequest
if err := c.Bind(&makeInvoiceRequest); err != nil {

View file

@ -564,6 +564,10 @@ func (cs *CashuService) executeCommandResetWallet() (*lnclient.CustomNodeCommand
}, nil
}
func (svc *CashuService) MakeOffer(ctx context.Context, description string) (string, error) {
return "", errors.New("not supported")
}
func (cs *CashuService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
return nil, errors.ErrUnsupported
}

View file

@ -437,6 +437,17 @@ func getMaxTotalRoutingFeeLimit(amountMsat uint64) ldk_node.MaxTotalRoutingFeeLi
}
}
func (ls *LDKService) MakeOffer(ctx context.Context, description string) (string, error) {
offer, err := ls.node.Bolt12Payment().ReceiveVariableAmount(description, nil)
if err != nil {
logger.Logger.WithError(err).Error("Failed to generate BOLT12 offer")
return "", err
}
logger.Logger.WithField("offer", offer).Info("Generated BOLT12 offer")
return offer, nil
}
func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64, timeoutSeconds *int64) (*lnclient.PayInvoiceResponse, error) {
sendPaymentTimeout := int64(constants.SEND_PAYMENT_TIMEOUT)
if timeoutSeconds != nil {
@ -1291,6 +1302,34 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails)
paymentHash = bolt11PaymentKind.Hash
}
bolt12PaymentKind, isBolt12PaymentKind := payment.Kind.(ldk_node.PaymentKindBolt12Offer)
if isBolt12PaymentKind {
createdAt = int64(payment.CreatedAt)
if bolt12PaymentKind.Hash == nil {
return nil, errors.New("BOLT-12 payment has no payment hash")
}
paymentHash = *bolt12PaymentKind.Hash
offer := map[string]interface{}{}
offer["id"] = bolt12PaymentKind.OfferId
if bolt12PaymentKind.PayerNote != nil {
offer["payer_note"] = *bolt12PaymentKind.PayerNote
}
metadata["offer"] = offer
if payment.Status == ldk_node.PaymentStatusSucceeded {
if bolt12PaymentKind.Preimage != nil {
preimage = *bolt12PaymentKind.Preimage
}
lastUpdate := int64(payment.LatestUpdateTimestamp)
settledAt = &lastUpdate
}
}
spontaneousPaymentKind, isSpontaneousPaymentKind := payment.Kind.(ldk_node.PaymentKindSpontaneous)
if isSpontaneousPaymentKind {
// keysend payment
@ -1964,12 +2003,169 @@ func (ls *LDKService) GetPubkey() string {
return ls.pubkey
}
func (ls *LDKService) PayOfferSync(ctx context.Context, offer string, amount uint64, payerNote string) (*lnclient.PayOfferResponse, error) {
// TODO: this is only for testing MakeOffer and needs improvements
// (+ BOLT-12 payments need to go through transactions service)
// TODO: send liquidity event if amount too large
paymentStart := time.Now()
ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
// TODO: use normal send if no amount is provided
paymentId, err := checkLDKErr(ls.node.Bolt12Payment().SendUsingAmount(offer, amount, nil, &payerNote))
if err != nil {
logger.Logger.WithError(err).Error("Failed to initiate BOLT-12 variable amount payment")
return nil, errors.New("failed to initiate BOLT-12 variable amount payment")
}
logger.Logger.WithFields(logrus.Fields{
"payment_id": paymentId,
}).Info("Initiated BOLT-12 variable amount payment")
fee := uint64(0)
preimage := ""
payment := ls.node.Payment(paymentId)
if payment == nil {
return nil, errors.New("payment not found by payment ID")
}
paymentHash := ""
for start := time.Now(); time.Since(start) < time.Second*60; {
event := <-ldkEventSubscription
eventPaymentSuccessful, isEventPaymentSuccessfulEvent := (*event).(ldk_node.EventPaymentSuccessful)
eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed)
if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentId != nil && *eventPaymentSuccessful.PaymentId == paymentId {
logger.Logger.Info("Got payment success event")
payment := ls.node.Payment(paymentId)
if payment == nil {
logger.Logger.Errorf("Couldn't find payment by payment ID: %v", paymentId)
return nil, errors.New("payment not found")
}
bolt12PaymentKind, ok := payment.Kind.(ldk_node.PaymentKindBolt12Offer)
if !ok {
logger.Logger.WithFields(logrus.Fields{
"payment": payment,
}).Error("Payment is not a BOLT-12 offer kind")
return nil, errors.New("payment is not a BOLT-12 offer")
}
if bolt12PaymentKind.Preimage == nil {
logger.Logger.Errorf("No payment preimage for payment ID: %v", paymentId)
return nil, errors.New("payment preimage not found")
}
preimage = *bolt12PaymentKind.Preimage
if bolt12PaymentKind.Hash == nil {
logger.Logger.Errorf("No payment hash for payment ID: %v", paymentId)
return nil, errors.New("payment hash not found")
}
paymentHash = *bolt12PaymentKind.Hash
if eventPaymentSuccessful.FeePaidMsat != nil {
fee = *eventPaymentSuccessful.FeePaidMsat
}
break
}
if isEventPaymentFailedEvent && eventPaymentFailed.PaymentId != nil && *eventPaymentFailed.PaymentId == paymentId {
reason := ls.getPaymentFailReason(&eventPaymentFailed)
logger.Logger.WithFields(logrus.Fields{
"payment_id": paymentId,
"reason": reason,
}).Error("Received payment failed event")
return nil, fmt.Errorf("received payment failed event: %s", reason)
}
}
if preimage == "" {
logger.Logger.WithFields(logrus.Fields{
"payment_id": paymentId,
}).Warn("Timed out waiting for payment to be sent")
return nil, lnclient.NewTimeoutError()
}
logger.Logger.WithFields(logrus.Fields{
"duration": time.Since(paymentStart).Milliseconds(),
"fee": fee,
}).Info("Successful payment")
return &lnclient.PayOfferResponse{
PaymentHash: paymentHash,
Preimage: preimage,
Fee: fee,
}, nil
}
const nodeCommandPayBOLT12Offer = "payBOLT12Offer"
func (ls *LDKService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
return nil
return []lnclient.CustomNodeCommandDef{
{
Name: nodeCommandPayBOLT12Offer,
Description: "Send payments to a BOLT-12 offer. NOTE: this is for testing only. Payment will not show in transaction list.",
Args: []lnclient.CustomNodeCommandArgDef{
{
Name: "offer",
Description: "BOLT-12 offer of receiver",
},
{
Name: "amount",
Description: "amount to send in millisats",
},
{
Name: "payerNote",
Description: "note to the recepient",
},
},
},
}
}
func (ls *LDKService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
return nil, nil
switch command.Name {
case nodeCommandPayBOLT12Offer:
var offer string
var amount uint64
var payerNote string
var err error
for i := range command.Args {
switch command.Args[i].Name {
case "offer":
offer = command.Args[i].Value
case "amount":
amount, err = strconv.ParseUint(string(command.Args[i].Value), 10, 64)
case "payerNote":
payerNote = command.Args[i].Value
}
}
if err != nil {
return nil, err
}
payOfferResponse, err := ls.PayOfferSync(ctx, offer, amount, payerNote)
if err != nil {
return nil, err
}
return &lnclient.CustomNodeCommandResponse{
Response: map[string]interface{}{
"paymentHash": payOfferResponse.PaymentHash,
"preimage": payOfferResponse.Preimage,
"fee": payOfferResponse.Fee,
},
}, nil
}
return nil, lnclient.ErrUnknownCustomNodeCommand
}
func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (*lnclient.Transaction, error) {

View file

@ -1573,6 +1573,10 @@ func (svc *LNDService) ExecuteCustomNodeCommand(ctx context.Context, command *ln
return nil, nil
}
func (svc *LNDService) MakeOffer(ctx context.Context, description string) (string, error) {
return "", errors.New("not supported")
}
func (svc *LNDService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
resp, err := svc.client.GetTransactions(ctx, &lnrpc.GetTransactionsRequest{})
if err != nil {

View file

@ -77,6 +77,7 @@ type LNClient interface {
CloseChannel(ctx context.Context, closeChannelRequest *CloseChannelRequest) (*CloseChannelResponse, error)
UpdateChannel(ctx context.Context, updateChannelRequest *UpdateChannelRequest) error
DisconnectPeer(ctx context.Context, peerId string) error
MakeOffer(ctx context.Context, description string) (string, error)
GetNewOnchainAddress(ctx context.Context) (string, error)
ResetRouter(key string) error
GetOnchainBalance(ctx context.Context) (*OnchainBalanceResponse, error)
@ -191,6 +192,12 @@ type PayInvoiceResponse struct {
Fee uint64 `json:"fee"`
}
type PayOfferResponse = struct {
Preimage string `json:"preimage"`
Fee uint64 `json:"fee"`
PaymentHash string `json:"payment_hash"`
}
type PayKeysendResponse struct {
Fee uint64 `json:"fee"`
}

View file

@ -553,6 +553,10 @@ func (svc *PhoenixService) ExecuteCustomNodeCommand(ctx context.Context, command
return nil, nil
}
func (svc *PhoenixService) MakeOffer(ctx context.Context, description string) (string, error) {
return "", errors.New("not supported")
}
func (svc *PhoenixService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
return nil, errors.ErrUnsupported
}

View file

@ -235,6 +235,10 @@ func (mln *MockLn) ExecuteCustomNodeCommand(ctx context.Context, command *lnclie
return nil, nil
}
func (mln *MockLn) MakeOffer(ctx context.Context, description string) (string, error) {
return "", errors.New("not supported")
}
func (mln *MockLn) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
return nil, errors.ErrUnsupported
}

View file

@ -59,26 +59,15 @@ type MockConfig_ChangeUnlockPassword_Call struct {
}
// ChangeUnlockPassword is a helper method to define mock.On call
// - currentUnlockPassword string
// - newUnlockPassword string
// - currentUnlockPassword
// - newUnlockPassword
func (_e *MockConfig_Expecter) ChangeUnlockPassword(currentUnlockPassword interface{}, newUnlockPassword interface{}) *MockConfig_ChangeUnlockPassword_Call {
return &MockConfig_ChangeUnlockPassword_Call{Call: _e.mock.On("ChangeUnlockPassword", currentUnlockPassword, newUnlockPassword)}
}
func (_c *MockConfig_ChangeUnlockPassword_Call) Run(run func(currentUnlockPassword string, newUnlockPassword string)) *MockConfig_ChangeUnlockPassword_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
run(args[0].(string), args[1].(string))
})
return _c
}
@ -116,20 +105,14 @@ type MockConfig_CheckUnlockPassword_Call struct {
}
// CheckUnlockPassword is a helper method to define mock.On call
// - password string
// - password
func (_e *MockConfig_Expecter) CheckUnlockPassword(password interface{}) *MockConfig_CheckUnlockPassword_Call {
return &MockConfig_CheckUnlockPassword_Call{Call: _e.mock.On("CheckUnlockPassword", password)}
}
func (_c *MockConfig_CheckUnlockPassword_Call) Run(run func(password string)) *MockConfig_CheckUnlockPassword_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
run(args[0].(string))
})
return _c
}
@ -176,26 +159,15 @@ type MockConfig_Get_Call struct {
}
// Get is a helper method to define mock.On call
// - key string
// - encryptionKey string
// - key
// - encryptionKey
func (_e *MockConfig_Expecter) Get(key interface{}, encryptionKey interface{}) *MockConfig_Get_Call {
return &MockConfig_Get_Call{Call: _e.mock.On("Get", key, encryptionKey)}
}
func (_c *MockConfig_Get_Call) Run(run func(key string, encryptionKey string)) *MockConfig_Get_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
run(args[0].(string), args[1].(string))
})
return _c
}
@ -455,20 +427,14 @@ type MockConfig_SaveUnlockPasswordCheck_Call struct {
}
// SaveUnlockPasswordCheck is a helper method to define mock.On call
// - encryptionKey string
// - encryptionKey
func (_e *MockConfig_Expecter) SaveUnlockPasswordCheck(encryptionKey interface{}) *MockConfig_SaveUnlockPasswordCheck_Call {
return &MockConfig_SaveUnlockPasswordCheck_Call{Call: _e.mock.On("SaveUnlockPasswordCheck", encryptionKey)}
}
func (_c *MockConfig_SaveUnlockPasswordCheck_Call) Run(run func(encryptionKey string)) *MockConfig_SaveUnlockPasswordCheck_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
run(args[0].(string))
})
return _c
}
@ -506,20 +472,14 @@ type MockConfig_SetAutoUnlockPassword_Call struct {
}
// SetAutoUnlockPassword is a helper method to define mock.On call
// - unlockPassword string
// - unlockPassword
func (_e *MockConfig_Expecter) SetAutoUnlockPassword(unlockPassword interface{}) *MockConfig_SetAutoUnlockPassword_Call {
return &MockConfig_SetAutoUnlockPassword_Call{Call: _e.mock.On("SetAutoUnlockPassword", unlockPassword)}
}
func (_c *MockConfig_SetAutoUnlockPassword_Call) Run(run func(unlockPassword string)) *MockConfig_SetAutoUnlockPassword_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
run(args[0].(string))
})
return _c
}
@ -557,20 +517,14 @@ type MockConfig_SetCurrency_Call struct {
}
// SetCurrency is a helper method to define mock.On call
// - value string
// - value
func (_e *MockConfig_Expecter) SetCurrency(value interface{}) *MockConfig_SetCurrency_Call {
return &MockConfig_SetCurrency_Call{Call: _e.mock.On("SetCurrency", value)}
}
func (_c *MockConfig_SetCurrency_Call) Run(run func(value string)) *MockConfig_SetCurrency_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
run(args[0].(string))
})
return _c
}
@ -608,32 +562,16 @@ type MockConfig_SetIgnore_Call struct {
}
// SetIgnore is a helper method to define mock.On call
// - key string
// - value string
// - encryptionKey string
// - key
// - value
// - encryptionKey
func (_e *MockConfig_Expecter) SetIgnore(key interface{}, value interface{}, encryptionKey interface{}) *MockConfig_SetIgnore_Call {
return &MockConfig_SetIgnore_Call{Call: _e.mock.On("SetIgnore", key, value, encryptionKey)}
}
func (_c *MockConfig_SetIgnore_Call) Run(run func(key string, value string, encryptionKey string)) *MockConfig_SetIgnore_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
run(args[0].(string), args[1].(string), args[2].(string))
})
return _c
}
@ -671,32 +609,16 @@ type MockConfig_SetUpdate_Call struct {
}
// SetUpdate is a helper method to define mock.On call
// - key string
// - value string
// - encryptionKey string
// - key
// - value
// - encryptionKey
func (_e *MockConfig_Expecter) SetUpdate(key interface{}, value interface{}, encryptionKey interface{}) *MockConfig_SetUpdate_Call {
return &MockConfig_SetUpdate_Call{Call: _e.mock.On("SetUpdate", key, value, encryptionKey)}
}
func (_c *MockConfig_SetUpdate_Call) Run(run func(key string, value string, encryptionKey string)) *MockConfig_SetUpdate_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
run(args[0].(string), args[1].(string), args[2].(string))
})
return _c
}

File diff suppressed because it is too large Load diff

View file

@ -555,20 +555,14 @@ type MockService_StartApp_Call struct {
}
// StartApp is a helper method to define mock.On call
// - encryptionKey string
// - encryptionKey
func (_e *MockService_Expecter) StartApp(encryptionKey interface{}) *MockService_StartApp_Call {
return &MockService_StartApp_Call{Call: _e.mock.On("StartApp", encryptionKey)}
}
func (_c *MockService_StartApp_Call) Run(run func(encryptionKey string)) *MockService_StartApp_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
run(args[0].(string))
})
return _c
}

View file

@ -944,6 +944,27 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: *nodeHealth, Error: ""}
case "/api/offers":
makeOfferRequest := &api.MakeOfferRequest{}
err := json.Unmarshal([]byte(body), makeOfferRequest)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to decode request to wails router")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
offer, err := app.api.MakeOffer(ctx, makeOfferRequest.Description)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to generate BOLT-12 offer")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: offer, Error: ""}
case "/api/commands":
nodeCommandsResponse, err := app.api.GetCustomNodeCommands()
if err != nil {