File size: 4,894 Bytes
a18e5eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
 * Settlement Engine β€” SEALFORGE β†’ FORGE token
 *
 * Reads ALL WORM entries, filters to type === 'event', derives a bytes32
 * wormHash from each chainHash, checks the contract for prior consumption,
 * and calls wormMint for any un-minted entries.
 *
 * Settlement state is persisted to ./data/settlement-state.json as an
 * append-only record.  Processed entries are NEVER modified or deleted.
 *
 * NEXUS β€” Orchestration Agent (connecting SEALFORGE Node 1 β†’ FORGE Node 2)
 */

import * as fs from 'fs'
import * as path from 'path'
import { ethers } from 'ethers'
import { WormLog, WormEntry } from '../storage/worm-log'
import { ForgeContract } from './forge-contract'
import { toBytes32 } from './bytes32'

// 0.01 FRG per settled WORM event (testnet rate)
const BASE_EMISSION = ethers.parseEther('0.01')
const BASE_EMISSION_STR = '0.01'

/** A record of a successful (or attempted) settlement. */
export interface SettlementRecord {
  wormEntryId: string
  wormHash: string
  txHash: string
  recipient: string
  amountEther: string
  settledAt: string
}

/** Loaded at startup; keyed by wormEntryId for O(1) de-dupe. */
export interface SettlementState {
  processed: Record<string, SettlementRecord>
}

function getStateFilePath(): string {
  const dataDir = process.env.DATA_DIR ?? './data'
  if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
  return path.join(dataDir, 'settlement-state.json')
}

function loadState(): SettlementState {
  const fp = getStateFilePath()
  if (!fs.existsSync(fp)) return { processed: {} }
  try {
    const raw = fs.readFileSync(fp, 'utf8')
    return JSON.parse(raw) as SettlementState
  } catch {
    console.error('[NEXUS:SETTLEMENT] Could not parse settlement-state.json β€” starting fresh')
    return { processed: {} }
  }
}

function saveState(state: SettlementState): void {
  const fp = getStateFilePath()
  fs.writeFileSync(fp, JSON.stringify(state, null, 2), 'utf8')
}


export interface SettlementRunResult {
  checked: number
  minted: number
  skipped: number
  errors: number
  txHashes: string[]
}

/**
 * Run one pass of the settlement engine.
 * Returns a summary of the run.
 */
export async function runSettlement(
  worm: WormLog,
  forge: ForgeContract,
): Promise<SettlementRunResult> {
  const state = loadState()
  const recipient = process.env.MINT_RECIPIENT
  if (!recipient) throw new Error('[NEXUS:SETTLEMENT] MINT_RECIPIENT env var is required')

  // Pull all entries β€” use a large limit; production should paginate if needed
  const { entries, chainValid } = worm.getAll(100_000, 0)
  if (!chainValid) throw new Error('[NEXUS:SETTLEMENT] WORM chain integrity failure β€” aborting')

  // Only settle 'event' type entries β€” skip lifecycle noise
  const events: WormEntry[] = entries.filter(e => e.type === 'event')

  const result: SettlementRunResult = {
    checked: events.length,
    minted: 0,
    skipped: 0,
    errors: 0,
    txHashes: [],
  }

  for (const entry of events) {
    // Already settled locally β€” skip without hitting the RPC
    if (state.processed[entry.id]) {
      result.skipped++
      continue
    }

    const wormHash = toBytes32(entry.chainHash)

    try {
      // Check on-chain β€” skip if already consumed
      const consumed = await forge.isConsumed(wormHash)
      if (consumed) {
        console.log(`[NEXUS:SETTLEMENT] SKIP (on-chain consumed) β€” entry ${entry.id}`)
        // Record locally so we don't re-check next cycle
        const record: SettlementRecord = {
          wormEntryId: entry.id,
          wormHash,
          txHash: 'ALREADY_CONSUMED',
          recipient,
          amountEther: BASE_EMISSION_STR,
          settledAt: new Date().toISOString(),
        }
        state.processed[entry.id] = record
        result.skipped++
        continue
      }

      console.log(`[NEXUS:SETTLEMENT] MINT β€” entry ${entry.id} β†’ ${recipient}`)
      const txHash = await forge.mint(recipient, BASE_EMISSION_STR, wormHash)
      console.log(`[NEXUS:SETTLEMENT] TX ${txHash} confirmed`)

      const record: SettlementRecord = {
        wormEntryId: entry.id,
        wormHash,
        txHash,
        recipient,
        amountEther: BASE_EMISSION_STR,
        settledAt: new Date().toISOString(),
      }

      // Append-only β€” write after each successful mint so a crash mid-run
      // doesn't lose prior confirmations
      state.processed[entry.id] = record
      saveState(state)

      result.minted++
      result.txHashes.push(txHash)
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : String(err)
      console.error(`[NEXUS:SETTLEMENT] ERROR on entry ${entry.id}: ${msg}`)
      result.errors++
      // Continue to next entry β€” never let one failure stop the run
    }
  }

  // Persist final state (covers the skipped-but-recorded entries)
  saveState(state)

  return result
}