diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1cb2956..0e6bb18 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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" - ], - "rules": { - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_" }], + plugins: ["@typescript-eslint", "solid", "import"], + 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 } } } -} +}; diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a2bf320 --- /dev/null +++ b/.prettierignore @@ -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/ diff --git a/.prettierrc b/.prettierrc index 55dd57c..e8b8c13 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,9 +1,9 @@ { "trailingComma": "none", - "tabWidth": 2, + "tabWidth": 4, "semi": true, "singleQuote": false, "arrowParens": "always", - "printWidth": 100, + "printWidth": 80, "useTabs": false } diff --git a/README.md b/README.md index 0c39f37..8a83a8f 100644 --- a/README.md +++ b/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`. diff --git a/justfile b/justfile index 7628d12..db2fed0 100644 --- a/justfile +++ b/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 diff --git a/package.json b/package.json index fb4e2dd..3756de5 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,58 @@ { - "name": "mws", - "version": "0.3.7", - "license": "MIT", - "packageManager": "pnpm@8.3.1", - "scripts": { - "dev": "solid-start dev", - "host": "solid-start dev --host", - "build": "solid-start build", - "start": "solid-start start", - "lint": "eslint src --ext .ts,.tsx,.js" - }, - "type": "module", - "devDependencies": { - "@playwright/test": "^1.34.3", - "@types/node": "^18.16.15", - "@typescript-eslint/eslint-plugin": "^5.59.7", - "@typescript-eslint/parser": "^5.59.7", - "autoprefixer": "^10.4.14", - "esbuild": "^0.14.54", - "eslint": "^8.41.0", - "eslint-import-resolver-typescript": "2.7.1", - "eslint-plugin-import": "2.27.5", - "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-solid": "0.11.0", - "postcss": "^8.4.23", - "solid-start-node": "^0.2.26", - "tailwindcss": "^3.3.2", - "typescript": "^4.9.5", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.14.7", - "vite-plugin-wasm": "^3.2.2", - "workbox-window": "^6.5.4" - }, - "dependencies": { - "@kobalte/core": "^0.9.6", - "@kobalte/tailwindcss": "^0.5.0", - "@modular-forms/solid": "^0.13.2", - "@mutinywallet/mutiny-wasm": "0.3.7", - "@mutinywallet/waila-wasm": "^0.2.0", - "@solid-primitives/upload": "^0.0.111", - "@solidjs/meta": "^0.28.5", - "@solidjs/router": "^0.8.2", - "@thisbeyond/solid-select": "^0.14.0", - "class-variance-authority": "^0.4.0", - "nostr-tools": "^1.11.1", - "qr-scanner": "^1.4.2", - "solid-js": "^1.7.5", - "solid-qr-code": "^0.0.8", - "solid-start": "^0.2.26", - "undici": "^5.22.1" - }, - "engines": { - "node": ">=16.8" - } + "name": "mws", + "version": "0.3.7", + "license": "MIT", + "packageManager": "pnpm@8.3.1", + "scripts": { + "dev": "solid-start dev", + "host": "solid-start dev --host", + "build": "solid-start build", + "start": "solid-start start", + "lint": "eslint src --ext .ts,.tsx,.js", + "format": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\"" + }, + "type": "module", + "devDependencies": { + "@playwright/test": "^1.34.3", + "@types/node": "^18.16.15", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", + "autoprefixer": "^10.4.14", + "esbuild": "^0.14.54", + "eslint": "^8.41.0", + "eslint-import-resolver-typescript": "2.7.1", + "eslint-plugin-import": "2.27.5", + "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", + "vite": "^4.3.9", + "vite-plugin-pwa": "^0.14.7", + "vite-plugin-wasm": "^3.2.2", + "workbox-window": "^6.5.4" + }, + "dependencies": { + "@kobalte/core": "^0.9.6", + "@kobalte/tailwindcss": "^0.5.0", + "@modular-forms/solid": "^0.13.2", + "@mutinywallet/mutiny-wasm": "0.3.7", + "@mutinywallet/waila-wasm": "^0.2.0", + "@solid-primitives/upload": "^0.0.111", + "@solidjs/meta": "^0.28.5", + "@solidjs/router": "^0.8.2", + "@thisbeyond/solid-select": "^0.14.0", + "class-variance-authority": "^0.4.0", + "nostr-tools": "^1.11.1", + "qr-scanner": "^1.4.2", + "solid-js": "^1.7.5", + "solid-qr-code": "^0.0.8", + "solid-start": "^0.2.26", + "undici": "^5.22.1" + }, + "engines": { + "node": ">=16.8" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0644d28..1ad9529 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/src/assets/svg/Back.tsx b/src/assets/svg/Back.tsx index 5447ec9..823f5cb 100644 --- a/src/assets/svg/Back.tsx +++ b/src/assets/svg/Back.tsx @@ -1,6 +1,18 @@ export function Back() { - return ( - - - ) -} \ No newline at end of file + return ( + + + + ); +} diff --git a/src/assets/svg/Paste.tsx b/src/assets/svg/Paste.tsx index 07adc78..cab7990 100644 --- a/src/assets/svg/Paste.tsx +++ b/src/assets/svg/Paste.tsx @@ -1,8 +1,24 @@ export function Paste() { - return ( - - - - ) + return ( + + + + + + ); } - diff --git a/src/assets/svg/Scan.tsx b/src/assets/svg/Scan.tsx index ef09950..484b4db 100644 --- a/src/assets/svg/Scan.tsx +++ b/src/assets/svg/Scan.tsx @@ -1,6 +1,18 @@ export function Scan() { - return ( - - ) + return ( + + + + ); } - diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 32b5947..48e25e3 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -6,112 +6,125 @@ import { ActivityItem, HackActivityType } from "./ActivityItem"; 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"; + "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 = { - txid: string; - received: number; - sent: number; - fee?: number; - confirmation_time?: { - Confirmed?: { - height: number; - time: number; + txid: string; + received: number; + sent: number; + fee?: number; + confirmation_time?: { + Confirmed?: { + height: number; + time: number; + }; }; - }; - labels: string[]; + labels: string[]; }; export type UtxoItem = { - outpoint: string; - txout: { - value: number; - script_pubkey: string; - }; - keychain: string; - is_spent: boolean; - redshifted?: boolean; + outpoint: string; + txout: { + value: number; + script_pubkey: string; + }; + keychain: string; + is_spent: boolean; + redshifted?: boolean; }; function UnifiedActivityItem(props: { - item: MutinyActivity; - onClick: (id: string, kind: HackActivityType) => void; + item: MutinyActivity; + onClick: (id: string, kind: HackActivityType) => void; }) { - const click = () => { - props.onClick(props.item.id, props.item.kind as unknown as HackActivityType); - }; + const click = () => { + props.onClick( + props.item.id, + props.item.kind as unknown as HackActivityType + ); + }; - return ( - - ); + return ( + + ); } export function CombinedActivity(props: { limit?: number }) { - const [state, actions] = useMegaStore(); + const [state, actions] = useMegaStore(); - const [detailsOpen, setDetailsOpen] = createSignal(false); - const [detailsKind, setDetailsKind] = createSignal(); - const [detailsId, setDetailsId] = createSignal(""); + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + const [detailsId, setDetailsId] = createSignal(""); - function openDetailsModal(id: string, kind: HackActivityType) { - console.log("Opening details modal: ", id, kind); + function openDetailsModal(id: string, kind: HackActivityType) { + console.log("Opening details modal: ", id, kind); - setDetailsId(id); - setDetailsKind(kind); - setDetailsOpen(true); - } - - createEffect(() => { - if (!state.wallet_loading && !state.is_syncing) { - actions.syncActivity(); + setDetailsId(id); + setDetailsKind(kind); + setDetailsOpen(true); } - }); - return ( - <> - - - - - -
- Receive some sats to get started -
-
- props.limit}> - - {(activityItem) => ( - - )} - - - = 0}> - - {(activityItem) => ( - - )} - - -
- - ); + createEffect(() => { + if (!state.wallet_loading && !state.is_syncing) { + actions.syncActivity(); + } + }); + + return ( + <> + + + + + +
+ Receive some sats to get started +
+
+ props.limit} + > + + {(activityItem) => ( + + )} + + + = 0}> + + {(activityItem) => ( + + )} + + +
+ + ); } diff --git a/src/components/ActivityItem.tsx b/src/components/ActivityItem.tsx index f2134d8..6035086 100644 --- a/src/components/ActivityItem.tsx +++ b/src/components/ActivityItem.tsx @@ -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"; @@ -9,137 +15,161 @@ import { useMegaStore } from "~/state/megaStore"; import { Contact } from "@mutinywallet/mutiny-wasm"; export const ActivityAmount: ParentComponent<{ - amount: string; - price: number; - positive?: boolean; - center?: boolean; + amount: string; + price: number; + positive?: boolean; + center?: boolean; }> = (props) => { - const amountInUsd = createMemo(() => { - const parsed = Number(props.amount); - if (isNaN(parsed)) { - return props.amount; - } else { - return satsToUsd(props.price, parsed, true); - } - }); + const amountInUsd = createMemo(() => { + const parsed = Number(props.amount); + if (isNaN(parsed)) { + return props.amount; + } else { + return satsToUsd(props.price, parsed, true); + } + }); - const prettyPrint = createMemo(() => { - const parsed = Number(props.amount); - if (isNaN(parsed)) { - return props.amount; - } else { - return parsed.toLocaleString(); - } - }); + const prettyPrint = createMemo(() => { + const parsed = Number(props.amount); + if (isNaN(parsed)) { + return props.amount; + } else { + return parsed.toLocaleString(); + } + }); - return ( -
-
- {props.positive && "+ "} - {prettyPrint()} SATS -
-
- ≈ {amountInUsd()} USD -
-
- ); + return ( +
+
+ {props.positive && "+ "} + {prettyPrint()} SATS +
+
+ ≈ {amountInUsd()}  + USD +
+
+ ); }; -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 || "?"); - }); +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 ? "≡" : "?"; - const bg = () => (props.name && props.contact ? gradient() : "gray"); + const text = () => + props.contact && props.name && props.name.length + ? props.name[0] + : props.label + ? "≡" + : "?"; + const bg = () => (props.name && props.contact ? gradient() : "gray"); - return ( -
- {text()} -
- ); + return ( +
+ {text()} +
+ ); } export type HackActivityType = "Lightning" | "OnChain" | "ChannelOpen"; export function ActivityItem(props: { - // This is actually the ActivityType enum but wasm is hard - kind: HackActivityType; - contacts: Contact[]; - labels: string[]; - amount: number | bigint; - date?: number | bigint; - positive?: boolean; - onClick?: () => void; + // This is actually the ActivityType enum but wasm is hard + kind: HackActivityType; + contacts: Contact[]; + labels: string[]; + amount: number | bigint; + date?: number | bigint; + positive?: boolean; + onClick?: () => void; }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const firstContact = () => (props.contacts?.length ? props.contacts[0] : null); + const firstContact = () => + props.contacts?.length ? props.contacts[0] : null; - return ( -
props.onClick && props.onClick()} - class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0" - classList={{ "cursor-pointer": !!props.onClick }} - > -
-
- - - lightning - - - onchain - - - swap - - + return ( +
props.onClick && props.onClick()} + class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0" + classList={{ "cursor-pointer": !!props.onClick }} + > +
+
+ + + lightning + + + onchain + + + swap + + +
+
+ 0} + label={props.labels?.length > 0} + /> +
+
+
+ + + + {firstContact()?.name} + + + 0}> + + {props.labels[0]} + + + + + Unknown + + + + + 2147483647}> + + + + + + +
+
+ +
-
- 0} - label={props.labels?.length > 0} - /> -
-
-
- - - {firstContact()?.name} - - 0}> - {props.labels[0]} - - - Unknown - - - - 2147483647}> - - - - - - -
-
- -
-
- ); + ); } diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx index 97fb8e2..d54c48e 100644 --- a/src/components/Amount.tsx +++ b/src/components/Amount.tsx @@ -1,49 +1,56 @@ -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 n.toLocaleString() + if (!n || n.valueOf() === 0) { + return "0"; + } + return n.toLocaleString(); } export function Amount(props: { - amountSats: bigint | number | undefined; - showFiat?: boolean; - loading?: boolean; - centered?: boolean; + amountSats: bigint | number | undefined; + showFiat?: boolean; + loading?: boolean; + centered?: boolean; }) { - const [state, _] = useMegaStore(); + const [state, _] = useMegaStore(); - const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true); + const amountInUsd = () => + satsToUsd(state.price, Number(props.amountSats) || 0, true); - return ( -
-

- {props.loading ? "..." : prettyPrintAmount(props.amountSats)}  - SATS -

- -

- ≈ {props.loading ? "..." : amountInUsd()}  - USD -

-
-
- ); + return ( +
+

+ {props.loading ? "..." : prettyPrintAmount(props.amountSats)} +   + SATS +

+ +

+ ≈ {props.loading ? "..." : amountInUsd()}  + USD +

+
+
+ ); } export function AmountSmall(props: { - amountSats: bigint | number | undefined + amountSats: bigint | number | undefined; }) { - return ( - - {prettyPrintAmount(props.amountSats)}  - - {props.amountSats === 1 || props.amountSats === 1n ? "SAT" : "SATS"} - - - ); + return ( + + {prettyPrintAmount(props.amountSats)}  + + {props.amountSats === 1 || props.amountSats === 1n + ? "SAT" + : "SATS"} + + + ); } diff --git a/src/components/AmountCard.tsx b/src/components/AmountCard.tsx index c6cc476..eec9f05 100644 --- a/src/components/AmountCard.tsx +++ b/src/components/AmountCard.tsx @@ -5,132 +5,172 @@ import { satsToUsd } from "~/utils/conversions"; import { AmountEditable } from "./AmountEditable"; const noop = () => { - // do nothing + // do nothing }; const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => { - return ( -
-
{props.key}
-
{props.children}
-
- ); + return ( +
+
{props.key}
+
{props.children}
+
+ ); }; -export const InlineAmount: ParentComponent<{ amount: string; sign?: string; fiat?: boolean }> = ( - props -) => { - const prettyPrint = createMemo(() => { - const parsed = Number(props.amount); - if (isNaN(parsed)) { - return props.amount; - } else { - return parsed.toLocaleString(); - } - }); +export const InlineAmount: ParentComponent<{ + amount: string; + sign?: string; + fiat?: boolean; +}> = (props) => { + const prettyPrint = createMemo(() => { + const parsed = Number(props.amount); + if (isNaN(parsed)) { + return props.amount; + } else { + return parsed.toLocaleString(); + } + }); - return ( -
- {props.sign ? `${props.sign} ` : ""} - {props.fiat ? "$" : ""} - {prettyPrint()} {props.fiat ? "USD" : "SATS"} -
- ); + return ( +
+ {props.sign ? `${props.sign} ` : ""} + {props.fiat ? "$" : ""} + {prettyPrint()}{" "} + {props.fiat ? "USD" : "SATS"} +
+ ); }; function USDShower(props: { amountSats: string; fee?: string }) { - const [state, _] = useMegaStore(); - const amountInUsd = () => satsToUsd(state.price, add(props.amountSats, props.fee), true); + const [state, _] = useMegaStore(); + const amountInUsd = () => + satsToUsd(state.price, add(props.amountSats, props.fee), true); - return ( - - -
- ≈ {amountInUsd()} USD -
-
-
- ); + return ( + + +
+ ≈ {amountInUsd()}  + USD +
+
+
+ ); } function add(a: string, b?: string) { - return Number(a || 0) + Number(b || 0); + return Number(a || 0) + Number(b || 0); } export function AmountCard(props: { - amountSats: string; - fee?: string; - reserve?: string; - initialOpen?: boolean; - isAmountEditable?: boolean; - setAmountSats?: (amount: bigint) => void; + amountSats: string; + fee?: string; + reserve?: string; + initialOpen?: boolean; + isAmountEditable?: boolean; + setAmountSats?: (amount: bigint) => void; }) { - return ( - - - - -
- - } - > - - - - - - -
-
-
- - - - -
-
- -
- - - - - - -
-
-
- - - - -
-
- -
- - } - > - - - - -
-
-
-
-
- ); + return ( + + + + +
+ + + } + > + + + + + + +
+
+
+ + + + +
+
+ +
+ + + + + + +
+
+
+ + + + +
+
+ +
+ + + } + > + + + + +
+
+
+
+
+ ); } diff --git a/src/components/AmountEditable.tsx b/src/components/AmountEditable.tsx index 2cd53c4..dca324b 100644 --- a/src/components/AmountEditable.tsx +++ b/src/components/AmountEditable.tsx @@ -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,328 +17,393 @@ 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" }, - { label: "100k", amount: "100000" }, - { label: "1m", amount: "1000000" } + { label: "10k", amount: "10000" }, + { label: "100k", amount: "100000" }, + { label: "1m", amount: "1000000" } ]; const FIXED_AMOUNTS_USD = [ - { label: "$1", amount: "1" }, - { label: "$10", amount: "10" }, - { label: "$100", amount: "100" } + { label: "$1", amount: "1" }, + { label: "$10", amount: "10" }, + { label: "$100", amount: "100" } ]; function fiatInputSanitizer(input: string): string { - // Make sure only numbers and a single decimal point are allowed - const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1"); - - // Remove leading zeros if not a decimal, add 0 if starts with a decimal - const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0."); + // Make sure only numbers and a single decimal point are allowed + const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1"); - // 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; + // Remove leading zeros if not a decimal, add 0 if starts with a decimal + const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0."); - // Truncate any numbers two past the decimal - const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1"); + // 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; - return twoDecimals; + // Truncate any numbers two past the decimal + const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1"); + + return twoDecimals; } function satsInputSanitizer(input: string): string { - // Make sure only numbers are allowed - const numeric = input.replace(/[^0-9]/g, ""); - // If it starts with a 0, remove the 0 - const noLeadingZero = numeric.replace(/^0([^.]|$)/g, "$1"); + // Make sure only numbers are allowed + const numeric = input.replace(/[^0-9]/g, ""); + // If it starts with a 0, remove the 0 + const noLeadingZero = numeric.replace(/^0([^.]|$)/g, "$1"); - return noLeadingZero; + return noLeadingZero; } function SingleDigitButton(props: { - character: string; - onClick: (c: string) => void; - fiat: boolean; + character: string; + onClick: (c: string) => void; + fiat: boolean; }) { - return ( - // Skip the "." if it's fiat - }> - - - ); + return ( + // Skip the "." if it's fiat + } + > + + + ); } function BigScalingText(props: { text: string; fiat: boolean }) { - const chars = () => props.text.length; + const chars = () => props.text.length; - return ( -

9, - "scale-95": chars() > 8, - "scale-100": chars() > 7, - "scale-105": chars() > 6, - "scale-110": chars() > 5, - "scale-125": chars() > 4, - "scale-150": chars() <= 4 - }} - > - {props.text} {props.fiat ? "USD" : "SATS"} -

- ); + return ( +

9, + "scale-95": chars() > 8, + "scale-100": chars() > 7, + "scale-105": chars() > 6, + "scale-110": chars() > 5, + "scale-125": chars() > 4, + "scale-150": chars() <= 4 + }} + > + {props.text}  + {props.fiat ? "USD" : "SATS"} +

+ ); } function SmallSubtleAmount(props: { text: string; fiat: boolean }) { - return ( -

- ≈ {props.text} {props.fiat ? "USD" : "SATS"} -

- ); + return ( +

+ ≈ {props.text}  + {props.fiat ? "USD" : "SATS"} +

+ ); } function toDisplayHandleNaN(input: string, _fiat: boolean): string { - const parsed = Number(input); - if (isNaN(parsed)) { - return "0"; - } else { - return parsed.toLocaleString(); - } + const parsed = Number(input); + if (isNaN(parsed)) { + return "0"; + } else { + return parsed.toLocaleString(); + } } export const AmountEditable: ParentComponent<{ - initialAmountSats: string; - initialOpen: boolean; - setAmountSats: (s: bigint) => void; + initialAmountSats: string; + initialOpen: boolean; + setAmountSats: (s: bigint) => void; }> = (props) => { - 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 [localFiat, setLocalFiat] = createSignal( - satsToUsd(state.price, parseInt(props.initialAmountSats || "0") || 0, false) - ); + 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 [localFiat, setLocalFiat] = createSignal( + satsToUsd( + state.price, + parseInt(props.initialAmountSats || "0") || 0, + false + ) + ); - const displaySats = () => toDisplayHandleNaN(localSats(), false); - const displayFiat = () => `$${toDisplayHandleNaN(localFiat(), true)}`; + const displaySats = () => toDisplayHandleNaN(localSats(), false); + const displayFiat = () => `$${toDisplayHandleNaN(localFiat(), true)}`; - let satsInputRef!: HTMLInputElement; - let fiatInputRef!: HTMLInputElement; + let satsInputRef!: HTMLInputElement; + let fiatInputRef!: HTMLInputElement; - const [inboundCapacity] = createResource(async () => { - const channels = await state.mutiny_wallet?.list_channels(); - let inbound = 0; + const [inboundCapacity] = createResource(async () => { + const channels = await state.mutiny_wallet?.list_channels(); + let inbound = 0; - for (const channel of channels) { - inbound += channel.size - (channel.balance + channel.reserve); + for (const channel of channels) { + inbound += channel.size - (channel.balance + channel.reserve); + } + + return inbound; + }); + + const warningText = () => { + if ((state.balance?.lightning || 0n) === 0n) { + const network = state.mutiny_wallet?.get_network() as Network; + if (network === "bitcoin") { + return "Your first lightning receive needs to be 50,000 sats or greater."; + } else { + return "Your first lightning receive needs to be 10,000 sats or greater."; + } + } + + const parsed = Number(localSats()); + if (isNaN(parsed)) { + return undefined; + } + + if (parsed > (inboundCapacity() || 0)) { + return "A lightning setup fee will be charged if paid over lightning."; + } + + return undefined; + }; + + function handleCharacterInput(character: string) { + const isFiatMode = mode() === "fiat"; + const inputSanitizer = isFiatMode + ? fiatInputSanitizer + : satsInputSanitizer; + const localValue = isFiatMode ? localFiat : localSats; + + let sane; + + if (character === "DEL") { + sane = inputSanitizer(localValue().slice(0, -1)); + } else { + if (localValue() === "0") { + sane = inputSanitizer(character); + } else { + sane = inputSanitizer(localValue() + character); + } + } + + if (isFiatMode) { + setLocalFiat(sane); + setLocalSats( + usdToSats(state.price, parseFloat(sane || "0") || 0, false) + ); + } else { + setLocalSats(sane); + setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false)); + } + + // After a button press make sure we re-focus the input + focus(); } - return inbound; - }); - - const warningText = () => { - if ((state.balance?.lightning || 0n) === 0n) { - const network = state.mutiny_wallet?.get_network() as Network; - if (network === "bitcoin") { - return "Your first lightning receive needs to be 50,000 sats or greater."; - } else { - return "Your first lightning receive needs to be 10,000 sats or greater."; - } + function setFixedAmount(amount: string) { + if (mode() === "fiat") { + setLocalFiat(amount); + setLocalSats( + usdToSats(state.price, parseFloat(amount || "0") || 0, false) + ); + } else { + setLocalSats(amount); + setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false)); + } } - const parsed = Number(localSats()); - if (isNaN(parsed)) { - return undefined; + // What we're all here for in the first place: returning a value + function handleSubmit(e: SubmitEvent | MouseEvent) { + e.preventDefault(); + props.setAmountSats(BigInt(localSats())); + setIsOpen(false); } - if (parsed > (inboundCapacity() || 0)) { - return "A lightning setup fee will be charged if paid over lightning."; + function handleSatsInput(e: InputEvent) { + const { value } = e.target as HTMLInputElement; + const sane = satsInputSanitizer(value); + setLocalSats(sane); + setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false)); } - return undefined; - }; - - function handleCharacterInput(character: string) { - const isFiatMode = mode() === "fiat"; - const inputSanitizer = isFiatMode ? fiatInputSanitizer : satsInputSanitizer; - const localValue = isFiatMode ? localFiat : localSats; - - let sane; - - if (character === "DEL") { - sane = inputSanitizer(localValue().slice(0, -1)); - } else { - if (localValue() === "0") { - sane = inputSanitizer(character); - } else { - sane = inputSanitizer(localValue() + character); - } + function handleFiatInput(e: InputEvent) { + const { value } = e.target as HTMLInputElement; + const sane = fiatInputSanitizer(value); + setLocalFiat(sane); + setLocalSats( + usdToSats(state.price, parseFloat(sane || "0") || 0, false) + ); } - if (isFiatMode) { - setLocalFiat(sane); - setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false)); - } else { - setLocalSats(sane); - setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false)); + function toggle() { + setMode((m) => (m === "sats" ? "fiat" : "sats")); + focus(); } - // After a button press make sure we re-focus the input - focus(); - } + onMount(() => { + focus(); + }); - function setFixedAmount(amount: string) { - if (mode() === "fiat") { - setLocalFiat(amount); - setLocalSats(usdToSats(state.price, parseFloat(amount || "0") || 0, false)); - } else { - setLocalSats(amount); - setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false)); + function focus() { + // Make sure we actually have the inputs mounted before we try to focus them + if (isOpen() && satsInputRef && fiatInputRef) { + if (mode() === "sats") { + satsInputRef.focus(); + } else { + fiatInputRef.focus(); + } + } } - } - // What we're all here for in the first place: returning a value - function handleSubmit(e: SubmitEvent | MouseEvent) { - e.preventDefault(); - props.setAmountSats(BigInt(localSats())); - setIsOpen(false); - } - - function handleSatsInput(e: InputEvent) { - const { value } = e.target as HTMLInputElement; - const sane = satsInputSanitizer(value); - setLocalSats(sane); - setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false)); - } - - function handleFiatInput(e: InputEvent) { - const { value } = e.target as HTMLInputElement; - const sane = fiatInputSanitizer(value); - setLocalFiat(sane); - setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false)); - } - - function toggle() { - setMode((m) => (m === "sats" ? "fiat" : "sats")); - focus(); - } - - onMount(() => { - focus(); - }); - - function focus() { - // Make sure we actually have the inputs mounted before we try to focus them - if (isOpen() && satsInputRef && fiatInputRef) { - if (mode() === "sats") { - satsInputRef.focus(); - } else { - fiatInputRef.focus(); - } - } - } - - return ( - -
} - > - - - Edit - {/* {props.children} */} - - - {/* */} -
- setIsOpen(false)}> - {/* TODO: figure out how to submit on enter */} -
- -
- {/*
*/} - - (satsInputRef = el)} - disabled={mode() === "fiat"} - type="text" - value={localSats()} - onInput={handleSatsInput} - inputMode="none" - /> - (fiatInputRef = el)} - disabled={mode() === "sats"} - type="text" - value={localFiat()} - onInput={handleFiatInput} - inputMode="none" - /> -
- -
-
- - -
- - {warningText()} - -
- - {(amount) => ( -
+ } + > + + + Edit + {/* {props.children} */} + + + {/* */} +
+ setIsOpen(false)} > - {amount.label} - - )} - -
-
- - {(character) => ( - - )} - -
- -
-
-
-
- - ); + {/* TODO: figure out how to submit on enter */} +
+ +
+ {/*
*/} + + (satsInputRef = el)} + disabled={mode() === "fiat"} + type="text" + value={localSats()} + onInput={handleSatsInput} + inputMode="none" + /> + (fiatInputRef = el)} + disabled={mode() === "sats"} + type="text" + value={localFiat()} + onInput={handleFiatInput} + inputMode="none" + /> +
+ +
+
+ + +
+ + + {warningText()} + + +
+ + {(amount) => ( + + )} + +
+
+ + {(character) => ( + + )} + +
+ +
+ + + + + ); }; diff --git a/src/components/App.tsx b/src/components/App.tsx index c50b883..fc9416b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,60 +1,66 @@ -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() { const [state, _actions] = useMegaStore(); return ( - - -
- logo - - Activity - -
- - - - - - -
- - }> - - - {/* View All */} - - 0}> - - View All - - - -

- Bugs? Feedback?{" "} - - - Create an issue - - -

- - - + + +
+ logo + + Activity + +
+ + + + + + +
+ + } + > + + + {/* View All */} + + 0}> + + View All + + + +

+ Bugs? Feedback?{" "} + + + Create an issue + + +

+ + + ); } diff --git a/src/components/BalanceBox.tsx b/src/components/BalanceBox.tsx index 636c21b..34a83f4 100644 --- a/src/components/BalanceBox.tsx +++ b/src/components/BalanceBox.tsx @@ -6,71 +6,90 @@ import { A, useNavigate } from "solid-start"; import shuffle from "~/assets/icons/shuffle.svg"; export function LoadingShimmer() { - return ( -
-

-
-

-

-
-

-
- ); + return ( +
+

+
+

+

+
+

+
+ ); } const STYLE = - "px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold"; + "px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold"; export default function BalanceBox(props: { loading?: boolean }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const emptyBalance = () => - (state.balance?.confirmed || 0n) === 0n && - (state.balance?.lightning || 0n) === 0n && - (state.balance?.force_close || 0n) === 0n && - (state.balance?.unconfirmed || 0n) === 0n; + const emptyBalance = () => + (state.balance?.confirmed || 0n) === 0n && + (state.balance?.lightning || 0n) === 0n && + (state.balance?.force_close || 0n) === 0n && + (state.balance?.unconfirmed || 0n) === 0n; - const navigate = useNavigate(); + 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 ( - <> - - }> - - - + return ( + <> + + }> + + + - - }> -
- - -
- - swap - -
-
-
-
-
-
- - -
- - ); + + }> +
+ + +
+ + swap + +
+
+
+
+
+
+ + +
+ + ); } diff --git a/src/components/ContactEditor.tsx b/src/components/ContactEditor.tsx index 2c5fb9e..ae3523d 100644 --- a/src/components/ContactEditor.tsx +++ b/src/components/ContactEditor.tsx @@ -1,50 +1,69 @@ -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 = (c: ContactFormValues) => { - props.createContact(c) + const handleSubmit: SubmitHandler = ( + c: ContactFormValues + ) => { + props.createContact(c); setIsOpen(false); - } + }; return ( - - setIsOpen(true)}>+ Add Contact + setIsOpen(true)}> + + Add Contact +
- setIsOpen(false)}> + setIsOpen(false)} + >
-
- +
-
+ ); } diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index b6a10b1..5f55855 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -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, initialValues?: ContactFormValues, title: string, cta: string }) { - const [_contactForm, { Form, Field }] = createForm({ initialValues: props.initialValues }); +export function ContactForm(props: { + handleSubmit: SubmitHandler; + initialValues?: ContactFormValues; + title: string; + cta: string; +}) { + const [_contactForm, { Form, Field }] = createForm({ + initialValues: props.initialValues + }); return ( -
+
{props.title} - + {(field, props) => ( - + )} {/* @@ -27,5 +46,5 @@ export function ContactForm(props: { handleSubmit: SubmitHandler - ) -} \ No newline at end of file + ); +} diff --git a/src/components/ContactViewer.tsx b/src/components/ContactViewer.tsx index c2b15e7..b897251 100644 --- a/src/components/ContactViewer.tsx +++ b/src/components/ContactViewer.tsx @@ -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 = (c: ContactFormValues) => { + const handleSubmit: SubmitHandler = ( + 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 ( -
- setIsOpen(false)}> + setIsOpen(false)} + >
-
- +
-
{props.contact.name[0]}
-

{props.contact.name}

+

+ {props.contact.name} +

- No payments yet with {props.contact.name} + + No payments yet with{" "} + + {props.contact.name} + +
- - + +
@@ -73,6 +122,6 @@ export function ContactViewer(props: { contact: Contact, gradient: string, saveC
-
+ ); } diff --git a/src/components/CopyableQR.tsx b/src/components/CopyableQR.tsx index 1538a8f..ea83da0 100644 --- a/src/components/CopyableQR.tsx +++ b/src/components/CopyableQR.tsx @@ -3,15 +3,22 @@ import { QRCodeSVG } from "solid-qr-code"; import { useCopy } from "~/utils/useCopy"; export function CopyableQR(props: { value: string }) { - const [copy, copied] = useCopy({ copiedTimeout: 1000 }); - return ( -
copy(props.value)}> - -
-

Copied

+ const [copy, copied] = useCopy({ copiedTimeout: 1000 }); + return ( +
copy(props.value)} + > + +
+

Copied

+
+
+
- - -
- ); + ); } diff --git a/src/components/DeleteEverything.tsx b/src/components/DeleteEverything.tsx index a02e989..135e504 100644 --- a/src/components/DeleteEverything.tsx +++ b/src/components/DeleteEverything.tsx @@ -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 ( <> - setConfirmOpen(false)}> + setConfirmOpen(false)} + > This will delete your node's state. This can't be undone! - ) + ); } diff --git a/src/components/DetailsModal.tsx b/src/components/DetailsModal.tsx index d57a4f7..db8fe38 100644 --- a/src/components/DetailsModal.tsx +++ b/src/components/DetailsModal.tsx @@ -1,14 +1,14 @@ -import { Dialog } from "@kobalte/core" +import { Dialog } from "@kobalte/core"; import { - For, - Match, - ParentComponent, - Show, - Suspense, - Switch, - createEffect, - createMemo, - createResource + For, + Match, + ParentComponent, + Show, + Suspense, + Switch, + createEffect, + createMemo, + createResource } from "solid-js"; import { Hr, ModalCloseButton, TinyButton, VStack } from "~/components/layout"; import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"; @@ -29,286 +29,320 @@ 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"; + "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"; function LightningHeader(props: { info: MutinyInvoice }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const tags = createMemo(() => { - if (props.info.labels.length) { - const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]); - if (contact) { - return [tagToMutinyTag(contact)]; - } else { - return []; - } - } else { - return []; - } - }); + const tags = createMemo(() => { + if (props.info.labels.length) { + const contact = state.mutiny_wallet?.get_contact( + props.info.labels[0] + ); + if (contact) { + return [tagToMutinyTag(contact)]; + } else { + return []; + } + } else { + return []; + } + }); - return ( -
-
- lightning bolt -
-

- {props.info.inbound ? "Lightning receive" : "Lightning send"} -

- - - {(tag) => ( - { - // noop - }} - > - {tag.name} - - )} - -
- ); + return ( +
+
+ lightning bolt +
+

+ {props.info.inbound ? "Lightning receive" : "Lightning send"} +

+ + + {(tag) => ( + { + // noop + }} + > + {tag.name} + + )} + +
+ ); } function OnchainHeader(props: { info: OnChainTx }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const tags = createMemo(() => { - if (props.info.labels.length) { - const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]); - if (contact) { - return [tagToMutinyTag(contact)]; - } else { - return []; - } - } else { - return []; - } - }); + const tags = createMemo(() => { + if (props.info.labels.length) { + const contact = state.mutiny_wallet?.get_contact( + props.info.labels[0] + ); + if (contact) { + return [tagToMutinyTag(contact)]; + } else { + return []; + } + } else { + return []; + } + }); - const isSend = () => { - return props.info.sent > props.info.received; - }; + const isSend = () => { + return props.info.sent > props.info.received; + }; - const amount = () => { - if (isSend()) { - return (props.info.sent - props.info.received).toString(); - } else { - return (props.info.received - props.info.sent).toString(); - } - }; + const amount = () => { + if (isSend()) { + return (props.info.sent - props.info.received).toString(); + } else { + return (props.info.received - props.info.sent).toString(); + } + }; - return ( -
-
- blockchain -
-

{isSend() ? "On-chain send" : "On-chain receive"}

- - - {(tag) => ( - { - // noop - }} - > - {tag.name} - - )} - -
- ); + return ( +
+
+ blockchain +
+

+ {isSend() ? "On-chain send" : "On-chain receive"} +

+ + + {(tag) => ( + { + // noop + }} + > + {tag.name} + + )} + +
+ ); } const KeyValue: ParentComponent<{ key: string }> = (props) => { - return ( -
  • - {props.key} - {props.children} -
  • - ); + return ( +
  • + + {props.key} + + {props.children} +
  • + ); }; function MiniStringShower(props: { text: string }) { - const [copy, _copied] = useCopy({ copiedTimeout: 1000 }); + const [copy, _copied] = useCopy({ copiedTimeout: 1000 }); - return ( -
    -
    {props.text}
    - -
    - ); + return ( +
    +
    {props.text}
    + +
    + ); } function LightningDetails(props: { info: MutinyInvoice }) { - return ( - -
      - - {props.info.paid ? "Paid" : "Unpaid"} - - - {prettyPrintTime(Number(props.info.last_updated))} - - - - {props.info.description} - - - - - - - - - - - - - - - - -
    -
    - ); + return ( + +
      + + + {props.info.paid ? "Paid" : "Unpaid"} + + + + + {prettyPrintTime(Number(props.info.last_updated))} + + + + + + {props.info.description} + + + + + + + + + + + + + + + + + +
    +
    + ); } function OnchainDetails(props: { info: OnChainTx }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const confirmationTime = () => { - return props.info.confirmation_time?.Confirmed?.time; - }; + const confirmationTime = () => { + return props.info.confirmation_time?.Confirmed?.time; + }; - const network = state.mutiny_wallet?.get_network() as Network; + const network = state.mutiny_wallet?.get_network() as Network; - return ( - - {/*
    {JSON.stringify(props.info, null, 2)}
    */} -
      - - {confirmationTime() ? "Confirmed" : "Unconfirmed"} - - - - - {confirmationTime() ? prettyPrintTime(Number(confirmationTime())) : "Pending"} - - - - 0}> - - - - - - - - - -
    - - Mempool.space - -
    - ); + return ( + + {/*
    {JSON.stringify(props.info, null, 2)}
    */} +
      + + + {confirmationTime() ? "Confirmed" : "Unconfirmed"} + + + + + + {confirmationTime() + ? prettyPrintTime(Number(confirmationTime())) + : "Pending"} + + + + 0}> + + + + + + + + + +
    + + Mempool.space + +
    + ); } export function DetailsIdModal(props: { - open: boolean; - kind?: HackActivityType; - id: string; - setOpen: (open: boolean) => void; + open: boolean; + kind?: HackActivityType; + id: string; + setOpen: (open: boolean) => void; }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const id = () => props.id; - const kind = () => props.kind; + const id = () => props.id; + const kind = () => props.kind; - // TODO: is there a cleaner way to do refetch when id changes? - const [data, { refetch }] = createResource(async () => { - if (kind() === "Lightning") { - console.log("reading invoice: ", id()); - const invoice = await state.mutiny_wallet?.get_invoice_by_hash(id()); - return invoice; - } else { - console.log("reading tx: ", id()); - const tx = await state.mutiny_wallet?.get_transaction(id()); - return tx; - } - }); + // TODO: is there a cleaner way to do refetch when id changes? + const [data, { refetch }] = createResource(async () => { + if (kind() === "Lightning") { + console.log("reading invoice: ", id()); + const invoice = await state.mutiny_wallet?.get_invoice_by_hash( + id() + ); + return invoice; + } else { + console.log("reading tx: ", id()); + const tx = await state.mutiny_wallet?.get_transaction(id()); + return tx; + } + }); - createEffect(() => { - if (props.id && props.kind && props.open) { - refetch(); - } - }); + createEffect(() => { + if (props.id && props.kind && props.open) { + refetch(); + } + }); - const json = createMemo(() => JSON.stringify(data() || "", null, 2)); + const json = createMemo(() => JSON.stringify(data() || "", null, 2)); - const isInvoice = () => { - return props.kind === "Lightning"; - }; + const isInvoice = () => { + return props.kind === "Lightning"; + }; - return ( - - - -
    - - -
    -
    - - - -
    - - - - - - - - - - -
    - - - - - - - - - -
    - + return ( + + + +
    + + +
    +
    + + + +
    + + + + + + + + + + +
    + + + + + + + + + +
    + +
    +
    + +
    - -
    -
    -
    -
    -
    - ); + + + ); } diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 73197c6..d52fff6 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -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 ( @@ -15,18 +21,27 @@ export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; o
    - Are you sure? + + Are you sure? +
    {props.children}
    - +
    -
    - ) -} \ No newline at end of file + + ); +}; diff --git a/src/components/ErrorDisplay.tsx b/src/components/ErrorDisplay.tsx index e14089d..8c76195 100644 --- a/src/components/ErrorDisplay.tsx +++ b/src/components/ErrorDisplay.tsx @@ -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 }) { Error This never should've happened

    - - {props.error.name}: {props.error.message} + {props.error.name}:{" "} + {props.error.message}

    - + ); diff --git a/src/components/ImportExport.tsx b/src/components/ImportExport.tsx index 1dbdc6c..63ab795 100644 --- a/src/components/ImportExport.tsx +++ b/src/components/ImportExport.tsx @@ -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((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() { - setConfirmOpen(false)}> + setConfirmOpen(false)} + > Do you want to replace your state with {files()[0].name}? - ) + ); } diff --git a/src/components/InfoBox.tsx b/src/components/InfoBox.tsx index 8d9f50d..cdfa4c2 100644 --- a/src/components/InfoBox.tsx +++ b/src/components/InfoBox.tsx @@ -1,23 +1,25 @@ 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) => { - return ( -
    -
    - info -
    -
    -

    {props.children}

    -
    -
    - ); -} +export const InfoBox: ParentComponent<{ + accent: "red" | "blue" | "green" | "white"; +}> = (props) => { + return ( +
    +
    + info +
    +
    +

    {props.children}

    +
    +
    + ); +}; diff --git a/src/components/JsonModal.tsx b/src/components/JsonModal.tsx index 56abcb5..6cb78fd 100644 --- a/src/components/JsonModal.tsx +++ b/src/components/JsonModal.tsx @@ -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 ( @@ -15,9 +28,7 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
    - - {props.title} - + {props.title} @@ -35,6 +46,6 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
    -
    - ) -} \ No newline at end of file + + ); +} diff --git a/src/components/KitchenSink.tsx b/src/components/KitchenSink.tsx index 7b90dfd..8369d92 100644 --- a/src/components/KitchenSink.tsx +++ b/src/components/KitchenSink.tsx @@ -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"; @@ -15,190 +30,221 @@ import { ExternalLink } from "./layout/ExternalLink"; // TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise type RefetchPeersType = ( - info?: unknown + info?: unknown ) => MutinyPeer[] | Promise | null | undefined; function PeerItem(props: { peer: MutinyPeer }) { - const [state, _] = useMegaStore(); + const [state, _] = useMegaStore(); - const handleDisconnectPeer = async () => { - const nodes = await state.mutiny_wallet?.list_nodes(); - const firstNode = (nodes[0] as string) || ""; + const handleDisconnectPeer = async () => { + const nodes = await state.mutiny_wallet?.list_nodes(); + const firstNode = (nodes[0] as string) || ""; - if (props.peer.is_connected) { - await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey); - } else { - await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey); - } - }; + if (props.peer.is_connected) { + await state.mutiny_wallet?.disconnect_peer( + firstNode, + props.peer.pubkey + ); + } else { + await state.mutiny_wallet?.delete_peer( + firstNode, + props.peer.pubkey + ); + } + }; - return ( - - -

    - {">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey} -

    -
    - - -
    -            {JSON.stringify(props.peer, null, 2)}
    -          
    - -
    -
    -
    - ); + return ( + + +

    + {">"}{" "} + {props.peer.alias ? props.peer.alias : props.peer.pubkey} +

    +
    + + +
    +                        {JSON.stringify(props.peer, null, 2)}
    +                    
    + +
    +
    +
    + ); } function PeersList() { - const [state, _] = useMegaStore(); + const [state, _] = useMegaStore(); - const getPeers = async () => { - return (await state.mutiny_wallet?.list_peers()) as Promise; - }; + const getPeers = async () => { + return (await state.mutiny_wallet?.list_peers()) as Promise< + MutinyPeer[] + >; + }; - const [peers, { refetch }] = createResource(getPeers); + const [peers, { refetch }] = createResource(getPeers); - createEffect(() => { - // refetch peers every 5 seconds - const interval = setTimeout(() => { - refetch(); - }, 5000); - onCleanup(() => { - clearInterval(interval); + createEffect(() => { + // refetch peers every 5 seconds + const interval = setTimeout(() => { + refetch(); + }, 5000); + onCleanup(() => { + clearInterval(interval); + }); }); - }); - return ( - <> - Peers - {/* By wrapping this in a suspense I don't cause the page to jump to the top */} - - - No peers}> - {(peer) => } - - - - - - - ); + return ( + <> + Peers + {/* By wrapping this in a suspense I don't cause the page to jump to the top */} + + + No peers}> + {(peer) => } + + + + + + + ); } function ConnectPeer(props: { refetchPeers: RefetchPeersType }) { - const [state, _] = useMegaStore(); + const [state, _] = useMegaStore(); - const [value, setValue] = createSignal(""); + const [value, setValue] = createSignal(""); - const onSubmit = async (e: SubmitEvent) => { - e.preventDefault(); + const onSubmit = async (e: SubmitEvent) => { + e.preventDefault(); - const peerConnectString = value().trim(); - const nodes = await state.mutiny_wallet?.list_nodes(); - const firstNode = (nodes[0] as string) || ""; + const peerConnectString = value().trim(); + 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(); + await props.refetchPeers(); - setValue(""); - }; + setValue(""); + }; - return ( - -
    - - Connect Peer - - - Expecting something like mutiny:abc123... - - - -
    -
    - ); + return ( + +
    + + + Connect Peer + + + + Expecting something like mutiny:abc123... + + + +
    +
    + ); } type RefetchChannelsListType = ( - info?: unknown + info?: unknown ) => MutinyChannel[] | Promise | null | undefined; function ChannelItem(props: { channel: MutinyChannel; network?: Network }) { - const [state, _] = useMegaStore(); + const [state, _] = useMegaStore(); - const [confirmOpen, setConfirmOpen] = createSignal(false); - const [confirmLoading, setConfirmLoading] = createSignal(false); + const [confirmOpen, setConfirmOpen] = createSignal(false); + const [confirmLoading, setConfirmLoading] = createSignal(false); - function handleCloseChannel() { - setConfirmOpen(true); - } - - async function confirmCloseChannel() { - setConfirmLoading(true); - try { - await state.mutiny_wallet?.close_channel(props.channel.outpoint as string); - } catch (e) { - console.error(e); - showToast(eify(e)); + function handleCloseChannel() { + setConfirmOpen(true); } - setConfirmLoading(false); - setConfirmOpen(false); - } - return ( - - -

    - {">"} {props.channel.peer} -

    -
    - - -
    -            {JSON.stringify(props.channel, null, 2)}
    -          
    - - View Transaction - - -
    - setConfirmOpen(false)} - loading={confirmLoading()} - > -

    Are you sure you want to close this channel?

    -
    -
    -
    - ); + async function confirmCloseChannel() { + setConfirmLoading(true); + try { + await state.mutiny_wallet?.close_channel( + props.channel.outpoint as string + ); + } catch (e) { + console.error(e); + showToast(eify(e)); + } + setConfirmLoading(false); + setConfirmOpen(false); + } + + return ( + + +

    + {">"} {props.channel.peer} +

    +
    + + +
    +                        {JSON.stringify(props.channel, null, 2)}
    +                    
    + + View Transaction + + +
    + setConfirmOpen(false)} + loading={confirmLoading()} + > +

    Are you sure you want to close this channel?

    +
    +
    +
    + ); } function ChannelsList() { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); const getChannels = async () => { - return await state.mutiny_wallet?.list_channels() as Promise + 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 ( <> - - Channels - + Channels {/* By wrapping this in a suspense I don't cause the page to jump to the top */} No channels}> {(channel) => ( )} - - + - ) + ); } function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); const [creationError, setCreationError] = createSignal(); @@ -256,63 +308,82 @@ 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)); } }; const network = state.mutiny_wallet?.get_network() as Network; return ( - <> - -
    - - Pubkey - - - - Amount - - - -
    -
    - -
    -            {JSON.stringify(newChannel()?.outpoint, null, 2)}
    -          
    -
    {newChannel()?.outpoint}
    - - View Transaction - -
    - -
    {creationError()?.message}
    -
    - + <> + +
    + + + Pubkey + + + + + + Amount + + + + +
    +
    + +
    +                    {JSON.stringify(newChannel()?.outpoint, null, 2)}
    +                
    +
    {newChannel()?.outpoint}
    + + View Transaction + +
    + +
    {creationError()?.message}
    +
    + ); } function LnUrlAuth() { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); const [value, setValue] = createSignal(""); @@ -320,37 +391,50 @@ 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(""); }; return ( -
    + - LNURL Auth - - Expecting something like LNURL... + + LNURL Auth + + + + Expecting something like LNURL... + - -
    + +
    - ) + ); } function ListTags() { - const [_state, actions] = useMegaStore() + const [_state, actions] = useMegaStore(); - const [tags] = createResource(actions.listTags) + const [tags] = createResource(actions.listTags); return ( -

    @@ -365,13 +449,9 @@ function ListTags() { - - ) + ); } - - - export default function KitchenSink() { return ( @@ -390,5 +470,5 @@ export default function KitchenSink() {
    - ) -} \ No newline at end of file + ); +} diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index b602209..f642a5f 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -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() { - - ) -} \ No newline at end of file + ); +} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index e1775d4..4035be3 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -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 ( - + ); +} diff --git a/src/components/NostrWalletConnectModal.tsx b/src/components/NostrWalletConnectModal.tsx index 866d774..b7a5858 100644 --- a/src/components/NostrWalletConnectModal.tsx +++ b/src/components/NostrWalletConnectModal.tsx @@ -1,22 +1,23 @@ -import {QRCodeSVG} from "solid-qr-code"; -import {As, Dialog} from "@kobalte/core"; -import {Button, Card} from "~/components/layout"; -import {useMegaStore} from "~/state/megaStore"; -import {createResource, Show} from "solid-js"; +import { QRCodeSVG } from "solid-qr-code"; +import { As, Dialog } from "@kobalte/core"; +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() {
    - Nostr Wallet Connect + + Nostr Wallet Connect + X @@ -53,17 +56,24 @@ export default function NostrWalletConnectModal() {
    - +
    - {connectionURI() || ""} + + {connectionURI() || ""} +
    - +
    - - ) -} \ No newline at end of file + + ); +} diff --git a/src/components/OnboardWarning.tsx b/src/components/OnboardWarning.tsx index 7f30771..1f62e18 100644 --- a/src/components/OnboardWarning.tsx +++ b/src/components/OnboardWarning.tsx @@ -7,80 +7,97 @@ import close from "~/assets/icons/close.svg"; import restore from "~/assets/icons/upload.svg"; export function OnboardWarning() { - const [state, actions] = useMegaStore(); - const [dismissedBackup, setDismissedBackup] = createSignal(false); + const [state, actions] = useMegaStore(); + const [dismissedBackup, setDismissedBackup] = createSignal(false); - function hasMoney() { - return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed || state.balance?.force_close; - } + function hasMoney() { + return ( + state.balance?.confirmed || + state.balance?.lightning || + state.balance?.unconfirmed || + state.balance?.force_close + ); + } - return ( - <> - {/* TODO: show this once we have a restore flow */} - -
    -
    - backup -
    -
    -
    - Welcome! -

    - If you've used Mutiny before you can restore from a backup. Otherwise you can skip - this and enjoy your new wallet! -

    -
    - +
    + +
    +
    + - Restore - -
    - -

    - - -
    -
    - backup -
    -
    -
    - Secure your funds -

    - You have money stored in this browser. Let's make sure you have a backup. -

    -
    -
    - - Backup - -
    -
    - -
    -
    - - ); +
    +
    + backup +
    +
    +
    + Secure your funds +

    + You have money stored in this browser. Let's + make sure you have a backup. +

    +
    +
    + + Backup + +
    +
    + +
    + + + ); } diff --git a/src/components/PeerConnectModal.tsx b/src/components/PeerConnectModal.tsx index 05d0472..cc23b77 100644 --- a/src/components/PeerConnectModal.tsx +++ b/src/components/PeerConnectModal.tsx @@ -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 ( @@ -41,7 +41,9 @@ export default function PeerConnectModal() {
    - Peer connect info + + Peer connect info + X @@ -49,16 +51,21 @@ export default function PeerConnectModal() {
    - +
    - {peerConnectString() || ""} + + {peerConnectString() || ""} +
    - - ) -} \ No newline at end of file + + ); +} diff --git a/src/components/Reader.tsx b/src/components/Reader.tsx index 03f59a7..c9b2a50 100644 --- a/src/components/Reader.tsx +++ b/src/components/Reader.tsx @@ -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 ( <>
    -
    ); diff --git a/src/components/ReceiveQrShower.tsx b/src/components/ReceiveQrShower.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/Reload.tsx b/src/components/Reload.tsx index 5185cc7..24e9ce2 100644 --- a/src/components/Reload.tsx +++ b/src/components/Reload.tsx @@ -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 = () => { */} - ) -} + ); +}; -export default ReloadPrompt \ No newline at end of file +export default ReloadPrompt; diff --git a/src/components/Restart.tsx b/src/components/Restart.tsx index 50a4a1b..b85441e 100644 --- a/src/components/Restart.tsx +++ b/src/components/Restart.tsx @@ -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 ( - Something *extra* screwy going on? Stop the nodes! + + Something *extra* screwy going on? Stop the nodes! + - - ) + ); } diff --git a/src/components/SeedWords.tsx b/src/components/SeedWords.tsx index 1df63e0..75f436e 100644 --- a/src/components/SeedWords.tsx +++ b/src/components/SeedWords.tsx @@ -1,36 +1,42 @@ -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 () -} \ No newline at end of file + +
      + + {(word) => ( +
    1. {word}
    2. + )} +
      +
    +
    + + + ); +} diff --git a/src/components/SettingsStringsEditor.tsx b/src/components/SettingsStringsEditor.tsx index aa98f2d..00e8038 100644 --- a/src/components/SettingsStringsEditor.tsx +++ b/src/components/SettingsStringsEditor.tsx @@ -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({ initialValues: existingSettings }); + const [_settingsForm, { Form, Field }] = + createForm({ + 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 -
    -

    Don't trust us! Use your own servers to back Mutiny.

    -
    - Network -
    -                    {existingSettings.network}
    -                
    -
    + return ( + + +

    + Don't trust us! Use your own servers to back Mutiny. +

    +
    + Network +
    {existingSettings.network}
    +
    - - {(field, props) => ( - - )} - - - {(field, props) => ( - - )} - - - {(field, props) => ( - - )} - - - {(field, props) => ( - - )} - - - -
    - -} \ No newline at end of file + + {(field, props) => ( + + )} + + + {(field, props) => ( + + )} + + + {(field, props) => ( + + )} + + + {(field, props) => ( + + )} + + + +
    + ); +} diff --git a/src/components/ShareCard.tsx b/src/components/ShareCard.tsx index 69e9842..7badaa9 100644 --- a/src/components/ShareCard.tsx +++ b/src/components/ShareCard.tsx @@ -1,69 +1,83 @@ 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 ( - - ) + + ); } function TruncateMiddle(props: { text: string }) { - return ( -
    - {props.text} - {props.text.length > 8 ? props.text.slice(-8) : ""} -
    - ); + return ( +
    + {props.text} + + {props.text.length > 8 ? props.text.slice(-8) : ""} + +
    + ); } export function StringShower(props: { text: string }) { - const [open, setOpen] = createSignal(false); - return ( - <> - -
    - - -
    - - ); + const [open, setOpen] = createSignal(false); + return ( + <> + +
    + + +
    + + ); } -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 ( - - ) + + ); } export function ShareCard(props: { text?: string }) { @@ -78,7 +92,6 @@ export function ShareCard(props: { text?: string }) {
    - - ) - -} \ No newline at end of file + + ); +} diff --git a/src/components/SimpleSelect.tsx b/src/components/SimpleSelect.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/TagEditor.tsx b/src/components/TagEditor.tsx index 5188552..78c860f 100644 --- a/src/components/TagEditor.tsx +++ b/src/components/TagEditor.tsx @@ -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 => { }; export function TagEditor(props: { - selectedValues: Partial[], - setSelectedValues: (value: Partial[]) => void, - placeholder: string + selectedValues: Partial[]; + setSelectedValues: (value: Partial[]) => void; + placeholder: string; }) { const [_state, actions] = useMegaStore(); const [availableTags, setAvailableTags] = createSignal([]); 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]); } }; @@ -50,7 +57,7 @@ export function TagEditor(props: { }; return ( -
    +