Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 29 additions & 22 deletions ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,41 @@ import LiquidityDisplay from './components/status/LiquidityDisplay';
import ChannelDisplay from './components/status/ChannelDisplay';
import { VoltProviderContextProvider } from './contexts/VoltProviderContext';
import { HotWalletContextProvider } from './contexts/HotWalletContext';
import { PaymentUpdateContextProvider } from './contexts/PaymentUpdateContext';
import { ApiProvider } from './contexts/ApiContext';

export function App() {
return (
<FeedbackContextProvider>
<HotWalletContextProvider>
<ChannelContextProvider>
<VoltProviderContextProvider>
<InvoiceContextProvider>
<main className="bg-slate-200 h-screen">
<div className="flex h-full overflow-y-hidden">
<div className="bg-zinc-100 w-1/2 pt-20">
<Commands />
</div>
<div className='flex flex-col h-full w-1/2'>
<div className='h-1/3 bg-zinc-800 overflow-scroll rounded-b-sm'>
<CommandFeedback />
<ApiProvider>
<HotWalletContextProvider>
<ChannelContextProvider>
<PaymentUpdateContextProvider>
<VoltProviderContextProvider>
<InvoiceContextProvider>
<main className="bg-slate-200 h-screen">
<div className="flex h-full overflow-y-hidden">
<div className="bg-zinc-100 w-1/2 pt-20">
<Commands />
</div>
<div className='h-2/3 bg-zinc-300 text-gray-500 rounded-sm'>
<LiquidityDisplay/>
<ChannelDisplay />
<div className='flex flex-col h-full w-1/2'>
<div className='h-1/3 bg-zinc-800 overflow-scroll rounded-b-sm'>
<CommandFeedback />
</div>
<div className='h-2/3 bg-zinc-300 text-gray-500 rounded-sm'>
<LiquidityDisplay/>
<ChannelDisplay />
</div>
</div>
</div>
</div>
</div>
</main>
</InvoiceContextProvider>
</VoltProviderContextProvider>
</ChannelContextProvider>
</HotWalletContextProvider>
</main>
</InvoiceContextProvider>
</VoltProviderContextProvider>
</PaymentUpdateContextProvider>
</ChannelContextProvider>
</HotWalletContextProvider>
</ApiProvider>
</FeedbackContextProvider>

);
}
14 changes: 6 additions & 8 deletions ui/src/components/commands/InvoiceAndPay.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React, { useState, useContext, memo } from 'react';
import React, { useState, useContext } from 'react';
import Urbit from '@urbit/http-api';
import { isValidPatp, preSig } from '@urbit/aura'
import Button from './shared/Button';
import { FeedbackContext } from '../../contexts/FeedbackContext';
import Command from '../../types/Command';
import Input from './shared/Input';
import CommandForm from './shared/CommandForm';
import BitcoinAmount from '../../types/BitcoinAmount';
import Dropdown from './shared/Dropdown';
import Network from '../../types/Network';

