diff --git a/api/api.go b/api/api.go
index 7f3fb944..8d44cecd 100644
--- a/api/api.go
+++ b/api/api.go
@@ -491,6 +491,39 @@ func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) {
return balances, nil
}
+func (api *api) GetTransactions(ctx context.Context) (*TransactionsResponse, error) {
+ if api.svc.GetLNClient() == nil {
+ return nil, errors.New("LNClient not started")
+ }
+ transactions, err := api.svc.GetLNClient().ListTransactions(ctx, 0, 100000000000, 1000, 0, false, "")
+ if err != nil {
+ return nil, err
+ }
+ return &transactions, nil
+}
+
+func (api *api) SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error) {
+ if api.svc.GetLNClient() == nil {
+ return nil, errors.New("LNClient not started")
+ }
+ resp, err := api.svc.GetLNClient().SendPaymentSync(ctx, invoice)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+func (api *api) CreateInvoice(ctx context.Context, amount int64, description string) (*CreateInvoiceResponse, error) {
+ if api.svc.GetLNClient() == nil {
+ return nil, errors.New("LNClient not started")
+ }
+ invoice, err := api.svc.GetLNClient().MakeInvoice(ctx, amount, description, "", 0)
+ if err != nil {
+ return nil, err
+ }
+ return invoice, nil
+}
+
// TODO: remove dependency on this endpoint
func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) {
url := api.cfg.GetEnv().MempoolApi + endpoint
diff --git a/api/models.go b/api/models.go
index 7d72d83b..c1d201f1 100644
--- a/api/models.go
+++ b/api/models.go
@@ -34,6 +34,9 @@ type API interface {
SignMessage(ctx context.Context, message string) (*SignMessageResponse, error)
RedeemOnchainFunds(ctx context.Context, toAddress string) (*RedeemOnchainFundsResponse, error)
GetBalances(ctx context.Context) (*BalancesResponse, error)
+ GetTransactions(ctx context.Context) (*TransactionsResponse, error)
+ SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error)
+ CreateInvoice(ctx context.Context, amount int64, description string) (*CreateInvoiceResponse, error)
RequestMempoolApi(endpoint string) (interface{}, error)
GetInfo(ctx context.Context) (*InfoResponse, error)
GetEncryptedMnemonic() *EncryptedMnemonicResponse
@@ -177,6 +180,10 @@ type RedeemOnchainFundsResponse struct {
type OnchainBalanceResponse = lnclient.OnchainBalanceResponse
type BalancesResponse = lnclient.BalancesResponse
+type SendPaymentResponse = lnclient.PayInvoiceResponse
+type CreateInvoiceResponse = lnclient.Transaction
+type TransactionsResponse = []lnclient.Transaction
+
// debug api
type SendPaymentProbesRequest struct {
Invoice string `json:"invoice"`
@@ -217,6 +224,15 @@ type SignMessageResponse struct {
Signature string `json:"signature"`
}
+type WalletSendRequest struct {
+ Invoice string `json:"invoice"`
+}
+
+type WalletReceiveRequest struct {
+ Amount int64 `json:"amount"`
+ Description string `json:"description"`
+}
+
type ResetRouterRequest struct {
Key string `json:"key"`
}
diff --git a/frontend/src/components/TransactionsList.tsx b/frontend/src/components/TransactionsList.tsx
new file mode 100644
index 00000000..4104460d
--- /dev/null
+++ b/frontend/src/components/TransactionsList.tsx
@@ -0,0 +1,102 @@
+import dayjs from "dayjs";
+import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
+
+import Loading from "src/components/Loading";
+import { useTransactions } from "src/hooks/useTransactions";
+
+function TransactionsList() {
+ const { data: transactions } = useTransactions();
+
+ if (!transactions) {
+ return ;
+ }
+
+ console.info(transactions);
+
+ return (
+
+ {!transactions?.length ? (
+
+ No transactions, yet.
+ {/* Deposit Bitcoin Button */}
+
+ ) : (
+ <>
+ {transactions?.map((tx, i) => {
+ const type = tx.type;
+
+ return (
+
openDetails(tx)}
+ >
+
+
+ {type == "outgoing" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {type == "incoming" ? "Received" : "Sent"}
+
+
+
+ {dayjs(tx.settled_at * 1000).fromNow()}
+ {tx.description}
+
+
+
+
+
+ {type == "outgoing" ? "-" : "+"}{" "}
+ {Math.floor(tx.amount / 1000)}
+
+
sats
+
+ {/* {!!tx.totalAmountFiat && (
+
+ ~{tx.totalAmountFiat}
+
+ )} */}
+
+
+
+
+ );
+ })}
+ {/* {transaction && (
+
{
+ setModalOpen(false);
+ }}
+ />
+ )} */}
+ >
+ )}
+
+ );
+}
+
+export default TransactionsList;
diff --git a/frontend/src/hooks/useTransactions.ts b/frontend/src/hooks/useTransactions.ts
new file mode 100644
index 00000000..394768fd
--- /dev/null
+++ b/frontend/src/hooks/useTransactions.ts
@@ -0,0 +1,16 @@
+import useSWR, { SWRConfiguration } from "swr";
+
+import { Transaction } from "src/types";
+import { swrFetcher } from "src/utils/swr";
+
+const pollConfiguration: SWRConfiguration = {
+ refreshInterval: 3000,
+};
+
+export function useTransactions(poll = false) {
+ return useSWR(
+ "/api/transactions",
+ swrFetcher,
+ poll ? pollConfiguration : undefined
+ );
+}
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index ab719334..50ef15b8 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -47,6 +47,8 @@ import { LNDForm } from "src/screens/setup/node/LNDForm";
import { PhoenixdForm } from "src/screens/setup/node/PhoenixdForm";
import { PresetNodeForm } from "src/screens/setup/node/PresetNodeForm";
import Wallet from "src/screens/wallet";
+import Receive from "src/screens/wallet/Receive";
+import Send from "src/screens/wallet/Send";
import SignMessage from "src/screens/wallet/SignMessage";
const routes = [
@@ -68,6 +70,16 @@ const routes = [
index: true,
element: ,
},
+ {
+ path: "receive",
+ element: ,
+ handle: { crumb: () => "Receive" },
+ },
+ {
+ path: "send",
+ element: ,
+ handle: { crumb: () => "Send" },
+ },
{
path: "sign-message",
element: ,
diff --git a/frontend/src/screens/wallet/Receive.tsx b/frontend/src/screens/wallet/Receive.tsx
new file mode 100644
index 00000000..9d5e3629
--- /dev/null
+++ b/frontend/src/screens/wallet/Receive.tsx
@@ -0,0 +1,96 @@
+import React from "react";
+import AppHeader from "src/components/AppHeader";
+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 { useCSRF } from "src/hooks/useCSRF";
+import { CreateInvoiceRequest, Transaction } from "src/types";
+import { request } from "src/utils/request";
+
+export default function Receive() {
+ const { data: csrf } = useCSRF();
+ const { toast } = useToast();
+ const [isLoading, setLoading] = React.useState(false);
+ const [amount, setAmount] = React.useState();
+ const [description, setDescription] = React.useState("");
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!csrf) {
+ throw new Error("csrf not loaded");
+ }
+ try {
+ setLoading(true);
+ const invoice = await request("/api/wallet/receive", {
+ method: "POST",
+ headers: {
+ "X-CSRF-Token": csrf,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ amount, description } as CreateInvoiceRequest),
+ });
+ setAmount(null);
+ setDescription("");
+ if (invoice) {
+ // setSignature(signMessageResponse.signature);
+ console.info(invoice);
+ toast({
+ title: "Successfully created invoice",
+ });
+ }
+ } catch (e) {
+ toast({
+ variant: "destructive",
+ title: "Failed to create invoice: " + e,
+ });
+ console.error(e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/screens/wallet/Send.tsx b/frontend/src/screens/wallet/Send.tsx
new file mode 100644
index 00000000..4356c333
--- /dev/null
+++ b/frontend/src/screens/wallet/Send.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+import AppHeader from "src/components/AppHeader";
+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 { useCSRF } from "src/hooks/useCSRF";
+import { PayInvoiceResponse } from "src/types";
+import { request } from "src/utils/request";
+
+export default function Send() {
+ const { data: csrf } = useCSRF();
+ const { toast } = useToast();
+ const [isLoading, setLoading] = React.useState(false);
+ const [invoice, setInvoice] = React.useState("");
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!csrf) {
+ throw new Error("csrf not loaded");
+ }
+ try {
+ setLoading(true);
+ const payInvoiceResponse = await request(
+ "/api/wallet/send",
+ {
+ method: "POST",
+ headers: {
+ "X-CSRF-Token": csrf,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ invoice: invoice.trim() }),
+ }
+ );
+ // setSignatureMessage(message);
+ // setMessage("");
+ if (payInvoiceResponse) {
+ // setSignature(signMessageResponse.signature);
+ toast({
+ title: "Successfully paid invoice",
+ });
+ }
+ } catch (e) {
+ toast({
+ variant: "destructive",
+ title: "Failed to send: " + e,
+ });
+ console.error(e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/screens/wallet/index.tsx b/frontend/src/screens/wallet/index.tsx
index 565b22cf..fe9077c9 100644
--- a/frontend/src/screens/wallet/index.tsx
+++ b/frontend/src/screens/wallet/index.tsx
@@ -1,9 +1,16 @@
-import { ExternalLinkIcon } from "lucide-react";
+import {
+ ArrowDownIcon,
+ ArrowUpIcon,
+ ExternalLinkIcon,
+ ScanIcon,
+} from "lucide-react";
+import { Link } from "react-router-dom";
import AlbyHead from "src/assets/images/alby-head.svg";
import AppHeader from "src/components/AppHeader";
import BreezRedeem from "src/components/BreezRedeem";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
+import TransactionsList from "src/components/TransactionsList";
import { Button } from "src/components/ui/button";
import {
Card,
@@ -30,13 +37,33 @@ function Wallet() {
return (
<>
-
+
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
+
+
+
+
+
+
+
+
+
+
+
@@ -104,6 +131,8 @@ function Wallet() {
+
+
>
);
}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index a2ca746a..c5978f95 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -207,6 +207,16 @@ export type SignMessageResponse = {
signature: string;
};
+export type PayInvoiceResponse = {
+ preimage: string;
+ fee: number;
+};
+
+export type CreateInvoiceRequest = {
+ amount: number;
+ description: string;
+};
+
export type OpenChannelRequest = {
pubkey: string;
amount: number;
@@ -315,6 +325,21 @@ export type BalancesResponse = {
lightning: LightningBalanceResponse;
};
+export type Transaction = {
+ type: string;
+ invoice: string;
+ description: string;
+ description_hash: string;
+ preimage: string;
+ payment_hash: string;
+ amount: number;
+ fees_paid: number;
+ created_at: number;
+ expires_at: number;
+ settled_at: number;
+ metadata: string[];
+};
+
export type NewChannelOrderStatus = "pay" | "success" | "opening";
export type NewChannelOrder = {
diff --git a/http/http_service.go b/http/http_service.go
index d9c26c53..0e2fb620 100644
--- a/http/http_service.go
+++ b/http/http_service.go
@@ -106,6 +106,9 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
e.POST("/api/wallet/redeem-onchain-funds", httpSvc.redeemOnchainFundsHandler, authMiddleware)
e.POST("/api/wallet/sign-message", httpSvc.signMessageHandler, authMiddleware)
e.POST("/api/wallet/sync", httpSvc.walletSyncHandler, authMiddleware)
+ e.POST("/api/wallet/send", httpSvc.sendHandler, authMiddleware)
+ e.POST("/api/wallet/receive", httpSvc.receiveHandler, authMiddleware)
+ e.GET("/api/transactions", httpSvc.transactionsHandler, authMiddleware)
e.GET("/api/balances", httpSvc.balancesHandler, authMiddleware)
e.POST("/api/reset-router", httpSvc.resetRouterHandler, authMiddleware)
e.POST("/api/stop", httpSvc.stopHandler, authMiddleware)
@@ -393,6 +396,58 @@ func (httpSvc *HttpService) balancesHandler(c echo.Context) error {
return c.JSON(http.StatusOK, balances)
}
+func (httpSvc *HttpService) sendHandler(c echo.Context) error {
+ var walletSendRequest api.WalletSendRequest
+ if err := c.Bind(&walletSendRequest); err != nil {
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Message: fmt.Sprintf("Bad request: %s", err.Error()),
+ })
+ }
+
+ resp, err := httpSvc.api.SendPayment(c.Request().Context(), walletSendRequest.Invoice)
+
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, ErrorResponse{
+ Message: err.Error(),
+ })
+ }
+
+ return c.JSON(http.StatusOK, resp)
+}
+
+func (httpSvc *HttpService) receiveHandler(c echo.Context) error {
+ var walletReceiveRequest api.WalletReceiveRequest
+ if err := c.Bind(&walletReceiveRequest); err != nil {
+ return c.JSON(http.StatusBadRequest, ErrorResponse{
+ Message: fmt.Sprintf("Bad request: %s", err.Error()),
+ })
+ }
+
+ invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), walletReceiveRequest.Amount, walletReceiveRequest.Description)
+
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, ErrorResponse{
+ Message: err.Error(),
+ })
+ }
+
+ return c.JSON(http.StatusOK, invoice)
+}
+
+func (httpSvc *HttpService) transactionsHandler(c echo.Context) error {
+ ctx := c.Request().Context()
+
+ transactions, err := httpSvc.api.GetTransactions(ctx)
+
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, ErrorResponse{
+ Message: err.Error(),
+ })
+ }
+
+ return c.JSON(http.StatusOK, transactions)
+}
+
func (httpSvc *HttpService) walletSyncHandler(c echo.Context) error {
httpSvc.api.SyncWallet()