1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts) - Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode) - Generates and warns about ephemeral key if env var absent - sign(EventTemplate) → finalizeEvent() with Timmy's key - encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt() - Logs npub at server startup 2. ZapService (artifacts/api-server/src/lib/zap.ts) - Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key - Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware) - Logs every outbound event to timmy_nostr_events audit table - maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess() - Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10) - Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0 3. Engagement engine (artifacts/api-server/src/lib/engagement.ts) - Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled) - Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold - Generates personalised DM via agentService.chatReply() - Encrypts as NIP-04 DM (kind 4), signs with Timmy's key - Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set - First run delayed 60s after startup to avoid cold-start noise 4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts) - POST /api/identity/vouch: requires X-Nostr-Token with elite tier - Verifies optional Nostr event signature from voucher - Records relationship in nostr_trust_vouches table - Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score - GET /api/identity/timmy: public endpoint returning npub + zap count 5. DB schema additions (lib/db/src/schema/) - timmy_nostr_events: audit log for all outbound Nostr events - nostr_trust_vouches: voucher/vouchee social graph with boost amount - Tables created in production DB via drizzle-kit push 6. Frontend identity card (the-matrix/) - #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count - timmy-id.js: initTimmyId() fetches /api/identity/timmy on load - Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback - Refreshes every 60s to show live zap count - Wired into main.js on firstInit
132 lines
3.8 KiB
JavaScript
132 lines
3.8 KiB
JavaScript
import { initWorld, onWindowResize, disposeWorld } from './world.js';
|
|
import {
|
|
initAgents, updateAgents, getAgentCount,
|
|
disposeAgents, getAgentStates, applyAgentStates,
|
|
getTimmyGroup, applySlap, getCameraShakeStrength,
|
|
} from './agents.js';
|
|
import { initEffects, updateEffects, disposeEffects } from './effects.js';
|
|
import { initUI, updateUI } from './ui.js';
|
|
import { initInteraction, disposeInteraction, registerSlapTarget } from './interaction.js';
|
|
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
|
|
import { initPaymentPanel } from './payment.js';
|
|
import { initSessionPanel } from './session.js';
|
|
import { initNostrIdentity } from './nostr-identity.js';
|
|
import { warmup as warmupEdgeWorker, onReady as onEdgeWorkerReady } from './edge-worker-client.js';
|
|
import { setEdgeWorkerReady } from './ui.js';
|
|
import { initTimmyId } from './timmy-id.js';
|
|
|
|
let running = false;
|
|
let canvas = null;
|
|
|
|
function buildWorld(firstInit, stateSnapshot) {
|
|
const { scene, camera, renderer } = initWorld(canvas);
|
|
canvas = renderer.domElement;
|
|
|
|
initEffects(scene);
|
|
initAgents(scene);
|
|
|
|
if (stateSnapshot) applyAgentStates(stateSnapshot);
|
|
|
|
initInteraction(camera, renderer);
|
|
registerSlapTarget(getTimmyGroup(), applySlap);
|
|
|
|
if (firstInit) {
|
|
initUI();
|
|
initWebSocket(scene);
|
|
initPaymentPanel();
|
|
initSessionPanel();
|
|
// Nostr identity init (async — non-blocking)
|
|
void initNostrIdentity('/api');
|
|
// Warm up edge-worker models in the background; show ready badge when done
|
|
warmupEdgeWorker();
|
|
onEdgeWorkerReady(() => setEdgeWorkerReady());
|
|
// Fetch Timmy's Nostr identity and populate identity card
|
|
void initTimmyId();
|
|
}
|
|
|
|
const ac = new AbortController();
|
|
window.addEventListener('resize', () => onWindowResize(camera, renderer), { signal: ac.signal });
|
|
|
|
let frameCount = 0;
|
|
let lastFpsTime = performance.now();
|
|
let currentFps = 0;
|
|
|
|
running = true;
|
|
|
|
function animate() {
|
|
if (!running) return;
|
|
requestAnimationFrame(animate);
|
|
|
|
const now = performance.now();
|
|
frameCount++;
|
|
if (now - lastFpsTime >= 1000) {
|
|
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
|
|
frameCount = 0;
|
|
lastFpsTime = now;
|
|
}
|
|
|
|
updateEffects(now);
|
|
updateAgents(now);
|
|
updateUI({
|
|
fps: currentFps,
|
|
agentCount: getAgentCount(),
|
|
jobCount: getJobCount(),
|
|
connectionState: getConnectionState(),
|
|
});
|
|
|
|
// Camera shake — apply transient offset, render, then restore (no drift)
|
|
const shakeStr = getCameraShakeStrength();
|
|
let sx = 0, sy = 0;
|
|
if (shakeStr > 0) {
|
|
const mag = shakeStr * 0.22;
|
|
sx = (Math.random() - 0.5) * mag;
|
|
sy = (Math.random() - 0.5) * mag * 0.45;
|
|
camera.position.x += sx;
|
|
camera.position.y += sy;
|
|
}
|
|
renderer.render(scene, camera);
|
|
if (shakeStr > 0) {
|
|
camera.position.x -= sx;
|
|
camera.position.y -= sy;
|
|
}
|
|
}
|
|
|
|
animate();
|
|
return { scene, renderer, ac };
|
|
}
|
|
|
|
function teardown({ scene, renderer, ac }) {
|
|
running = false;
|
|
ac.abort();
|
|
disposeInteraction();
|
|
disposeEffects();
|
|
disposeAgents();
|
|
disposeWorld(renderer, scene);
|
|
}
|
|
|
|
function main() {
|
|
const $overlay = document.getElementById('webgl-recovery-overlay');
|
|
let handle = buildWorld(true, null);
|
|
|
|
canvas.addEventListener('webglcontextlost', event => {
|
|
event.preventDefault();
|
|
running = false;
|
|
if ($overlay) $overlay.style.display = 'flex';
|
|
});
|
|
|
|
canvas.addEventListener('webglcontextrestored', () => {
|
|
const snapshot = getAgentStates();
|
|
teardown(handle);
|
|
handle = buildWorld(false, snapshot);
|
|
if ($overlay) $overlay.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
main();
|
|
|
|
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register(import.meta.env.BASE_URL + 'sw.js').catch(() => {});
|
|
});
|
|
}
|