{"version":3,"file":"tx-ledger.mjs","names":[],"sources":["../../../src/services/tx-ledger.ts"],"sourcesContent":["/**\n * Event-Sourced Transaction Ledger — append-only log of every on-chain action.\n *\n * Inspired by Lemon's event-sourced game engine and IronClaw's audit boundary.\n *\n * Every on-chain action the agent takes (swap, transfer, bridge, approve, launch,\n * etc.) is recorded as an immutable event. The ledger provides:\n *\n * 1. Complete audit trail for regulatory/tax purposes\n * 2. Foundation for heartbeat monitoring (knows all open positions)\n * 3. Replay capability — can reconstruct portfolio state from events\n * 4. Cross-session continuity — survives restarts\n *\n * Events are append-only: once written, they are never modified or deleted.\n * Each event gets a monotonically increasing sequence number.\n */\n\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport type TxEventType =\n  | 'swap'\n  | 'transfer'\n  | 'bridge'\n  | 'approve'\n  | 'launch'\n  | 'claim_fees'\n  | 'add_liquidity'\n  | 'remove_liquidity'\n  | 'compound_action'\n  | 'bankr_swap'\n  | 'bankr_launch'\n  | 'bankr_automate'\n  | 'bankr_polymarket'\n  | 'bankr_leverage'\n  | 'permit2'\n  | 'lend_supply'\n  | 'lend_borrow'\n  | 'lend_repay'\n  | 'lend_withdraw'\n  | 'approval_revoke'\n  | 'stake'\n  | 'stake_unstake'\n  | 'stake_wrap'\n  | 'stake_unwrap'\n  | 'nft_transfer'\n  | 'nft_buy'\n  | 'nft_list'\n  | 'privacy_deposit'\n  | 'privacy_withdraw'\n  | 'privacy_transfer'\n  | 'yield_deposit'\n  | 'yield_withdraw'\n  | 'governance_vote'\n  | 'governance_delegate'\n  | 'safe_propose'\n  | 'safe_confirm'\n  | 'airdrop_check'\n  | 'airdrop_claim'\n  | 'order_create'\n  | 'order_cancel'\n  | 'hummingbot_action'\n  | 'molten_action'\n  | 'clawnx_post'\n  | 'unknown';\n\nexport interface TxEvent {\n  /** Monotonically increasing sequence number (assigned by ledger). */\n  seq: number;\n  /** Event type. */\n  type: TxEventType;\n  /** ISO-8601 timestamp. */\n  timestamp: string;\n  /** Unix timestamp in ms. */\n  timestampMs: number;\n  /** User/session that triggered this action. */\n  userId: string;\n  /** On-chain transaction hash (null if tx hasn't been submitted yet). */\n  txHash: string | null;\n  /** Chain ID. */\n  chainId: number;\n  /** Chain name (base, ethereum, arbitrum, etc.). */\n  chain: string;\n  /** Wallet address that signed the tx. */\n  from: string;\n  /** Target contract/address. */\n  to: string | null;\n  /** Status of the action. */\n  status: 'pending' | 'confirmed' | 'failed' | 'cancelled';\n  /** Human-readable summary (e.g., \"Swap 1.5 ETH → 4200 USDC on Base\"). */\n  summary: string;\n  /** Structured payload — differs per event type. */\n  data: Record<string, unknown>;\n  /** Gas cost in USD (filled after confirmation). */\n  gasCostUsd?: number;\n  /** Tool that generated this event. */\n  tool: string;\n  /** Error message if the action failed. */\n  error?: string;\n}\n\nexport interface LedgerQuery {\n  /** Filter by user ID. */\n  userId?: string;\n  /** Filter by event type(s). */\n  types?: TxEventType[];\n  /** Filter by chain ID. */\n  chainId?: number;\n  /** Filter by status. */\n  status?: TxEvent['status'];\n  /** Only events after this timestamp (ms). */\n  afterMs?: number;\n  /** Only events before this timestamp (ms). */\n  beforeMs?: number;\n  /** Max number of events to return (most recent first). */\n  limit?: number;\n  /** Starting sequence number (for pagination). */\n  afterSeq?: number;\n}\n\nexport interface LedgerStats {\n  totalEvents: number;\n  byType: Record<string, number>;\n  byStatus: Record<string, number>;\n  byChain: Record<string, number>;\n  oldestEventMs: number | null;\n  newestEventMs: number | null;\n}\n\n// ─── Chain Name Map ─────────────────────────────────────────────────────\n\nconst CHAIN_NAMES: Record<number, string> = {\n  1: 'ethereum',\n  8453: 'base',\n  42161: 'arbitrum',\n  10: 'optimism',\n  137: 'polygon',\n};\n\n// ─── Tool → Event Type Map ──────────────────────────────────────────────\n\nconst TOOL_EVENT_MAP: Record<string, TxEventType> = {\n  defi_swap: 'swap',\n  transfer: 'transfer',\n  bridge: 'bridge',\n  permit2: 'permit2',\n  clawnch_launch: 'launch',\n  clawnch_fees: 'claim_fees',\n  liquidity: 'add_liquidity',\n  compound_action: 'compound_action',\n  bankr_launch: 'bankr_launch',\n  bankr_automate: 'bankr_automate',\n  bankr_polymarket: 'bankr_polymarket',\n  bankr_leverage: 'bankr_leverage',\n  defi_lend: 'lend_supply', // Default — overridden by action-specific mapping below\n  approvals: 'approval_revoke',\n  defi_stake: 'stake', // Default — overridden by action-specific mapping below\n  nft: 'nft_transfer', // Default — overridden by action-specific mapping below\n  privacy: 'privacy_deposit', // Default — overridden by action-specific mapping below\n  yield: 'yield_deposit', // Default — overridden by action-specific mapping below\n  governance: 'governance_vote', // Default — overridden by action-specific mapping below\n  safe: 'safe_propose', // Default — overridden by action-specific mapping below\n  airdrop: 'airdrop_check', // Default — overridden by action-specific mapping below\n  manage_orders: 'order_create', // Default — overridden by action-specific mapping below\n  hummingbot: 'hummingbot_action',\n  molten: 'molten_action',\n  clawnx: 'clawnx_post',\n};\n\n/**\n * Map defi_lend action names to specific event types.\n * Used by the lending tool to record granular events.\n */\nexport const LEND_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  supply: 'lend_supply',\n  borrow: 'lend_borrow',\n  repay: 'lend_repay',\n  withdraw: 'lend_withdraw',\n};\n\n/**\n * Map defi_stake action names to specific event types.\n */\nexport const STAKE_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  stake: 'stake',\n  unstake: 'stake_unstake',\n  wrap: 'stake_wrap',\n  unwrap: 'stake_unwrap',\n};\n\n/**\n * Map nft action names to specific event types.\n */\nexport const NFT_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  transfer: 'nft_transfer',\n  buy: 'nft_buy',\n  list: 'nft_list',\n};\n\n/**\n * Map privacy action names to specific event types.\n */\nexport const PRIVACY_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  deposit: 'privacy_deposit',\n  withdraw: 'privacy_withdraw',\n  transfer: 'privacy_transfer',\n};\n\n/**\n * Map yield action names to specific event types.\n */\nexport const YIELD_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  deposit: 'yield_deposit',\n  withdraw: 'yield_withdraw',\n};\n\n/**\n * Map governance action names to specific event types.\n */\nexport const GOVERNANCE_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  vote: 'governance_vote',\n  delegate: 'governance_delegate',\n};\n\n/**\n * Map safe action names to specific event types.\n * Safe operations are off-chain (REST API) but recorded for audit trail.\n */\nexport const SAFE_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  propose: 'safe_propose',\n  confirm: 'safe_confirm',\n};\n\n/**\n * Map airdrop action names to specific event types.\n */\nexport const AIRDROP_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  check: 'airdrop_check',\n  check_all: 'airdrop_check',\n  claim: 'airdrop_claim',\n};\n\n/**\n * Map manage_orders action names to specific event types.\n */\nexport const ORDER_ACTION_EVENT_MAP: Record<string, TxEventType> = {\n  create: 'order_create',\n  cancel: 'order_cancel',\n};\n\nexport function toolToEventType(toolName: string): TxEventType {\n  return TOOL_EVENT_MAP[toolName] ?? 'unknown';\n}\n\nexport function chainIdToName(chainId: number): string {\n  return CHAIN_NAMES[chainId] ?? String(chainId);\n}\n\n// ─── Transaction Ledger ─────────────────────────────────────────────────\n\nclass TxLedger {\n  private events: TxEvent[] = [];\n  private nextSeq = 1;\n  private dirty = false;\n  private readonly ledgerPath: string;\n\n  constructor() {\n    this.ledgerPath = this.getLedgerPath();\n    this.loadFromDisk();\n  }\n\n  /**\n   * Append a new event to the ledger. Returns the assigned sequence number.\n   */\n  append(event: Omit<TxEvent, 'seq' | 'timestamp' | 'timestampMs'>): TxEvent {\n    const now = Date.now();\n    const full: TxEvent = {\n      ...event,\n      seq: this.nextSeq++,\n      timestamp: new Date(now).toISOString(),\n      timestampMs: now,\n    };\n\n    this.events.push(full);\n    this.dirty = true;\n    this.persistAppend(full);\n\n    return full;\n  }\n\n  /**\n   * Update the status of an existing event (e.g., pending → confirmed).\n   * This is the ONLY mutation allowed — and it creates a new event rather\n   * than modifying the original, preserving the append-only invariant.\n   */\n  updateStatus(\n    seq: number,\n    status: TxEvent['status'],\n    updates?: { txHash?: string; gasCostUsd?: number; error?: string },\n  ): TxEvent | null {\n    const original = this.events.find(e => e.seq === seq);\n    if (!original) return null;\n\n    // Append a status-update event that references the original\n    return this.append({\n      type: original.type,\n      userId: original.userId,\n      txHash: updates?.txHash ?? original.txHash,\n      chainId: original.chainId,\n      chain: original.chain,\n      from: original.from,\n      to: original.to,\n      status,\n      summary: `[status update] ${original.summary}`,\n      data: { ...original.data, _refSeq: original.seq, _previousStatus: original.status },\n      gasCostUsd: updates?.gasCostUsd ?? original.gasCostUsd,\n      tool: original.tool,\n      error: updates?.error ?? original.error,\n    });\n  }\n\n  /**\n   * Query events with optional filters. Returns newest-first.\n   */\n  query(q: LedgerQuery = {}): TxEvent[] {\n    let results = [...this.events];\n\n    if (q.userId) results = results.filter(e => e.userId === q.userId);\n    if (q.types?.length) results = results.filter(e => q.types!.includes(e.type));\n    if (q.chainId) results = results.filter(e => e.chainId === q.chainId);\n    if (q.status) results = results.filter(e => e.status === q.status);\n    if (q.afterMs) results = results.filter(e => e.timestampMs > q.afterMs!);\n    if (q.beforeMs) results = results.filter(e => e.timestampMs < q.beforeMs!);\n    if (q.afterSeq) results = results.filter(e => e.seq > q.afterSeq!);\n\n    // Newest first\n    results.sort((a, b) => b.seq - a.seq);\n\n    if (q.limit) results = results.slice(0, q.limit);\n\n    return results;\n  }\n\n  /**\n   * Get a single event by sequence number.\n   */\n  getBySeq(seq: number): TxEvent | null {\n    return this.events.find(e => e.seq === seq) ?? null;\n  }\n\n  /**\n   * Get the most recent event for a given tx hash.\n   */\n  getByTxHash(txHash: string): TxEvent | null {\n    for (let i = this.events.length - 1; i >= 0; i--) {\n      if (this.events[i]!.txHash === txHash) return this.events[i]!;\n    }\n    return null;\n  }\n\n  /**\n   * Get aggregate statistics.\n   */\n  getStats(): LedgerStats {\n    const byType: Record<string, number> = {};\n    const byStatus: Record<string, number> = {};\n    const byChain: Record<string, number> = {};\n\n    for (const e of this.events) {\n      byType[e.type] = (byType[e.type] ?? 0) + 1;\n      byStatus[e.status] = (byStatus[e.status] ?? 0) + 1;\n      byChain[e.chain] = (byChain[e.chain] ?? 0) + 1;\n    }\n\n    return {\n      totalEvents: this.events.length,\n      byType,\n      byStatus,\n      byChain,\n      oldestEventMs: this.events.length > 0 ? this.events[0]!.timestampMs : null,\n      newestEventMs: this.events.length > 0 ? this.events[this.events.length - 1]!.timestampMs : null,\n    };\n  }\n\n  /**\n   * Get the total number of events.\n   */\n  get size(): number {\n    return this.events.length;\n  }\n\n  // ── Persistence ──────────────────────────────────────────────────────\n\n  private getLedgerPath(): string {\n    const dir = process.env.OPENCLAWNCH_TX_DIR\n      ? join(process.env.OPENCLAWNCH_TX_DIR, '..', 'ledger')\n      : join(process.env.HOME ?? '/tmp', '.openclawnch', 'ledger');\n\n    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n    return join(dir, 'tx-ledger.jsonl');\n  }\n\n  private loadFromDisk(): void {\n    try {\n      if (!existsSync(this.ledgerPath)) return;\n\n      const content = readFileSync(this.ledgerPath, 'utf8');\n      const lines = content.split('\\n').filter(l => l.trim());\n\n      for (const line of lines) {\n        try {\n          const event = JSON.parse(line) as TxEvent;\n          this.events.push(event);\n          if (event.seq >= this.nextSeq) {\n            this.nextSeq = event.seq + 1;\n          }\n        } catch {\n          // Skip malformed lines\n        }\n      }\n    } catch {\n      // Fresh start if file can't be read\n    }\n  }\n\n  private persistAppend(event: TxEvent): void {\n    try {\n      appendFileSync(this.ledgerPath, JSON.stringify(event) + '\\n', 'utf8');\n      this.dirty = false;\n    } catch {\n      // Best effort — don't crash on write failure\n    }\n  }\n}\n\n// ─── Singleton ───────────────────────────────────────────────────────────\n\nlet _instance: TxLedger | null = null;\n\nexport function getTxLedger(): TxLedger {\n  if (!_instance) {\n    _instance = new TxLedger();\n  }\n  return _instance;\n}\n\nexport function resetTxLedger(): void {\n  _instance = null;\n}\n"],"mappings":"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"}