web worker

check if already initialized

more progress, zap feed not loading?

request send receive

fix setup

profile editing and show zaps

wallet connections

kitchen sink

mutiny plus and misc

get rid of swap

backup / restore, nostr stuff

get rid of gifts

channels stuff

manage federations and profile fixes

cleanup

fix build

fix chrome android

update to cap 6

bump to actual 6.0.0

update xcode version

fix interpolation again (regression)

move all static methods to the worker

add doc strings

get rid of window.nostr, make parse params async

fight load flicker

use a "-test" bundle for debug builds so they don't clobber

add back swaps and do some cleanup

fix activity flicker
This commit is contained in:
Paul Miller
2024-04-18 10:24:13 -05:00
committed by Tony Giorgio
parent f34b4b8e02
commit e01b8465d5
84 changed files with 3210 additions and 2379 deletions

View File

@@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
android {
namespace "com.mutinywallet.mutinywallet"
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.mutinywallet.mutinywallet"
minSdkVersion rootProject.ext.minSdkVersion

View File

@@ -15,6 +15,7 @@ dependencies {
implementation project(':capacitor-clipboard')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-haptics')
implementation project(':capacitor-network')
implementation project(':capacitor-share')
implementation project(':capacitor-status-bar')
implementation project(':capacitor-toast')

View File

@@ -7,8 +7,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.2'
classpath 'com.google.gms:google-services:4.3.15'
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@@ -1,33 +1,36 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/android/capacitor')
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/android/capacitor')
include ':capacitor-mlkit-barcode-scanning'
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.4.0_@capacitor+core@5.7.4/node_modules/@capacitor-mlkit/barcode-scanning/android')
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor-mlkit/barcode-scanning/android')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app/android')
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app/android')
include ':capacitor-app-launcher'
project(':capacitor-app-launcher').projectDir = new File('../node_modules/.pnpm/@capacitor+app-launcher@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app-launcher/android')
project(':capacitor-app-launcher').projectDir = new File('../node_modules/.pnpm/@capacitor+app-launcher@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app-launcher/android')
include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/clipboard/android')
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/clipboard/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@5.2.1_@capacitor+core@5.7.4/node_modules/@capacitor/filesystem/android')
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/filesystem/android')
include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/haptics/android')
project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/haptics/android')
include ':capacitor-network'
project(':capacitor-network').projectDir = new File('../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/share/android')
project(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share/android')
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@capacitor+status-bar@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/status-bar/android')
project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar/android')
include ':capacitor-toast'
project(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/toast/android')
project(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/toast/android')
include ':capacitor-secure-storage-plugin'
project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.7.4/node_modules/capacitor-secure-storage-plugin/android')
project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@6.0.0/node_modules/capacitor-secure-storage-plugin/android')

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,14 +1,14 @@
ext {
minSdkVersion = 22
compileSdkVersion = 33
targetSdkVersion = 33
androidxActivityVersion = '1.7.0'
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.10.0'
androidxFragmentVersion = '1.5.6'
coreSplashScreenVersion = '1.0.0'
androidxWebkitVersion = '1.6.1'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'

View File

@@ -61,7 +61,7 @@ test("test local encrypt", async ({ page }) => {
// The "Encrypt" button should not be disabled
const encryptButton = await page.locator("button", { hasText: "Encrypt" });
await expect(encryptButton).not.toBeDisabled();
await expect(encryptButton).toBeEnabled();
// wait 5 seconds for no reason (SADLY THIS IS IMPORTANT FOR THE TEST TO PASS)
await page.waitForTimeout(5000);
@@ -69,11 +69,8 @@ test("test local encrypt", async ({ page }) => {
// Click the "Encrypt" button
await encryptButton.click();
// wait for a while just to see what happens
// await page.waitForTimeout(10000);
// Wait for a modal with the text "Enter your password"
await page.waitForSelector("text=Enter your password");
await page.getByText("Enter your password").waitFor();
// Find the input field with the name "password"
const passwordInput2 = await page.locator(`input[name='password']`);
@@ -85,5 +82,5 @@ test("test local encrypt", async ({ page }) => {
await page.click("text=Decrypt Wallet");
// Wait for an element matching the selector to appear in DOM.
await page.locator(`text=0 sats`).first();
await page.locator(`text=0 sats`).first().waitFor();
});

View File

@@ -124,8 +124,8 @@ test("rountrip receive and send", async ({ page }) => {
await page.click("text=Online Channels");
// Give it just a second to settle down
await page.waitForTimeout(2000);
// Idk why the node isn't ready to close channels right away
await page.waitForTimeout(5000);
await page.click("text=Close");

View File

@@ -9,7 +9,6 @@ const routes = [
"/scanner",
"/search",
"/send",
"/swap",
"/settings"
];
@@ -156,13 +155,6 @@ test("visit each route", async ({ page }) => {
"Add Connection"
);
// Swap
await page.goto("http://localhost:3420/swap");
await expect(
page.getByRole("heading", { name: "Swap to Lightning" })
).toBeVisible();
checklist.set("/swap", true);
// print how many routes we've visited
checklist.forEach((value, key) => {
console.log(`${key}: ${value}`);

View File

@@ -362,7 +362,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.6.8;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.mutinywallet.mutiny;
PRODUCT_BUNDLE_IDENTIFIER = "com.mutinywallet.mutiny-test";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = ""
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,4 +1,4 @@
require_relative '../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios/scripts/pods_helpers'
require_relative '../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
use_frameworks!
@@ -9,18 +9,19 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios'
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.4.0_@capacitor+core@5.7.4/node_modules/@capacitor-mlkit/barcode-scanning'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app'
pod 'CapacitorAppLauncher', :path => '../../node_modules/.pnpm/@capacitor+app-launcher@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app-launcher'
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/clipboard'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@5.2.1_@capacitor+core@5.7.4/node_modules/@capacitor/filesystem'
pod 'CapacitorHaptics', :path => '../../node_modules/.pnpm/@capacitor+haptics@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/haptics'
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/share'
pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/status-bar'
pod 'CapacitorToast', :path => '../../node_modules/.pnpm/@capacitor+toast@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/toast'
pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.7.4/node_modules/capacitor-secure-storage-plugin'
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios'
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor-mlkit/barcode-scanning'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app'
pod 'CapacitorAppLauncher', :path => '../../node_modules/.pnpm/@capacitor+app-launcher@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app-launcher'
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/clipboard'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/filesystem'
pod 'CapacitorHaptics', :path => '../../node_modules/.pnpm/@capacitor+haptics@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/haptics'
pod 'CapacitorNetwork', :path => '../../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network'
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share'
pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar'
pod 'CapacitorToast', :path => '../../node_modules/.pnpm/@capacitor+toast@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/toast'
pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@6.0.0/node_modules/capacitor-secure-storage-plugin'
end
target 'App' do

View File

@@ -1,28 +1,30 @@
PODS:
- Capacitor (5.7.4):
- Capacitor (6.0.0):
- CapacitorCordova
- CapacitorApp (5.0.7):
- CapacitorApp (6.0.0):
- Capacitor
- CapacitorAppLauncher (5.0.7):
- CapacitorAppLauncher (6.0.0):
- Capacitor
- CapacitorClipboard (5.0.7):
- CapacitorClipboard (6.0.0):
- Capacitor
- CapacitorCordova (5.7.4)
- CapacitorFilesystem (5.2.1):
- CapacitorCordova (6.0.0)
- CapacitorFilesystem (6.0.0):
- Capacitor
- CapacitorHaptics (5.0.7):
- CapacitorHaptics (6.0.0):
- Capacitor
- CapacitorMlkitBarcodeScanning (5.4.0):
- CapacitorMlkitBarcodeScanning (6.0.0):
- Capacitor
- GoogleMLKit/BarcodeScanning (= 4.0.0)
- CapacitorNetwork (6.0.0):
- Capacitor
- CapacitorSecureStoragePlugin (0.9.0):
- Capacitor
- SwiftKeychainWrapper
- CapacitorShare (5.0.7):
- CapacitorShare (6.0.0):
- Capacitor
- CapacitorStatusBar (5.0.7):
- CapacitorStatusBar (6.0.0):
- Capacitor
- CapacitorToast (5.0.7):
- CapacitorToast (6.0.0):
- Capacitor
- GoogleDataTransport (9.2.5):
- GoogleUtilities/Environment (~> 7.7)
@@ -81,18 +83,19 @@ PODS:
- SwiftKeychainWrapper (4.0.1)
DEPENDENCIES:
- "Capacitor (from `../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/.pnpm/@capacitor+app@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app`)"
- "CapacitorAppLauncher (from `../../node_modules/.pnpm/@capacitor+app-launcher@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app-launcher`)"
- "CapacitorClipboard (from `../../node_modules/.pnpm/@capacitor+clipboard@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/clipboard`)"
- "CapacitorCordova (from `../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/.pnpm/@capacitor+filesystem@5.2.1_@capacitor+core@5.7.4/node_modules/@capacitor/filesystem`)"
- "CapacitorHaptics (from `../../node_modules/.pnpm/@capacitor+haptics@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/haptics`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.4.0_@capacitor+core@5.7.4/node_modules/@capacitor-mlkit/barcode-scanning`)"
- "CapacitorSecureStoragePlugin (from `../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.7.4/node_modules/capacitor-secure-storage-plugin`)"
- "CapacitorShare (from `../../node_modules/.pnpm/@capacitor+share@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/share`)"
- "CapacitorStatusBar (from `../../node_modules/.pnpm/@capacitor+status-bar@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/status-bar`)"
- "CapacitorToast (from `../../node_modules/.pnpm/@capacitor+toast@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/toast`)"
- "Capacitor (from `../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/.pnpm/@capacitor+app@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app`)"
- "CapacitorAppLauncher (from `../../node_modules/.pnpm/@capacitor+app-launcher@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app-launcher`)"
- "CapacitorClipboard (from `../../node_modules/.pnpm/@capacitor+clipboard@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/clipboard`)"
- "CapacitorCordova (from `../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/.pnpm/@capacitor+filesystem@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/filesystem`)"
- "CapacitorHaptics (from `../../node_modules/.pnpm/@capacitor+haptics@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/haptics`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor-mlkit/barcode-scanning`)"
- "CapacitorNetwork (from `../../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network`)"
- "CapacitorSecureStoragePlugin (from `../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@6.0.0/node_modules/capacitor-secure-storage-plugin`)"
- "CapacitorShare (from `../../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share`)"
- "CapacitorStatusBar (from `../../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar`)"
- "CapacitorToast (from `../../node_modules/.pnpm/@capacitor+toast@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/toast`)"
SPEC REPOS:
trunk:
@@ -112,43 +115,46 @@ SPEC REPOS:
EXTERNAL SOURCES:
Capacitor:
:path: "../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios"
:path: "../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios"
CapacitorApp:
:path: "../../node_modules/.pnpm/@capacitor+app@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app"
:path: "../../node_modules/.pnpm/@capacitor+app@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app"
CapacitorAppLauncher:
:path: "../../node_modules/.pnpm/@capacitor+app-launcher@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/app-launcher"
:path: "../../node_modules/.pnpm/@capacitor+app-launcher@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app-launcher"
CapacitorClipboard:
:path: "../../node_modules/.pnpm/@capacitor+clipboard@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/clipboard"
:path: "../../node_modules/.pnpm/@capacitor+clipboard@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/clipboard"
CapacitorCordova:
:path: "../../node_modules/.pnpm/@capacitor+ios@5.7.4_@capacitor+core@5.7.4/node_modules/@capacitor/ios"
:path: "../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios"
CapacitorFilesystem:
:path: "../../node_modules/.pnpm/@capacitor+filesystem@5.2.1_@capacitor+core@5.7.4/node_modules/@capacitor/filesystem"
:path: "../../node_modules/.pnpm/@capacitor+filesystem@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/filesystem"
CapacitorHaptics:
:path: "../../node_modules/.pnpm/@capacitor+haptics@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/haptics"
:path: "../../node_modules/.pnpm/@capacitor+haptics@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/haptics"
CapacitorMlkitBarcodeScanning:
:path: "../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.4.0_@capacitor+core@5.7.4/node_modules/@capacitor-mlkit/barcode-scanning"
:path: "../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor-mlkit/barcode-scanning"
CapacitorNetwork:
:path: "../../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network"
CapacitorSecureStoragePlugin:
:path: "../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.7.4/node_modules/capacitor-secure-storage-plugin"
:path: "../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@6.0.0/node_modules/capacitor-secure-storage-plugin"
CapacitorShare:
:path: "../../node_modules/.pnpm/@capacitor+share@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/share"
:path: "../../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share"
CapacitorStatusBar:
:path: "../../node_modules/.pnpm/@capacitor+status-bar@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/status-bar"
:path: "../../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar"
CapacitorToast:
:path: "../../node_modules/.pnpm/@capacitor+toast@5.0.7_@capacitor+core@5.7.4/node_modules/@capacitor/toast"
:path: "../../node_modules/.pnpm/@capacitor+toast@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/toast"
SPEC CHECKSUMS:
Capacitor: 4fe9adf012caceb4c71ffea2f1f4d005cdcbeea7
CapacitorApp: 17fecd0e6cb23feafac7eb0939417389038b0979
CapacitorAppLauncher: 7b2705481a74cbe322441bd374f56194de59ab1a
CapacitorClipboard: 45e5e25f2271f98712985d422776cdc5a779cca1
CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7
CapacitorFilesystem: 9f3e3c7fea2fff12f46dd5b07a2914f2103e4cfc
CapacitorHaptics: 7c7c206f0c96a628fed073830c96d28c4b2e772e
CapacitorMlkitBarcodeScanning: 8fb81cbef3c6ffe0c0e2dbd15ed6dca889a5a062
Capacitor: 559d073c4ca6c27f8e7002c807eea94c3ba435a9
CapacitorApp: 9d53aec7101f7b030a950c5bdc4df8612576b279
CapacitorAppLauncher: 24ce4be152a84883378d22114ffe6c492056ced2
CapacitorClipboard: 80282f684154124b9019ebf401235b70b0cf4994
CapacitorCordova: 8c4bfdf69368512e85b1d8b724dd7546abeb30af
CapacitorFilesystem: 60e59ba274c234a979e7a3be2552feaadcee4263
CapacitorHaptics: 9ebc9363f0e9b8eb4295088a0b474530acf1859b
CapacitorMlkitBarcodeScanning: 1cc47dc2163628b44be5400864de32696362045b
CapacitorNetwork: f15a94c16a33cba7c47a17814cb6bcfe3ea34ded
CapacitorSecureStoragePlugin: e91d7df060f2495a1acff9583641a6953e3aacba
CapacitorShare: c6a1ebbf0114ff9e863b966cd6052678fa25d480
CapacitorStatusBar: f390fbb49b82ffb754ea4b3cf71dc8b048baf3e7
CapacitorToast: c8bb89eeb59a23c1fc298f138cc06c8ff4d90ac1
CapacitorShare: a771200d3b924a5d7ad9d9fecbac517e4c0aa74f
CapacitorStatusBar: 2e4369f99166125435641b1908d05f561eaba6f6
CapacitorToast: ba573a7bc5dfd622e78d5be126a84ee221da4180
GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2
GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e
GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34
@@ -163,6 +169,6 @@ SPEC CHECKSUMS:
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
PODFILE CHECKSUM: 3864776b838da083701f08e4d80ef59ac02798db
PODFILE CHECKSUM: 7d4dccfabb68790e47370312014de5fc49a89668
COCOAPODS: 1.14.3

View File

@@ -15,43 +15,44 @@
},
"type": "module",
"devDependencies": {
"@capacitor/assets": "^2.0.4",
"@capacitor/cli": "^5.5.1",
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
"@playwright/test": "^1.39.0",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "6.0.0",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@playwright/test": "^1.42.1",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-solid": "0.13.1",
"postcss": "^8.4.35",
"prettier": "^3.0.3",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.12",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vite-plugin-pwa": "^0.16.7",
"vite-plugin-solid": "^2.10.1",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-pwa": "^0.19.8",
"vite-plugin-solid": "^2.10.2",
"vite-plugin-wasm": "^3.3.0",
"workbox-window": "^7.0.0"
"workbox-window": "^7.0.0",
"vite-plugin-comlink": "^4.0.3"
},
"dependencies": {
"i18next-http-backend": "^2.5.0",
"@capacitor-mlkit/barcode-scanning": "^5.3.0",
"@capacitor/android": "^5.5.1",
"@capacitor/app": "^5.0.6",
"@capacitor/app-launcher": "^5.0.6",
"@capacitor/clipboard": "^5.0.6",
"@capacitor/core": "^5.5.1",
"@capacitor/filesystem": "^5.1.4",
"@capacitor/haptics": "^5.0.6",
"@capacitor/ios": "^5.5.1",
"@capacitor/share": "^5.0.6",
"@capacitor/status-bar": "^5.0.6",
"@capacitor/toast": "^5.0.6",
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
"@capacitor/android": "^6.0.0",
"@capacitor/app": "^6.0.0",
"@capacitor/app-launcher": "^6.0.0",
"@capacitor/clipboard": "^6.0.0",
"@capacitor/core": "^6.0.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/haptics": "^6.0.0",
"@capacitor/ios": "^6.0.0",
"@capacitor/network": "^6.0.0",
"@capacitor/share": "^6.0.0",
"@capacitor/status-bar": "^6.0.0",
"@capacitor/toast": "^6.0.0",
"@kobalte/core": "^0.12.6",
"@kobalte/tailwindcss": "^0.9.0",
"@mutinywallet/mutiny-wasm": "0.6.7",
@@ -61,12 +62,14 @@
"@solidjs/router": "^0.13.1",
"capacitor-secure-storage-plugin": "^0.9.0",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-http-backend": "^2.5.0",
"lucide-solid": "^0.363.0",
"qr-scanner": "^1.4.2",
"solid-js": "^1.8.16",
"solid-qr-code": "^0.0.8",
"solid-transition-group": "^0.2.3"
"solid-transition-group": "^0.2.3",
"comlink": "^4.4.1"
},
"engines": {
"node": ">=18"

813
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -400,7 +400,8 @@
"import_state": "Import State From File",
"confirm_replace": "Do you want to replace your state with",
"password": "Enter your password to decrypt",
"decrypt_wallet": "Decrypt Wallet"
"decrypt_wallet": "Decrypt Wallet",
"decrypt_export": "Enter your password to save"
},
"logs": {
"title": "Download debug logs",

View File

@@ -18,12 +18,13 @@ import {
Button,
ButtonCard,
ContactButton,
LoadingShimmer,
NiceP,
SimpleDialog
} from "~/components";
import { useI18n } from "~/i18n/context";
import { PrivacyLevel } from "~/routes";
import { useMegaStore } from "~/state/megaStore";
import { useMegaStore, WalletWorker } from "~/state/megaStore";
import {
actuallyFetchNostrProfile,
createDeepSignal,
@@ -56,9 +57,11 @@ export interface IActivityItem {
}
async function fetchContactForNpub(
sw: WalletWorker,
npub: string
): Promise<PseudoContact | undefined> {
const hexpub = await hexpubFromNpub(npub);
const hexpub = await hexpubFromNpub(sw, npub);
console.log("fetchContactForNpub", hexpub);
if (!hexpub) {
return undefined;
}
@@ -75,6 +78,7 @@ export function UnifiedActivityItem(props: {
onClick: (id: string, kind: HackActivityType) => void;
onNewContactClick: (profile: PseudoContact) => void;
}) {
const [_state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const click = () => {
@@ -97,10 +101,12 @@ export function UnifiedActivityItem(props: {
});
const getContact = cache(async (npub) => {
return await fetchContactForNpub(npub);
return await fetchContactForNpub(sw, npub);
}, "profile");
const profileFromNostr = createAsync(async () => {
// Complaining about a "tracked scope" but I think we're good
// eslint-disable-next-line solid/reactivity
const getProfileFromNostr = cache(async () => {
if (props.item.contacts.length === 0) {
if (props.item.labels) {
const npub = props.item.labels.find((l) =>
@@ -137,6 +143,10 @@ export function UnifiedActivityItem(props: {
}
}
return undefined;
}, "profileFromNostr");
const profileFromNostr = createAsync(async () => {
return await getProfileFromNostr();
});
// TODO: figure out what other shit we should filter out
@@ -300,12 +310,11 @@ function NewContactModal(props: { profile: PseudoContact; close: () => void }) {
const i18n = useI18n();
const navigate = useNavigate();
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
async function createContact() {
try {
const existingContact =
await state.mutiny_wallet?.get_contact_for_npub(
const existingContact = await sw.get_contact_for_npub(
props.profile.hexpub
);
@@ -314,7 +323,7 @@ function NewContactModal(props: { profile: PseudoContact; close: () => void }) {
return;
}
const contactId = await state.mutiny_wallet?.create_new_contact(
const contactId = await sw.create_new_contact(
props.profile.name,
props.profile.hexpub,
props.profile.ln_address,
@@ -326,7 +335,7 @@ function NewContactModal(props: { profile: PseudoContact; close: () => void }) {
throw new Error("no contact id returned");
}
const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
const tagItem = await sw.get_tag_item(contactId);
if (!tagItem) {
throw new Error("no contact returned");
@@ -365,7 +374,7 @@ function NewContactModal(props: { profile: PseudoContact; close: () => void }) {
}
export function CombinedActivity() {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const [detailsOpen, setDetailsOpen] = createSignal(false);
@@ -386,25 +395,16 @@ export function CombinedActivity() {
setDetailsOpen(true);
}
async function getActivity() {
async function fetchActivity() {
try {
console.log("refetching activity");
const activity = await state.mutiny_wallet?.get_activity(
50,
undefined
);
if (!activity) return [];
return activity as IActivityItem[];
return await sw.get_activity(50, undefined);
} catch (e) {
console.error(e);
return [] as IActivityItem[];
}
}
const [activity, { refetch }] = createResource(getActivity, {
initialValue: [],
const [activity, { refetch }] = createResource(fetchActivity, {
storage: createDeepSignal
});
@@ -433,11 +433,14 @@ export function CombinedActivity() {
close={() => setNewContact(undefined)}
/>
</Show>
<Suspense fallback={<LoadingShimmer />}>
<Switch>
<Match when={activity.latest.length === 0}>
<Match when={activity.latest?.length === 0}>
<Show when={state.federations?.length === 0}>
<ButtonCard
onClick={() => navigate("/settings/federations")}
onClick={() =>
navigate("/settings/federations")
}
>
<div class="flex items-center gap-2">
<Users class="inline-block text-m-red" />
@@ -457,28 +460,10 @@ export function CombinedActivity() {
<NiceP>{i18n.t("home.find")}</NiceP>
</div>
</ButtonCard>
<Show when={!state.has_backed_up}>
<ButtonCard
onClick={() => navigate("/settings/backup")}
>
<div class="flex items-center gap-2">
<Save class="inline-block text-m-red" />
<NiceP>{i18n.t("home.backup")}</NiceP>
</div>
</ButtonCard>
</Show>
</Match>
<Match when={activity.latest.length >= 0}>
<Show when={!state.has_backed_up}>
<ButtonCard
onClick={() => navigate("/settings/backup")}
<Match
when={activity.latest && activity.latest!.length >= 0}
>
<div class="flex items-center gap-2">
<Save class="inline-block text-m-red" />
<NiceP>{i18n.t("home.backup")}</NiceP>
</div>
</ButtonCard>
</Show>
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
<For each={activity.latest}>
{(activityItem) => (
@@ -492,6 +477,18 @@ export function CombinedActivity() {
</div>
</Match>
</Switch>
<Show when={!state.has_backed_up}>
<ButtonCard
red
onClick={() => navigate("/settings/backup")}
>
<div class="flex items-center gap-2">
<Save class="inline-block text-neutral-200" />
<NiceP>{i18n.t("home.backup")}</NiceP>
</div>
</ButtonCard>
</Show>
</Suspense>
</>
);
}

View File

@@ -1,13 +1,13 @@
import { Dialog } from "@kobalte/core";
import {
MutinyChannel,
ActivityItem,
MutinyInvoice,
TagItem
} from "@mutinywallet/mutiny-wasm";
import { createAsync } from "@solidjs/router";
import { Copy, Link, Shuffle, Zap } from "lucide-solid";
import {
createEffect,
createMemo,
createResource,
Match,
ParentComponent,
@@ -30,7 +30,6 @@ import {
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { BalanceBar } from "~/routes/settings/Channels";
import { useMegaStore } from "~/state/megaStore";
import { mempoolTxUrl, prettyPrintTime, useCopy } from "~/utils";
@@ -301,24 +300,21 @@ function OnchainDetails(props: {
tags?: TagItem;
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
const confirmationTime = () => {
return props.info.confirmation_time?.Confirmed?.time;
};
const network = state.mutiny_wallet?.get_network() as Network;
const network = state.network || "signet";
// Can return nothing if the channel is already closed
const [channelInfo] = createResource(async () => {
if (props.kind === "ChannelOpen") {
try {
const channels =
await (state.mutiny_wallet?.list_channels() as Promise<
MutinyChannel[]
>);
const channel = channels.find((channel) =>
const channels = await sw.list_channels();
const channel = channels?.find((channel) =>
channel.outpoint?.startsWith(props.info.txid)
);
return channel;
@@ -500,7 +496,7 @@ export function ActivityDetailsModal(props: {
id: string;
setOpen: (open: boolean) => void;
}) {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const id = () => props.id;
const kind = () => props.kind;
@@ -508,18 +504,16 @@ export function ActivityDetailsModal(props: {
try {
if (kind() === "Lightning") {
console.debug("reading invoice: ", id());
const invoice =
await state.mutiny_wallet?.get_invoice_by_hash(id());
const invoice = await sw.get_invoice_by_hash(id());
return invoice;
} else if (kind() === "ChannelClose") {
console.debug("reading channel close: ", id());
const closeItem =
await state.mutiny_wallet?.get_channel_closure(id());
const closeItem = await sw.get_channel_closure(id());
return closeItem;
} else {
console.debug("reading tx: ", id());
const tx = await state.mutiny_wallet?.get_transaction(id());
const tx = await sw.get_transaction(id());
return tx;
}
@@ -528,17 +522,18 @@ export function ActivityDetailsModal(props: {
return undefined;
}
});
const tags = createMemo(() => {
const tags = createAsync(async () => {
if (
!!data() &&
// @ts-expect-error we're narrowing the type here
data()?.labels !== undefined &&
// @ts-expect-error we're narrowing the type here
typeof data()?.labels[0] === "string"
) {
const typedData = data() as MutinyInvoice | ActivityItem;
try {
// find if there's just one for now
const tags = state.mutiny_wallet?.get_tag_item(
data().labels[0]
);
const tags = await sw.get_tag_item(typedData.labels[0]);
if (tags) {
return tags;
} else {
@@ -598,7 +593,7 @@ export function ActivityDetailsModal(props: {
>
<OnchainHeader
info={
data() as OnChainTx
data() as unknown as OnChainTx
}
kind={kind()}
/>
@@ -612,7 +607,9 @@ export function ActivityDetailsModal(props: {
<Switch>
<Match when={kind() === "Lightning"}>
<LightningDetails
info={data() as MutinyInvoice}
info={
data() as unknown as MutinyInvoice
}
tags={tags()}
/>
</Match>
@@ -623,14 +620,18 @@ export function ActivityDetailsModal(props: {
}
>
<OnchainDetails
info={data() as OnChainTx}
info={
data() as unknown as OnChainTx
}
kind={kind()}
tags={tags()}
/>
</Match>
<Match when={kind() === "ChannelClose"}>
<ChannelCloseDetails
info={data() as ChannelClosure}
info={
data() as unknown as ChannelClosure
}
/>
</Match>
</Switch>

View File

@@ -1,3 +1,4 @@
import { createAsync } from "@solidjs/router";
import { Link, Users, Zap } from "lucide-solid";
import { Show } from "solid-js";
@@ -76,16 +77,19 @@ export function AmountFiat(props: {
amountSats: bigint | number | undefined;
denominationSize?: "sm" | "lg" | "xl";
}) {
const [state, _] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const amountInFiat = () =>
(state.fiat.value === "BTC" ? "" : "~") +
satsToFormattedFiat(
const amountInFiat = createAsync(async () => {
const formattedFiat = await satsToFormattedFiat(
state.price,
Number(props.amountSats) || 0,
state.fiat
state.fiat,
sw
);
return (state.fiat.value === "BTC" ? "" : "~") + formattedFiat;
});
return (
<h2 class="whitespace-nowrap font-light">
{amountInFiat()}

View File

@@ -35,25 +35,35 @@ function methodToIcon(method: MethodChoice["method"]) {
export const AmountEditable: ParentComponent<{
initialAmountSats: string | bigint;
setAmountSats: (s: bigint) => void;
fee?: string;
frozenAmount?: boolean;
onSubmit?: () => void;
activeMethod?: MethodChoice;
methods?: MethodChoice[];
setChosenMethod?: (method: MethodChoice) => void;
}> = (props) => {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [mode, setMode] = createSignal<"fiat" | "sats">("sats");
const [localSats, setLocalSats] = createSignal(
props.initialAmountSats.toString() || "0"
);
const [localFiat, setLocalFiat] = createSignal(
const [rawFiatAmount, setRawFiatAmount] = createSignal(
props.initialAmountSats.toString() || "0"
);
const [localFiat, setLocalFiat] = createSignal("0");
createEffect(() => {
if (rawFiatAmount()) {
satsToFiat(
state.price,
parseInt(props.initialAmountSats.toString() || "0") || 0,
state.fiat
)
);
Number(rawFiatAmount()) || 0,
state.fiat,
sw
).then((sats) => {
console.log("sats", sats);
setLocalFiat(sats);
});
}
});
const displaySats = () => toDisplayHandleNaN(localSats());
const displayFiat = () =>
@@ -72,7 +82,7 @@ export const AmountEditable: ParentComponent<{
const { value } = e.target as HTMLInputElement;
const sane = satsInputSanitizer(value);
setLocalSats(sane);
setLocalFiat(satsToFiat(state.price, Number(sane) || 0, state.fiat));
setRawFiatAmount(Number(sane).toString() || "0");
}
/** This behaves the same as handleCharacterInput but allows for the keyboard to be used instead of the virtual keypad
@@ -88,7 +98,7 @@ export const AmountEditable: ParentComponent<{
* result - 123.45
*/
function handleFiatInput(e: InputEvent) {
async function handleFiatInput(e: InputEvent) {
const { value } = e.currentTarget as HTMLInputElement;
let sane;
@@ -101,7 +111,7 @@ export const AmountEditable: ParentComponent<{
} else {
sane = fiatInputSanitizer(
// This allows us to handle the backspace key and fight float rounding
btcFloatRounding(localFiat()),
btcFloatRounding(localFiat() || "0"),
state.fiat.maxFractionalDigits
);
}
@@ -112,9 +122,13 @@ export const AmountEditable: ParentComponent<{
);
}
setLocalFiat(sane);
setLocalSats(
fiatToSats(state.price, parseFloat(sane || "0") || 0, false)
const sats = await fiatToSats(
state.price,
Number(sane) || 0,
false,
sw
);
setLocalSats(sats);
}
function toggle(disabled: boolean) {

View File

@@ -1,6 +1,6 @@
import { A, useNavigate } from "@solidjs/router";
import { Shuffle, Users } from "lucide-solid";
import { Match, Show, Switch } from "solid-js";
import { createMemo, Match, Show, Suspense, Switch } from "solid-js";
import {
AmountFiat,
@@ -51,13 +51,18 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
const navigate = useNavigate();
const i18n = useI18n();
const totalOnchain = () =>
const totalOnchain = createMemo(
() =>
(state.balance?.confirmed || 0n) +
(state.balance?.unconfirmed || 0n) +
(state.balance?.force_close || 0n);
(state.balance?.force_close || 0n)
);
const usableOnchain = () =>
(state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
const usableOnchain = createMemo(
() =>
(state.balance?.confirmed || 0n) +
(state.balance?.unconfirmed || 0n)
);
return (
<VStack>
@@ -82,12 +87,15 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
/>
</div>
<div class="text-lg text-white/70">
<Suspense>
<AmountFiat
amountSats={
state.balance?.federation || 0n
state.balance?.federation ||
0n
}
denominationSize="sm"
/>
</Suspense>
</div>
</div>
<Show
@@ -145,12 +153,14 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
/>
</div>
<div class="text-lg text-white/70">
<Suspense>
<AmountFiat
amountSats={
state.balance?.lightning || 0
}
denominationSize="sm"
/>
</Suspense>
</div>
</div>
</Match>
@@ -168,10 +178,12 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
/>
</div>
<div class="text-lg text-white/70">
<Suspense>
<AmountFiat
amountSats={totalOnchain()}
denominationSize="sm"
/>
</Suspense>
</div>
</div>
<div class="flex flex-col items-end justify-between gap-1">

View File

@@ -8,14 +8,15 @@ import {
import { Button, ContactFormValues, TextField, VStack } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore, WalletWorker } from "~/state/megaStore";
import { hexpubFromNpub } from "~/utils";
const validateNpub = async (value?: string) => {
const validateNpub = async (sw: WalletWorker, value?: string) => {
if (!value) {
return false;
}
try {
const hexpub = await hexpubFromNpub(value);
const hexpub = await hexpubFromNpub(sw, value);
if (!hexpub) {
return false;
}
@@ -30,6 +31,7 @@ export function ContactForm(props: {
initialValues?: ContactFormValues;
cta: string;
}) {
const [_state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
initialValues: props.initialValues
@@ -78,7 +80,10 @@ export function ContactForm(props: {
<Field
name="npub"
validate={[
custom(validateNpub, i18n.t("contacts.npub_error"))
custom(
(v) => validateNpub(sw, v),
i18n.t("contacts.npub_error")
)
]}
>
{(field, props) => (

View File

@@ -32,7 +32,7 @@ export function ContactViewer(props: {
const i18n = useI18n();
const [isOpen, setIsOpen] = createSignal(false);
const [isEditing, setIsEditing] = createSignal(false);
const [state, actions] = useMegaStore();
const [state, actions, sw] = useMegaStore();
const navigate = useNavigate();
const [confirmOpen, setConfirmOpen] = createSignal(false);
@@ -52,13 +52,13 @@ export function ContactViewer(props: {
setIsOpen(false);
};
const handlePay = () => {
const network = state.mutiny_wallet?.get_network() || "signet";
const handlePay = async () => {
const network = state.network || "signet";
const lnurl = props.contact.lnurl || props.contact.ln_address || "";
if (lnurl) {
const result = toParsedParams(lnurl, network);
const result = await toParsedParams(lnurl, network, sw);
if (!result.ok) {
showToast(result.error);
return;

View File

@@ -1,4 +1,3 @@
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
import { createSignal } from "solid-js";
@@ -9,7 +8,7 @@ import { eify } from "~/utils";
export function DeleteEverything(props: { emergency?: boolean }) {
const i18n = useI18n();
const [state, actions] = useMegaStore();
const [state, actions, sw] = useMegaStore();
async function confirmReset() {
setConfirmOpen(true);
@@ -29,7 +28,7 @@ export function DeleteEverything(props: { emergency?: boolean }) {
// If we're in a context where the wallet is loaded we want to use the regular action to delete it
// Otherwise we just call the import_json method directly
if (state.mutiny_wallet && !props.emergency) {
if (state.load_stage === "done" && !props.emergency) {
try {
await actions.deleteMutinyWallet();
} catch (e) {
@@ -37,9 +36,7 @@ export function DeleteEverything(props: { emergency?: boolean }) {
console.error(e);
}
} else {
// If there's no mutiny_wallet loaded we might need to initialize WASM
await initMutinyWallet();
await MutinyWallet.import_json("{}");
await sw.import_json("{}");
}
showToast({

View File

@@ -20,7 +20,7 @@ export function EditProfileForm(props: {
saving: boolean;
cta: string;
}) {
const [state] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [uploading, setUploading] = createSignal(false);
const [uploadError, setUploadError] = createSignal<Error>();
@@ -51,8 +51,7 @@ export function EditProfileForm(props: {
if (files() && files().length) {
const base64 = await blobToBase64(files()[0].file);
if (base64) {
imageUrl =
await state.mutiny_wallet?.upload_profile_pic(base64);
imageUrl = await sw.upload_profile_pic(base64);
}
}
await props.onSave({

View File

@@ -1,4 +1,5 @@
import { createMemo, ParentComponent, Show } from "solid-js";
import { createAsync } from "@solidjs/router";
import { createMemo, ParentComponent, Show, Suspense } from "solid-js";
import { VStack } from "~/components";
import { useI18n } from "~/i18n/context";
@@ -20,20 +21,22 @@ const AmountKeyValue: ParentComponent<{ key: string; gray?: boolean }> = (
};
function USDShower(props: { amountSats: string; fee?: string }) {
const [state, _] = useMegaStore();
const amountInFiat = () =>
(state.fiat.value === "BTC" ? "" : "~") +
satsToFormattedFiat(
const [state, _actions, sw] = useMegaStore();
const amountInFiat = createAsync(async () => {
const formattedFiat = await satsToFormattedFiat(
state.price,
add(props.amountSats, props.fee),
state.fiat
state.fiat,
sw
);
return (state.fiat.value === "BTC" ? "" : "~") + formattedFiat;
});
return (
<Show when={!(props.amountSats === "0")}>
<AmountKeyValue gray key="">
<div class="self-end whitespace-nowrap">
{`${amountInFiat()} `}
<Suspense>{`${amountInFiat()} `}</Suspense>
<span class="text-sm">{state.fiat.value}</span>
</div>
</AmountKeyValue>

View File

@@ -114,7 +114,10 @@ export function GenericItem(props: {
<div class="flex w-full items-center gap-1 text-m-grey-400">
<Clock4 class="w-3" />
<span class="text-xs text-m-grey-400">
{i18n.t("common.expires", { time: props.due })}
{i18n.t("common.expires", {
time: props.due,
interpolation: { escapeValue: false }
})}
</span>
</div>
</Show>

View File

@@ -1,22 +0,0 @@
import { A, useLocation } from "@solidjs/router";
import { Gift } from "lucide-solid";
import { useI18n } from "~/i18n/context";
export function GiftLink() {
const i18n = useI18n();
const location = useLocation();
return (
<A
class="flex items-center gap-2 font-semibold text-m-red no-underline"
href="/settings/gift"
state={{
previous: location.pathname
}}
>
{i18n.t("settings.gift.give_sats_link")}
<Gift class="h-5 w-5" />
</A>
);
}

View File

@@ -1,4 +1,4 @@
import { Match, Switch } from "solid-js";
import { createMemo, Match, Suspense, Switch } from "solid-js";
import { AmountFiat, AmountSats } from "~/components/Amount";
import { useMegaStore } from "~/state/megaStore";
@@ -6,11 +6,13 @@ import { useMegaStore } from "~/state/megaStore";
export function HomeBalance() {
const [state, actions] = useMegaStore();
const combinedBalance = () =>
const combinedBalance = createMemo(
() =>
(state.balance?.federation || 0n) +
(state.balance?.lightning || 0n) +
(state.balance?.confirmed || 0n) +
(state.balance?.unconfirmed || 0n);
(state.balance?.unconfirmed || 0n)
);
// TODO: do some sort of status indicator
// const fullyReady = () => state.load_stage === "done" && state.price !== 0;
@@ -29,10 +31,12 @@ export function HomeBalance() {
/>
</Match>
<Match when={state.balanceView === "fiat"}>
<Suspense>
<AmountFiat
amountSats={combinedBalance()}
denominationSize="lg"
/>
</Suspense>
</Match>
<Match when={state.balanceView === "hidden"}>
<div class="flex items-center gap-2">

View File

@@ -35,7 +35,7 @@ const ImageWithFallback = (props: { src: string; alt: string }) => {
};
export function HomePrompt() {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const [params, setParams] = useSearchParams();
@@ -68,7 +68,7 @@ export function HomePrompt() {
lsps_token: params.token
};
try {
await state.mutiny_wallet?.change_lsp(
await sw.change_lsp(
values.lsp ? values.lsp : undefined,
values.lsps_connection_string
? values.lsps_connection_string
@@ -100,10 +100,9 @@ export function HomePrompt() {
async function handleLnurlAuth() {
setAuthLoading(true);
try {
await state.mutiny_wallet?.lnurl_auth(lnurlauthResult()!);
await sw.lnurl_auth(lnurlauthResult()!);
setIsAuthenticated(true);
} catch (e) {
// lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9ashq6f0d3hxzat5dqlhgct884kx7emfdcnxkvfavvurwdtrvgmkyd3489skgcfexqckxd3svg6xgwr98q6nsd3c893kzcfkvc6nsdr9xpjxvc3jvejrxwpevyurqvfev3nxvvnxx5ergdc8g6gzl
console.error(e);
setAuthError(eify(e));
} finally {

View File

@@ -18,7 +18,7 @@ import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function HomeSubnav() {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const i18n = useI18n();
@@ -32,8 +32,7 @@ export function HomeSubnav() {
const [pending, { refetch }] = createResource(async () => {
try {
const pending =
await state.mutiny_wallet?.get_pending_nwc_invoices();
const pending = await sw.get_pending_nwc_invoices();
return pending?.length || 0;
} catch (e) {
console.error(e);
@@ -90,7 +89,7 @@ export function HomeSubnav() {
"text-white": !!((pending.latest || 0) > 0)
}}
>
{pending()}
{pending.latest}
</span>
</Show>
</Suspense>

View File

@@ -1,4 +1,3 @@
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { createFileUploader } from "@solid-primitives/upload";
import { createSignal, Show } from "solid-js";
@@ -19,7 +18,7 @@ import { downloadTextFile, eify } from "~/utils";
export function ImportExport(props: { emergency?: boolean }) {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [error, setError] = createSignal<Error>();
const [exportDecrypt, setExportDecrypt] = createSignal(false);
@@ -28,7 +27,7 @@ export function ImportExport(props: { emergency?: boolean }) {
async function handleSave() {
try {
setError(undefined);
const json = await MutinyWallet.export_json();
const json = await sw.export_json();
await downloadTextFile(json || "", "mutiny-state.json");
} catch (e) {
console.error(e);
@@ -52,7 +51,7 @@ export function ImportExport(props: { emergency?: boolean }) {
)
);
}
const json = await MutinyWallet.export_json(password());
const json = await sw.export_json(password());
await downloadTextFile(json || "", "mutiny-state.json");
} catch (e) {
console.error(e);
@@ -103,24 +102,22 @@ export function ImportExport(props: { emergency?: boolean }) {
fileReader.readAsText(file, "UTF-8");
});
if (state.mutiny_wallet && !props.emergency) {
if (state.load_stage === "done" && !props.emergency) {
console.log("Mutiny wallet loaded, stopping");
try {
await state.mutiny_wallet.stop();
await sw.stop();
} catch (e) {
console.error(e);
setError(eify(e));
}
} else {
// If there's no mutiny wallet loaded we need to initialize WASM
console.log("Initializing WASM");
await initMutinyWallet();
await sw.initializeWasm();
}
// This should throw if there's a parse error, so we won't end up clearing
if (text) {
JSON.parse(text);
await MutinyWallet.import_json(text);
await sw.import_json(text);
}
setTimeout(() => {
@@ -191,9 +188,10 @@ export function ImportExport(props: { emergency?: boolean }) {
{/* TODO: this is pretty redundant with the DecryptDialog, could make a shared component */}
<SimpleDialog
title={i18n.t(
"settings.emergency_kit.import_export.confirm_replace"
"settings.emergency_kit.import_export.decrypt_export"
)}
open={exportDecrypt()}
setOpen={() => setExportDecrypt(false)}
>
<form onSubmit={savePassword}>
<div class="flex flex-col gap-4">

View File

@@ -1,4 +1,3 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
import { createSignal, Show } from "solid-js";
@@ -7,7 +6,7 @@ import { Button, InfoBox, SimpleInput } from "~/components";
import { useMegaStore } from "~/state/megaStore";
export function ImportNsecForm(props: { setup?: boolean }) {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const [nsec, setNsec] = createSignal("");
const [saving, setSaving] = createSignal(false);
@@ -18,16 +17,13 @@ export function ImportNsecForm(props: { setup?: boolean }) {
setError(undefined);
const trimmedNsec = nsec().trim();
try {
const npub = await MutinyWallet.nsec_to_npub(trimmedNsec);
const npub = await sw.nsec_to_npub(trimmedNsec);
if (!npub) {
throw new Error("Invalid nsec");
}
await SecureStoragePlugin.set({ key: "nsec", value: trimmedNsec });
const new_npub = await state.mutiny_wallet?.change_nostr_keys(
trimmedNsec,
undefined
);
const new_npub = await sw.change_nostr_keys(trimmedNsec, undefined);
console.log("Changed to new npub: ", new_npub);
if (props.setup) {
navigate("/addfederation");

View File

@@ -43,13 +43,13 @@ type RefetchPeersType = (
function PeerItem(props: { peer: MutinyPeer; refetchPeers: RefetchPeersType }) {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const handleDisconnectPeer = async () => {
if (props.peer.is_connected) {
await state.mutiny_wallet?.disconnect_peer(props.peer.pubkey);
await sw.disconnect_peer(props.peer.pubkey);
} else {
await state.mutiny_wallet?.delete_peer(props.peer.pubkey);
await sw.delete_peer(props.peer.pubkey);
}
await props.refetchPeers();
};
@@ -82,12 +82,10 @@ function PeerItem(props: { peer: MutinyPeer; refetchPeers: RefetchPeersType }) {
function PeersList() {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const getPeers = async () => {
return (await state.mutiny_wallet?.list_peers()) as Promise<
MutinyPeer[]
>;
return await sw.list_peers();
};
const [peers, { refetch }] = createResource(getPeers);
@@ -125,7 +123,7 @@ function PeersList() {
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [value, setValue] = createSignal("");
@@ -134,7 +132,7 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
const peerConnectString = value().trim();
await state.mutiny_wallet?.connect_to_peer(peerConnectString);
await sw.connect_to_peer(peerConnectString);
await props.refetchPeers();
@@ -176,7 +174,7 @@ type PendingChannelAction = "close" | "force_close" | "abandon";
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [pendingChannelAction, setPendingChannelAction] =
createSignal<PendingChannelAction>();
@@ -187,7 +185,7 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
if (!action) return;
setConfirmLoading(true);
try {
await state.mutiny_wallet?.close_channel(
await sw.close_channel(
props.channel.outpoint as string,
action === "force_close",
action === "abandon"
@@ -279,17 +277,15 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
function ChannelsList() {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const getChannels = async () => {
return (await state.mutiny_wallet?.list_channels()) as Promise<
MutinyChannel[]
>;
return sw.list_channels();
};
const [channels, { refetch }] = createResource(getChannels);
const network = state.mutiny_wallet?.get_network() as Network;
const network = state.network;
return (
<>
@@ -329,7 +325,7 @@ function ChannelsList() {
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [creationError, setCreationError] = createSignal<Error>();
@@ -347,11 +343,11 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
try {
const pubkey = peerPubkey().trim();
const bigAmount = BigInt(amount());
console.log("pubkey", pubkey);
console.log("bigAmount", bigAmount);
const new_channel = await state.mutiny_wallet?.open_channel(
pubkey,
bigAmount
);
const new_channel = await sw.open_channel(pubkey, bigAmount);
console.log("new_channel", new_channel);
setNewChannel(new_channel);
@@ -364,7 +360,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
}
};
const network = state.mutiny_wallet?.get_network() as Network;
const network = state.network;
return (
<>
@@ -421,10 +417,10 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
function ListNodes() {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const getNodeIds = async () => {
const nodes = await state.mutiny_wallet?.list_nodes();
const nodes = await sw.list_nodes();
return nodes as string[];
};
@@ -450,7 +446,7 @@ function ListNodes() {
function LSPS(props: { initialSettings: MutinyWalletSettingStrings }) {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [lspSettingsForm, { Form, Field }] =
createForm<MutinyWalletSettingStrings>({
validate: (values) => {
@@ -469,7 +465,7 @@ function LSPS(props: { initialSettings: MutinyWalletSettingStrings }) {
async function handleSubmit(values: MutinyWalletSettingStrings) {
console.log("values", values);
try {
await state.mutiny_wallet?.change_lsp(
await sw.change_lsp(
values.lsp ? values.lsp : undefined,
values.lsps_connection_string
? values.lsps_connection_string

View File

@@ -43,7 +43,7 @@ function LoadingBar(props: { value: number; max: number }) {
}
export function LoadingIndicator() {
const [state, _actions] = useMegaStore();
const [state] = useMegaStore();
const loadStageValue = () => {
switch (state.load_stage) {

View File

@@ -1,12 +1,13 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { createSignal, Show } from "solid-js";
import { Button, InfoBox, InnerCard, NiceP, VStack } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { downloadTextFile, eify } from "~/utils";
export function Logs() {
const i18n = useI18n();
const [_state, _actions, sw] = useMegaStore();
// Create state for errors, password and dialog visibility
const [error, setError] = createSignal<Error>();
@@ -14,7 +15,7 @@ export function Logs() {
async function handleSave() {
try {
setError(undefined);
const logs = await MutinyWallet.get_logs();
const logs = await sw.get_logs();
await downloadTextFile(
logs.join("") || "",
"mutiny-logs.txt",

View File

@@ -104,7 +104,7 @@ export function NWCEditor(props: {
initialNWA?: string;
onSave: (indexToOpen?: number, nwcUriForCallback?: string) => Promise<void>;
}) {
const [state] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const nwa = createMemo(() => parseNWA(props.initialNWA));
@@ -152,7 +152,7 @@ export function NWCEditor(props: {
async function createNwa(f: BudgetForm) {
if (!f.nwaString) throw new Error("We lost the NWA string!");
try {
await state.mutiny_wallet?.approve_nostr_wallet_auth(
await sw.approve_nostr_wallet_auth(
f.connection_name || "Nostr Wallet Auth",
// can we do better than ! here?
f.nwaString
@@ -177,7 +177,7 @@ export function NWCEditor(props: {
try {
const profile: NwcProfile | undefined =
await state.mutiny_wallet?.get_nwc_profile(index);
await sw.get_nwc_profile(index);
console.log(profile);
return profile;
} catch (e) {
@@ -200,8 +200,7 @@ export function NWCEditor(props: {
if (!label) return undefined;
try {
const contact: TagItem | undefined =
await state.mutiny_wallet?.get_tag_item(label);
const contact: TagItem | undefined = await sw.get_tag_item(label);
return contact;
} catch (e) {
console.error(e);
@@ -215,12 +214,11 @@ export function NWCEditor(props: {
let newProfile: NwcProfile | undefined = undefined;
if (!f.profileIndex) throw new Error("No profile index!");
if (!f.auto_approve || f.budget_amount === "0") {
newProfile =
await state.mutiny_wallet?.set_nwc_profile_require_approval(
newProfile = await sw.set_nwc_profile_require_approval(
f.profileIndex
);
} else {
newProfile = await state.mutiny_wallet?.set_nwc_profile_budget(
newProfile = await sw.set_nwc_profile_budget(
f.profileIndex,
BigInt(f.budget_amount),
mapIntervalToBudgetPeriod(f.interval)
@@ -240,11 +238,9 @@ export function NWCEditor(props: {
let newProfile: NwcProfile | undefined = undefined;
if (!f.auto_approve || f.budget_amount === "0") {
newProfile = await state.mutiny_wallet?.create_nwc_profile(
f.connection_name
);
newProfile = await sw.create_nwc_profile(f.connection_name);
} else {
newProfile = await state.mutiny_wallet?.create_budget_nwc_profile(
newProfile = await sw.create_budget_nwc_profile(
f.connection_name,
BigInt(f.budget_amount),
mapIntervalToBudgetPeriod(f.interval),

View File

@@ -1,4 +1,3 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { createAsync, useNavigate } from "@solidjs/router";
import { Search } from "lucide-solid";
import {
@@ -38,9 +37,9 @@ export function Avatar(props: { image_url?: string; large?: boolean }) {
export function NostrActivity() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const npub = createAsync(async () => state.mutiny_wallet?.get_npub());
const npub = createAsync(async () => await sw.get_npub());
const [data, { refetch }] = createResource(npub, fetchZaps);
@@ -72,14 +71,14 @@ export function NostrActivity() {
// TODO: can this be part of mutiny wallet?
async function newContactFromHexpub(hexpub: string) {
try {
const npub = await MutinyWallet.hexpub_to_npub(hexpub);
const npub = await sw.hexpub_to_npub(hexpub);
console.log("newContactFromHexpub", npub);
if (!npub) {
throw new Error("No npub for that hexpub");
}
const existingContact =
await state.mutiny_wallet?.get_contact_for_npub(npub);
const existingContact = await sw.get_contact_for_npub(npub);
if (existingContact) {
navigate(`/chat/${existingContact.id}`);
@@ -94,7 +93,7 @@ export function NostrActivity() {
const ln_address = parsed.lud16 || undefined;
const lnurl = parsed.lud06 || undefined;
const contactId = await state.mutiny_wallet?.create_new_contact(
const contactId = await sw.create_new_contact(
name,
npub,
ln_address,
@@ -106,7 +105,7 @@ export function NostrActivity() {
throw new Error("no contact id returned");
}
const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
const tagItem = await sw.get_tag_item(contactId);
if (!tagItem) {
throw new Error("no contact returned");

View File

@@ -31,21 +31,20 @@ type PendingItem = {
export function PendingNwc() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [error, setError] = createSignal<Error>();
const navigate = useNavigate();
async function fetchPendingRequests() {
const profiles = await state.mutiny_wallet?.get_nwc_profiles();
const profiles = await sw.get_nwc_profiles();
if (!profiles) return [];
const contacts: TagItem[] | undefined =
await state.mutiny_wallet?.get_contacts_sorted();
const contacts: TagItem[] | undefined = await sw.get_contacts_sorted();
if (!contacts) return [];
const pending = await state.mutiny_wallet?.get_pending_nwc_invoices();
const pending = await sw.get_pending_nwc_invoices();
if (!pending) return [];
const pendingItems: PendingItem[] = [];
@@ -88,7 +87,7 @@ export function PendingNwc() {
try {
// setPaying(item.id);
setPayList([...payList(), item.id]);
await state.mutiny_wallet?.approve_invoice(item.id);
await sw.approve_invoice(item.id);
await vibrateSuccess();
} catch (e) {
const err = eify(e);
@@ -97,7 +96,7 @@ export function PendingNwc() {
if (err.message === "An invoice must not get payed twice.") {
// wrap in try/catch so we don't crash if the invoice is already gone
try {
await state.mutiny_wallet?.deny_invoice(item.id);
await sw.deny_invoice(item.id);
} catch (_e) {
// do nothing
}
@@ -121,7 +120,7 @@ export function PendingNwc() {
async function denyAll() {
try {
await state.mutiny_wallet?.deny_all_pending_nwc();
await sw.deny_all_pending_nwc();
} catch (e) {
setError(eify(e));
console.error(e);
@@ -133,7 +132,7 @@ export function PendingNwc() {
async function rejectItem(item: PendingItem) {
try {
setPayList([...payList(), item.id]);
await state.mutiny_wallet?.deny_invoice(item.id);
await sw.deny_invoice(item.id);
} catch (e) {
setError(eify(e));
console.error(e);
@@ -161,7 +160,11 @@ export function PendingNwc() {
return (
<Switch>
<Match when={pendingRequests() && pendingRequests()!.length > 0}>
<Match
when={
pendingRequests.latest && pendingRequests.latest!.length > 0
}
>
<ButtonCard onClick={() => navigate("/settings/connections")}>
<div class="flex items-center gap-2">
<PlugZap class="inline-block text-m-red" />
@@ -197,7 +200,7 @@ export function PendingNwc() {
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<For each={pendingRequests()}>
<For each={pendingRequests.latest}>
{(pendingItem) => (
<GenericItem
primaryAvatarUrl={pendingItem.image || ""}

View File

@@ -6,25 +6,30 @@ import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function ReceiveWarnings(props: {
amountSats: string | bigint;
amountSats: bigint;
from_fedi_to_ln?: boolean;
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [inboundCapacity] = createResource(async () => {
try {
const channels = await state.mutiny_wallet?.list_channels();
let inbound = 0;
const channels = await sw.list_channels();
if (!channels) return 0n;
let inbound = 0n;
// PAIN: mutiny-wasm types say these are bigints, but they're actually numbers
for (const channel of channels) {
inbound += channel.size - (channel.balance + channel.reserve);
inbound +=
BigInt(channel.size) -
BigInt(channel.balance + channel.reserve);
}
return inbound;
} catch (e) {
console.error(e);
return 0;
return 0n;
}
});
@@ -41,12 +46,7 @@ export function ReceiveWarnings(props: {
});
}
const parsed = Number(props.amountSats);
if (isNaN(parsed)) {
return undefined;
}
if (parsed > (inboundCapacity() || 0)) {
if (props.amountSats > (inboundCapacity() || 0n)) {
return i18n.t("receive.amount_editable.setup_fee_lightning");
}

View File

@@ -6,16 +6,16 @@ import { useMegaStore } from "~/state/megaStore";
export function Restart() {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [hasStopped, setHasStopped] = createSignal(false);
async function toggle() {
try {
if (hasStopped()) {
await state.mutiny_wallet?.start();
await sw.start();
setHasStopped(false);
} else {
await state.mutiny_wallet?.stop();
await sw.stop();
setHasStopped(true);
}
} catch (e) {

View File

@@ -4,11 +4,11 @@ import { useMegaStore } from "~/state/megaStore";
export function ResyncOnchain() {
const i18n = useI18n();
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
async function reset() {
try {
await state.mutiny_wallet?.reset_onchain_tracker();
await sw.reset_onchain_tracker();
} catch (e) {
console.error(e);
}

View File

@@ -1,4 +1,3 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { Title } from "@solidjs/meta";
import { MonitorSmartphone } from "lucide-solid";
import { createResource, Match, Switch } from "solid-js";
@@ -21,6 +20,7 @@ import {
MutinyWalletSettingStrings
} from "~/logic/mutinyWalletSetup";
import { FeedbackLink } from "~/routes/Feedback";
import { useMegaStore } from "~/state/megaStore";
function ErrorFooter() {
return (
@@ -38,6 +38,7 @@ export function SetupErrorDisplay(props: {
password?: string;
}) {
// Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value
const [_state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const error = props.initialError;
@@ -45,7 +46,7 @@ export function SetupErrorDisplay(props: {
if (error.message.startsWith("Mutiny is already running")) {
const settings: MutinyWalletSettingStrings = await getSettings();
try {
const secs = await MutinyWallet.get_device_lock_remaining_secs(
const secs = await sw.get_device_lock_remaining_secs(
props.password,
settings.auth,
settings.storage

View File

@@ -1,7 +1,7 @@
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { cache, createAsync, useNavigate } from "@solidjs/router";
import { Scan, Search } from "lucide-solid";
import { createMemo, For, Suspense } from "solid-js";
import { For, Suspense } from "solid-js";
import { Circle, LabelCircle, showToast } from "~/components";
import { useMegaStore } from "~/state/megaStore";
@@ -10,13 +10,12 @@ export function SocialActionRow(props: {
onSearch: () => void;
onScan: () => void;
}) {
const [state, actions] = useMegaStore();
const [_state, actions, sw] = useMegaStore();
const navigate = useNavigate();
const getContacts = cache(async () => {
try {
const contacts: TagItem[] =
(await state.mutiny_wallet?.get_contacts_sorted()) || [];
const contacts: TagItem[] = (await sw.get_contacts_sorted()) || [];
// contact must have a npub, ln_address, or lnurl
return contacts.filter(
@@ -33,16 +32,17 @@ export function SocialActionRow(props: {
const contacts = createAsync(() => getContacts(), { initialValue: [] });
const profileDeleted = createMemo(
() => state.mutiny_wallet?.get_nostr_profile().deleted
);
const profileDeleted = createAsync(async () => {
const profile = await sw.get_nostr_profile();
return profile?.deleted;
});
// TODO this is mostly copy pasted from chat, could be a shared util maybe
function sendToContact(contact?: TagItem) {
async function sendToContact(contact?: TagItem) {
if (!contact) return;
const address = contact.ln_address || contact.lnurl;
if (address) {
actions.handleIncomingString(
await actions.handleIncomingString(
(address || "").trim(),
(error) => {
showToast(error);
@@ -60,6 +60,14 @@ export function SocialActionRow(props: {
}
}
async function handleClick(contact: TagItem) {
if (profileDeleted() || !contact.npub) {
sendToContact(contact);
} else {
navigate(`/chat/${contact.id}`);
}
}
return (
<div class="-ml-4 -mr-4 flex gap-2 overflow-x-scroll py-[1px] pl-4">
<Circle color="red" onClick={props.onSearch}>
@@ -76,13 +84,7 @@ export function SocialActionRow(props: {
label={false}
name={contact.name}
image_url={contact.primal_image_url}
onClick={() => {
if (profileDeleted() || !contact.npub) {
sendToContact(contact);
} else {
navigate(`/chat/${contact.id}`);
}
}}
onClick={() => handleClick(contact)}
/>
)}
</For>

View File

@@ -1,77 +0,0 @@
import { createForm, required, SubmitHandler } from "@modular-forms/solid";
import { createSignal, Show } from "solid-js";
import { Button, VStack } from "~/components";
import { InfoBox } from "~/components/InfoBox";
import { TextField } from "~/components/layout/TextField";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { eify } from "~/utils";
type NostrContactsForm = {
npub: string;
};
export function SyncContactsForm() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [error, setError] = createSignal<Error>();
const [feedbackForm, { Form, Field }] = createForm<NostrContactsForm>({
initialValues: {
npub: ""
}
});
const handleSubmit: SubmitHandler<NostrContactsForm> = async (
f: NostrContactsForm
) => {
try {
const npub = f.npub.trim();
await state.mutiny_wallet?.sync_nostr_contacts(npub);
} catch (e) {
console.error(e);
setError(eify(e));
}
};
return (
<Form onSubmit={handleSubmit}>
<VStack>
<Field
name="npub"
validate={[
required(
i18n.t("settings.nostr_contacts.npub_required")
)
]}
>
{(field, props) => (
<TextField
{...props}
value={field.value}
error={field.error}
label={i18n.t("settings.nostr_contacts.npub_label")}
placeholder="npub..."
/>
)}
</Field>
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<Button
loading={feedbackForm.submitting}
disabled={
!feedbackForm.dirty ||
feedbackForm.submitting ||
feedbackForm.invalid
}
intent="blue"
type="submit"
>
{i18n.t("settings.nostr_contacts.sync")}
</Button>
</VStack>
</Form>
);
}

View File

@@ -37,8 +37,6 @@ export * from "./Failure";
export * from "./ShareCard";
export * from "./Toaster";
export * from "./NostrActivity";
export * from "./SyncContactsForm";
export * from "./GiftLink";
export * from "./MutinyPlusCta";
export * from "./ToggleHodl";
export * from "./IOSbanner";

View File

@@ -51,11 +51,16 @@ export const Card: ParentComponent<{
export const ButtonCard: ParentComponent<{
onClick: () => void;
red?: boolean;
}> = (props) => {
return (
<button
onClick={() => props.onClick()}
class="flex w-full rounded-xl border border-white/10 bg-neutral-900 p-4 active:-mb-[1px] active:mt-[1px] active:opacity-70"
class="flex w-full rounded-xl border border-white/10 p-4 active:-mb-[1px] active:mt-[1px] active:opacity-70"
classList={{
"bg-neutral-900": !props.red,
"bg-m-red": props.red
}}
>
{props.children}
</button>
@@ -172,12 +177,14 @@ const FullscreenLoader = () => {
};
export const MutinyWalletGuard: ParentComponent = (props) => {
const [state, _] = useMegaStore();
const [state] = useMegaStore();
return (
<Suspense fallback={<FullscreenLoader />}>
<Switch>
<Match when={state.mutiny_wallet && !state.wallet_loading}>
<Match
when={!state.wallet_loading && state.load_stage === "done"}
>
{props.children}
</Match>
<Match when={true}>

View File

@@ -1,6 +1,3 @@
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
export type Network = "bitcoin" | "testnet" | "regtest" | "signet";
export type MutinyWalletSettingStrings = {
@@ -139,7 +136,7 @@ export async function getSettings() {
// Expect urls like /_services/proxy and /_services/storage
if (selfhosted) {
let base = window.location.origin;
let base = location.origin;
console.log("Self-hosted mode enabled, using base URL", base);
const storage = settings.storage;
if (storage && storage.startsWith("/")) {
@@ -178,26 +175,6 @@ export async function setSettings(newSettings: MutinyWalletSettingStrings) {
});
}
export async function checkForWasm() {
try {
if (
typeof WebAssembly === "object" &&
typeof WebAssembly.instantiate === "function"
) {
const module = new WebAssembly.Module(
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
);
if (!(module instanceof WebAssembly.Module)) {
throw new Error("Couldn't instantiate WASM Module");
}
} else {
throw new Error("No WebAssembly global object found");
}
} catch (e) {
console.error(e);
}
}
export async function doubleInitDefense() {
console.log("Starting init...");
// Ultimate defense against getting multiple instances of the wallet running.
@@ -213,146 +190,3 @@ export async function doubleInitDefense() {
window.location.reload();
}
}
export async function initializeWasm() {
// Actually intialize the WASM, this should be the first thing that requires the WASM blob to be downloaded
// If WASM is already initialized, don't init twice
try {
const _sats_the_standard = MutinyWallet.convert_btc_to_sats(1);
console.debug("MutinyWallet WASM already initialized, skipping init");
return;
} catch (e) {
console.debug("MutinyWallet WASM about to be initialized");
await initMutinyWallet();
}
}
export async function setupMutinyWallet(
settings: MutinyWalletSettingStrings,
password?: string,
safeMode?: boolean,
shouldZapHodl?: boolean
): Promise<MutinyWallet | undefined> {
console.log("Starting setup...");
// https://developer.mozilla.org/en-US/docs/Web/API/Storage_API
// Ask the browser to not clear storage
if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist().then((persistent) => {
if (persistent) {
console.log(
"Storage will not be cleared except by explicit user action"
);
} else {
console.log(
"Storage may be cleared by the UA under storage pressure."
);
}
});
}
const {
network,
proxy,
esplora,
rgs,
lsp,
lsps_connection_string,
lsps_token,
auth,
subscriptions,
storage,
scorer,
primal_api,
blind_auth,
hermes
} = settings;
let nsec;
// get nsec from secure storage
try {
const value = await SecureStoragePlugin.get({ key: "nsec" });
nsec = value.value;
} catch (e) {
console.log("No nsec stored");
}
// if we didn't get an nsec from storage, try to use extension
let extension_key;
if (!nsec && Object.prototype.hasOwnProperty.call(window, "nostr")) {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ignore nostr not existing, only does if they have extension
extension_key = await window.nostr.getPublicKey();
} catch (_) {
console.log("No NIP-07 extension");
}
}
console.log("Initializing Mutiny Manager");
console.log("Using network", network);
console.log("Using proxy", proxy);
console.log("Using esplora address", esplora);
console.log("Using rgs address", rgs);
console.log("Using lsp address", lsp);
console.log("Using lsp connection string", lsps_connection_string);
console.log("Using lsp token", lsps_token);
console.log("Using auth address", auth);
console.log("Using subscriptions address", subscriptions);
console.log("Using storage address", storage);
console.log("Using scorer address", scorer);
console.log("Using primal api", primal_api);
console.log("Using blind auth", blind_auth);
console.log("Using hermes", hermes);
console.log(safeMode ? "Safe mode enabled" : "Safe mode disabled");
console.log(shouldZapHodl ? "Hodl zaps enabled" : "Hodl zaps disabled");
// Only use lsps if there's no lsp set
const shouldUseLSPS = !lsp && lsps_connection_string && lsps_token;
const mutinyWallet = await new MutinyWallet(
// Password
password ? password : undefined,
// Mnemonic
undefined,
proxy,
network,
esplora,
rgs,
shouldUseLSPS ? undefined : lsp,
shouldUseLSPS ? lsps_connection_string : undefined,
shouldUseLSPS ? lsps_token : undefined,
auth,
subscriptions,
storage,
scorer,
// Do not connect peers
undefined,
// Do not skip device lock
undefined,
// Safe mode
safeMode || undefined,
// Skip hodl invoices? (defaults to true, so if shouldZapHodl is true that's when we pass false)
shouldZapHodl ? false : undefined,
// Nsec override
nsec,
// Nip7
extension_key ? extension_key : undefined,
// primal URL
primal_api || "https://primal-cache.mutinywallet.com/api",
/// blind auth url
blind_auth,
/// hermes url
hermes
);
sessionStorage.setItem("MUTINY_WALLET_INITIALIZED", Date.now().toString());
if (mutinyWallet) {
return mutinyWallet;
} else {
return undefined;
}
}

View File

@@ -1,5 +1,4 @@
import { PaymentParams } from "@mutinywallet/mutiny-wasm";
import { WalletWorker } from "~/state/megaStore";
import { Result } from "~/utils";
export type ParsedParams = {
@@ -20,13 +19,14 @@ export type ParsedParams = {
contact_id?: string;
};
export function toParsedParams(
export async function toParsedParams(
str: string,
ourNetwork: string
): Result<ParsedParams> {
ourNetwork: string,
sw: WalletWorker
): Promise<Result<ParsedParams>> {
let params;
try {
params = new PaymentParams(str || "");
params = await sw.parse_params(str || "");
} catch (e) {
return { ok: false, error: new Error("Invalid payment request") };
}

View File

@@ -8,6 +8,7 @@ import {
JSX,
Match,
onCleanup,
onMount,
Suspense,
Switch
} from "solid-js";
@@ -22,7 +23,6 @@ import {
Chat,
EditProfile,
Feedback,
Gift as GiftReceive,
Main,
NotFound,
Profile,
@@ -43,7 +43,6 @@ import {
Currency,
EmergencyKit,
Encrypt,
Gift,
ImportProfileSettings,
Language,
LightningAddress,
@@ -63,11 +62,36 @@ import {
} from "~/routes/setup";
import { Provider as MegaStoreProvider, useMegaStore } from "~/state/megaStore";
function GlobalListeners() {
const setStatusBarStyleDark = async () => {
await StatusBar.setStyle({ style: Style.Dark });
};
if (Capacitor.isNativePlatform()) {
await setStatusBarStyleDark();
}
function ChildrenOrError(props: { children: JSX.Element }) {
const [state] = useMegaStore();
return (
<Switch>
<Match when={state.setup_error}>
<SetupErrorDisplay
initialError={state.setup_error!}
password={state.password}
/>
</Match>
<Match when={true}>{props.children}</Match>
</Switch>
);
}
export function Router() {
// listeners for native navigation handling
// Check if the platform is Android to handle back
onMount(async () => {
if (Capacitor.getPlatform() === "android") {
const { remove } = CapacitorApp.addListener(
const { remove } = await CapacitorApp.addListener(
"backButton",
({ canGoBack }) => {
if (!canGoBack) {
@@ -88,56 +112,32 @@ function GlobalListeners() {
// Handle app links on native platforms
if (Capacitor.isNativePlatform()) {
const navigate = useNavigate();
const { remove } = CapacitorApp.addListener("appUrlOpen", (data) => {
const { remove } = await CapacitorApp.addListener(
"appUrlOpen",
(data) => {
const url = new URL(data.url);
const path = url.pathname;
const urlParams = new URLSearchParams(url.search);
console.log(`Navigating to ${path}?${urlParams.toString()}`);
console.log(
`Navigating to ${path}?${urlParams.toString()}`
);
navigate(`${path}?${urlParams.toString()}`);
});
}
);
onCleanup(() => {
console.debug("cleaning up appUrlOpen listener");
remove();
});
}
return null;
}
const setStatusBarStyleDark = async () => {
await StatusBar.setStyle({ style: Style.Dark });
};
if (Capacitor.isNativePlatform()) {
await setStatusBarStyleDark();
}
function ChildrenOrError(props: { children: JSX.Element }) {
const [state, _] = useMegaStore();
return (
<Switch>
<Match when={state.setup_error}>
<SetupErrorDisplay
initialError={state.setup_error!}
password={state.password}
/>
</Match>
<Match when={true}>{props.children}</Match>
</Switch>
);
}
export function Router() {
});
return (
<SolidRouter
root={(props) => (
<MetaProvider>
<Title>Mutiny Wallet</Title>
<ErrorBoundary fallback={(e) => <ErrorDisplay error={e} />}>
<GlobalListeners />
<Suspense>
<ErrorBoundary
fallback={(e) => <ErrorDisplay error={e} />}
@@ -172,7 +172,6 @@ export function Router() {
<Route path="/profile" component={Profile} />
<Route path="/chat/:id" component={Chat} />
<Route path="/feedback" component={Feedback} />
<Route path="/gift" component={GiftReceive} />
<Route path="/receive" component={Receive} />
<Route path="/redeem" component={Redeem} />
<Route path="/request/:id" component={RequestRoute} />
@@ -191,7 +190,6 @@ export function Router() {
<Route path="/language" component={Language} />
<Route path="/emergencykit" component={EmergencyKit} />
<Route path="/encrypt" component={Encrypt} />
<Route path="/gift" component={Gift} />
<Route path="/plus" component={Plus} />
<Route path="/restore" component={Restore} />
<Route path="/servers" component={Servers} />

View File

@@ -43,14 +43,14 @@ import { MiniFab } from "~/components/Fab";
import { useI18n } from "~/i18n/context";
import { ParsedParams, toParsedParams } from "~/logic/waila";
import { useMegaStore } from "~/state/megaStore";
import { eify, hexpubFromNpub, timeAgo } from "~/utils";
import { createDeepSignal, eify, hexpubFromNpub, timeAgo } from "~/utils";
type CombinedMessagesAndActivity =
| { kind: "message"; content: FakeDirectMessage }
| { kind: "activity"; content: IActivityItem };
// TODO: Use the actual type from MutinyWallet
type FakeDirectMessage = {
export type FakeDirectMessage = {
from: string;
to: string;
message: string;
@@ -70,8 +70,8 @@ function SingleMessage(props: {
counterPartyNpub: string;
counterPartyContactId: string;
}) {
const [state, actions] = useMegaStore();
const network = state.mutiny_wallet?.get_network() || "signet";
const [state, actions, sw] = useMegaStore();
const network = state.network || "signet";
const navigate = useNavigate();
const parsed = createAsync(
@@ -82,7 +82,7 @@ function SingleMessage(props: {
const split_message_by_whitespace = props.dm.message.split(/\s+/g);
for (const word of split_message_by_whitespace) {
if (word.length > 15) {
result = toParsedParams(word, network);
result = await toParsedParams(word, network, sw);
if (result.ok) {
break;
}
@@ -94,9 +94,8 @@ function SingleMessage(props: {
}
if (result.value?.invoice) {
console.log("about to get invoice");
try {
const alreadyPaid = await state.mutiny_wallet?.get_invoice(
const alreadyPaid = await sw.get_invoice(
result.value.invoice
);
if (alreadyPaid?.paid) {
@@ -149,9 +148,10 @@ function SingleMessage(props: {
navWithContactId();
}
function handlePay(invoice: string) {
actions.handleIncomingString(
invoice,
async function handlePay() {
if (!parsed()) return;
await actions.handleIncomingString(
parsed()!.value,
(error) => {
showToast(error);
},
@@ -191,7 +191,7 @@ function SingleMessage(props: {
<Button
intent="blue"
layout="xs"
onClick={() => handlePay(parsed()?.value || "")}
onClick={handlePay}
>
Pay
</Button>
@@ -308,17 +308,17 @@ function FixedChatHeader(props: {
sendToContact: (contact: TagItem) => void;
requestFromContact: (contact: TagItem) => void;
}) {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
async function saveContact(id: string, contact: ContactFormValues) {
console.log("saving contact", id, contact);
const hexpub = await hexpubFromNpub(contact.npub?.trim());
const hexpub = await hexpubFromNpub(sw, contact.npub?.trim());
try {
const existing = state.mutiny_wallet?.get_tag_item(id);
const existing = await sw.get_tag_item(id);
// This shouldn't happen
if (!existing) throw new Error("No existing contact");
await state.mutiny_wallet?.edit_contact(
await sw.edit_contact(
id,
contact.name,
hexpub ? hexpub : undefined,
@@ -337,7 +337,7 @@ function FixedChatHeader(props: {
async function deleteContact(id: string) {
try {
await state.mutiny_wallet?.delete_contact(id);
await sw.delete_contact(id);
} catch (e) {
console.error(e);
showToast(eify(e));
@@ -352,7 +352,7 @@ function FixedChatHeader(props: {
try {
if (!props.contact.npub) throw new Error("No npub");
await state.mutiny_wallet?.follow_npub(props.contact.npub);
await sw.follow_npub(props.contact.npub);
props.refetch();
} catch (e) {
console.error(e);
@@ -366,7 +366,7 @@ function FixedChatHeader(props: {
try {
if (!props.contact.npub) throw new Error("No npub");
await state.mutiny_wallet?.unfollow_npub(props.contact.npub);
await sw.unfollow_npub(props.contact.npub);
props.refetch();
} catch (e) {
console.error(e);
@@ -447,26 +447,16 @@ function FixedChatHeader(props: {
export function Chat() {
const params = useParams();
const [state, actions] = useMegaStore();
const [_state, actions, sw] = useMegaStore();
const [messageValue, setMessageValue] = createSignal("");
const [sending, setSending] = createSignal(false);
const i18n = useI18n();
// const contact = createAsync(async () => {
// try {
// return state.mutiny_wallet?.get_tag_item(params.id);
// } catch (e) {
// console.error("couldn't find contact");
// console.error(e);
// return undefined;
// }
// });
const [contact, { refetch: refetchContact }] = createResource(async () => {
try {
return state.mutiny_wallet?.get_tag_item(params.id);
return await sw.get_tag_item(params.id);
} catch (e) {
console.error("couldn't find contact");
console.error(e);
@@ -474,6 +464,7 @@ export function Chat() {
}
});
// TODO: could probably move this to the web worker and make it even snappier
const [convo, { refetch }] = createResource(
contact,
async (contact?: TagItem) => {
@@ -484,7 +475,7 @@ export function Chat() {
let dms = [] as FakeDirectMessage[];
try {
acts = (await state.mutiny_wallet?.get_label_activity(
acts = (await sw.get_label_activity(
params.id
)) as IActivityItem[];
} catch (e) {
@@ -492,7 +483,7 @@ export function Chat() {
}
try {
dms = (await state.mutiny_wallet?.get_dm_conversation(
dms = (await sw.get_dm_conversation(
contact.npub,
20n,
undefined,
@@ -529,13 +520,14 @@ export function Chat() {
return b_time - a_time; // Descending order
});
console.log("combined activity", combined);
return combined as CombinedMessagesAndActivity[];
} catch (e) {
console.error("error getting convo:", e);
return [] as CombinedMessagesAndActivity[];
}
},
{
storage: createDeepSignal
}
);
@@ -546,10 +538,7 @@ export function Chat() {
const rememberedValue = messageValue();
setMessageValue("");
try {
const dmResult = await state.mutiny_wallet?.send_dm(
npub,
rememberedValue
);
const dmResult = await sw.send_dm(npub, rememberedValue);
console.log("dmResult:", dmResult);
refetch();
} catch (e) {
@@ -567,11 +556,11 @@ export function Chat() {
});
});
function sendToContact(contact?: TagItem) {
async function sendToContact(contact?: TagItem) {
if (!contact) return;
const address = contact.ln_address || contact.lnurl;
if (address) {
actions.handleIncomingString(
await actions.handleIncomingString(
(address || "").trim(),
(error) => {
showToast(error);

View File

@@ -1,5 +1,5 @@
import { useNavigate } from "@solidjs/router";
import { createMemo, createSignal, Show } from "solid-js";
import { createAsync, useNavigate } from "@solidjs/router";
import { createSignal, Show } from "solid-js";
import {
BackLink,
@@ -14,14 +14,14 @@ import { useMegaStore } from "~/state/megaStore";
import { DEFAULT_NOSTR_NAME } from "~/utils";
export function EditProfile() {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
// const i18n = useI18n();
const navigate = useNavigate();
const [saving, setSaving] = createSignal(false);
const originalProfile = createMemo(() => {
const profile = state.mutiny_wallet?.get_nostr_profile();
const originalProfile = createAsync(async () => {
const profile = await sw.get_nostr_profile();
return {
name: profile?.display_name || profile?.name || DEFAULT_NOSTR_NAME,
@@ -37,11 +37,11 @@ export function EditProfile() {
console.log("new profile", profile);
const newProfile = await state.mutiny_wallet?.edit_nostr_profile(
const newProfile = await sw.edit_nostr_profile(
profile.nym ? profile.nym : undefined,
profile.imageUrl ? profile.imageUrl : undefined,
profile.lightningAddress ? profile.lightningAddress : undefined,
originalProfile().nip05 ? originalProfile().nip05 : undefined
originalProfile()?.nip05 ? originalProfile()?.nip05 : undefined
);
console.log("newProfile", newProfile);
@@ -62,9 +62,9 @@ export function EditProfile() {
<Show when={originalProfile()}>
<EditProfileForm
initialProfile={{
nym: originalProfile().name,
lightningAddress: originalProfile().lud16,
imageUrl: originalProfile().picture
nym: originalProfile()?.name,
lightningAddress: originalProfile()?.lud16,
imageUrl: originalProfile()?.picture
}}
onSave={saveProfile}
saving={saving()}

View File

@@ -1,270 +0,0 @@
import { useSearchParams } from "@solidjs/router";
import {
createMemo,
createResource,
createSignal,
Match,
Show,
Suspense,
Switch
} from "solid-js";
import treasureClosed from "~/assets/treasure-closed.png";
import treasure from "~/assets/treasure.gif";
import {
AmountFiat,
AmountSats,
BackLink,
Button,
ButtonLink,
DefaultMain,
FancyCard,
FeesModal,
InfoBox,
Logo,
MutinyWalletGuard,
NavBar,
NiceP,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore";
import { eify } from "~/utils";
function InboundWarning() {
const [state, _] = useMegaStore();
const i18n = useI18n();
const [searchParams] = useSearchParams();
const [inboundCapacity] = createResource(async () => {
try {
const channels = await state.mutiny_wallet?.list_channels();
let inbound = 0n;
for (const channel of channels) {
inbound =
inbound +
BigInt(channel.size) -
BigInt(channel.balance + channel.reserve);
}
return inbound;
} catch (e) {
console.error(e);
return 0n;
}
});
const warningText = createMemo(() => {
if (isNaN(Number(searchParams.amount))) {
return undefined;
}
const amountNumber = Number(searchParams.amount);
const amount = BigInt(amountNumber);
const network = state.mutiny_wallet?.get_network() as Network;
const threshold = network === "bitcoin" ? 100000 : 10000;
const balance =
(state.balance?.lightning || 0n) +
(state.balance?.federation || 0n);
if (balance === 0n && amount < threshold) {
return i18n.t("settings.gift.receive_too_small", {
amount: network === "bitcoin" ? "100,000" : "10,000"
});
}
if (inboundCapacity() && inboundCapacity()! > amount) {
return undefined;
} else {
return i18n.t("settings.gift.setup_fee_lightning");
}
});
return (
<Show when={warningText()}>
<InfoBox accent="blue">
{warningText()} <FeesModal />
</InfoBox>
</Show>
);
}
export function Gift() {
const [state, _] = useMegaStore();
const i18n = useI18n();
const [claimSuccess, setClaimSuccess] = createSignal(false);
const [error, setError] = createSignal<Error>();
const [loading, setLoading] = createSignal(false);
const [searchParams] = useSearchParams();
async function claim() {
const amount = Number(searchParams.amount);
const nwc = searchParams.nwc_uri;
setLoading(true);
if (!nwc) {
throw new Error(i18n.t("settings.gift.something_went_wrong"));
}
try {
const claimResult = await state.mutiny_wallet?.claim_single_use_nwc(
BigInt(amount),
nwc
);
if (claimResult === "Already Claimed") {
throw new Error(i18n.t("settings.gift.already_claimed"));
}
if (
claimResult ===
"Failed to pay invoice: We do not have enough balance to pay the given amount."
) {
throw new Error(i18n.t("settings.gift.sender_is_poor"));
}
// Fallback for any other errors
if (claimResult) {
throw new Error(
i18n.t("settings.gift.sender_generic_error", {
error: claimResult
})
);
}
setClaimSuccess(true);
} catch (e) {
console.error(e);
const err = eify(e);
if (err.message === "Payment timed out.") {
setError(new Error(i18n.t("settings.gift.sender_timed_out")));
} else {
setError(err);
}
} finally {
setLoading(false);
}
}
async function tryAgain() {
setError(undefined);
await claim();
}
return (
<MutinyWalletGuard>
<DefaultMain>
<BackLink />
<Show when={searchParams.nwc_uri && searchParams.amount}>
<VStack>
<FancyCard>
<VStack>
<div class="flex items-start justify-between">
<VStack smallgap>
<span class="text-3xl">
<AmountSats
denominationSize="xl"
amountSats={Number(
searchParams.amount
)}
/>
</span>
<span class="text-xl text-white/70">
<AmountFiat
denominationSize="xl"
amountSats={Number(
searchParams.amount
)}
/>
</span>
</VStack>
<Logo />
</div>
<div
class="relative transition-all duration-500"
classList={{
"grayscale filter opacity-75":
!claimSuccess()
}}
>
<img
src={treasureClosed}
fetchpriority="high"
class="mx-auto w-1/2"
classList={{
hidden: !!claimSuccess()
}}
/>
<img
src={treasure}
fetchpriority="high"
class="mx-auto w-1/2"
classList={{
hidden: !claimSuccess()
}}
/>
</div>
<h2 class="text-center text-3xl font-semibold">
{i18n.t("settings.gift.receive_header")}
</h2>
<NiceP>
{i18n.t(
"settings.gift.receive_description"
)}
</NiceP>
<Show when={!claimSuccess()}>
<Suspense>
<InboundWarning />
</Suspense>
</Show>
<Switch>
<Match when={error()}>
<InfoBox accent="red">
{error()?.message}
</InfoBox>
<ButtonLink href="/" intent="red">
{i18n.t("common.dangit")}
</ButtonLink>
<Button
intent="inactive"
onClick={tryAgain}
loading={loading()}
>
{i18n.t(
"settings.gift.receive_try_again"
)}
</Button>
</Match>
<Match when={claimSuccess()}>
<InfoBox accent="green">
{i18n.t(
"settings.gift.receive_claimed"
)}
</InfoBox>
<ButtonLink href="/" intent="inactive">
{i18n.t("common.nice")}
</ButtonLink>
</Match>
<Match when={true}>
<Button
intent="inactive"
onClick={claim}
loading={loading()}
>
{i18n.t(
"settings.gift.receive_cta"
)}
</Button>
</Match>
</Switch>
</VStack>
</FancyCard>
</VStack>
</Show>
</DefaultMain>
<NavBar activeTab="none" />
</MutinyWalletGuard>
);
}

View File

@@ -1,5 +1,5 @@
import { createAsync, useNavigate } from "@solidjs/router";
import { createMemo, Show, Suspense } from "solid-js";
import { Show, Suspense } from "solid-js";
import {
Circle,
@@ -16,45 +16,37 @@ import {
} from "~/components";
import { Fab } from "~/components/Fab";
import { useMegaStore } from "~/state/megaStore";
import { DEFAULT_NOSTR_NAME } from "~/utils";
export function WalletHeader(props: { loading: boolean }) {
const navigate = useNavigate();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
async function getProfile() {
const profile = state.mutiny_wallet?.get_nostr_profile();
return {
name: profile?.display_name || profile?.name || DEFAULT_NOSTR_NAME,
picture: profile?.picture || undefined,
// TODO: this but for real
lud16: profile?.lud16 || undefined
};
}
const profile = createAsync(() => getProfile());
const profileImage = createMemo(() => {
const profile = createAsync(async () => {
if (props.loading) {
return undefined;
}
if (profile() && profile()!.picture) {
return profile()!.picture;
}
return undefined;
return await sw.get_nostr_profile();
});
return (
<header class="grid grid-cols-[auto_minmax(0,_1fr)_auto] items-center gap-4">
<Suspense
fallback={
<LabelCircle
contact
label={false}
image_url={profileImage()}
image_url={undefined}
onClick={() => navigate("/profile")}
/>
}
>
<LabelCircle
contact
label={false}
image_url={profile()?.picture}
onClick={() => navigate("/profile")}
/>
</Suspense>
<HomeBalance />
<Circle onClick={() => navigate("/settings")}>
<img
@@ -76,7 +68,7 @@ export function WalletHeader(props: { loading: boolean }) {
}
export function Main() {
const [state, _actions] = useMegaStore();
const [state] = useMegaStore();
const navigate = useNavigate();
@@ -90,9 +82,7 @@ export function Main() {
<div class="flex-1" />
</Show>
<Show when={state.load_stage === "done"}>
<Suspense>
<WalletHeader loading={false} />
</Suspense>
<Show when={!state.wallet_loading && !state.safe_mode}>
<SocialActionRow

View File

@@ -1,6 +1,6 @@
import { useNavigate } from "@solidjs/router";
import { createAsync, useNavigate } from "@solidjs/router";
import { AtSign, Edit, Import } from "lucide-solid";
import { createMemo, Show } from "solid-js";
import { createMemo, Show, Suspense } from "solid-js";
import {
BackLink,
@@ -9,6 +9,7 @@ import {
DefaultMain,
LabelCircle,
LightningAddressShower,
LoadingShimmer,
MutinyWalletGuard,
NavBar,
NiceP
@@ -25,13 +26,13 @@ export type UserProfile = {
};
export function Profile() {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const navigate = useNavigate();
const profile = createMemo(() => {
const profile = state.mutiny_wallet?.get_nostr_profile();
const profile = createAsync(async () => {
const profile = await sw.get_nostr_profile();
const userProfile: UserProfile = {
name: profile?.display_name || profile?.name || DEFAULT_NOSTR_NAME,
@@ -43,17 +44,17 @@ export function Profile() {
});
const profileDeleted = createMemo(() => {
return profile().deleted === true || profile().deleted === "true";
return profile()?.deleted === true || profile()?.deleted === "true";
});
const hasMutinyAddress = createMemo(() => {
if (profile().lud16) {
if (profile()?.lud16) {
const hermes = import.meta.env.VITE_HERMES;
if (!hermes) {
return false;
}
const hermesDomain = new URL(hermes).hostname;
const afterAt = profile().lud16!.split("@")[1];
const afterAt = profile()?.lud16!.split("@")[1];
if (afterAt && afterAt.includes(hermesDomain)) {
return true;
}
@@ -70,11 +71,13 @@ export function Profile() {
<LabelCircle
contact
label={false}
image_url={profile().picture}
image_url={profile()?.picture}
size="xl"
/>
<h1 class="text-3xl font-semibold">
<Show when={profile().name}>{profile().name}</Show>
<Show when={profile()?.name}>
{profile()?.name}
</Show>
</h1>
<LightningAddressShower profile={profile()} />
@@ -107,7 +110,7 @@ export function Profile() {
</ButtonCard>
</Show>
</Show>
<Show when={profile() && profile().deleted}>
<Show when={profile() && profile()?.deleted}>
<ButtonCard
onClick={() => navigate("/settings/importprofile")}
>
@@ -119,7 +122,9 @@ export function Profile() {
</div>
</ButtonCard>
</Show>
<Suspense fallback={<LoadingShimmer />}>
<BalanceBox loading={state.wallet_loading} />
</Suspense>
<NavBar activeTab="profile" />
</DefaultMain>
</MutinyWalletGuard>

View File

@@ -47,7 +47,7 @@ import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { eify, objectToSearchParams, vibrateSuccess } from "~/utils";
type OnChainTx = {
export type OnChainTx = {
transaction: {
version: number;
lock_time: number;
@@ -123,7 +123,7 @@ function ReceiveMethodHelp() {
}
export function Receive() {
const [state, actions] = useMegaStore();
const [state, actions, sw] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
@@ -242,10 +242,7 @@ export function Receive() {
// First we try to get both an invoice and an address
try {
console.log("big amount", bigAmount);
const raw = await state.mutiny_wallet?.create_bip21(
bigAmount,
tags
);
const raw = await sw.create_bip21(bigAmount, tags);
// Save the raw info so we can watch the address and invoice
setBip21Raw(raw);
@@ -270,7 +267,7 @@ export function Receive() {
// If we didn't return before this, that means create_bip21 failed
// So now we'll just try and get an address without the invoice
try {
const raw = await state.mutiny_wallet?.get_new_address(tags);
const raw = await sw.get_new_address(tags);
// Save the raw info so we can watch the address
setBip21Raw(raw);
@@ -314,8 +311,7 @@ export function Receive() {
try {
// Lightning invoice might be blank
if (lightning) {
const invoice =
await state.mutiny_wallet?.get_invoice(lightning);
const invoice = await sw.get_invoice(lightning);
// If the invoice has a fees amount that's probably the LSP fee
if (invoice?.fees_paid) {
@@ -330,9 +326,9 @@ export function Receive() {
}
}
const tx = (await state.mutiny_wallet?.check_address(
address
)) as OnChainTx | undefined;
const tx = (await sw.check_address(address)) as
| OnChainTx
| undefined;
if (tx) {
setReceiveState("paid");
@@ -396,7 +392,7 @@ export function Receive() {
onSubmit={getQr}
/>
<ReceiveWarnings
amountSats={amount() || "0"}
amountSats={amount() || 0n}
from_fedi_to_ln={false}
/>
</VStack>

View File

@@ -36,7 +36,7 @@ import { eify, vibrateSuccess } from "~/utils";
type RedeemState = "edit" | "paid";
export function Redeem() {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
@@ -52,8 +52,9 @@ export function Redeem() {
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal<string>("");
function mSatsToSats(mSats: bigint) {
return mSats / 1000n;
function mSatsToSats(mSats: bigint | number) {
const bigMsats = BigInt(mSats);
return bigMsats / 1000n;
}
function clearAll() {
@@ -69,9 +70,7 @@ export function Redeem() {
const [decodedLnurl] = createResource(async () => {
if (state.scan_result) {
if (state.scan_result.lnurl) {
const decoded = await state.mutiny_wallet?.decode_lnurl(
state.scan_result.lnurl
);
const decoded = await sw.decode_lnurl(state.scan_result.lnurl);
return decoded;
}
}
@@ -126,10 +125,7 @@ export function Redeem() {
setLoading(true);
try {
const success = await state.mutiny_wallet?.lnurl_withdraw(
lnurlString(),
amount()
);
const success = await sw.lnurl_withdraw(lnurlString(), amount());
if (!success) {
setError(i18n.t("redeem.lnurl_redeem_failed"));
} else {
@@ -170,7 +166,7 @@ export function Redeem() {
</Show>
</Suspense>
<ReceiveWarnings
amountSats={amount() || "0"}
amountSats={amount() || 0n}
from_fedi_to_ln={false}
/>
<Show when={lnurlAmountText() && !fixedAmount()}>

View File

@@ -22,7 +22,7 @@ import { eify } from "~/utils";
import { DestinationItem } from "./Send";
export function RequestRoute() {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
@@ -46,12 +46,12 @@ export function RequestRoute() {
tags.push(whatForInput().trim());
}
const raw = await state.mutiny_wallet?.create_bip21(amount(), tags);
const raw = await sw.create_bip21(amount(), tags);
if (!raw || !raw.invoice)
throw new Error("Invoice creation failed");
await state.mutiny_wallet?.send_dm(npub, raw.invoice);
await sw.send_dm(npub, raw.invoice);
navigate("/chat/" + params.id);
} catch (e) {
@@ -64,7 +64,7 @@ export function RequestRoute() {
async function getContact(id: string) {
console.log("fetching contact", id);
try {
const contact = state.mutiny_wallet?.get_tag_item(id);
const contact = await sw.get_tag_item(id);
console.log("fetching contact", contact);
// This shouldn't happen
if (!contact) throw new Error("Contact not found");
@@ -103,7 +103,7 @@ export function RequestRoute() {
setAmountSats={setAmount}
onSubmit={handleSubmit}
/>
<ReceiveWarnings amountSats={amount() || "0"} />
<ReceiveWarnings amountSats={amount() || 0n} />
</VStack>
<div class="flex-1" />
<VStack>

View File

@@ -74,7 +74,7 @@ export function Search() {
function ActualSearch(props: { initialValue?: string }) {
const [searchValue, setSearchValue] = createSignal("");
const [debouncedSearchValue, setDebouncedSearchValue] = createSignal("");
const [state, actions] = useMegaStore();
const [_state, actions, sw] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
@@ -88,7 +88,7 @@ function ActualSearch(props: { initialValue?: string }) {
const getContacts = cache(async () => {
try {
const contacts = await state.mutiny_wallet?.get_contacts_sorted();
const contacts = await sw.get_contacts_sorted();
return contacts || ([] as TagItem[]);
} catch (e) {
console.error(e);
@@ -124,7 +124,7 @@ function ActualSearch(props: { initialValue?: string }) {
type SearchState = "notsendable" | "sendable" | "sendableWithContact";
const searchState = createMemo<SearchState>(() => {
const searchState = createAsync<SearchState>(async () => {
if (debouncedSearchValue() === "") {
return "notsendable";
}
@@ -134,7 +134,7 @@ function ActualSearch(props: { initialValue?: string }) {
return "notsendable";
}
let state: SearchState = "notsendable";
actions.handleIncomingString(
await actions.handleIncomingString(
text,
(_error) => {
// noop
@@ -147,6 +147,7 @@ function ActualSearch(props: { initialValue?: string }) {
}
}
);
console.log("params searchState", state);
return state;
});
@@ -160,8 +161,8 @@ function ActualSearch(props: { initialValue?: string }) {
});
}
function handleContinue() {
actions.handleIncomingString(
async function handleContinue() {
await actions.handleIncomingString(
debouncedSearchValue().trim(),
(error) => {
showToast(error);
@@ -181,16 +182,17 @@ function ActualSearch(props: { initialValue?: string }) {
);
}
const profileDeleted = createMemo(
() => state.mutiny_wallet?.get_nostr_profile().deleted
);
const profileDeleted = createAsync(async () => {
const profile = await sw.get_nostr_profile();
return profile?.deleted;
});
// TODO this is mostly copy pasted from chat, could be a shared util maybe
function navToSend(contact?: TagItem) {
async function navToSend(contact?: TagItem) {
if (!contact) return;
const address = contact.ln_address || contact.lnurl;
if (address) {
actions.handleIncomingString(
await actions.handleIncomingString(
(address || "").trim(),
(error) => {
showToast(error);
@@ -208,9 +210,9 @@ function ActualSearch(props: { initialValue?: string }) {
}
}
function sendToContact(contact: TagItem) {
async function sendToContact(contact: TagItem) {
if (profileDeleted()) {
navToSend(contact);
await navToSend(contact);
} else {
navWithSearchValue(`/chat/${contact.id}`);
}
@@ -224,11 +226,11 @@ function ActualSearch(props: { initialValue?: string }) {
);
if (existingContact) {
sendToContact(existingContact);
await sendToContact(existingContact);
return;
}
const contactId = await state.mutiny_wallet?.create_new_contact(
const contactId = await sw.create_new_contact(
contact.name,
contact.npub ? contact.npub.trim() : undefined,
contact.ln_address ? contact.ln_address.trim() : undefined,
@@ -240,7 +242,7 @@ function ActualSearch(props: { initialValue?: string }) {
throw new Error("no contact id returned");
}
const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
const tagItem = await sw.get_tag_item(contactId);
if (!tagItem) {
throw new Error("no contact returned");
@@ -249,9 +251,9 @@ function ActualSearch(props: { initialValue?: string }) {
// if the new contact has an npub, send to chat
// otherwise, send to send page
if (tagItem.npub) {
sendToContact(tagItem);
await sendToContact(tagItem);
} else if (tagItem.ln_address) {
actions.handleIncomingString(
await actions.handleIncomingString(
tagItem.ln_address,
() => {},
() => {
@@ -259,7 +261,7 @@ function ActualSearch(props: { initialValue?: string }) {
}
);
} else if (tagItem.lnurl) {
actions.handleIncomingString(
await actions.handleIncomingString(
tagItem.lnurl,
() => {},
() => {
@@ -293,14 +295,14 @@ function ActualSearch(props: { initialValue?: string }) {
const trimText = text.trim();
setSearchValue(trimText);
parsePaste(trimText);
await parsePaste(trimText);
} catch (e) {
console.error(e);
}
}
function parsePaste(text: string) {
actions.handleIncomingString(
async function parsePaste(text: string) {
await actions.handleIncomingString(
text,
(error) => {
showToast(error);
@@ -354,6 +356,7 @@ function ActualSearch(props: { initialValue?: string }) {
</button>
</Show>
</div>
<Suspense>
<div class="flex-0 flex w-full">
<Show when={searchState() !== "notsendable"}>
<Button intent="green" onClick={handleContinue}>
@@ -370,7 +373,9 @@ function ActualSearch(props: { initialValue?: string }) {
{(contact) => (
<ContactButton
contact={contact}
onClick={() => sendToContact(contact)}
onClick={() =>
sendToContact(contact)
}
/>
)}
</For>
@@ -393,6 +398,7 @@ function ActualSearch(props: { initialValue?: string }) {
<div class="h-4" />
</VStack>
</Show>
</Suspense>
</>
);
}
@@ -402,10 +408,11 @@ function GlobalSearch(props: {
sendToContact: (contact: TagItem) => void;
foundNpubs: (string | undefined)[];
}) {
const [_state, _actions, sw] = useMegaStore();
const hexpubs = createMemo(() => {
const hexpubs: Set<string> = new Set();
for (const npub of props.foundNpubs) {
hexpubFromNpub(npub)
hexpubFromNpub(sw, npub)
.then((h) => {
if (h) {
hexpubs.add(h);
@@ -425,7 +432,7 @@ function GlobalSearch(props: {
try {
// Handling case when value starts with "npub"
if (args.value?.toLowerCase().startsWith("npub")) {
const hexpub = await hexpubFromNpub(args.value);
const hexpub = await hexpubFromNpub(sw, args.value);
if (!hexpub) return [];
const profile = await actuallyFetchNostrProfile(hexpub);
@@ -491,23 +498,25 @@ function SingleContact(props: {
contact: PseudoContact;
sendToContact: (contact: TagItem) => void;
}) {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
async function createContactFromSearchResult(contact: PseudoContact) {
try {
const contactId = await state.mutiny_wallet?.create_new_contact(
const contactId = await sw.create_new_contact(
contact.name,
contact.hexpub ? contact.hexpub : undefined,
contact.hexpub ? contact.hexpub : "",
contact.ln_address ? contact.ln_address : undefined,
undefined,
contact.image_url ? contact.image_url : undefined
);
console.log("contactId", contactId);
if (!contactId) {
throw new Error("no contact id returned");
}
const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
const tagItem = await sw.get_tag_item(contactId);
if (!tagItem) {
throw new Error("no contact returned");

View File

@@ -1,5 +1,10 @@
import { MutinyInvoice, TagItem } from "@mutinywallet/mutiny-wasm";
import { useLocation, useNavigate, useSearchParams } from "@solidjs/router";
import {
createAsync,
useLocation,
useNavigate,
useSearchParams
} from "@solidjs/router";
import { Eye, EyeOff, Link, X, Zap } from "lucide-solid";
import {
createEffect,
@@ -158,7 +163,7 @@ export function DestinationItem(props: {
}
export function Send() {
const [state, actions] = useMegaStore();
const [state, actions, sw] = useMegaStore();
const navigate = useNavigate();
const [params, setParams] = useSearchParams();
const i18n = useI18n();
@@ -220,8 +225,8 @@ export function Send() {
}
// TODO: can I dedupe this from the search page?
function parsePaste(text: string) {
actions.handleIncomingString(
async function parsePaste(text: string) {
await actions.handleIncomingString(
text,
(error) => {
showToast(error);
@@ -233,9 +238,10 @@ export function Send() {
);
}
// TODO: do we actually use this anywhere?
// send?invoice=... need to check for wallet because we can't parse until we have the wallet
createEffect(() => {
if (params.invoice && state.mutiny_wallet) {
if (params.invoice && state.load_stage === "done") {
parsePaste(params.invoice);
setParams({ invoice: undefined });
}
@@ -300,7 +306,7 @@ export function Send() {
});
// Rerun every time the amount changes if we're onchain
const feeEstimate = createMemo(() => {
const feeEstimate = createAsync(async () => {
if (
source() === "onchain" &&
amountSats() &&
@@ -310,16 +316,16 @@ export function Send() {
try {
// If max we want to use the sweep fee estimator
if (isMax()) {
return state.mutiny_wallet?.estimate_sweep_tx_fee(
address()!
);
return await sw.estimate_sweep_tx_fee(address()!);
}
return state.mutiny_wallet?.estimate_tx_fee(
const estimate = await sw.estimate_tx_fee(
address()!,
amountSats(),
undefined
);
console.log("estimate", estimate);
return estimate;
} catch (e) {
setError(eify(e).message);
}
@@ -367,15 +373,15 @@ export function Send() {
// A ParsedParams with an invoice in it
function processInvoice(source: ParsedParams & { invoice: string }) {
state.mutiny_wallet
?.decode_invoice(source.invoice!)
sw.decode_invoice(source.invoice!)
.then((invoice) => {
if (!invoice) return;
if (invoice.expire <= Date.now() / 1000) {
navigate("/search");
throw new Error(i18n.t("send.error_expired"));
}
if (invoice?.amount_sats) {
if (invoice.amount_sats) {
setAmountSats(invoice.amount_sats);
setIsAmtEditable(false);
}
@@ -396,8 +402,7 @@ export function Send() {
// A ParsedParams with an lnurl in it
function processLnurl(source: ParsedParams & { lnurl: string }) {
setDecodingLnUrl(true);
state.mutiny_wallet
?.decode_lnurl(source.lnurl)
sw.decode_lnurl(source.lnurl)
.then((lnurlParams) => {
setDecodingLnUrl(false);
if (lnurlParams.tag === "payRequest") {
@@ -470,7 +475,7 @@ export function Send() {
sentDetails.destination = bolt11;
// If the invoice has sats use that, otherwise we pass the user-defined amount
if (invoice()?.amount_sats) {
const payment = await state.mutiny_wallet?.pay_invoice(
const payment = await sw.pay_invoice(
bolt11,
undefined,
tags
@@ -479,7 +484,7 @@ export function Send() {
sentDetails.payment_hash = payment?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
} else {
const payment = await state.mutiny_wallet?.pay_invoice(
const payment = await sw.pay_invoice(
bolt11,
amountSats(),
tags
@@ -489,7 +494,7 @@ export function Send() {
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "lightning" && nodePubkey()) {
const payment = await state.mutiny_wallet?.keysend(
const payment = await sw.keysend(
nodePubkey()!,
amountSats(),
undefined, // todo add optional keysend message
@@ -509,7 +514,7 @@ export function Send() {
visibility() !== "Not Available" && contact()?.npub
? contact()?.npub
: undefined;
const payment = await state.mutiny_wallet?.lnurl_pay(
const payment = await sw.lnurl_pay(
lnurlp()!,
amountSats(),
zapNpub, // zap_npub
@@ -530,17 +535,14 @@ export function Send() {
if (isMax()) {
// If we're trying to send the max amount, use the sweep method instead of regular send
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const txid = await state.mutiny_wallet?.sweep_wallet(
address()!,
tags
);
const txid = await sw.sweep_wallet(address()!, tags);
sentDetails.amount = amountSats();
sentDetails.destination = address();
sentDetails.txid = txid;
sentDetails.fee_estimate = feeEstimate() ?? 0;
} else if (payjoinEnabled()) {
const txid = await state.mutiny_wallet?.send_payjoin(
const txid = await sw.send_payjoin(
originalScan()!,
amountSats(),
tags
@@ -551,7 +553,7 @@ export function Send() {
sentDetails.fee_estimate = feeEstimate() ?? 0;
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const txid = await state.mutiny_wallet?.send_to_address(
const txid = await sw.send_to_address(
address()!,
amountSats(),
tags
@@ -665,7 +667,7 @@ export function Send() {
async function getContact(id: string) {
console.log("fetching contact", id);
try {
const contact = state.mutiny_wallet?.get_tag_item(id);
const contact = await sw.get_tag_item(id);
console.log("fetching contact", contact);
// This shouldn't happen
if (!contact) throw new Error("Contact not found");
@@ -773,7 +775,6 @@ export function Send() {
<AmountEditable
initialAmountSats={amountSats()}
setAmountSats={setAmountInput}
fee={feeEstimate()?.toString()}
onSubmit={() =>
sendButtonDisabled() ? undefined : handleSend()
}
@@ -791,7 +792,6 @@ export function Send() {
<AmountEditable
initialAmountSats={amountSats()}
setAmountSats={setAmountInput}
fee={feeEstimate()?.toString()}
frozenAmount={true}
onSubmit={() =>
sendButtonDisabled() ? undefined : handleSend()
@@ -801,6 +801,7 @@ export function Send() {
setChosenMethod={setSourceFromMethod}
/>
</Show>
<Suspense>
<Show when={feeEstimate()}>
<FeeDisplay
amountSats={amountSats().toString()}
@@ -808,6 +809,7 @@ export function Send() {
maxAmountSats={maxAmountSats()}
/>
</Show>
</Suspense>
<Show when={isHodlInvoice()}>
<InfoBox accent="red">
<p>{i18n.t("send.hodl_invoice_warning")}</p>

View File

@@ -1,6 +1,6 @@
import { createForm, required } from "@modular-forms/solid";
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { MutinyChannel } from "@mutinywallet/mutiny-wasm";
import { createAsync, useNavigate } from "@solidjs/router";
import {
createMemo,
createResource,
@@ -8,6 +8,7 @@ import {
For,
Match,
Show,
Suspense,
Switch
} from "solid-js";
@@ -33,7 +34,6 @@ import {
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore";
import { eify, vibrateSuccess } from "~/utils";
@@ -50,7 +50,7 @@ type ChannelOpenDetails = {
};
export function Swap() {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
@@ -100,9 +100,7 @@ export function Swap() {
};
const getPeers = async () => {
return (await state.mutiny_wallet?.list_peers()) as Promise<
MutinyPeer[]
>;
return await sw?.list_peers();
};
const [peers, { refetch }] = createResource(getPeers);
@@ -114,7 +112,7 @@ export function Swap() {
try {
const peerConnectString = values.peer.trim();
await state.mutiny_wallet?.connect_to_peer(peerConnectString);
await sw.connect_to_peer(peerConnectString);
await refetch();
@@ -156,12 +154,11 @@ export function Swap() {
}
if (isMax()) {
const new_channel =
await state.mutiny_wallet?.sweep_all_to_channel(peer);
const new_channel = await sw.sweep_all_to_channel(peer);
setChannelOpenResult({ channel: new_channel });
} else {
const new_channel = await state.mutiny_wallet?.open_channel(
const new_channel = await sw.open_channel(
peer,
amountSats()
);
@@ -182,7 +179,7 @@ export function Swap() {
const balance =
(state.balance?.confirmed || 0n) +
(state.balance?.unconfirmed || 0n);
const network = state.mutiny_wallet?.get_network() as Network;
const network = state.network || "signet";
if (network === "bitcoin") {
return (
@@ -199,12 +196,12 @@ export function Swap() {
}
};
const amountWarning = () => {
const amountWarning = createAsync(async () => {
if (amountSats() === 0n || !!channelOpenResult()) {
return undefined;
}
const network = state.mutiny_wallet?.get_network() as Network;
const network = state.network || "signet";
if (network === "bitcoin" && amountSats() < 100000n) {
return i18n.t("swap.channel_too_small", { amount: "100,000" });
@@ -224,7 +221,7 @@ export function Swap() {
}
return undefined;
};
});
function calculateMaxOnchain() {
return (
@@ -241,12 +238,12 @@ export function Swap() {
return amountSats() === calculateMaxOnchain();
});
const feeEstimate = createMemo(() => {
const max = calculateMaxOnchain();
const feeEstimate = createAsync(async () => {
const max = maxOnchain();
// If max we want to use the sweep fee estimator
if (amountSats() > 0n && amountSats() === max) {
try {
return state.mutiny_wallet?.estimate_sweep_channel_open_fee();
return await sw.estimate_sweep_channel_open_fee();
} catch (e) {
console.error(e);
return undefined;
@@ -255,7 +252,7 @@ export function Swap() {
if (amountSats() > 0n) {
try {
return state.mutiny_wallet?.estimate_tx_fee(
return await sw.estimate_tx_fee(
CHANNEL_FEE_ESTIMATE_ADDRESS,
amountSats(),
undefined
@@ -328,6 +325,7 @@ export function Swap() {
})}
</p>
<div class="text-center text-sm text-white/70">
<Suspense>
<AmountFiat
amountSats={
Number(
@@ -340,6 +338,7 @@ export function Swap() {
)
}
/>
</Suspense>
</div>
</div>
<hr class="w-16 bg-m-grey-400" />
@@ -425,7 +424,6 @@ export function Swap() {
<AmountEditable
initialAmountSats={amountSats()}
setAmountSats={setAmountSats}
fee={feeEstimate()?.toString()}
activeMethod={{
method: "onchain",
maxAmountSats: maxOnchain()
@@ -437,6 +435,7 @@ export function Swap() {
}
]}
/>
<Suspense>
<Show when={feeEstimate() && amountSats() > 0n}>
<FeeDisplay
amountSats={amountSats().toString()}
@@ -444,9 +443,14 @@ export function Swap() {
maxAmountSats={maxOnchain()}
/>
</Show>
</Suspense>
<Suspense>
<Show when={amountWarning() && amountSats() > 0n}>
<InfoBox accent={"red"}>{amountWarning()}</InfoBox>
<InfoBox accent={"red"}>
{amountWarning()}
</InfoBox>
</Show>
</Suspense>
</VStack>
<div class="flex-1" />
<VStack>

View File

@@ -1,6 +1,13 @@
import { FedimintSweepResult } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { createMemo, createSignal, Match, Show, Switch } from "solid-js";
import {
createMemo,
createSignal,
Match,
Show,
Suspense,
Switch
} from "solid-js";
import {
AmountEditable,
@@ -31,7 +38,7 @@ type SweepResultDetails = {
};
export function SwapLightning() {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
@@ -63,17 +70,11 @@ export function SwapLightning() {
setFeeEstimateWarning(undefined);
if (isMax()) {
const result =
await state.mutiny_wallet?.sweep_federation_balance(
undefined
);
const result = await sw.sweep_federation_balance(undefined);
setSweepResult({ result: result });
} else {
const result =
await state.mutiny_wallet?.sweep_federation_balance(
amountSats()
);
const result = await sw.sweep_federation_balance(amountSats());
setSweepResult({ result: result });
}
@@ -128,8 +129,7 @@ export function SwapLightning() {
setLoading(true);
setFeeEstimateWarning(undefined);
const fee =
await state.mutiny_wallet?.estimate_sweep_federation_fee(
const fee = await sw.estimate_sweep_federation_fee(
isMax() ? undefined : amountSats()
);
@@ -201,11 +201,13 @@ export function SwapLightning() {
})}
</p>
<div class="text-center text-sm text-white/70">
<Suspense>
<AmountFiat
amountSats={Number(
sweepResult()?.result?.amount
)}
/>
</Suspense>
</div>
</div>
<hr class="w-16 bg-m-grey-400" />
@@ -236,7 +238,7 @@ export function SwapLightning() {
]}
/>
<ReceiveWarnings
amountSats={amountSats() || "0"}
amountSats={amountSats() || 0n}
from_fedi_to_ln={true}
/>
<Show
@@ -246,11 +248,13 @@ export function SwapLightning() {
{amountWarning()}
</InfoBox>
</Show>
<Suspense>
<Show when={feeEstimateWarning()}>
<InfoBox accent={"red"}>
{feeEstimateWarning()}
</InfoBox>
</Show>
</Suspense>
</VStack>
<div class="flex-1" />
<VStack>

View File

@@ -1,15 +1,14 @@
export * from "./[...404]";
export * from "./Feedback";
export * from "./Gift";
export * from "./Main";
export * from "./Receive";
export * from "./Scanner";
export * from "./Send";
export * from "./Swap";
export * from "./SwapLightning";
export * from "./Search";
export * from "./Redeem";
export * from "./Profile";
export * from "./Chat";
export * from "./Request";
export * from "./EditProfile";
export * from "./Swap";
export * from "./SwapLightning";

View File

@@ -1,4 +1,4 @@
import { useNavigate } from "@solidjs/router";
import { createAsync, useNavigate } from "@solidjs/router";
import { createEffect, createSignal, Show } from "solid-js";
import {
@@ -53,7 +53,7 @@ function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
export function Backup() {
const i18n = useI18n();
const [store, actions] = useMegaStore();
const [_store, actions, sw] = useMegaStore();
const navigate = useNavigate();
const [hasSeenBackup, setHasSeenBackup] = createSignal(false);
@@ -67,6 +67,8 @@ export function Backup() {
setLoading(false);
}
const words = createAsync(async () => await sw.show_seed());
return (
<MutinyWalletGuard>
<DefaultMain>
@@ -79,7 +81,7 @@ export function Backup() {
<NiceP>{i18n.t("settings.backup.warning_one")}</NiceP>
<NiceP>{i18n.t("settings.backup.warning_two")}</NiceP>
<SeedWords
words={store.mutiny_wallet?.show_seed() || ""}
words={words() || ""}
setHasSeen={setHasSeenBackup}
/>
<Show when={hasSeenBackup()}>

View File

@@ -30,7 +30,6 @@ import {
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore";
import { createDeepSignal, eify, mempoolTxUrl } from "~/utils";
@@ -100,8 +99,8 @@ function splitChannelNumbers(channel: MutinyChannel): {
function SingleChannelItem(props: { channel: MutinyChannel; online: boolean }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const network = state.mutiny_wallet?.get_network() as Network;
const [state, _actions, sw] = useMegaStore();
const network = state.network;
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [confirmLoading, setConfirmLoading] = createSignal(false);
@@ -115,11 +114,7 @@ function SingleChannelItem(props: { channel: MutinyChannel; online: boolean }) {
if (!props.channel.outpoint) return;
setConfirmLoading(true);
const forceClose = !props.online;
await state.mutiny_wallet?.close_channel(
props.channel.outpoint,
forceClose,
false
);
await sw.close_channel(props.channel.outpoint, forceClose, false);
} catch (e) {
console.error(e);
showToast(eify(e));
@@ -180,12 +175,11 @@ function SingleChannelItem(props: { channel: MutinyChannel; online: boolean }) {
function LiquidityMonitor() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
async function listChannels() {
try {
const channels: MutinyChannel[] | undefined =
await state.mutiny_wallet?.list_channels();
const channels = await sw.list_channels();
if (!channels)
return {
@@ -232,22 +226,24 @@ function LiquidityMonitor() {
return (
<Switch>
<Match when={channelInfo()?.channelCount}>
<Match
when={channelInfo.latest && channelInfo.latest?.channelCount}
>
<VStack>
<Card>
<NiceP>
{i18n.t("settings.channels.have_channels")}{" "}
{channelInfo()?.channelCount}{" "}
{channelInfo()?.channelCount === 1
{channelInfo.latest?.channelCount}{" "}
{channelInfo.latest?.channelCount === 1
? i18n.t("settings.channels.have_channels_one")
: i18n.t(
"settings.channels.have_channels_many"
)}
</NiceP>{" "}
<BalanceBar
inbound={Number(channelInfo()?.inbound) || 0}
reserve={Number(channelInfo()?.reserve) || 0}
outbound={Number(channelInfo()?.outbound) || 0}
inbound={Number(channelInfo.latest?.inbound) || 0}
reserve={Number(channelInfo.latest?.reserve) || 0}
outbound={Number(channelInfo.latest?.outbound) || 0}
/>
<TinyText>
{i18n.t("settings.channels.inbound_outbound_tip")}
@@ -256,7 +252,7 @@ function LiquidityMonitor() {
{i18n.t("settings.channels.reserve_tip")}
</TinyText>
</Card>
<Show when={channelInfo()?.online?.length}>
<Show when={channelInfo.latest?.online?.length}>
<SettingsCard>
<Collapser
title={i18n.t(
@@ -265,7 +261,7 @@ function LiquidityMonitor() {
activityLight="on"
>
<VStack>
<For each={channelInfo()?.online}>
<For each={channelInfo.latest?.online}>
{(channel) => (
<SingleChannelItem
channel={channel}
@@ -277,7 +273,7 @@ function LiquidityMonitor() {
</Collapser>
</SettingsCard>
</Show>
<Show when={channelInfo()?.offline?.length}>
<Show when={channelInfo.latest?.offline?.length}>
<SettingsCard>
<Collapser
title={i18n.t(
@@ -286,7 +282,7 @@ function LiquidityMonitor() {
activityLight="off"
>
<VStack>
<For each={channelInfo()?.offline}>
<For each={channelInfo.latest?.offline}>
{(channel) => (
<SingleChannelItem
channel={channel}

View File

@@ -77,7 +77,7 @@ function NwcDetails(props: {
onEdit?: () => void;
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [confirmOpen, setConfirmOpen] = createSignal(false);
@@ -87,7 +87,7 @@ function NwcDetails(props: {
async function deleteProfile() {
try {
await state.mutiny_wallet?.delete_nwc_profile(props.profile.index);
await sw.delete_nwc_profile(props.profile.index);
setConfirmOpen(false);
props.refetch();
} catch (e) {
@@ -187,11 +187,12 @@ function NwcDetails(props: {
function Nwc() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
async function fetchNwcProfiles() {
try {
const profiles = await state.mutiny_wallet?.get_nwc_profiles();
const profiles = await sw.get_nwc_profiles();
console.log("profiles", profiles);
if (!profiles) return [];
return profiles;

View File

@@ -26,7 +26,7 @@ type EncryptPasswordForm = {
export function Encrypt() {
const i18n = useI18n();
const [store, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [error, setError] = createSignal<Error>();
const [loading, setLoading] = createSignal(false);
@@ -56,7 +56,7 @@ export function Encrypt() {
const handleFormSubmit = async (f: EncryptPasswordForm) => {
setLoading(true);
try {
await store.mutiny_wallet?.change_password(
await sw.change_password(
f.existingPassword === "" ? undefined : f.existingPassword,
f.password === "" ? undefined : f.password
);

View File

@@ -1,323 +0,0 @@
import {
createForm,
getValue,
required,
reset,
setValue,
SubmitHandler
} from "@modular-forms/solid";
import { NwcProfile } from "@mutinywallet/mutiny-wasm";
import {
createEffect,
createResource,
createSignal,
For,
Match,
Show,
Suspense,
Switch
} from "solid-js";
import {
AmountEditable,
BackPop,
Button,
Collapser,
ConfirmDialog,
DefaultMain,
InfoBox,
IntegratedQr,
LargeHeader,
LoadingSpinner,
MutinyPlusCta,
MutinyWalletGuard,
NavBar,
NiceP,
SettingsCard,
TextField,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { eify, isFreeGiftingDay } from "~/utils";
import { baseUrlAccountingForNative } from "~/utils/baseUrl";
import { createDeepSignal } from "~/utils/deepSignal";
type CreateGiftForm = {
name: string;
amount: string;
};
function SingleGift(props: { profile: NwcProfile; onDelete?: () => void }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const network = state.mutiny_wallet?.get_network();
const baseUrl = baseUrlAccountingForNative(network);
const sharableUrl = () => baseUrl.concat(props.profile.url_suffix || "");
const amount = () => props.profile.budget_amount?.toString() || "0";
const [confirmOpen, setConfirmOpen] = createSignal(false);
const handleConfirmDelete = async () => {
try {
await state.mutiny_wallet?.delete_nwc_profile(props.profile.index);
setConfirmOpen(false);
props.onDelete && props.onDelete();
} catch (e) {
console.error(e);
}
};
return (
<VStack>
<IntegratedQr
amountSats={amount()}
value={sharableUrl()}
kind="gift"
/>
<Button intent="red" onClick={() => setConfirmOpen(true)}>
{i18n.t("settings.gift.send_delete_button")}
</Button>
<ConfirmDialog
loading={false}
open={confirmOpen()}
onConfirm={handleConfirmDelete}
onCancel={() => setConfirmOpen(false)}
>
{i18n.t("settings.gift.send_delete_confirm")}
</ConfirmDialog>
</VStack>
);
}
function ExistingGifts() {
const [state, _actions] = useMegaStore();
const [giftNWCProfiles, { refetch }] = createResource(async () => {
try {
const profiles = await state.mutiny_wallet?.get_nwc_profiles();
if (!profiles) return [];
const filteredForGifts = profiles.filter((p) => p.tag === "Gift");
return filteredForGifts;
} catch (e) {
console.error(e);
}
});
return (
<Show when={giftNWCProfiles() && giftNWCProfiles()!.length > 0}>
<SettingsCard title={"Existing Gifts"}>
<For each={giftNWCProfiles()}>
{(profile) => (
<Collapser title={profile.name} activityLight={"on"}>
<SingleGift profile={profile} onDelete={refetch} />
</Collapser>
)}
</For>
</SettingsCard>
</Show>
);
}
export function Gift() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [_error, setError] = createSignal<Error>();
const [giftResult, setGiftResult] = createSignal<NwcProfile>();
const [giftForm, { Form, Field }] = createForm<CreateGiftForm>({
initialValues: {
name: "",
amount: "100000"
}
});
function resetGifting() {
reset(giftForm);
setGiftResult(undefined);
}
const handleSubmit: SubmitHandler<CreateGiftForm> = async (
f: CreateGiftForm
) => {
const nwc_name = f.name.trim();
const amount = Number(f.amount);
try {
const profile = await state.mutiny_wallet?.create_single_use_nwc(
nwc_name,
BigInt(amount)
);
setGiftResult(profile);
} catch (e) {
console.error(e);
setError(eify(e));
}
};
async function fetchProfile(gift?: NwcProfile) {
if (!gift) return;
try {
const fresh = await state.mutiny_wallet?.get_nwc_profile(
gift.index
);
return fresh;
} catch (e) {
console.error(e);
// If the gift is not found it means it's been deleted because it was redeemed
return undefined;
}
}
const [freshProfile, { refetch }] = createResource(
() => giftResult(),
fetchProfile,
{
storage: createDeepSignal
}
);
createEffect(() => {
// Should re-run after every sync
if (!state.is_syncing) {
refetch();
}
});
const lessThanMinChannelSize = () => {
return Number(getValue(giftForm, "amount")) < 100000;
};
const selfHosted = state.settings?.selfhosted === "true";
const freeDay = isFreeGiftingDay();
const canGift = state.mutiny_plus || selfHosted || freeDay;
return (
<MutinyWalletGuard>
<DefaultMain>
<BackPop default="/settings" />
<Show when={!canGift}>
<VStack>
<LargeHeader>
{i18n.t("settings.gift.send_header")}
</LargeHeader>
<NiceP>{i18n.t("settings.gift.need_plus")}</NiceP>
<MutinyPlusCta />
</VStack>
</Show>
<Show when={giftResult()}>
<VStack>
<Switch>
<Match when={!freshProfile()}>
<LargeHeader>
{i18n.t(
"settings.gift.send_header_claimed"
)}
</LargeHeader>
<NiceP>
{i18n.t("settings.gift.send_claimed")}
</NiceP>
</Match>
<Match when={true}>
<LargeHeader>
{i18n.t(
"settings.gift.send_sharable_header"
)}
</LargeHeader>
<NiceP>
{i18n.t("settings.gift.send_instructions")}
</NiceP>
<InfoBox accent="green">
{i18n.t("settings.gift.send_tip")}
</InfoBox>
<SingleGift
profile={freshProfile()!}
onDelete={resetGifting}
/>
</Match>
</Switch>
<Button intent="green" onClick={resetGifting}>
{i18n.t("settings.gift.send_another")}
</Button>
</VStack>
</Show>
<Show when={!giftResult() && canGift}>
<LargeHeader>
{i18n.t("settings.gift.send_header")}
</LargeHeader>
<Form onSubmit={handleSubmit}>
<VStack>
<NiceP>
{i18n.t("settings.gift.send_explainer")}
</NiceP>
<Field name="amount">
{(field) => (
<AmountEditable
initialAmountSats={field.value || "0"}
setAmountSats={(newAmount) =>
setValue(
giftForm,
"amount",
newAmount.toString()
)
}
/>
)}
</Field>
<Field
name="name"
validate={[
required(
i18n.t(
"settings.gift.send_name_required"
)
)
]}
>
{(field, props) => (
<TextField
{...props}
value={field.value}
error={field.error}
label={i18n.t(
"settings.gift.send_name_label"
)}
placeholder="Satoshi Nakamoto"
/>
)}
</Field>
<Show when={lessThanMinChannelSize()}>
<InfoBox accent="green">
{i18n.t("settings.gift.send_small_warning")}
</InfoBox>
</Show>
<Button
intent="blue"
type="submit"
loading={giftForm.submitting}
>
{i18n.t("settings.gift.send_cta")}
</Button>
</VStack>
</Form>
<Suspense fallback={<LoadingSpinner />}>
<ExistingGifts />
</Suspense>
</Show>
</DefaultMain>
<NavBar activeTab="settings" />
</MutinyWalletGuard>
);
}

View File

@@ -1,7 +1,13 @@
import { BackLink, DefaultMain, ImportNsecForm } from "~/components";
import {
BackLink,
DefaultMain,
ImportNsecForm,
MutinyWalletGuard
} from "~/components";
export function ImportProfileSettings() {
return (
<MutinyWalletGuard>
<DefaultMain>
<BackLink title="Back" href="/settings/nostrkeys" />
<div class="mx-auto flex max-w-[20rem] flex-1 flex-col items-center gap-4">
@@ -16,5 +22,6 @@ export function ImportProfileSettings() {
<div class="flex-1" />
</div>
</DefaultMain>
</MutinyWalletGuard>
);
}

View File

@@ -6,7 +6,7 @@ import {
reset,
SubmitHandler
} from "@modular-forms/solid";
import { useNavigate } from "@solidjs/router";
import { createAsync, useNavigate } from "@solidjs/router";
import { Users } from "lucide-solid";
import {
createMemo,
@@ -49,7 +49,7 @@ const validateLowerCase = (value?: string) => {
// todo(paul) put this somewhere else
function HermesForm(props: { onSubmit: (name: string) => void }) {
const [state, _] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const [error, setError] = createSignal<Error>();
const [success, setSuccess] = createSignal("");
@@ -70,17 +70,16 @@ function HermesForm(props: { onSubmit: (name: string) => void }) {
setError(undefined);
try {
const name = f.name.trim().toLowerCase();
const available =
await state.mutiny_wallet?.check_available_lnurl_name(name);
const available = await sw.check_available_lnurl_name(name);
if (!available) {
throw new Error("Name already taken");
}
await state.mutiny_wallet?.reserve_lnurl_name(name);
await sw.reserve_lnurl_name(name);
console.log("lnurl name reserved:", name);
const formattedName = `${name}@${hermesDomain}`;
const _ = await state.mutiny_wallet?.edit_nostr_profile(
const _ = await sw.edit_nostr_profile(
undefined,
undefined,
// lnurl
@@ -144,14 +143,14 @@ function HermesForm(props: { onSubmit: (name: string) => void }) {
export function LightningAddress() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const navigate = useNavigate();
const [error, setError] = createSignal<Error>();
const [settingLnAddress, setSettingLnAddress] = createSignal(false);
const [lnurlName] = createResource(async () => {
try {
const name = await state.mutiny_wallet?.check_lnurl_name();
const name = await sw.check_lnurl_name();
return name;
} catch (e) {
setError(eify(e));
@@ -172,10 +171,10 @@ export function LightningAddress() {
}
});
const profileLnAddress = createMemo(() => {
const profileLnAddress = createAsync(async () => {
if (lnurlName()) {
const profile = state.mutiny_wallet?.get_nostr_profile();
if (profile?.lud16) {
const profile = await sw.get_nostr_profile();
if (profile && profile.lud16) {
return profile.lud16;
}
}
@@ -187,7 +186,7 @@ export function LightningAddress() {
setSettingLnAddress(true);
setError(undefined);
const _ = await state.mutiny_wallet?.edit_nostr_profile(
const _ = await sw.edit_nostr_profile(
undefined,
undefined,
newAddress,

View File

@@ -89,7 +89,7 @@ export function AddFederationForm(props: {
browseOnly?: boolean;
}) {
const i18n = useI18n();
const [state, actions] = useMegaStore();
const [_state, actions, sw] = useMegaStore();
const navigate = useNavigate();
const [error, setError] = createSignal<Error>();
const [success, setSuccess] = createSignal("");
@@ -122,8 +122,7 @@ export function AddFederationForm(props: {
const [federations] = createResource(async () => {
try {
const federations: DiscoveredFederation[] =
await state.mutiny_wallet?.discover_federations();
const federations = await sw.discover_federations();
return federations;
} catch (e) {
console.error(e);
@@ -139,8 +138,7 @@ export function AddFederationForm(props: {
try {
console.log("Adding federation:", inviteCode);
setLoadingFederation(inviteCode);
const newFederation =
await state.mutiny_wallet?.new_federation(inviteCode);
const newFederation = await sw.new_federation(inviteCode);
console.log("New federation added:", newFederation);
break;
} catch (e) {
@@ -345,7 +343,7 @@ export function AddFederationForm(props: {
}
function RecommendButton(props: { fed: MutinyFederationIdentity }) {
const [state] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const [recommendLoading, setRecommendLoading] = createSignal(false);
// This is just some local state that makes it feel like they've recommended it
@@ -354,8 +352,7 @@ function RecommendButton(props: { fed: MutinyFederationIdentity }) {
const [recommendedByMe, { refetch }] = createResource(async () => {
try {
const hasRecommended =
await state.mutiny_wallet?.has_recommended_federation(
const hasRecommended = await sw.has_recommended_federation(
props.fed.federation_id
);
return hasRecommended;
@@ -368,7 +365,7 @@ function RecommendButton(props: { fed: MutinyFederationIdentity }) {
async function recommendFederation() {
setRecommendLoading(true);
try {
const event_id = await state.mutiny_wallet?.recommend_federation(
const event_id = await sw.recommend_federation(
props.fed.invite_code
);
console.log("Recommended federation: ", event_id);
@@ -383,9 +380,7 @@ function RecommendButton(props: { fed: MutinyFederationIdentity }) {
async function deleteRecommendation() {
setRecommendLoading(true);
try {
await state.mutiny_wallet?.delete_federation_recommendation(
props.fed.federation_id
);
await sw.delete_federation_recommendation(props.fed.federation_id);
setRecommended(false);
refetch();
} catch (e) {
@@ -430,14 +425,12 @@ function FederationListItem(props: {
balance?: bigint;
}) {
const i18n = useI18n();
const [state, actions] = useMegaStore();
const [_state, actions, sw] = useMegaStore();
async function removeFederation() {
setConfirmLoading(true);
try {
await state.mutiny_wallet?.remove_federation(
props.fed.federation_id
);
await sw.remove_federation(props.fed.federation_id);
await actions.refreshFederations();
} catch (e) {
console.error(e);
@@ -522,12 +515,11 @@ function FederationListItem(props: {
export function ManageFederations() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
const [balances, { refetch }] = createResource(async () => {
try {
const balances =
await state.mutiny_wallet?.get_federation_balances();
const balances = await sw.get_federation_balances();
return balances?.balances || [];
} catch (e) {
console.error(e);

View File

@@ -23,7 +23,7 @@ import { useMegaStore } from "~/state/megaStore";
function DeleteAccount() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
async function confirmDelete() {
setConfirmOpen(true);
@@ -35,7 +35,7 @@ function DeleteAccount() {
async function deleteNostrAccount() {
setConfirmLoading(true);
try {
await state.mutiny_wallet?.delete_profile();
await sw.delete_profile();
// Remove the nsec from secure storage if it exists
await SecureStoragePlugin.clear();
window.location.href = "/";
@@ -113,14 +113,11 @@ function UnlinkAccount() {
export function NostrKeys() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const npub = createAsync(async () => state.mutiny_wallet?.get_npub());
const nsec = createAsync(async () => state.mutiny_wallet?.export_nsec());
const profile = () => state.mutiny_wallet?.get_nostr_profile();
// @ts-expect-error we're checking for an extension
const windowHasNostr = window.nostr && window.nostr.getPublicKey;
const npub = createAsync(async () => await sw.get_npub());
const nsec = createAsync(async () => await sw.export_nsec());
const profile = createAsync(async () => await sw.get_nostr_profile());
const [nsecInSecureStorage] = createResource(async () => {
try {
@@ -144,7 +141,7 @@ export function NostrKeys() {
</ExternalLink>
</NiceP>
<Switch>
<Match when={profile() && !profile().deleted}>
<Match when={profile() && !profile()?.deleted}>
<FancyCard>
<VStack>
<div class="w-[10rem] self-center rounded bg-white p-[1rem]">
@@ -169,7 +166,6 @@ export function NostrKeys() {
</Show>
</VStack>
</FancyCard>
<Show when={!windowHasNostr}>
<A
href="/settings/importprofile"
class="flex w-full items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2 text-m-grey-350 no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70"
@@ -177,19 +173,16 @@ export function NostrKeys() {
<Import class="w-4" />
{i18n.t("settings.nostr_keys.import_profile")}
</A>
</Show>
<Switch>
<Match when={nsecInSecureStorage()}>
<UnlinkAccount />
</Match>
<Match
when={!nsecInSecureStorage() && !windowHasNostr}
>
<Match when={!nsecInSecureStorage()}>
<DeleteAccount />
</Match>
</Switch>
</Match>
<Match when={profile() && profile().deleted}>
<Match when={profile() && profile()?.deleted}>
<A
href="/settings/importprofile"
class="flex w-full items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2 text-m-grey-350 no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70"

View File

@@ -56,7 +56,7 @@ function Perks(props: { alreadySubbed?: boolean }) {
function PlusCTA() {
const i18n = useI18n();
const [state, actions] = useMegaStore();
const [state, actions, sw] = useMegaStore();
const [subbing, setSubbing] = createSignal(false);
const [confirmOpen, setConfirmOpen] = createSignal(false);
@@ -65,7 +65,7 @@ function PlusCTA() {
const [planDetails] = createResource(async () => {
try {
const plans = await state.mutiny_wallet?.get_subscription_plans();
const plans = await sw.get_subscription_plans();
console.log("plans:", plans);
if (!plans) return undefined;
return plans[0];
@@ -82,17 +82,12 @@ function PlusCTA() {
if (planDetails()?.id === undefined || planDetails()?.id === null)
throw new Error(i18n.t("settings.plus.error_no_plan"));
const invoice = await state.mutiny_wallet?.subscribe_to_plan(
planDetails().id
);
const invoice = await sw.subscribe_to_plan(planDetails()!.id);
if (!invoice?.bolt11)
throw new Error(i18n.t("settings.plus.error_failure"));
await state.mutiny_wallet?.pay_subscription_invoice(
invoice?.bolt11,
true
);
await sw.pay_subscription_invoice(invoice?.bolt11, true);
await vibrateSuccess();
@@ -112,7 +107,7 @@ function PlusCTA() {
return (
(state.balance?.lightning || 0n) +
(state.balance?.federation || 0n) >
planDetails().amount_sat
planDetails()!.amount_sat
);
};
@@ -126,7 +121,7 @@ function PlusCTA() {
</strong>{" "}
{i18n.t("settings.plus.sats_per_month", {
amount: Number(
planDetails().amount_sat
planDetails()!.amount_sat
).toLocaleString()
})}
</NiceP>
@@ -137,7 +132,7 @@ function PlusCTA() {
<TinyText>
{i18n.t("settings.plus.lightning_balance", {
amount: Number(
planDetails().amount_sat
planDetails()!.amount_sat
).toLocaleString()
})}
</TinyText>

View File

@@ -9,7 +9,6 @@ import {
SubmitHandler,
validate
} from "@modular-forms/solid";
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { LucideClipboard } from "lucide-solid";
import { createSignal, For, Show, splitProps } from "solid-js";
@@ -79,7 +78,7 @@ function SeedTextField(props: TextFieldProps) {
export function TwelveWordsEntry() {
const i18n = useI18n();
const [state, actions] = useMegaStore();
const [state, actions, sw] = useMegaStore();
const [error, setError] = createSignal<Error>();
const [mnemnoic, setMnemonic] = createSignal<string>();
@@ -129,17 +128,16 @@ export function TwelveWordsEntry() {
try {
setConfirmLoading(true);
if (state.mutiny_wallet) {
if (state.load_stage === "done") {
console.log("Mutiny wallet loaded, stopping");
try {
await state.mutiny_wallet.stop();
actions.dropMutinyWallet();
await sw.stop();
} catch (e) {
console.error(e);
}
}
await MutinyWallet.restore_mnemonic(mnemnoic() || "", "");
await sw.restore_mnemonic(mnemnoic() || "", "");
actions.setHasBackedUp();

View File

@@ -7,7 +7,6 @@ export * from "./Currency";
export * from "./Language";
export * from "./EmergencyKit";
export * from "./Encrypt";
export * from "./Gift";
export * from "./Plus";
export * from "./Restore";
export * from "./Servers";

View File

@@ -12,7 +12,7 @@ import { useMegaStore } from "~/state/megaStore";
import { DEFAULT_NOSTR_NAME } from "~/utils";
export function NewProfile() {
const [state, _actions] = useMegaStore();
const [_state, _actions, sw] = useMegaStore();
const i18n = useI18n();
const [creating, setCreating] = createSignal(false);
@@ -23,7 +23,7 @@ export function NewProfile() {
async function handleSkip() {
setSkipping(true);
// set up an empty profile so we at least have some kind0 event
const profile = await state.mutiny_wallet?.setup_new_profile(
const profile = await sw.setup_new_profile(
DEFAULT_NOSTR_NAME,
undefined,
undefined,
@@ -38,7 +38,7 @@ export function NewProfile() {
async function createProfile(p: EditableProfile) {
setCreating(true);
try {
const profile = await state.mutiny_wallet?.setup_new_profile(
const profile = await sw.setup_new_profile(
p.nym ? p.nym : DEFAULT_NOSTR_NAME,
p.imageUrl ? p.imageUrl : undefined,
undefined,

View File

@@ -39,9 +39,8 @@ export function Setup() {
return (
<DefaultMain>
{/* <LargeHeader>Setup</LargeHeader> */}
<div class="flex flex-1 flex-col items-center justify-between gap-4">
<div class="flex-[2]" />
<div class="flex-1" />
<div class="flex flex-col items-center gap-4">
<img
id="mutiny-logo"

View File

@@ -1,12 +1,8 @@
/* @refresh reload */
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
import {
MutinyBalance,
MutinyWallet,
TagItem
} from "@mutinywallet/mutiny-wasm";
import { MutinyBalance, TagItem } from "@mutinywallet/mutiny-wasm";
import { useNavigate, useSearchParams } from "@solidjs/router";
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
import { Remote } from "comlink";
import {
createContext,
onCleanup,
@@ -20,10 +16,10 @@ import { checkBrowserCompatibility } from "~/logic/browserCompatibility";
import {
doubleInitDefense,
getSettings,
initializeWasm,
MutinyWalletSettingStrings,
setSettings,
setupMutinyWallet
Network,
setSettings
// setupMutinyWallet
} from "~/logic/mutinyWalletSetup";
import { ParsedParams, toParsedParams } from "~/logic/waila";
import { MutinyFederationIdentity } from "~/routes/settings";
@@ -35,8 +31,6 @@ import {
USD_OPTION
} from "~/utils";
const MegaStoreContext = createContext<MegaStore>();
type LoadStage =
| "fresh"
| "checking_double_init"
@@ -45,69 +39,22 @@ type LoadStage =
| "setup"
| "done";
type MegaStore = [
{
mutiny_wallet?: MutinyWallet;
deleting: boolean;
scan_result?: ParsedParams;
balance?: MutinyBalance;
is_syncing?: boolean;
last_sync?: number;
price_sync_backoff_multiple?: number;
price: number;
fiat: Currency;
lang?: string;
has_backed_up: boolean;
wallet_loading: boolean;
setup_error?: Error;
is_pwa: boolean;
existing_tab_detected: boolean;
subscription_timestamp?: number;
readonly mutiny_plus: boolean;
needs_password: boolean;
password?: string;
load_stage: LoadStage;
settings?: MutinyWalletSettingStrings;
safe_mode?: boolean;
preferredInvoiceType: "unified" | "lightning" | "onchain";
testflightPromptDismissed: boolean;
should_zap_hodl: boolean;
federations?: MutinyFederationIdentity[];
balanceView: "sats" | "fiat" | "hidden";
},
{
setup(password?: string): Promise<void>;
deleteMutinyWallet(): Promise<void>;
setScanResult(scan_result: ParsedParams | undefined): void;
sync(): Promise<void>;
setHasBackedUp(): void;
listTags(): Promise<TagItem[]>;
checkForSubscription(justPaid?: boolean): Promise<void>;
fetchPrice(fiat: Currency): Promise<number>;
saveFiat(fiat: Currency): void;
saveLanguage(lang: string): void;
setPreferredInvoiceType(
type: "unified" | "lightning" | "onchain"
): void;
handleIncomingString(
str: string,
onError: (e: Error) => void,
onSuccess: (value: ParsedParams) => void
): void;
setTestFlightPromptDismissed(): void;
toggleHodl(): void;
dropMutinyWallet(): void;
refreshFederations(): Promise<void>;
cycleBalanceView(): void;
}
];
export type WalletWorker = Remote<typeof import("../workers/walletWorker")>;
export const Provider: ParentComponent = (props) => {
export const makeMegaStoreContext = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
// Not actually a shared worker, but it's the same code
const sw = new ComlinkWorker<typeof import("../workers/walletWorker")>(
new URL("../workers/walletWorker", import.meta.url),
{
type: "module"
}
);
const [state, setState] = createStore({
mutiny_wallet: undefined as MutinyWallet | undefined,
network: undefined as Network | undefined,
deleting: false,
scan_result: undefined as ParsedParams | undefined,
price: 0,
@@ -115,7 +62,7 @@ export const Provider: ParentComponent = (props) => {
? (JSON.parse(localStorage.getItem("fiat_currency")!) as Currency)
: USD_OPTION,
has_backed_up: localStorage.getItem("has_backed_up") === "true",
balance: undefined as MutinyBalance | undefined,
balance: undefined as Partial<MutinyBalance> | undefined,
last_sync: undefined as number | undefined,
price_sync_backoff_multiple: 1,
is_syncing: false,
@@ -146,7 +93,7 @@ export const Provider: ParentComponent = (props) => {
const actions = {
async checkForSubscription(justPaid?: boolean): Promise<void> {
try {
const timestamp = await state.mutiny_wallet?.check_subscribed();
const timestamp = await sw.check_subscribed();
// Check that timestamp is a number
if (timestamp && !isNaN(Number(timestamp))) {
@@ -160,21 +107,27 @@ export const Provider: ParentComponent = (props) => {
console.error(e);
}
},
async preSetup(): Promise<void> {
async preSetup(): Promise<boolean> {
try {
// If we're already in an error state there should be no reason to continue
if (state.setup_error) {
throw state.setup_error;
}
// If there's already a mutiny wallet in state abort!
if (state.mutiny_wallet) {
setState({
setup_error: new Error(
"Existing Mutiny Wallet already running, aborting setup"
)
});
return;
setState({ wallet_loading: true });
await this.checkForExistingTab();
if (state.existing_tab_detected) {
return false;
}
console.log("checking for browser compatibility");
try {
await checkBrowserCompatibility();
} catch (e) {
setState({ setup_error: eify(e) });
return false;
}
setState({
@@ -183,11 +136,22 @@ export const Provider: ParentComponent = (props) => {
});
await doubleInitDefense();
setState({ load_stage: "downloading" });
await initializeWasm();
await sw.initializeWasm();
setState({ load_stage: "checking_for_existing_wallet" });
const existing = await sw.has_node_manager();
if (!existing && !searchParams.skip_setup) {
navigate("/setup");
return false;
}
return true;
} catch (e) {
console.error(e);
setState({ setup_error: eify(e) });
return false;
}
},
async setup(password?: string): Promise<void> {
@@ -224,24 +188,29 @@ export const Provider: ParentComponent = (props) => {
}
}, 1000);
const mutinyWallet = await setupMutinyWallet(
let nsec;
// get nsec from secure storage
try {
const value = await SecureStoragePlugin.get({
key: "nsec"
});
nsec = value.value;
} catch (e) {
console.log("No nsec stored");
}
const success = await sw.setupMutinyWallet(
settings,
password,
state.safe_mode,
state.should_zap_hodl
state.should_zap_hodl,
nsec
);
// Done with the timeout shenanigans
clearInterval(interval);
// I've never managed to trigger this but it's just some extra safety I guess
if (!mutinyWallet) {
setState({
setup_error: new Error(
"Failed to initialize Mutiny Wallet"
)
});
return;
if (!success) {
throw new Error("Failed to initialize mutiny wallet");
}
// Give other components access to settings via the store
@@ -250,21 +219,32 @@ export const Provider: ParentComponent = (props) => {
// If we get this far then we don't need the password anymore
setState({ needs_password: false });
// Get network
const network = await sw.get_network();
// Get balance
const balance = await mutinyWallet.get_balance();
const balance = await sw.get_balance();
// Get federations
const federations =
(await mutinyWallet.list_federations()) as MutinyFederationIdentity[];
(await sw.list_federations()) as MutinyFederationIdentity[];
setState({
mutiny_wallet: mutinyWallet,
wallet_loading: false,
load_stage: "done",
balance,
federations
federations,
network: network as Network
});
// Timestamp our initialization for double init defense
sessionStorage.setItem(
"MUTINY_WALLET_INITIALIZED",
Date.now().toString()
);
console.log("Wallet initialized");
await actions.postSetup();
} catch (e) {
console.error(e);
@@ -277,7 +257,7 @@ export const Provider: ParentComponent = (props) => {
}
},
async postSetup(): Promise<void> {
if (!state.mutiny_wallet) {
if (!sw) {
console.error(
"Unable to run post setup, no mutiny_wallet is set"
);
@@ -286,8 +266,7 @@ export const Provider: ParentComponent = (props) => {
// Check if we're subscribed and update the timestamp
try {
const timestamp = await state.mutiny_wallet.check_subscribed();
const timestamp = await sw.check_subscribed();
// Check that timestamp is a number
if (timestamp && !isNaN(Number(timestamp))) {
setState({ subscription_timestamp: Number(timestamp) });
@@ -319,9 +298,9 @@ export const Provider: ParentComponent = (props) => {
...prevState,
deleting: true
}));
if (state.mutiny_wallet) {
await state.mutiny_wallet?.stop();
await state.mutiny_wallet?.delete_all();
if (sw) {
await sw.stop();
await sw.delete_all();
}
} catch (e) {
console.error(e);
@@ -346,9 +325,9 @@ export const Provider: ParentComponent = (props) => {
},
async sync(): Promise<void> {
try {
if (state.mutiny_wallet && !state.is_syncing) {
if (sw && !state.is_syncing) {
setState({ is_syncing: true });
const newBalance = await state.mutiny_wallet?.get_balance();
const newBalance = await sw.get_balance();
try {
setState({
balance: newBalance,
@@ -376,7 +355,7 @@ export const Provider: ParentComponent = (props) => {
return price;
} else {
try {
price = await state.mutiny_wallet?.get_bitcoin_price(
price = await sw.get_bitcoin_price(
fiat.value.toLowerCase() || "usd"
);
return price;
@@ -395,7 +374,7 @@ export const Provider: ParentComponent = (props) => {
},
async listTags(): Promise<TagItem[] | undefined> {
try {
return state.mutiny_wallet?.get_tag_items();
return sw.get_tag_items();
} catch (e) {
console.error(e);
return [];
@@ -416,18 +395,13 @@ export const Provider: ParentComponent = (props) => {
setPreferredInvoiceType(type: "unified" | "lightning" | "onchain") {
setState({ preferredInvoiceType: type });
},
handleIncomingString(
async handleIncomingString(
str: string,
onError: (e: Error) => void,
onSuccess: (value: ParsedParams) => void
): void {
): Promise<void> {
try {
const url = new URL(str);
if (url && url.pathname.startsWith("/gift")) {
navigate(url.pathname + url.search);
return;
}
if (url && url.pathname.startsWith("/settings/plus")) {
navigate(url.pathname + url.search);
return;
@@ -436,9 +410,10 @@ export const Provider: ParentComponent = (props) => {
// If it's not a URL, we'll just continue with normal parsing
}
const network = state.mutiny_wallet?.get_network() || "signet";
const result = toParsedParams(str || "", network);
if (!result.ok) {
const network = state.network || "signet";
const result = await toParsedParams(str || "", network, sw);
if (!result || !result.ok) {
if (onError) {
onError(result.error);
}
@@ -489,12 +464,8 @@ export const Provider: ParentComponent = (props) => {
localStorage.setItem("should_zap_hodl", should_zap_hodl.toString());
setState({ should_zap_hodl });
},
dropMutinyWallet() {
setState({ mutiny_wallet: undefined });
},
async refreshFederations() {
const federations =
(await state.mutiny_wallet?.list_federations()) as MutinyFederationIdentity[];
const federations = await sw.list_federations();
setState({ federations });
},
cycleBalanceView() {
@@ -508,23 +479,8 @@ export const Provider: ParentComponent = (props) => {
localStorage.setItem("balanceView", "sats");
setState({ balanceView: "sats" });
}
}
};
onCleanup(() => {
console.warn("Parent Component is being unmounted!!!");
state.mutiny_wallet
?.stop()
.then(() => {
console.warn("Successfully stopped mutiny wallet");
sessionStorage.removeItem("MUTINY_WALLET_INITIALIZED");
})
.catch((e) => {
console.error("Error stopping mutiny wallet", e);
});
});
async function checkForExistingTab() {
},
async checkForExistingTab() {
// Set up existing tab detector
const channel = new BroadcastChannel("tab-detector");
@@ -551,67 +507,43 @@ export const Provider: ParentComponent = (props) => {
}
};
}
};
const [params, _] = useSearchParams();
return [state, actions, sw] as const;
};
type MegaStoreContextType = ReturnType<typeof makeMegaStoreContext>;
export const MegaStoreContext = createContext<MegaStoreContextType>();
export const useMegaStore = () => useContext(MegaStoreContext)!;
export const Provider: ParentComponent = (props) => {
const [state, actions, sw] = makeMegaStoreContext();
onMount(async () => {
await checkForExistingTab();
if (state.existing_tab_detected) {
return;
}
console.log("checking for browser compatibility");
try {
await checkBrowserCompatibility();
} catch (e) {
setState({ setup_error: eify(e) });
return;
}
await actions.preSetup();
setState({ load_stage: "checking_for_existing_wallet" });
const existing = await MutinyWallet.has_node_manager();
if (!existing && !params.skip_setup) {
navigate("/setup");
return;
}
// Setup catches its own errors and sets state itself
console.log("running setup node manager");
const shouldSetup = await actions.preSetup();
console.log("Should run setup?", shouldSetup);
if (
!state.mutiny_wallet &&
shouldSetup &&
sw &&
!state.existing_tab_detected &&
!state.deleting &&
!state.setup_error &&
!state.existing_tab_detected
!state.setup_error
) {
await actions.setup();
} else {
console.warn("setup aborted");
return;
}
// After we have the mutiny wallet we still need to check for subscription and sync nostr
// await actions.postSetup();
console.log("node manager setup done");
});
const store = [state, actions] as MegaStore;
onCleanup(async () => {
console.warn("Parent Component is being unmounted!!!");
await sw.stop();
console.warn("Successfully stopped mutiny wallet");
sessionStorage.removeItem("MUTINY_WALLET_INITIALIZED");
});
return (
<MegaStoreContext.Provider value={store}>
<MegaStoreContext.Provider value={[state, actions, sw]}>
{props.children}
</MegaStoreContext.Provider>
);
};
export function useMegaStore() {
// This is a trick to narrow the typescript types: https://docs.solidjs.com/references/api-reference/component-apis/createContext
const context = useContext(MegaStoreContext);
if (!context) {
throw new Error("useMegaStore: cannot find a MegaStoreContext");
}
return context;
}

View File

@@ -1,4 +1,4 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { WalletWorker } from "~/state/megaStore";
import { Currency } from "./currencies";
@@ -9,18 +9,17 @@ import { Currency } from "./currencies";
* @param {Currency} fiat - Takes {@link Currency} object options to determine how to format the amount input
*/
export function satsToFiat(
export async function satsToFiat(
amount: number | undefined,
price: number,
fiat: Currency
): string {
fiat: Currency,
sw: WalletWorker
): Promise<string> {
if (typeof amount !== "number" || isNaN(amount)) {
return "";
}
try {
const btc = MutinyWallet.convert_sats_to_btc(
BigInt(Math.floor(amount))
);
const btc = await sw.convert_sats_to_btc(BigInt(Math.floor(amount)));
const fiatPrice = btc * price;
const roundedFiat = Math.round(fiatPrice);
if (
@@ -45,18 +44,17 @@ export function satsToFiat(
* @param {Currency} fiat - Takes {@link Currency} object options to determine how to format the amount input
*/
export function satsToFormattedFiat(
export async function satsToFormattedFiat(
amount: number | undefined,
price: number,
fiat: Currency
): string {
fiat: Currency,
sw: WalletWorker
): Promise<string> {
if (typeof amount !== "number" || isNaN(amount)) {
return "";
}
try {
const btc = MutinyWallet.convert_sats_to_btc(
BigInt(Math.floor(amount))
);
const btc = await sw.convert_sats_to_btc(BigInt(Math.floor(amount)));
const fiatPrice = btc * price;
//Handles currencies not supported by .toLocaleString() like BTC
//Returns a string with a currency symbol and a number with decimals equal to the maxFractionalDigits
@@ -82,17 +80,18 @@ export function satsToFormattedFiat(
}
}
export function fiatToSats(
export async function fiatToSats(
amount: number | undefined,
price: number,
formatted: boolean
): string {
formatted: boolean,
sw: WalletWorker
): Promise<string> {
if (typeof amount !== "number" || isNaN(amount)) {
return "";
}
try {
const btc = price / amount;
const sats = MutinyWallet.convert_btc_to_sats(btc);
const sats = await sw.convert_btc_to_sats(btc);
if (formatted) {
return parseInt(sats.toString()).toLocaleString();
} else {

View File

@@ -1,7 +1,6 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { ResourceFetcher } from "solid-js";
import { useMegaStore } from "~/state/megaStore";
import { useMegaStore, WalletWorker } from "~/state/megaStore";
import {
getPrimalImageUrl,
hexpubFromNpub,
@@ -71,7 +70,7 @@ function getZapKind(event: NostrEvent): "public" | "private" | "anonymous" {
async function simpleZapFromEvent(
event: NostrEvent,
wallet: MutinyWallet
sw: WalletWorker
): Promise<SimpleZapItem | undefined> {
if (event.kind === 9735 && event.tags?.length > 0) {
const to = findByTag(event.tags, "p") || "";
@@ -94,8 +93,8 @@ async function simpleZapFromEvent(
if (bolt11) {
try {
// We hardcode the "bitcoin" network because we don't have a good source of mutinynet zaps
const decoded = await wallet.decode_invoice(bolt11, "bitcoin");
if (decoded.amount_sats) {
const decoded = await sw.decode_invoice(bolt11, "bitcoin");
if (decoded?.amount_sats) {
amount = decoded.amount_sats;
} else {
console.log("no amount in decoded invoice");
@@ -181,7 +180,7 @@ export const fetchZaps: ResourceFetcher<
until?: number;
}
> = async (npub, info) => {
const [state, _actions] = useMegaStore();
const [state, _actions, sw] = useMegaStore();
try {
console.log("fetching zaps for:", npub);
@@ -197,16 +196,18 @@ export const fetchZaps: ResourceFetcher<
// Only have to ask the relays for follows one time
if (follows.length === 0) {
const contacts = await state.mutiny_wallet?.get_contacts_sorted();
const contacts = await sw.get_contacts_sorted();
const hexpubs = [];
if (contacts) {
for (const contact of contacts) {
if (contact.npub) {
const hexpub = await hexpubFromNpub(contact.npub);
const hexpub = await hexpubFromNpub(sw, contact.npub);
if (hexpub) {
hexpubs.push(hexpub);
}
}
}
}
follows = hexpubs;
}
@@ -239,10 +240,7 @@ export const fetchZaps: ResourceFetcher<
if (object.kind === 9735) {
// console.log("got a 9735 object", object);
try {
const event = await simpleZapFromEvent(
object,
state.mutiny_wallet!
);
const event = await simpleZapFromEvent(object, sw);
// Only add it if it's a valid zap (not undefined)
if (event) {

View File

@@ -1,4 +1,4 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { WalletWorker } from "~/state/megaStore";
export type NostrTag = string[];
export declare enum NostrKind {
@@ -45,6 +45,7 @@ export declare enum NostrKind {
}
export async function hexpubFromNpub(
sw: WalletWorker,
npub?: string
): Promise<string | undefined> {
if (!npub) {
@@ -55,7 +56,7 @@ export async function hexpubFromNpub(
}
try {
const hexpub = await MutinyWallet.npub_to_hexpub(npub);
const hexpub = await sw?.npub_to_hexpub(npub);
return hexpub;
} catch (err) {
console.error(err);

1
src/vite-env.d.ts vendored
View File

@@ -1 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-comlink/client" />

1574
src/workers/walletWorker.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import child from "node:child_process";
import path from "node:path";
import { defineConfig } from "vite";
import { comlink } from "vite-plugin-comlink";
import { VitePWA, VitePWAOptions } from "vite-plugin-pwa";
import solid from "vite-plugin-solid";
import wasm from "vite-plugin-wasm";
@@ -40,7 +41,11 @@ export default defineConfig({
allow: [".."]
}
},
plugins: [wasm(), solid(), VitePWA(pwaOptions)],
plugins: [comlink(), wasm(), solid(), VitePWA(pwaOptions)],
worker: {
plugins: () => [comlink(), wasm()],
format: "es"
},
define: {
"import.meta.env.__COMMIT_HASH__": JSON.stringify(commitHash),
"import.meta.env.__RELEASE_VERSION__": JSON.stringify(
@@ -69,6 +74,9 @@ export default defineConfig({
"@capacitor/toast"
],
// This is necessary because otherwise `vite dev` can't find the wasm
exclude: ["@mutinywallet/mutiny-wasm"]
exclude: ["@mutinywallet/mutiny-wasm"],
esbuildOptions: {
target: "esnext"
}
}
});