Skip to content

Commit

Permalink
feat: send and receive functionality (#465)
Browse files Browse the repository at this point in the history
Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
  • Loading branch information
3 people authored Jun 28, 2024
1 parent fa2e70f commit 6a56ea4
Show file tree
Hide file tree
Showing 26 changed files with 1,142 additions and 98 deletions.
39 changes: 39 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,45 @@ func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) {
return balances, nil
}

// TODO: accept offset, limit params for pagination
func (api *api) ListTransactions(ctx context.Context) (*ListTransactionsResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transactions, err := api.svc.GetLNClient().ListTransactions(ctx, 0, 0, 20, 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) (*MakeInvoiceResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
invoice, err := api.svc.GetLNClient().MakeInvoice(ctx, amount, description, "", 0)
return invoice, err
}

func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
invoice, err := api.svc.GetLNClient().LookupInvoice(ctx, paymentHash)
return invoice, err
}

// TODO: remove dependency on this endpoint
func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) {
url := api.cfg.GetEnv().MempoolApi + endpoint
Expand Down
14 changes: 14 additions & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ 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)
ListTransactions(ctx context.Context) (*ListTransactionsResponse, error)
SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error)
CreateInvoice(ctx context.Context, amount int64, description string) (*MakeInvoiceResponse, error)
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
RequestMempoolApi(endpoint string) (interface{}, error)
GetInfo(ctx context.Context) (*InfoResponse, error)
GetEncryptedMnemonic() *EncryptedMnemonicResponse
Expand Down Expand Up @@ -179,6 +183,11 @@ type RedeemOnchainFundsResponse struct {
type OnchainBalanceResponse = lnclient.OnchainBalanceResponse
type BalancesResponse = lnclient.BalancesResponse

type SendPaymentResponse = lnclient.PayInvoiceResponse
type MakeInvoiceResponse = lnclient.Transaction
type LookupInvoiceResponse = lnclient.Transaction
type ListTransactionsResponse = []lnclient.Transaction

// debug api
type SendPaymentProbesRequest struct {
Invoice string `json:"invoice"`
Expand Down Expand Up @@ -219,6 +228,11 @@ type SignMessageResponse struct {
Signature string `json:"signature"`
}

type MakeInvoiceRequest struct {
Amount int64 `json:"amount"`
Description string `json:"description"`
}

type ResetRouterRequest struct {
Key string `json:"key"`
}
Expand Down
110 changes: 110 additions & 0 deletions frontend/src/components/TransactionsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import dayjs from "dayjs";
import { ArrowDownIcon, ArrowUpIcon, Drum } from "lucide-react";
import EmptyState from "src/components/EmptyState";

import Loading from "src/components/Loading";
import { useTransactions } from "src/hooks/useTransactions";

function TransactionsList() {
const { data: transactions, isLoading } = useTransactions();

if (isLoading) {
return <Loading />;
}

return (
<div>
{!transactions?.length ? (
<EmptyState
icon={Drum}
title="No transactions yet"
description="Your most recent incoming and outgoing payments will show up here."
buttonText="Receive Your First Payment"
buttonLink="/wallet/receive"
/>
) : (
<>
{transactions?.map((tx, i) => {
const type = tx.type;

return (
<div
key={`tx-${i}`}
className="p-3 mb-4 rounded-md"
// TODO: Add modal onclick to show payment details
>
<div className="flex gap-3">
<div className="flex items-center">
{type == "outgoing" ? (
<div
className={
"flex justify-center items-center bg-orange-100 dark:bg-orange-950 rounded-full w-10 h-10 md:w-14 md:h-14"
}
>
<ArrowUpIcon
strokeWidth={3}
className="w-6 h-6 md:w-8 md:h-8 text-orange-400 dark:text-amber-600 stroke-orange-400 dark:stroke-amber-600"
/>
</div>
) : (
<div className="flex justify-center items-center bg-green-100 dark:bg-emerald-950 rounded-full w-10 h-10 md:w-14 md:h-14">
<ArrowDownIcon
strokeWidth={3}
className="w-6 h-6 md:w-8 md:h-8 text-green-500 dark:text-emerald-500 stroke-green-400 dark:stroke-emerald-500"
/>
</div>
)}
</div>
<div className="overflow-hidden mr-3">
<div className="flex items-center gap-2 truncate dark:text-white">
<p className="text-lg md:text-xl font-semibold">
{type == "incoming" ? "Received" : "Sent"}
</p>
<p className="text-sm md:text-base truncate text-muted-foreground">
{dayjs(tx.settled_at * 1000).fromNow()}
</p>
</div>
<p className="text-sm md:text-base text-muted-foreground">
{tx.description || "Lightning invoice"}
</p>
</div>
<div className="flex ml-auto text-right space-x-3 shrink-0 dark:text-white">
<div className="flex items-center gap-2 text-xl">
<p
className={`font-semibold ${
type == "incoming" &&
"text-green-600 dark:color-green-400"
}`}
>
{type == "outgoing" ? "-" : "+"}{" "}
{Math.floor(tx.amount / 1000)}
</p>
<p className="text-muted-foreground">sats</p>

{/* {!!tx.totalAmountFiat && (
<p className="text-xs text-gray-400 dark:text-neutral-600">
~{tx.totalAmountFiat}
</p>
)} */}
</div>
</div>
</div>
</div>
);
})}
{/* {transaction && (
<TransactionModal
transaction={transaction}
isOpen={modalOpen}
onClose={() => {
setModalOpen(false);
}}
/>
)} */}
</>
)}
</div>
);
}

export default TransactionsList;
5 changes: 5 additions & 0 deletions frontend/src/components/layouts/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
EllipsisVertical,
ExternalLinkIcon,
FlaskRound,
Home,
Lock,
Megaphone,
Menu,
Expand Down Expand Up @@ -115,6 +116,10 @@ export default function AppLayout() {
function MainMenuContent() {
return (
<>
<MenuItem to="/home">
<Home className="h-4 w-4" />
Home
</MenuItem>
<MenuItem to="/wallet">
<Wallet className="h-4 w-4" />
Wallet
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/redirects/HomeRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function HomeRedirect() {
setTimeout(() => {
window.localStorage.removeItem(localStorageKeys.returnTo);
}, 100);
to = returnTo || "/wallet";
to = returnTo || "/home";
} else {
to = "/alby/auth";
}
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/hooks/useTransaction.ts
Original file line number Diff line number Diff line change
@@ -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 useTransaction(paymentHash: string, poll = false) {
return useSWR<Transaction>(
paymentHash && `/api/transactions/${paymentHash}`,
swrFetcher,
poll ? pollConfiguration : undefined
);
}
16 changes: 16 additions & 0 deletions frontend/src/hooks/useTransactions.ts
Original file line number Diff line number Diff line change
@@ -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<Transaction[]>(
"/api/transactions",
swrFetcher,
poll ? pollConfiguration : undefined
);
}
24 changes: 24 additions & 0 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { StartRedirect } from "src/components/redirects/StartRedirect";
import { BackupMnemonic } from "src/screens/BackupMnemonic";
import { BackupNode } from "src/screens/BackupNode";
import { BackupNodeSuccess } from "src/screens/BackupNodeSuccess";
import Home from "src/screens/Home";
import { Intro } from "src/screens/Intro";
import NotFound from "src/screens/NotFound";
import Start from "src/screens/Start";
Expand Down Expand Up @@ -48,6 +49,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 = [
Expand All @@ -60,6 +63,17 @@ const routes = [
index: true,
element: <HomeRedirect />,
},
{
path: "home",
element: <DefaultRedirect />,
handle: { crumb: () => "Dashboard" },
children: [
{
index: true,
element: <Home />,
},
],
},
{
path: "wallet",
element: <DefaultRedirect />,
Expand All @@ -69,6 +83,16 @@ const routes = [
index: true,
element: <Wallet />,
},
{
path: "receive",
element: <Receive />,
handle: { crumb: () => "Receive" },
},
{
path: "send",
element: <Send />,
handle: { crumb: () => "Send" },
},
{
path: "sign-message",
element: <SignMessage />,
Expand Down
Loading

0 comments on commit 6a56ea4

Please sign in to comment.