diff --git a/.github/workflows/ios-prod.yml b/.github/workflows/ios-prod.yml index 65da35a9..d575a206 100644 --- a/.github/workflows/ios-prod.yml +++ b/.github/workflows/ios-prod.yml @@ -2,8 +2,6 @@ name: Release iOS Prod on: push: - tags: - - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 branches: - master diff --git a/android/app/build.gradle b/android/app/build.gradle index 51bd603c..bea3c213 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.mutinywallet.mutinywallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 37 - versionName "0.4.36" + versionCode 39 + versionName "0.4.38" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 97544329..a963132f 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.4.36; + MARKETING_VERSION = 1.4.38; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.mutinywallet.mutiny; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -387,7 +387,7 @@ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.4.36; + MARKETING_VERSION = 1.4.38; PRODUCT_BUNDLE_IDENTIFIER = com.mutinywallet.mutiny; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/package.json b/package.json index c7c93546..becb30cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mutiny-wallet", - "version": "0.4.36", + "version": "0.4.38", "license": "MIT", "packageManager": "pnpm@8.6.6", "scripts": { @@ -55,7 +55,7 @@ "@kobalte/core": "^0.9.8", "@kobalte/tailwindcss": "^0.5.0", "@modular-forms/solid": "^0.18.1", - "@mutinywallet/mutiny-wasm": "0.4.36", + "@mutinywallet/mutiny-wasm": "0.4.38", "@mutinywallet/waila-wasm": "^0.2.4", "@solid-primitives/upload": "^0.0.111", "@solid-primitives/websocket": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e2ae483..4047ceae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,8 +54,8 @@ importers: specifier: ^0.18.1 version: 0.18.1(solid-js@1.8.5) '@mutinywallet/mutiny-wasm': - specifier: 0.4.36 - version: 0.4.36 + specifier: 0.4.38 + version: 0.4.38 '@mutinywallet/waila-wasm': specifier: ^0.2.4 version: 0.2.4 @@ -2584,8 +2584,8 @@ packages: solid-js: 1.8.5 dev: false - /@mutinywallet/mutiny-wasm@0.4.36: - resolution: {integrity: sha512-5JhGrOgzQKWzac/RRRHQvsjctssermvfoGhHJSTgMNTVzsszSPgY7yhmzXOKkuEo2XrmscVCO4Y5e7xdTaDlQQ==} + /@mutinywallet/mutiny-wasm@0.4.38: + resolution: {integrity: sha512-OaYwKLYwKYT0QrjNhn1muKWTIH0TVTjiBeWTLuwepabSJbAZ+hCxuPXxDiXzYJArLm9Xvvmq3RVjDktdt0jOwA==} dev: false /@mutinywallet/waila-wasm@0.2.4: diff --git a/src/i18n/en/translations.ts b/src/i18n/en/translations.ts index f3c7b4fd..e0735d8a 100644 --- a/src/i18n/en/translations.ts +++ b/src/i18n/en/translations.ts @@ -121,6 +121,9 @@ export default { "We do not have enough balance to pay the given amount.", error_invoice_match: "Amount requested, {{amount}} SATS, does not equal amount set.", + error_channel_reserves: "Not enough available funds.", + error_channel_reserves_explained: + "A portion of your channel balance is reserved for fees. Try sending a smaller amount or adding funds.", error_clipboard: "Clipboard not supported", error_keysend: "Keysend failed", error_LNURL: "LNURL Pay failed", @@ -310,9 +313,14 @@ export default { inbound_outbound_tip: "Outbound is the amount of money you can spend on lightning. Inbound is the amount you can receive without incurring a lightning service fee.", reserve_tip: - "About 1% of your channel balance is reserved on lightning for fees.", + "About 1% of your channel balance is reserved on lightning for fees. Additional reserves are required for channels you opened via swap.", no_channels: - "It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!" + "It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!", + close_channel: "Close", + online_channels: "Online Channels", + offline_channels: "Offline Channels", + close_channel_confirm: + "Closing this channel will move the balance on-chain and incur an on-chain fee." }, connections: { title: "Wallet Connections", diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index ae03baa5..3fa04ae9 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -1,7 +1,7 @@ import { Clipboard } from "@capacitor/clipboard"; import { Capacitor } from "@capacitor/core"; import { Contact, MutinyInvoice, TagItem } from "@mutinywallet/mutiny-wasm"; -import { useNavigate } from "@solidjs/router"; +import { A, useNavigate } from "@solidjs/router"; import { createEffect, createMemo, @@ -227,6 +227,18 @@ function Failure(props: { reason: string }) { {i18n.t("send.payment_pending_description")} + + +

