aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/socket.js
blob: efc2651b12654dd4400a62a00d2412d2d1acb9a9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { get } from "svelte/store";
import { rate, user, token, ws } from '$lib/store';

let interval, socket;

export const messages = (data) => ({
	rate() {
		rate.set(data);
	},

	payment() {
		console.log(data);
	},

	connected() {
		socket.send(JSON.stringify({ type: 'login', data: { token: get(token) } }));
	},

	login() {
		user.set(data);
	}
});

const initialReconnectDelay = 1000;
const maxReconnectDelay = 16000;

let currentReconnectDelay = initialReconnectDelay;

export function connect() {
	clearInterval(interval);

  if (socket) socket.close();
	socket = new WebSocket('ws://localhost:3119/ws');
	socket.addEventListener('open', onWebsocketOpen);
	socket.addEventListener('close', onWebsocketClose);
	socket.addEventListener('message', onWebsocketMessage);

	interval = setInterval(
		() => socket.readyState === 1 && socket.send(JSON.stringify({ type: 'heartbeat' })),
		5000
	);

	ws.set(socket);
}

function onWebsocketMessage(msg) {
	let { type, data } = JSON.parse(msg.data);
	messages(data)[type] && messages(data)[type]();
}

function onWebsocketOpen() {
	currentReconnectDelay = initialReconnectDelay;
}

function onWebsocketClose() {
	ws.set(null);
	setTimeout(() => {
		reconnectToWebsocket();
	}, currentReconnectDelay + Math.floor(Math.random() * 3000));
}

function reconnectToWebsocket() {
	if (currentReconnectDelay < maxReconnectDelay) {
		currentReconnectDelay *= 2;
	}
	connect();
}
span class="w"> }; } export function decryptWithCode(code, ct) { return nip44.decrypt(ct, base64urlnopad.decode((code || '').trim())); } // Profiles live across the network, so look them up on popular relays (broader // than the sync relays). export const PROFILE_RELAYS = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.coinos.io']; // Fetch a recipient's profile (name + picture) for a pubkey, newest across relays. export async function fetchNostrProfile(pubkeyHex, relays = PROFILE_RELAYS) { let events; try { events = await pool.querySync(relays, { kinds: [0], authors: [pubkeyHex] }, { maxWait: 5000 }); } catch { return null; } const best = events.sort((a, b) => b.created_at - a.created_at)[0]; if (!best) return null; try { const m = JSON.parse(best.content); return { name: m.display_name || m.name || null, picture: m.picture || null, nip05: m.nip05 || null }; } catch { return null; } } // Where to deliver a DM to a pubkey: prefer their NIP-17 DM relay list (kind // 10050), else their NIP-65 read relays (kind 10002), else [] (caller falls back). export async function fetchInboxRelays(pubkeyHex, relays = PROFILE_RELAYS) { let events; try { events = await pool.querySync(relays, { kinds: [10002, 10050], authors: [pubkeyHex] }, { maxWait: 5000 }); } catch { return []; } const newest = (kind) => events.filter((e) => e.kind === kind).sort((a, b) => b.created_at - a.created_at)[0]; const dm = newest(10050); if (dm) { const r = dm.tags.filter((t) => t[0] === 'relay' && t[1]).map((t) => t[1]); if (r.length) return r; } const nip65 = newest(10002); if (nip65) { const r = nip65.tags.filter((t) => t[0] === 'r' && t[1] && (!t[2] || t[2] === 'read')).map((t) => t[1]); if (r.length) return r; } return []; } const DTAG = 'bitcoin-wallet'; const SYNC_KEY = 'btc-wallet-sync'; export const DEFAULT_SYNC_RELAYS = ['wss://relay.coinos.io']; // --- sync preference (enabled + relays), global, persisted in localStorage --- export function getSyncConfig() { try { const raw = localStorage.getItem(SYNC_KEY); if (raw) { const c = JSON.parse(raw); const relays = Array.isArray(c.relays) ? c.relays : []; return { enabled: c.enabled !== false, relays: relays.length ? relays : DEFAULT_SYNC_RELAYS, }; } } catch {} return { enabled: true, relays: DEFAULT_SYNC_RELAYS }; // default: on, coinos relay } export function setSyncConfig({ enabled, relays }) { try { localStorage.setItem(SYNC_KEY, JSON.stringify({ enabled: !!enabled, relays: relays || DEFAULT_SYNC_RELAYS })); } catch {} } export class NostrSync { constructor() { this.sk = null; this.pk = null; this.ck = null; // self conversation key for NIP-44 this.relays = DEFAULT_SYNC_RELAYS; } load(mnemonic, passphrase = '') { this.sk = nip06.privateKeyFromSeedWords(mnemonic, passphrase || undefined); this.pk = getPublicKey(this.sk); this.ck = nip44.getConversationKey(this.sk, this.pk); } unload() { this.sk = this.pk = this.ck = null; } setRelays(relays) { this.relays = Array.isArray(relays) && relays.length ? relays : DEFAULT_SYNC_RELAYS; } // Encrypt + publish the latest state to every relay (best-effort). async publish(stateObj) { if (!this.sk) return; let evt; try { const content = nip44.encrypt(JSON.stringify(stateObj), this.ck); evt = finalizeEvent({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', DTAG]], content }, this.sk); } catch { return; } await Promise.allSettled(pool.publish(this.relays, evt)); } // Deliver an encrypted DM (a locked gift's claim code) as a NIP-17 gift wrap // (kind 1059) — the modern standard every current client supports. Goes to the // recipient's published inbox relays (NIP-17/65) plus a broad fallback. Returns // true if ≥1 relay accepted it. async sendDM(recipientPkHex, text, relays = null) { if (!this.sk) return false; let evt; try { evt = nip17WrapEvent(this.sk, { publicKey: recipientPkHex }, text); } catch { return false; } let targets = relays; if (!targets) { const inbox = await fetchInboxRelays(recipientPkHex); targets = [...new Set([...inbox, ...PROFILE_RELAYS])].slice(0, 8); // recipient's inbox + safety net } const res = await Promise.allSettled(pool.publish(targets, evt)); return res.some((x) => x.status === 'fulfilled'); } // Fetch the newest decrypted state across relays, or null. async fetch() { if (!this.sk) return null; let events; try { events = await pool.querySync(this.relays, { kinds: [30078], authors: [this.pk], '#d': [DTAG] }, { maxWait: 6000 }); } catch { return null; } for (const e of events.sort((a, b) => b.created_at - a.created_at)) { try { const v = JSON.parse(nip44.decrypt(e.content, this.ck)); if (v) return v; } catch {} } return null; } }