import { randomUUID } from 'node:crypto'; import { resolve } from 'node:path'; import { createWallet, importWalletMnemonic, getWallet, listWallets, createPolicy, deletePolicy, createApiKey, revokeApiKey, deleteWallet, exportWallet, type WalletInfo, type ApiKeyResult, } from '@open-wallet-standard/core'; // ── Types ────────────────────────────────────────────── export interface DelegationRequest { id: string; agentAddress: string; reason: string; chains: string[]; // CAIP-2 chain IDs e.g. ["eip155:8453"] operations: string[]; // "sign_message" | "sign_tx" maxAmountPerAction: string; // e.g. "100.00" totalBudget: string; // e.g. "500.00" allowedRecipients?: string[]; // vendor allowlist — approved contract/wallet addresses expiresAt: string; // ISO 8601 status: 'pending' | 'approved' | 'rejected' | 'expired' | 'revoked'; createdAt: string; decidedAt?: string; sessionKeyId?: string; sessionKeyToken?: string; policyId?: string; opsUsed: number; zerionPortfolio?: WalletPortfolio; aiInterpretation?: AiInterpretation; } export interface WalletPortfolio { totalValueUsd: number; tokens: { symbol: string; balance: string; valueUsd: number }[]; } export interface AiInterpretation { summary: string; riskLevel: 'low' | 'medium' | 'high'; warnings: string[]; suggestedBounds?: string; } export interface AgentPnL { totalEarned: number; totalSpent: number; profit: number; queryCount: number; } // ── Config ───────────────────────────────────────────── export const OWS_VAULT = process.env.OWS_VAULT_PATH ?? undefined; // undefined = OWS default (~/.ows/) const DATA_DIR = process.env.SPENDOS_DATA_DIR ?? './data'; // ── State (persisted to disk) ────────────────────────── import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'; const DELEGATIONS_FILE = `${DATA_DIR}/delegations.json`; const PNL_FILE = `${DATA_DIR}/pnl.json`; mkdirSync(DATA_DIR, { recursive: true }); function loadDelegations(): Map { if (!existsSync(DELEGATIONS_FILE)) return new Map(); try { const arr: DelegationRequest[] = JSON.parse(readFileSync(DELEGATIONS_FILE, 'utf-8')); return new Map(arr.map(d => [d.id, d])); } catch { return new Map(); } } function saveDelegations(): void { writeFileSync(DELEGATIONS_FILE, JSON.stringify(Array.from(delegations.values()), null, 2)); } function loadPnL(): AgentPnL { if (!existsSync(PNL_FILE)) return { totalEarned: 0, totalSpent: 0, profit: 0, queryCount: 0 }; try { return JSON.parse(readFileSync(PNL_FILE, 'utf-8')); } catch { return { totalEarned: 0, totalSpent: 0, profit: 0, queryCount: 0 }; } } function savePnL(): void { writeFileSync(PNL_FILE, JSON.stringify(pnl, null, 2)); } const delegations = loadDelegations(); const pnl: AgentPnL = loadPnL(); console.log(`[SpendOS] Loaded ${delegations.size} delegations, P&L: $${pnl.totalEarned.toFixed(3)} earned`); const WALLET_NAME = 'spendos-treasury'; const PASSPHRASE = process.env.OWS_PASSPHRASE ?? 'spendos-demo'; let treasuryWallet: WalletInfo | null = null; // ── Wallet Init ──────────────────────────────────────── export function initWallet(): WalletInfo { if (OWS_VAULT) console.log(`[SpendOS] OWS vault: ${OWS_VAULT}`); const importMnemonic = process.env.OWS_IMPORT_MNEMONIC; const forceReimport = process.env.OWS_FORCE_REIMPORT === 'true'; // If force reimport, delete existing wallet first if (forceReimport && importMnemonic) { try { deleteWallet(WALLET_NAME, OWS_VAULT); console.log(`[SpendOS] Deleted existing wallet for reimport`); } catch { /* didn't exist */ } } try { treasuryWallet = getWallet(WALLET_NAME, OWS_VAULT); console.log(`[SpendOS] Loaded existing wallet: ${WALLET_NAME}`); } catch { if (importMnemonic) { treasuryWallet = importWalletMnemonic(WALLET_NAME, importMnemonic, PASSPHRASE, undefined, OWS_VAULT); console.log(`[SpendOS] Imported wallet from OWS_IMPORT_MNEMONIC`); } else { treasuryWallet = createWallet(WALLET_NAME, PASSPHRASE, undefined, OWS_VAULT); console.log(`[SpendOS] Created new wallet: ${WALLET_NAME}`); } } const evmAccount = treasuryWallet.accounts.find(a => a.chainId.startsWith('eip155:')); console.log(`[SpendOS] EVM address: ${evmAccount?.address ?? 'none'}`); return treasuryWallet; } export function getTreasuryWallet(): WalletInfo { if (!treasuryWallet) throw new Error('Wallet not initialized'); return treasuryWallet; } export function getTreasuryAddress(): string { const w = getTreasuryWallet(); return w.accounts.find(a => a.chainId.startsWith('eip155:'))?.address ?? ''; } // ── Delegation Management ────────────────────────────── export function createDelegation( req: Omit ): DelegationRequest { const delegation: DelegationRequest = { ...req, id: randomUUID(), status: 'pending', createdAt: new Date().toISOString(), opsUsed: 0, }; delegations.set(delegation.id, delegation); saveDelegations(); console.log(`[SpendOS] New delegation request: ${delegation.id} — ${delegation.reason}`); return delegation; } export function approveDelegation(id: string): DelegationRequest { const d = delegations.get(id); if (!d) throw new Error(`Delegation ${id} not found`); if (d.status !== 'pending') throw new Error(`Delegation ${id} is ${d.status}, not pending`); // Check expiry if (new Date(d.expiresAt) <= new Date()) { d.status = 'expired'; d.decidedAt = new Date().toISOString(); throw new Error(`Delegation ${id} has already expired`); } // 1. Build OWS policy with declarative rules + optional executable const policyId = `spendos-${d.id}`; const policyExecPath = resolve(process.cwd(), 'policies', 'enforce-bounds.mjs'); const hasExecutable = existsSync(policyExecPath); const policyJson = JSON.stringify({ id: policyId, name: `SpendOS: ${d.reason}`, version: 1, created_at: new Date().toISOString(), rules: [ { type: 'allowed_chains', chain_ids: d.chains }, { type: 'expires_at', timestamp: d.expiresAt }, ], executable: hasExecutable ? policyExecPath : null, config: hasExecutable ? { allowed_recipients: d.allowedRecipients ?? [], max_native_value_wei: '100000000000000000', // 0.1 ETH default cap max_daily_total_wei: '500000000000000000', // 0.5 ETH daily cap } : null, action: 'deny', }); createPolicy(policyJson, OWS_VAULT); // 2. Create API key scoped to wallet + policy (rollback policy on failure) const wallet = getTreasuryWallet(); let apiKey: ApiKeyResult; try { apiKey = createApiKey( `spendos-session-${d.id}`, [wallet.id], [policyId], PASSPHRASE, d.expiresAt, OWS_VAULT, ); } catch (err) { deletePolicy(policyId, OWS_VAULT); // Rollback on failure throw err; } // 3. Update delegation d.status = 'approved'; d.decidedAt = new Date().toISOString(); d.sessionKeyId = apiKey.id; d.sessionKeyToken = apiKey.token; d.policyId = policyId; saveDelegations(); console.log(`[SpendOS] Approved delegation ${id} — session key: ${apiKey.id}`); return d; } export function rejectDelegation(id: string): DelegationRequest { const d = delegations.get(id); if (!d) throw new Error(`Delegation ${id} not found`); if (d.status !== 'pending') throw new Error(`Delegation ${id} is ${d.status}, not pending`); d.status = 'rejected'; d.decidedAt = new Date().toISOString(); saveDelegations(); console.log(`[SpendOS] Rejected delegation ${id}`); return d; } export function revokeDelegation(id: string): DelegationRequest { const d = delegations.get(id); if (!d) throw new Error(`Delegation ${id} not found`); if (d.status !== 'approved') throw new Error(`Delegation ${id} is ${d.status}, not approved`); // Revoke OWS API key + delete policy if (d.sessionKeyId) revokeApiKey(d.sessionKeyId, OWS_VAULT); if (d.policyId) deletePolicy(d.policyId, OWS_VAULT); d.status = 'revoked'; d.decidedAt = new Date().toISOString(); saveDelegations(); console.log(`[SpendOS] Revoked delegation ${id}`); return d; } // ── Expiry Check ─────────────────────────────────────── export function expireStale(): void { const now = new Date(); const approvedCount = Array.from(delegations.values()).filter(d => d.status === 'approved').length; let changed = false; for (const d of delegations.values()) { if (d.status === 'approved' && new Date(d.expiresAt) <= now) { try { if (d.sessionKeyId) revokeApiKey(d.sessionKeyId, OWS_VAULT); if (d.policyId) deletePolicy(d.policyId, OWS_VAULT); } catch { // Key may already be revoked } d.status = 'expired'; d.decidedAt = now.toISOString(); console.log(`[SpendOS] Auto-expired delegation ${d.id} (dead man's switch)`); changed = true; } if (d.status === 'pending' && new Date(d.expiresAt) <= now) { d.status = 'expired'; d.decidedAt = now.toISOString(); console.log(`[SpendOS] Expired pending delegation ${d.id}`); changed = true; } } if (changed) saveDelegations(); } // ── Queries ──────────────────────────────────────────── export function getDelegations(): DelegationRequest[] { return Array.from(delegations.values()).sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); } export function getActiveDelegations(): DelegationRequest[] { return getDelegations().filter(d => d.status === 'approved'); } export function getPendingDelegations(): DelegationRequest[] { return getDelegations().filter(d => d.status === 'pending'); } export function getDelegation(id: string): DelegationRequest | undefined { return delegations.get(id); } // ── P&L Tracking ─────────────────────────────────────── export function recordEarning(amount: number): void { pnl.totalEarned += amount; pnl.profit = pnl.totalEarned - pnl.totalSpent; pnl.queryCount++; savePnL(); } export function recordSpending(amount: number): void { pnl.totalSpent += amount; pnl.profit = pnl.totalEarned - pnl.totalSpent; savePnL(); } export function getPnL(): AgentPnL { return { ...pnl }; } export function resetPnL(): void { pnl.totalEarned = 0; pnl.totalSpent = 0; pnl.profit = 0; pnl.queryCount = 0; savePnL(); console.log('[SpendOS] P&L reset to zero'); } // ── Heuristic AI Interpretation ──────────────────────── export function generateHeuristicInterpretation(d: DelegationRequest): AiInterpretation { const warnings: string[] = []; let riskLevel: 'low' | 'medium' | 'high' = 'low'; // Check for long delegation const expiresInMs = new Date(d.expiresAt).getTime() - Date.now(); if (expiresInMs > 60 * 60 * 1000) { warnings.push('Delegation lifetime exceeds one hour.'); riskLevel = 'medium'; } // Check for multi-chain if (d.chains.length > 1) { warnings.push('Multi-chain delegation requested.'); } // Check budget const budget = parseFloat(d.totalBudget); if (budget > 100) { warnings.push(`Budget of $${d.totalBudget} exceeds $100.`); riskLevel = 'high'; } const summary = `Agent wants to ${d.reason.toLowerCase()}. ` + `Chains: ${d.chains.join(', ')}. ` + `Operations: ${d.operations.join(', ')}. ` + `Budget: $${d.totalBudget}, expires in ${Math.round(expiresInMs / 60000)}m.`; return { summary, riskLevel, warnings }; }