+ {i18n.t("send.error_channel_reserves")} +

+ + {i18n.t("send.error_channel_reserves_explained")}{" "} + {i18n.t("common.why")} + +

diff --git a/src/routes/settings/Channels.tsx b/src/routes/settings/Channels.tsx index 18b664c5..e49a5e7b 100644 --- a/src/routes/settings/Channels.tsx +++ b/src/routes/settings/Channels.tsx @@ -1,37 +1,62 @@ -import { createResource, Match, Switch } from "solid-js"; +import { MutinyChannel } from "@mutinywallet/mutiny-wasm"; +import { + createEffect, + createMemo, + createResource, + createSignal, + For, + Match, + Show, + Suspense, + Switch +} from "solid-js"; import { AmountSmall, BackLink, Card, + Collapser, + ConfirmDialog, DefaultMain, + ExternalLink, LargeHeader, MutinyWalletGuard, NavBar, NiceP, SafeArea, + SettingsCard, + showToast, SmallHeader, TinyText, VStack } from "~/components"; import { useI18n } from "~/i18n/context"; +import { Network } from "~/logic/mutinyWalletSetup"; import { useMegaStore } from "~/state/megaStore"; +import { createDeepSignal, eify, mempoolTxUrl } from "~/utils"; export function BalanceBar(props: { inbound: number; reserve: number; outbound: number; + hideHeader?: boolean; }) { const i18n = useI18n(); return ( -
- - {i18n.t("settings.channels.outbound")} - - {i18n.t("settings.channels.reserve")} - {i18n.t("settings.channels.inbound")} -
+ +
+ + {i18n.t("settings.channels.outbound")} + + + {i18n.t("settings.channels.reserve")} + + + {i18n.t("settings.channels.inbound")} + +
+
splitChannelNumbers(props.channel)); + + return ( + + + +
+ + {i18n.t("common.view_transaction")} + + +
+ setConfirmOpen(false)} + > + {i18n.t("settings.channels.close_channel_confirm")} + +
+
+ ); +} + export function LiquidityMonitor() { const i18n = useI18n(); const [state, _actions] = useMegaStore(); - const [channelInfo] = createResource(async () => { + async function listChannels() { try { - const channels = await state.mutiny_wallet?.list_channels(); + const channels: MutinyChannel[] | undefined = + await state.mutiny_wallet?.list_channels(); + + if (!channels) + return { + inbound: 0, + reserve: 0, + outbound: 0, + channelCount: 0 + }; + let outbound = 0n; let inbound = 0n; let reserve = 0n; for (const channel of channels) { - inbound = - inbound + - BigInt(channel.size) - - BigInt(channel.balance + channel.reserve); + inbound = inbound + BigInt(channel.inbound); reserve = reserve + BigInt(channel.reserve); outbound = outbound + BigInt(channel.balance); } @@ -86,37 +200,94 @@ export function LiquidityMonitor() { inbound, reserve, outbound, - channelCount: channels?.length + channelCount: channels?.length, + online: channels?.filter((c) => c.is_usable), + offline: channels?.filter((c) => !c.is_usable) }; } catch (e) { console.error(e); return { inbound: 0, reserve: 0, outbound: 0, channelCount: 0 }; } + } + + const [channelInfo, { refetch }] = createResource(listChannels, { + storage: createDeepSignal + }); + + createEffect(() => { + // Refetch on the sync interval + if (!state.is_syncing) { + refetch(); + } }); return ( - - - {i18n.t("settings.channels.have_channels")}{" "} - {channelInfo()?.channelCount}{" "} - {channelInfo()?.channelCount === 1 - ? i18n.t("settings.channels.have_channels_one") - : i18n.t("settings.channels.have_channels_many")} - {" "} - - - {i18n.t("settings.channels.inbound_outbound_tip")} - - - {i18n.t("settings.channels.reserve_tip")} - - + + + + {i18n.t("settings.channels.have_channels")}{" "} + {channelInfo()?.channelCount}{" "} + {channelInfo()?.channelCount === 1 + ? i18n.t("settings.channels.have_channels_one") + : i18n.t( + "settings.channels.have_channels_many" + )} + {" "} + + + {i18n.t("settings.channels.inbound_outbound_tip")} + + + {i18n.t("settings.channels.reserve_tip")} + + + + + + + + {(channel) => ( + + )} + + + + + + + + + + + {(channel) => ( + + )} + + + + + + {i18n.t("settings.channels.no_channels")} @@ -138,7 +309,9 @@ export function Channels() { {i18n.t("settings.channels.title")} - + + +