snort/packages/app/src/Utils/Login/Functions.ts

168 lines
5.4 KiB
TypeScript

import * as utils from "@noble/curves/abstract/utils";
import * as secp from "@noble/curves/secp256k1";
import { unixNowMs } from "@snort/shared";
import {
EventPublisher,
KeyStorage,
Nip7Signer,
Nip46Signer,
PrivateKeySigner,
RelaySettings,
SystemInterface,
UserMetadata,
} from "@snort/system";
import { GiftsCache } from "@/Cache";
import SnortApi from "@/External/SnortApi";
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
import { Blasters } from "@/Utils/Const";
import { LoginSession, LoginSessionType, LoginStore, SnortAppData } from "@/Utils/Login/index";
import { entropyToPrivateKey, generateBip39Entropy } from "@/Utils/nip6";
import { SubscriptionEvent } from "@/Utils/Subscription";
import { Nip7OsSigner } from "./Nip7OsSigner";
export function logout(id: string) {
LoginStore.removeSession(id);
GiftsCache.clear();
deleteRefCode();
localStorage.clear();
}
export function markNotificationsRead(state: LoginSession) {
state.readNotifications = unixNowMs();
LoginStore.updateSession(state);
}
export function clearEntropy(state: LoginSession) {
state.generatedEntropy = undefined;
LoginStore.updateSession(state);
}
/**
* Generate a new key and login with this generated key
*/
export async function generateNewLogin(
system: SystemInterface,
pin: (key: string) => Promise<KeyStorage>,
profile: UserMetadata,
) {
const ent = generateBip39Entropy();
const entropy = utils.bytesToHex(ent);
const privateKey = entropyToPrivateKey(ent);
const newRelays = {} as Record<string, RelaySettings>;
// Use current timezone info to determine approx location
// use closest 5 relays
const country = getCountry();
const api = new SnortApi();
const closeRelays = await api.closeRelays(country.lat, country.lon, 20);
for (const cr of closeRelays.sort((a, b) => (a.distance > b.distance ? 1 : -1)).filter(a => !a.is_paid)) {
const rr = sanitizeRelayUrl(cr.url);
if (rr) {
newRelays[rr] = { read: true, write: true };
if (Object.keys(newRelays).length >= 5) {
break;
}
}
}
for (const [k, v] of Object.entries(CONFIG.defaultRelays)) {
if (!newRelays[k]) {
newRelays[k] = v;
}
}
// connect to new relays
await Promise.all(Object.entries(newRelays).map(([k, v]) => system.ConnectToRelay(k, v)));
const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
const publisher = EventPublisher.privateKey(privateKey);
// Create new contact list following self and site account
const contactList = [publicKey, ...CONFIG.signUp.defaultFollows.map(a => bech32ToHex(a))].map(a => ["p", a]) as Array<
[string, string]
>;
const ev = await publisher.contactList(contactList, newRelays);
system.BroadcastEvent(ev);
// Create relay metadata event
const ev2 = await publisher.relayList(newRelays);
system.BroadcastEvent(ev2);
Promise.all(Blasters.map(a => system.WriteOnceToRelay(a, ev2)));
// Publish new profile
const ev3 = await publisher.metadata(profile);
system.BroadcastEvent(ev3);
Promise.all(Blasters.map(a => system.WriteOnceToRelay(a, ev3)));
LoginStore.loginWithPrivateKey(await pin(privateKey), entropy, newRelays);
}
export function generateRandomKey() {
const privateKey = utils.bytesToHex(secp.schnorr.utils.randomPrivateKey());
const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
return {
privateKey,
publicKey,
};
}
export function updateSession(id: string, fn: (state: LoginSession) => void) {
const session = LoginStore.get(id);
if (session) {
fn(session);
LoginStore.updateSession(session);
}
}
export async function setAppData(state: LoginSession, data: SnortAppData) {
const pub = LoginStore.getPublisher(state.id);
if (!pub) return;
await state.state.setAppData(data);
LoginStore.updateSession(state);
}
export async function updateAppData(id: string, fn: (data: SnortAppData) => SnortAppData) {
const session = LoginStore.get(id);
if (session?.state.appdata) {
const next = fn(session.state.appdata);
await setAppData(session, next);
}
}
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
const newSubs = dedupeById([...(state.subscriptions || []), ...subs]);
if (newSubs.length !== state.subscriptions.length) {
state.subscriptions = newSubs;
LoginStore.updateSession(state);
}
}
export function sessionNeedsPin(l: LoginSession) {
return l.privateKeyData && l.privateKeyData.shouldUnlock();
}
export function createPublisher(l: LoginSession) {
switch (l.type) {
case LoginSessionType.PrivateKey: {
return EventPublisher.privateKey(unwrap(l.privateKeyData as KeyStorage).value);
}
case LoginSessionType.Nip46: {
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
const inner = new PrivateKeySigner(unwrap(l.privateKeyData as KeyStorage).value);
const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
nip46.on("oauth", url => {
window.open(url, CONFIG.appNameCapitalized, "width=600,height=800,popup=yes");
});
return new EventPublisher(nip46, unwrap(l.publicKey));
}
case LoginSessionType.Nip7os: {
return new EventPublisher(new Nip7OsSigner(), unwrap(l.publicKey));
}
case LoginSessionType.Nip7: {
return new EventPublisher(new Nip7Signer(), unwrap(l.publicKey));
}
}
}