Skip to content

Commit

Permalink
add in initial frontend layout
Browse files Browse the repository at this point in the history
  • Loading branch information
Jwyman328 committed Mar 3, 2024
1 parent ded20f1 commit f9f4faa
Show file tree
Hide file tree
Showing 15 changed files with 432 additions and 57 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions src/app/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.App {
text-align: center;
}

.App-logo {
height: 40vmin;
pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}

.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}

.App-link {
color: #61dafb;
}

@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
69 changes: 69 additions & 0 deletions src/app/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
GetUtxosResponseType,
GetBalanceResponseType,
CurrentFeesResponseType,
} from './types';
async function fetchHandler(url: string, method = 'GET', body?: any) {
const response = await fetch(url, {
method: method,
body: JSON.stringify(body),
});

if (!response.ok) {
throw new Error('Network response was not ok');
}

return response;
}

// TODO move to types file
export type UtxoRequestParam = {
id: string;
vout: number;
};

export class ApiClient {
static async getBalance() {
const response = await fetchHandler('http://localhost:5011/balance');

const data = await response.json();
return data as GetBalanceResponseType;
}
static async getUtxos() {
const response = await fetchHandler('http://localhost:5011/utxos');

const data = await response.json();

return data as GetUtxosResponseType;
}
static async createTxFeeEstimation(
utxos: UtxoRequestParam[],
feeRate: number = 1,
) {
const response = await fetchHandler(
`http://localhost:5011/utxos/fees?feeRate=${feeRate}`,
'POST',
utxos,
);

const data = await response.json();
return data;
}

static async getCurrentFees() {
const response = await fetchHandler('http://localhost:5011/fees/current');
const data = await response.json();
return data as CurrentFeesResponseType;
}

static async initiateWallet(walletDescriptor: string) {
const response = await fetchHandler(
`http://localhost:5011/wallet`,
'POST',
{ descriptor: walletDescriptor },
);

const data = await response.json();
return data;
}
}
21 changes: 21 additions & 0 deletions src/app/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type GetUtxosResponseType = {
utxos: Utxo[];
};

export type Utxo = {
amount: number;
txid: string;
vout: number;
};

export type GetBalanceResponseType = {
confirmed: number;
spendable: number;
total: number;
};

export type CurrentFeesResponseType = {
low: number;
medium: number;
high: number;
};
20 changes: 20 additions & 0 deletions src/app/components/currentFeeRates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useGetCurrentFees } from "../hooks/utxos";
export const CurrentFeeRates = () => {
const getCurrentFeesQueryRequest = useGetCurrentFees();

return (
<div className="mt-4">
{getCurrentFeesQueryRequest.isSuccess ? (
<>
<h2> Current fee rate </h2>
<p> low: {getCurrentFeesQueryRequest.data.low} sat/vB </p>
<p> medium: {getCurrentFeesQueryRequest.data.medium} sat/vB </p>
<p> high: {getCurrentFeesQueryRequest.data.high} sat/vB </p>
</>
) : null}
{getCurrentFeesQueryRequest.isError ? (
<p className="text-red-400"> Error fetch current fee rates </p>
) : null}
</div>
);
};
117 changes: 117 additions & 0 deletions src/app/components/utxosDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useEffect, useState } from "react";
import { UtxoRequestParam } from "../api/api";
import { Utxo } from "../api/types";
import { useCreateTxFeeEstimate } from "../hooks/utxos";

