kitchen sink peer connect and channel create

This commit is contained in:
Paul Miller
2023-04-11 10:10:18 -05:00
parent 9173701c13
commit f5b48c28b2
4 changed files with 226 additions and 16 deletions

View File

@@ -1,20 +1,198 @@
import { useMegaStore } from "~/state/megaStore";
import { ButtonLink, Card, SmallHeader } from "~/components/layout";
import { ButtonLink, Card, Hr, SmallHeader, Button } from "~/components/layout";
import PeerConnectModal from "~/components/PeerConnectModal";
import { createResource } from "solid-js";
import { For, Show, Suspense, createResource, createSignal } from "solid-js";
import { MutinyChannel, MutinyPeer } from "@mutinywallet/node-manager";
import { TextField } from "@kobalte/core";
import mempoolTxUrl from "~/utils/mempoolTxUrl";
import eify from "~/utils/eify";
function PeersList() {
const [state, _] = useMegaStore()
const getPeers = async () => {
return await state.node_manager?.list_peers() as Promise<MutinyPeer[]>
};
const [peers, { refetch }] = createResource(getPeers);
return (
<>
<SmallHeader>
Peers
</SmallHeader>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense>
<For each={peers()} fallback={<code>No peers</code>}>
{(peer) => (
<pre class="overflow-x-auto whitespace-pre-line break-all">
{JSON.stringify(peer, null, 2)}
</pre>
)}
</For>
</Suspense>
<Button layout="small" onClick={refetch}>Refresh Peers</Button>
<ConnectPeer refetchPeers={refetch} />
</>
)
}
function ConnectPeer(props: { refetchPeers: () => any }) {
const [state, _] = useMegaStore()
const [value, setValue] = createSignal("");
const onSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const peerConnectString = value().trim();
const nodes = await state.node_manager?.list_nodes();
const firstNode = nodes[0] as string || ""
await state.node_manager?.connect_to_peer(firstNode, peerConnectString)
await props.refetchPeers()
setValue("");
};
return (
<form class="border border-white/20 rounded-xl p-2 flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={value()}
onValueChange={setValue}
validationState={(value() == "" || value().startsWith("mutiny:")) ? "valid" : "invalid"}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Connect Peer</TextField.Label>
<TextField.Input class="w-full p-2 rounded-lg text-black" placeholder="mutiny:028241..." />
<TextField.ErrorMessage class="text-red-500">Expecting something like mutiny:abc123...</TextField.ErrorMessage>
</TextField.Root>
<Button layout="small" type="submit">Connect</Button>
</form >
)
}
function ChannelsList() {
const [state, _] = useMegaStore()
const getChannels = async () => {
return await state.node_manager?.list_channels() as Promise<MutinyChannel[]>
};
const [channels, { refetch }] = createResource(getChannels);
const network = state.node_manager?.get_network();
return (
<>
<SmallHeader>
Channels
</SmallHeader>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense>
<For each={channels()} fallback={<code>No channels</code>}>
{(channel) => (
<>
<pre class="overflow-x-auto whitespace-pre-line break-all">
{JSON.stringify(channel, null, 2)}
</pre>
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(channel.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
</>
)}
</For>
</Suspense>
<Button type="button" layout="small" onClick={(e) => { e.preventDefault(); refetch() }}>Refresh Channels</Button>
<OpenChannel refetchChannels={refetch} />
</>
)
}
function OpenChannel(props: { refetchChannels: () => any }) {
const [state, _] = useMegaStore()
const [creationError, setCreationError] = createSignal<Error>();
const [amount, setAmount] = createSignal("");
const [peerPubkey, setPeerPubkey] = createSignal("");
const [newChannel, setNewChannel] = createSignal<MutinyChannel>();
const onSubmit = async (e: SubmitEvent) => {
e.preventDefault();
// TODO: figure out why this doesn't catch the rust error
// src/logging.rs:29
// ERROR: Could not create a signed transaction to open channel with: The invoice or address is on a different network.
try {
const pubkey = peerPubkey().trim();
const bigAmount = BigInt(amount());
const nodes = await state.node_manager?.list_nodes();
const firstNode = nodes[0] as string || ""
const new_channel = await state.node_manager?.open_channel(firstNode, pubkey, bigAmount)
setNewChannel(new_channel)
await props.refetchChannels()
setAmount("");
setPeerPubkey("");
} catch (e) {
setCreationError(eify(e))
}
};
return (
<>
<form class="border border-white/20 rounded-xl p-2 flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={peerPubkey()}
onValueChange={setPeerPubkey}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Pubkey</TextField.Label>
<TextField.Input class="w-full p-2 rounded-lg text-black" />
</TextField.Root>
<TextField.Root
value={amount()}
onValueChange={setAmount}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Amount</TextField.Label>
<TextField.Input
type="number"
class="w-full p-2 rounded-lg text-black" />
</TextField.Root>
<Button layout="small" type="submit">Open Channel</Button>
</form >
<Show when={newChannel()}>
<pre class="overflow-x-auto whitespace-pre-line break-all">
{JSON.stringify(newChannel()?.outpoint, null, 2)}
</pre>
<pre>{newChannel()?.outpoint}</pre>
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], "signet")} target="_blank" rel="noreferrer">
Mempool Link
</a>
</Show>
<Show when={creationError()}>
<pre>{creationError()?.message}</pre>
</Show>
</>
)
}
export default function KitchenSink() {
const [state, _] = useMegaStore()
// TODO: would be nice if this was just newest unused address
const getNewAddress = async () => {
if (state.node_manager) {
console.log("Getting new address");
const address = await state.node_manager?.get_new_address();
return address
} else {
return undefined
}
return await state.node_manager?.get_new_address();
};
const [address] = createResource(getNewAddress);
@@ -23,12 +201,10 @@ export default function KitchenSink() {
<Card title="Kitchen Sink">
<PeerConnectModal />
<ButtonLink target="_blank" rel="noopener noreferrer" href={`https://faucet.mutinynet.com/?address=${address()}`}>Tap the Faucet</ButtonLink>
<SmallHeader>
Peers
</SmallHeader>
<Hr />
<PeersList />
<Hr />
<ChannelsList />
</Card>
)
}

