mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-21 08:14:27 +01:00
kitchen sink peer connect and channel create
This commit is contained in:
@@ -1,20 +1,198 @@
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
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 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() {
|
export default function KitchenSink() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore()
|
||||||
|
|
||||||
// TODO: would be nice if this was just newest unused address
|
// TODO: would be nice if this was just newest unused address
|
||||||
const getNewAddress = async () => {
|
const getNewAddress = async () => {
|
||||||
if (state.node_manager) {
|
return await state.node_manager?.get_new_address();
|
||||||
console.log("Getting new address");
|
|
||||||
const address = await state.node_manager?.get_new_address();
|
|
||||||
return address
|
|
||||||
} else {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [address] = createResource(getNewAddress);
|
const [address] = createResource(getNewAddress);
|
||||||
@@ -23,12 +201,10 @@ export default function KitchenSink() {
|
|||||||
<Card title="Kitchen Sink">
|
<Card title="Kitchen Sink">
|
||||||
<PeerConnectModal />
|
<PeerConnectModal />
|
||||||
<ButtonLink target="_blank" rel="noopener noreferrer" href={`https://faucet.mutinynet.com/?address=${address()}`}>Tap the Faucet</ButtonLink>
|
<ButtonLink target="_blank" rel="noopener noreferrer" href={`https://faucet.mutinynet.com/?address=${address()}`}>Tap the Faucet</ButtonLink>
|
||||||
<SmallHeader>
|
<Hr />
|
||||||
Peers
|
<PeersList />
|
||||||
</SmallHeader>
|
<Hr />
|
||||||
|
<ChannelsList />
|
||||||
|
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ParentComponent } from "solid-js"
|
import { ParentComponent } from "solid-js"
|
||||||
import Linkify from "./Linkify"
|
import Linkify from "./Linkify"
|
||||||
import { Button, ButtonLink } from "./Button"
|
import { Button, ButtonLink } from "./Button"
|
||||||
|
import { Separator } from "@kobalte/core"
|
||||||
|
|
||||||
const SmallHeader: ParentComponent = (props) => <header class='text-sm font-semibold uppercase'>{props.children}</header>
|
const SmallHeader: ParentComponent = (props) => <header class='text-sm font-semibold uppercase'>{props.children}</header>
|
||||||
|
|
||||||
@@ -34,4 +35,6 @@ const LoadingSpinner = () => {
|
|||||||
</div>);
|
</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
10
src/utils/eify.ts
Normal 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
21
src/utils/mempoolTxUrl.ts
Normal 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}`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user