type UtxosDisplayProps = {
utxos: Utxo[];
feeRate: number;
};
export const UtxosDisplay = ({ utxos, feeRate }: UtxosDisplayProps) => {
const [selectedUtxos, setSelectedUtxos] = useState<UtxoRequestParam[]>([]);

const addUtxoToSelectedList = (e: any, txid: string, vout: number) => {
const isAlreadyChecked = selectedUtxos.some((selectedUtxo) => {
return txid === selectedUtxo.id && selectedUtxo.vout === vout;
});
if (isAlreadyChecked) {
const newSelectedUtxos: UtxoRequestParam[] = selectedUtxos.filter(
(selectedUtxo) => {
return selectedUtxo.id !== txid && selectedUtxo.vout !== vout;
},
);
setSelectedUtxos(newSelectedUtxos);
} else {
setSelectedUtxos([...selectedUtxos, { id: txid, vout: vout }]);
}
};

const {
mutate,
isLoading,
data: batchedTxData,
mutateAsync,
} = useCreateTxFeeEstimate(selectedUtxos, feeRate);

const calculateFeeEstimate = async () => {
const response = await mutateAsync();
console.log("response", response);
};

const UtxoDisplay = (utxo: Utxo) => {
const { mutate, data } = useCreateTxFeeEstimate(
[{ id: utxo.txid, vout: utxo.vout }],
feeRate,
);
const fee = data?.fee;
const percentOfTxFee = fee && utxo?.amount ? (fee / utxo.amount) * 100 : 0;
const isSpendable: boolean = data?.spendable;

useEffect(() => {
mutate();
}, [mutate]);
return (
<div>
<p className="text-black">txid: {utxo.txid}</p>
<p className="text-black">txvout: {utxo.vout} </p>
<p className="text-black">
amount: {(utxo.amount / 100000000).toFixed(8)}
</p>

{isSpendable ? (
<div>
<p className="text-black">
fee estimate: {(fee / 100000000).toFixed(8) || "pending"}
</p>
<p className="text-black">
% of tx fee: {percentOfTxFee.toFixed(5).replace(/\.?0+$/, "")}%
</p>
</div>
) : (
<p className="text-red-600">not spendable</p>
)}
<input
type="checkbox"
onClick={(e) => addUtxoToSelectedList(e, utxo.txid, utxo.vout)}
className="mt-3 bg-blue-400"
checked={selectedUtxos.some((selectedUtxo) => {
return (
utxo.txid === selectedUtxo.id && selectedUtxo.vout === utxo.vout
);
})}
/>
</div>
);
};

const DisplayBatchTxData = () => {
const fee = batchedTxData?.fee;
const percentOfTxFee = batchedTxData?.percent_fee_is_of_utxo;
const isSpendable: boolean = batchedTxData?.spendable;

return isSpendable ? (
<div>
<p>batched tx fee: {fee}</p>
<p>batched % of tx fee: {percentOfTxFee}</p>
</div>
) : (
<p className="text-red-600">not spendable</p>
);
};
return (
<div>
<p className="text-blue-600">utxos display</p>
{utxos.map((utxo) => {
return UtxoDisplay(utxo);
})}

<button
className="border rounded border-black"
onClick={calculateFeeEstimate}
>
estimate fee for a transaction using the selected utxos
</button>
{batchedTxData ? <DisplayBatchTxData /> : null}
</div>
);
};
25 changes: 25 additions & 0 deletions src/app/hooks/utxos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiClient, UtxoRequestParam } from "../api/api";
import { useMutation, useQuery } from "react-query";

const queryKeys = {
getBalance: ["getBalance"],
getUtxos: ["getUtxos"],
getCurrentFees: ["getCurrentFees"],
};

export function useGetBalance() {
return useQuery(queryKeys.getBalance, () => ApiClient.getBalance());
}
export function useGetUtxos() {
return useQuery(queryKeys.getUtxos, () => ApiClient.getUtxos());
}
export function useCreateTxFeeEstimate(
utxos: UtxoRequestParam[],
feeRate: number,
) {
return useMutation(() => ApiClient.createTxFeeEstimation(utxos, feeRate));
}

export function useGetCurrentFees() {
return useQuery(queryKeys.getCurrentFees, () => ApiClient.getCurrentFees());
}
17 changes: 17 additions & 0 deletions src/app/hooks/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useMutation } from 'react-query';
import { ApiClient } from '../api/api';

export function useCreateWallet(
descriptor: string,
onSuccess: () => void,
onError: () => void,
) {
return useMutation(() => ApiClient.initiateWallet(descriptor), {
onSuccess: () => {
onSuccess();
},
onError: () => {
onError();
},
});
}
3 changes: 3 additions & 0 deletions src/app/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
1 change: 1 addition & 0 deletions src/app/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/app/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState } from 'react';
import { CurrentFeeRates } from '../components/currentFeeRates';
import { UtxosDisplay } from '../components/utxosDisplay';
import { useGetBalance, useGetUtxos } from '../hooks/utxos';
import { useNavigate } from 'react-router-dom';
// use a component libary for ui components
function Home() {
const getBalanceQueryRequest = useGetBalance();
const navigate = useNavigate();
const getUtxosQueryRequest = useGetUtxos();

const logOut = () => {
navigate('/');
};

const [feeRate, setFeeRate] = useState(1);
return (
<div className="mt-4 h-full">
<div className="mt-4 text-red-400"> Welcome to the family wallet </div>
<p>{getBalanceQueryRequest?.data?.total} sats</p>
<CurrentFeeRates />
<button className="border rounded border-black" onClick={() => {}}>
get utxo fees
</button>
<p>fee rate</p>
<input
type="number"
value={feeRate}
onChange={(e) => setFeeRate(parseInt(e.target.value))}
/>
{getUtxosQueryRequest.isSuccess ? (
<UtxosDisplay
feeRate={feeRate}
utxos={getUtxosQueryRequest?.data?.utxos}
/>
) : null}

<button onClick={logOut}>log out</button>
</div>
);
}

export default Home;
Loading

0 comments on commit f9f4faa

Please sign in to comment.