Files
mutiny-web/src/routes/Redshift.tsx
2023-05-23 19:03:52 -05:00

543 lines
17 KiB
TypeScript

import {
createEffect,
createMemo,
createResource,
createSignal,
For,
Match,
onMount,
ParentComponent,
Show,
Suspense,
Switch,
} from "solid-js"
import {
CENTER_COLUMN,
MISSING_LABEL,
REDSHIFT_LABEL,
RIGHT_COLUMN,
THREE_COLUMNS,
UtxoItem,
} from "~/components/Activity"
import {
Card,
DefaultMain,
LargeHeader,
LoadingSpinner,
NiceP,
MutinyWalletGuard,
SafeArea,
SmallAmount,
SmallHeader,
VStack,
} from "~/components/layout"
import { BackLink } from "~/components/layout/BackLink"
import { StyledRadioGroup } from "~/components/layout/Radio"
import NavBar from "~/components/NavBar"
import { useMegaStore } from "~/state/megaStore"
import wave from "~/assets/wave.gif"
import utxoIcon from "~/assets/icons/coin.svg"
import { Button } from "~/components/layout/Button"
import { ProgressBar } from "~/components/layout/ProgressBar"
import { MutinyChannel } from "@mutinywallet/mutiny-wasm"
import mempoolTxUrl from "~/utils/mempoolTxUrl"
import { Amount } from "~/components/Amount"
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels"
import { Network } from "~/logic/mutinyWalletSetup"
type ShiftOption = "utxo" | "lightning"
type ShiftStage = "choose" | "observe" | "success" | "failure"
type OutPoint = string // Replace with the actual TypeScript type for OutPoint
type RedshiftStatus = string // Replace with the actual TypeScript type for RedshiftStatus
type RedshiftRecipient = unknown // Replace with the actual TypeScript type for RedshiftRecipient
type PublicKey = unknown // Replace with the actual TypeScript type for PublicKey
interface RedshiftResult {
id: string
input_utxo: OutPoint
status: RedshiftStatus
recipient: RedshiftRecipient
output_utxo?: OutPoint
introduction_channel?: OutPoint
output_channel?: OutPoint
introduction_node: PublicKey
amount_sats: bigint
change_amt?: bigint
fees_paid: bigint
}
const dummyRedshift: RedshiftResult = {
id: "44036599c37d590899e8d5d920860286",
input_utxo:
"44036599c37d590899e8d5d92086028695d2c2966fdc354ce1da9a9eac610a53:1",
status: "Completed", // Replace with a dummy value for RedshiftStatus
recipient: {}, // Replace with a dummy value for RedshiftRecipient
output_utxo:
"44036599c37d590899e8d5d92086028695d2c2966fdc354ce1da9a9eac610a53:1",
introduction_channel:
"a7773e57f8595848a635e9af105927cac9ecaf292d71a76456ae0455bd3c9c64:0",
output_channel:
"a7773e57f8595848a635e9af105927cac9ecaf292d71a76456ae0455bd3c9c64:0",
introduction_node: {}, // Replace with a dummy value for PublicKey
amount_sats: BigInt(1000000),
change_amt: BigInt(12345),
fees_paid: BigInt(2500),
}
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
const [state, _actions] = useMegaStore()
const getUtXos = async () => {
console.log("Getting utxos")
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]
}
// function findUtxoByOutpoint(
// outpoint?: string,
// utxos: UtxoItem[] = []
// ): UtxoItem | undefined {
// if (!outpoint) return undefined
// return utxos.find((utxo) => utxo.outpoint === outpoint)
// }
const [_utxos, { refetch: _refetchUtxos }] = createResource(getUtXos)
// const inputUtxo = createMemo(() => {
// console.log(utxos())
// const foundUtxo = findUtxoByOutpoint(props.redshift.input_utxo, utxos())
// console.log("Found utxo:", foundUtxo)
// return foundUtxo
// })
const [redshiftResource, { refetch: _refetchRedshift }] = createResource(
async () => {
console.log("Checking redshift", props.redshift.id)
const redshift = await state.mutiny_wallet?.get_redshift(props.redshift.id)
console.log(redshift)
return redshift
}
)
onMount(() => {
// const interval = setInterval(() => {
// if (redshiftResource()) refetch()
// // if (sentAmount() === 200000) {
// // clearInterval(interval)
// // props.setShiftStage("success");
// // // setSentAmount((0))
// // } else {
// // setSentAmount((sentAmount() + 50000))
// // }
// }, 1000)
})
// const outputUtxo = createMemo(() => {
// return findUtxoByOutpoint(redshiftResource()?.output_utxo, utxos())
// })
createEffect(() => {
setRedshifted(true, redshiftResource()?.output_utxo)
})
const network = state.mutiny_wallet?.get_network() as Network
return (
<VStack biggap>
{/* <VStack>
<NiceP>We did it. Here's your new UTXO:</NiceP>
<Show when={utxos() && outputUtxo()}>
<Card>
<Utxo item={outputUtxo()!} />
</Card>
</Show>
</VStack> */}
<VStack>
<NiceP>What happened?</NiceP>
<Show when={redshiftResource()}>
<Card>
<VStack biggap>
{/* <KV key="Input utxo">
<Show when={utxos() && inputUtxo()}>
<Utxo item={inputUtxo()!} />
</Show>
</KV> */}
<KV key="Starting amount">
<Amount amountSats={redshiftResource()!.amount_sats} />
</KV>
<KV key="Fees paid">
<Amount amountSats={redshiftResource()!.fees_paid} />
</KV>
<KV key="Change">
<Amount amountSats={redshiftResource()!.change_amt} />
</KV>
<KV key="Outbound channel">
<VStack>
<pre class="whitespace-pre-wrap break-all">
{redshiftResource()!.introduction_channel}
</pre>
<a
class=""
href={mempoolTxUrl(
redshiftResource()!.introduction_channel?.split(":")[0],
network
)}
target="_blank"
rel="noreferrer"
>
View on mempool
</a>
</VStack>
</KV>
<Show when={redshiftResource()!.output_channel}>
<KV key="Return channel">
<VStack>
<pre class="whitespace-pre-wrap break-all">
{redshiftResource()!.output_channel}
</pre>
<a
class=""
href={mempoolTxUrl(
redshiftResource()!.output_channel?.split(":")[0],
network
)}
target="_blank"
rel="noreferrer"
>
View on mempool
</a>
</VStack>
</KV>
</Show>
</VStack>
</Card>
</Show>
</VStack>
</VStack>
)
}
const SHIFT_OPTIONS = [
{ value: "utxo", label: "UTXO", caption: "Trade your UTXO for a fresh UTXO" },
{
value: "lightning",
label: "Lightning",
caption: "Convert your UTXO into Lightning",
},
]
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
const redshifted = createMemo(() => getRedshifted(props.item.outpoint))
return (
<>
<div class={THREE_COLUMNS} onClick={() => props.onClick && props.onClick()}>
<div class="flex items-center">
<img src={utxoIcon} alt="coin" />
</div>
<div class={CENTER_COLUMN}>
<div class="flex gap-2">
<Show
when={redshifted()}
fallback={<h2 class={MISSING_LABEL}>Unknown</h2>}
>
<h2 class={REDSHIFT_LABEL}>Redshift</h2>
</Show>
</div>
<SmallAmount amount={props.item.txout.value} />
</div>
<div class={RIGHT_COLUMN}>
<SmallHeader
class={props.item?.is_spent ? "text-m-red" : "text-m-green"}
>
{/* {props.item?.is_spent ? "SPENT" : "UNSPENT"} */}
</SmallHeader>
</div>
</div>
</>
)
}
const FAKE_STATES = [
"Creating a new node",
"Opening a channel",
"Sending funds through",
"Closing the channel",
"Redshift complete",
]
function ShiftObserver(props: {
setShiftStage: (stage: ShiftStage) => void
redshiftId: string
}) {
const [_state, _actions] = useMegaStore()
const [fakeStage, _setFakeStage] = createSignal(2)
const [sentAmount, setSentAmount] = createSignal(0)
onMount(() => {
const interval = setInterval(() => {
if (sentAmount() === 200000) {
clearInterval(interval)
props.setShiftStage("success")
// setSentAmount((0))
} else {
setSentAmount(sentAmount() + 50000)
}
}, 1000)
})
// async function checkRedshift(id: string) {
// console.log("Checking redshift", id)
// const redshift = await state.mutiny_wallet?.get_redshift(id)
// console.log(redshift)
// return redshift
// }
// const [redshiftResource, { refetch }] = createResource(
// props.redshiftId,
// checkRedshift
// )
// onMount(() => {
// const interval = setInterval(() => {
// if (redshiftResource()) refetch();
// // if (sentAmount() === 200000) {
// // clearInterval(interval)
// // props.setShiftStage("success");
// // // setSentAmount((0))
// // } else {
// // setSentAmount((sentAmount() + 50000))
// // }
// }, 1000)
// })
// createEffect(() => {
// const interval = setInterval(() => {
// if (chosenUtxo()) refetch();
// }, 1000); // Poll every second
// onCleanup(() => {
// clearInterval(interval);
// });
// });
return (
<>
<NiceP>Watch it go!</NiceP>
<Card>
<VStack>
<pre class="self-center">{FAKE_STATES[fakeStage()]}</pre>
<ProgressBar value={sentAmount()} max={200000} />
<img src={wave} class="h-4 self-center" alt="sine wave" />
</VStack>
</Card>
</>
)
}
const KV: ParentComponent<{ key: string }> = (props) => {
return (
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold uppercase">{props.key}</p>
{props.children}
</div>
)
}
export default function Redshift() {
const [state, _actions] = useMegaStore()
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose")
const [shiftType, setShiftType] = createSignal<ShiftOption>("utxo")
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>()
const getUtXos = async () => {
console.log("Getting utxos")
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]
}
const getChannels = async () => {
console.log("Getting channels")
await state.mutiny_wallet?.sync()
const channels = (await state.mutiny_wallet?.list_channels()) as Promise<
MutinyChannel[]
>
console.log(channels)
return channels
}
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos)
const [_channels, { refetch: _refetchChannels }] = createResource(getChannels)
const redshiftedUtxos = createMemo(() => {
return utxos()?.filter((utxo) => getRedshifted(utxo.outpoint))
})
const unredshiftedUtxos = createMemo(() => {
return utxos()?.filter((utxo) => !getRedshifted(utxo.outpoint))
})
function resetState() {
setShiftStage("choose")
setShiftType("utxo")
setChosenUtxo(undefined)
}
async function redshiftUtxo(utxo: UtxoItem) {
console.log("Redshifting utxo", utxo.outpoint)
const redshift = await state.mutiny_wallet?.init_redshift(utxo.outpoint)
console.log("Redshift initialized:")
console.log(redshift)
return redshift
}
const [initializedRedshift, { refetch: _refetchRedshift }] = createResource(
chosenUtxo,
redshiftUtxo
)
createEffect(() => {
if (chosenUtxo() && initializedRedshift()) {
// window.location.href = "/"
setShiftStage("observe")
}
})
return (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<BackLink />
<LargeHeader>Redshift (coming soon)</LargeHeader>
<div class="relative filter grayscale pointer-events-none opacity-75">
<VStack biggap>
{/* <pre>{JSON.stringify(redshiftResource(), null, 2)}</pre> */}
<Switch>
<Match when={shiftStage() === "choose"}>
<VStack>
<NiceP>Where is this going?</NiceP>
<StyledRadioGroup
accent="red"
value={shiftType()}
onValueChange={(newValue) =>
setShiftType(newValue as ShiftOption)
}
choices={SHIFT_OPTIONS}
/>
</VStack>
<VStack>
<NiceP>
Choose your{" "}
<span class="inline-block">
<img class="h-4" src={wave} alt="sine wave" />
</span>{" "}
UTXO to begin
</NiceP>
<Suspense>
<Card title="Unshifted UTXOs">
<Switch>
<Match when={utxos.loading}>
<LoadingSpinner wide />
</Match>
<Match
when={
utxos.state === "ready" &&
unredshiftedUtxos()?.length === 0
}
>
<code>No utxos (empty state)</code>
</Match>
<Match
when={
utxos.state === "ready" &&
unredshiftedUtxos() &&
unredshiftedUtxos()!.length >= 0
}
>
<For each={unredshiftedUtxos()}>
{(utxo) => (
<Utxo
item={utxo}
onClick={() => setChosenUtxo(utxo)}
/>
)}
</For>
</Match>
</Switch>
</Card>
</Suspense>
<Suspense>
<Card
titleElement={
<SmallHeader>
<span class="text-m-red">Redshifted </span>UTXOs
</SmallHeader>
}
>
<Switch>
<Match when={utxos.loading}>
<LoadingSpinner wide />
</Match>
<Match
when={
utxos.state === "ready" &&
redshiftedUtxos()?.length === 0
}
>
<code>No utxos (empty state)</code>
</Match>
<Match
when={
utxos.state === "ready" &&
redshiftedUtxos() &&
redshiftedUtxos()!.length >= 0
}
>
<For each={redshiftedUtxos()}>
{(utxo) => <Utxo item={utxo} />}
</For>
</Match>
</Switch>
</Card>
</Suspense>
</VStack>
</Match>
<Match when={shiftStage() === "observe" && chosenUtxo()}>
<ShiftObserver
setShiftStage={setShiftStage}
redshiftId="dummy-redshift"
/>
</Match>
<Match when={shiftStage() === "success" && chosenUtxo()}>
<VStack biggap>
<RedshiftReport
redshift={dummyRedshift}
utxo={chosenUtxo()!}
/>
<Button intent="red" onClick={resetState}>
Nice
</Button>
</VStack>
</Match>
<Match when={shiftStage() === "failure"}>
<NiceP>Oh dear</NiceP>
<NiceP>Here's what happened:</NiceP>
<Button intent="red" onClick={resetState}>
Dangit
</Button>
</Match>
</Switch>
</VStack>
</div>
</DefaultMain>
<NavBar activeTab="redshift" />
</SafeArea>
</MutinyWalletGuard>
)
}