mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 05:09:36 +02:00
move more stuff out to applesauce
This commit is contained in:
parent
aa2f2104f0
commit
38bc52b4c9
@ -42,7 +42,9 @@
|
||||
"@uiw/codemirror-theme-github": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||
"applesauce-core": "^0.4.0",
|
||||
"applesauce-channel": "^0.5.0",
|
||||
"applesauce-core": "^0.5.0",
|
||||
"applesauce-signer": "^0.5.0",
|
||||
"bech32": "^2.0.0",
|
||||
"blossom-client-sdk": "^0.7.0",
|
||||
"blossom-drive-sdk": "^0.4.0",
|
||||
|
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
@ -90,9 +90,15 @@ importers:
|
||||
'@webscopeio/react-textarea-autocomplete':
|
||||
specifier: ^4.9.2
|
||||
version: 4.9.2(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
applesauce-channel:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(typescript@5.6.2)
|
||||
applesauce-core:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(typescript@5.6.2)
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(typescript@5.6.2)
|
||||
applesauce-signer:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(typescript@5.6.2)
|
||||
bech32:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@ -2182,9 +2188,6 @@ packages:
|
||||
'@types/zen-observable@0.8.7':
|
||||
resolution: {integrity: sha512-LKzNTjj+2j09wAo/vvVjzgw5qckJJzhdGgWHW7j69QIGdq/KnZrMAMIHQiWGl3Ccflh5/CudBAntTPYdprPltA==}
|
||||
|
||||
'@types/zen-push@0.1.4':
|
||||
resolution: {integrity: sha512-FB0u2p9qwYRItXTio+Jwn+usx4zWIgdDXwMcSiZtjEFru/mIfJb9SedruzbBvEGHvq77sGt2e5cursvheyYcRQ==}
|
||||
|
||||
'@uiw/codemirror-extensions-basic-setup@4.23.3':
|
||||
resolution: {integrity: sha512-nEMjgbCyeLx+UQgOGAAoUWYFE34z5TlyaKNszuig/BddYFDb0WKcgmC37bDFxR2dZssf3K/lwGWLpXnGKXePbA==}
|
||||
peerDependencies:
|
||||
@ -2280,8 +2283,14 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
applesauce-core@0.4.0:
|
||||
resolution: {integrity: sha512-UvzmM+mh9D5g697NHJv6x9bzkXk64uaiFLiIJR5Yb+FpYYOsIRJZGT73fVmgVC8EAWkItZ9jYS25IUi4a0cgrg==}
|
||||
applesauce-channel@0.5.0:
|
||||
resolution: {integrity: sha512-0B4ldPKzO7RFfWpOyQnqf1JIAk2JKTx88atSGPtbZGYWM9kfG2FzBIEaaamKbQ12dfZqNzZRWvCFMww+5jX1Eg==}
|
||||
|
||||
applesauce-core@0.5.0:
|
||||
resolution: {integrity: sha512-PqjiTsxBWnrlyBe9mP5KNJs9L7AQjrqWJoj/F52IqxdYgGjli3FubfV/lwjq+UQzwK22CMiwPoxTG3jLDlfpaw==}
|
||||
|
||||
applesauce-signer@0.5.0:
|
||||
resolution: {integrity: sha512-MF1I0KQ0nm3vq/Z/lXL0279KqTJcu0BctvKSORNUBeV0GTlDsDdvrsYyKmsjJVVJunr8xhuOrsPvAew5dT54Lw==}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
@ -7101,10 +7110,6 @@ snapshots:
|
||||
|
||||
'@types/zen-observable@0.8.7': {}
|
||||
|
||||
'@types/zen-push@0.1.4':
|
||||
dependencies:
|
||||
'@types/zen-observable': 0.8.7
|
||||
|
||||
'@uiw/codemirror-extensions-basic-setup@4.23.3(@codemirror/autocomplete@6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.1))(@codemirror/commands@6.6.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.1)
|
||||
@ -7209,18 +7214,39 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
applesauce-core@0.4.0(typescript@5.6.2):
|
||||
applesauce-channel@0.5.0(typescript@5.6.2):
|
||||
dependencies:
|
||||
applesauce-core: 0.5.0(typescript@5.6.2)
|
||||
nostr-tools: 2.7.2(typescript@5.6.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-core@0.5.0(typescript@5.6.2):
|
||||
dependencies:
|
||||
'@types/zen-push': 0.1.4
|
||||
debug: 4.3.7
|
||||
json-stringify-deterministic: 1.0.12
|
||||
nanoid: 5.0.7
|
||||
nostr-tools: 2.7.2(typescript@5.6.2)
|
||||
zen-observable: 0.10.0
|
||||
zen-push: 0.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-signer@0.5.0(typescript@5.6.2):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.5.0
|
||||
'@noble/secp256k1': 1.7.1
|
||||
'@scure/base': 1.1.9
|
||||
'@types/dom-serial': 1.0.6
|
||||
applesauce-core: 0.5.0(typescript@5.6.2)
|
||||
debug: 4.3.7
|
||||
nostr-tools: 2.7.2(typescript@5.6.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
import { Nip07Signer } from "../../types/nostr-extensions";
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
|
||||
export class Account {
|
||||
readonly type: string = "unknown";
|
||||
pubkey: string;
|
||||
localSettings?: AppSettings;
|
||||
|
||||
protected _signer?: Nip07Signer | undefined;
|
||||
public get signer(): Nip07Signer | undefined {
|
||||
protected _signer?: Nip07Interface | undefined;
|
||||
public get signer(): Nip07Interface | undefined {
|
||||
return this._signer;
|
||||
}
|
||||
public set signer(value: Nip07Signer | undefined) {
|
||||
public set signer(value: Nip07Interface | undefined) {
|
||||
this._signer = value;
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
import AmberSigner from "../signers/amber-signer";
|
||||
import { AmberClipboardSigner } from "applesauce-signer";
|
||||
import { Account } from "./account";
|
||||
|
||||
export default class AmberAccount extends Account {
|
||||
readonly type = "amber";
|
||||
|
||||
protected declare _signer?: AmberSigner | undefined;
|
||||
public get signer(): AmberSigner | undefined {
|
||||
protected declare _signer?: AmberClipboardSigner | undefined;
|
||||
public get signer(): AmberClipboardSigner | undefined {
|
||||
return this._signer;
|
||||
}
|
||||
public set signer(value: AmberSigner | undefined) {
|
||||
public set signer(value: AmberClipboardSigner | undefined) {
|
||||
this._signer = value;
|
||||
}
|
||||
|
||||
constructor(pubkey: string) {
|
||||
super(pubkey);
|
||||
this.signer = new AmberSigner();
|
||||
this.signer = new AmberClipboardSigner();
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Nip07Signer } from "../../types/nostr-extensions";
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
import { Account } from "./account";
|
||||
|
||||
export default class ExtensionAccount extends Account {
|
||||
readonly type = "extension";
|
||||
|
||||
public get signer(): Nip07Signer | undefined {
|
||||
public get signer(): Nip07Interface | undefined {
|
||||
return window.nostr;
|
||||
}
|
||||
set signer(signer: Nip07Signer) {
|
||||
set signer(signer: Nip07Interface) {
|
||||
throw new Error("Cant update signer");
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
|
||||
import SimpleSigner from "../signers/simple-signer";
|
||||
import { SimpleSigner } from "applesauce-signer";
|
||||
import { Account } from "./account";
|
||||
|
||||
export default class NsecAccount extends Account {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SerialPortSigner from "../signers/serial-port-signer";
|
||||
import { SerialPortSigner } from "applesauce-signer";
|
||||
import { Account } from "./account";
|
||||
|
||||
export default class SerialPortAccount extends Account {
|
||||
|
@ -1,167 +0,0 @@
|
||||
import { EventTemplate, NostrEvent, VerifiedEvent, getEventHash, nip19, verifyEvent } from "nostr-tools";
|
||||
|
||||
import createDefer, { Deferred } from "../deferred";
|
||||
import { getPubkeyFromDecodeResult, isHex, isHexKey } from "../../helpers/nip19";
|
||||
import { Nip07Signer } from "../../types/nostr-extensions";
|
||||
|
||||
export default class AmberSigner implements Nip07Signer {
|
||||
static SUPPORTED = navigator.userAgent.includes("Android") && navigator.clipboard && navigator.clipboard.readText;
|
||||
private pendingRequest: Deferred<string> | null = null;
|
||||
public pubkey?: string;
|
||||
|
||||
verifyEvent: typeof verifyEvent = verifyEvent;
|
||||
|
||||
nip04?:
|
||||
| {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
}
|
||||
| undefined;
|
||||
nip44?:
|
||||
| {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
constructor() {
|
||||
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
||||
|
||||
this.nip04 = {
|
||||
encrypt: this.nip04Encrypt.bind(this),
|
||||
decrypt: this.nip04Decrypt.bind(this),
|
||||
};
|
||||
this.nip44 = {
|
||||
encrypt: this.nip44Encrypt.bind(this),
|
||||
decrypt: this.nip44Decrypt.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private onVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
if (!this.pendingRequest || !navigator.clipboard) return;
|
||||
|
||||
// read the result from the clipboard
|
||||
setTimeout(() => {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((result) => this.pendingRequest?.resolve(result))
|
||||
.catch((e) => this.pendingRequest?.reject(e));
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
private async intentRequest(intent: string) {
|
||||
this.rejectPending();
|
||||
const request = createDefer<string>();
|
||||
window.open(intent, "_blank");
|
||||
// NOTE: wait 500ms before setting the pending request since the visibilitychange event fires as soon as window.open is called
|
||||
setTimeout(() => {
|
||||
this.pendingRequest = request;
|
||||
}, 500);
|
||||
const result = await request;
|
||||
if (result.length === 0) throw new Error("Empty clipboard");
|
||||
return result;
|
||||
}
|
||||
|
||||
rejectPending() {
|
||||
if (this.pendingRequest) {
|
||||
this.pendingRequest.reject("Canceled");
|
||||
this.pendingRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
||||
}
|
||||
|
||||
private checkSupport() {
|
||||
if (!AmberSigner.SUPPORTED) throw new Error("Cant use Amber on non-Android device");
|
||||
}
|
||||
|
||||
public async getPublicKey() {
|
||||
this.checkSupport();
|
||||
if (this.pubkey) return this.pubkey;
|
||||
|
||||
const result = await this.intentRequest(AmberSigner.createGetPublicKeyIntent());
|
||||
if (isHexKey(result)) {
|
||||
this.pubkey = result;
|
||||
return result;
|
||||
} else if (result.startsWith("npub") || result.startsWith("nprofile")) {
|
||||
const decode = nip19.decode(result);
|
||||
const pubkey = getPubkeyFromDecodeResult(decode);
|
||||
if (!pubkey) throw new Error("Expected npub from clipboard");
|
||||
this.pubkey = pubkey;
|
||||
return pubkey;
|
||||
}
|
||||
throw new Error("Expected clipboard to have pubkey");
|
||||
}
|
||||
|
||||
public async signEvent(draft: EventTemplate & { pubkey?: string }): Promise<VerifiedEvent> {
|
||||
this.checkSupport();
|
||||
const pubkey = draft.pubkey || this.pubkey;
|
||||
if (!pubkey) throw new Error("Unknown signer pubkey");
|
||||
|
||||
const draftWithId = { ...draft, id: getEventHash({ ...draft, pubkey }) };
|
||||
const sig = await this.intentRequest(AmberSigner.createSignEventIntent(draftWithId));
|
||||
if (!isHex(sig)) throw new Error("Expected hex signature");
|
||||
|
||||
const event: NostrEvent = { ...draftWithId, sig, pubkey };
|
||||
if (!this.verifyEvent(event)) throw new Error("Invalid signature");
|
||||
return event;
|
||||
}
|
||||
|
||||
// NIP-04
|
||||
public async nip04Encrypt(pubkey: string, plaintext: string): Promise<string> {
|
||||
this.checkSupport();
|
||||
const data = await this.intentRequest(AmberSigner.createNip04EncryptIntent(pubkey, plaintext));
|
||||
return data;
|
||||
}
|
||||
public async nip04Decrypt(pubkey: string, data: string): Promise<string> {
|
||||
this.checkSupport();
|
||||
const plaintext = await this.intentRequest(AmberSigner.createNip04DecryptIntent(pubkey, data));
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
// NIP-44
|
||||
public async nip44Encrypt(pubkey: string, plaintext: string): Promise<string> {
|
||||
this.checkSupport();
|
||||
const data = await this.intentRequest(AmberSigner.createNip44EncryptIntent(pubkey, plaintext));
|
||||
return data;
|
||||
}
|
||||
public async nip44Decrypt(pubkey: string, data: string): Promise<string> {
|
||||
this.checkSupport();
|
||||
const plaintext = await this.intentRequest(AmberSigner.createNip44DecryptIntent(pubkey, data));
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
// static methods
|
||||
static createGetPublicKeyIntent() {
|
||||
return `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end`;
|
||||
}
|
||||
static createSignEventIntent(draft: EventTemplate) {
|
||||
return `intent:${encodeURIComponent(
|
||||
JSON.stringify(draft),
|
||||
)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;end`;
|
||||
}
|
||||
static createNip04EncryptIntent(pubkey: string, plainText: string) {
|
||||
return `intent:${encodeURIComponent(
|
||||
plainText,
|
||||
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip04_encrypt;end`;
|
||||
}
|
||||
static createNip04DecryptIntent(pubkey: string, ciphertext: string) {
|
||||
return `intent:${encodeURIComponent(
|
||||
ciphertext,
|
||||
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip04_decrypt;end`;
|
||||
}
|
||||
static createNip44EncryptIntent(pubkey: string, plainText: string) {
|
||||
return `intent:${encodeURIComponent(
|
||||
plainText,
|
||||
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip44_encrypt;end`;
|
||||
}
|
||||
static createNip44DecryptIntent(pubkey: string, ciphertext: string) {
|
||||
return `intent:${encodeURIComponent(
|
||||
ciphertext,
|
||||
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip44_decrypt;end`;
|
||||
}
|
||||
}
|
@ -11,11 +11,11 @@ import {
|
||||
import dayjs from "dayjs";
|
||||
import { nanoid } from "nanoid";
|
||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
|
||||
import MultiSubscription from "../multi-subscription";
|
||||
import { logger } from "../../helpers/debug";
|
||||
import createDefer, { Deferred } from "../deferred";
|
||||
import { Nip07Signer } from "../../types/nostr-extensions";
|
||||
|
||||
export function isErrorResponse(response: any): response is NostrConnectErrorResponse {
|
||||
return !!response.error;
|
||||
@ -66,7 +66,7 @@ export type NostrConnectErrorResponse = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export default class NostrConnectSigner implements Nip07Signer {
|
||||
export default class NostrConnectSigner implements Nip07Interface {
|
||||
sub: MultiSubscription;
|
||||
log = logger.extend("NostrConnectSigner");
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { EventTemplate, finalizeEvent, getPublicKey, nip04, nip44 } from "nostr-tools";
|
||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||
import { encrypt, decrypt } from "nostr-tools/nip49";
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
|
||||
import { Nip07Signer } from "../../types/nostr-extensions";
|
||||
import createDefer, { Deferred } from "../deferred";
|
||||
import db from "../../services/db";
|
||||
|
||||
@ -75,7 +75,7 @@ async function subltCryptoDecryptSecKey(buffer: ArrayBuffer, iv: Uint8Array, pas
|
||||
}
|
||||
}
|
||||
|
||||
export default class PasswordSigner implements Nip07Signer {
|
||||
export default class PasswordSigner implements Nip07Interface {
|
||||
key: Uint8Array | null = null;
|
||||
|
||||
// legacy
|
||||
|
@ -1,263 +0,0 @@
|
||||
import { EventTemplate, getEventHash, NostrEvent, verifyEvent } 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 { Nip07Signer } from "../../types/nostr-extensions";
|
||||
import createDefer, { Deferred } from "../deferred";
|
||||
|
||||
type Callback = () => void;
|
||||
type DeviceOpts = {
|
||||
onConnect?: Callback;
|
||||
onDisconnect?: Callback;
|
||||
onError?: (err: Error) => void;
|
||||
onDone?: Callback;
|
||||
};
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
function xOnlyToXY(p: string) {
|
||||
return Point.fromHex(p).toHex().substring(2);
|
||||
}
|
||||
|
||||
export const utf8Decoder = new TextDecoder("utf-8");
|
||||
export const utf8Encoder = new TextEncoder();
|
||||
|
||||
export default class SerialPortSigner implements Nip07Signer {
|
||||
log = logger.extend("SerialSigner");
|
||||
writer: WritableStreamDefaultWriter<string> | null = null;
|
||||
pubkey?: string;
|
||||
|
||||
get isConnected() {
|
||||
return !!this.writer;
|
||||
}
|
||||
|
||||
verifyEvent: typeof verifyEvent = verifyEvent;
|
||||
nip04?:
|
||||
| {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
constructor() {
|
||||
this.nip04 = {
|
||||
encrypt: this.nip04Encrypt.bind(this),
|
||||
decrypt: this.nip04Decrypt.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
lastCommand: Deferred<string> | null = null;
|
||||
async callMethodOnDevice(method: string, params: string[], opts: DeviceOpts = {}) {
|
||||
if (!SerialPortSigner.SUPPORTED) throw new Error("Serial devices are not supported");
|
||||
if (!this.writer) await this.connectToDevice(opts);
|
||||
|
||||
// only one command can be pending at any time
|
||||
// but each will only wait 6 seconds
|
||||
if (this.lastCommand) throw new Error("Previous command to device still pending!");
|
||||
const command = createDefer<string>();
|
||||
this.lastCommand = command;
|
||||
|
||||
// send actual command
|
||||
this.sendCommand(method, params);
|
||||
setTimeout(() => {
|
||||
command.reject(new Error("Device timeout"));
|
||||
if (this.lastCommand === command) this.lastCommand = null;
|
||||
}, 6000);
|
||||
|
||||
return this.lastCommand;
|
||||
}
|
||||
|
||||
async 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 = this.readFromSerialPort(reader);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await readStringUntil("\n");
|
||||
if (value) {
|
||||
const { method, data } = this.parseResponse(value);
|
||||
|
||||
// if (method === "/log") deviceLog(data);
|
||||
if (method === "/ping") this.log("Pong");
|
||||
|
||||
if (SerialPortSigner.PUBLIC_METHODS.indexOf(method) === -1) {
|
||||
// ignore /ping, /log responses
|
||||
continue;
|
||||
}
|
||||
|
||||
this.log("Received: ", method, data);
|
||||
|
||||
if (this.lastCommand) {
|
||||
this.lastCommand.resolve(data);
|
||||
this.lastCommand = null;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
this.lastCommand = null;
|
||||
this.writer = null;
|
||||
if (onDone) onDone();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
this.writer = null;
|
||||
if (onError) onError(error);
|
||||
if (this.lastCommand) {
|
||||
this.lastCommand.reject(error);
|
||||
this.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);
|
||||
this.writer = textEncoder.writable.getWriter();
|
||||
|
||||
// send ping first
|
||||
await this.sendCommand(SerialPortSigner.METHOD_PING);
|
||||
await this.sendCommand(SerialPortSigner.METHOD_PING, [window.location.host]);
|
||||
|
||||
if (onConnect) onConnect();
|
||||
|
||||
port.addEventListener("disconnect", () => {
|
||||
this.log("Disconnected");
|
||||
this.lastCommand = null;
|
||||
this.writer = null;
|
||||
if (onDisconnect) onDisconnect();
|
||||
});
|
||||
}
|
||||
|
||||
async sendCommand(method: string, params: string[] = []) {
|
||||
if (!this.writer) return;
|
||||
this.log("Send command", method, params);
|
||||
const message = [method].concat(params).join(" ");
|
||||
await this.writer.write(message + "\n");
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private parseResponse(value: string) {
|
||||
const method = value.split(" ")[0];
|
||||
const data = value.substring(method.length).trim();
|
||||
|
||||
return { method, data };
|
||||
}
|
||||
|
||||
// NIP-04
|
||||
public async nip04Encrypt(pubkey: string, text: string) {
|
||||
const sharedSecretStr = await this.callMethodOnDevice(SerialPortSigner.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}`;
|
||||
}
|
||||
public async nip04Decrypt(pubkey: string, data: string) {
|
||||
let [ctb64, ivb64] = data.split("?iv=");
|
||||
|
||||
const sharedSecretStr = await this.callMethodOnDevice(SerialPortSigner.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;
|
||||
}
|
||||
|
||||
public async getPublicKey() {
|
||||
const pubkey = await this.callMethodOnDevice(SerialPortSigner.METHOD_PUBLIC_KEY, []);
|
||||
this.pubkey = pubkey;
|
||||
return pubkey;
|
||||
}
|
||||
public async signEvent(draft: EventTemplate & { pubkey?: string }) {
|
||||
const pubkey = draft.pubkey || this.pubkey;
|
||||
if (!pubkey) throw new Error("Unknown signer pubkey");
|
||||
|
||||
const draftWithId = { ...draft, id: getEventHash({ ...draft, pubkey }) };
|
||||
const sig = await this.callMethodOnDevice(SerialPortSigner.METHOD_SIGN_MESSAGE, [draftWithId.id]);
|
||||
|
||||
const event: NostrEvent = { ...draftWithId, sig, pubkey };
|
||||
if (!this.verifyEvent(event)) throw new Error("Invalid signature");
|
||||
return event;
|
||||
}
|
||||
|
||||
public ping() {
|
||||
this.sendCommand(SerialPortSigner.METHOD_PING, [window.location.host]);
|
||||
}
|
||||
|
||||
// static const
|
||||
static SUPPORTED = !!navigator.serial;
|
||||
|
||||
static METHOD_PING = "/ping";
|
||||
static METHOD_LOG = "/log";
|
||||
|
||||
static METHOD_SIGN_MESSAGE = "/sign-message";
|
||||
static METHOD_SHARED_SECRET = "/shared-secret";
|
||||
static METHOD_PUBLIC_KEY = "/public-key";
|
||||
|
||||
static PUBLIC_METHODS = [
|
||||
SerialPortSigner.METHOD_PUBLIC_KEY,
|
||||
SerialPortSigner.METHOD_SIGN_MESSAGE,
|
||||
SerialPortSigner.METHOD_SHARED_SECRET,
|
||||
];
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { EventTemplate, finalizeEvent, generateSecretKey, getPublicKey, nip04, nip44 } from "nostr-tools";
|
||||
|
||||
export default class SimpleSigner {
|
||||
key: Uint8Array;
|
||||
constructor(key?: Uint8Array) {
|
||||
this.key = key || generateSecretKey();
|
||||
}
|
||||
|
||||
async getPublicKey() {
|
||||
return getPublicKey(this.key);
|
||||
}
|
||||
async signEvent(event: EventTemplate) {
|
||||
return finalizeEvent(event, this.key);
|
||||
}
|
||||
|
||||
nip04 = {
|
||||
encrypt: async (pubkey: string, plaintext: string) => nip04.encrypt(this.key, pubkey, plaintext),
|
||||
decrypt: async (pubkey: string, ciphertext: string) => nip04.decrypt(this.key, pubkey, ciphertext),
|
||||
};
|
||||
nip44 = {
|
||||
encrypt: async (pubkey: string, plaintext: string) =>
|
||||
nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
|
||||
decrypt: async (pubkey: string, ciphertext: string) =>
|
||||
nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-expect-error
|
||||
window.SimpleSigner = SimpleSigner;
|
||||
}
|
@ -2,12 +2,12 @@ import { SubCloser } from "nostr-tools/abstract-pool";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { generateSecretKey, nip19, NostrEvent } from "nostr-tools";
|
||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||
import { SimpleSigner } from "applesauce-signer";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import NostrWebRTCPeer, { Pool, RTCDescriptionEventKind, Signer } from "./nostr-webrtc-peer";
|
||||
import { isHex } from "../../helpers/nip19";
|
||||
import { logger } from "../../helpers/debug";
|
||||
import SimpleSigner from "../signers/simple-signer";
|
||||
|
||||
type EventMap = {
|
||||
call: [NostrEvent];
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
|
||||
import { Tag, isATag, isETag, isPTag } from "../types/nostr-event";
|
||||
import { safeRelayUrls } from "./relay";
|
||||
import { parseCoordinate } from "./nostr/event";
|
||||
|
||||
export function isHex(str?: string) {
|
||||
if (str?.match(/^[0-9a-f]+$/i)) return true;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { Queries } from "applesauce-core";
|
||||
import { ChannelMetadataQuery } from "applesauce-channel";
|
||||
|
||||
import { RequestOptions } from "../services/replaceable-events";
|
||||
import channelMetadataService from "../services/channel-metadata";
|
||||
@ -17,7 +17,7 @@ export default function useChannelMetadata(
|
||||
return channelMetadataService.requestMetadata(relays, channelId, opts);
|
||||
}, [channelId, Array.from(relays).join("|"), opts?.alwaysRequest, opts?.ignoreCache]);
|
||||
|
||||
const metadata = useStoreQuery(Queries.ChannelMetadataQuery, channel && [channel]);
|
||||
const metadata = useStoreQuery(ChannelMetadataQuery, channel && [channel]);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
@ -1,13 +1,8 @@
|
||||
import { AmberClipboardSigner } from "applesauce-signer";
|
||||
import { alwaysVerify } from "./verify-event";
|
||||
import AmberSigner from "../classes/signers/amber-signer";
|
||||
|
||||
/** @deprecated nip NostrConnectClient instead */
|
||||
const amberSignerService = new AmberSigner();
|
||||
/** @deprecated use AmberClipboardSigner class instead */
|
||||
const amberSignerService = new AmberClipboardSigner();
|
||||
amberSignerService.verifyEvent = alwaysVerify;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.amberSignerService = amberSignerService;
|
||||
}
|
||||
|
||||
export default amberSignerService;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import SerialPortSigner from "../classes/signers/serial-port-signer";
|
||||
import { SerialPortSigner } from "applesauce-signer";
|
||||
import { alwaysVerify } from "./verify-event";
|
||||
|
||||
/** @deprecated */
|
||||
/** @deprecated use SerialPortSigner class instead */
|
||||
const serialPortService = new SerialPortSigner();
|
||||
serialPortService.verifyEvent = alwaysVerify;
|
||||
|
||||
@ -11,9 +11,4 @@ setInterval(() => {
|
||||
}
|
||||
}, 1000 * 10);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
//@ts-ignore
|
||||
window.serialPortService = serialPortService;
|
||||
}
|
||||
|
||||
export default serialPortService;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NostrEvent, SimplePool } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import { SimpleSigner } from "applesauce-signer";
|
||||
|
||||
import { logger } from "../helpers/debug";
|
||||
import NostrWebRtcBroker from "../classes/webrtc/nostr-webrtc-broker";
|
||||
@ -7,7 +8,6 @@ import WebRtcRelayClient from "../classes/webrtc/webrtc-relay-client";
|
||||
import WebRtcRelayServer from "../classes/webrtc/webrtc-relay-server";
|
||||
import NostrWebRTCPeer from "../classes/webrtc/nostr-webrtc-peer";
|
||||
import verifyEventMethod from "./verify-event";
|
||||
import SimpleSigner from "../classes/signers/simple-signer";
|
||||
import { localRelay } from "./local-relay";
|
||||
import localSettings from "./local-settings";
|
||||
import { DEFAULT_ICE_SERVERS } from "../const";
|
||||
|
17
src/types/nostr-extensions.d.ts
vendored
17
src/types/nostr-extensions.d.ts
vendored
@ -1,21 +1,8 @@
|
||||
import { EventTemplate, NostrEvent, UnsignedEvent, VerifiedEvent } from "nostr-tools";
|
||||
|
||||
export type Nip07Signer = {
|
||||
getPublicKey: () => Promise<string> | string;
|
||||
signEvent: (template: EventTemplate) => Promise<VerifiedEvent> | VerifiedEvent;
|
||||
getRelays?: () => Record<string, { read: boolean; write: boolean }> | string[];
|
||||
nip04?: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
};
|
||||
nip44?: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
};
|
||||
};
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: Nip07Signer;
|
||||
nostr?: Nip07Interface;
|
||||
}
|
||||
}
|
||||
|
1
src/types/serial.d.ts
vendored
1
src/types/serial.d.ts
vendored
@ -1 +0,0 @@
|
||||
import "@types/dom-serial";
|
@ -2,7 +2,7 @@ import { memo, useCallback, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { Queries } from "applesauce-core/query-store";
|
||||
import { ChannelHiddenQuery, ChannelMessagesQuery, ChannelMutedQuery } from "applesauce-channel";
|
||||
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
@ -28,9 +28,9 @@ import { truncateId } from "../../helpers/string";
|
||||
import { useStoreQuery } from "../../hooks/use-store-query";
|
||||
|
||||
const ChannelChatLog = memo(({ timeline, channel }: { timeline: TimelineLoader; channel: NostrEvent }) => {
|
||||
const messages = useStoreQuery(Queries.ChannelMessagesQuery, [channel]) ?? [];
|
||||
const mutes = useStoreQuery(Queries.ChannelMutedQuery, [channel]);
|
||||
const hidden = useStoreQuery(Queries.ChannelHiddenQuery, [channel]);
|
||||
const messages = useStoreQuery(ChannelMessagesQuery, [channel]) ?? [];
|
||||
const mutes = useStoreQuery(ChannelMutedQuery, [channel]);
|
||||
const hidden = useStoreQuery(ChannelHiddenQuery, [channel]);
|
||||
|
||||
const filteredMessages = useMemo(
|
||||
() =>
|
||||
|
@ -1,16 +1,6 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Input,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { Box, Button, ButtonGroup, Divider, Flex, Heading, Text } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { SimpleSigner } from "applesauce-signer";
|
||||
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
@ -21,7 +11,6 @@ import accountService from "../../../services/account";
|
||||
import AccountTypeBadge from "../../../components/account-info-badge";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import PasswordSigner from "../../../classes/signers/password-signer";
|
||||
import SimpleSigner from "../../../classes/signers/simple-signer";
|
||||
import SimpleSignerBackup from "./simple-signer-backup";
|
||||
import PasswordSignerBackup from "./password-signer-backup";
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@ -11,16 +10,13 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { encrypt } from "nostr-tools/nip49";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import SimpleSigner from "../../../classes/signers/simple-signer";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import EyeOff from "../../../components/icons/eye-off";
|
||||
import Eye from "../../../components/icons/eye";
|
||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||
import PasswordSigner from "../../../classes/signers/password-signer";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
const fake = Array(48).fill("x");
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Heading,
|
||||
IconButton,
|
||||
@ -15,8 +13,8 @@ import {
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { encrypt } from "nostr-tools/nip49";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { SimpleSigner } from "applesauce-signer";
|
||||
|
||||
import SimpleSigner from "../../../classes/signers/simple-signer";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import EyeOff from "../../../components/icons/eye-off";
|
||||
import Eye from "../../../components/icons/eye";
|
||||
|
@ -1,21 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { Button, ButtonGroup, Divider, Flex, IconButton, Link, Spinner, Text, useToast } from "@chakra-ui/react";
|
||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
import { AmberClipboardSigner, SerialPortSigner } from "applesauce-signer";
|
||||
|
||||
import Key01 from "../../components/icons/key-01";
|
||||
import Diamond01 from "../../components/icons/diamond-01";
|
||||
import UsbFlashDrive from "../../components/icons/usb-flash-drive";
|
||||
import HelpCircle from "../../components/icons/help-circle";
|
||||
|
||||
import { COMMON_CONTACT_RELAY } from "../../const";
|
||||
import accountService from "../../services/account";
|
||||
import serialPortService from "../../services/serial-port";
|
||||
import amberSignerService from "../../services/amber-signer";
|
||||
import AmberSigner from "../../classes/signers/amber-signer";
|
||||
import { AtIcon } from "../../components/icons";
|
||||
import Package from "../../components/icons/package";
|
||||
import Eye from "../../components/icons/eye";
|
||||
import SerialPortSigner from "../../classes/signers/serial-port-signer";
|
||||
import ExtensionAccount from "../../classes/accounts/extension-account";
|
||||
import SerialPortAccount from "../../classes/accounts/serial-port-account";
|
||||
import AmberAccount from "../../classes/accounts/amber-account";
|
||||
@ -98,7 +96,7 @@ export default function LoginStartView() {
|
||||
/>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
{AmberSigner.SUPPORTED && (
|
||||
{AmberClipboardSigner.SUPPORTED && (
|
||||
<ButtonGroup colorScheme="orange" w="full">
|
||||
<Button onClick={signinWithAmber} leftIcon={<Diamond01 boxSize={6} />} flex={1}>
|
||||
Use Amber
|
||||
|
Loading…
x
Reference in New Issue
Block a user