const InvoiceAndPay = ({ api }: { api: Urbit }) => {
const { displayCommandSuccess, displayCommandError, displayJsError } = useContext(FeedbackContext);
const { displayJsError, displayJsSuccess } = useContext(FeedbackContext);

const [shipInput, setShipInput] = useState('~');
const [ship, setShip] = useState<string | null>(null);
Expand Down Expand Up @@ -51,7 +50,7 @@ const InvoiceAndPay = ({ api }: { api: Urbit }) => {
displayJsError('Invalid ship');
valid = false;
} else if (ship === api.ship || ship === `~${api.ship}`) {
displayJsError("Cannot invoice and pau self")
displayJsError("Cannot invoice and pay self")
valid = false;
}
if (!amount) {
Expand All @@ -68,9 +67,8 @@ const InvoiceAndPay = ({ api }: { api: Urbit }) => {
const invoiceAndPay = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateInvoiceAndPayParams()) return;
console.log('ship', ship);
try {
const res = await api.thread({
await api.thread({
inputMark: 'volt-invoice-and-pay-params',
outputMark: 'volt-update',
threadName: 'api-invoice-and-pay',
Expand All @@ -83,9 +81,9 @@ const InvoiceAndPay = ({ api }: { api: Urbit }) => {
}
}
)
console.log('thread done', res);
displayJsSuccess('Thread !api-invoice-and-pay succeeded');
} catch (e) {
displayJsError('Error sending payment');
displayJsError('Error running thread !api-invoice-and-pay');
console.error(e);
}
};
Expand Down
62 changes: 58 additions & 4 deletions ui/src/components/commands/SendPayment.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,60 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useMemo } from 'react';
import Urbit from '@urbit/http-api';
import { isValidPatp, preSig } from '@urbit/aura'
import Button from './shared/Button';
import { FeedbackContext } from '../../contexts/FeedbackContext';
import Command from '../../types/Command';
import Input from './shared/Input';
import CommandForm from './shared/CommandForm';
import BitcoinAmount from '../../types/BitcoinAmount';
import Text from './shared/Text';
import { PayreqAmountScryResponse } from '../../types/Response';

type DecodedPayreqAmountScryResponse = {
amount: BitcoinAmount | null,
isValid: boolean
}

const SendPayment = ({ api }: { api: Urbit }) => {
const { displayCommandSuccess, displayCommandError, displayJsError } = useContext(FeedbackContext);
const { displayCommandSuccess, displayCommandError, displayJsError, displayJsSuccess } = useContext(FeedbackContext);

const [payreq, setPayreq] = useState('');
const [payreqValid, setPayreqValid] = useState<boolean | null>(null);
const [payreqAmount, setPayreqAmount] = useState<BitcoinAmount | null>(null);
const [shipInput, setShipInput] = useState('~');
const [ship, setShip] = useState<string | null>(null);

const onChangePayreq = (e: React.ChangeEvent<HTMLInputElement>) => {
const getPayreqAmount = async (invoiceString: string): Promise<DecodedPayreqAmountScryResponse> => {
try {
const response: PayreqAmountScryResponse = await api.scry({
app: "volt",
path: `/utils/payreq/amount/${invoiceString}`,
});
displayJsSuccess(`Scry /utils/payreq/amount succeeded`);
const amount = (response.msats === null) ? null : new BitcoinAmount(response.msats);
return { amount, isValid: response['is-valid'] };
} catch (e) {
console.error(e);
displayJsError(`Scry /utils/payreq/amount failed`);
throw e;
}
}

const onChangePayreq = async (e: React.ChangeEvent<HTMLInputElement>) => {
setPayreq(e.target.value);
if (e.target.value === '') {
setPayreqValid(null);
setPayreqAmount(null);
return;
}
try {
const { amount, isValid } = await getPayreqAmount(e.target.value);
setPayreqAmount(amount);
setPayreqValid(isValid);
} catch (e) {
setPayreqValid(null);
setPayreqAmount(null);
}
};

const onChangeShipInput = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -29,7 +68,7 @@ const SendPayment = ({ api }: { api: Urbit }) => {

const sendPayment = (e: React.FormEvent) => {
e.preventDefault();
if (!payreq) {
if (!payreq || !payreqValid || !payreqAmount) {
displayJsError("Payreq required");
return;
}
Expand All @@ -52,13 +91,28 @@ const SendPayment = ({ api }: { api: Urbit }) => {
}
};

const payreqText: string | null = useMemo(() => {
let text = 'Amount: ~';
if (payreqValid === true) {
if (payreqAmount) {
text = `Amount: ${payreqAmount.displayAsSats()}`;
} else {
text = `Payreq doesn't specify amount`;
}
} else if (payreqValid === false) {
text = 'Payreq is invalid';
}
return text;
}, [payreqValid, payreqAmount]);

return (
<CommandForm>
<Input
label={"Payreq"}
value={payreq}
onChange={onChangePayreq}
/>
{payreqText ? <Text className='py-2' text={payreqText} /> : null}
<Input
label={"Ship (optional)"}
value={shipInput}
Expand Down
23 changes: 22 additions & 1 deletion ui/src/contexts/ApiContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext } from 'react';
import React, { createContext, useEffect } from 'react';
import Urbit from '@urbit/http-api';

type ApiContextValue = Urbit;
Expand All @@ -9,6 +9,27 @@ api.ship = process.env.VITE_SHIP_NAME || window.ship;
export const ApiContext = createContext<ApiContextValue>(api);

export const ApiProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {

useEffect(() => {
const redirectToAuthIfNotLoggedIn = async () => {
try {
await api.scry({
app: "volt",
path: "/hot-wallet-fee",
});
} catch (e: any) {
if (e?.status === 403) {
document.location = `${document.location.protocol}//${document.location.host}`;
}
}
}
// Redirect to the auth page happens automatically if this is running from a glob
// So this is only useful for local development
if (process.env.VITE_SHIP_NAME) {
redirectToAuthIfNotLoggedIn();
}
}, []);

return (
<ApiContext.Provider value={api}>
{children}
Expand Down
33 changes: 32 additions & 1 deletion ui/src/contexts/ChannelContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface ChannelContextValue {
inboundCapacity: BitcoinAmount;
outboundCapacity: BitcoinAmount;
channels: Array<Channel>;
refreshChannelState: () => void;
channelsByStatus: {
preopening: Channel[];
opening: Channel[];
Expand All @@ -37,6 +38,7 @@ export const ChannelContext = createContext<ChannelContextValue>({
inboundCapacity: new BitcoinAmount(0),
outboundCapacity: new BitcoinAmount(0),
channels: [],
refreshChannelState: () => {},
channelsByStatus: {
preopening: [],
opening: [],
Expand Down Expand Up @@ -91,6 +93,34 @@ export const ChannelContextProvider: React.FC<{ children: React.ReactNode }> = (
return channelsByStatus.preopening;
}, [channelsByStatus]);


const refreshChannelState = () => {
// Refresh channel state after a short delay; otherwise, in most cases we would refresh
// e.g. after sending payment, balances won't be updated
setTimeout(async () => {
try {
const response = await api.scry({
app: "volt",
path: "/chan-state",
});
displayJsSuccess('Scry /chan-state succeeded');
const { chans: jsonChans }: { 'chans': Array<ChannelJson> } = response;
const channels: Array<Channel> = jsonChans.map((chan) => {
return {
...chan,
his: new BitcoinAmount(chan.his),
our: new BitcoinAmount(chan.our),
}
});
setChannels(channels);
} catch (e) {
console.error(e);
displayJsError('Scry /chan-state failed');
}
}, 1000);
};


useEffect(() => {
const handleAllUpdate = (update: Update) => {
if (!subscriptionSuccessful.current) {
Expand All @@ -110,7 +140,7 @@ export const ChannelContextProvider: React.FC<{ children: React.ReactNode }> = (
displayJsInfo('Got new channel update from /all');
handleNewChannel(update as NewChannelUpdate);
} else if (update.type === UpdateType.TempChanUpgraded) {
displayJsInfo('Got channel deleted update from /all');
displayJsInfo('Got channel upgraded update from /all');
handleTemporaryChannelUpgraded(update as TempChanUpgradedUpdate);
} else {
console.log('Unimplemented update type', update);
Expand Down Expand Up @@ -228,6 +258,7 @@ export const ChannelContextProvider: React.FC<{ children: React.ReactNode }> = (

const value = {
channels,
refreshChannelState,
channelsByStatus,
inboundCapacity,
outboundCapacity,
Expand Down
3 changes: 2 additions & 1 deletion ui/src/contexts/HotWalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { createContext, useContext, useEffect, useRef, useState } from 'r
import BitcoinAmount from '../types/BitcoinAmount';
import { ApiContext } from './ApiContext';
import { FeedbackContext } from './FeedbackContext';
import { HotWalletFeeScryResponse } from '../types/Response';

interface HotWalletContextValue {
hotWalletFee: BitcoinAmount | null;
Expand All @@ -22,7 +23,7 @@ export const HotWalletContextProvider: React.FC<{ children: React.ReactNode }> =
if (scryInProgress.current) return;
scryInProgress.current = true;
try {
const response = await api.scry({
const response: HotWalletFeeScryResponse = await api.scry({
app: "volt",
path: "/hot-wallet-fee",
});
Expand Down
Loading