feat: nsecbunker
This commit is contained in:
parent
64d64b29c3
commit
1cb27c1881
@ -1,15 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { EventPublisher, Nip7Signer } from "@snort/system";
|
|
||||||
|
|
||||||
export default function useEventPublisher() {
|
export default function useEventPublisher() {
|
||||||
const { publicKey, privateKey } = useLogin();
|
const { publisher } = useLogin();
|
||||||
return useMemo(() => {
|
return publisher;
|
||||||
if (privateKey) {
|
|
||||||
return EventPublisher.privateKey(privateKey);
|
|
||||||
}
|
|
||||||
if (publicKey) {
|
|
||||||
return new EventPublisher(new Nip7Signer(), publicKey);
|
|
||||||
}
|
|
||||||
}, [publicKey, privateKey]);
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { EmailRegex, MnemonicRegex } from "Const";
|
import { EmailRegex, MnemonicRegex } from "Const";
|
||||||
import { LoginStore } from "Login";
|
import { LoginSessionType, LoginStore } from "Login";
|
||||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||||
import { getNip05PubKey } from "Pages/LoginPage";
|
import { getNip05PubKey } from "Pages/LoginPage";
|
||||||
import { bech32ToHex } from "SnortUtils";
|
import { bech32ToHex } from "SnortUtils";
|
||||||
@ -28,10 +28,10 @@ export default function useLoginHandler() {
|
|||||||
}
|
}
|
||||||
} else if (key.startsWith("npub")) {
|
} else if (key.startsWith("npub")) {
|
||||||
const hexKey = bech32ToHex(key);
|
const hexKey = bech32ToHex(key);
|
||||||
LoginStore.loginWithPubkey(hexKey);
|
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
|
||||||
} else if (key.match(EmailRegex)) {
|
} else if (key.match(EmailRegex)) {
|
||||||
const hexKey = await getNip05PubKey(key);
|
const hexKey = await getNip05PubKey(key);
|
||||||
LoginStore.loginWithPubkey(hexKey);
|
LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
|
||||||
} else if (key.match(MnemonicRegex)?.length === 24) {
|
} else if (key.match(MnemonicRegex)?.length === 24) {
|
||||||
if (!hasSubtleCrypto) {
|
if (!hasSubtleCrypto) {
|
||||||
throw new Error(insecureMsg);
|
throw new Error(insecureMsg);
|
||||||
@ -50,7 +50,8 @@ export default function useLoginHandler() {
|
|||||||
await nip46.init();
|
await nip46.init();
|
||||||
|
|
||||||
const loginPubkey = await nip46.getPubKey();
|
const loginPubkey = await nip46.getPubKey();
|
||||||
LoginStore.loginWithPubkey(loginPubkey);
|
LoginStore.loginWithPubkey(loginPubkey, LoginSessionType.Nip46, undefined, nip46.relays);
|
||||||
|
nip46.close();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("INVALID PRIVATE KEY");
|
throw new Error("INVALID PRIVATE KEY");
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { HexKey, RelaySettings, u256 } from "@snort/system";
|
import { HexKey, RelaySettings, u256, EventPublisher } from "@snort/system";
|
||||||
import { UserPreferences } from "Login";
|
import { UserPreferences } from "Login";
|
||||||
import { SubscriptionEvent } from "Subscription";
|
import { SubscriptionEvent } from "Subscription";
|
||||||
|
|
||||||
@ -10,7 +10,19 @@ interface Newest<T> {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LoginSessionType {
|
||||||
|
PrivateKey = "private_key",
|
||||||
|
PublicKey = "public_key",
|
||||||
|
Nip7 = "nip7",
|
||||||
|
Nip46 = "nip46",
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginSession {
|
export interface LoginSession {
|
||||||
|
/**
|
||||||
|
* Type of login session
|
||||||
|
*/
|
||||||
|
type: LoginSessionType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current user private key
|
* Current user private key
|
||||||
*/
|
*/
|
||||||
@ -80,4 +92,14 @@ export interface LoginSession {
|
|||||||
* Snort subscriptions licences
|
* Snort subscriptions licences
|
||||||
*/
|
*/
|
||||||
subscriptions: Array<SubscriptionEvent>;
|
subscriptions: Array<SubscriptionEvent>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote signer relays (NIP-46)
|
||||||
|
*/
|
||||||
|
remoteSignerRelays?: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance event publisher
|
||||||
|
*/
|
||||||
|
publisher?: EventPublisher;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import * as secp from "@noble/curves/secp256k1";
|
import * as secp from "@noble/curves/secp256k1";
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
|
|
||||||
import { HexKey, RelaySettings } from "@snort/system";
|
import { HexKey, RelaySettings, EventPublisher, Nip46Signer, Nip7Signer } from "@snort/system";
|
||||||
import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
|
import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
|
||||||
|
|
||||||
import { DefaultRelays } from "Const";
|
import { DefaultRelays } from "Const";
|
||||||
import { LoginSession } from "Login";
|
import { LoginSession, LoginSessionType } from "Login";
|
||||||
import { DefaultPreferences, UserPreferences } from "./Preferences";
|
import { DefaultPreferences, UserPreferences } from "./Preferences";
|
||||||
|
|
||||||
const AccountStoreKey = "sessions";
|
const AccountStoreKey = "sessions";
|
||||||
const LoggedOut = {
|
const LoggedOut = {
|
||||||
|
type: "public_key",
|
||||||
preferences: DefaultPreferences,
|
preferences: DefaultPreferences,
|
||||||
tags: {
|
tags: {
|
||||||
item: [],
|
item: [],
|
||||||
@ -60,7 +61,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
super();
|
super();
|
||||||
const existing = window.localStorage.getItem(AccountStoreKey);
|
const existing = window.localStorage.getItem(AccountStoreKey);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
this.#accounts = new Map((JSON.parse(existing) as Array<LoginSession>).map(a => [unwrap(a.publicKey), a]));
|
const logins = JSON.parse(existing);
|
||||||
|
this.#accounts = new Map((logins as Array<LoginSession>).map(a => [unwrap(a.publicKey), a]));
|
||||||
} else {
|
} else {
|
||||||
this.#accounts = new Map();
|
this.#accounts = new Map();
|
||||||
}
|
}
|
||||||
@ -68,6 +70,9 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (!this.#activeAccount) {
|
if (!this.#activeAccount) {
|
||||||
this.#activeAccount = this.#accounts.keys().next().value;
|
this.#activeAccount = this.#accounts.keys().next().value;
|
||||||
}
|
}
|
||||||
|
for (const [, v] of this.#accounts) {
|
||||||
|
v.publisher = this.#createPublisher(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSessions() {
|
getSessions() {
|
||||||
@ -85,20 +90,28 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loginWithPubkey(key: HexKey, relays?: Record<string, RelaySettings>) {
|
loginWithPubkey(
|
||||||
|
key: HexKey,
|
||||||
|
type: LoginSessionType,
|
||||||
|
relays?: Record<string, RelaySettings>,
|
||||||
|
remoteSignerRelays?: Array<string>
|
||||||
|
) {
|
||||||
if (this.#accounts.has(key)) {
|
if (this.#accounts.has(key)) {
|
||||||
throw new Error("Already logged in with this pubkey");
|
throw new Error("Already logged in with this pubkey");
|
||||||
}
|
}
|
||||||
const initRelays = this.decideInitRelays(relays);
|
const initRelays = this.decideInitRelays(relays);
|
||||||
const newSession = {
|
const newSession = {
|
||||||
...LoggedOut,
|
...LoggedOut,
|
||||||
|
type,
|
||||||
publicKey: key,
|
publicKey: key,
|
||||||
relays: {
|
relays: {
|
||||||
item: initRelays,
|
item: initRelays,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
},
|
},
|
||||||
preferences: deepClone(DefaultPreferences),
|
preferences: deepClone(DefaultPreferences),
|
||||||
|
remoteSignerRelays,
|
||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
|
newSession.publisher = this.#createPublisher(newSession);
|
||||||
|
|
||||||
this.#accounts.set(key, newSession);
|
this.#accounts.set(key, newSession);
|
||||||
this.#activeAccount = key;
|
this.#activeAccount = key;
|
||||||
@ -121,6 +134,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
||||||
const newSession = {
|
const newSession = {
|
||||||
...LoggedOut,
|
...LoggedOut,
|
||||||
|
type: LoginSessionType.PrivateKey,
|
||||||
privateKey: key,
|
privateKey: key,
|
||||||
publicKey: pubKey,
|
publicKey: pubKey,
|
||||||
generatedEntropy: entropy,
|
generatedEntropy: entropy,
|
||||||
@ -130,6 +144,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
},
|
},
|
||||||
preferences: deepClone(DefaultPreferences),
|
preferences: deepClone(DefaultPreferences),
|
||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
|
newSession.publisher = this.#createPublisher(newSession);
|
||||||
|
|
||||||
this.#accounts.set(pubKey, newSession);
|
this.#accounts.set(pubKey, newSession);
|
||||||
this.#activeAccount = pubKey;
|
this.#activeAccount = pubKey;
|
||||||
this.#save();
|
this.#save();
|
||||||
@ -157,7 +173,26 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
|
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
|
||||||
if (!s) return LoggedOut;
|
if (!s) return LoggedOut;
|
||||||
|
|
||||||
return deepClone(s);
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createPublisher(l: LoginSession) {
|
||||||
|
switch (l.type) {
|
||||||
|
case LoginSessionType.PrivateKey: {
|
||||||
|
return EventPublisher.privateKey(unwrap(l.privateKey));
|
||||||
|
}
|
||||||
|
case LoginSessionType.Nip46: {
|
||||||
|
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
|
||||||
|
const inner = new Nip7Signer();
|
||||||
|
const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
|
||||||
|
return new EventPublisher(nip46, unwrap(l.publicKey));
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (l.publicKey) {
|
||||||
|
return new EventPublisher(new Nip7Signer(), l.publicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#migrate() {
|
#migrate() {
|
||||||
@ -203,6 +238,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update session types
|
||||||
|
for (const [, v] of this.#accounts) {
|
||||||
|
if (v.privateKey) {
|
||||||
|
v.type = LoginSessionType.PrivateKey;
|
||||||
|
didMigrate = true;
|
||||||
|
}
|
||||||
|
if (!v.type) {
|
||||||
|
v.type = LoginSessionType.Nip7;
|
||||||
|
didMigrate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (didMigrate) {
|
if (didMigrate) {
|
||||||
console.debug("Finished migration to MultiAccountStore");
|
console.debug("Finished migration to MultiAccountStore");
|
||||||
this.#save();
|
this.#save();
|
||||||
|
@ -3,16 +3,22 @@ import "./LoginPage.css";
|
|||||||
import { CSSProperties, useEffect, useState } from "react";
|
import { CSSProperties, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useIntl, FormattedMessage } from "react-intl";
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import { HexKey } from "@snort/system";
|
import { HexKey, Nip46Signer, PrivateKeySigner } from "@snort/system";
|
||||||
|
|
||||||
import { bech32ToHex, unwrap } from "SnortUtils";
|
import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils";
|
||||||
import ZapButton from "Element/ZapButton";
|
import ZapButton from "Element/ZapButton";
|
||||||
import useImgProxy from "Hooks/useImgProxy";
|
import useImgProxy from "Hooks/useImgProxy";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { generateNewLogin, LoginStore } from "Login";
|
import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useLoginHandler from "Hooks/useLoginHandler";
|
import useLoginHandler from "Hooks/useLoginHandler";
|
||||||
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||||
|
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||||
|
import Modal from "Element/Modal";
|
||||||
|
import QrCode from "Element/QrCode";
|
||||||
|
import Copy from "Element/Copy";
|
||||||
|
import { delay } from "SnortUtils";
|
||||||
|
|
||||||
interface ArtworkEntry {
|
interface ArtworkEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@ -73,6 +79,7 @@ export default function LoginPage() {
|
|||||||
const loginHandler = useLoginHandler();
|
const loginHandler = useLoginHandler();
|
||||||
const hasNip7 = "nostr" in window;
|
const hasNip7 = "nostr" in window;
|
||||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||||
|
const [nostrConnect, setNostrConnect] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (login.publicKey) {
|
if (login.publicKey) {
|
||||||
@ -112,7 +119,27 @@ export default function LoginPage() {
|
|||||||
const relays =
|
const relays =
|
||||||
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
|
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
|
||||||
const pubKey = await unwrap(window.nostr).getPublicKey();
|
const pubKey = await unwrap(window.nostr).getPublicKey();
|
||||||
LoginStore.loginWithPubkey(pubKey, relays);
|
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startNip46() {
|
||||||
|
const meta = {
|
||||||
|
name: "Snort",
|
||||||
|
url: window.location.href,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newKey = bytesToHex(secp256k1.utils.randomPrivateKey());
|
||||||
|
const relays = ["wss://relay.damus.io"].map(a => `relay=${encodeURIComponent(a)}`);
|
||||||
|
const connectUrl = `nostrconnect://${getPublicKey(newKey)}?${[
|
||||||
|
...relays,
|
||||||
|
`metadata=${encodeURIComponent(JSON.stringify(meta))}`,
|
||||||
|
].join("&")}`;
|
||||||
|
setNostrConnect(connectUrl);
|
||||||
|
|
||||||
|
const signer = new Nip46Signer(connectUrl, new PrivateKeySigner(newKey));
|
||||||
|
await signer.init();
|
||||||
|
await delay(500);
|
||||||
|
await signer.describe();
|
||||||
}
|
}
|
||||||
|
|
||||||
function altLogins() {
|
function altLogins() {
|
||||||
@ -121,12 +148,25 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" onClick={doNip07Login}>
|
<>
|
||||||
<FormattedMessage
|
<AsyncButton type="button" onClick={doNip07Login}>
|
||||||
defaultMessage="Login with Extension (NIP-07)"
|
<FormattedMessage
|
||||||
description="Login button for NIP7 key manager extension"
|
defaultMessage="Login with Extension (NIP-07)"
|
||||||
/>
|
description="Login button for NIP7 key manager extension"
|
||||||
</button>
|
/>
|
||||||
|
</AsyncButton>
|
||||||
|
<AsyncButton type="button" onClick={startNip46}>
|
||||||
|
<FormattedMessage defaultMessage="Nostr Connect (NIP-46)" description="Login button for NIP-46 signer app" />
|
||||||
|
</AsyncButton>
|
||||||
|
{nostrConnect && (
|
||||||
|
<Modal onClose={() => setNostrConnect("")}>
|
||||||
|
<div className="flex f-col">
|
||||||
|
<QrCode data={nostrConnect} />
|
||||||
|
<Copy text={nostrConnect} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,9 +267,9 @@ export default function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div dir="auto" className="login-actions">
|
<div dir="auto" className="login-actions">
|
||||||
<button type="button" onClick={doLogin}>
|
<AsyncButton type="button" onClick={doLogin}>
|
||||||
<FormattedMessage defaultMessage="Login" description="Login button" />
|
<FormattedMessage defaultMessage="Login" description="Login button" />
|
||||||
</button>
|
</AsyncButton>
|
||||||
<AsyncButton onClick={() => makeRandomKey()}>
|
<AsyncButton onClick={() => makeRandomKey()}>
|
||||||
<FormattedMessage defaultMessage="Create Account" />
|
<FormattedMessage defaultMessage="Create Account" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
@ -27,64 +27,93 @@ interface Nip46Request {
|
|||||||
interface Nip46Response {
|
interface Nip46Response {
|
||||||
id: string
|
id: string
|
||||||
result: any
|
result: any
|
||||||
error?: string
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueObj {
|
interface QueueObj {
|
||||||
resolve: (o: Nip46Response) => void;
|
resolve: (o: any) => void;
|
||||||
reject: (e: Error) => void;
|
reject: (e: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip46Signer implements EventSigner {
|
export class Nip46Signer implements EventSigner {
|
||||||
#conn?: Connection;
|
#conn?: Connection;
|
||||||
#relay: string;
|
#relay: string;
|
||||||
#target: string;
|
#localPubkey: string;
|
||||||
|
#remotePubkey?: string;
|
||||||
#token?: string;
|
#token?: string;
|
||||||
#insideSigner: EventSigner;
|
#insideSigner: EventSigner;
|
||||||
#commandQueue: Map<string, QueueObj> = new Map();
|
#commandQueue: Map<string, QueueObj> = new Map();
|
||||||
#log = debug("NIP-46");
|
#log = debug("NIP-46");
|
||||||
|
#proto: string;
|
||||||
|
#didInit: boolean = false;
|
||||||
|
|
||||||
constructor(config: string, insideSigner?: EventSigner) {
|
constructor(config: string, insideSigner?: EventSigner) {
|
||||||
const u = new URL(config);
|
const u = new URL(config);
|
||||||
this.#target = u.pathname.substring(2);
|
this.#proto = u.protocol;
|
||||||
|
this.#localPubkey = u.pathname.substring(2);
|
||||||
|
|
||||||
if (u.hash.length > 1) {
|
if (u.hash.length > 1) {
|
||||||
this.#token = u.hash.substring(1);
|
this.#token = u.hash.substring(1);
|
||||||
}
|
}
|
||||||
if (this.#target.startsWith("npub")) {
|
if (this.#localPubkey.startsWith("npub")) {
|
||||||
this.#target = bech32ToHex(this.#target);
|
this.#localPubkey = bech32ToHex(this.#localPubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#relay = unwrap(u.searchParams.get("relay"));
|
this.#relay = unwrap(u.searchParams.get("relay"));
|
||||||
this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey())
|
this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get relays() {
|
||||||
|
return [this.#relay];
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
const isBunker = this.#proto === "bunker:";
|
||||||
|
if (isBunker) {
|
||||||
|
this.#remotePubkey = this.#localPubkey;
|
||||||
|
this.#localPubkey = await this.#insideSigner.getPubKey();
|
||||||
|
}
|
||||||
return await new Promise<void>((resolve, reject) => {
|
return await new Promise<void>((resolve, reject) => {
|
||||||
this.#conn = new Connection(this.#relay, { read: true, write: true });
|
this.#conn = new Connection(this.#relay, { read: true, write: true });
|
||||||
this.#conn.OnEvent = async (sub, e) => {
|
this.#conn.OnEvent = async (sub, e) => {
|
||||||
await this.#onReply(e);
|
await this.#onReply(e);
|
||||||
}
|
}
|
||||||
this.#conn.OnConnected = async () => {
|
this.#conn.OnConnected = async () => {
|
||||||
const insidePubkey = await this.#insideSigner.getPubKey();
|
|
||||||
this.#conn!.QueueReq(["REQ", "reply", {
|
this.#conn!.QueueReq(["REQ", "reply", {
|
||||||
kinds: [NIP46_KIND],
|
kinds: [NIP46_KIND],
|
||||||
authors: [this.#target],
|
"#p": [this.#localPubkey]
|
||||||
"#p": [insidePubkey]
|
|
||||||
}], () => { });
|
}], () => { });
|
||||||
|
|
||||||
const rsp = await this.#connect(insidePubkey);
|
if (isBunker) {
|
||||||
if (rsp === "ack") {
|
await this.#connect(unwrap(this.#remotePubkey));
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject();
|
this.#commandQueue.set("connect", {
|
||||||
|
reject,
|
||||||
|
resolve
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#conn.Connect();
|
this.#conn.Connect();
|
||||||
|
this.#didInit = true;
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
if (this.#conn) {
|
||||||
|
await this.#disconnect();
|
||||||
|
this.#conn.CloseReq("reply");
|
||||||
|
this.#conn.Close();
|
||||||
|
this.#conn = undefined;
|
||||||
|
this.#didInit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async describe() {
|
||||||
|
return await this.#rpc<Array<string>>("describe", []);
|
||||||
|
}
|
||||||
|
|
||||||
async getPubKey() {
|
async getPubKey() {
|
||||||
return await this.#rpc<string>("get_public_key", []);
|
return await this.#rpc<string>("get_public_key", []);
|
||||||
}
|
}
|
||||||
@ -98,7 +127,12 @@ export class Nip46Signer implements EventSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sign(ev: NostrEvent) {
|
async sign(ev: NostrEvent) {
|
||||||
return await this.#rpc<NostrEvent>("nip04_decrypt", [ev]);
|
const evStr = await this.#rpc<string>("sign_event", [JSON.stringify(ev)]);
|
||||||
|
return JSON.parse(evStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #disconnect() {
|
||||||
|
return await this.#rpc("disconnect", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #connect(pk: string) {
|
async #connect(pk: string) {
|
||||||
@ -114,10 +148,21 @@ export class Nip46Signer implements EventSigner {
|
|||||||
throw new Error("Unknown event kind");
|
throw new Error("Unknown event kind");
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, this.#target);
|
const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, e.pubkey);
|
||||||
const reply = JSON.parse(decryptedContent) as Nip46Response;
|
const reply = JSON.parse(decryptedContent) as Nip46Request | Nip46Response;
|
||||||
|
|
||||||
const pending = this.#commandQueue.get(reply.id);
|
let id = reply.id;
|
||||||
|
this.#log("Recv: %O", reply);
|
||||||
|
if ("method" in reply && reply.method === "connect") {
|
||||||
|
this.#remotePubkey = reply.params[0];
|
||||||
|
await this.#sendCommand({
|
||||||
|
id: reply.id,
|
||||||
|
result: "ack",
|
||||||
|
error: ""
|
||||||
|
}, unwrap(this.#remotePubkey));
|
||||||
|
id = "connect";
|
||||||
|
}
|
||||||
|
const pending = this.#commandQueue.get(id);
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
throw new Error("No pending command found");
|
throw new Error("No pending command found");
|
||||||
}
|
}
|
||||||
@ -127,32 +172,38 @@ export class Nip46Signer implements EventSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #rpc<T>(method: string, params: Array<any>) {
|
async #rpc<T>(method: string, params: Array<any>) {
|
||||||
|
if (!this.#didInit) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
if (!this.#conn) throw new Error("Connection error");
|
if (!this.#conn) throw new Error("Connection error");
|
||||||
|
|
||||||
const id = uuid();
|
|
||||||
const payload = {
|
const payload = {
|
||||||
id,
|
id: uuid(),
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
} as Nip46Request;
|
} as Nip46Request;
|
||||||
this.#log("Request: %O", payload);
|
|
||||||
|
|
||||||
const eb = new EventBuilder();
|
|
||||||
eb.kind(NIP46_KIND as EventKind)
|
|
||||||
.content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), this.#target))
|
|
||||||
.tag(["p", this.#target]);
|
|
||||||
|
|
||||||
const evCommand = await eb.buildAndSign(this.#insideSigner);
|
|
||||||
await this.#conn.SendAsync(evCommand);
|
|
||||||
|
|
||||||
|
this.#sendCommand(payload, unwrap(this.#remotePubkey));
|
||||||
return await new Promise<T>((resolve, reject) => {
|
return await new Promise<T>((resolve, reject) => {
|
||||||
this.#commandQueue.set(id, {
|
this.#commandQueue.set(payload.id, {
|
||||||
resolve: async (o: Nip46Response) => {
|
resolve: async (o: Nip46Response) => {
|
||||||
this.#log("Reply: %O", o);
|
|
||||||
resolve(o.result as T);
|
resolve(o.result as T);
|
||||||
},
|
},
|
||||||
reject,
|
reject,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #sendCommand(payload: Nip46Request | Nip46Response, target: string) {
|
||||||
|
if (!this.#conn) return;
|
||||||
|
|
||||||
|
const eb = new EventBuilder();
|
||||||
|
eb.kind(NIP46_KIND as EventKind)
|
||||||
|
.content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), target))
|
||||||
|
.tag(["p", target]);
|
||||||
|
|
||||||
|
this.#log("Send: %O", payload);
|
||||||
|
const evCommand = await eb.buildAndSign(this.#insideSigner);
|
||||||
|
await this.#conn.SendAsync(evCommand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user