mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-17 06:14:21 +01:00
format everything with prettier
This commit is contained in:
@@ -1,48 +1,50 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
"extends": [
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:solid/typescript",
|
||||
"plugin:import/typescript",
|
||||
"plugin:import/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"tsconfigRootDir": "./",
|
||||
"project": ["./tsconfig.json"],
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
overrides: [],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: "./",
|
||||
project: ["./tsconfig.json"],
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"solid",
|
||||
"import"
|
||||
plugins: ["@typescript-eslint", "solid", "import"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||
"solid/reactivity": "warn",
|
||||
"solid/no-destructure": "warn",
|
||||
"solid/jsx-no-undef": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"settings": {
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"project": ["./tsconfig.json"],
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
typescript: {
|
||||
project: ["./tsconfig.json"],
|
||||
alwaysTryTypes: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
32
.prettierignore
Normal file
32
.prettierignore
Normal file
@@ -0,0 +1,32 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# PWA dev stuff
|
||||
dev-dist
|
||||
.solid
|
||||
/test-results/
|
||||
/tests-examples/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"printWidth": 80,
|
||||
"useTabs": false
|
||||
}
|
||||
|
||||
24
README.md
24
README.md
@@ -9,9 +9,31 @@ pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Env
|
||||
|
||||
The easiest way to get start with development is to create a file called `.env.local` and copy the contents of `.env.example` into it. This is basically identical to the env that `signet-app.mutinywallet.com` uses.
|
||||
|
||||
### Testing
|
||||
|
||||
We have a couple Playwright e2e tests in the e2e folder. You can run these with:
|
||||
|
||||
```
|
||||
just test
|
||||
```
|
||||
|
||||
Or get a visual look into what's happening:
|
||||
|
||||
```
|
||||
just test-ui
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
Hopefully your editor picks up on the `.prettirrc` file and auto formats accordingly. If you want to format everything in the project run `pnpm run format`.
|
||||
|
||||
### Local
|
||||
|
||||
To make local development easier with a latest local version of [the node manager](https://github.com/MutinyWallet/mutiny-node), you may want to `pnpm link` it.
|
||||
If you want to develop against a local version of [the node manager](https://github.com/MutinyWallet/mutiny-node), you may want to `pnpm link` it.
|
||||
|
||||
Due to how [Vite's dev server works](https://vitejs.dev/config/server-options.html#server-fs-allow), the linked `mutiny-node` project folder should be a sibling of this `mutiny-web` folder. Alternatively you can change the allow path in `vite.config.ts`.
|
||||
|
||||
|
||||
6
justfile
6
justfile
@@ -8,3 +8,9 @@ local:
|
||||
|
||||
remote:
|
||||
pnpm unlink "@mutinywallet/mutiny-wasm" && pnpm install
|
||||
|
||||
test:
|
||||
pnpm exec playwright test
|
||||
|
||||
test-ui:
|
||||
pnpm exec playwright test --ui
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"host": "solid-start dev --host",
|
||||
"build": "solid-start build",
|
||||
"start": "solid-start start",
|
||||
"lint": "eslint src --ext .ts,.tsx,.js"
|
||||
"lint": "eslint src --ext .ts,.tsx,.js",
|
||||
"format": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\""
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
@@ -24,6 +25,7 @@
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-solid": "0.11.0",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.8",
|
||||
"solid-start-node": "^0.2.26",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^4.9.5",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -87,6 +87,9 @@ devDependencies:
|
||||
postcss:
|
||||
specifier: ^8.4.23
|
||||
version: 8.4.23
|
||||
prettier:
|
||||
specifier: ^2.8.8
|
||||
version: 2.8.8
|
||||
solid-start-node:
|
||||
specifier: ^0.2.26
|
||||
version: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.9)
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
export function Back() {
|
||||
return (<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.546 8 8 17.546l9.546 9.546" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
return (
|
||||
<svg
|
||||
width="36"
|
||||
height="36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.546 8 8 17.546l9.546 9.546"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,24 @@
|
||||
export function Paste() {
|
||||
return (<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z" fill="currentColor" />
|
||||
return (
|
||||
<svg
|
||||
width="36"
|
||||
height="36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="currentColor" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z" fill="currentColor" />
|
||||
</svg>)
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
export function Scan() {
|
||||
return (<svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z" fill="currentColor" />
|
||||
</svg>)
|
||||
return (
|
||||
<svg
|
||||
width="37"
|
||||
height="36"
|
||||
viewBox="0 0 37 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ import { DetailsIdModal } from "./DetailsModal";
|
||||
export const THREE_COLUMNS =
|
||||
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0";
|
||||
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full";
|
||||
export const MISSING_LABEL = "py-1 px-2 bg-white/10 rounded inline-block text-sm";
|
||||
export const REDSHIFT_LABEL = "py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
|
||||
export const MISSING_LABEL =
|
||||
"py-1 px-2 bg-white/10 rounded inline-block text-sm";
|
||||
export const REDSHIFT_LABEL =
|
||||
"py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
|
||||
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]";
|
||||
|
||||
export type OnChainTx = {
|
||||
@@ -42,7 +44,10 @@ function UnifiedActivityItem(props: {
|
||||
onClick: (id: string, kind: HackActivityType) => void;
|
||||
}) {
|
||||
const click = () => {
|
||||
props.onClick(props.item.id, props.item.kind as unknown as HackActivityType);
|
||||
props.onClick(
|
||||
props.item.id,
|
||||
props.item.kind as unknown as HackActivityType
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -97,17 +102,25 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
<NiceP>Receive some sats to get started</NiceP>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={props.limit && state.activity.length > props.limit}>
|
||||
<Match
|
||||
when={props.limit && state.activity.length > props.limit}
|
||||
>
|
||||
<For each={state.activity.slice(0, props.limit)}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={state.activity.length >= 0}>
|
||||
<For each={state.activity}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Match, ParentComponent, Switch, createMemo, createResource } from "solid-js";
|
||||
import {
|
||||
Match,
|
||||
ParentComponent,
|
||||
Switch,
|
||||
createMemo,
|
||||
createResource
|
||||
} from "solid-js";
|
||||
import { satsToUsd } from "~/utils/conversions";
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import chain from "~/assets/icons/chain.svg";
|
||||
@@ -35,27 +41,42 @@ export const ActivityAmount: ParentComponent<{
|
||||
return (
|
||||
<div
|
||||
class="flex flex-col"
|
||||
classList={{ "items-end": !props.center, "items-center": props.center }}
|
||||
classList={{
|
||||
"items-end": !props.center,
|
||||
"items-center": props.center
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="text-base"
|
||||
classList={{ "text-m-green": props.positive }}
|
||||
>
|
||||
<div class="text-base" classList={{ "text-m-green": props.positive }}>
|
||||
{props.positive && "+ "}
|
||||
{prettyPrint()} <span class="text-sm">SATS</span>
|
||||
</div>
|
||||
<div class="text-sm text-neutral-500">
|
||||
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
||||
≈ {amountInUsd()}
|
||||
<span class="text-sm">USD</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function LabelCircle(props: { name?: string; contact: boolean; label: boolean }) {
|
||||
function LabelCircle(props: {
|
||||
name?: string;
|
||||
contact: boolean;
|
||||
label: boolean;
|
||||
}) {
|
||||
// TODO: don't need to run this if it's not a contact
|
||||
const [gradient] = createResource(async () => {
|
||||
return generateGradient(props.name || "?");
|
||||
});
|
||||
|
||||
const text = () =>
|
||||
props.contact && props.name && props.name.length ? props.name[0] : props.label ? "≡" : "?";
|
||||
props.contact && props.name && props.name.length
|
||||
? props.name[0]
|
||||
: props.label
|
||||
? "≡"
|
||||
: "?";
|
||||
const bg = () => (props.name && props.contact ? gradient() : "gray");
|
||||
|
||||
return (
|
||||
@@ -82,7 +103,8 @@ export function ActivityItem(props: {
|
||||
}) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const firstContact = () => (props.contacts?.length ? props.contacts[0] : null);
|
||||
const firstContact = () =>
|
||||
props.contacts?.length ? props.contacts[0] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -115,13 +137,19 @@ export function ActivityItem(props: {
|
||||
<div class="flex flex-col">
|
||||
<Switch>
|
||||
<Match when={firstContact()?.name}>
|
||||
<span class="text-base font-semibold truncate">{firstContact()?.name}</span>
|
||||
<span class="text-base font-semibold truncate">
|
||||
{firstContact()?.name}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={props.labels.length > 0}>
|
||||
<span class="text-base font-semibold truncate">{props.labels[0]}</span>
|
||||
<span class="text-base font-semibold truncate">
|
||||
{props.labels[0]}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span class="text-base font-semibold text-neutral-500">Unknown</span>
|
||||
<span class="text-base font-semibold text-neutral-500">
|
||||
Unknown
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Switch>
|
||||
@@ -129,7 +157,9 @@ export function ActivityItem(props: {
|
||||
<time class="text-sm text-neutral-500">Pending</time>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
|
||||
<time class="text-sm text-neutral-500">
|
||||
{timeAgo(props.date)}
|
||||
</time>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Show } from "solid-js"
|
||||
import { useMegaStore } from "~/state/megaStore"
|
||||
import { satsToUsd } from "~/utils/conversions"
|
||||
import { Show } from "solid-js";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToUsd } from "~/utils/conversions";
|
||||
|
||||
function prettyPrintAmount(n?: number | bigint): string {
|
||||
if (!n || n.valueOf() === 0) {
|
||||
return "0"
|
||||
return "0";
|
||||
}
|
||||
return n.toLocaleString()
|
||||
return n.toLocaleString();
|
||||
}
|
||||
|
||||
export function Amount(props: {
|
||||
@@ -17,12 +17,17 @@ export function Amount(props: {
|
||||
}) {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
||||
const amountInUsd = () =>
|
||||
satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2" classList={{ "items-center": props.centered }}>
|
||||
<div
|
||||
class="flex flex-col gap-2"
|
||||
classList={{ "items-center": props.centered }}
|
||||
>
|
||||
<h1 class="text-4xl font-light">
|
||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
||||
|
||||
<span class="text-xl">SATS</span>
|
||||
</h1>
|
||||
<Show when={props.showFiat}>
|
||||
@@ -36,13 +41,15 @@ export function Amount(props: {
|
||||
}
|
||||
|
||||
export function AmountSmall(props: {
|
||||
amountSats: bigint | number | undefined
|
||||
amountSats: bigint | number | undefined;
|
||||
}) {
|
||||
return (
|
||||
<span class="font-light">
|
||||
{prettyPrintAmount(props.amountSats)}
|
||||
<span class="text-sm">
|
||||
{props.amountSats === 1 || props.amountSats === 1n ? "SAT" : "SATS"}
|
||||
{props.amountSats === 1 || props.amountSats === 1n
|
||||
? "SAT"
|
||||
: "SATS"}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -10,16 +10,21 @@ const noop = () => {
|
||||
|
||||
const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
|
||||
return (
|
||||
<div class="flex justify-between items-center" classList={{ "text-neutral-400": props.gray }}>
|
||||
<div
|
||||
class="flex justify-between items-center"
|
||||
classList={{ "text-neutral-400": props.gray }}
|
||||
>
|
||||
<div class="font-semibold uppercase">{props.key}</div>
|
||||
<div class="font-light">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InlineAmount: ParentComponent<{ amount: string; sign?: string; fiat?: boolean }> = (
|
||||
props
|
||||
) => {
|
||||
export const InlineAmount: ParentComponent<{
|
||||
amount: string;
|
||||
sign?: string;
|
||||
fiat?: boolean;
|
||||
}> = (props) => {
|
||||
const prettyPrint = createMemo(() => {
|
||||
const parsed = Number(props.amount);
|
||||
if (isNaN(parsed)) {
|
||||
@@ -33,20 +38,23 @@ export const InlineAmount: ParentComponent<{ amount: string; sign?: string; fiat
|
||||
<div class="inline-block text-lg">
|
||||
{props.sign ? `${props.sign} ` : ""}
|
||||
{props.fiat ? "$" : ""}
|
||||
{prettyPrint()} <span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||
{prettyPrint()}{" "}
|
||||
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function USDShower(props: { amountSats: string; fee?: string }) {
|
||||
const [state, _] = useMegaStore();
|
||||
const amountInUsd = () => satsToUsd(state.price, add(props.amountSats, props.fee), true);
|
||||
const amountInUsd = () =>
|
||||
satsToUsd(state.price, add(props.amountSats, props.fee), true);
|
||||
|
||||
return (
|
||||
<Show when={!(props.amountSats === "0")}>
|
||||
<KeyValue gray key="">
|
||||
<div class="self-end">
|
||||
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
||||
≈ {amountInUsd()}
|
||||
<span class="text-sm">USD</span>
|
||||
</div>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
@@ -74,12 +82,20 @@ export function AmountCard(props: {
|
||||
<KeyValue key="Amount">
|
||||
<Show
|
||||
when={props.isAmountEditable}
|
||||
fallback={<InlineAmount amount={props.amountSats} />}
|
||||
fallback={
|
||||
<InlineAmount
|
||||
amount={props.amountSats}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AmountEditable
|
||||
initialOpen={props.initialOpen ?? false}
|
||||
initialAmountSats={props.amountSats.toString()}
|
||||
setAmountSats={props.setAmountSats ? props.setAmountSats : noop}
|
||||
setAmountSats={
|
||||
props.setAmountSats
|
||||
? props.setAmountSats
|
||||
: noop
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</KeyValue>
|
||||
@@ -90,15 +106,28 @@ export function AmountCard(props: {
|
||||
<hr class="border-white/20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Total">
|
||||
<InlineAmount amount={add(props.amountSats, props.fee).toString()} />
|
||||
<InlineAmount
|
||||
amount={add(
|
||||
props.amountSats,
|
||||
props.fee
|
||||
).toString()}
|
||||
/>
|
||||
</KeyValue>
|
||||
<USDShower amountSats={props.amountSats} fee={props.fee} />
|
||||
<USDShower
|
||||
amountSats={props.amountSats}
|
||||
fee={props.fee}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={props.reserve}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Channel size">
|
||||
<InlineAmount amount={add(props.amountSats, props.reserve).toString()} />
|
||||
<InlineAmount
|
||||
amount={add(
|
||||
props.amountSats,
|
||||
props.reserve
|
||||
).toString()}
|
||||
/>
|
||||
</KeyValue>
|
||||
<KeyValue gray key="- Channel Reserve">
|
||||
<InlineAmount amount={props.reserve || "0"} />
|
||||
@@ -109,7 +138,10 @@ export function AmountCard(props: {
|
||||
<KeyValue key="Spendable">
|
||||
<InlineAmount amount={props.amountSats} />
|
||||
</KeyValue>
|
||||
<USDShower amountSats={props.amountSats} fee={props.reserve} />
|
||||
<USDShower
|
||||
amountSats={props.amountSats}
|
||||
fee={props.reserve}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!props.fee && !props.reserve}>
|
||||
@@ -117,12 +149,20 @@ export function AmountCard(props: {
|
||||
<KeyValue key="Amount">
|
||||
<Show
|
||||
when={props.isAmountEditable}
|
||||
fallback={<InlineAmount amount={props.amountSats} />}
|
||||
fallback={
|
||||
<InlineAmount
|
||||
amount={props.amountSats}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AmountEditable
|
||||
initialOpen={props.initialOpen ?? false}
|
||||
initialAmountSats={props.amountSats.toString()}
|
||||
setAmountSats={props.setAmountSats ? props.setAmountSats : noop}
|
||||
setAmountSats={
|
||||
props.setAmountSats
|
||||
? props.setAmountSats
|
||||
: noop
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</KeyValue>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { For, ParentComponent, Show, createResource, createSignal, onMount } from "solid-js";
|
||||
import {
|
||||
For,
|
||||
ParentComponent,
|
||||
Show,
|
||||
createResource,
|
||||
createSignal,
|
||||
onMount
|
||||
} from "solid-js";
|
||||
import { Button } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToUsd, usdToSats } from "~/utils/conversions";
|
||||
@@ -10,7 +17,20 @@ import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
import { InfoBox } from "./InfoBox";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
|
||||
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "DEL"];
|
||||
const CHARACTERS = [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
".",
|
||||
"0",
|
||||
"DEL"
|
||||
];
|
||||
|
||||
const FIXED_AMOUNTS_SATS = [
|
||||
{ label: "10k", amount: "10000" },
|
||||
@@ -32,7 +52,9 @@ function fiatInputSanitizer(input: string): string {
|
||||
const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
|
||||
|
||||
// If there are three characters after the decimal, shift the decimal
|
||||
const shifted = cleaned.match(/(\.[0-9]{3}).*/g) ? (parseFloat(cleaned) * 10).toFixed(2) : cleaned;
|
||||
const shifted = cleaned.match(/(\.[0-9]{3}).*/g)
|
||||
? (parseFloat(cleaned) * 10).toFixed(2)
|
||||
: cleaned;
|
||||
|
||||
// Truncate any numbers two past the decimal
|
||||
const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1");
|
||||
@@ -56,7 +78,10 @@ function SingleDigitButton(props: {
|
||||
}) {
|
||||
return (
|
||||
// Skip the "." if it's fiat
|
||||
<Show when={props.fiat || !(props.character === ".")} fallback={<div />}>
|
||||
<Show
|
||||
when={props.fiat || !(props.character === ".")}
|
||||
fallback={<div />}
|
||||
>
|
||||
<button
|
||||
class="disabled:opacity-50 p-2 rounded-lg md:hover:bg-white/10 active:bg-m-blue text-white text-4xl font-semi font-mono"
|
||||
onClick={() => props.onClick(props.character)}
|
||||
@@ -83,7 +108,8 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
|
||||
"scale-150": chars() <= 4
|
||||
}}
|
||||
>
|
||||
{props.text} <span class="text-xl">{props.fiat ? "USD" : "SATS"}</span>
|
||||
{props.text}
|
||||
<span class="text-xl">{props.fiat ? "USD" : "SATS"}</span>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -91,7 +117,8 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
|
||||
function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
||||
return (
|
||||
<h2 class="text-xl font-light text-neutral-400">
|
||||
≈ {props.text} <span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||
≈ {props.text}
|
||||
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
@@ -113,9 +140,15 @@ export const AmountEditable: ParentComponent<{
|
||||
const [isOpen, setIsOpen] = createSignal(props.initialOpen);
|
||||
const [state, _actions] = useMegaStore();
|
||||
const [mode, setMode] = createSignal<"fiat" | "sats">("sats");
|
||||
const [localSats, setLocalSats] = createSignal(props.initialAmountSats || "0");
|
||||
const [localSats, setLocalSats] = createSignal(
|
||||
props.initialAmountSats || "0"
|
||||
);
|
||||
const [localFiat, setLocalFiat] = createSignal(
|
||||
satsToUsd(state.price, parseInt(props.initialAmountSats || "0") || 0, false)
|
||||
satsToUsd(
|
||||
state.price,
|
||||
parseInt(props.initialAmountSats || "0") || 0,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
||||
@@ -159,7 +192,9 @@ export const AmountEditable: ParentComponent<{
|
||||
|
||||
function handleCharacterInput(character: string) {
|
||||
const isFiatMode = mode() === "fiat";
|
||||
const inputSanitizer = isFiatMode ? fiatInputSanitizer : satsInputSanitizer;
|
||||
const inputSanitizer = isFiatMode
|
||||
? fiatInputSanitizer
|
||||
: satsInputSanitizer;
|
||||
const localValue = isFiatMode ? localFiat : localSats;
|
||||
|
||||
let sane;
|
||||
@@ -176,7 +211,9 @@ export const AmountEditable: ParentComponent<{
|
||||
|
||||
if (isFiatMode) {
|
||||
setLocalFiat(sane);
|
||||
setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false));
|
||||
setLocalSats(
|
||||
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||
);
|
||||
} else {
|
||||
setLocalSats(sane);
|
||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
||||
@@ -189,7 +226,9 @@ export const AmountEditable: ParentComponent<{
|
||||
function setFixedAmount(amount: string) {
|
||||
if (mode() === "fiat") {
|
||||
setLocalFiat(amount);
|
||||
setLocalSats(usdToSats(state.price, parseFloat(amount || "0") || 0, false));
|
||||
setLocalSats(
|
||||
usdToSats(state.price, parseFloat(amount || "0") || 0, false)
|
||||
);
|
||||
} else {
|
||||
setLocalSats(amount);
|
||||
setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false));
|
||||
@@ -214,7 +253,9 @@ export const AmountEditable: ParentComponent<{
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
const sane = fiatInputSanitizer(value);
|
||||
setLocalFiat(sane);
|
||||
setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false));
|
||||
setLocalSats(
|
||||
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||
);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -245,7 +286,9 @@ export const AmountEditable: ParentComponent<{
|
||||
>
|
||||
<Show
|
||||
when={localSats() !== "0"}
|
||||
fallback={<div class="inline-block font-semibold">Set amount</div>}
|
||||
fallback={
|
||||
<div class="inline-block font-semibold">Set amount</div>
|
||||
}
|
||||
>
|
||||
<InlineAmount amount={localSats()} />
|
||||
</Show>
|
||||
@@ -255,7 +298,10 @@ export const AmountEditable: ParentComponent<{
|
||||
<Dialog.Portal>
|
||||
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
|
||||
<Dialog.Content
|
||||
class={DIALOG_CONTENT}
|
||||
onEscapeKeyDown={() => setIsOpen(false)}
|
||||
>
|
||||
{/* TODO: figure out how to submit on enter */}
|
||||
<div class="w-full flex justify-end">
|
||||
<button
|
||||
@@ -266,7 +312,10 @@ export const AmountEditable: ParentComponent<{
|
||||
</button>
|
||||
</div>
|
||||
{/* <form onSubmit={handleSubmit} class="text-black"> */}
|
||||
<form onSubmit={handleSubmit} class="opacity-0 absolute -z-10">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
class="opacity-0 absolute -z-10"
|
||||
>
|
||||
<input
|
||||
ref={(el) => (satsInputRef = el)}
|
||||
disabled={mode() === "fiat"}
|
||||
@@ -286,21 +335,40 @@ export const AmountEditable: ParentComponent<{
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col flex-1 justify-around gap-2 max-w-[400px] mx-auto w-full">
|
||||
<div class="p-4 flex flex-col gap-4 items-center justify-center" onClick={toggle}>
|
||||
<div
|
||||
class="p-4 flex flex-col gap-4 items-center justify-center"
|
||||
onClick={toggle}
|
||||
>
|
||||
<BigScalingText
|
||||
text={mode() === "fiat" ? displayFiat() : displaySats()}
|
||||
text={
|
||||
mode() === "fiat"
|
||||
? displayFiat()
|
||||
: displaySats()
|
||||
}
|
||||
fiat={mode() === "fiat"}
|
||||
/>
|
||||
<SmallSubtleAmount
|
||||
text={mode() === "fiat" ? displaySats() : displayFiat()}
|
||||
text={
|
||||
mode() === "fiat"
|
||||
? displaySats()
|
||||
: displayFiat()
|
||||
}
|
||||
fiat={mode() !== "fiat"}
|
||||
/>
|
||||
</div>
|
||||
<Show when={warningText()}>
|
||||
<InfoBox accent="green">{warningText()}</InfoBox>
|
||||
<InfoBox accent="green">
|
||||
{warningText()}
|
||||
</InfoBox>
|
||||
</Show>
|
||||
<div class="flex justify-center gap-4 my-2">
|
||||
<For each={mode() === "fiat" ? FIXED_AMOUNTS_USD : FIXED_AMOUNTS_SATS}>
|
||||
<For
|
||||
each={
|
||||
mode() === "fiat"
|
||||
? FIXED_AMOUNTS_USD
|
||||
: FIXED_AMOUNTS_SATS
|
||||
}
|
||||
>
|
||||
{(amount) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -325,7 +393,11 @@ export const AmountEditable: ParentComponent<{
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Button intent="blue" class="w-full flex-none" onClick={handleSubmit}>
|
||||
<Button
|
||||
intent="blue"
|
||||
class="w-full flex-none"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Set Amount
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
||||
import logo from "~/assets/icons/mutiny-logo.svg";
|
||||
import { DefaultMain, SafeArea, VStack, Card } from "~/components/layout";
|
||||
import BalanceBox, { LoadingShimmer } from "~/components/BalanceBox";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import ReloadPrompt from "~/components/Reload";
|
||||
import { A } from 'solid-start';
|
||||
import { OnboardWarning } from '~/components/OnboardWarning';
|
||||
import { CombinedActivity } from './Activity';
|
||||
import userClock from '~/assets/icons/user-clock.svg';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { Show } from 'solid-js';
|
||||
import { A } from "solid-start";
|
||||
import { OnboardWarning } from "~/components/OnboardWarning";
|
||||
import { CombinedActivity } from "./Activity";
|
||||
import userClock from "~/assets/icons/user-clock.svg";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Show } from "solid-js";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
|
||||
export default function App() {
|
||||
@@ -19,7 +19,10 @@ export default function App() {
|
||||
<DefaultMain>
|
||||
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
||||
<img src={logo} class="h-10" alt="logo" />
|
||||
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/activity">
|
||||
<A
|
||||
class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
||||
href="/activity"
|
||||
>
|
||||
<img src={userClock} alt="Activity" class="h-8 w-8" />
|
||||
</A>
|
||||
</header>
|
||||
@@ -31,7 +34,10 @@ export default function App() {
|
||||
<Card title="Activity">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
||||
<Show
|
||||
when={!state.wallet_loading}
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<CombinedActivity limit={3} />
|
||||
</Show>
|
||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
||||
|
||||
@@ -32,19 +32,30 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const totalOnchain = () => (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n) + (state.balance?.force_close || 0n);
|
||||
const totalOnchain = () =>
|
||||
(state.balance?.confirmed || 0n) +
|
||||
(state.balance?.unconfirmed || 0n) +
|
||||
(state.balance?.force_close || 0n);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FancyCard title="Lightning">
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||
<Amount
|
||||
amountSats={state.balance?.lightning || 0}
|
||||
showFiat
|
||||
/>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
|
||||
<FancyCard
|
||||
title="On-Chain"
|
||||
subtitle={((Number(state.balance?.unconfirmed) || 0) + (Number(state.balance?.force_close) || 0)) ? "Unconfirmed" : undefined}
|
||||
subtitle={
|
||||
(Number(state.balance?.unconfirmed) || 0) +
|
||||
(Number(state.balance?.force_close) || 0)
|
||||
? "Unconfirmed"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<div class="flex justify-between">
|
||||
@@ -52,7 +63,11 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
||||
<Show when={!emptyBalance()}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swap" class={STYLE}>
|
||||
<img src={shuffle} alt="swap" class="h-8 w-8" />
|
||||
<img
|
||||
src={shuffle}
|
||||
alt="swap"
|
||||
class="h-8 w-8"
|
||||
/>
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -67,7 +82,11 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">
|
||||
<Button
|
||||
onClick={() => navigate("/receive")}
|
||||
disabled={props.loading}
|
||||
intent="blue"
|
||||
>
|
||||
Receive
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +1,66 @@
|
||||
import { Match, Switch, createSignal } from 'solid-js';
|
||||
import { SmallHeader, TinyButton } from '~/components/layout';
|
||||
import { Dialog } from '@kobalte/core';
|
||||
import { Match, Switch, createSignal } from "solid-js";
|
||||
import { SmallHeader, TinyButton } from "~/components/layout";
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import { SubmitHandler } from '@modular-forms/solid';
|
||||
import { ContactForm } from './ContactForm';
|
||||
import { ContactFormValues } from './ContactViewer';
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from '~/styles/dialogs';
|
||||
import { SubmitHandler } from "@modular-forms/solid";
|
||||
import { ContactForm } from "./ContactForm";
|
||||
import { ContactFormValues } from "./ContactViewer";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
|
||||
export function ContactEditor(props: { createContact: (contact: ContactFormValues) => void, list?: boolean }) {
|
||||
export function ContactEditor(props: {
|
||||
createContact: (contact: ContactFormValues) => void;
|
||||
list?: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
// What we're all here for in the first place: returning a value
|
||||
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
|
||||
props.createContact(c)
|
||||
const handleSubmit: SubmitHandler<ContactFormValues> = (
|
||||
c: ContactFormValues
|
||||
) => {
|
||||
props.createContact(c);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen()}>
|
||||
<Switch>
|
||||
<Match when={props.list}>
|
||||
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
class="flex flex-col items-center gap-2"
|
||||
>
|
||||
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
|
||||
<span class="leading-[4rem]">+</span>
|
||||
</div>
|
||||
<SmallHeader class="overflow-ellipsis">
|
||||
new
|
||||
</SmallHeader>
|
||||
<SmallHeader class="overflow-ellipsis">new</SmallHeader>
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={!props.list}>
|
||||
<TinyButton onClick={() => setIsOpen(true)}>+ Add Contact</TinyButton>
|
||||
<TinyButton onClick={() => setIsOpen(true)}>
|
||||
+ Add Contact
|
||||
</TinyButton>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Dialog.Portal>
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
|
||||
<Dialog.Content
|
||||
class={DIALOG_CONTENT}
|
||||
onEscapeKeyDown={() => setIsOpen(false)}
|
||||
>
|
||||
<div class="w-full flex justify-end">
|
||||
<button tabindex="-1" onClick={() => setIsOpen(false)} class="hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||
<button
|
||||
tabindex="-1"
|
||||
onClick={() => setIsOpen(false)}
|
||||
class="hover:bg-white/10 rounded-lg active:bg-m-blue"
|
||||
>
|
||||
<img src={close} alt="Close" />
|
||||
</button>
|
||||
</div>
|
||||
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} />
|
||||
<ContactForm
|
||||
title="New contact"
|
||||
cta="Create contact"
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
|
||||
@@ -3,17 +3,36 @@ import { Button, LargeHeader, VStack } from "~/components/layout";
|
||||
import { TextField } from "~/components/layout/TextField";
|
||||
import { ContactFormValues } from "./ContactViewer";
|
||||
|
||||
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValues>, initialValues?: ContactFormValues, title: string, cta: string }) {
|
||||
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({ initialValues: props.initialValues });
|
||||
export function ContactForm(props: {
|
||||
handleSubmit: SubmitHandler<ContactFormValues>;
|
||||
initialValues?: ContactFormValues;
|
||||
title: string;
|
||||
cta: string;
|
||||
}) {
|
||||
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
|
||||
initialValues: props.initialValues
|
||||
});
|
||||
|
||||
return (
|
||||
<Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
|
||||
<Form
|
||||
onSubmit={props.handleSubmit}
|
||||
class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full"
|
||||
>
|
||||
<div>
|
||||
<LargeHeader>{props.title}</LargeHeader>
|
||||
<VStack>
|
||||
<Field name="name" validate={[required("We at least need a name")]}>
|
||||
<Field
|
||||
name="name"
|
||||
validate={[required("We at least need a name")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" />
|
||||
<TextField
|
||||
{...props}
|
||||
placeholder="Satoshi"
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Name"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
{/* <Field name="npub" validate={[]}>
|
||||
@@ -27,5 +46,5 @@ export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValu
|
||||
{props.cta}
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,44 @@
|
||||
import { Match, Switch, createSignal } from 'solid-js';
|
||||
import { Button, Card, NiceP, SmallHeader } from '~/components/layout';
|
||||
import { Dialog } from '@kobalte/core';
|
||||
import { Match, Switch, createSignal } from "solid-js";
|
||||
import { Button, Card, NiceP, SmallHeader } from "~/components/layout";
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import { SubmitHandler } from '@modular-forms/solid';
|
||||
import { ContactForm } from './ContactForm';
|
||||
import { showToast } from './Toaster';
|
||||
import { Contact } from '@mutinywallet/mutiny-wasm';
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from '~/styles/dialogs';
|
||||
import { SubmitHandler } from "@modular-forms/solid";
|
||||
import { ContactForm } from "./ContactForm";
|
||||
import { showToast } from "./Toaster";
|
||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
|
||||
export type ContactFormValues = {
|
||||
name: string,
|
||||
npub?: string,
|
||||
}
|
||||
name: string;
|
||||
npub?: string;
|
||||
};
|
||||
|
||||
export function ContactViewer(props: { contact: Contact, gradient: string, saveContact: (contact: Contact) => void }) {
|
||||
export function ContactViewer(props: {
|
||||
contact: Contact;
|
||||
gradient: string;
|
||||
saveContact: (contact: Contact) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [isEditing, setIsEditing] = createSignal(false);
|
||||
|
||||
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
|
||||
const handleSubmit: SubmitHandler<ContactFormValues> = (
|
||||
c: ContactFormValues
|
||||
) => {
|
||||
// FIXME: merge with existing contact if saving (need edit contact method)
|
||||
// FIXME: npub not valid? other undefineds
|
||||
const contact = new Contact(c.name, undefined, undefined, undefined)
|
||||
props.saveContact(contact)
|
||||
setIsEditing(false)
|
||||
}
|
||||
const contact = new Contact(c.name, undefined, undefined, undefined);
|
||||
props.saveContact(contact);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen()}>
|
||||
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden">
|
||||
<div class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||
style={{ background: props.gradient }}
|
||||
>
|
||||
{props.contact.name[0]}
|
||||
@@ -39,33 +49,72 @@ export function ContactViewer(props: { contact: Contact, gradient: string, saveC
|
||||
</button>
|
||||
<Dialog.Portal>
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
|
||||
<Dialog.Content
|
||||
class={DIALOG_CONTENT}
|
||||
onEscapeKeyDown={() => setIsOpen(false)}
|
||||
>
|
||||
<div class="w-full flex justify-end">
|
||||
<button tabindex="-1" onClick={() => setIsOpen(false)} class="hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||
<button
|
||||
tabindex="-1"
|
||||
onClick={() => setIsOpen(false)}
|
||||
class="hover:bg-white/10 rounded-lg active:bg-m-blue"
|
||||
>
|
||||
<img src={close} alt="Close" />
|
||||
</button>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={isEditing()}>
|
||||
<ContactForm title="Edit contact" cta="Save contact" handleSubmit={handleSubmit} initialValues={props.contact} />
|
||||
<ContactForm
|
||||
title="Edit contact"
|
||||
cta="Save contact"
|
||||
handleSubmit={handleSubmit}
|
||||
initialValues={props.contact}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!isEditing()}>
|
||||
<div class="flex flex-col flex-1 justify-around items-center gap-4 max-w-[400px] mx-auto w-full">
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex-none h-32 w-32 rounded-full flex items-center justify-center text-8xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||
style={{ background: props.gradient }}
|
||||
<div
|
||||
class="flex-none h-32 w-32 rounded-full flex items-center justify-center text-8xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||
style={{
|
||||
background: props.gradient
|
||||
}}
|
||||
>
|
||||
{props.contact.name[0]}
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">{props.contact.name}</h1>
|
||||
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">
|
||||
{props.contact.name}
|
||||
</h1>
|
||||
<Card title="Payment history">
|
||||
<NiceP>No payments yet with <span class="font-semibold">{props.contact.name}</span></NiceP>
|
||||
<NiceP>
|
||||
No payments yet with{" "}
|
||||
<span class="font-semibold">
|
||||
{props.contact.name}
|
||||
</span>
|
||||
</NiceP>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full gap-2">
|
||||
<Button layout="flex" intent="green" onClick={() => setIsEditing(true)}>Edit</Button>
|
||||
<Button intent="blue" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Pay</Button>
|
||||
<Button
|
||||
layout="flex"
|
||||
intent="green"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
intent="blue"
|
||||
onClick={() => {
|
||||
showToast({
|
||||
title: "Unimplemented",
|
||||
description:
|
||||
"We don't do that yet"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Pay
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
@@ -5,13 +5,20 @@ import { useCopy } from "~/utils/useCopy";
|
||||
export function CopyableQR(props: { value: string }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
return (
|
||||
<div id="qr" class="w-full bg-white rounded-xl relative" onClick={() => copy(props.value)}>
|
||||
<div
|
||||
id="qr"
|
||||
class="w-full bg-white rounded-xl relative"
|
||||
onClick={() => copy(props.value)}
|
||||
>
|
||||
<Show when={copied()}>
|
||||
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
||||
<p class="text-xl font-bold">Copied</p>
|
||||
</div>
|
||||
</Show>
|
||||
<QRCodeSVG value={props.value} class="w-full h-full p-8 max-h-[400px]" />
|
||||
<QRCodeSVG
|
||||
value={props.value}
|
||||
class="w-full h-full p-8 max-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,19 +15,18 @@ export function DeleteEverything() {
|
||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||
|
||||
|
||||
async function resetNode() {
|
||||
try {
|
||||
setConfirmLoading(true);
|
||||
await actions.deleteMutinyWallet();
|
||||
showToast({ title: "Deleted", description: `Deleted all data` })
|
||||
showToast({ title: "Deleted", description: `Deleted all data` });
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showToast(eify(e))
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
} finally {
|
||||
setConfirmOpen(false);
|
||||
setConfirmLoading(false);
|
||||
@@ -37,9 +36,14 @@ export function DeleteEverything() {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={confirmReset}>Delete Everything</Button>
|
||||
<ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={resetNode} onCancel={() => setConfirmOpen(false)}>
|
||||
<ConfirmDialog
|
||||
loading={confirmLoading()}
|
||||
open={confirmOpen()}
|
||||
onConfirm={resetNode}
|
||||
onCancel={() => setConfirmOpen(false)}
|
||||
>
|
||||
This will delete your node's state. This can't be undone!
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dialog } from "@kobalte/core"
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
@@ -29,7 +29,8 @@ import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { AmountSmall } from "./Amount";
|
||||
|
||||
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
export const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||
export const DIALOG_POSITIONER =
|
||||
"fixed inset-0 z-50 flex items-center justify-center";
|
||||
export const DIALOG_CONTENT =
|
||||
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
|
||||
@@ -38,7 +39,9 @@ function LightningHeader(props: { info: MutinyInvoice }) {
|
||||
|
||||
const tags = createMemo(() => {
|
||||
if (props.info.labels.length) {
|
||||
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
|
||||
const contact = state.mutiny_wallet?.get_contact(
|
||||
props.info.labels[0]
|
||||
);
|
||||
if (contact) {
|
||||
return [tagToMutinyTag(contact)];
|
||||
} else {
|
||||
@@ -84,7 +87,9 @@ function OnchainHeader(props: { info: OnChainTx }) {
|
||||
|
||||
const tags = createMemo(() => {
|
||||
if (props.info.labels.length) {
|
||||
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
|
||||
const contact = state.mutiny_wallet?.get_contact(
|
||||
props.info.labels[0]
|
||||
);
|
||||
if (contact) {
|
||||
return [tagToMutinyTag(contact)];
|
||||
} else {
|
||||
@@ -112,8 +117,15 @@ function OnchainHeader(props: { info: OnChainTx }) {
|
||||
<div class="p-4 bg-neutral-100 rounded-full">
|
||||
<img src={chain} alt="blockchain" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">{isSend() ? "On-chain send" : "On-chain receive"}</h1>
|
||||
<ActivityAmount center amount={amount() ?? "0"} price={state.price} positive={!isSend()} />
|
||||
<h1 class="uppercase font-semibold">
|
||||
{isSend() ? "On-chain send" : "On-chain receive"}
|
||||
</h1>
|
||||
<ActivityAmount
|
||||
center
|
||||
amount={amount() ?? "0"}
|
||||
price={state.price}
|
||||
positive={!isSend()}
|
||||
/>
|
||||
<For each={tags()}>
|
||||
{(tag) => (
|
||||
<TinyButton
|
||||
@@ -133,7 +145,9 @@ function OnchainHeader(props: { info: OnChainTx }) {
|
||||
const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||
return (
|
||||
<li class="flex justify-between items-center gap-4">
|
||||
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
|
||||
<span class="uppercase font-semibold whitespace-nowrap">
|
||||
{props.key}
|
||||
</span>
|
||||
<span class="font-light">{props.children}</span>
|
||||
</li>
|
||||
);
|
||||
@@ -157,14 +171,20 @@ function LightningDetails(props: { info: MutinyInvoice }) {
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<span class="text-neutral-300">{props.info.paid ? "Paid" : "Unpaid"}</span>
|
||||
<span class="text-neutral-300">
|
||||
{props.info.paid ? "Paid" : "Unpaid"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="When">
|
||||
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
|
||||
<span class="text-neutral-300">
|
||||
{prettyPrintTime(Number(props.info.last_updated))}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={props.info.description}>
|
||||
<KeyValue key="Description">
|
||||
<span class="text-neutral-300 truncate">{props.info.description}</span>
|
||||
<span class="text-neutral-300 truncate">
|
||||
{props.info.description}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Fees">
|
||||
@@ -200,12 +220,16 @@ function OnchainDetails(props: { info: OnChainTx }) {
|
||||
{/* <pre>{JSON.stringify(props.info, null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<span class="text-neutral-300">{confirmationTime() ? "Confirmed" : "Unconfirmed"}</span>
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={confirmationTime()}>
|
||||
<KeyValue key="When">
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime() ? prettyPrintTime(Number(confirmationTime())) : "Pending"}
|
||||
{confirmationTime()
|
||||
? prettyPrintTime(Number(confirmationTime()))
|
||||
: "Pending"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
@@ -247,7 +271,9 @@ export function DetailsIdModal(props: {
|
||||
const [data, { refetch }] = createResource(async () => {
|
||||
if (kind() === "Lightning") {
|
||||
console.log("reading invoice: ", id());
|
||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(id());
|
||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
|
||||
id()
|
||||
);
|
||||
return invoice;
|
||||
} else {
|
||||
console.log("reading tx: ", id());
|
||||
@@ -284,10 +310,14 @@ export function DetailsIdModal(props: {
|
||||
<Dialog.Title>
|
||||
<Switch>
|
||||
<Match when={isInvoice()}>
|
||||
<LightningHeader info={data() as MutinyInvoice} />
|
||||
<LightningHeader
|
||||
info={data() as MutinyInvoice}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<OnchainHeader info={data() as OnChainTx} />
|
||||
<OnchainHeader
|
||||
info={data() as OnChainTx}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Dialog.Title>
|
||||
@@ -295,10 +325,14 @@ export function DetailsIdModal(props: {
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Switch>
|
||||
<Match when={isInvoice()}>
|
||||
<LightningDetails info={data() as MutinyInvoice} />
|
||||
<LightningDetails
|
||||
info={data() as MutinyInvoice}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<OnchainDetails info={data() as OnChainTx} />
|
||||
<OnchainDetails
|
||||
info={data() as OnChainTx}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div class="flex justify-center">
|
||||
|
||||
@@ -2,12 +2,18 @@ import { Dialog } from "@kobalte/core";
|
||||
import { ParentComponent } from "solid-js";
|
||||
import { Button, SmallHeader } from "./layout";
|
||||
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||
const DIALOG_CONTENT =
|
||||
"w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
|
||||
// TODO: implement this like toast so it's just one global confirm and I can call it with `confirm({ title: "Are you sure?", description: "This will delete your node" })`
|
||||
export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; onCancel: () => void, onConfirm: () => void }> = (props) => {
|
||||
export const ConfirmDialog: ParentComponent<{
|
||||
open: boolean;
|
||||
loading: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
|
||||
<Dialog.Portal>
|
||||
@@ -15,18 +21,27 @@ export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; o
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title><SmallHeader>Are you sure?</SmallHeader></Dialog.Title>
|
||||
<Dialog.Title>
|
||||
<SmallHeader>Are you sure?</SmallHeader>
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
{props.children}
|
||||
<div class="flex gap-4 w-full justify-end">
|
||||
<Button onClick={props.onCancel}>Cancel</Button>
|
||||
<Button intent="red" onClick={props.onConfirm} loading={props.loading} disabled={props.loading}>Confirm</Button>
|
||||
<Button
|
||||
intent="red"
|
||||
onClick={props.onConfirm}
|
||||
loading={props.loading}
|
||||
disabled={props.loading}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Title } from "solid-start";
|
||||
import { Button, DefaultMain, LargeHeader, SafeArea, SmallHeader } from "~/components/layout";
|
||||
import {
|
||||
Button,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
SafeArea,
|
||||
SmallHeader
|
||||
} from "~/components/layout";
|
||||
|
||||
export default function ErrorDisplay(props: { error: Error }) {
|
||||
return (
|
||||
@@ -9,11 +15,16 @@ export default function ErrorDisplay(props: { error: Error }) {
|
||||
<LargeHeader>Error</LargeHeader>
|
||||
<SmallHeader>This never should've happened</SmallHeader>
|
||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||
<span class="font-bold">
|
||||
{props.error.name}</span>: {props.error.message}
|
||||
<span class="font-bold">{props.error.name}</span>:{" "}
|
||||
{props.error.message}
|
||||
</p>
|
||||
<div class="h-full" />
|
||||
<Button onClick={() => window.location.href = "/"} intent="red">Dangit</Button>
|
||||
<Button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
intent="red"
|
||||
>
|
||||
Dangit
|
||||
</Button>
|
||||
</DefaultMain>
|
||||
</SafeArea>
|
||||
);
|
||||
|
||||
@@ -4,16 +4,16 @@ import { createSignal } from "solid-js";
|
||||
import eify from "~/utils/eify";
|
||||
import { showToast } from "./Toaster";
|
||||
import { downloadTextFile } from "~/utils/download";
|
||||
import { createFileUploader } from "@solid-primitives/upload"
|
||||
import { createFileUploader } from "@solid-primitives/upload";
|
||||
import { ConfirmDialog } from "./Dialog";
|
||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
|
||||
export function ImportExport() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
async function handleSave() {
|
||||
const json = await state.mutiny_wallet?.export_json()
|
||||
downloadTextFile(json || "", "mutiny-state.json")
|
||||
const json = await state.mutiny_wallet?.export_json();
|
||||
downloadTextFile(json || "", "mutiny-state.json");
|
||||
}
|
||||
|
||||
const { files, selectFiles } = createFileUploader();
|
||||
@@ -26,7 +26,7 @@ export function ImportExport() {
|
||||
const file: File = files()[0].file;
|
||||
|
||||
const text = await new Promise<string | null>((resolve, reject) => {
|
||||
fileReader.onload = e => {
|
||||
fileReader.onload = (e) => {
|
||||
const result = e.target?.result?.toString();
|
||||
if (result) {
|
||||
resolve(result);
|
||||
@@ -34,7 +34,8 @@ export function ImportExport() {
|
||||
reject(new Error("No text found in file"));
|
||||
}
|
||||
};
|
||||
fileReader.onerror = _e => reject(new Error("File read error"));
|
||||
fileReader.onerror = (_e) =>
|
||||
reject(new Error("File read error"));
|
||||
fileReader.readAsText(file, "UTF-8");
|
||||
});
|
||||
|
||||
@@ -49,7 +50,6 @@ export function ImportExport() {
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
|
||||
} catch (e) {
|
||||
showToast(eify(e));
|
||||
} finally {
|
||||
@@ -59,12 +59,12 @@ export function ImportExport() {
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
selectFiles(async files => {
|
||||
selectFiles(async (files) => {
|
||||
if (files.length) {
|
||||
setConfirmOpen(true);
|
||||
return;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||
@@ -78,9 +78,14 @@ export function ImportExport() {
|
||||
<Button onClick={uploadFile}>Upload Saved State</Button>
|
||||
</VStack>
|
||||
</InnerCard>
|
||||
<ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={importJson} onCancel={() => setConfirmOpen(false)}>
|
||||
<ConfirmDialog
|
||||
loading={confirmLoading()}
|
||||
open={confirmOpen()}
|
||||
onConfirm={importJson}
|
||||
onCancel={() => setConfirmOpen(false)}
|
||||
>
|
||||
Do you want to replace your state with {files()[0].name}?
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ParentComponent } from "solid-js";
|
||||
import info from "~/assets/icons/info.svg"
|
||||
import info from "~/assets/icons/info.svg";
|
||||
|
||||
export const InfoBox: ParentComponent<{ accent: "red" | "blue" | "green" | "white" }> = (props) => {
|
||||
export const InfoBox: ParentComponent<{
|
||||
accent: "red" | "blue" | "green" | "white";
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div
|
||||
class="grid grid-cols-[auto_minmax(0,_1fr)] rounded-xl px-4 py-2 md:p-4 gap-4 bg-neutral-950/50 border"
|
||||
@@ -20,4 +22,4 @@ export const InfoBox: ParentComponent<{ accent: "red" | "blue" | "green" | "whit
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { JSX, createMemo } from "solid-js";
|
||||
import { ModalCloseButton, SmallHeader } from "~/components/layout";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER, OVERLAY } from "~/components/DetailsModal";
|
||||
import {
|
||||
DIALOG_CONTENT,
|
||||
DIALOG_POSITIONER,
|
||||
OVERLAY
|
||||
} from "~/components/DetailsModal";
|
||||
import { CopyButton } from "./ShareCard";
|
||||
|
||||
export function JsonModal(props: { title: string, open: boolean, plaintext?: string, data?: unknown, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
||||
const json = createMemo(() => props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2));
|
||||
export function JsonModal(props: {
|
||||
title: string;
|
||||
open: boolean;
|
||||
plaintext?: string;
|
||||
data?: unknown;
|
||||
setOpen: (open: boolean) => void;
|
||||
children?: JSX.Element;
|
||||
}) {
|
||||
const json = createMemo(() =>
|
||||
props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||
@@ -15,9 +28,7 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2 items-center">
|
||||
<Dialog.Title>
|
||||
<SmallHeader>
|
||||
{props.title}
|
||||
</SmallHeader>
|
||||
<SmallHeader>{props.title}</SmallHeader>
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton>
|
||||
<ModalCloseButton />
|
||||
@@ -36,5 +47,5 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,23 @@
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Card, Hr, SmallHeader, Button, InnerCard, VStack } from "~/components/layout";
|
||||
import {
|
||||
Card,
|
||||
Hr,
|
||||
SmallHeader,
|
||||
Button,
|
||||
InnerCard,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
import PeerConnectModal from "~/components/PeerConnectModal";
|
||||
import NostrWalletConnectModal from "~/components/NostrWalletConnectModal";
|
||||
import { For, Show, Suspense, createEffect, createResource, createSignal, onCleanup } from "solid-js";
|
||||
import {
|
||||
For,
|
||||
Show,
|
||||
Suspense,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
onCleanup
|
||||
} from "solid-js";
|
||||
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
||||
import { Collapsible, TextField } from "@kobalte/core";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
@@ -26,9 +41,15 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
if (props.peer.is_connected) {
|
||||
await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey);
|
||||
await state.mutiny_wallet?.disconnect_peer(
|
||||
firstNode,
|
||||
props.peer.pubkey
|
||||
);
|
||||
} else {
|
||||
await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey);
|
||||
await state.mutiny_wallet?.delete_peer(
|
||||
firstNode,
|
||||
props.peer.pubkey
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,7 +57,8 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||
{">"}{" "}
|
||||
{props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
@@ -44,7 +66,11 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.peer, null, 2)}
|
||||
</pre>
|
||||
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>
|
||||
<Button
|
||||
intent="glowy"
|
||||
layout="xs"
|
||||
onClick={handleDisconnectPeer}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</VStack>
|
||||
@@ -57,7 +83,9 @@ function PeersList() {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getPeers = async () => {
|
||||
return (await state.mutiny_wallet?.list_peers()) as Promise<MutinyPeer[]>;
|
||||
return (await state.mutiny_wallet?.list_peers()) as Promise<
|
||||
MutinyPeer[]
|
||||
>;
|
||||
};
|
||||
|
||||
const [peers, { refetch }] = createResource(getPeers);
|
||||
@@ -103,7 +131,10 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString);
|
||||
await state.mutiny_wallet?.connect_to_peer(
|
||||
firstNode,
|
||||
peerConnectString
|
||||
);
|
||||
|
||||
await props.refetchPeers();
|
||||
|
||||
@@ -119,7 +150,9 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
validationState={value() == "" ? "valid" : "invalid"}
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">Connect Peer</TextField.Label>
|
||||
<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..."
|
||||
@@ -153,7 +186,9 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
async function confirmCloseChannel() {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
await state.mutiny_wallet?.close_channel(props.channel.outpoint as string);
|
||||
await state.mutiny_wallet?.close_channel(
|
||||
props.channel.outpoint as string
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
@@ -174,10 +209,19 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.channel, null, 2)}
|
||||
</pre>
|
||||
<ExternalLink href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)}>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
props.channel.outpoint?.split(":")[0],
|
||||
props.network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>
|
||||
<Button
|
||||
intent="glowy"
|
||||
layout="xs"
|
||||
onClick={handleCloseChannel}
|
||||
>
|
||||
Close Channel
|
||||
</Button>
|
||||
</VStack>
|
||||
@@ -195,10 +239,12 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
}
|
||||
|
||||
function ChannelsList() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getChannels = async () => {
|
||||
return await state.mutiny_wallet?.list_channels() as Promise<MutinyChannel[]>
|
||||
return (await state.mutiny_wallet?.list_channels()) as Promise<
|
||||
MutinyChannel[]
|
||||
>;
|
||||
};
|
||||
|
||||
const [channels, { refetch }] = createResource(getChannels);
|
||||
@@ -211,32 +257,38 @@ function ChannelsList() {
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SmallHeader>
|
||||
Channels
|
||||
</SmallHeader>
|
||||
<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) => (
|
||||
<ChannelItem channel={channel} network={network} />
|
||||
)}
|
||||
|
||||
</For>
|
||||
</Suspense>
|
||||
<Button type="button" layout="small" onClick={(e) => { e.preventDefault(); refetch() }}>Refresh Channels</Button>
|
||||
<Button
|
||||
type="button"
|
||||
layout="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Refresh Channels
|
||||
</Button>
|
||||
<OpenChannel refetchChannels={refetch} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [creationError, setCreationError] = createSignal<Error>();
|
||||
|
||||
@@ -256,19 +308,22 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
const bigAmount = BigInt(amount());
|
||||
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
const new_channel = await state.mutiny_wallet?.open_channel(firstNode, pubkey, bigAmount)
|
||||
const new_channel = await state.mutiny_wallet?.open_channel(
|
||||
firstNode,
|
||||
pubkey,
|
||||
bigAmount
|
||||
);
|
||||
|
||||
setNewChannel(new_channel)
|
||||
setNewChannel(new_channel);
|
||||
|
||||
await props.refetchChannels()
|
||||
await props.refetchChannels();
|
||||
|
||||
setAmount("");
|
||||
setPeerPubkey("");
|
||||
|
||||
} catch (e) {
|
||||
setCreationError(eify(e))
|
||||
setCreationError(eify(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -283,12 +338,23 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
onChange={setPeerPubkey}
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">Pubkey</TextField.Label>
|
||||
<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()} onChange={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
|
||||
value={amount()}
|
||||
onChange={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
|
||||
@@ -300,7 +366,12 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
||||
</pre>
|
||||
<pre>{newChannel()?.outpoint}</pre>
|
||||
<ExternalLink href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)}>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
newChannel()?.outpoint?.split(":")[0],
|
||||
network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
@@ -312,7 +383,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
}
|
||||
|
||||
function LnUrlAuth() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [value, setValue] = createSignal("");
|
||||
|
||||
@@ -320,7 +391,7 @@ function LnUrlAuth() {
|
||||
e.preventDefault();
|
||||
|
||||
const lnurl = value().trim();
|
||||
await state.mutiny_wallet?.lnurl_auth(0, lnurl)
|
||||
await state.mutiny_wallet?.lnurl_auth(0, lnurl);
|
||||
|
||||
setValue("");
|
||||
};
|
||||
@@ -331,26 +402,39 @@ function LnUrlAuth() {
|
||||
<TextField.Root
|
||||
value={value()}
|
||||
onChange={setValue}
|
||||
validationState={(value() == "" || value().toLowerCase().startsWith("lnurl")) ? "valid" : "invalid"}
|
||||
validationState={
|
||||
value() == "" ||
|
||||
value().toLowerCase().startsWith("lnurl")
|
||||
? "valid"
|
||||
: "invalid"
|
||||
}
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase" >LNURL Auth</TextField.Label>
|
||||
<TextField.Input class="w-full p-2 rounded-lg text-black" placeholder="LNURL..." />
|
||||
<TextField.ErrorMessage class="text-red-500">Expecting something like LNURL...</TextField.ErrorMessage>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">
|
||||
LNURL Auth
|
||||
</TextField.Label>
|
||||
<TextField.Input
|
||||
class="w-full p-2 rounded-lg text-black"
|
||||
placeholder="LNURL..."
|
||||
/>
|
||||
<TextField.ErrorMessage class="text-red-500">
|
||||
Expecting something like LNURL...
|
||||
</TextField.ErrorMessage>
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">Auth</Button>
|
||||
<Button layout="small" type="submit">
|
||||
Auth
|
||||
</Button>
|
||||
</form>
|
||||
</InnerCard>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ListTags() {
|
||||
const [_state, actions] = useMegaStore()
|
||||
const [_state, actions] = useMegaStore();
|
||||
|
||||
const [tags] = createResource(actions.listTags)
|
||||
const [tags] = createResource(actions.listTags);
|
||||
|
||||
return (
|
||||
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
@@ -365,13 +449,9 @@ function ListTags() {
|
||||
</VStack>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default function KitchenSink() {
|
||||
return (
|
||||
<Card title="Kitchen Sink">
|
||||
@@ -390,5 +470,5 @@ export default function KitchenSink() {
|
||||
<Hr />
|
||||
<ImportExport />
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import { useMegaStore } from "~/state/megaStore";
|
||||
import { downloadTextFile } from "~/utils/download";
|
||||
|
||||
export function Logs() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
async function handleSave() {
|
||||
const logs = await state.mutiny_wallet?.get_logs()
|
||||
downloadTextFile(logs.join("") || "", "mutiny-logs.txt", "text/plain")
|
||||
const logs = await state.mutiny_wallet?.get_logs();
|
||||
downloadTextFile(logs.join("") || "", "mutiny-logs.txt", "text/plain");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -17,6 +17,5 @@ export function Logs() {
|
||||
<Button onClick={handleSave}>Download Logs</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,51 +1,91 @@
|
||||
import mutiny_m from '~/assets/icons/m.svg';
|
||||
import airplane from '~/assets/icons/airplane.svg';
|
||||
import settings from '~/assets/icons/settings.svg';
|
||||
import receive from '~/assets/icons/big-receive.svg';
|
||||
import redshift from '~/assets/icons/rs.svg';
|
||||
import userClock from '~/assets/icons/user-clock.svg';
|
||||
import mutiny_m from "~/assets/icons/m.svg";
|
||||
import airplane from "~/assets/icons/airplane.svg";
|
||||
import settings from "~/assets/icons/settings.svg";
|
||||
import receive from "~/assets/icons/big-receive.svg";
|
||||
import redshift from "~/assets/icons/rs.svg";
|
||||
import userClock from "~/assets/icons/user-clock.svg";
|
||||
|
||||
import { A } from "solid-start";
|
||||
|
||||
type ActiveTab = 'home' | 'scan' | 'send' | 'receive' | 'settings' | 'redshift' | 'activity' | 'none';
|
||||
type ActiveTab =
|
||||
| "home"
|
||||
| "scan"
|
||||
| "send"
|
||||
| "receive"
|
||||
| "settings"
|
||||
| "redshift"
|
||||
| "activity"
|
||||
| "none";
|
||||
|
||||
export default function NavBar(props: { activeTab: ActiveTab }) {
|
||||
const activeStyle = 'border-t-0 border-b-0 p-2 bg-black rounded-lg'
|
||||
const inactiveStyle = "p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
||||
const activeStyle = "border-t-0 border-b-0 p-2 bg-black rounded-lg";
|
||||
const inactiveStyle = "p-2 hover:bg-white/5 rounded-lg active:bg-m-blue";
|
||||
return (
|
||||
<nav class='hidden md:block fixed shadow-none z-40 safe-bottom top-0 bottom-auto left-0 h-full'>
|
||||
<ul class='h-16 flex flex-col justify-start gap-4 px-4 mt-4'>
|
||||
<li class={props.activeTab === "home" ? activeStyle : inactiveStyle}>
|
||||
<nav class="hidden md:block fixed shadow-none z-40 safe-bottom top-0 bottom-auto left-0 h-full">
|
||||
<ul class="h-16 flex flex-col justify-start gap-4 px-4 mt-4">
|
||||
<li
|
||||
class={
|
||||
props.activeTab === "home" ? activeStyle : inactiveStyle
|
||||
}
|
||||
>
|
||||
<A href="/">
|
||||
<img src={mutiny_m} alt="home" />
|
||||
</A>
|
||||
</li>
|
||||
<li class={props.activeTab === "send" ? activeStyle : inactiveStyle}>
|
||||
<li
|
||||
class={
|
||||
props.activeTab === "send" ? activeStyle : inactiveStyle
|
||||
}
|
||||
>
|
||||
<A href="/send">
|
||||
<img src={airplane} alt="send" />
|
||||
</A>
|
||||
</li>
|
||||
<li class={props.activeTab === "receive" ? activeStyle : inactiveStyle}>
|
||||
<li
|
||||
class={
|
||||
props.activeTab === "receive"
|
||||
? activeStyle
|
||||
: inactiveStyle
|
||||
}
|
||||
>
|
||||
<A href="/receive">
|
||||
<img src={receive} alt="receive" />
|
||||
</A>
|
||||
</li>
|
||||
<li class={props.activeTab === "activity" ? activeStyle : inactiveStyle}>
|
||||
<li
|
||||
class={
|
||||
props.activeTab === "activity"
|
||||
? activeStyle
|
||||
: inactiveStyle
|
||||
}
|
||||
>
|
||||
<A href="/activity">
|
||||
<img src={userClock} alt="activity" />
|
||||
</A>
|
||||
</li>
|
||||
<li class={props.activeTab === "redshift" ? activeStyle : inactiveStyle}>
|
||||
<li
|
||||
class={
|
||||
props.activeTab === "redshift"
|
||||
? activeStyle
|
||||
: inactiveStyle
|
||||
}
|
||||
>
|
||||
<A href="/redshift">
|
||||
<img src={redshift} alt="redshift" width={36} />
|
||||
</A>
|
||||
</li>
|
||||
<li class={props.activeTab === "settings" ? activeStyle : inactiveStyle}>
|
||||
<li
|
||||
class={
|
||||
props.activeTab === "settings"
|
||||
? activeStyle
|
||||
: inactiveStyle
|
||||
}
|
||||
>
|
||||
<A href="/settings">
|
||||
<img src={settings} alt="settings" />
|
||||
</A>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -4,19 +4,20 @@ import {Button, Card} from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { createResource, Show } from "solid-js";
|
||||
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] max-h-[100dvh] overflow-y-auto disable-scrollbars p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
const SMALL_HEADER = "text-sm font-semibold uppercase"
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||
const DIALOG_CONTENT =
|
||||
"w-[80vw] max-w-[400px] max-h-[100dvh] overflow-y-auto disable-scrollbars p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
const SMALL_HEADER = "text-sm font-semibold uppercase";
|
||||
|
||||
export default function NostrWalletConnectModal() {
|
||||
const [state, actions] = useMegaStore()
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
const getConnectionURI = () => {
|
||||
if (state.mutiny_wallet) {
|
||||
return state.mutiny_wallet.get_nwc_uri()
|
||||
return state.mutiny_wallet.get_nwc_uri();
|
||||
} else {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,15 +25,15 @@ export default function NostrWalletConnectModal() {
|
||||
|
||||
const toggleNwc = async () => {
|
||||
if (state.nwc_enabled) {
|
||||
actions.setNwc(false)
|
||||
window.location.reload()
|
||||
actions.setNwc(false);
|
||||
window.location.reload();
|
||||
} else {
|
||||
actions.setNwc(true)
|
||||
actions.setNwc(true);
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = nodes[0] as string || "";
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
await state.mutiny_wallet?.start_nostr_wallet_connect(firstNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
||||
return (
|
||||
@@ -45,7 +46,9 @@ export default function NostrWalletConnectModal() {
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title class={SMALL_HEADER}>Nostr Wallet Connect</Dialog.Title>
|
||||
<Dialog.Title class={SMALL_HEADER}>
|
||||
Nostr Wallet Connect
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="dialog__close-button">
|
||||
<code>X</code>
|
||||
</Dialog.CloseButton>
|
||||
@@ -53,17 +56,24 @@ export default function NostrWalletConnectModal() {
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Show when={connectionURI()}>
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG value={connectionURI() || ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
<QRCodeSVG
|
||||
value={connectionURI() || ""}
|
||||
class="w-full h-full p-8 max-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<code class="break-all">{connectionURI() || ""}</code>
|
||||
<code class="break-all">
|
||||
{connectionURI() || ""}
|
||||
</code>
|
||||
</Card>
|
||||
</Show>
|
||||
<Button onClick={toggleNwc}>{state.nwc_enabled ? "Disable" : "Enable"}</Button>
|
||||
<Button onClick={toggleNwc}>
|
||||
{state.nwc_enabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,12 @@ export function OnboardWarning() {
|
||||
const [dismissedBackup, setDismissedBackup] = createSignal(false);
|
||||
|
||||
function hasMoney() {
|
||||
return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed || state.balance?.force_close;
|
||||
return (
|
||||
state.balance?.confirmed ||
|
||||
state.balance?.lightning ||
|
||||
state.balance?.unconfirmed ||
|
||||
state.balance?.force_close
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -26,8 +31,9 @@ export function OnboardWarning() {
|
||||
<div class="flex flex-col">
|
||||
<SmallHeader>Welcome!</SmallHeader>
|
||||
<p class="text-base font-light">
|
||||
If you've used Mutiny before you can restore from a backup. Otherwise you can skip
|
||||
this and enjoy your new wallet!
|
||||
If you've used Mutiny before you can restore
|
||||
from a backup. Otherwise you can skip this and
|
||||
enjoy your new wallet!
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -35,7 +41,10 @@ export function OnboardWarning() {
|
||||
layout="xs"
|
||||
class="self-start md:self-auto"
|
||||
onClick={() => {
|
||||
showToast({ title: "Unimplemented", description: "We don't do that yet" });
|
||||
showToast({
|
||||
title: "Unimplemented",
|
||||
description: "We don't do that yet"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Restore
|
||||
@@ -52,7 +61,9 @@ export function OnboardWarning() {
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
|
||||
<Show
|
||||
when={!state.has_backed_up && hasMoney() && !dismissedBackup()}
|
||||
>
|
||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
|
||||
<div class="self-center">
|
||||
<img src={save} alt="backup" class="w-8 h-8" />
|
||||
@@ -61,11 +72,17 @@ export function OnboardWarning() {
|
||||
<div class="flex flex-col">
|
||||
<SmallHeader>Secure your funds</SmallHeader>
|
||||
<p class="text-base font-light max-md:hidden">
|
||||
You have money stored in this browser. Let's make sure you have a backup.
|
||||
You have money stored in this browser. Let's
|
||||
make sure you have a backup.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ButtonLink intent="blue" layout="xs" class="self-auto" href="/backup">
|
||||
<ButtonLink
|
||||
intent="blue"
|
||||
layout="xs"
|
||||
class="self-auto"
|
||||
href="/backup"
|
||||
>
|
||||
Backup
|
||||
</ButtonLink>
|
||||
</div>
|
||||
|
||||
@@ -6,30 +6,30 @@ import { Show, createResource } from "solid-js";
|
||||
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
||||
import getHostname from "~/utils/getHostname";
|
||||
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
const SMALL_HEADER = "text-sm font-semibold uppercase"
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||
const DIALOG_CONTENT =
|
||||
"w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
const SMALL_HEADER = "text-sm font-semibold uppercase";
|
||||
|
||||
export default function PeerConnectModal() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getPeerConnectString = async () => {
|
||||
if (state.mutiny_wallet) {
|
||||
const { proxy } = getExistingSettings();
|
||||
const nodes = await state.mutiny_wallet.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
const hostName = getHostname(proxy || "")
|
||||
const connectString = `mutiny:${firstNode}@${hostName}`
|
||||
return connectString
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
const hostName = getHostname(proxy || "");
|
||||
const connectString = `mutiny:${firstNode}@${hostName}`;
|
||||
return connectString;
|
||||
} else {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const [peerConnectString] = createResource(getPeerConnectString);
|
||||
|
||||
|
||||
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
||||
return (
|
||||
<Dialog.Root>
|
||||
@@ -41,7 +41,9 @@ export default function PeerConnectModal() {
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title class={SMALL_HEADER}>Peer connect info</Dialog.Title>
|
||||
<Dialog.Title class={SMALL_HEADER}>
|
||||
Peer connect info
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="dialog__close-button">
|
||||
<code>X</code>
|
||||
</Dialog.CloseButton>
|
||||
@@ -49,10 +51,15 @@ export default function PeerConnectModal() {
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Show when={peerConnectString()}>
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG value={peerConnectString() || ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
<QRCodeSVG
|
||||
value={peerConnectString() || ""}
|
||||
class="w-full h-full p-8 max-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<code class="break-all">{peerConnectString() || ""}</code>
|
||||
<code class="break-all">
|
||||
{peerConnectString() || ""}
|
||||
</code>
|
||||
</Card>
|
||||
</Show>
|
||||
</Dialog.Description>
|
||||
@@ -60,5 +67,5 @@ export default function PeerConnectModal() {
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import QrScanner from 'qr-scanner';
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import QrScanner from "qr-scanner";
|
||||
import { createSignal, onCleanup, onMount } from "solid-js";
|
||||
|
||||
export default function Scanner(props: { onResult: (result: string) => void }) {
|
||||
let container: HTMLVideoElement | null;
|
||||
@@ -9,19 +9,15 @@ export default function Scanner(props: { onResult: (result: string) => void }) {
|
||||
|
||||
const handleResult = (result: { data: string }) => {
|
||||
props.onResult(result.data);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (container) {
|
||||
const newScanner = new QrScanner(
|
||||
container,
|
||||
handleResult,
|
||||
{
|
||||
returnDetailedScanResult: true,
|
||||
}
|
||||
);
|
||||
const newScanner = new QrScanner(container, handleResult, {
|
||||
returnDetailedScanResult: true
|
||||
});
|
||||
newScanner.start();
|
||||
setScanner(newScanner)
|
||||
setScanner(newScanner);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +30,10 @@ export default function Scanner(props: { onResult: (result: string) => void }) {
|
||||
return (
|
||||
<>
|
||||
<div id="video-container">
|
||||
<video ref={el => container = el} class="w-full h-full fixed object-cover bg-gray" />
|
||||
<video
|
||||
ref={(el) => (container = el)}
|
||||
class="w-full h-full fixed object-cover bg-gray"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { Component } from 'solid-js'
|
||||
import { Show } from 'solid-js'
|
||||
import type { Component } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { useRegisterSW } from 'virtual:pwa-register/solid'
|
||||
import { useRegisterSW } from "virtual:pwa-register/solid";
|
||||
|
||||
const ReloadPrompt: Component = () => {
|
||||
const {
|
||||
offlineReady: [offlineReady, _setOfflineReady],
|
||||
needRefresh: [needRefresh, _setNeedRefresh],
|
||||
updateServiceWorker: _update,
|
||||
updateServiceWorker: _update
|
||||
} = useRegisterSW({
|
||||
immediate: true,
|
||||
onRegisteredSW(swUrl, r) {
|
||||
console.log('SW Registered: ' + r?.scope)
|
||||
console.log("SW Registered: " + r?.scope);
|
||||
},
|
||||
onRegisterError(error: Error) {
|
||||
console.log('SW registration error', error)
|
||||
},
|
||||
})
|
||||
console.log("SW registration error", error);
|
||||
}
|
||||
});
|
||||
|
||||
// const close = () => {
|
||||
// setOfflineReady(false)
|
||||
@@ -40,7 +40,7 @@ const ReloadPrompt: Component = () => {
|
||||
<Button onClick={() => close()}>Close</Button>
|
||||
</Card> */}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ReloadPrompt
|
||||
export default ReloadPrompt;
|
||||
|
||||
@@ -2,19 +2,20 @@ import { Button, Card, NiceP, VStack } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function Restart() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
async function handleStop() {
|
||||
await state.mutiny_wallet?.stop()
|
||||
await state.mutiny_wallet?.stop();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<VStack>
|
||||
<NiceP>Something *extra* screwy going on? Stop the nodes!</NiceP>
|
||||
<NiceP>
|
||||
Something *extra* screwy going on? Stop the nodes!
|
||||
</NiceP>
|
||||
<Button onClick={handleStop}>Stop</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { For, Match, Switch, createMemo, createSignal } from "solid-js"
|
||||
import { For, Match, Switch, createMemo, createSignal } from "solid-js";
|
||||
|
||||
export function SeedWords(props: { words: string, setHasSeen?: (hasSeen: boolean) => void }) {
|
||||
const [shouldShow, setShouldShow] = createSignal(false)
|
||||
export function SeedWords(props: {
|
||||
words: string;
|
||||
setHasSeen?: (hasSeen: boolean) => void;
|
||||
}) {
|
||||
const [shouldShow, setShouldShow] = createSignal(false);
|
||||
|
||||
function toggleShow() {
|
||||
setShouldShow(!shouldShow())
|
||||
setShouldShow(!shouldShow());
|
||||
if (shouldShow()) {
|
||||
props.setHasSeen?.(true)
|
||||
props.setHasSeen?.(true);
|
||||
}
|
||||
}
|
||||
|
||||
const splitWords = createMemo(() => props.words.split(" "))
|
||||
const splitWords = createMemo(() => props.words.split(" "));
|
||||
|
||||
return (<button class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden" onClick={toggleShow}>
|
||||
return (
|
||||
<button
|
||||
class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden"
|
||||
onClick={toggleShow}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={!shouldShow()}>
|
||||
<div class="cursor-pointer">
|
||||
@@ -24,13 +31,12 @@ export function SeedWords(props: { words: string, setHasSeen?: (hasSeen: boolean
|
||||
<ol class="cursor-pointer overflow-hidden grid grid-cols-2 w-full list-decimal list-inside">
|
||||
<For each={splitWords()}>
|
||||
{(word) => (
|
||||
<li class="font-mono text-left">
|
||||
{word}
|
||||
</li>
|
||||
<li class="font-mono text-left">{word}</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</Match>
|
||||
</Switch>
|
||||
</button >)
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +1,100 @@
|
||||
import { createForm, url } from '@modular-forms/solid';
|
||||
import { TextField } from '~/components/layout/TextField';
|
||||
import { MutinyWalletSettingStrings, getExistingSettings } from '~/logic/mutinyWalletSetup';
|
||||
import { Button, Card, SmallHeader } from '~/components/layout';
|
||||
import { showToast } from './Toaster';
|
||||
import eify from '~/utils/eify';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { createForm, url } from "@modular-forms/solid";
|
||||
import { TextField } from "~/components/layout/TextField";
|
||||
import {
|
||||
MutinyWalletSettingStrings,
|
||||
getExistingSettings
|
||||
} from "~/logic/mutinyWalletSetup";
|
||||
import { Button, Card, SmallHeader } from "~/components/layout";
|
||||
import { showToast } from "./Toaster";
|
||||
import eify from "~/utils/eify";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function SettingsStringsEditor() {
|
||||
const existingSettings = getExistingSettings();
|
||||
const [_settingsForm, { Form, Field }] = createForm<MutinyWalletSettingStrings>({ initialValues: existingSettings });
|
||||
const [_settingsForm, { Form, Field }] =
|
||||
createForm<MutinyWalletSettingStrings>({
|
||||
initialValues: existingSettings
|
||||
});
|
||||
const [_store, actions] = useMegaStore();
|
||||
|
||||
async function handleSubmit(values: MutinyWalletSettingStrings) {
|
||||
try {
|
||||
const existing = getExistingSettings();
|
||||
const newSettings = { ...existing, ...values }
|
||||
const newSettings = { ...existing, ...values };
|
||||
await actions.setupMutinyWallet(newSettings);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showToast(eify(e))
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
console.log(values)
|
||||
console.log(values);
|
||||
}
|
||||
|
||||
return <Card>
|
||||
return (
|
||||
<Card>
|
||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
||||
<h2 class="text-2xl font-light">Don't trust us! Use your own servers to back Mutiny.</h2>
|
||||
<h2 class="text-2xl font-light">
|
||||
Don't trust us! Use your own servers to back Mutiny.
|
||||
</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<SmallHeader>Network</SmallHeader>
|
||||
<pre>
|
||||
{existingSettings.network}
|
||||
</pre>
|
||||
<pre>{existingSettings.network}</pre>
|
||||
</div>
|
||||
|
||||
<Field name="proxy" validate={[url("Should be a url starting with wss://")]}>
|
||||
<Field
|
||||
name="proxy"
|
||||
validate={[url("Should be a url starting with wss://")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} value={field.value} error={field.error} label="Websockets Proxy" />
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Websockets Proxy"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="esplora" validate={[url("That doesn't look like a URL")]}>
|
||||
<Field
|
||||
name="esplora"
|
||||
validate={[url("That doesn't look like a URL")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} value={field.value} error={field.error} label="Esplora" />
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Esplora"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="rgs" validate={[url("That doesn't look like a URL")]}>
|
||||
<Field
|
||||
name="rgs"
|
||||
validate={[url("That doesn't look like a URL")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} value={field.value} error={field.error} label="RGS" />
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="RGS"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="lsp" validate={[url("That doesn't look like a URL")]}>
|
||||
<Field
|
||||
name="lsp"
|
||||
validate={[url("That doesn't look like a URL")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} value={field.value} error={field.error} label="LSP" />
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="LSP"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Button type="submit">Save</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -1,40 +1,46 @@
|
||||
import { Card, VStack } from "~/components/layout";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
import copyIcon from "~/assets/icons/copy.svg"
|
||||
import shareIcon from "~/assets/icons/share.svg"
|
||||
import eyeIcon from "~/assets/icons/eye.svg"
|
||||
import copyIcon from "~/assets/icons/copy.svg";
|
||||
import shareIcon from "~/assets/icons/share.svg";
|
||||
import eyeIcon from "~/assets/icons/eye.svg";
|
||||
import { Show, createSignal } from "solid-js";
|
||||
import { JsonModal } from "./JsonModal";
|
||||
|
||||
const STYLE = "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors"
|
||||
const STYLE =
|
||||
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
|
||||
|
||||
export function ShareButton(props: { receiveString: string }) {
|
||||
async function share(receiveString: string) {
|
||||
// If the browser doesn't support share we can just copy the address
|
||||
if (!navigator.share) {
|
||||
console.error("Share not supported")
|
||||
console.error("Share not supported");
|
||||
}
|
||||
const shareData: ShareData = {
|
||||
title: "Mutiny Wallet",
|
||||
text: receiveString,
|
||||
}
|
||||
text: receiveString
|
||||
};
|
||||
try {
|
||||
await navigator.share(shareData)
|
||||
await navigator.share(shareData);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button class={STYLE} onClick={(_) => share(props.receiveString)}><span>Share</span><img src={shareIcon} alt="share" /></button>
|
||||
)
|
||||
<button class={STYLE} onClick={(_) => share(props.receiveString)}>
|
||||
<span>Share</span>
|
||||
<img src={shareIcon} alt="share" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function TruncateMiddle(props: { text: string }) {
|
||||
return (
|
||||
<div class="flex text-neutral-400 font-mono">
|
||||
<span class="truncate">{props.text}</span>
|
||||
<span class="pr-2">{props.text.length > 8 ? props.text.slice(-8) : ""}</span>
|
||||
<span class="pr-2">
|
||||
{props.text.length > 8 ? props.text.slice(-8) : ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +49,12 @@ export function StringShower(props: { text: string }) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
return (
|
||||
<>
|
||||
<JsonModal open={open()} plaintext={props.text} title="Details" setOpen={setOpen} />
|
||||
<JsonModal
|
||||
open={open()}
|
||||
plaintext={props.text}
|
||||
title="Details"
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
||||
<TruncateMiddle text={props.text} />
|
||||
<button class="w-[2rem]" onClick={() => setOpen(true)}>
|
||||
@@ -54,16 +65,19 @@ export function StringShower(props: { text: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyButton(props: { text?: string, title?: string }) {
|
||||
export function CopyButton(props: { text?: string; title?: string }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
function handleCopy() {
|
||||
copy(props.text ?? "")
|
||||
copy(props.text ?? "");
|
||||
}
|
||||
|
||||
return (
|
||||
<button class={STYLE} onClick={handleCopy}>{copied() ? "Copied" : props.title ?? "Copy"}<img src={copyIcon} alt="copy" /></button>
|
||||
)
|
||||
<button class={STYLE} onClick={handleCopy}>
|
||||
{copied() ? "Copied" : props.title ?? "Copy"}
|
||||
<img src={copyIcon} alt="copy" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShareCard(props: { text?: string }) {
|
||||
@@ -79,6 +93,5 @@ export function ShareCard(props: { text?: string }) {
|
||||
</div>
|
||||
</VStack>
|
||||
</Card>
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Select, createOptions } from "@thisbeyond/solid-select";
|
||||
import "~/styles/solid-select.css"
|
||||
import "~/styles/solid-select.css";
|
||||
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
||||
import { TinyButton } from "./layout";
|
||||
import { MutinyTagItem, sortByLastUsed } from "~/utils/tags";
|
||||
@@ -10,36 +10,43 @@ const createLabelValue = (label: string): Partial<MutinyTagItem> => {
|
||||
};
|
||||
|
||||
export function TagEditor(props: {
|
||||
selectedValues: Partial<MutinyTagItem>[],
|
||||
setSelectedValues: (value: Partial<MutinyTagItem>[]) => void,
|
||||
placeholder: string
|
||||
selectedValues: Partial<MutinyTagItem>[];
|
||||
setSelectedValues: (value: Partial<MutinyTagItem>[]) => void;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const [_state, actions] = useMegaStore();
|
||||
const [availableTags, setAvailableTags] = createSignal<MutinyTagItem[]>([]);
|
||||
|
||||
onMount(async () => {
|
||||
const tags = await actions.listTags()
|
||||
const tags = await actions.listTags();
|
||||
if (tags) {
|
||||
setAvailableTags(tags.filter((tag) => tag.kind === "Contact").sort(sortByLastUsed))
|
||||
setAvailableTags(
|
||||
tags
|
||||
.filter((tag) => tag.kind === "Contact")
|
||||
.sort(sortByLastUsed)
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const selectProps = createMemo(() => {
|
||||
return createOptions(availableTags() || [], {
|
||||
key: "name",
|
||||
filterable: true, // Default
|
||||
createable: createLabelValue,
|
||||
createable: createLabelValue
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
const onChange = (selected: MutinyTagItem[]) => {
|
||||
props.setSelectedValues(selected);
|
||||
|
||||
console.log(selected)
|
||||
console.log(selected);
|
||||
|
||||
const lastValue = selected[selected.length - 1];
|
||||
if (lastValue && availableTags() && !availableTags()!.includes(lastValue)) {
|
||||
if (
|
||||
lastValue &&
|
||||
availableTags() &&
|
||||
!availableTags()!.includes(lastValue)
|
||||
) {
|
||||
setAvailableTags([...availableTags(), lastValue]);
|
||||
}
|
||||
};
|
||||
@@ -71,5 +78,5 @@ export function TagEditor(props: {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -10,37 +10,52 @@ export function Toaster() {
|
||||
<Toast.List class="z-[9999] max-w-[100vw] w-[400px] mt-8 flex flex-col gap-4" />
|
||||
</Toast.Region>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
type ToastArg = { title: string, description: string } | Error
|
||||
type ToastArg = { title: string; description: string } | Error;
|
||||
|
||||
export function showToast(arg: ToastArg) {
|
||||
if (arg instanceof Error) {
|
||||
return toaster.show(props => (
|
||||
<ToastItem title="Error" description={arg.message} isError {...props} />
|
||||
))
|
||||
return toaster.show((props) => (
|
||||
<ToastItem
|
||||
title="Error"
|
||||
description={arg.message}
|
||||
isError
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
return toaster.show(props => (
|
||||
<ToastItem title={arg.title} description={arg.description} {...props} />
|
||||
))
|
||||
return toaster.show((props) => (
|
||||
<ToastItem
|
||||
title={arg.title}
|
||||
description={arg.description}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export function ToastItem(props: { toastId: number, title: string, description: string, isError?: boolean }) {
|
||||
export function ToastItem(props: {
|
||||
toastId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
isError?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Toast.Root toastId={props.toastId} class={`w-[80vw] max-w-[400px] mx-auto p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${props.isError ? "border-m-red/50" : "border-white/10"} `}>
|
||||
<Toast.Root
|
||||
toastId={props.toastId}
|
||||
class={`w-[80vw] max-w-[400px] mx-auto p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${
|
||||
props.isError ? "border-m-red/50" : "border-white/10"
|
||||
} `}
|
||||
>
|
||||
<div class="flex gap-4 w-full justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<Toast.Title>
|
||||
<SmallHeader>
|
||||
{props.title}
|
||||
</SmallHeader>
|
||||
<SmallHeader>{props.title}</SmallHeader>
|
||||
</Toast.Title>
|
||||
<Toast.Description>
|
||||
<p>
|
||||
{props.description}
|
||||
</p>
|
||||
<p>{props.description}</p>
|
||||
</Toast.Description>
|
||||
</div>
|
||||
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue flex-0">
|
||||
@@ -48,5 +63,5 @@ export function ToastItem(props: { toastId: number, title: string, description:
|
||||
</Toast.CloseButton>
|
||||
</div>
|
||||
</Toast.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import { A } from "solid-start";
|
||||
import { Back } from "~/assets/svg/Back";
|
||||
|
||||
export function BackLink(props: { href?: string, title?: string }) {
|
||||
return (<A href={props.href ? props.href : "/"} class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"><Back />{props.title ? props.title : "Home"}</A>)
|
||||
export function BackLink(props: { href?: string; title?: string }) {
|
||||
return (
|
||||
<A
|
||||
href={props.href ? props.href : "/"}
|
||||
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"
|
||||
>
|
||||
<Back />
|
||||
{props.title ? props.title : "Home"}
|
||||
</A>
|
||||
);
|
||||
}
|
||||
@@ -11,9 +11,9 @@ const button = cva(
|
||||
// TODO: button hover has to work different than buttonlinks (like disabled state)
|
||||
intent: {
|
||||
active: "bg-white text-black border border-white hover:text-[#3B6CCC]",
|
||||
inactive: "bg-black text-white border border-white hover:text-[#3B6CCC]",
|
||||
glowy:
|
||||
"bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue",
|
||||
inactive:
|
||||
"bg-black text-white border border-white hover:text-[#3B6CCC]",
|
||||
glowy: "bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue",
|
||||
blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button",
|
||||
red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button",
|
||||
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button"
|
||||
@@ -34,15 +34,22 @@ const button = cva(
|
||||
|
||||
// Help from https://github.com/arpadgabor/credee/blob/main/packages/www/src/components/ui/button.tsx
|
||||
|
||||
type StyleProps = VariantProps<typeof button>
|
||||
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, StyleProps {
|
||||
loading?: boolean,
|
||||
disabled?: boolean,
|
||||
type StyleProps = VariantProps<typeof button>;
|
||||
interface ButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
StyleProps {
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Button: ParentComponent<ButtonProps> = props => {
|
||||
const slot = children(() => props.children)
|
||||
const [local, attrs] = splitProps(props, ['children', 'intent', 'layout', 'class'])
|
||||
export const Button: ParentComponent<ButtonProps> = (props) => {
|
||||
const slot = children(() => props.children);
|
||||
const [local, attrs] = splitProps(props, [
|
||||
"children",
|
||||
"intent",
|
||||
"layout",
|
||||
"class"
|
||||
]);
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -62,21 +69,31 @@ export const Button: ParentComponent<ButtonProps> = props => {
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface ButtonLinkProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLAnchorElement>,
|
||||
StyleProps {
|
||||
href: string;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
interface ButtonLinkProps extends JSX.ButtonHTMLAttributes<HTMLAnchorElement>, StyleProps {
|
||||
href: string
|
||||
target?: string
|
||||
rel?: string
|
||||
}
|
||||
|
||||
export const ButtonLink: ParentComponent<ButtonLinkProps> = props => {
|
||||
const slot = children(() => props.children)
|
||||
const [local, attrs] = splitProps(props, ['children', 'intent', 'layout', 'class', 'href', 'target', 'rel'])
|
||||
export const ButtonLink: ParentComponent<ButtonLinkProps> = (props) => {
|
||||
const slot = children(() => props.children);
|
||||
const [local, attrs] = splitProps(props, [
|
||||
"children",
|
||||
"intent",
|
||||
"layout",
|
||||
"class",
|
||||
"href",
|
||||
"target",
|
||||
"rel"
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
component={local.href?.includes('://') ? 'a' : A}
|
||||
component={local.href?.includes("://") ? "a" : A}
|
||||
href={local.href}
|
||||
target={local.target}
|
||||
rel={local.rel}
|
||||
@@ -84,10 +101,10 @@ export const ButtonLink: ParentComponent<ButtonLinkProps> = props => {
|
||||
class={button({
|
||||
class: `flex justify-center no-underline ${local.class || ""}`,
|
||||
intent: local.intent,
|
||||
layout: local.layout,
|
||||
layout: local.layout
|
||||
})}
|
||||
>
|
||||
{slot()}
|
||||
</Dynamic>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSX } from 'solid-js';
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
interface LinkifyProps {
|
||||
initialText: string;
|
||||
@@ -15,7 +15,7 @@ export default function Linkify(props: LinkifyProps): JSX.Element {
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const link = match[1];
|
||||
const href = link.startsWith('http') ? link : `https://${link}`;
|
||||
const href = link.startsWith("http") ? link : `https://${link}`;
|
||||
const beforeLink = text.slice(lastIndex, match.index);
|
||||
lastIndex = pattern.lastIndex;
|
||||
|
||||
@@ -23,7 +23,16 @@ export default function Linkify(props: LinkifyProps): JSX.Element {
|
||||
links.push(beforeLink);
|
||||
}
|
||||
|
||||
links.push(<a href={href} class="break-all" target="_blank" rel="noopener noreferrer">{link}</a>);
|
||||
links.push(
|
||||
<a
|
||||
href={href}
|
||||
class="break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const remainingText = text.slice(lastIndex);
|
||||
|
||||
@@ -3,36 +3,43 @@ import { SmallHeader } from ".";
|
||||
|
||||
export default function formatNumber(num: number) {
|
||||
const map = [
|
||||
{ suffix: 'T', threshold: 1e12 },
|
||||
{ suffix: 'B', threshold: 1e9 },
|
||||
{ suffix: 'M', threshold: 1e6 },
|
||||
{ suffix: 'K', threshold: 1e3 },
|
||||
{ suffix: '', threshold: 1 },
|
||||
{ suffix: "T", threshold: 1e12 },
|
||||
{ suffix: "B", threshold: 1e9 },
|
||||
{ suffix: "M", threshold: 1e6 },
|
||||
{ suffix: "K", threshold: 1e3 },
|
||||
{ suffix: "", threshold: 1 }
|
||||
];
|
||||
|
||||
const found = map.find((x) => Math.abs(num) >= x.threshold);
|
||||
if (found) {
|
||||
const formatted = (num / found.threshold).toLocaleString() + found.suffix;
|
||||
const formatted =
|
||||
(num / found.threshold).toLocaleString() + found.suffix;
|
||||
return formatted;
|
||||
}
|
||||
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
export function ProgressBar(props: { value: number, max: number }) {
|
||||
return (<Progress.Root
|
||||
export function ProgressBar(props: { value: number; max: number }) {
|
||||
return (
|
||||
<Progress.Root
|
||||
value={props.value}
|
||||
minValue={0}
|
||||
maxValue={props.max}
|
||||
getValueLabel={({ value, max }) => `${formatNumber(value)} of ${formatNumber(max)} sats sent`}
|
||||
getValueLabel={({ value, max }) =>
|
||||
`${formatNumber(value)} of ${formatNumber(max)} sats sent`
|
||||
}
|
||||
class="w-full flex flex-col gap-2"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<Progress.Label><SmallHeader>Sending...</SmallHeader></Progress.Label>
|
||||
<Progress.Label>
|
||||
<SmallHeader>Sending...</SmallHeader>
|
||||
</Progress.Label>
|
||||
<Progress.ValueLabel class="text-sm font-semibold uppercase" />
|
||||
</div>
|
||||
<Progress.Track class="h-6 bg-white/10 rounded">
|
||||
<Progress.Fill class="bg-m-red rounded h-full w-[var(--kb-progress-fill-width)] transition-[width]" />
|
||||
</Progress.Track>
|
||||
</Progress.Root>)
|
||||
</Progress.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
import { RadioGroup } from "@kobalte/core";
|
||||
import { For, Show } from "solid-js";
|
||||
|
||||
type Choices = { value: string; label: string; caption: string; disabled?: boolean }[];
|
||||
type Choices = {
|
||||
value: string;
|
||||
label: string;
|
||||
caption: string;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
|
||||
// TODO: how could would it be if we could just pass the estimated fees in here?
|
||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
|
||||
export function StyledRadioGroup(props: {
|
||||
value: string;
|
||||
choices: Choices;
|
||||
onValueChange: (value: string) => void;
|
||||
small?: boolean;
|
||||
accent?: "red" | "white";
|
||||
}) {
|
||||
return (
|
||||
// TODO: rewrite this with CVA, props are bad for tailwind
|
||||
<RadioGroup.Root
|
||||
@@ -24,7 +35,8 @@ export function StyledRadioGroup(props: { value: string, choices: Choices, onVal
|
||||
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
|
||||
classList={{
|
||||
"ui-checked:outline-m-red": props.accent === "red",
|
||||
"ui-checked:outline-white": props.accent === "white",
|
||||
"ui-checked:outline-white":
|
||||
props.accent === "white",
|
||||
"ui-disabled:opacity-50": choice.disabled
|
||||
}}
|
||||
disabled={choice.disabled}
|
||||
@@ -37,13 +49,18 @@ export function StyledRadioGroup(props: { value: string, choices: Choices, onVal
|
||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||
<div class="block">
|
||||
<div
|
||||
classList={{ "text-base": props.small, "text-lg": !props.small }}
|
||||
classList={{
|
||||
"text-base": props.small,
|
||||
"text-lg": !props.small
|
||||
}}
|
||||
class={`font-semibold max-sm:text-sm`}
|
||||
>
|
||||
{choice.label}
|
||||
</div>
|
||||
<Show when={!props.small}>
|
||||
<div class="text-sm font-light">{choice.caption}</div>
|
||||
<div class="text-sm font-light">
|
||||
{choice.caption}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</RadioGroup.ItemLabel>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TextField as KTextField } from '@kobalte/core';
|
||||
import { type JSX, Show, splitProps } from 'solid-js';
|
||||
import { TextField as KTextField } from "@kobalte/core";
|
||||
import { type JSX, Show, splitProps } from "solid-js";
|
||||
|
||||
type TextFieldProps = {
|
||||
name: string;
|
||||
type?: 'text' | 'email' | 'tel' | 'password' | 'url' | 'date';
|
||||
type?: "text" | "email" | "tel" | "password" | "url" | "date";
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value: string | undefined;
|
||||
@@ -11,25 +11,31 @@ type TextFieldProps = {
|
||||
required?: boolean;
|
||||
multiline?: boolean;
|
||||
ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
|
||||
onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
|
||||
onInput: JSX.EventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement,
|
||||
InputEvent
|
||||
>;
|
||||
onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
|
||||
onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
|
||||
onBlur: JSX.EventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement,
|
||||
FocusEvent
|
||||
>;
|
||||
};
|
||||
|
||||
export function TextField(props: TextFieldProps) {
|
||||
const [fieldProps] = splitProps(props, [
|
||||
'placeholder',
|
||||
'ref',
|
||||
'onInput',
|
||||
'onChange',
|
||||
'onBlur',
|
||||
"placeholder",
|
||||
"ref",
|
||||
"onInput",
|
||||
"onChange",
|
||||
"onBlur"
|
||||
]);
|
||||
return (
|
||||
<KTextField.Root
|
||||
class="flex flex-col gap-2"
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
validationState={props.error ? 'invalid' : 'valid'}
|
||||
validationState={props.error ? "invalid" : "valid"}
|
||||
isRequired={props.required}
|
||||
>
|
||||
<Show when={props.label}>
|
||||
@@ -39,9 +45,19 @@ export function TextField(props: TextFieldProps) {
|
||||
</Show>
|
||||
<Show
|
||||
when={props.multiline}
|
||||
fallback={<KTextField.Input {...fieldProps} type={props.type} class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400" />}
|
||||
fallback={
|
||||
<KTextField.Input
|
||||
{...fieldProps}
|
||||
type={props.type}
|
||||
class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<KTextField.TextArea {...fieldProps} autoResize class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400" />
|
||||
<KTextField.TextArea
|
||||
{...fieldProps}
|
||||
autoResize
|
||||
class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||
/>
|
||||
</Show>
|
||||
<KTextField.ErrorMessage>{props.error}</KTextField.ErrorMessage>
|
||||
</KTextField.Root>
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
import { JSX, ParentComponent, Show, Suspense, createResource } from "solid-js"
|
||||
import Linkify from "./Linkify"
|
||||
import { Button, ButtonLink } from "./Button"
|
||||
import { Checkbox as KCheckbox, Separator } from "@kobalte/core"
|
||||
import { useMegaStore } from "~/state/megaStore"
|
||||
import check from "~/assets/icons/check.svg"
|
||||
import { MutinyTagItem } from "~/utils/tags"
|
||||
import { generateGradient } from "~/utils/gradientHash"
|
||||
import close from "~/assets/icons/close.svg"
|
||||
import { JSX, ParentComponent, Show, Suspense, createResource } from "solid-js";
|
||||
import Linkify from "./Linkify";
|
||||
import { Button, ButtonLink } from "./Button";
|
||||
import { Checkbox as KCheckbox, Separator } from "@kobalte/core";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import check from "~/assets/icons/check.svg";
|
||||
import { MutinyTagItem } from "~/utils/tags";
|
||||
import { generateGradient } from "~/utils/gradientHash";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
|
||||
export {
|
||||
Button,
|
||||
ButtonLink,
|
||||
Linkify,
|
||||
}
|
||||
export { Button, ButtonLink, Linkify };
|
||||
|
||||
export const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
||||
return <header class={`text-sm font-semibold uppercase ${props.class}`}>{props.children}</header>
|
||||
}
|
||||
|
||||
export const Card: ParentComponent<{ title?: string, titleElement?: JSX.Element }> = (props) => {
|
||||
return (
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950/50 w-full'>
|
||||
<header class={`text-sm font-semibold uppercase ${props.class}`}>
|
||||
{props.children}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export const Card: ParentComponent<{
|
||||
title?: string;
|
||||
titleElement?: JSX.Element;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div class="rounded-xl p-4 flex flex-col gap-2 bg-neutral-950/50 w-full">
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.titleElement && props.titleElement}
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const InnerCard: ParentComponent<{ title?: string }> = (props) => {
|
||||
return (
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 border border-white/10 bg-[rgba(255,255,255,0.05)]'>
|
||||
<div class="rounded-xl p-4 flex flex-col gap-2 border border-white/10 bg-[rgba(255,255,255,0.05)]">
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const FancyCard: ParentComponent<{
|
||||
title?: string;
|
||||
@@ -47,7 +50,11 @@ export const FancyCard: ParentComponent<{
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.subtitle && <SmallHeader class="text-neutral-500">{props.subtitle}</SmallHeader>}
|
||||
{props.subtitle && (
|
||||
<SmallHeader class="text-neutral-500">
|
||||
{props.subtitle}
|
||||
</SmallHeader>
|
||||
)}
|
||||
</div>
|
||||
{props.tag && props.tag}
|
||||
</div>
|
||||
@@ -63,8 +70,8 @@ export const SafeArea: ParentComponent = (props) => {
|
||||
{props.children}
|
||||
{/* </div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultMain: ParentComponent = (props) => {
|
||||
return (
|
||||
@@ -73,8 +80,8 @@ export const DefaultMain: ParentComponent = (props) => {
|
||||
{/* CSS is hard sometimes */}
|
||||
<div class="py-4" />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const FullscreenLoader = () => {
|
||||
return (
|
||||
@@ -82,23 +89,28 @@ export const FullscreenLoader = () => {
|
||||
<LoadingSpinner wide />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const MutinyWalletGuard: ParentComponent = (props) => {
|
||||
const [state, _] = useMegaStore();
|
||||
return (
|
||||
<Suspense fallback={<FullscreenLoader />}>
|
||||
<Show when={state.mutiny_wallet && !state.wallet_loading}>{props.children}</Show>
|
||||
<Show when={state.mutiny_wallet && !state.wallet_loading}>
|
||||
{props.children}
|
||||
</Show>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const LoadingSpinner = (props: { big?: boolean, wide?: boolean }) => {
|
||||
export const LoadingSpinner = (props: { big?: boolean; wide?: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
class="w-full"
|
||||
classList={{ "flex justify-center": props.wide, "h-full grid": props.big }}
|
||||
classList={{
|
||||
"flex justify-center": props.wide,
|
||||
"h-full grid": props.big
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -119,78 +131,114 @@ export const LoadingSpinner = (props: { big?: boolean, wide?: boolean }) => {
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const Hr = () => <Separator.Root class="my-4 border-white/20" />
|
||||
export const Hr = () => <Separator.Root class="my-4 border-white/20" />;
|
||||
|
||||
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (props) => {
|
||||
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
||||
<h1 class="text-3xl font-semibold">{props.children}</h1>
|
||||
<Show when={props.action}>
|
||||
{props.action}
|
||||
</Show>
|
||||
<Show when={props.action}>{props.action}</Show>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const VStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
||||
return (<div class={`flex flex-col gap-${props.biggap ? "8" : "4"}`}>{props.children}</div>)
|
||||
}
|
||||
return (
|
||||
<div class={`flex flex-col gap-${props.biggap ? "8" : "4"}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
||||
return (<div class={`flex gap-${props.biggap ? "8" : "4"}`}>{props.children}</div>)
|
||||
}
|
||||
return (
|
||||
<div class={`flex gap-${props.biggap ? "8" : "4"}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SmallAmount: ParentComponent<{ amount: number | bigint, sign?: string }> = (props) => {
|
||||
return (<h2 class="font-light text-lg">{props.sign ? `${props.sign} ` : ""}{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
|
||||
}
|
||||
export const SmallAmount: ParentComponent<{
|
||||
amount: number | bigint;
|
||||
sign?: string;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<h2 class="font-light text-lg">
|
||||
{props.sign ? `${props.sign} ` : ""}
|
||||
{props.amount.toLocaleString()} <span class="text-sm">SATS</span>
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
||||
export const NiceP: ParentComponent = (props) => {
|
||||
return (<p class="text-xl font-light">{props.children}</p>)
|
||||
}
|
||||
return <p class="text-xl font-light">{props.children}</p>;
|
||||
};
|
||||
|
||||
export const TinyButton: ParentComponent<{ onClick: () => void, tag?: MutinyTagItem }> = (props) => {
|
||||
export const TinyButton: ParentComponent<{
|
||||
onClick: () => void;
|
||||
tag?: MutinyTagItem;
|
||||
}> = (props) => {
|
||||
// TODO: don't need to run this if it's not a contact
|
||||
const [gradient] = createResource(async () => {
|
||||
return generateGradient(props.tag?.name || "?")
|
||||
})
|
||||
return generateGradient(props.tag?.name || "?");
|
||||
});
|
||||
|
||||
const bg = () => (props.tag?.name && props.tag?.kind === "Contact") ? gradient() : "rgb(255 255 255 / 0.1)"
|
||||
const bg = () =>
|
||||
props.tag?.name && props.tag?.kind === "Contact"
|
||||
? gradient()
|
||||
: "rgb(255 255 255 / 0.1)";
|
||||
|
||||
return (
|
||||
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}
|
||||
<button
|
||||
class="py-1 px-2 rounded-lg bg-white/10"
|
||||
onClick={() => props.onClick()}
|
||||
style={{ background: bg() }}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const Indicator: ParentComponent = (props) => {
|
||||
return (
|
||||
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div>
|
||||
)
|
||||
}
|
||||
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function Checkbox(props: { label: string, checked: boolean, onChange: (checked: boolean) => void }) {
|
||||
export function Checkbox(props: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<KCheckbox.Root class="inline-flex items-center gap-2" checked={props.checked} onChange={props.onChange}>
|
||||
<KCheckbox.Root
|
||||
class="inline-flex items-center gap-2"
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
>
|
||||
<KCheckbox.Input class="" />
|
||||
<KCheckbox.Control class="flex-0 w-8 h-8 rounded-lg border-2 border-white bg-neutral-800 ui-checked:bg-m-red">
|
||||
<KCheckbox.Indicator>
|
||||
<img src={check} class="w-8 h-8" alt="check" />
|
||||
</KCheckbox.Indicator>
|
||||
</KCheckbox.Control>
|
||||
<KCheckbox.Label class="flex-1 text-xl font-light">{props.label}</KCheckbox.Label>
|
||||
<KCheckbox.Label class="flex-1 text-xl font-light">
|
||||
{props.label}
|
||||
</KCheckbox.Label>
|
||||
</KCheckbox.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalCloseButton() {
|
||||
return (<button
|
||||
class="self-center justify-self-center hover:bg-white/10 rounded-lg active:bg-m-blue"
|
||||
>
|
||||
return (
|
||||
<button class="self-center justify-self-center hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||
<img src={close} alt="Close" class="w-8 h-8" />
|
||||
</button>)
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
|
||||
export function MegaCheck() {
|
||||
return <img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] flex-shrink" />;
|
||||
return (
|
||||
<img
|
||||
src={megacheck}
|
||||
alt="success"
|
||||
class="w-1/2 mx-auto max-w-[50vh] flex-shrink"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import megaex from "~/assets/icons/megaex.png";
|
||||
|
||||
export function MegaEx() {
|
||||
return <img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />;
|
||||
return (
|
||||
<img
|
||||
src={megaex}
|
||||
alt="fail"
|
||||
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ export function SuccessModal(props: SuccessModalProps) {
|
||||
{props.children}
|
||||
</Dialog.Description>
|
||||
<div class="w-full flex max-w-[300px] mx-auto">
|
||||
<Button onClick={onNice}>{props.confirmText ?? "Nice"}</Button>
|
||||
<Button onClick={onNice}>
|
||||
{props.confirmText ?? "Nice"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Component, For, createEffect, createSignal } from "solid-js";
|
||||
|
||||
import { Event, nip19 } from "nostr-tools"
|
||||
import { Event, nip19 } from "nostr-tools";
|
||||
import { Linkify } from "~/components/layout";
|
||||
|
||||
type NostrEvent = {
|
||||
"content": string, "created_at": number, id?: string
|
||||
}
|
||||
content: string;
|
||||
created_at: number;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const Note: Component<{ e: NostrEvent }> = (props) => {
|
||||
const linkRoot = "https://snort.social/e/";
|
||||
@@ -14,46 +16,62 @@ const Note: Component<{ e: NostrEvent }> = (props) => {
|
||||
|
||||
createEffect(() => {
|
||||
if (props.e.id) {
|
||||
setNoteId(nip19.noteEncode(props.e.id))
|
||||
setNoteId(nip19.noteEncode(props.e.id));
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex gap-4 border-b border-faint-white py-6 items-start w-full">
|
||||
<img class="bg-black rounded-xl flex-0" src="../180.png" width={45} height={45} />
|
||||
<img
|
||||
class="bg-black rounded-xl flex-0"
|
||||
src="../180.png"
|
||||
width={45}
|
||||
height={45}
|
||||
/>
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<p class="break-words">
|
||||
{/* {props.e.content} */}
|
||||
<Linkify initialText={props.e.content} />
|
||||
</p>
|
||||
<a class="no-underline hover:underline hover:decoration-light-text" href={`${linkRoot}${noteId()}`}>
|
||||
<small class="text-light-text">{(new Date(props.e.created_at * 1000)).toLocaleString()}</small>
|
||||
<a
|
||||
class="no-underline hover:underline hover:decoration-light-text"
|
||||
href={`${linkRoot}${noteId()}`}
|
||||
>
|
||||
<small class="text-light-text">
|
||||
{new Date(props.e.created_at * 1000).toLocaleString()}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function filterReplies(event: Event) {
|
||||
// If there's a "p" tag or an "e" tag we want to return false, otherwise true
|
||||
for (const tag of event.tags) {
|
||||
if (tag[0] === "p" || tag[0] === "e") {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
const Notes: Component<{ notes: Event[] }> = (props) => {
|
||||
return (<ul class="flex flex-col">
|
||||
<For each={props.notes.filter(filterReplies).sort((a, b) => b.created_at - a.created_at)}>
|
||||
{(item) =>
|
||||
<li class="w-full"><Note e={item as NostrEvent} /></li>
|
||||
}
|
||||
return (
|
||||
<ul class="flex flex-col">
|
||||
<For
|
||||
each={props.notes
|
||||
.filter(filterReplies)
|
||||
.sort((a, b) => b.created_at - a.created_at)}
|
||||
>
|
||||
{(item) => (
|
||||
<li class="w-full">
|
||||
<Note e={item as NostrEvent} />
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>)
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default Notes
|
||||
export default Notes;
|
||||
|
||||
@@ -7,17 +7,16 @@ const relayUrls = [
|
||||
"wss://nostr.fmt.wiz.biz",
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land"
|
||||
]
|
||||
];
|
||||
|
||||
import { SimplePool } from 'nostr-tools'
|
||||
import { SimplePool } from "nostr-tools";
|
||||
import { LoadingSpinner } from "~/components/layout";
|
||||
import Notes from "~/components/waitlist/Notes";
|
||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
||||
import logo from "~/assets/icons/mutiny-logo.svg";
|
||||
|
||||
const pool = new SimplePool()
|
||||
const pool = new SimplePool();
|
||||
|
||||
const postsFetcher = async () => {
|
||||
|
||||
const filter = {
|
||||
authors: [
|
||||
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708"
|
||||
@@ -26,10 +25,10 @@ const postsFetcher = async () => {
|
||||
kinds: [1]
|
||||
};
|
||||
|
||||
const events = await pool.list(relayUrls, [filter])
|
||||
const events = await pool.list(relayUrls, [filter]);
|
||||
|
||||
return events;
|
||||
}
|
||||
};
|
||||
|
||||
export function WaitlistAlreadyIn() {
|
||||
const [posts] = createResource("", postsFetcher);
|
||||
@@ -40,7 +39,9 @@ export function WaitlistAlreadyIn() {
|
||||
<img src={logo} class="h-10" alt="logo" />
|
||||
</a>
|
||||
<h1 class="text-4xl font-bold">You're on a list!</h1>
|
||||
<h2 class="text-xl pr-4">We'll message you when Mutiny Wallet is ready.</h2>
|
||||
<h2 class="text-xl pr-4">
|
||||
We'll message you when Mutiny Wallet is ready.
|
||||
</h2>
|
||||
<div class="px-4 sm:px-8 py-8 rounded-xl bg-half-black w-full">
|
||||
<h2 class="text-sm font-semibold uppercase">Recent Updates</h2>
|
||||
<Show
|
||||
|
||||
@@ -2,109 +2,190 @@ import { Match, Switch, createSignal } from "solid-js";
|
||||
import { Button } from "~/components/layout";
|
||||
import { StyledRadioGroup } from "../layout/Radio";
|
||||
import { TextField } from "../layout/TextField";
|
||||
import { SubmitHandler, createForm, email, getValue, required, setValue } from "@modular-forms/solid";
|
||||
import {
|
||||
SubmitHandler,
|
||||
createForm,
|
||||
email,
|
||||
getValue,
|
||||
required,
|
||||
setValue
|
||||
} from "@modular-forms/solid";
|
||||
import { showToast } from "../Toaster";
|
||||
import eify from "~/utils/eify";
|
||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
||||
import logo from "~/assets/icons/mutiny-logo.svg";
|
||||
|
||||
const WAITLIST_ENDPOINT = "https://waitlist.mutiny-waitlist.workers.dev/waitlist";
|
||||
const WAITLIST_ENDPOINT =
|
||||
"https://waitlist.mutiny-waitlist.workers.dev/waitlist";
|
||||
|
||||
const COMMUNICATION_METHODS = [{ value: "nostr", label: "Nostr", caption: "Your freshest npub" }, { value: "email", label: "Email", caption: "Burners welcome" }]
|
||||
const COMMUNICATION_METHODS = [
|
||||
{ value: "nostr", label: "Nostr", caption: "Your freshest npub" },
|
||||
{ value: "email", label: "Email", caption: "Burners welcome" }
|
||||
];
|
||||
|
||||
type WaitlistForm = {
|
||||
user_type: "nostr" | "email",
|
||||
id: string
|
||||
comment?: string
|
||||
}
|
||||
user_type: "nostr" | "email";
|
||||
id: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
const initialValues: WaitlistForm = { user_type: "nostr", id: "", comment: "" };
|
||||
|
||||
export default function WaitlistForm() {
|
||||
const [waitlistForm, { Form, Field }] = createForm<WaitlistForm>({ initialValues });
|
||||
const [waitlistForm, { Form, Field }] = createForm<WaitlistForm>({
|
||||
initialValues
|
||||
});
|
||||
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
const newHandleSubmit: SubmitHandler<WaitlistForm> = async (f: WaitlistForm) => {
|
||||
const newHandleSubmit: SubmitHandler<WaitlistForm> = async (
|
||||
f: WaitlistForm
|
||||
) => {
|
||||
console.log(f);
|
||||
|
||||
// TODO: not sure why waitlistForm.submitting doesn't work for me
|
||||
// https://modularforms.dev/solid/guides/handle-submission
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(WAITLIST_ENDPOINT, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(f)
|
||||
})
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error("nope");
|
||||
} else {
|
||||
// On success set the id in local storage and reload the page
|
||||
localStorage.setItem('waitlist_id', f.id);
|
||||
localStorage.setItem("waitlist_id", f.id);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (f.user_type === "nostr") {
|
||||
const error = new Error("Something went wrong. Are you sure that's a valid npub?")
|
||||
showToast(eify(error))
|
||||
|
||||
const error = new Error(
|
||||
"Something went wrong. Are you sure that's a valid npub?"
|
||||
);
|
||||
showToast(eify(error));
|
||||
} else {
|
||||
const error = new Error("Something went wrong. Not sure what.")
|
||||
showToast(eify(error))
|
||||
const error = new Error("Something went wrong. Not sure what.");
|
||||
showToast(eify(error));
|
||||
}
|
||||
return
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main class='flex flex-col gap-8 py-8 px-4 max-w-xl mx-auto'>
|
||||
<main class="flex flex-col gap-8 py-8 px-4 max-w-xl mx-auto">
|
||||
<a href="https://mutinywallet.com">
|
||||
<img src={logo} class="h-10" alt="logo" />
|
||||
</a>
|
||||
<h1 class='text-4xl font-bold'>Join Waitlist</h1>
|
||||
<h1 class="text-4xl font-bold">Join Waitlist</h1>
|
||||
<h2 class="text-xl">
|
||||
Sign up for our waitlist and we'll send a message when Mutiny Wallet is ready for you.
|
||||
Sign up for our waitlist and we'll send a message when Mutiny
|
||||
Wallet is ready for you.
|
||||
</h2>
|
||||
<Form onSubmit={newHandleSubmit} class="flex flex-col gap-8">
|
||||
<Field name="user_type">
|
||||
{(field, _props) => (
|
||||
// TODO: there's probably a "real" way to do this with modular-forms
|
||||
<StyledRadioGroup value={field.value || "nostr"} onValueChange={(newValue) => setValue(waitlistForm, "user_type", newValue as "nostr" | "email")} choices={COMMUNICATION_METHODS} />
|
||||
<StyledRadioGroup
|
||||
value={field.value || "nostr"}
|
||||
onValueChange={(newValue) =>
|
||||
setValue(
|
||||
waitlistForm,
|
||||
"user_type",
|
||||
newValue as "nostr" | "email"
|
||||
)
|
||||
}
|
||||
choices={COMMUNICATION_METHODS}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Switch>
|
||||
<Match when={getValue(waitlistForm, 'user_type', { shouldActive: false }) === 'nostr'}>
|
||||
<Field name="id"
|
||||
validate={[required("We need some way to contact you")]}
|
||||
<Match
|
||||
when={
|
||||
getValue(waitlistForm, "user_type", {
|
||||
shouldActive: false
|
||||
}) === "nostr"
|
||||
}
|
||||
>
|
||||
<Field
|
||||
name="id"
|
||||
validate={[
|
||||
required("We need some way to contact you")
|
||||
]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} value={field.value} error={field.error} label="Nostr npub or NIP-05" placeholder="npub..." />
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Nostr npub or NIP-05"
|
||||
placeholder="npub..."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Match>
|
||||
<Match when={getValue(waitlistForm, 'user_type', { shouldActive: false }) === 'email'}>
|
||||
<Field name="id"
|
||||
validate={[required("We need some way to contact you"), email("That doesn't look like an email address to me")]}
|
||||
<Match
|
||||
when={
|
||||
getValue(waitlistForm, "user_type", {
|
||||
shouldActive: false
|
||||
}) === "email"
|
||||
}
|
||||
>
|
||||
<Field
|
||||
name="id"
|
||||
validate={[
|
||||
required("We need some way to contact you"),
|
||||
email(
|
||||
"That doesn't look like an email address to me"
|
||||
)
|
||||
]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField {...props} value={field.value} error={field.error} type="email" label="Email" placeholder="email@nokycemail.com" />
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="email@nokycemail.com"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Field name="comment">
|
||||
{(field, props) => (
|
||||
<TextField multiline {...props} value={field.value} error={field.error} label="Comments" placeholder="I want a lightning wallet that does..." />
|
||||
<TextField
|
||||
multiline
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Comments"
|
||||
placeholder="I want a lightning wallet that does..."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Button loading={loading()} disabled={loading() || !waitlistForm.dirty || waitlistForm.submitting || waitlistForm.invalid} class="self-start" intent="red" type="submit" layout="pad">Submit</Button>
|
||||
<Button
|
||||
loading={loading()}
|
||||
disabled={
|
||||
loading() ||
|
||||
!waitlistForm.dirty ||
|
||||
waitlistForm.submitting ||
|
||||
waitlistForm.invalid
|
||||
}
|
||||
class="self-start"
|
||||
intent="red"
|
||||
type="submit"
|
||||
layout="pad"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
createHandler,
|
||||
renderAsync,
|
||||
StartServer,
|
||||
StartServer
|
||||
} from "solid-start/entry-server";
|
||||
|
||||
export default createHandler(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
import initMutinyWallet, { MutinyWallet } from '@mutinywallet/mutiny-wasm';
|
||||
import initWaila from '@mutinywallet/waila-wasm'
|
||||
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
import initWaila from "@mutinywallet/waila-wasm";
|
||||
|
||||
// export type MutinyWalletSettingStrings = {
|
||||
// network?: string, proxy?: string, esplora?: string, rgs?: string, lsp?: string,
|
||||
@@ -8,20 +7,34 @@ import initWaila from '@mutinywallet/waila-wasm'
|
||||
|
||||
export type Network = "bitcoin" | "testnet" | "regtest" | "signet";
|
||||
export type MutinyWalletSettingStrings = {
|
||||
network?: Network, proxy?: string, esplora?: string, rgs?: string, lsp?: string,
|
||||
}
|
||||
network?: Network;
|
||||
proxy?: string;
|
||||
esplora?: string;
|
||||
rgs?: string;
|
||||
lsp?: string;
|
||||
};
|
||||
|
||||
export function getExistingSettings(): MutinyWalletSettingStrings {
|
||||
const network = localStorage.getItem('MUTINY_SETTINGS_network') || import.meta.env.VITE_NETWORK;
|
||||
const proxy = localStorage.getItem('MUTINY_SETTINGS_proxy') || import.meta.env.VITE_PROXY;
|
||||
const esplora = localStorage.getItem('MUTINY_SETTINGS_esplora') || import.meta.env.VITE_ESPLORA;
|
||||
const rgs = localStorage.getItem('MUTINY_SETTINGS_rgs') || import.meta.env.VITE_RGS;
|
||||
const lsp = localStorage.getItem('MUTINY_SETTINGS_lsp') || import.meta.env.VITE_LSP;
|
||||
const network =
|
||||
localStorage.getItem("MUTINY_SETTINGS_network") ||
|
||||
import.meta.env.VITE_NETWORK;
|
||||
const proxy =
|
||||
localStorage.getItem("MUTINY_SETTINGS_proxy") ||
|
||||
import.meta.env.VITE_PROXY;
|
||||
const esplora =
|
||||
localStorage.getItem("MUTINY_SETTINGS_esplora") ||
|
||||
import.meta.env.VITE_ESPLORA;
|
||||
const rgs =
|
||||
localStorage.getItem("MUTINY_SETTINGS_rgs") || import.meta.env.VITE_RGS;
|
||||
const lsp =
|
||||
localStorage.getItem("MUTINY_SETTINGS_lsp") || import.meta.env.VITE_LSP;
|
||||
|
||||
return { network, proxy, esplora, rgs, lsp }
|
||||
return { network, proxy, esplora, rgs, lsp };
|
||||
}
|
||||
|
||||
export async function setAndGetMutinySettings(settings?: MutinyWalletSettingStrings): Promise<MutinyWalletSettingStrings> {
|
||||
export async function setAndGetMutinySettings(
|
||||
settings?: MutinyWalletSettingStrings
|
||||
): Promise<MutinyWalletSettingStrings> {
|
||||
let { network, proxy, esplora, rgs, lsp } = settings || {};
|
||||
|
||||
const existingSettings = getExistingSettings();
|
||||
@@ -33,66 +46,84 @@ export async function setAndGetMutinySettings(settings?: MutinyWalletSettingStri
|
||||
lsp = lsp || existingSettings.lsp;
|
||||
|
||||
if (!network || !proxy || !esplora) {
|
||||
throw new Error("Missing a default setting for network, proxy, or esplora. Check your .env file to make sure it looks like .env.sample")
|
||||
throw new Error(
|
||||
"Missing a default setting for network, proxy, or esplora. Check your .env file to make sure it looks like .env.sample"
|
||||
);
|
||||
}
|
||||
|
||||
localStorage.setItem('MUTINY_SETTINGS_network', network);
|
||||
localStorage.setItem('MUTINY_SETTINGS_proxy', proxy);
|
||||
localStorage.setItem('MUTINY_SETTINGS_esplora', esplora);
|
||||
localStorage.setItem("MUTINY_SETTINGS_network", network);
|
||||
localStorage.setItem("MUTINY_SETTINGS_proxy", proxy);
|
||||
localStorage.setItem("MUTINY_SETTINGS_esplora", esplora);
|
||||
|
||||
if (!rgs || !lsp) {
|
||||
console.warn("RGS or LSP not set")
|
||||
console.warn("RGS or LSP not set");
|
||||
}
|
||||
|
||||
rgs && localStorage.setItem('MUTINY_SETTINGS_rgs', rgs);
|
||||
lsp && localStorage.setItem('MUTINY_SETTINGS_lsp', lsp);
|
||||
rgs && localStorage.setItem("MUTINY_SETTINGS_rgs", rgs);
|
||||
lsp && localStorage.setItem("MUTINY_SETTINGS_lsp", lsp);
|
||||
|
||||
return { network, proxy, esplora, rgs, lsp }
|
||||
return { network, proxy, esplora, rgs, lsp };
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForWasm() {
|
||||
try {
|
||||
if (typeof WebAssembly === "object"
|
||||
&& typeof WebAssembly.instantiate === "function") {
|
||||
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
|
||||
if (
|
||||
typeof WebAssembly === "object" &&
|
||||
typeof WebAssembly.instantiate === "function"
|
||||
) {
|
||||
const module = new WebAssembly.Module(
|
||||
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
|
||||
);
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
throw new Error("Couldn't instantiate WASM Module")
|
||||
throw new Error("Couldn't instantiate WASM Module");
|
||||
}
|
||||
} else {
|
||||
throw new Error("No WebAssembly global object found")
|
||||
throw new Error("No WebAssembly global object found");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<MutinyWallet> {
|
||||
export async function setupMutinyWallet(
|
||||
settings?: MutinyWalletSettingStrings
|
||||
): Promise<MutinyWallet> {
|
||||
await initMutinyWallet();
|
||||
// Might as well init waila while we're at it
|
||||
await initWaila();
|
||||
|
||||
console.time("Setup");
|
||||
console.log("Starting setup...")
|
||||
const { network, proxy, esplora, rgs, lsp } = await setAndGetMutinySettings(settings)
|
||||
console.log("Initializing Mutiny Manager")
|
||||
console.log("Starting setup...");
|
||||
const { network, proxy, esplora, rgs, lsp } = await setAndGetMutinySettings(
|
||||
settings
|
||||
);
|
||||
console.log("Initializing Mutiny Manager");
|
||||
console.log("Using network", network);
|
||||
console.log("Using proxy", proxy);
|
||||
console.log("Using esplora address", esplora);
|
||||
console.log("Using rgs address", rgs);
|
||||
console.log("Using lsp address", lsp);
|
||||
|
||||
const mutinyWallet = await new MutinyWallet("", undefined, proxy, network, esplora, rgs, lsp)
|
||||
const mutinyWallet = await new MutinyWallet(
|
||||
"",
|
||||
undefined,
|
||||
proxy,
|
||||
network,
|
||||
esplora,
|
||||
rgs,
|
||||
lsp
|
||||
);
|
||||
|
||||
const nodes = await mutinyWallet.list_nodes();
|
||||
|
||||
// If we don't have any nodes yet, create one
|
||||
if (!nodes.length) {
|
||||
await mutinyWallet?.new_node()
|
||||
await mutinyWallet?.new_node();
|
||||
}
|
||||
|
||||
return mutinyWallet
|
||||
return mutinyWallet;
|
||||
}
|
||||
19
src/root.tsx
19
src/root.tsx
@@ -10,7 +10,7 @@ import {
|
||||
Meta,
|
||||
Routes,
|
||||
Scripts,
|
||||
Title,
|
||||
Title
|
||||
} from "solid-start";
|
||||
import "./root.css";
|
||||
import { Provider as MegaStoreProvider } from "~/state/megaStore";
|
||||
@@ -29,10 +29,21 @@ export default function Root() {
|
||||
/>
|
||||
<Link rel="manifest" href="/manifest.webmanifest" />
|
||||
<Meta name="theme-color" content="rgb(23,23,23)" />
|
||||
<Meta name="description" content="Lightning wallet for the web" />
|
||||
<Meta
|
||||
name="description"
|
||||
content="Lightning wallet for the web"
|
||||
/>
|
||||
<Link rel="icon" href="/favicon.ico" />
|
||||
<Link rel="apple-touch-icon" href="/images/icon.png" sizes="512x512" />
|
||||
<Link rel="mask-icon" href="/mutiny_logo_mask.svg" color="#000" />
|
||||
<Link
|
||||
rel="apple-touch-icon"
|
||||
href="/images/icon.png"
|
||||
sizes="512x512"
|
||||
/>
|
||||
<Link
|
||||
rel="mask-icon"
|
||||
href="/mutiny_logo_mask.svg"
|
||||
color="#000"
|
||||
/>
|
||||
</Head>
|
||||
<Body>
|
||||
<Suspense>
|
||||
|
||||
@@ -112,7 +112,10 @@ export default function Activity() {
|
||||
<Card title="Activity">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
||||
<Show
|
||||
when={!state.wallet_loading}
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<CombinedActivity />
|
||||
</Show>
|
||||
</VStack>
|
||||
@@ -121,7 +124,10 @@ export default function Activity() {
|
||||
<Tabs.Content value="nostr">
|
||||
<VStack>
|
||||
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
||||
<NiceP>Import your contacts from nostr to see who they're zapping.</NiceP>
|
||||
<NiceP>
|
||||
Import your contacts from nostr to see
|
||||
who they're zapping.
|
||||
</NiceP>
|
||||
<Button disabled intent="blue">
|
||||
Coming soon
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { DeleteEverything } from "~/components/DeleteEverything";
|
||||
import KitchenSink from "~/components/KitchenSink";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { Card, DefaultMain, LargeHeader, MutinyWalletGuard, SafeArea, SmallHeader, VStack } from "~/components/layout";
|
||||
import {
|
||||
Card,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
MutinyWalletGuard,
|
||||
SafeArea,
|
||||
SmallHeader,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
|
||||
export default function Admin() {
|
||||
@@ -12,9 +20,14 @@ export default function Admin() {
|
||||
<BackLink href="/settings" title="Settings" />
|
||||
<LargeHeader>Admin</LargeHeader>
|
||||
<VStack>
|
||||
<Card><p>If you know what you're doing you're in the right place!</p></Card>
|
||||
<Card>
|
||||
<p>
|
||||
If you know what you're doing you're in the
|
||||
right place!
|
||||
</p>
|
||||
</Card>
|
||||
<KitchenSink />
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden'>
|
||||
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden">
|
||||
<SmallHeader>Danger zone</SmallHeader>
|
||||
<DeleteEverything />
|
||||
</div>
|
||||
@@ -23,5 +36,5 @@ export default function Admin() {
|
||||
<NavBar activeTab="none" />
|
||||
</SafeArea>
|
||||
</MutinyWalletGuard>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Button, DefaultMain, LargeHeader, NiceP, MutinyWalletGuard, SafeArea, VStack, Checkbox } from "~/components/layout";
|
||||
import {
|
||||
Button,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
NiceP,
|
||||
MutinyWalletGuard,
|
||||
SafeArea,
|
||||
VStack,
|
||||
Checkbox
|
||||
} from "~/components/layout";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { useNavigate } from 'solid-start';
|
||||
import { SeedWords } from '~/components/SeedWords';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { Show, createEffect, createSignal } from 'solid-js';
|
||||
import { useNavigate } from "solid-start";
|
||||
import { SeedWords } from "~/components/SeedWords";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Show, createEffect, createSignal } from "solid-js";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
|
||||
function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
|
||||
@@ -13,19 +22,31 @@ function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
|
||||
|
||||
createEffect(() => {
|
||||
if (one() && two() && three()) {
|
||||
props.setHasCheckedAll(true)
|
||||
props.setHasCheckedAll(true);
|
||||
} else {
|
||||
props.setHasCheckedAll(false)
|
||||
props.setHasCheckedAll(false);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<Checkbox checked={one()} onChange={setOne} label="I wrote down the words" />
|
||||
<Checkbox checked={two()} onChange={setTwo} label="I understand that my funds are my responsibility" />
|
||||
<Checkbox checked={three()} onChange={setThree} label="I'm not lying just to get this over with" />
|
||||
<Checkbox
|
||||
checked={one()}
|
||||
onChange={setOne}
|
||||
label="I wrote down the words"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={two()}
|
||||
onChange={setTwo}
|
||||
label="I understand that my funds are my responsibility"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={three()}
|
||||
onChange={setThree}
|
||||
label="I'm not lying just to get this over with"
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
@@ -36,8 +57,8 @@ export default function App() {
|
||||
const [hasCheckedAll, setHasCheckedAll] = createSignal(false);
|
||||
|
||||
function wroteDownTheWords() {
|
||||
actions.setHasBackedUp()
|
||||
navigate("/")
|
||||
actions.setHasBackedUp();
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -49,16 +70,32 @@ export default function App() {
|
||||
|
||||
<VStack>
|
||||
<NiceP>Let's get these funds secured.</NiceP>
|
||||
<NiceP>We'll show you 12 words. You write down the 12 words.</NiceP>
|
||||
<NiceP>
|
||||
If you clear your browser history, or lose your device, these 12 words are the only way you can restore your wallet.
|
||||
We'll show you 12 words. You write down the 12
|
||||
words.
|
||||
</NiceP>
|
||||
<NiceP>Mutiny is self-custodial. It's all up to you...</NiceP>
|
||||
<SeedWords words={store.mutiny_wallet?.show_seed() || ""} setHasSeen={setHasSeenBackup} />
|
||||
<NiceP>
|
||||
If you clear your browser history, or lose your
|
||||
device, these 12 words are the only way you can
|
||||
restore your wallet.
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
Mutiny is self-custodial. It's all up to you...
|
||||
</NiceP>
|
||||
<SeedWords
|
||||
words={store.mutiny_wallet?.show_seed() || ""}
|
||||
setHasSeen={setHasSeenBackup}
|
||||
/>
|
||||
<Show when={hasSeenBackup()}>
|
||||
<Quiz setHasCheckedAll={setHasCheckedAll} />
|
||||
</Show>
|
||||
<Button disabled={!hasSeenBackup() || !hasCheckedAll()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
|
||||
<Button
|
||||
disabled={!hasSeenBackup() || !hasCheckedAll()}
|
||||
intent="blue"
|
||||
onClick={wroteDownTheWords}
|
||||
>
|
||||
I wrote down the words
|
||||
</Button>
|
||||
</VStack>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="none" />
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Contact, MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||
import {
|
||||
Contact,
|
||||
MutinyBip21RawMaterials,
|
||||
MutinyInvoice
|
||||
} from "@mutinywallet/mutiny-wasm";
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
@@ -80,14 +84,16 @@ function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
|
||||
<Switch>
|
||||
<Match when={props.flavor === "unified"}>
|
||||
<InfoBox accent="green">
|
||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> will be charged if paid
|
||||
over lightning.
|
||||
A lightning setup fee of{" "}
|
||||
<AmountSmall amountSats={props.fee} /> will be charged
|
||||
if paid over lightning.
|
||||
</InfoBox>
|
||||
</Match>
|
||||
<Match when={props.flavor === "lightning"}>
|
||||
<InfoBox accent="green">
|
||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> will be charged for this
|
||||
receive.
|
||||
A lightning setup fee of{" "}
|
||||
<AmountSmall amountSats={props.fee} /> will be charged
|
||||
for this receive.
|
||||
</InfoBox>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -101,13 +107,15 @@ function FeeExplanation(props: { fee: bigint }) {
|
||||
<Switch>
|
||||
<Match when={props.fee > 1000n}>
|
||||
<InfoBox accent="green">
|
||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> was charged for this
|
||||
A lightning setup fee of{" "}
|
||||
<AmountSmall amountSats={props.fee} /> was charged for this
|
||||
receive.
|
||||
</InfoBox>
|
||||
</Match>
|
||||
<Match when={props.fee > 0n}>
|
||||
<InfoBox accent="green">
|
||||
A lightning service fee of <AmountSmall amountSats={props.fee} /> was charged for this
|
||||
A lightning service fee of{" "}
|
||||
<AmountSmall amountSats={props.fee} /> was charged for this
|
||||
receive.
|
||||
</InfoBox>
|
||||
</Match>
|
||||
@@ -123,12 +131,15 @@ export default function Receive() {
|
||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit");
|
||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||
const [unified, setUnified] = createSignal("");
|
||||
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true);
|
||||
const [shouldShowAmountEditor, setShouldShowAmountEditor] =
|
||||
createSignal(true);
|
||||
|
||||
const [lspFee, setLspFee] = createSignal(0n);
|
||||
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
|
||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
// The data we get after a payment
|
||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||
@@ -159,21 +170,31 @@ export default function Receive() {
|
||||
setSelectedValues([]);
|
||||
}
|
||||
|
||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
||||
async function processContacts(
|
||||
contacts: Partial<MutinyTagItem>[]
|
||||
): Promise<string[]> {
|
||||
console.log("Processing contacts", contacts);
|
||||
|
||||
if (contacts.length) {
|
||||
const first = contacts![0];
|
||||
|
||||
if (!first.name) {
|
||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
||||
console.error(
|
||||
"Something went wrong with contact creation, proceeding anyway"
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!first.id && first.name) {
|
||||
console.error("Creating new contact", first.name);
|
||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
||||
const c = new Contact(
|
||||
first.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const newContactId =
|
||||
await state.mutiny_wallet?.create_new_contact(c);
|
||||
if (newContactId) {
|
||||
return [newContactId];
|
||||
}
|
||||
@@ -185,7 +206,9 @@ export default function Receive() {
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
||||
console.error(
|
||||
"Something went wrong with contact creation, proceeding anyway"
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -193,7 +216,10 @@ export default function Receive() {
|
||||
const bigAmount = BigInt(amount);
|
||||
try {
|
||||
const tags = await processContacts(selectedValues());
|
||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags);
|
||||
const raw = await state.mutiny_wallet?.create_bip21(
|
||||
bigAmount,
|
||||
tags
|
||||
);
|
||||
// Save the raw info so we can watch the address and invoice
|
||||
setBip21Raw(raw);
|
||||
|
||||
@@ -204,7 +230,9 @@ export default function Receive() {
|
||||
|
||||
return `bitcoin:${raw?.address}?${params}`;
|
||||
} catch (e) {
|
||||
showToast(new Error("Couldn't create invoice. Are you asking for enough?"));
|
||||
showToast(
|
||||
new Error("Couldn't create invoice. Are you asking for enough?")
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
@@ -219,7 +247,9 @@ export default function Receive() {
|
||||
setShouldShowAmountEditor(false);
|
||||
}
|
||||
|
||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
||||
async function checkIfPaid(
|
||||
bip21?: MutinyBip21RawMaterials
|
||||
): Promise<PaidState | undefined> {
|
||||
if (bip21) {
|
||||
console.debug("checking if paid...");
|
||||
const lightning = bip21.invoice;
|
||||
@@ -238,7 +268,9 @@ export default function Receive() {
|
||||
return "lightning_paid";
|
||||
}
|
||||
|
||||
const tx = (await state.mutiny_wallet?.check_address(address)) as OnChainTx | undefined;
|
||||
const tx = (await state.mutiny_wallet?.check_address(address)) as
|
||||
| OnChainTx
|
||||
| undefined;
|
||||
|
||||
if (tx) {
|
||||
setReceiveState("paid");
|
||||
@@ -265,10 +297,23 @@ export default function Receive() {
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
||||
<BackButton onClick={() => setReceiveState("edit")} title="Edit" showOnDesktop />
|
||||
<Show
|
||||
when={receiveState() === "show"}
|
||||
fallback={<BackLink />}
|
||||
>
|
||||
<BackButton
|
||||
onClick={() => setReceiveState("edit")}
|
||||
title="Edit"
|
||||
showOnDesktop
|
||||
/>
|
||||
</Show>
|
||||
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>
|
||||
<LargeHeader
|
||||
action={
|
||||
receiveState() === "show" && (
|
||||
<Indicator>Checking</Indicator>
|
||||
)
|
||||
}
|
||||
>
|
||||
Receive Bitcoin
|
||||
</LargeHeader>
|
||||
<Switch>
|
||||
@@ -306,13 +351,16 @@ export default function Receive() {
|
||||
<p class="text-neutral-400 text-center">
|
||||
<Switch>
|
||||
<Match when={flavor() === "lightning"}>
|
||||
Show or share this invoice with the sender.
|
||||
Show or share this invoice with the
|
||||
sender.
|
||||
</Match>
|
||||
<Match when={flavor() === "onchain"}>
|
||||
Show or share this address with the sender.
|
||||
Show or share this address with the
|
||||
sender.
|
||||
</Match>
|
||||
<Match when={flavor() === "unified"}>
|
||||
Show or share this code with the sender. Sender decides method of payment.
|
||||
Show or share this code with the sender.
|
||||
Sender decides method of payment.
|
||||
</Match>
|
||||
</Switch>
|
||||
</p>
|
||||
@@ -325,7 +373,12 @@ export default function Receive() {
|
||||
/>{" "}
|
||||
<ShareCard text={receiveString() ?? ""} />
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||
<Match
|
||||
when={
|
||||
receiveState() === "paid" &&
|
||||
paidState() === "lightning_paid"
|
||||
}
|
||||
>
|
||||
<SuccessModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
@@ -339,10 +392,19 @@ export default function Receive() {
|
||||
>
|
||||
<MegaCheck />
|
||||
<FeeExplanation fee={lspFee()} />
|
||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat centered />
|
||||
<Amount
|
||||
amountSats={paymentInvoice()?.amount_sats}
|
||||
showFiat
|
||||
centered
|
||||
/>
|
||||
</SuccessModal>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
||||
<Match
|
||||
when={
|
||||
receiveState() === "paid" &&
|
||||
paidState() === "onchain_paid"
|
||||
}
|
||||
>
|
||||
<SuccessModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
@@ -355,8 +417,17 @@ export default function Receive() {
|
||||
}}
|
||||
>
|
||||
<MegaCheck />
|
||||
<Amount amountSats={paymentTx()?.received} showFiat centered />
|
||||
<ExternalLink href={mempoolTxUrl(paymentTx()?.txid, network)}>
|
||||
<Amount
|
||||
amountSats={paymentTx()?.received}
|
||||
showFiat
|
||||
centered
|
||||
/>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
paymentTx()?.txid,
|
||||
network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</SuccessModal>
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
ParentComponent,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch,
|
||||
} from "solid-js"
|
||||
Switch
|
||||
} from "solid-js";
|
||||
import {
|
||||
CENTER_COLUMN,
|
||||
MISSING_LABEL,
|
||||
REDSHIFT_LABEL,
|
||||
RIGHT_COLUMN,
|
||||
THREE_COLUMNS,
|
||||
UtxoItem,
|
||||
} from "~/components/Activity"
|
||||
UtxoItem
|
||||
} from "~/components/Activity";
|
||||
import {
|
||||
Card,
|
||||
DefaultMain,
|
||||
@@ -29,43 +29,43 @@ import {
|
||||
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"
|
||||
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 ShiftOption = "utxo" | "lightning";
|
||||
|
||||
type ShiftStage = "choose" | "observe" | "success" | "failure"
|
||||
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
|
||||
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
|
||||
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 = {
|
||||
@@ -83,16 +83,16 @@ const dummyRedshift: RedshiftResult = {
|
||||
introduction_node: {}, // Replace with a dummy value for PublicKey
|
||||
amount_sats: BigInt(1000000),
|
||||
change_amt: BigInt(12345),
|
||||
fees_paid: BigInt(2500),
|
||||
}
|
||||
fees_paid: BigInt(2500)
|
||||
};
|
||||
|
||||
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
const [state, _actions] = useMegaStore()
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const getUtXos = async () => {
|
||||
console.log("Getting utxos")
|
||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]
|
||||
}
|
||||
console.log("Getting utxos");
|
||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
||||
};
|
||||
|
||||
// function findUtxoByOutpoint(
|
||||
// outpoint?: string,
|
||||
@@ -102,7 +102,7 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
// return utxos.find((utxo) => utxo.outpoint === outpoint)
|
||||
// }
|
||||
|
||||
const [_utxos, { refetch: _refetchUtxos }] = createResource(getUtXos)
|
||||
const [_utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
|
||||
|
||||
// const inputUtxo = createMemo(() => {
|
||||
// console.log(utxos())
|
||||
@@ -112,14 +112,15 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
// })
|
||||
|
||||
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
|
||||
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()
|
||||
@@ -127,23 +128,21 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
// // 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)
|
||||
})
|
||||
setRedshifted(true, redshiftResource()?.output_utxo);
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<VStack biggap>
|
||||
@@ -166,23 +165,34 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
</Show>
|
||||
</KV> */}
|
||||
<KV key="Starting amount">
|
||||
<Amount amountSats={redshiftResource()!.amount_sats} />
|
||||
<Amount
|
||||
amountSats={redshiftResource()!.amount_sats}
|
||||
/>
|
||||
</KV>
|
||||
<KV key="Fees paid">
|
||||
<Amount amountSats={redshiftResource()!.fees_paid} />
|
||||
<Amount
|
||||
amountSats={redshiftResource()!.fees_paid}
|
||||
/>
|
||||
</KV>
|
||||
<KV key="Change">
|
||||
<Amount amountSats={redshiftResource()!.change_amt} />
|
||||
<Amount
|
||||
amountSats={redshiftResource()!.change_amt}
|
||||
/>
|
||||
</KV>
|
||||
<KV key="Outbound channel">
|
||||
<VStack>
|
||||
<pre class="whitespace-pre-wrap break-all">
|
||||
{redshiftResource()!.introduction_channel}
|
||||
{
|
||||
redshiftResource()!
|
||||
.introduction_channel
|
||||
}
|
||||
</pre>
|
||||
<a
|
||||
class=""
|
||||
href={mempoolTxUrl(
|
||||
redshiftResource()!.introduction_channel?.split(":")[0],
|
||||
redshiftResource()!.introduction_channel?.split(
|
||||
":"
|
||||
)[0],
|
||||
network
|
||||
)}
|
||||
target="_blank"
|
||||
@@ -201,7 +211,9 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
<a
|
||||
class=""
|
||||
href={mempoolTxUrl(
|
||||
redshiftResource()!.output_channel?.split(":")[0],
|
||||
redshiftResource()!.output_channel?.split(
|
||||
":"
|
||||
)[0],
|
||||
network
|
||||
)}
|
||||
target="_blank"
|
||||
@@ -217,23 +229,30 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
</Show>
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const SHIFT_OPTIONS = [
|
||||
{ value: "utxo", label: "UTXO", caption: "Trade your UTXO for a fresh UTXO" },
|
||||
{
|
||||
value: "utxo",
|
||||
label: "UTXO",
|
||||
caption: "Trade your UTXO for a fresh UTXO"
|
||||
},
|
||||
{
|
||||
value: "lightning",
|
||||
label: "Lightning",
|
||||
caption: "Convert your UTXO into Lightning",
|
||||
},
|
||||
]
|
||||
caption: "Convert your UTXO into Lightning"
|
||||
}
|
||||
];
|
||||
|
||||
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint))
|
||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
|
||||
return (
|
||||
<>
|
||||
<div class={THREE_COLUMNS} onClick={() => props.onClick && props.onClick()}>
|
||||
<div
|
||||
class={THREE_COLUMNS}
|
||||
onClick={() => props.onClick && props.onClick()}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<img src={utxoIcon} alt="coin" />
|
||||
</div>
|
||||
@@ -250,14 +269,16 @@ export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
<SmallHeader
|
||||
class={props.item?.is_spent ? "text-m-red" : "text-m-green"}
|
||||
class={
|
||||
props.item?.is_spent ? "text-m-red" : "text-m-green"
|
||||
}
|
||||
>
|
||||
{/* {props.item?.is_spent ? "SPENT" : "UNSPENT"} */}
|
||||
</SmallHeader>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const FAKE_STATES = [
|
||||
@@ -265,30 +286,30 @@ const FAKE_STATES = [
|
||||
"Opening a channel",
|
||||
"Sending funds through",
|
||||
"Closing the channel",
|
||||
"Redshift complete",
|
||||
]
|
||||
"Redshift complete"
|
||||
];
|
||||
|
||||
function ShiftObserver(props: {
|
||||
setShiftStage: (stage: ShiftStage) => void
|
||||
redshiftId: string
|
||||
setShiftStage: (stage: ShiftStage) => void;
|
||||
redshiftId: string;
|
||||
}) {
|
||||
const [_state, _actions] = useMegaStore()
|
||||
const [_state, _actions] = useMegaStore();
|
||||
|
||||
const [fakeStage, _setFakeStage] = createSignal(2)
|
||||
const [fakeStage, _setFakeStage] = createSignal(2);
|
||||
|
||||
const [sentAmount, setSentAmount] = createSignal(0)
|
||||
const [sentAmount, setSentAmount] = createSignal(0);
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (sentAmount() === 200000) {
|
||||
clearInterval(interval)
|
||||
props.setShiftStage("success")
|
||||
clearInterval(interval);
|
||||
props.setShiftStage("success");
|
||||
// setSentAmount((0))
|
||||
} else {
|
||||
setSentAmount(sentAmount() + 50000)
|
||||
setSentAmount(sentAmount() + 50000);
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// async function checkRedshift(id: string) {
|
||||
// console.log("Checking redshift", id)
|
||||
@@ -336,7 +357,7 @@ function ShiftObserver(props: {
|
||||
</VStack>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const KV: ParentComponent<{ key: string }> = (props) => {
|
||||
@@ -345,68 +366,72 @@ const KV: ParentComponent<{ key: string }> = (props) => {
|
||||
<p class="text-sm font-semibold uppercase">{props.key}</p>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default function Redshift() {
|
||||
const [state, _actions] = useMegaStore()
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose")
|
||||
const [shiftType, setShiftType] = createSignal<ShiftOption>("utxo")
|
||||
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose");
|
||||
const [shiftType, setShiftType] = createSignal<ShiftOption>("utxo");
|
||||
|
||||
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>()
|
||||
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>();
|
||||
|
||||
const getUtXos = async () => {
|
||||
console.log("Getting utxos")
|
||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]
|
||||
}
|
||||
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<
|
||||
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
|
||||
}
|
||||
>;
|
||||
console.log(channels);
|
||||
return channels;
|
||||
};
|
||||
|
||||
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos)
|
||||
const [_channels, { refetch: _refetchChannels }] = createResource(getChannels)
|
||||
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
|
||||
const [_channels, { refetch: _refetchChannels }] =
|
||||
createResource(getChannels);
|
||||
|
||||
const redshiftedUtxos = createMemo(() => {
|
||||
return utxos()?.filter((utxo) => getRedshifted(utxo.outpoint))
|
||||
})
|
||||
return utxos()?.filter((utxo) => getRedshifted(utxo.outpoint));
|
||||
});
|
||||
|
||||
const unredshiftedUtxos = createMemo(() => {
|
||||
return utxos()?.filter((utxo) => !getRedshifted(utxo.outpoint))
|
||||
})
|
||||
return utxos()?.filter((utxo) => !getRedshifted(utxo.outpoint));
|
||||
});
|
||||
|
||||
function resetState() {
|
||||
setShiftStage("choose")
|
||||
setShiftType("utxo")
|
||||
setChosenUtxo(undefined)
|
||||
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
|
||||
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")
|
||||
setShiftStage("observe");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
@@ -425,7 +450,9 @@ export default function Redshift() {
|
||||
accent="red"
|
||||
value={shiftType()}
|
||||
onValueChange={(newValue) =>
|
||||
setShiftType(newValue as ShiftOption)
|
||||
setShiftType(
|
||||
newValue as ShiftOption
|
||||
)
|
||||
}
|
||||
choices={SHIFT_OPTIONS}
|
||||
/>
|
||||
@@ -434,7 +461,11 @@ export default function Redshift() {
|
||||
<NiceP>
|
||||
Choose your{" "}
|
||||
<span class="inline-block">
|
||||
<img class="h-4" src={wave} alt="sine wave" />
|
||||
<img
|
||||
class="h-4"
|
||||
src={wave}
|
||||
alt="sine wave"
|
||||
/>
|
||||
</span>{" "}
|
||||
UTXO to begin
|
||||
</NiceP>
|
||||
@@ -446,24 +477,37 @@ export default function Redshift() {
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state === "ready" &&
|
||||
unredshiftedUtxos()?.length === 0
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
unredshiftedUtxos()
|
||||
?.length === 0
|
||||
}
|
||||
>
|
||||
<code>No utxos (empty state)</code>
|
||||
<code>
|
||||
No utxos (empty
|
||||
state)
|
||||
</code>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state === "ready" &&
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
unredshiftedUtxos() &&
|
||||
unredshiftedUtxos()!.length >= 0
|
||||
unredshiftedUtxos()!
|
||||
.length >= 0
|
||||
}
|
||||
>
|
||||
<For each={unredshiftedUtxos()}>
|
||||
<For
|
||||
each={unredshiftedUtxos()}
|
||||
>
|
||||
{(utxo) => (
|
||||
<Utxo
|
||||
item={utxo}
|
||||
onClick={() => setChosenUtxo(utxo)}
|
||||
onClick={() =>
|
||||
setChosenUtxo(
|
||||
utxo
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -475,7 +519,10 @@ export default function Redshift() {
|
||||
<Card
|
||||
titleElement={
|
||||
<SmallHeader>
|
||||
<span class="text-m-red">Redshifted </span>UTXOs
|
||||
<span class="text-m-red">
|
||||
Redshifted{" "}
|
||||
</span>
|
||||
UTXOs
|
||||
</SmallHeader>
|
||||
}
|
||||
>
|
||||
@@ -485,21 +532,34 @@ export default function Redshift() {
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state === "ready" &&
|
||||
redshiftedUtxos()?.length === 0
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
redshiftedUtxos()
|
||||
?.length === 0
|
||||
}
|
||||
>
|
||||
<code>No utxos (empty state)</code>
|
||||
<code>
|
||||
No utxos (empty
|
||||
state)
|
||||
</code>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state === "ready" &&
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
redshiftedUtxos() &&
|
||||
redshiftedUtxos()!.length >= 0
|
||||
redshiftedUtxos()!
|
||||
.length >= 0
|
||||
}
|
||||
>
|
||||
<For each={redshiftedUtxos()}>
|
||||
{(utxo) => <Utxo item={utxo} />}
|
||||
<For
|
||||
each={redshiftedUtxos()}
|
||||
>
|
||||
{(utxo) => (
|
||||
<Utxo
|
||||
item={utxo}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -507,19 +567,32 @@ export default function Redshift() {
|
||||
</Suspense>
|
||||
</VStack>
|
||||
</Match>
|
||||
<Match when={shiftStage() === "observe" && chosenUtxo()}>
|
||||
<Match
|
||||
when={
|
||||
shiftStage() === "observe" &&
|
||||
chosenUtxo()
|
||||
}
|
||||
>
|
||||
<ShiftObserver
|
||||
setShiftStage={setShiftStage}
|
||||
redshiftId="dummy-redshift"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={shiftStage() === "success" && chosenUtxo()}>
|
||||
<Match
|
||||
when={
|
||||
shiftStage() === "success" &&
|
||||
chosenUtxo()
|
||||
}
|
||||
>
|
||||
<VStack biggap>
|
||||
<RedshiftReport
|
||||
redshift={dummyRedshift}
|
||||
utxo={chosenUtxo()!}
|
||||
/>
|
||||
<Button intent="red" onClick={resetState}>
|
||||
<Button
|
||||
intent="red"
|
||||
onClick={resetState}
|
||||
>
|
||||
Nice
|
||||
</Button>
|
||||
</VStack>
|
||||
@@ -538,5 +611,5 @@ export default function Redshift() {
|
||||
<NavBar activeTab="redshift" />
|
||||
</SafeArea>
|
||||
</MutinyWalletGuard>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,27 +15,40 @@ export type ParsedParams = {
|
||||
memo?: string;
|
||||
node_pubkey?: string;
|
||||
lnurl?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export function toParsedParams(str: string, ourNetwork: string): Result<ParsedParams> {
|
||||
export function toParsedParams(
|
||||
str: string,
|
||||
ourNetwork: string
|
||||
): Result<ParsedParams> {
|
||||
let params;
|
||||
try {
|
||||
params = new PaymentParams(str || "")
|
||||
params = new PaymentParams(str || "");
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return { ok: false, error: new Error("Invalid payment request") }
|
||||
console.error(e);
|
||||
return { ok: false, error: new Error("Invalid payment request") };
|
||||
}
|
||||
|
||||
// If WAILA doesn't return a network we should default to our own
|
||||
// If the networks is testnet and we're on signet we should use signet
|
||||
const network = !params.network ? ourNetwork : params.network === "testnet" && ourNetwork === "signet" ? "signet" : params.network;
|
||||
const network = !params.network
|
||||
? ourNetwork
|
||||
: params.network === "testnet" && ourNetwork === "signet"
|
||||
? "signet"
|
||||
: params.network;
|
||||
|
||||
if (network !== ourNetwork) {
|
||||
return { ok: false, error: new Error(`Destination is for ${params.network} but you're on ${ourNetwork}`) }
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(
|
||||
`Destination is for ${params.network} but you're on ${ourNetwork}`
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true, value: {
|
||||
ok: true,
|
||||
value: {
|
||||
address: params.address,
|
||||
invoice: params.invoice,
|
||||
amount_sats: params.amount_sats,
|
||||
@@ -44,7 +57,7 @@ export function toParsedParams(str: string, ourNetwork: string): Result<ParsedPa
|
||||
node_pubkey: params.node_pubkey,
|
||||
lnurl: params.lnurl
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function Scanner() {
|
||||
@@ -62,14 +75,14 @@ export default function Scanner() {
|
||||
}
|
||||
|
||||
function handlePaste() {
|
||||
navigator.clipboard.readText().then(text => {
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
setScanResult(text);
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await init()
|
||||
})
|
||||
await init();
|
||||
});
|
||||
|
||||
// When we have a nice result we can head over to the send screen
|
||||
createEffect(() => {
|
||||
@@ -80,20 +93,27 @@ export default function Scanner() {
|
||||
showToast(result.error);
|
||||
return;
|
||||
} else {
|
||||
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey || result.value?.lnurl) {
|
||||
if (
|
||||
result.value?.address ||
|
||||
result.value?.invoice ||
|
||||
result.value?.node_pubkey ||
|
||||
result.value?.lnurl
|
||||
) {
|
||||
actions.setScanResult(result.value);
|
||||
navigate("/send")
|
||||
navigate("/send");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="safe-top safe-left safe-right safe-bottom h-full">
|
||||
<Reader onResult={onResult} />
|
||||
<div class="w-full flex flex-col items-center fixed bottom-[2rem] gap-8 px-8">
|
||||
<div class="w-full max-w-[800px] flex flex-col gap-2">
|
||||
<Button intent="blue" onClick={handlePaste}>Paste Something</Button>
|
||||
<Button intent="blue" onClick={handlePaste}>
|
||||
Paste Something
|
||||
</Button>
|
||||
<Button onClick={exit}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from "solid-js";
|
||||
import {
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onMount
|
||||
} from "solid-js";
|
||||
import { Amount } from "~/components/Amount";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import {
|
||||
@@ -58,18 +66,26 @@ export function MethodChooser(props: {
|
||||
|
||||
const methods = createMemo(() => {
|
||||
const lnBalance = store.balance?.lightning || 0n;
|
||||
const onchainBalance = (store.balance?.confirmed || 0n) + (store.balance?.unconfirmed || 0n);
|
||||
const onchainBalance =
|
||||
(store.balance?.confirmed || 0n) +
|
||||
(store.balance?.unconfirmed || 0n);
|
||||
return [
|
||||
{
|
||||
value: "lightning",
|
||||
label: "Lightning Balance",
|
||||
caption: lnBalance > 0n ? `${lnBalance.toLocaleString()} SATS` : "No balance",
|
||||
caption:
|
||||
lnBalance > 0n
|
||||
? `${lnBalance.toLocaleString()} SATS`
|
||||
: "No balance",
|
||||
disabled: lnBalance === 0n
|
||||
},
|
||||
{
|
||||
value: "onchain",
|
||||
label: "On-chain Balance",
|
||||
caption: onchainBalance > 0n ? `${onchainBalance.toLocaleString()} SATS` : "No balance",
|
||||
caption:
|
||||
onchainBalance > 0n
|
||||
? `${onchainBalance.toLocaleString()} SATS`
|
||||
: "No balance",
|
||||
disabled: onchainBalance === 0n
|
||||
}
|
||||
];
|
||||
@@ -115,11 +131,17 @@ function DestinationInput(props: {
|
||||
<SmallHeader>Destination</SmallHeader>
|
||||
<textarea
|
||||
value={props.fieldDestination}
|
||||
onInput={(e) => props.setFieldDestination(e.currentTarget.value)}
|
||||
onInput={(e) =>
|
||||
props.setFieldDestination(e.currentTarget.value)
|
||||
}
|
||||
placeholder="bitcoin:..."
|
||||
class="p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||
/>
|
||||
<Button disabled={!props.fieldDestination} intent="blue" onClick={props.handleDecode}>
|
||||
<Button
|
||||
disabled={!props.fieldDestination}
|
||||
intent="blue"
|
||||
onClick={props.handleDecode}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<HStack>
|
||||
@@ -191,7 +213,9 @@ export default function Send() {
|
||||
const [sentDetails, setSentDetails] = createSignal<SentDetails>();
|
||||
|
||||
// Tagging stuff
|
||||
const [selectedContacts, setSelectedContacts] = createSignal<Partial<MutinyTagItem>[]>([]);
|
||||
const [selectedContacts, setSelectedContacts] = createSignal<
|
||||
Partial<MutinyTagItem>[]
|
||||
>([]);
|
||||
|
||||
// Errors
|
||||
const [error, setError] = createSignal<string>();
|
||||
@@ -211,10 +235,19 @@ export default function Send() {
|
||||
const feeEstimate = () => {
|
||||
if (source() === "lightning") return undefined;
|
||||
|
||||
if (source() === "onchain" && amountSats() && amountSats() > 0n && address()) {
|
||||
if (
|
||||
source() === "onchain" &&
|
||||
amountSats() &&
|
||||
amountSats() > 0n &&
|
||||
address()
|
||||
) {
|
||||
setError(undefined);
|
||||
try {
|
||||
return state.mutiny_wallet?.estimate_tx_fee(address()!, amountSats(), undefined);
|
||||
return state.mutiny_wallet?.estimate_tx_fee(
|
||||
address()!,
|
||||
amountSats(),
|
||||
undefined
|
||||
);
|
||||
} catch (e) {
|
||||
setError(eify(e).message);
|
||||
}
|
||||
@@ -239,8 +272,11 @@ export default function Send() {
|
||||
if (source.memo) setDescription(source.memo);
|
||||
|
||||
if (source.invoice) {
|
||||
state.mutiny_wallet?.decode_invoice(source.invoice).then((invoice) => {
|
||||
if (invoice?.amount_sats) setAmountSats(invoice.amount_sats);
|
||||
state.mutiny_wallet
|
||||
?.decode_invoice(source.invoice)
|
||||
.then((invoice) => {
|
||||
if (invoice?.amount_sats)
|
||||
setAmountSats(invoice.amount_sats);
|
||||
setInvoice(invoice);
|
||||
setSource("lightning");
|
||||
});
|
||||
@@ -249,7 +285,9 @@ export default function Send() {
|
||||
setNodePubkey(source.node_pubkey);
|
||||
setSource("lightning");
|
||||
} else if (source.lnurl) {
|
||||
state.mutiny_wallet?.decode_lnurl(source.lnurl).then((lnurlParams) => {
|
||||
state.mutiny_wallet
|
||||
?.decode_lnurl(source.lnurl)
|
||||
.then((lnurlParams) => {
|
||||
if (lnurlParams.tag === "payRequest") {
|
||||
setAmountSats(source.amount_sats || 0n);
|
||||
setLnurlp(source.lnurl);
|
||||
@@ -303,7 +341,8 @@ export default function Send() {
|
||||
}
|
||||
|
||||
function handlePaste() {
|
||||
if (!navigator.clipboard.readText) return showToast(new Error("Clipboard not supported"));
|
||||
if (!navigator.clipboard.readText)
|
||||
return showToast(new Error("Clipboard not supported"));
|
||||
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
@@ -316,21 +355,31 @@ export default function Send() {
|
||||
});
|
||||
}
|
||||
|
||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
||||
async function processContacts(
|
||||
contacts: Partial<MutinyTagItem>[]
|
||||
): Promise<string[]> {
|
||||
console.log("Processing contacts", contacts);
|
||||
|
||||
if (contacts.length) {
|
||||
const first = contacts![0];
|
||||
|
||||
if (!first.name) {
|
||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
||||
console.error(
|
||||
"Something went wrong with contact creation, proceeding anyway"
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!first.id && first.name) {
|
||||
console.error("Creating new contact", first.name);
|
||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
||||
const c = new Contact(
|
||||
first.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const newContactId =
|
||||
await state.mutiny_wallet?.create_new_contact(c);
|
||||
if (newContactId) {
|
||||
return [newContactId];
|
||||
}
|
||||
@@ -342,7 +391,9 @@ export default function Send() {
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
||||
console.error(
|
||||
"Something went wrong with contact creation, proceeding anyway"
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -360,10 +411,20 @@ export default function Send() {
|
||||
sentDetails.destination = bolt11;
|
||||
// If the invoice has sats use that, otherwise we pass the user-defined amount
|
||||
if (invoice()?.amount_sats) {
|
||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tags);
|
||||
await state.mutiny_wallet?.pay_invoice(
|
||||
firstNode,
|
||||
bolt11,
|
||||
undefined,
|
||||
tags
|
||||
);
|
||||
sentDetails.amount = invoice()?.amount_sats;
|
||||
} else {
|
||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tags);
|
||||
await state.mutiny_wallet?.pay_invoice(
|
||||
firstNode,
|
||||
bolt11,
|
||||
amountSats(),
|
||||
tags
|
||||
);
|
||||
sentDetails.amount = amountSats();
|
||||
}
|
||||
} else if (source() === "lightning" && nodePubkey()) {
|
||||
@@ -399,7 +460,11 @@ export default function Send() {
|
||||
}
|
||||
} else if (source() === "onchain" && address()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
|
||||
const txid = await state.mutiny_wallet?.send_to_address(
|
||||
address()!,
|
||||
amountSats(),
|
||||
tags
|
||||
);
|
||||
sentDetails.amount = amountSats();
|
||||
sentDetails.destination = address();
|
||||
sentDetails.txid = txid;
|
||||
@@ -427,12 +492,22 @@ export default function Send() {
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<Show when={address() || invoice() || nodePubkey() || lnurlp()} fallback={<BackLink />}>
|
||||
<BackButton onClick={() => clearAll()} title="Start Over" />
|
||||
<Show
|
||||
when={
|
||||
address() || invoice() || nodePubkey() || lnurlp()
|
||||
}
|
||||
fallback={<BackLink />}
|
||||
>
|
||||
<BackButton
|
||||
onClick={() => clearAll()}
|
||||
title="Start Over"
|
||||
/>
|
||||
</Show>
|
||||
<LargeHeader>Send Bitcoin</LargeHeader>
|
||||
<SuccessModal
|
||||
title={sentDetails()?.amount ? "Sent" : "Payment Failed"}
|
||||
title={
|
||||
sentDetails()?.amount ? "Sent" : "Payment Failed"
|
||||
}
|
||||
confirmText={sentDetails()?.amount ? "Nice" : "Too Bad"}
|
||||
open={!!sentDetails()}
|
||||
setOpen={(open: boolean) => {
|
||||
@@ -445,16 +520,33 @@ export default function Send() {
|
||||
>
|
||||
<Switch>
|
||||
<Match when={sentDetails()?.failure_reason}>
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<img
|
||||
src={megaex}
|
||||
alt="fail"
|
||||
class="w-1/2 mx-auto max-w-[50vh]"
|
||||
/>
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||
{sentDetails()?.failure_reason}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<Amount amountSats={sentDetails()?.amount} showFiat centered />
|
||||
<img
|
||||
src={megacheck}
|
||||
alt="success"
|
||||
class="w-1/2 mx-auto max-w-[50vh]"
|
||||
/>
|
||||
<Amount
|
||||
amountSats={sentDetails()?.amount}
|
||||
showFiat
|
||||
centered
|
||||
/>
|
||||
<Show when={sentDetails()?.txid}>
|
||||
<ExternalLink href={mempoolTxUrl(sentDetails()?.txid, network)}>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
sentDetails()?.txid,
|
||||
network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
@@ -463,7 +555,14 @@ export default function Send() {
|
||||
</SuccessModal>
|
||||
<VStack biggap>
|
||||
<Switch>
|
||||
<Match when={address() || invoice() || nodePubkey() || lnurlp()}>
|
||||
<Match
|
||||
when={
|
||||
address() ||
|
||||
invoice() ||
|
||||
nodePubkey() ||
|
||||
lnurlp()
|
||||
}
|
||||
>
|
||||
<MethodChooser
|
||||
source={source()}
|
||||
setSource={setSource}
|
||||
@@ -483,7 +582,9 @@ export default function Send() {
|
||||
<SmallHeader>Private tags</SmallHeader>
|
||||
<TagEditor
|
||||
selectedValues={selectedContacts()}
|
||||
setSelectedValues={setSelectedContacts}
|
||||
setSelectedValues={
|
||||
setSelectedContacts
|
||||
}
|
||||
placeholder="Add the receiver for your records"
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ButtonLink, DefaultMain, LargeHeader, MutinyWalletGuard, SafeArea, VStack } from "~/components/layout";
|
||||
import {
|
||||
ButtonLink,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
MutinyWalletGuard,
|
||||
SafeArea,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
import { Logs } from "~/components/Logs";
|
||||
import { Restart } from "~/components/Restart";
|
||||
@@ -18,17 +25,23 @@ export default function Settings() {
|
||||
<LargeHeader>Settings</LargeHeader>
|
||||
<VStack biggap>
|
||||
<VStack>
|
||||
<p class="text-2xl font-light">Write down these words or you'll die!</p>
|
||||
<SeedWords words={store.mutiny_wallet?.show_seed() || ""} />
|
||||
<p class="text-2xl font-light">
|
||||
Write down these words or you'll die!
|
||||
</p>
|
||||
<SeedWords
|
||||
words={store.mutiny_wallet?.show_seed() || ""}
|
||||
/>
|
||||
</VStack>
|
||||
<SettingsStringsEditor />
|
||||
<Logs />
|
||||
<Restart />
|
||||
<ButtonLink href="/admin">"I know what I'm doing"</ButtonLink>
|
||||
<ButtonLink href="/admin">
|
||||
"I know what I'm doing"
|
||||
</ButtonLink>
|
||||
</VStack>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="settings" />
|
||||
</SafeArea>
|
||||
</MutinyWalletGuard>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { AmountCard } from "~/components/AmountCard";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { OnboardWarning } from "~/components/OnboardWarning";
|
||||
import { ShareCard } from "~/components/ShareCard";
|
||||
import { DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
|
||||
import {
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
SafeArea,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
|
||||
const SAMPLE =
|
||||
"bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6";
|
||||
@@ -19,5 +24,5 @@ export default function Admin() {
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="none" />
|
||||
</SafeArea>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import { createForm, required } from "@modular-forms/solid";
|
||||
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
||||
import { For, Match, Show, Switch, createResource, createSignal } from "solid-js";
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createResource,
|
||||
createSignal
|
||||
} from "solid-js";
|
||||
import { AmountCard } from "~/components/AmountCard";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
@@ -49,7 +56,8 @@ export default function Swap() {
|
||||
|
||||
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
|
||||
|
||||
const [channelOpenResult, setChannelOpenResult] = createSignal<ChannelOpenDetails>();
|
||||
const [channelOpenResult, setChannelOpenResult] =
|
||||
createSignal<ChannelOpenDetails>();
|
||||
|
||||
const feeEstimate = () => {
|
||||
if (amountSats()) {
|
||||
@@ -69,11 +77,16 @@ export default function Swap() {
|
||||
};
|
||||
|
||||
const hasLsp = () => {
|
||||
return !!localStorage.getItem("MUTINY_SETTINGS_lsp") || !!import.meta.env.VITE_LSP;
|
||||
return (
|
||||
!!localStorage.getItem("MUTINY_SETTINGS_lsp") ||
|
||||
!!import.meta.env.VITE_LSP
|
||||
);
|
||||
};
|
||||
|
||||
const getPeers = async () => {
|
||||
return (await state.mutiny_wallet?.list_peers()) as Promise<MutinyPeer[]>;
|
||||
return (await state.mutiny_wallet?.list_peers()) as Promise<
|
||||
MutinyPeer[]
|
||||
>;
|
||||
};
|
||||
|
||||
const [peers, { refetch }] = createResource(getPeers);
|
||||
@@ -87,12 +100,17 @@ export default function Swap() {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString);
|
||||
await state.mutiny_wallet?.connect_to_peer(
|
||||
firstNode,
|
||||
peerConnectString
|
||||
);
|
||||
|
||||
await refetch();
|
||||
|
||||
// If peers list contains the peer we just connected to, select it
|
||||
const peer = peers()?.find((p) => p.pubkey === peerConnectString.split("@")[0]);
|
||||
const peer = peers()?.find(
|
||||
(p) => p.pubkey === peerConnectString.split("@")[0]
|
||||
);
|
||||
|
||||
if (peer) {
|
||||
setSelectedPeer(peer.pubkey);
|
||||
@@ -146,13 +164,23 @@ export default function Swap() {
|
||||
};
|
||||
|
||||
const canSwap = () => {
|
||||
const balance = (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
|
||||
const balance =
|
||||
(state.balance?.confirmed || 0n) +
|
||||
(state.balance?.unconfirmed || 0n);
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
if (network === "bitcoin") {
|
||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 50000n && amountSats() <= balance;
|
||||
return (
|
||||
(!!selectedPeer() || !!hasLsp()) &&
|
||||
amountSats() >= 50000n &&
|
||||
amountSats() <= balance
|
||||
);
|
||||
} else {
|
||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 10000n && amountSats() <= balance;
|
||||
return (
|
||||
(!!selectedPeer() || !!hasLsp()) &&
|
||||
amountSats() >= 10000n &&
|
||||
amountSats() <= balance
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,7 +196,9 @@ export default function Swap() {
|
||||
}
|
||||
|
||||
if (
|
||||
amountSats() > (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n) ||
|
||||
amountSats() >
|
||||
(state.balance?.confirmed || 0n) +
|
||||
(state.balance?.unconfirmed || 0n) ||
|
||||
!feeEstimate()
|
||||
) {
|
||||
return "You don't have enough funds to make this channel";
|
||||
@@ -186,8 +216,14 @@ export default function Swap() {
|
||||
<BackLink />
|
||||
<LargeHeader>Swap to Lightning</LargeHeader>
|
||||
<SuccessModal
|
||||
title={channelOpenResult()?.channel ? "Swap Success" : "Swap Failed"}
|
||||
confirmText={channelOpenResult()?.channel ? "Nice" : "Too Bad"}
|
||||
title={
|
||||
channelOpenResult()?.channel
|
||||
? "Swap Success"
|
||||
: "Swap Failed"
|
||||
}
|
||||
confirmText={
|
||||
channelOpenResult()?.channel ? "Nice" : "Too Bad"
|
||||
}
|
||||
open={!!channelOpenResult()}
|
||||
setOpen={(open: boolean) => {
|
||||
if (!open) setChannelOpenResult(undefined);
|
||||
@@ -199,22 +235,45 @@ export default function Swap() {
|
||||
>
|
||||
<Switch>
|
||||
<Match when={channelOpenResult()?.failure_reason}>
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
||||
<img
|
||||
src={megaex}
|
||||
alt="fail"
|
||||
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||
/>
|
||||
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||
{channelOpenResult()?.failure_reason?.message}
|
||||
{
|
||||
channelOpenResult()?.failure_reason
|
||||
?.message
|
||||
}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
||||
<AmountCard
|
||||
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
|
||||
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
|
||||
<img
|
||||
src={megacheck}
|
||||
alt="success"
|
||||
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||
/>
|
||||
<Show when={channelOpenResult()?.channel?.outpoint}>
|
||||
<AmountCard
|
||||
amountSats={
|
||||
channelOpenResult()?.channel?.balance?.toString() ||
|
||||
""
|
||||
}
|
||||
reserve={
|
||||
channelOpenResult()?.channel?.reserve?.toString() ||
|
||||
""
|
||||
}
|
||||
/>
|
||||
<Show
|
||||
when={
|
||||
channelOpenResult()?.channel?.outpoint
|
||||
}
|
||||
>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
channelOpenResult()?.channel?.outpoint?.split(":")[0],
|
||||
channelOpenResult()?.channel?.outpoint?.split(
|
||||
":"
|
||||
)[0],
|
||||
network
|
||||
)}
|
||||
>
|
||||
@@ -226,13 +285,20 @@ export default function Swap() {
|
||||
</Switch>
|
||||
</SuccessModal>
|
||||
<VStack biggap>
|
||||
<MethodChooser source={source()} setSource={setSource} both={false} />
|
||||
<MethodChooser
|
||||
source={source()}
|
||||
setSource={setSource}
|
||||
both={false}
|
||||
/>
|
||||
<VStack>
|
||||
<Show when={!hasLsp()}>
|
||||
<Card>
|
||||
<VStack>
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<label for="peerselect" class="uppercase font-semibold text-sm">
|
||||
<label
|
||||
for="peerselect"
|
||||
class="uppercase font-semibold text-sm"
|
||||
>
|
||||
Use existing peer
|
||||
</label>
|
||||
<select
|
||||
@@ -241,19 +307,34 @@ export default function Swap() {
|
||||
onChange={handlePeerSelect}
|
||||
value={selectedPeer()}
|
||||
>
|
||||
<option value="" class="" selected>
|
||||
<option
|
||||
value=""
|
||||
class=""
|
||||
selected
|
||||
>
|
||||
Choose a peer
|
||||
</option>
|
||||
<For each={peers()}>
|
||||
{(peer) => (
|
||||
<option value={peer.pubkey}>{peer.alias ?? peer.pubkey}</option>
|
||||
<option
|
||||
value={peer.pubkey}
|
||||
>
|
||||
{peer.alias ??
|
||||
peer.pubkey}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
<Show when={!selectedPeer()}>
|
||||
<Form onSubmit={onSubmit} class="flex flex-col gap-4">
|
||||
<Field name="peer" validate={[required("")]}>
|
||||
<Form
|
||||
onSubmit={onSubmit}
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<Field
|
||||
name="peer"
|
||||
validate={[required("")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField
|
||||
{...props}
|
||||
@@ -264,8 +345,14 @@ export default function Swap() {
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Button layout="small" type="submit" disabled={isConnecting()}>
|
||||
{isConnecting() ? "Connecting..." : "Connect"}
|
||||
<Button
|
||||
layout="small"
|
||||
type="submit"
|
||||
disabled={isConnecting()}
|
||||
>
|
||||
{isConnecting()
|
||||
? "Connecting..."
|
||||
: "Connect"}
|
||||
</Button>
|
||||
</Form>
|
||||
</Show>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Title } from "solid-start";
|
||||
import { HttpStatusCode } from "solid-start/server";
|
||||
import { ButtonLink, DefaultMain, LargeHeader, SafeArea } from "~/components/layout";
|
||||
import {
|
||||
ButtonLink,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
SafeArea
|
||||
} from "~/components/layout";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
@@ -9,11 +14,11 @@ export default function NotFound() {
|
||||
<HttpStatusCode code={404} />
|
||||
<DefaultMain>
|
||||
<LargeHeader>Not Found</LargeHeader>
|
||||
<p>
|
||||
This is probably Paul's fault.
|
||||
</p>
|
||||
<p>This is probably Paul's fault.</p>
|
||||
<div class="h-full" />
|
||||
<ButtonLink href="/" intent="red">Dangit</ButtonLink>
|
||||
<ButtonLink href="/" intent="red">
|
||||
Dangit
|
||||
</ButtonLink>
|
||||
</DefaultMain>
|
||||
</SafeArea>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import App from "~/components/App";
|
||||
import { Switch, Match } from "solid-js";
|
||||
import { WaitlistAlreadyIn } from "~/components/waitlist/WaitlistAlreadyIn";
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
/* @refresh reload */
|
||||
|
||||
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
||||
import { ParentComponent, createContext, createEffect, onCleanup, onMount, useContext } from "solid-js";
|
||||
import {
|
||||
ParentComponent,
|
||||
createContext,
|
||||
createEffect,
|
||||
onCleanup,
|
||||
onMount,
|
||||
useContext
|
||||
} from "solid-js";
|
||||
import { createStore, reconcile } from "solid-js/store";
|
||||
import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup";
|
||||
import {
|
||||
MutinyWalletSettingStrings,
|
||||
setupMutinyWallet
|
||||
} from "~/logic/mutinyWalletSetup";
|
||||
import {
|
||||
MutinyBalance,
|
||||
MutinyWallet,
|
||||
@@ -64,7 +74,8 @@ export const Provider: ParentComponent = (props) => {
|
||||
balance: undefined as MutinyBalance | undefined,
|
||||
last_sync: undefined as number | undefined,
|
||||
is_syncing: false,
|
||||
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true",
|
||||
dismissed_restore_prompt:
|
||||
localStorage.getItem("dismissed_restore_prompt") === "true",
|
||||
wallet_loading: true,
|
||||
nwc_enabled: localStorage.getItem("nwc_enabled") === "true",
|
||||
activity: [] as MutinyActivity[]
|
||||
@@ -98,7 +109,9 @@ export const Provider: ParentComponent = (props) => {
|
||||
return "new_here";
|
||||
}
|
||||
},
|
||||
async setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void> {
|
||||
async setupMutinyWallet(
|
||||
settings?: MutinyWalletSettingStrings
|
||||
): Promise<void> {
|
||||
try {
|
||||
setState({ wallet_loading: true });
|
||||
const mutinyWallet = await setupMutinyWallet(settings);
|
||||
@@ -110,7 +123,11 @@ export const Provider: ParentComponent = (props) => {
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
await mutinyWallet.start_nostr_wallet_connect(firstNode);
|
||||
}
|
||||
setState({ mutiny_wallet: mutinyWallet, wallet_loading: false, balance });
|
||||
setState({
|
||||
mutiny_wallet: mutinyWallet,
|
||||
wallet_loading: false,
|
||||
balance
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -133,7 +150,8 @@ export const Provider: ParentComponent = (props) => {
|
||||
if (state.mutiny_wallet && !state.is_syncing) {
|
||||
setState({ is_syncing: true });
|
||||
const newBalance = await state.mutiny_wallet?.get_balance();
|
||||
const price = await state.mutiny_wallet?.get_bitcoin_price();
|
||||
const price =
|
||||
await state.mutiny_wallet?.get_bitcoin_price();
|
||||
setState({
|
||||
balance: newBalance,
|
||||
last_sync: Date.now(),
|
||||
@@ -181,9 +199,15 @@ export const Provider: ParentComponent = (props) => {
|
||||
setState({ user_status: status });
|
||||
|
||||
// Only load node manager when status is approved
|
||||
if (state.user_status === "approved" && !state.mutiny_wallet && !state.deleting) {
|
||||
if (
|
||||
state.user_status === "approved" &&
|
||||
!state.mutiny_wallet &&
|
||||
!state.deleting
|
||||
) {
|
||||
console.log("running setup node manager...");
|
||||
actions.setupMutinyWallet().then(() => console.log("node manager setup done"));
|
||||
actions
|
||||
.setupMutinyWallet()
|
||||
.then(() => console.log("node manager setup done"));
|
||||
|
||||
// Setup an event listener to stop the mutiny wallet when the page unloads
|
||||
window.onunload = async (_e) => {
|
||||
@@ -214,14 +238,18 @@ export const Provider: ParentComponent = (props) => {
|
||||
|
||||
const store = [state, actions] as MegaStore;
|
||||
|
||||
return <MegaStoreContext.Provider value={store}>{props.children}</MegaStoreContext.Provider>;
|
||||
return (
|
||||
<MegaStoreContext.Provider value={store}>
|
||||
{props.children}
|
||||
</MegaStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useMegaStore() {
|
||||
// This is a trick to narrow the typescript types: https://docs.solidjs.com/references/api-reference/component-apis/createContext
|
||||
const context = useContext(MegaStoreContext);
|
||||
if (!context) {
|
||||
throw new Error("useMegaStore: cannot find a MegaStoreContext")
|
||||
throw new Error("useMegaStore: cannot find a MegaStoreContext");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const DIALOG_POSITIONER = "fixed inset-0 h-[100dvh] z-50"
|
||||
export const DIALOG_CONTENT = "h-[100dvh] flex flex-col justify-between px-4 pt-4 pb-8 bg-neutral-800/80 backdrop-blur-xl touch-manipulation select-none"
|
||||
export const DIALOG_POSITIONER = "fixed inset-0 h-[100dvh] z-50";
|
||||
export const DIALOG_CONTENT =
|
||||
"h-[100dvh] flex flex-col justify-between px-4 pt-4 pb-8 bg-neutral-800/80 backdrop-blur-xl touch-manipulation select-none";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Take in a string that looks like this:
|
||||
// bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl
|
||||
// and return an object with this shape: { address: string, amount: number, label: string, lightning: string }
|
||||
// using typescript type annotations
|
||||
export function bip21decode(bip21: string): { address?: string, amount?: number, label?: string, lightning?: string } {
|
||||
const [scheme, data] = bip21.split(':')
|
||||
if (scheme !== 'bitcoin') {
|
||||
// TODO: this is a WAILA job I just want to debug more of the send flow
|
||||
if (bip21.startsWith('lnt')) {
|
||||
return { lightning: bip21 }
|
||||
} else if (bip21.startsWith('tb1')) {
|
||||
return { address: bip21 }
|
||||
} else {
|
||||
throw new Error('Not a bitcoin URI')
|
||||
}
|
||||
}
|
||||
const [address, query] = data.split('?')
|
||||
const params = new URLSearchParams(query)
|
||||
return {
|
||||
address,
|
||||
amount: Number(params.get('amount')) || undefined,
|
||||
label: params.get('label') || undefined,
|
||||
lightning: params.get('lightning') || undefined
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
|
||||
export function satsToUsd(amount: number | undefined, price: number, formatted: boolean): string {
|
||||
export function satsToUsd(
|
||||
amount: number | undefined,
|
||||
price: number,
|
||||
formatted: boolean
|
||||
): string {
|
||||
if (typeof amount !== "number" || isNaN(amount)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const btc = MutinyWallet.convert_sats_to_btc(BigInt(Math.floor(amount)));
|
||||
const btc = MutinyWallet.convert_sats_to_btc(
|
||||
BigInt(Math.floor(amount))
|
||||
);
|
||||
const usd = btc * price;
|
||||
|
||||
if (formatted) {
|
||||
return usd.toLocaleString("en-US", { style: "currency", currency: "USD" });
|
||||
return usd.toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
} else {
|
||||
// Some float fighting shenaningans
|
||||
const roundedUsd = Math.round(usd);
|
||||
@@ -25,7 +34,11 @@ export function satsToUsd(amount: number | undefined, price: number, formatted:
|
||||
}
|
||||
}
|
||||
|
||||
export function usdToSats(amount: number | undefined, price: number, formatted: boolean): string {
|
||||
export function usdToSats(
|
||||
amount: number | undefined,
|
||||
price: number,
|
||||
formatted: boolean
|
||||
): string {
|
||||
if (typeof amount !== "number" || isNaN(amount)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file
|
||||
|
||||
export function downloadTextFile(content: string, fileName: string, type?: string) {
|
||||
export function downloadTextFile(
|
||||
content: string,
|
||||
fileName: string,
|
||||
type?: string
|
||||
) {
|
||||
const contentType = type ? type : "application/json";
|
||||
const a = document.createElement("a");
|
||||
const file = new Blob([content], { type: contentType });
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
export default function eify(e: unknown): Error {
|
||||
if (e instanceof Error) {
|
||||
return e;
|
||||
} else if (typeof e === 'string') {
|
||||
} else if (typeof e === "string") {
|
||||
return new Error(e);
|
||||
} else {
|
||||
return new Error('Unknown error');
|
||||
return new Error("Unknown error");
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
// Simple storage for fake labels
|
||||
// For each outpoint string, we can store a boolean whether it's redshifted or not
|
||||
|
||||
function setRedshifted(redshifted: boolean, outpoint?: string,) {
|
||||
function setRedshifted(redshifted: boolean, outpoint?: string) {
|
||||
if (outpoint === undefined) return;
|
||||
localStorage.setItem(outpoint, redshifted.toString())
|
||||
localStorage.setItem(outpoint, redshifted.toString());
|
||||
}
|
||||
|
||||
function getRedshifted(outpoint: string): boolean {
|
||||
const redshifted = localStorage.getItem(outpoint)
|
||||
const redshifted = localStorage.getItem(outpoint);
|
||||
if (redshifted === null) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return redshifted === 'true'
|
||||
return redshifted === "true";
|
||||
}
|
||||
|
||||
const TEST_UTXO = "47651763fbd74488a478aad80e4205c3e34bbadcfc42b5cd9557ef12a15ab00c:1"
|
||||
const TEST_UTXO =
|
||||
"47651763fbd74488a478aad80e4205c3e34bbadcfc42b5cd9557ef12a15ab00c:1";
|
||||
|
||||
export { setRedshifted, getRedshifted, TEST_UTXO }
|
||||
export { setRedshifted, getRedshifted, TEST_UTXO };
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
export async function generateGradient(str: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const digestBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const digestBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const digestArray = new Uint8Array(digestBuffer);
|
||||
const h1 = digestArray[0] % 360;
|
||||
const h2 = (h1 + 180) % 360;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Network } from "~/logic/mutinyWalletSetup"
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
|
||||
export default function mempoolTxUrl(txid?: string, network?: Network) {
|
||||
if (!txid || !network) {
|
||||
console.error("Problem creating the mempool url")
|
||||
return "#"
|
||||
console.error("Problem creating the mempool url");
|
||||
return "#";
|
||||
}
|
||||
|
||||
if (network) {
|
||||
switch (network) {
|
||||
case "bitcoin":
|
||||
return `https://mempool.space/tx/${txid}`
|
||||
return `https://mempool.space/tx/${txid}`;
|
||||
case "testnet":
|
||||
return `https://mempool.space/testnet/tx/${txid}`
|
||||
return `https://mempool.space/testnet/tx/${txid}`;
|
||||
case "signet":
|
||||
return `https://mutinynet.com/tx/${txid}`
|
||||
return `https://mutinynet.com/tx/${txid}`;
|
||||
default:
|
||||
return `https://mempool.space/tx/${txid}`
|
||||
return `https://mempool.space/tx/${txid}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `https://mempool.space/tx/${txid}`
|
||||
return `https://mempool.space/tx/${txid}`;
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
export function objectToSearchParams<T extends Record<string, string | undefined>>(obj: T): string {
|
||||
return Object.entries(obj)
|
||||
export function objectToSearchParams<
|
||||
T extends Record<string, string | undefined>
|
||||
>(obj: T): string {
|
||||
return (
|
||||
Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
// Value shouldn't be null we just filtered it out but typescript is dumb
|
||||
.map(([key, value]) => value ? `${encodeURIComponent(key)}=${encodeURIComponent(value)}` : "")
|
||||
.join("&");
|
||||
.map(([key, value]) =>
|
||||
value
|
||||
? `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
||||
: ""
|
||||
)
|
||||
.join("&")
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
export function prettyPrintTime(ts: number) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric"
|
||||
};
|
||||
|
||||
return new Date(ts * 1000).toLocaleString('en-US', options);
|
||||
return new Date(ts * 1000).toLocaleString("en-US", options);
|
||||
}
|
||||
|
||||
export function timeAgo(ts?: number | bigint): string {
|
||||
@@ -23,15 +23,15 @@ export function timeAgo(ts?: number | bigint): string {
|
||||
if (elapsedSeconds < 60) {
|
||||
return "Just now";
|
||||
} else if (elapsedMinutes < 60) {
|
||||
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? 's' : ''} ago`;
|
||||
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? "s" : ""} ago`;
|
||||
} else if (elapsedHours < 24) {
|
||||
return `${elapsedHours} hour${elapsedHours > 1 ? 's' : ''} ago`;
|
||||
return `${elapsedHours} hour${elapsedHours > 1 ? "s" : ""} ago`;
|
||||
} else if (elapsedDays < 7) {
|
||||
return `${elapsedDays} day${elapsedDays > 1 ? 's' : ''} ago`;
|
||||
return `${elapsedDays} day${elapsedDays > 1 ? "s" : ""} ago`;
|
||||
} else {
|
||||
const date = new Date(timestamp);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
return `${month}/${day}/${year}`;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm"
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
|
||||
export type MutinyTagItem = {
|
||||
id: string,
|
||||
kind: "Label" | "Contact"
|
||||
name: string,
|
||||
last_used_time: bigint,
|
||||
npub?: string,
|
||||
ln_address?: string,
|
||||
lnurl?: string,
|
||||
}
|
||||
id: string;
|
||||
kind: "Label" | "Contact";
|
||||
name: string;
|
||||
last_used_time: bigint;
|
||||
npub?: string;
|
||||
ln_address?: string;
|
||||
lnurl?: string;
|
||||
};
|
||||
|
||||
export const UNKNOWN_TAG: MutinyTagItem = { id: "Unknown", kind: "Label", name: "Unknown", last_used_time: 0n }
|
||||
export const UNKNOWN_TAG: MutinyTagItem = {
|
||||
id: "Unknown",
|
||||
kind: "Label",
|
||||
name: "Unknown",
|
||||
last_used_time: 0n
|
||||
};
|
||||
|
||||
export function tagsToIds(tags?: MutinyTagItem[]): string[] {
|
||||
if (!tags) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id)
|
||||
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id);
|
||||
}
|
||||
|
||||
export function tagToMutinyTag(tag: TagItem): MutinyTagItem {
|
||||
// @ts-expect-error: FIXME: make typescript less mad about this
|
||||
return tag as MutinyTagItem
|
||||
return tag as MutinyTagItem;
|
||||
}
|
||||
|
||||
export function sortByLastUsed(a: MutinyTagItem, b: MutinyTagItem) {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
// Thanks you https://soorria.com/snippets/use-copy-solidjs
|
||||
import type { Accessor } from 'solid-js'
|
||||
import { createSignal } from 'solid-js'
|
||||
import type { Accessor } from "solid-js";
|
||||
import { createSignal } from "solid-js";
|
||||
export type UseCopyProps = {
|
||||
copiedTimeout?: number
|
||||
}
|
||||
type CopyFn = (text: string) => Promise<void>
|
||||
copiedTimeout?: number;
|
||||
};
|
||||
type CopyFn = (text: string) => Promise<void>;
|
||||
export const useCopy = ({ copiedTimeout = 2000 }: UseCopyProps = {}): [
|
||||
copy: CopyFn,
|
||||
copied: Accessor<boolean>
|
||||
] => {
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
let timeout: NodeJS.Timeout
|
||||
const copy: CopyFn = async text => {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
if (timeout) clearTimeout(timeout)
|
||||
timeout = setTimeout(() => setCopied(false), copiedTimeout)
|
||||
}
|
||||
return [copy, copied]
|
||||
}
|
||||
const [copied, setCopied] = createSignal(false);
|
||||
let timeout: NodeJS.Timeout;
|
||||
const copy: CopyFn = async (text) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => setCopied(false), copiedTimeout);
|
||||
};
|
||||
return [copy, copied];
|
||||
};
|
||||
|
||||
@@ -56,50 +56,41 @@ module.exports = {
|
||||
require("@kobalte/tailwindcss"),
|
||||
plugin(function ({ addUtilities }) {
|
||||
const newUtilities = {
|
||||
'.safe-top': {
|
||||
paddingTop: 'constant(safe-area-inset-top)',
|
||||
paddingTop: 'env(safe-area-inset-top)'
|
||||
".safe-top": {
|
||||
paddingTop: "constant(safe-area-inset-top)",
|
||||
paddingTop: "env(safe-area-inset-top)"
|
||||
},
|
||||
'.safe-left': {
|
||||
paddingLeft: 'constant(safe-area-inset-left)',
|
||||
paddingLeft: 'env(safe-area-inset-left)'
|
||||
".safe-left": {
|
||||
paddingLeft: "constant(safe-area-inset-left)",
|
||||
paddingLeft: "env(safe-area-inset-left)"
|
||||
},
|
||||
'.safe-right': {
|
||||
paddingRight: 'constant(safe-area-inset-right)',
|
||||
paddingRight: 'env(safe-area-inset-right)'
|
||||
".safe-right": {
|
||||
paddingRight: "constant(safe-area-inset-right)",
|
||||
paddingRight: "env(safe-area-inset-right)"
|
||||
},
|
||||
'.safe-bottom': {
|
||||
paddingBottom: 'constant(safe-area-inset-bottom)',
|
||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
||||
".safe-bottom": {
|
||||
paddingBottom: "constant(safe-area-inset-bottom)",
|
||||
paddingBottom: "env(safe-area-inset-bottom)"
|
||||
},
|
||||
'.h-screen-safe': {
|
||||
height: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||
".disable-scrollbars": {
|
||||
scrollbarWidth: "none",
|
||||
"-ms-overflow-style": "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
background: "transparent",
|
||||
display: "none"
|
||||
},
|
||||
'.min-h-screen-safe': {
|
||||
minHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||
"& *::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
background: "transparent",
|
||||
display: "none"
|
||||
},
|
||||
'.max-h-screen-safe': {
|
||||
maxHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||
},
|
||||
'.disable-scrollbars': {
|
||||
scrollbarWidth: 'none',
|
||||
'-ms-overflow-style': 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '0px',
|
||||
background: 'transparent',
|
||||
display: 'none'
|
||||
},
|
||||
'& *::-webkit-scrollbar': {
|
||||
width: '0px',
|
||||
background: 'transparent',
|
||||
display: 'none'
|
||||
},
|
||||
'& *': {
|
||||
scrollbarWidth: 'none',
|
||||
'-ms-overflow-style': 'none'
|
||||
}
|
||||
"& *": {
|
||||
scrollbarWidth: "none",
|
||||
"-ms-overflow-style": "none"
|
||||
}
|
||||
}
|
||||
};
|
||||
addUtilities(newUtilities);
|
||||
}),
|
||||
// Text shadow!
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"include": ["global.d.ts", "src/**/*"],
|
||||
"include": [
|
||||
"global.d.ts",
|
||||
"src/**/*",
|
||||
"tailwind.config.cjs",
|
||||
".eslintrc.cjs"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
Reference in New Issue
Block a user