View File

@@ -1,6 +1,7 @@
import { ParentComponent } from "solid-js"
import Linkify from "./Linkify"
import { Button, ButtonLink } from "./Button"
import { Separator } from "@kobalte/core"
const SmallHeader: ParentComponent = (props) => <header class='text-sm font-semibold uppercase'>{props.children}</header>
@@ -34,4 +35,6 @@ const LoadingSpinner = () => {
</div>);
}
export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify }
const Hr = () => <Separator.Root class="my-4 border-white/20" />
export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify, Hr }

10
src/utils/eify.ts Normal file
View File

@@ -0,0 +1,10 @@
/// Sometimes we catch an error as `unknown` so this turns it into an Error.
export default function eify(e: unknown): Error {
if (e instanceof Error) {
return e;
} else if (typeof e === 'string') {
return new Error(e);
} else {
return new Error('Unknown error');
}
}

21
src/utils/mempoolTxUrl.ts Normal file
View File

@@ -0,0 +1,21 @@
export default function mempoolTxUrl(txid?: string, network?: string) {
if (!txid || !network) {
console.error("Problem creating the mempool url")
return "#"
}
if (network) {
switch (network) {
case "mainnet":
return `https://mempool.space/tx/${txid}`
case "testnet":
return `https://mempool.space/testnet/tx/${txid}`
case "signet":
return `https://mutinynet.com/tx/${txid}`
default:
return `https://mempool.space/tx/${txid}`
}
}
return `https://mempool.space/tx/${txid}`
}