mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-29 13:03:33 +02:00
add support for nostr signing device
This commit is contained in:
5
.changeset/quick-radios-collect.md
Normal file
5
.changeset/quick-radios-collect.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for Nostr Signing Device
|
@@ -24,6 +24,7 @@
|
|||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@getalby/bitcoin-connect-react": "^2.4.2",
|
"@getalby/bitcoin-connect-react": "^2.4.2",
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
|
"@noble/secp256k1": "^1.7.0",
|
||||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
@@ -44,7 +45,6 @@
|
|||||||
"match-sorter": "^6.3.1",
|
"match-sorter": "^6.3.1",
|
||||||
"nanoid": "^5.0.2",
|
"nanoid": "^5.0.2",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
"noble-secp256k1": "^1.2.14",
|
|
||||||
"nostr-tools": "^1.17.0",
|
"nostr-tools": "^1.17.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"@changesets/cli": "^2.26.2",
|
"@changesets/cli": "^2.26.2",
|
||||||
"@types/chroma-js": "^2.4.1",
|
"@types/chroma-js": "^2.4.1",
|
||||||
"@types/debug": "^4.1.8",
|
"@types/debug": "^4.1.8",
|
||||||
|
"@types/dom-serial": "^1.0.6",
|
||||||
"@types/identicon.js": "^2.3.1",
|
"@types/identicon.js": "^2.3.1",
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/leaflet": "^1.9.3",
|
||||||
"@types/leaflet.locatecontrol": "^0.74.1",
|
"@types/leaflet.locatecontrol": "^0.74.1",
|
||||||
|
@@ -2,13 +2,20 @@ import { Badge, BadgeProps } from "@chakra-ui/react";
|
|||||||
import { Account } from "../services/account";
|
import { Account } from "../services/account";
|
||||||
|
|
||||||
export default function AccountInfoBadge({ account, ...props }: BadgeProps & { account: Account }) {
|
export default function AccountInfoBadge({ account, ...props }: BadgeProps & { account: Account }) {
|
||||||
if (account.useExtension) {
|
if (account.connectionType === "extension") {
|
||||||
return (
|
return (
|
||||||
<Badge {...props} variant="solid" colorScheme="green">
|
<Badge {...props} variant="solid" colorScheme="green">
|
||||||
extension
|
extension
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (account.connectionType === "serial") {
|
||||||
|
return (
|
||||||
|
<Badge {...props} variant="solid" colorScheme="teal">
|
||||||
|
serial
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (account.secKey) {
|
if (account.secKey) {
|
||||||
return (
|
return (
|
||||||
<Badge {...props} variant="solid" colorScheme="red">
|
<Badge {...props} variant="solid" colorScheme="red">
|
||||||
|
@@ -3,6 +3,8 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
import { GlobalProviders } from "./providers";
|
import { GlobalProviders } from "./providers";
|
||||||
|
|
||||||
|
import "./services/serial-port";
|
||||||
|
|
||||||
// setup dayjs
|
// setup dayjs
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTimePlugin from "dayjs/plugin/relativeTime";
|
import relativeTimePlugin from "dayjs/plugin/relativeTime";
|
||||||
|
@@ -8,7 +8,7 @@ export type Account = {
|
|||||||
relays?: string[];
|
relays?: string[];
|
||||||
secKey?: ArrayBuffer;
|
secKey?: ArrayBuffer;
|
||||||
iv?: Uint8Array;
|
iv?: Uint8Array;
|
||||||
useExtension?: boolean;
|
connectionType?: 'extension'|'serial';
|
||||||
localSettings?: AppSettings;
|
localSettings?: AppSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -17,7 +17,6 @@ const DEFAULT_RELAYS = [
|
|||||||
{ url: "wss://relay.damus.io", mode: RelayMode.READ },
|
{ url: "wss://relay.damus.io", mode: RelayMode.READ },
|
||||||
{ url: "wss://nostr.wine", mode: RelayMode.READ },
|
{ url: "wss://nostr.wine", mode: RelayMode.READ },
|
||||||
{ url: "wss://relay.snort.social", mode: RelayMode.READ },
|
{ url: "wss://relay.snort.social", mode: RelayMode.READ },
|
||||||
{ url: "wss://eden.nostr.land", mode: RelayMode.READ },
|
|
||||||
{ url: "wss://nos.lol", mode: RelayMode.READ },
|
{ url: "wss://nos.lol", mode: RelayMode.READ },
|
||||||
{ url: "wss://purplerelay.com", mode: RelayMode.READ },
|
{ url: "wss://purplerelay.com", mode: RelayMode.READ },
|
||||||
];
|
];
|
||||||
|
255
src/services/serial-port.ts
Normal file
255
src/services/serial-port.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import { getEventHash, validateEvent } from "nostr-tools";
|
||||||
|
import { base64 } from "@scure/base";
|
||||||
|
import { randomBytes, hexToBytes } from "@noble/hashes/utils";
|
||||||
|
import { Point } from "@noble/secp256k1";
|
||||||
|
|
||||||
|
import { logger } from "../helpers/debug";
|
||||||
|
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||||
|
import createDefer, { Deferred } from "../classes/deferred";
|
||||||
|
|
||||||
|
const METHOD_PING = "/ping";
|
||||||
|
// const METHOD_LOG = '/log'
|
||||||
|
|
||||||
|
export const METHOD_SIGN_MESSAGE = "/sign-message";
|
||||||
|
export const METHOD_SHARED_SECRET = "/shared-secret";
|
||||||
|
export const METHOD_PUBLIC_KEY = "/public-key";
|
||||||
|
|
||||||
|
export const PUBLIC_METHODS = [METHOD_PUBLIC_KEY, METHOD_SIGN_MESSAGE, METHOD_SHARED_SECRET];
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
const log = logger.extend("SerialPortService");
|
||||||
|
const deviceLog = log.extend("DeviceLog");
|
||||||
|
let writer: WritableStreamDefaultWriter<string> | null;
|
||||||
|
|
||||||
|
export function isConnected() {
|
||||||
|
return !!writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Callback = () => void;
|
||||||
|
type DeviceOpts = {
|
||||||
|
onConnect?: Callback;
|
||||||
|
onDisconnect?: Callback;
|
||||||
|
onError?: (err: Error) => void;
|
||||||
|
onDone?: Callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastCommand: Deferred<string> | null = null;
|
||||||
|
export async function callMethodOnDevice(method: string, params: string[], opts: DeviceOpts = {}) {
|
||||||
|
if (!writer) await connectToDevice(opts);
|
||||||
|
|
||||||
|
// only one command can be pending at any time
|
||||||
|
// but each will only wait 6 seconds
|
||||||
|
if (lastCommand) throw new Error("Previous command to device still pending!");
|
||||||
|
const command = createDefer<string>();
|
||||||
|
lastCommand = command;
|
||||||
|
|
||||||
|
// send actual command
|
||||||
|
sendCommand(method, params);
|
||||||
|
setTimeout(() => {
|
||||||
|
command.reject(new Error("Device timeout"));
|
||||||
|
if (lastCommand === command) lastCommand = null;
|
||||||
|
}, 6000);
|
||||||
|
|
||||||
|
return lastCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectToDevice({ onConnect, onDisconnect, onError, onDone }: DeviceOpts): Promise<void> {
|
||||||
|
let port: SerialPort = await navigator.serial.requestPort();
|
||||||
|
let reader;
|
||||||
|
|
||||||
|
const startSerialPortReading = async () => {
|
||||||
|
// reading responses
|
||||||
|
while (port && port.readable) {
|
||||||
|
const textDecoder = new window.TextDecoderStream();
|
||||||
|
port.readable.pipeTo(textDecoder.writable);
|
||||||
|
reader = textDecoder.readable.getReader();
|
||||||
|
const readStringUntil = readFromSerialPort(reader);
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await readStringUntil("\n");
|
||||||
|
if (value) {
|
||||||
|
const { method, data } = parseResponse(value);
|
||||||
|
|
||||||
|
if (method === "/log") deviceLog(data);
|
||||||
|
if (method === "/ping") log("Pong");
|
||||||
|
|
||||||
|
if (PUBLIC_METHODS.indexOf(method) === -1) {
|
||||||
|
// ignore /ping, /log responses
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Received: ", method, data);
|
||||||
|
|
||||||
|
if (lastCommand) {
|
||||||
|
lastCommand.resolve(data);
|
||||||
|
lastCommand = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
lastCommand = null;
|
||||||
|
writer = null;
|
||||||
|
if (onDone) onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
writer = null;
|
||||||
|
if (onError) onError(error);
|
||||||
|
if (lastCommand) {
|
||||||
|
lastCommand.reject(error);
|
||||||
|
lastCommand = null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await port.open({ baudRate: 9600 });
|
||||||
|
|
||||||
|
// this `sleep()` is a hack, I know!
|
||||||
|
// but `port.onconnect` is never called. I don't know why!
|
||||||
|
await sleep(1000);
|
||||||
|
startSerialPortReading();
|
||||||
|
|
||||||
|
const textEncoder = new window.TextEncoderStream();
|
||||||
|
textEncoder.readable.pipeTo(port.writable);
|
||||||
|
writer = textEncoder.writable.getWriter();
|
||||||
|
|
||||||
|
// send ping first
|
||||||
|
await sendCommand(METHOD_PING);
|
||||||
|
await sendCommand(METHOD_PING, [window.location.host]);
|
||||||
|
|
||||||
|
if (onConnect) onConnect();
|
||||||
|
|
||||||
|
port.addEventListener("disconnect", () => {
|
||||||
|
log("Disconnected");
|
||||||
|
lastCommand = null;
|
||||||
|
writer = null;
|
||||||
|
if (onDisconnect) onDisconnect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCommand(method: string, params: string[] = []) {
|
||||||
|
if (!writer) return;
|
||||||
|
log("Send command", method, params);
|
||||||
|
const message = [method].concat(params).join(" ");
|
||||||
|
await writer.write(message + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFromSerialPort(reader: ReadableStreamDefaultReader<string>) {
|
||||||
|
let partialChunk: string | undefined;
|
||||||
|
let fulliness: string[] = [];
|
||||||
|
|
||||||
|
const readStringUntil = async (separator = "\n") => {
|
||||||
|
if (fulliness.length) return { value: fulliness.shift()!.trim(), done: false };
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
if (partialChunk) {
|
||||||
|
// leftovers from previous read
|
||||||
|
chunks.push(partialChunk);
|
||||||
|
partialChunk = undefined;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (value) {
|
||||||
|
const values = value.split(separator);
|
||||||
|
// found one or more separators
|
||||||
|
if (values.length > 1) {
|
||||||
|
chunks.push(values.shift()); // first element
|
||||||
|
partialChunk = values.pop(); // last element
|
||||||
|
fulliness = values; // full lines
|
||||||
|
return { value: chunks.join("").trim(), done: false };
|
||||||
|
}
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
if (done) return { value: chunks.join("").trim(), done: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return readStringUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResponse(value: string) {
|
||||||
|
const method = value.split(" ")[0];
|
||||||
|
const data = value.substring(method.length).trim();
|
||||||
|
|
||||||
|
return { method, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const utf8Decoder = new TextDecoder("utf-8");
|
||||||
|
export const utf8Encoder = new TextEncoder();
|
||||||
|
|
||||||
|
export async function encrypt(pubkey: string, text: string) {
|
||||||
|
const sharedSecretStr = await callMethodOnDevice(METHOD_SHARED_SECRET, [xOnlyToXY(pubkey)]);
|
||||||
|
const sharedSecret = hexToBytes(sharedSecretStr);
|
||||||
|
|
||||||
|
let iv = Uint8Array.from(randomBytes(16));
|
||||||
|
let plaintext = utf8Encoder.encode(text);
|
||||||
|
let cryptoKey = await crypto.subtle.importKey("raw", sharedSecret, { name: "AES-CBC" }, false, ["encrypt"]);
|
||||||
|
let ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, cryptoKey, plaintext);
|
||||||
|
let ctb64 = base64.encode(new Uint8Array(ciphertext));
|
||||||
|
let ivb64 = base64.encode(new Uint8Array(iv.buffer));
|
||||||
|
|
||||||
|
return `${ctb64}?iv=${ivb64}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(pubkey: string, data: string) {
|
||||||
|
let [ctb64, ivb64] = data.split("?iv=");
|
||||||
|
|
||||||
|
const sharedSecretStr = await callMethodOnDevice(METHOD_SHARED_SECRET, [xOnlyToXY(pubkey)]);
|
||||||
|
const sharedSecret = hexToBytes(sharedSecretStr);
|
||||||
|
|
||||||
|
let cryptoKey = await crypto.subtle.importKey("raw", sharedSecret, { name: "AES-CBC" }, false, ["decrypt"]);
|
||||||
|
let ciphertext = base64.decode(ctb64);
|
||||||
|
let iv = base64.decode(ivb64);
|
||||||
|
|
||||||
|
let plaintext = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, cryptoKey, ciphertext);
|
||||||
|
|
||||||
|
let text = utf8Decoder.decode(plaintext);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xOnlyToXY(p: string) {
|
||||||
|
return Point.fromHex(p).toHex().substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicKey() {
|
||||||
|
return await callMethodOnDevice(METHOD_PUBLIC_KEY, []);
|
||||||
|
}
|
||||||
|
async function signEvent(draft: DraftNostrEvent) {
|
||||||
|
let signed = { ...draft } as NostrEvent;
|
||||||
|
|
||||||
|
if (!signed.pubkey) signed.pubkey = await callMethodOnDevice(METHOD_PUBLIC_KEY, []);
|
||||||
|
if (!signed.created_at) signed.created_at = Math.round(Date.now() / 1000);
|
||||||
|
if (!signed.id) signed.id = getEventHash(signed);
|
||||||
|
if (!validateEvent(signed)) throw new Error("Tnvalid event");
|
||||||
|
|
||||||
|
signed.sig = await callMethodOnDevice(METHOD_SIGN_MESSAGE, [signed.id]);
|
||||||
|
return signed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialPortService = {
|
||||||
|
supported: !!navigator.serial,
|
||||||
|
signEvent,
|
||||||
|
getPublicKey,
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
callMethodOnDevice,
|
||||||
|
connectToDevice,
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (writer) {
|
||||||
|
sendCommand(METHOD_PING, [window.location.host]);
|
||||||
|
}
|
||||||
|
}, 1000 * 10);
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
//@ts-ignore
|
||||||
|
window.serialPortService = serialPortService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default serialPortService;
|
@@ -2,6 +2,7 @@ import { nip04, getPublicKey, finishEvent } from "nostr-tools";
|
|||||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||||
import { Account } from "./account";
|
import { Account } from "./account";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
|
import serialPortService from "./serial-port";
|
||||||
|
|
||||||
const decryptedKeys = new Map<string, string>();
|
const decryptedKeys = new Map<string, string>();
|
||||||
|
|
||||||
@@ -78,13 +79,21 @@ class SigningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async requestSignature(draft: DraftNostrEvent, account: Account) {
|
async requestSignature(draft: DraftNostrEvent, account: Account) {
|
||||||
if (account?.readonly) throw new Error("Cant sign in readonly mode");
|
if (account.readonly) throw new Error("Cant sign in readonly mode");
|
||||||
if (account?.useExtension) {
|
if (account.connectionType) {
|
||||||
if (window.nostr) {
|
if (account.connectionType === "extension") {
|
||||||
const signed = await window.nostr.signEvent(draft);
|
if (window.nostr) {
|
||||||
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey!");
|
const signed = await window.nostr.signEvent(draft);
|
||||||
return signed;
|
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey!");
|
||||||
} else throw new Error("Missing nostr extension");
|
return signed;
|
||||||
|
} else throw new Error("Missing nostr extension");
|
||||||
|
} else if (account.connectionType === "serial") {
|
||||||
|
if (serialPortService.supported) {
|
||||||
|
const signed = await serialPortService.signEvent(draft);
|
||||||
|
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey!");
|
||||||
|
return signed;
|
||||||
|
} else throw new Error("Serial devices are not supported");
|
||||||
|
} else throw new Error("Unknown connection type " + account.connectionType);
|
||||||
} else if (account?.secKey) {
|
} else if (account?.secKey) {
|
||||||
const secKey = await this.decryptSecKey(account);
|
const secKey = await this.decryptSecKey(account);
|
||||||
const tmpDraft = { ...draft, pubkey: getPublicKey(secKey) };
|
const tmpDraft = { ...draft, pubkey: getPublicKey(secKey) };
|
||||||
@@ -95,13 +104,17 @@ class SigningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async requestDecrypt(data: string, pubkey: string, account: Account) {
|
async requestDecrypt(data: string, pubkey: string, account: Account) {
|
||||||
if (account?.readonly) throw new Error("Cant decrypt in readonly mode");
|
if (account.readonly) throw new Error("Cant decrypt in readonly mode");
|
||||||
if (account?.useExtension) {
|
if (account.connectionType) {
|
||||||
if (window.nostr) {
|
if (account.connectionType === "extension") {
|
||||||
if (window.nostr.nip04) {
|
if (window.nostr) {
|
||||||
return await window.nostr.nip04.decrypt(pubkey, data);
|
if (window.nostr.nip04) {
|
||||||
} else throw new Error("Extension dose not support decryption");
|
return await window.nostr.nip04.decrypt(pubkey, data);
|
||||||
} else throw new Error("Missing nostr extension");
|
} else throw new Error("Extension dose not support decryption");
|
||||||
|
} else throw new Error("Missing nostr extension");
|
||||||
|
} else if (account.connectionType === "serial") {
|
||||||
|
return await serialPortService.decrypt(pubkey, data);
|
||||||
|
} else throw new Error("Unknown connection type " + account.connectionType);
|
||||||
} else if (account?.secKey) {
|
} else if (account?.secKey) {
|
||||||
const secKey = await this.decryptSecKey(account);
|
const secKey = await this.decryptSecKey(account);
|
||||||
return await nip04.decrypt(secKey, pubkey, data);
|
return await nip04.decrypt(secKey, pubkey, data);
|
||||||
@@ -109,13 +122,17 @@ class SigningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async requestEncrypt(text: string, pubkey: string, account: Account) {
|
async requestEncrypt(text: string, pubkey: string, account: Account) {
|
||||||
if (account?.readonly) throw new Error("Cant encrypt in readonly mode");
|
if (account.readonly) throw new Error("Cant encrypt in readonly mode");
|
||||||
if (account?.useExtension) {
|
if (account.connectionType) {
|
||||||
if (window.nostr) {
|
if (account.connectionType === "extension") {
|
||||||
if (window.nostr.nip04) {
|
if (window.nostr) {
|
||||||
return await window.nostr.nip04.encrypt(pubkey, text);
|
if (window.nostr.nip04) {
|
||||||
} else throw new Error("Extension dose not support encryption");
|
return await window.nostr.nip04.encrypt(pubkey, text);
|
||||||
} else throw new Error("Missing nostr extension");
|
} else throw new Error("Extension dose not support encryption");
|
||||||
|
} else throw new Error("Missing nostr extension");
|
||||||
|
} else if (account.connectionType === "serial") {
|
||||||
|
return await serialPortService.encrypt(pubkey, text);
|
||||||
|
} else throw new Error("Unknown connection type " + account.connectionType);
|
||||||
} else if (account?.secKey) {
|
} else if (account?.secKey) {
|
||||||
const secKey = await this.decryptSecKey(account);
|
const secKey = await this.decryptSecKey(account);
|
||||||
return await nip04.encrypt(secKey, pubkey, text);
|
return await nip04.encrypt(secKey, pubkey, text);
|
||||||
|
1
src/types/serial.d.ts
vendored
Normal file
1
src/types/serial.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "@types/dom-serial";
|
@@ -1,11 +1,25 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge, Button, Flex, Spinner, Text, useDisclosure, useToast } from "@chakra-ui/react";
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
import Key01 from "../../components/icons/key-01";
|
import Key01 from "../../components/icons/key-01";
|
||||||
import ChevronDown from "../../components/icons/chevron-down";
|
import ChevronDown from "../../components/icons/chevron-down";
|
||||||
import ChevronUp from "../../components/icons/chevron-up";
|
import ChevronUp from "../../components/icons/chevron-up";
|
||||||
|
import serialPortService from "../../services/serial-port";
|
||||||
|
import UsbFlashDrive from "../../components/icons/usb-flash-drive";
|
||||||
|
import HelpCircle from "../../components/icons/help-circle";
|
||||||
|
|
||||||
export default function LoginStartView() {
|
export default function LoginStartView() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -33,17 +47,44 @@ export default function LoginStartView() {
|
|||||||
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
||||||
}
|
}
|
||||||
|
|
||||||
accountService.addAccount({ pubkey, relays, useExtension: true, readonly: false });
|
accountService.addAccount({ pubkey, relays, connectionType: "extension", readonly: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
accountService.switchAccount(pubkey);
|
accountService.switchAccount(pubkey);
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({ status: "warning", title: "Cant find extension" });
|
||||||
status: "warning",
|
}
|
||||||
title: "Cant find extension",
|
};
|
||||||
});
|
const loginWithSerial = async () => {
|
||||||
|
if (serialPortService.supported) {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const pubkey = await serialPortService.getPublicKey();
|
||||||
|
|
||||||
|
if (!accountService.hasAccount(pubkey)) {
|
||||||
|
let relays: string[] = [];
|
||||||
|
|
||||||
|
// TODO: maybe get relays from device
|
||||||
|
|
||||||
|
if (relays.length === 0) {
|
||||||
|
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
||||||
|
}
|
||||||
|
|
||||||
|
accountService.addAccount({ pubkey, relays, connectionType: "serial", readonly: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
accountService.switchAccount(pubkey);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
toast({ status: "warning", title: "Serial is not supported" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +95,21 @@ export default function LoginStartView() {
|
|||||||
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
|
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
|
||||||
Sign in with extension
|
Sign in with extension
|
||||||
</Button>
|
</Button>
|
||||||
|
{serialPortService.supported && (
|
||||||
|
<ButtonGroup colorScheme="purple">
|
||||||
|
<Button onClick={loginWithSerial} leftIcon={<UsbFlashDrive boxSize={6} />} w="xs">
|
||||||
|
Use Signing Device
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
as={Link}
|
||||||
|
aria-label="What is NSD?"
|
||||||
|
title="What is NSD?"
|
||||||
|
isExternal
|
||||||
|
href="https://lnbits.github.io/nostr-signing-device/installer/"
|
||||||
|
icon={<HelpCircle boxSize={5} />}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={advanced.onToggle}
|
onClick={advanced.onToggle}
|
||||||
|
15
yarn.lock
15
yarn.lock
@@ -2382,6 +2382,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
||||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||||
|
|
||||||
|
"@noble/secp256k1@^1.7.0":
|
||||||
|
version "1.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
|
||||||
|
integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||||
@@ -2685,6 +2690,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/ms" "*"
|
"@types/ms" "*"
|
||||||
|
|
||||||
|
"@types/dom-serial@^1.0.6":
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/dom-serial/-/dom-serial-1.0.6.tgz#48462122c0e943195d0611027ff9de979f04f43f"
|
||||||
|
integrity sha512-eUHKbc6mdMgMm75/oBLocs3wkOQkPQ/oNCT+b5OgUT6mLgIvDTp3wCCE9tYZNvDPPh6Cj9lVg2IguWfS/mDrrQ==
|
||||||
|
|
||||||
"@types/estree@0.0.39":
|
"@types/estree@0.0.39":
|
||||||
version "0.0.39"
|
version "0.0.39"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||||
@@ -5556,11 +5566,6 @@ ngraph.random@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ngraph.random/-/ngraph.random-1.1.0.tgz#5345c4bb63865c85d98ee6f13eab1395d8545a90"
|
resolved "https://registry.yarnpkg.com/ngraph.random/-/ngraph.random-1.1.0.tgz#5345c4bb63865c85d98ee6f13eab1395d8545a90"
|
||||||
integrity sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==
|
integrity sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==
|
||||||
|
|
||||||
noble-secp256k1@^1.2.14:
|
|
||||||
version "1.2.14"
|
|
||||||
resolved "https://registry.yarnpkg.com/noble-secp256k1/-/noble-secp256k1-1.2.14.tgz#39429c941d51211ca40161569cee03e61d72599e"
|
|
||||||
integrity sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==
|
|
||||||
|
|
||||||
node-domexception@^1.0.0:
|
node-domexception@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
|
Reference in New Issue
Block a user