Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
feat: send and receive functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
im-adithya committed Jun 19, 2024
1 parent 118c687 commit 8eca617
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 2 deletions.
33 changes: 33 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
}
Expand Down
102 changes: 102 additions & 0 deletions frontend/src/components/TransactionsList.tsx
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
}

console.info(transactions);

return (
<div>
{!transactions?.length ? (
<p className="text-center py-16 text-gray-500 dark:text-neutral-400">
No transactions, yet.
{/* Deposit Bitcoin Button */}
</p>
) : (
<>
{transactions?.map((tx, i) => {
const type = tx.type;

return (
<div
key={`tx-${i}`}
className="p-3 mb-4 hover:bg-gray-100 dark:hover:bg-surface-02dp cursor-pointer rounded-md"
// onClick={() => openDetails(tx)}
>
<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-14 h-14">
<ArrowUpIcon
strokeWidth={3}
className="w-8 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-14 h-14">
<ArrowDownIcon
strokeWidth={3}
className="w-8 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="text-xl font-semibold text-black truncate dark:text-white">
<p className="truncate">
{type == "incoming" ? "Received" : "Sent"}
</p>
</div>
<p className="text-muted-foreground">
{dayjs(tx.settled_at * 1000).fromNow()}
{tx.description}
</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;
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
);
}
12 changes: 12 additions & 0 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -68,6 +70,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
96 changes: 96 additions & 0 deletions frontend/src/screens/wallet/Receive.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>();
const [description, setDescription] = React.useState("");

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!csrf) {
throw new Error("csrf not loaded");
}
try {
setLoading(true);
const invoice = await request<Transaction>("/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 (
<div className="grid gap-5">
<AppHeader
title="Receive"
description="Request instant and specific amount bitcoin payments"
/>
<div className="max-w-lg">
<form onSubmit={handleSubmit} className="grid gap-5">
<div>
<Label htmlFor="amount">Amount</Label>
<Input
id="amount"
type="number"
value={amount?.toString()}
placeholder="Amount in Satoshi..."
onChange={(e) => {
setAmount(parseInt(e.target.value));
}}
min={1}
autoFocus
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Input
id="description"
type="text"
value={description}
placeholder="For e.g. who is sending this payment?"
onChange={(e) => {
setDescription(e.target.value);
}}
/>
</div>
<div>
<LoadingButton loading={isLoading} type="submit" disabled={!amount}>
Create Invoice
</LoadingButton>
</div>
</form>
</div>
</div>
);
}
Loading

0 comments on commit 8eca617

Please sign in to comment.