From 623ba643091014d5a7bbcbc2987ba83c696ed273 Mon Sep 17 00:00:00 2001
From: Flemmli97 <34157027+Flemmli97@users.noreply.github.com>
Date: Fri, 20 Dec 2024 13:29:06 +0100
Subject: [PATCH] feat(wasm): Import/export account (#893)
---
.../components/settings/OrderedPhrase.svelte | 21 +-
src/lib/elements/Input/FileInput.svelte | 4 +-
src/lib/elements/Input/Input.svelte | 6 +-
src/lib/lang/en.json | 18 ++
src/lib/layouts/login/Entrypoint.svelte | 8 +-
src/lib/layouts/login/ImportAccount.svelte | 201 ++++++++++++++++++
src/lib/layouts/login/Unlock.svelte | 6 +
src/lib/wasm/HandleWarpErrors.ts | 3 +
src/lib/wasm/MultipassStore.ts | 24 +++
src/lib/wasm/TesseractStore.ts | 4 +
src/lib/wasm/WarpStore.ts | 13 +-
src/routes/auth/+page.svelte | 6 +-
src/routes/settings/developer/+page.svelte | 2 +-
src/routes/settings/profile/+page.svelte | 43 +++-
14 files changed, 344 insertions(+), 15 deletions(-)
create mode 100644 src/lib/layouts/login/ImportAccount.svelte
diff --git a/src/lib/components/settings/OrderedPhrase.svelte b/src/lib/components/settings/OrderedPhrase.svelte
index 9b5dd7064..a782fbd19 100644
--- a/src/lib/components/settings/OrderedPhrase.svelte
+++ b/src/lib/components/settings/OrderedPhrase.svelte
@@ -2,10 +2,13 @@
import { fade } from "svelte/transition"
import { animationDelay, animationDuration } from "$lib/globals/animations"
import { Text, Loader } from "$lib/elements"
+ import Input from "$lib/elements/Input/Input.svelte"
+ import type { Writable } from "svelte/store"
export let number: number = 0
export let word: string = "UNKNOWN"
export let loading: boolean = false
+ export let editable: boolean = false
@@ -18,12 +21,16 @@
{/if}
-
+
{#if loading}
{:else}
- {word}
+ {#if editable}
+
+ {:else}
+ {word}
+ {/if}
{/if}
@@ -64,6 +71,16 @@
display: inline-block;
flex: 1;
padding: var(--padding-minimal) var(--padding);
+ &.editable {
+ padding: 0;
+ }
+ }
+
+ :global(.input-group > .input-container) {
+ border: none;
+ }
+ :global(.input-group > .input-container::selection) {
+ border: none;
}
}
diff --git a/src/lib/elements/Input/FileInput.svelte b/src/lib/elements/Input/FileInput.svelte
index 483e2f501..86446f0d1 100644
--- a/src/lib/elements/Input/FileInput.svelte
+++ b/src/lib/elements/Input/FileInput.svelte
@@ -3,6 +3,8 @@
export let hidden: boolean = false
export let clss: string = ""
+ export let allowed: string | undefined = undefined
+ export let multiple: boolean = true
const dispatch: EventDispatcher> = createEventDispatcher()
@@ -24,4 +26,4 @@
}
-
+
diff --git a/src/lib/elements/Input/Input.svelte b/src/lib/elements/Input/Input.svelte
index 19f85d411..839a342e5 100644
--- a/src/lib/elements/Input/Input.svelte
+++ b/src/lib/elements/Input/Input.svelte
@@ -25,6 +25,7 @@
export let rich: boolean = false
export let autoFocus: boolean = false
export let rules: InputRules = new InputRules()
+ export let noCapitalize: boolean = false
let errorMessage: string = ""
@@ -179,6 +180,8 @@
+ autofocus={isAndroidOriOS() ? isKeyboardOpened : autoFocus}
+ on:paste />
{#if errorMessage}
diff --git a/src/lib/lang/en.json b/src/lib/lang/en.json
index 65acbd084..6492f6148 100644
--- a/src/lib/lang/en.json
+++ b/src/lib/lang/en.json
@@ -276,6 +276,16 @@
"new": "Create New Account",
"import": "Import Account",
"profile": "Create new profile"
+ },
+ "import": {
+ "title": "Import Account",
+ "description": "Import your account from remote or a backup file",
+ "passphrase": "Add passphrase",
+ "remote": "Import from remote",
+ "file": "Import from file",
+ "warning": "By continuing your old account will be overwritten!",
+ "fail": "Failed to import account",
+ "fail.null": "Failed to create identity during account import"
}
}
},
@@ -370,6 +380,14 @@
"remove": "Your recovery phrase will not be stored anymore and will be removed. Make sure to save the phrase! This change is irreversible!",
"remove.yes": "I understand",
"remove.no": "Cancel"
+ },
+ "export": {
+ "label": "Export",
+ "description": "Export your account manually to remote or a file",
+ "remote": "Remote",
+ "file": "File",
+ "fail": "Failed to export account",
+ "success": "Successfully exported account to remote"
}
},
"calling": {
diff --git a/src/lib/layouts/login/Entrypoint.svelte b/src/lib/layouts/login/Entrypoint.svelte
index 51bb5b7e7..3326a3de6 100644
--- a/src/lib/layouts/login/Entrypoint.svelte
+++ b/src/lib/layouts/login/Entrypoint.svelte
@@ -63,11 +63,9 @@
- {#if get(SettingsStore.state).devmode}
-
- {/if}
+
diff --git a/src/lib/layouts/login/ImportAccount.svelte b/src/lib/layouts/login/ImportAccount.svelte
new file mode 100644
index 000000000..977d8b60d
--- /dev/null
+++ b/src/lib/layouts/login/ImportAccount.svelte
@@ -0,0 +1,201 @@
+
+
+{#if pin.length == 0}
+ {
+ await setupTesseract(e.detail.pin)
+ e.detail.done()
+ }} />
+{:else}
+
+
+ {#each passphrase as word, i}
+
{
+ if (i == 0) {
+ passphrase = parsePhrase(e.clipboardData?.getData("Text") ?? "")
+ e.preventDefault()
+ e.stopPropagation()
+ }
+ }} />
+ {/each}
+
+
+
+
+
+ {
+ readPassphrase(e.detail)
+ }} />
+ {
+ importAccount(e.detail)
+ }} />
+
+
+{/if}
+
+
diff --git a/src/lib/layouts/login/Unlock.svelte b/src/lib/layouts/login/Unlock.svelte
index 73c91bfc2..86ccc9fc1 100644
--- a/src/lib/layouts/login/Unlock.svelte
+++ b/src/lib/layouts/login/Unlock.svelte
@@ -18,6 +18,8 @@
import { ToastMessage } from "$lib/state/ui/toast"
import { TesseractStoreInstance } from "$lib/wasm/TesseractStore"
export let create: boolean = false
+ export let importing: boolean = false
+
const dispatch = createEventDispatcher()
let loading = false
@@ -119,6 +121,10 @@
{/if}
+ {#if importing}
+ {$_("pages.auth.import.warning")}
+ {/if}
+
{#if loading}
{:else}
diff --git a/src/lib/wasm/HandleWarpErrors.ts b/src/lib/wasm/HandleWarpErrors.ts
index 32231ffbf..716a7da87 100644
--- a/src/lib/wasm/HandleWarpErrors.ts
+++ b/src/lib/wasm/HandleWarpErrors.ts
@@ -15,6 +15,7 @@ export enum WarpError {
CONSTELLATION_NOT_FOUND = "Constellation instance not found",
RAYGUN_NOT_FOUND = "Raygun instance not found",
FILE_SIZE_EXCEEDED = "File size exceeded",
+ INVALID_PHRASE = "Invalid word in phrase",
}
export function handleErrors(error: any): WarpError {
@@ -40,6 +41,8 @@ export function handleErrors(error: any): WarpError {
return WarpError.ITEM_ALREADY_EXIST_WITH_SAME_NAME
case message.includes(WarpError.ITEM_DOES_NOT_EXIST):
return WarpError.ITEM_DOES_NOT_EXIST
+ case message.includes(WarpError.INVALID_PHRASE.toLowerCase()):
+ return WarpError.INVALID_PHRASE
default:
return WarpError.GENERAL_ERROR
}
diff --git a/src/lib/wasm/MultipassStore.ts b/src/lib/wasm/MultipassStore.ts
index 3e5567060..d4f1a56c2 100644
--- a/src/lib/wasm/MultipassStore.ts
+++ b/src/lib/wasm/MultipassStore.ts
@@ -620,6 +620,30 @@ class MultipassStore {
}
}
+ async importAccount(passphrase: string, settings?: { to?: Uint8Array; multipassBox?: wasm.MultiPassBox }): Promise> {
+ let multipass = settings?.multipassBox ? settings.multipassBox : get(this.multipassWritable)
+ if (multipass) {
+ let result = multipass
+ .import_identity(passphrase, settings?.to)
+ .then(value => {
+ return success(value)
+ })
+ .catch(reason => {
+ return failure(handleErrors(reason))
+ })
+ return await result
+ }
+ return failure(WarpError.MULTIPASS_NOT_FOUND)
+ }
+
+ async exportAccount(memory?: boolean): Promise> {
+ let multipass = get(this.multipassWritable)
+ if (multipass) {
+ return success(await multipass.export_identity(memory))
+ }
+ return failure(WarpError.MULTIPASS_NOT_FOUND)
+ }
+
async identity_from_did(id: string, maxRetries = 3): Promise {
let multipass = get(this.multipassWritable)
let lastErr
diff --git a/src/lib/wasm/TesseractStore.ts b/src/lib/wasm/TesseractStore.ts
index 68ad33f47..53c7e09f3 100644
--- a/src/lib/wasm/TesseractStore.ts
+++ b/src/lib/wasm/TesseractStore.ts
@@ -15,6 +15,10 @@ class TesseractStore {
this.tesseractWritable = writable(null)
}
+ clearTesseract() {
+ this.tesseractWritable.set(null)
+ }
+
/**
* Retrieves the Tesseract instance.
* @returns {wasm.Tesseract} The current Tesseract instance.
diff --git a/src/lib/wasm/WarpStore.ts b/src/lib/wasm/WarpStore.ts
index 10efcbb75..d67b2d138 100644
--- a/src/lib/wasm/WarpStore.ts
+++ b/src/lib/wasm/WarpStore.ts
@@ -38,8 +38,12 @@ class Store {
}
await initWarp()
let warp_instance = await this.createIpfs(addresses)
- let tesseract = warp_instance.multipass.tesseract()
- let locked = createLock(warp_instance)
+ this.updateWarpInstance(warp_instance)
+ }
+
+ async updateWarpInstance(instance: wasm.WarpInstance) {
+ let tesseract = instance.multipass.tesseract()
+ let locked = createLock(instance)
// After passing tesseract to Ipfs the current ref is consumed so we fetch it from Ipfs again
TesseractStoreInstance.initTesseract(tesseract)
this.warp.tesseract.set(tesseract)
@@ -55,7 +59,7 @@ class Store {
* @returns {Promise} A promise that resolves to a WarpInstance.
* @private
*/
- private async createIpfs(addresses?: string[]): Promise {
+ async createIpfs(addresses?: string[]): Promise {
let tesseract: wasm.Tesseract = await TesseractStoreInstance.getTesseract()
let config: wasm.Config
if (addresses && addresses.length > 0) {
@@ -63,6 +67,9 @@ class Store {
} else {
config = wasm.Config.minimal_basic()
}
+ // Shuttle address is temporary! For testing!
+ // TODO: Uncomment later or add a flag to enable or disable
+ // config.enable_shuttle_discovery(["/ip4/138.197.62.183/tcp/7008/ws/p2p/12D3KooW9tAgS6kZPmEtfRiBHsYC46dN2hv6hT6gQ8bqxzWw7y5X"])
config.set_save_phrase(get(AuthStore.state).saveSeedPhrase)
config.set_thumbnail_size(500, 500)
config.with_thumbnail_exact_format(true)
diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte
index b5e2b7b2e..ac79f2905 100644
--- a/src/routes/auth/+page.svelte
+++ b/src/routes/auth/+page.svelte
@@ -3,6 +3,7 @@
import { Route } from "$lib/enums"
import { Entrypoint, NewAccount, LoginPage, RecoveryCopy, Unlock } from "$lib/layouts/login"
+ import ImportAccount from "$lib/layouts/login/ImportAccount.svelte"
import { AuthStore } from "$lib/state/auth"
import { Store } from "$lib/state/Store"
import { ToastMessage } from "$lib/state/ui/toast"
@@ -40,7 +41,7 @@
await WarpStore.initWarpInstances(addressed)
}
let ownIdentity = await MultipassStoreInstance.getOwnIdentity()
- ownIdentity.fold(
+ await ownIdentity.fold(
async (_: any) => {
if (username === "") return
AuthStore.setStoredPin(pin)
@@ -84,11 +85,14 @@
{:else if currentPage == LoginPage.Username}
+{:else if currentPage == LoginPage.Import}
+
{:else if currentPage == LoginPage.Pin}
{
await auth(e.detail.pin)
+ console.log("done")
e.detail.done()
}} />
{:else if currentPage == LoginPage.RecoveryCopy}
diff --git a/src/routes/settings/developer/+page.svelte b/src/routes/settings/developer/+page.svelte
index 52b279eda..11bde2adb 100644
--- a/src/routes/settings/developer/+page.svelte
+++ b/src/routes/settings/developer/+page.svelte
@@ -51,8 +51,8 @@
on:click={async _ => {
await clearState()
.then(() => {
- goto(Route.Unlock)
setTimeout(() => {
+ goto(Route.Unlock)
location.reload()
}, 500)
})
diff --git a/src/routes/settings/profile/+page.svelte b/src/routes/settings/profile/+page.svelte
index e305c7983..6b4d9f06d 100644
--- a/src/routes/settings/profile/+page.svelte
+++ b/src/routes/settings/profile/+page.svelte
@@ -303,6 +303,26 @@
return true
}
}
+
+ async function exportAccount(file?: boolean) {
+ let res = await MultipassStoreInstance.exportAccount(file)
+ res.onSuccess(memory => {
+ if (memory) {
+ let blob = new Blob([memory])
+ const elem = window.document.createElement("a")
+ elem.href = window.URL.createObjectURL(blob)
+ elem.download = "export.upk"
+ document.body.appendChild(elem)
+ elem.click()
+ document.body.removeChild(elem)
+ } else {
+ Store.addToastNotification(new ToastMessage($_("settings.profile.export.success"), "", 2))
+ }
+ })
+ res.onFailure(err => {
+ Store.addToastNotification(new ToastMessage($_("settings.profile.export.fail"), err, 2))
+ })
+ }
@@ -644,7 +664,28 @@
{/if}
-
+
+
+
+
+
+