Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,36 @@ import { describe, expect, it } from "vitest"

import {
buildOriginWithHostname,
pairingSecretFromWallet,
resolveExternalHostUrl,
resolvePairingNodeHostname,
} from "@/lib/pairing"

describe("pairingSecretFromWallet", () => {
const KEY = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
const SEED =
"legal winner thank year wave sausage worth useful legal winner thank yellow"

it("prefers the mnemonic when the wallet has one", () => {
expect(pairingSecretFromWallet({ private_key: KEY, seed: SEED })).toBe(SEED)
})

it("0x-prefixes a raw private key, which storage saves bare", () => {
// Without the prefix a scanner reads the key as a seed phrase and rejects
// it, so every mnemonic-less node would fail to pair.
expect(pairingSecretFromWallet({ private_key: KEY })).toBe(`0x${KEY}`)
})

it("leaves an already-prefixed key alone", () => {
expect(pairingSecretFromWallet({ private_key: `0x${KEY}` })).toBe(`0x${KEY}`)
})

it("falls back to the key when seed is null or empty", () => {
expect(pairingSecretFromWallet({ private_key: KEY, seed: null })).toBe(`0x${KEY}`)
expect(pairingSecretFromWallet({ private_key: KEY, seed: "" })).toBe(`0x${KEY}`)
})
})

describe("buildOriginWithHostname", () => {
it("replaces hostname while preserving protocol and port", () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SetupService } from "@/client"
import { loadPassword } from "@/lib/device-key"
import { fetchNodeConfig } from "@/lib/node-config"
import { fetchOwnWalletExport, type WalletExportData } from "@/lib/wallet-export"

export function buildOriginWithHostname(origin: string, hostname: string): string {
const url = new URL(origin)
Expand Down Expand Up @@ -60,18 +61,39 @@ export async function resolvePairingNodeUrl(): Promise<string> {
return resolvePairingNodeHostname(vpnIp, localIp, window.location.origin)
}

/** The secret a scanner should import.
*
* `seed` wins when there is one: a mnemonic restores anywhere, and a node set
* up from a raw key has none to give.
*
* The `0x` prefix is not cosmetic. Wallet storage strips it before saving
* (`WalletEntry.private_key = private_key.removeprefix("0x")`), so the export
* hands back bare hex, and the scanner has nothing but the value's shape to
* tell a key from a phrase by. Unprefixed, a private key reads as a seed
* phrase and fails to import. */
export function pairingSecretFromWallet(wallet: WalletExportData): string {
if (wallet.seed) return wallet.seed
return wallet.private_key.startsWith("0x")
? wallet.private_key
: `0x${wallet.private_key}`
}

/** The QR carries the wallet itself, not a way to ask this node for it: the
* export happens here, while the browser still holds an authenticated session.
* A phone that scans it can restore the account with the node switched off. */
export async function buildPairingQrValue() {
const address = localStorage.getItem("auth_username") || ""
const passphrase = (await loadPassword()) ?? ""
if (!address || !passphrase) {
const password = (await loadPassword()) ?? ""
if (!address || !password) {
throw new Error(
"No active wallet session — log out and back in to refresh device key.",
)
}
const url = await resolvePairingNodeUrl()
const wallet = await fetchOwnWalletExport()
return JSON.stringify({
url,
address,
passphrase,
password: pairingSecretFromWallet(wallet),
})
}
Loading