pyre-world-kit 2.0.12 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.prettierrc.json +6 -0
  2. package/dist/index.d.ts +46 -4
  3. package/dist/index.js +105 -85
  4. package/dist/providers/action.provider.d.ts +46 -0
  5. package/dist/providers/action.provider.js +331 -0
  6. package/dist/providers/intel.provider.d.ts +29 -0
  7. package/dist/providers/intel.provider.js +363 -0
  8. package/dist/providers/mapper.provider.d.ts +197 -0
  9. package/dist/providers/mapper.provider.js +158 -0
  10. package/dist/providers/registry.provider.d.ts +25 -0
  11. package/dist/providers/registry.provider.js +229 -0
  12. package/dist/providers/state.provider.d.ts +42 -0
  13. package/dist/providers/state.provider.js +348 -0
  14. package/dist/pyre_world.json +34 -229
  15. package/dist/types/action.types.d.ts +41 -0
  16. package/dist/types/action.types.js +2 -0
  17. package/dist/types/intel.types.d.ts +20 -0
  18. package/dist/types/intel.types.js +2 -0
  19. package/dist/types/mapper.types.d.ts +27 -0
  20. package/dist/types/mapper.types.js +22 -0
  21. package/dist/types/registry.types.d.ts +0 -0
  22. package/dist/types/registry.types.js +1 -0
  23. package/dist/types/state.types.d.ts +112 -0
  24. package/dist/types/state.types.js +2 -0
  25. package/dist/types.d.ts +8 -24
  26. package/dist/util.d.ts +29 -0
  27. package/dist/util.js +144 -0
  28. package/dist/vanity.d.ts +3 -3
  29. package/dist/vanity.js +18 -15
  30. package/package.json +4 -2
  31. package/readme.md +184 -142
  32. package/src/index.ts +133 -92
  33. package/src/providers/action.provider.ts +443 -0
  34. package/src/providers/intel.provider.ts +383 -0
  35. package/src/providers/mapper.provider.ts +195 -0
  36. package/src/providers/registry.provider.ts +277 -0
  37. package/src/providers/state.provider.ts +357 -0
  38. package/src/pyre_world.json +35 -230
  39. package/src/types/action.types.ts +76 -0
  40. package/src/types/intel.types.ts +22 -0
  41. package/src/types/mapper.types.ts +84 -0
  42. package/src/types/registry.types.ts +0 -0
  43. package/src/types/state.types.ts +144 -0
  44. package/src/types.ts +329 -333
  45. package/src/util.ts +148 -0
  46. package/src/vanity.ts +27 -14
  47. package/tests/test_e2e.ts +339 -172
  48. package/src/actions.ts +0 -719
  49. package/src/intel.ts +0 -521
  50. package/src/mappers.ts +0 -302
  51. package/src/registry.ts +0 -317
  52. package/tests/test_devnet_e2e.ts +0 -401
  53. package/tests/test_sim.ts +0 -458
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Pyre World Agent Registry Provider
3
+ *
4
+ * On-chain agent identity and state persistence.
5
+ * Agents checkpoint their action distributions and personality summaries
6
+ * so any machine with the wallet key can reconstruct the agent.
7
+ */
8
+
9
+ import { Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'
10
+ import { BN, Program, AnchorProvider, type Wallet } from '@coral-xyz/anchor'
11
+ import type { TransactionResult } from 'torchsdk'
12
+ import type {
13
+ RegistryProfile,
14
+ RegistryWalletLink,
15
+ RegisterAgentParams,
16
+ CheckpointParams,
17
+ LinkAgentWalletParams,
18
+ UnlinkAgentWalletParams,
19
+ TransferAgentAuthorityParams,
20
+ } from '../types'
21
+
22
+ import idl from '../pyre_world.json'
23
+
24
+ // ─── Program ID ─────────────────────────────────────────────────────
25
+
26
+ export const REGISTRY_PROGRAM_ID = new PublicKey(idl.address)
27
+
28
+ // ─── PDA Seeds ──────────────────────────────────────────────────────
29
+
30
+ const AGENT_SEED = 'pyre_agent'
31
+ const AGENT_WALLET_SEED = 'pyre_agent_wallet'
32
+
33
+ // ─── PDA Helpers ────────────────────────────────────────────────────
34
+
35
+ export function getAgentProfilePda(creator: PublicKey): [PublicKey, number] {
36
+ return PublicKey.findProgramAddressSync(
37
+ [Buffer.from(AGENT_SEED), creator.toBuffer()],
38
+ REGISTRY_PROGRAM_ID,
39
+ )
40
+ }
41
+
42
+ export function getAgentWalletLinkPda(wallet: PublicKey): [PublicKey, number] {
43
+ return PublicKey.findProgramAddressSync(
44
+ [Buffer.from(AGENT_WALLET_SEED), wallet.toBuffer()],
45
+ REGISTRY_PROGRAM_ID,
46
+ )
47
+ }
48
+
49
+ // ─── Anchor Program Helper ──────────────────────────────────────────
50
+
51
+ function makeDummyProvider(connection: Connection, payer: PublicKey): AnchorProvider {
52
+ const dummyWallet = {
53
+ publicKey: payer,
54
+ signTransaction: async (t: Transaction) => t,
55
+ signAllTransactions: async (t: Transaction[]) => t,
56
+ }
57
+ return new AnchorProvider(connection, dummyWallet as unknown as Wallet, {})
58
+ }
59
+
60
+ async function finalizeTransaction(
61
+ connection: Connection,
62
+ tx: Transaction,
63
+ feePayer: PublicKey,
64
+ ): Promise<void> {
65
+ const { blockhash } = await connection.getLatestBlockhash()
66
+ tx.recentBlockhash = blockhash
67
+ tx.feePayer = feePayer
68
+ }
69
+
70
+ // ─── Provider ───────────────────────────────────────────────────────
71
+
72
+ export class RegistryProvider {
73
+ constructor(private connection: Connection) {}
74
+
75
+ private getProgram(payer: PublicKey): Program {
76
+ const provider = makeDummyProvider(this.connection, payer)
77
+ return new Program(idl as any, provider)
78
+ }
79
+
80
+ // ─── Read Operations ──────────────────────────────────────────────
81
+
82
+ async getProfile(creator: string): Promise<RegistryProfile | null> {
83
+ const creatorPk = new PublicKey(creator)
84
+ const [profilePda] = getAgentProfilePda(creatorPk)
85
+ const program = this.getProgram(creatorPk)
86
+
87
+ try {
88
+ const account = await (program.account as any).agentProfile.fetch(profilePda)
89
+ return {
90
+ address: profilePda.toBase58(),
91
+ creator: account.creator.toBase58(),
92
+ authority: account.authority.toBase58(),
93
+ linked_wallet: account.linkedWallet.toBase58(),
94
+ personality_summary: account.personalitySummary,
95
+ last_checkpoint: account.lastCheckpoint.toNumber(),
96
+ joins: account.joins.toNumber(),
97
+ defects: account.defects.toNumber(),
98
+ rallies: account.rallies.toNumber(),
99
+ launches: account.launches.toNumber(),
100
+ messages: account.messages.toNumber(),
101
+ fuds: account.fuds.toNumber(),
102
+ infiltrates: account.infiltrates.toNumber(),
103
+ reinforces: account.reinforces.toNumber(),
104
+ war_loans: account.warLoans.toNumber(),
105
+ repay_loans: account.repayLoans.toNumber(),
106
+ sieges: account.sieges.toNumber(),
107
+ ascends: account.ascends.toNumber(),
108
+ razes: account.razes.toNumber(),
109
+ tithes: account.tithes.toNumber(),
110
+ created_at: account.createdAt.toNumber(),
111
+ bump: account.bump,
112
+ total_sol_spent: account.totalSolSpent?.toNumber() ?? 0,
113
+ total_sol_received: account.totalSolReceived?.toNumber() ?? 0,
114
+ }
115
+ } catch {
116
+ return null
117
+ }
118
+ }
119
+
120
+ async getWalletLink(wallet: string): Promise<RegistryWalletLink | null> {
121
+ const walletPk = new PublicKey(wallet)
122
+ const [linkPda] = getAgentWalletLinkPda(walletPk)
123
+ const program = this.getProgram(walletPk)
124
+
125
+ try {
126
+ const account = await (program.account as any).agentWalletLink.fetch(linkPda)
127
+ return {
128
+ address: linkPda.toBase58(),
129
+ profile: account.profile.toBase58(),
130
+ wallet: account.wallet.toBase58(),
131
+ linked_at: account.linkedAt.toNumber(),
132
+ bump: account.bump,
133
+ }
134
+ } catch {
135
+ return null
136
+ }
137
+ }
138
+
139
+ // ─── Transaction Builders ─────────────────────────────────────────
140
+
141
+ async register(params: RegisterAgentParams): Promise<TransactionResult> {
142
+ const creator = new PublicKey(params.creator)
143
+ const [profile] = getAgentProfilePda(creator)
144
+ const [walletLink] = getAgentWalletLinkPda(creator)
145
+ const program = this.getProgram(creator)
146
+
147
+ const tx = new Transaction()
148
+ const ix = await (program.methods.register() as any)
149
+ .accounts({ creator, profile, walletLink, systemProgram: SystemProgram.programId })
150
+ .instruction()
151
+
152
+ tx.add(ix)
153
+ await finalizeTransaction(this.connection, tx, creator)
154
+
155
+ return {
156
+ transaction: tx,
157
+ message: `Register agent profile [${profile.toBase58()}]`,
158
+ }
159
+ }
160
+
161
+ async checkpoint(params: CheckpointParams): Promise<TransactionResult> {
162
+ const signer = new PublicKey(params.signer)
163
+ const creatorPk = new PublicKey(params.creator)
164
+ const [profile] = getAgentProfilePda(creatorPk)
165
+ const program = this.getProgram(signer)
166
+
167
+ const args = {
168
+ joins: new BN(params.joins),
169
+ defects: new BN(params.defects),
170
+ rallies: new BN(params.rallies),
171
+ launches: new BN(params.launches),
172
+ messages: new BN(params.messages),
173
+ fuds: new BN(params.fuds),
174
+ infiltrates: new BN(params.infiltrates),
175
+ reinforces: new BN(params.reinforces),
176
+ warLoans: new BN(params.war_loans),
177
+ repayLoans: new BN(params.repay_loans),
178
+ sieges: new BN(params.sieges),
179
+ ascends: new BN(params.ascends),
180
+ razes: new BN(params.razes),
181
+ tithes: new BN(params.tithes),
182
+ personalitySummary: params.personality_summary,
183
+ totalSolSpent: new BN(params.total_sol_spent),
184
+ totalSolReceived: new BN(params.total_sol_received),
185
+ }
186
+
187
+ const tx = new Transaction()
188
+ const ix = await (program.methods.checkpoint(args) as any)
189
+ .accounts({ signer, profile, systemProgram: SystemProgram.programId })
190
+ .instruction()
191
+
192
+ tx.add(ix)
193
+ await finalizeTransaction(this.connection, tx, signer)
194
+
195
+ return {
196
+ transaction: tx,
197
+ message: `Checkpoint agent [${profile.toBase58()}]`,
198
+ }
199
+ }
200
+
201
+ async linkWallet(params: LinkAgentWalletParams): Promise<TransactionResult> {
202
+ const authority = new PublicKey(params.authority)
203
+ const creatorPk = new PublicKey(params.creator)
204
+ const walletToLink = new PublicKey(params.wallet_to_link)
205
+ const [profile] = getAgentProfilePda(creatorPk)
206
+ const [walletLink] = getAgentWalletLinkPda(walletToLink)
207
+ const program = this.getProgram(authority)
208
+
209
+ const tx = new Transaction()
210
+ const ix = await (program.methods.linkWallet() as any)
211
+ .accounts({
212
+ authority,
213
+ profile,
214
+ walletToLink,
215
+ walletLink,
216
+ systemProgram: SystemProgram.programId,
217
+ })
218
+ .instruction()
219
+
220
+ tx.add(ix)
221
+ await finalizeTransaction(this.connection, tx, authority)
222
+
223
+ return {
224
+ transaction: tx,
225
+ message: `Link wallet ${walletToLink.toBase58()} to agent [${profile.toBase58()}]`,
226
+ }
227
+ }
228
+
229
+ async unlinkWallet(params: UnlinkAgentWalletParams): Promise<TransactionResult> {
230
+ const authority = new PublicKey(params.authority)
231
+ const creatorPk = new PublicKey(params.creator)
232
+ const walletToUnlink = new PublicKey(params.wallet_to_unlink)
233
+ const [profile] = getAgentProfilePda(creatorPk)
234
+ const [walletLink] = getAgentWalletLinkPda(walletToUnlink)
235
+ const program = this.getProgram(authority)
236
+
237
+ const tx = new Transaction()
238
+ const ix = await (program.methods.unlinkWallet() as any)
239
+ .accounts({
240
+ authority,
241
+ profile,
242
+ walletToUnlink,
243
+ walletLink,
244
+ systemProgram: SystemProgram.programId,
245
+ })
246
+ .instruction()
247
+
248
+ tx.add(ix)
249
+ await finalizeTransaction(this.connection, tx, authority)
250
+
251
+ return {
252
+ transaction: tx,
253
+ message: `Unlink wallet ${walletToUnlink.toBase58()} from agent [${profile.toBase58()}]`,
254
+ }
255
+ }
256
+
257
+ async transferAuthority(params: TransferAgentAuthorityParams): Promise<TransactionResult> {
258
+ const authority = new PublicKey(params.authority)
259
+ const creatorPk = new PublicKey(params.creator)
260
+ const newAuthority = new PublicKey(params.new_authority)
261
+ const [profile] = getAgentProfilePda(creatorPk)
262
+ const program = this.getProgram(authority)
263
+
264
+ const tx = new Transaction()
265
+ const ix = await (program.methods.transferAuthority() as any)
266
+ .accounts({ authority, profile, newAuthority })
267
+ .instruction()
268
+
269
+ tx.add(ix)
270
+ await finalizeTransaction(this.connection, tx, authority)
271
+
272
+ return {
273
+ transaction: tx,
274
+ message: `Transfer agent authority to ${newAuthority.toBase58()}`,
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Pyre Kit State Provider
3
+ *
4
+ * Objective game state tracking for an agent.
5
+ * Owns holdings, action counts, vault resolution, tick counter.
6
+ * Injected into ActionProvider so every action automatically updates state.
7
+ */
8
+
9
+ import { Connection, PublicKey } from '@solana/web3.js'
10
+ import type {
11
+ State,
12
+ AgentGameState,
13
+ SerializedGameState,
14
+ TrackedAction,
15
+ CheckpointConfig,
16
+ } from '../types/state.types'
17
+ import { RegistryProvider } from './registry.provider'
18
+ import { isPyreMint } from '../vanity'
19
+ import { isBlacklistedMint } from '../util'
20
+
21
+ const EMPTY_COUNTS: Record<TrackedAction, number> = {
22
+ join: 0,
23
+ defect: 0,
24
+ rally: 0,
25
+ launch: 0,
26
+ message: 0,
27
+ reinforce: 0,
28
+ war_loan: 0,
29
+ repay_loan: 0,
30
+ siege: 0,
31
+ ascend: 0,
32
+ raze: 0,
33
+ tithe: 0,
34
+ infiltrate: 0,
35
+ fud: 0,
36
+ }
37
+
38
+ export class StateProvider implements State {
39
+ private _state: AgentGameState | null = null
40
+ private checkpointConfig: CheckpointConfig | null = null
41
+ private ticksSinceCheckpoint = 0
42
+
43
+ constructor(
44
+ private connection: Connection,
45
+ private publicKey: string,
46
+ private registry: RegistryProvider,
47
+ ) {}
48
+
49
+ // ─── Readonly accessors ─────────────────────────────────────────
50
+
51
+ get state() {
52
+ return this._state
53
+ }
54
+ get vaultCreator() {
55
+ return this._state?.vaultCreator ?? null
56
+ }
57
+ get initialized() {
58
+ return this._state?.initialized ?? false
59
+ }
60
+ get tick() {
61
+ return this._state?.tick ?? 0
62
+ }
63
+
64
+ // ─── Configuration ──────────────────────────────────────────────
65
+
66
+ /** Configure auto-checkpoint behavior */
67
+ setCheckpointConfig(config: CheckpointConfig) {
68
+ this.checkpointConfig = config
69
+ }
70
+
71
+ // ─── Initialization ─────────────────────────────────────────────
72
+
73
+ async init(): Promise<AgentGameState> {
74
+ if (this._state?.initialized) return this._state
75
+
76
+ const state: AgentGameState = {
77
+ publicKey: this.publicKey,
78
+ vaultCreator: null,
79
+ stronghold: null,
80
+ tick: 0,
81
+ actionCounts: { ...EMPTY_COUNTS },
82
+ holdings: new Map(),
83
+ activeLoans: new Set(),
84
+ founded: [],
85
+ rallied: new Set(),
86
+ voted: new Set(),
87
+ sentiment: new Map(),
88
+ recentHistory: [],
89
+ personalitySummary: null,
90
+ totalSolSpent: 0,
91
+ totalSolReceived: 0,
92
+ initialized: false,
93
+ }
94
+
95
+ this._state = state
96
+
97
+ // Resolve vault link
98
+ const { getVaultForWallet } = await import('torchsdk')
99
+ try {
100
+ const vault = await getVaultForWallet(this.connection, this.publicKey)
101
+ if (vault) {
102
+ state.vaultCreator = vault.creator
103
+ state.stronghold = {
104
+ address: vault.address,
105
+ creator: vault.creator,
106
+ authority: vault.authority,
107
+ sol_balance: vault.sol_balance,
108
+ total_deposited: vault.total_deposited,
109
+ total_withdrawn: vault.total_withdrawn,
110
+ total_spent: vault.total_spent,
111
+ total_received: vault.total_received,
112
+ linked_agents: vault.linked_wallets,
113
+ created_at: vault.created_at,
114
+ }
115
+ }
116
+ } catch {}
117
+
118
+ // Load registry profile — personality, action counts, SOL totals
119
+ try {
120
+ const profile = await this.registry.getProfile(this.publicKey)
121
+ if (profile) {
122
+ state.personalitySummary = profile.personality_summary || null
123
+ state.totalSolSpent = profile.total_sol_spent
124
+ state.totalSolReceived = profile.total_sol_received
125
+ // Seed action counts from on-chain checkpoint
126
+ state.actionCounts.join = Math.max(state.actionCounts.join, profile.joins)
127
+ state.actionCounts.defect = Math.max(state.actionCounts.defect, profile.defects)
128
+ state.actionCounts.rally = Math.max(state.actionCounts.rally, profile.rallies)
129
+ state.actionCounts.launch = Math.max(state.actionCounts.launch, profile.launches)
130
+ state.actionCounts.message = Math.max(state.actionCounts.message, profile.messages)
131
+ state.actionCounts.reinforce = Math.max(state.actionCounts.reinforce, profile.reinforces)
132
+ state.actionCounts.fud = Math.max(state.actionCounts.fud, profile.fuds)
133
+ state.actionCounts.infiltrate = Math.max(state.actionCounts.infiltrate, profile.infiltrates)
134
+ state.actionCounts.war_loan = Math.max(state.actionCounts.war_loan, profile.war_loans)
135
+ state.actionCounts.repay_loan = Math.max(state.actionCounts.repay_loan, profile.repay_loans)
136
+ state.actionCounts.siege = Math.max(state.actionCounts.siege, profile.sieges)
137
+ state.actionCounts.ascend = Math.max(state.actionCounts.ascend, profile.ascends)
138
+ state.actionCounts.raze = Math.max(state.actionCounts.raze, profile.razes)
139
+ state.actionCounts.tithe = Math.max(state.actionCounts.tithe, profile.tithes)
140
+ // Set tick to total actions from checkpoint
141
+ const totalFromCheckpoint = Object.values(state.actionCounts).reduce((a, b) => a + b, 0)
142
+ state.tick = totalFromCheckpoint
143
+ }
144
+ } catch {}
145
+
146
+ // Load holdings (wallet + vault token accounts)
147
+ await this.refreshHoldings()
148
+
149
+ state.initialized = true
150
+ return state
151
+ }
152
+
153
+ // ─── Action Recording ───────────────────────────────────────────
154
+
155
+ async record(action: TrackedAction, mint?: string, description?: string): Promise<void> {
156
+ if (!this._state) throw new Error('State not initialized — call init() first')
157
+
158
+ this._state.tick++
159
+ this._state.actionCounts[action]++
160
+ this.ticksSinceCheckpoint++
161
+
162
+ // Track founded factions
163
+ if (action === 'launch' && mint) {
164
+ this._state.founded.push(mint)
165
+ }
166
+
167
+ // Update sentiment for the target faction
168
+ if (mint) {
169
+ this.updateSentiment(action, mint)
170
+ }
171
+
172
+ // Track recent history for LLM memory block
173
+ if (description) {
174
+ this._state.recentHistory.push(description)
175
+ if (this._state.recentHistory.length > 20) {
176
+ this._state.recentHistory = this._state.recentHistory.slice(-20)
177
+ }
178
+ }
179
+
180
+ // Refresh holdings after any trade action
181
+ await this.refreshHoldings()
182
+
183
+ // Auto-checkpoint check
184
+ if (this.checkpointConfig && this.ticksSinceCheckpoint >= this.checkpointConfig.interval) {
185
+ this.ticksSinceCheckpoint = 0
186
+ this.onCheckpointDue?.()
187
+ }
188
+ }
189
+
190
+ /** Update sentiment score for a faction based on action type */
191
+ private updateSentiment(action: TrackedAction, mint: string): void {
192
+ if (!this._state) return
193
+
194
+ const current = this._state.sentiment.get(mint) ?? 0
195
+
196
+ const SENTIMENT_DELTAS: Partial<Record<TrackedAction, number>> = {
197
+ join: 1,
198
+ reinforce: 1.5,
199
+ defect: -2,
200
+ rally: 3,
201
+ infiltrate: -5,
202
+ message: 0.5,
203
+ fud: -1.5,
204
+ war_loan: 1,
205
+ launch: 3,
206
+ }
207
+
208
+ const delta = SENTIMENT_DELTAS[action] ?? 0
209
+ if (delta !== 0) {
210
+ this._state.sentiment.set(mint, Math.max(-10, Math.min(10, current + delta)))
211
+ }
212
+ }
213
+
214
+ /** Callback set by PyreKit to handle checkpoint triggers */
215
+ onCheckpointDue: (() => void) | null = null
216
+
217
+ // ─── Holdings ───────────────────────────────────────────────────
218
+
219
+ async refreshHoldings(): Promise<void> {
220
+ if (!this._state) return
221
+
222
+ const { TOKEN_2022_PROGRAM_ID } = await import('@solana/spl-token')
223
+ const walletPk = new PublicKey(this.publicKey)
224
+
225
+ // Scan wallet token accounts
226
+ let walletValues: any[] = []
227
+ try {
228
+ const walletAccounts = await this.connection.getParsedTokenAccountsByOwner(walletPk, {
229
+ programId: TOKEN_2022_PROGRAM_ID,
230
+ })
231
+ walletValues = walletAccounts.value
232
+ } catch {}
233
+
234
+ // Scan vault token accounts
235
+ let vaultValues: any[] = []
236
+ if (this._state.stronghold) {
237
+ try {
238
+ const vaultPk = new PublicKey(this._state.stronghold.address)
239
+ const vaultAccounts = await this.connection.getParsedTokenAccountsByOwner(vaultPk, {
240
+ programId: TOKEN_2022_PROGRAM_ID,
241
+ })
242
+ vaultValues = vaultAccounts.value
243
+ } catch {}
244
+ }
245
+
246
+ // Merge balances
247
+ const newHoldings = new Map<string, number>()
248
+ for (const a of [...walletValues, ...vaultValues]) {
249
+ const mint = a.account.data.parsed.info.mint as string
250
+ const balance = Number(a.account.data.parsed.info.tokenAmount.uiAmount ?? 0)
251
+ if (balance > 0 && isPyreMint(mint) && !isBlacklistedMint(mint)) {
252
+ newHoldings.set(mint, (newHoldings.get(mint) ?? 0) + balance)
253
+ }
254
+ }
255
+
256
+ // Update — clear stale, set fresh
257
+ this._state.holdings.clear()
258
+ for (const [mint, balance] of newHoldings) {
259
+ this._state.holdings.set(mint, balance)
260
+ }
261
+ }
262
+
263
+ getSentiment(mint: string): number {
264
+ return this._state?.sentiment.get(mint) ?? 0
265
+ }
266
+
267
+ get sentimentMap(): ReadonlyMap<string, number> {
268
+ return this._state?.sentiment ?? new Map()
269
+ }
270
+
271
+ get history(): readonly string[] {
272
+ return this._state?.recentHistory ?? []
273
+ }
274
+
275
+ getBalance(mint: string): number {
276
+ return this._state?.holdings.get(mint) ?? 0
277
+ }
278
+
279
+ // ─── Dedup Guards ───────────────────────────────────────────────
280
+
281
+ hasVoted(mint: string): boolean {
282
+ return this._state?.voted.has(mint) ?? false
283
+ }
284
+
285
+ hasRallied(mint: string): boolean {
286
+ return this._state?.rallied.has(mint) ?? false
287
+ }
288
+
289
+ markVoted(mint: string): void {
290
+ this._state?.voted.add(mint)
291
+ }
292
+
293
+ markRallied(mint: string): void {
294
+ this._state?.rallied.add(mint)
295
+ }
296
+
297
+ // ─── Serialization ──────────────────────────────────────────────
298
+
299
+ serialize(): SerializedGameState {
300
+ if (!this._state) {
301
+ return {
302
+ publicKey: this.publicKey,
303
+ vaultCreator: null,
304
+ tick: 0,
305
+ actionCounts: { ...EMPTY_COUNTS },
306
+ holdings: {},
307
+ activeLoans: [],
308
+ founded: [],
309
+ rallied: [],
310
+ voted: [],
311
+ sentiment: {},
312
+ recentHistory: [],
313
+ personalitySummary: null,
314
+ totalSolSpent: 0,
315
+ totalSolReceived: 0,
316
+ }
317
+ }
318
+
319
+ return {
320
+ publicKey: this._state.publicKey,
321
+ vaultCreator: this._state.vaultCreator,
322
+ tick: this._state.tick,
323
+ actionCounts: { ...this._state.actionCounts },
324
+ holdings: Object.fromEntries(this._state.holdings),
325
+ activeLoans: Array.from(this._state.activeLoans),
326
+ founded: [...this._state.founded],
327
+ rallied: Array.from(this._state.rallied),
328
+ voted: Array.from(this._state.voted),
329
+ sentiment: Object.fromEntries(this._state.sentiment),
330
+ recentHistory: this._state.recentHistory.slice(-20),
331
+ personalitySummary: this._state.personalitySummary,
332
+ totalSolSpent: this._state.totalSolSpent,
333
+ totalSolReceived: this._state.totalSolReceived,
334
+ }
335
+ }
336
+
337
+ hydrate(saved: SerializedGameState): void {
338
+ this._state = {
339
+ publicKey: saved.publicKey,
340
+ vaultCreator: saved.vaultCreator,
341
+ stronghold: null, // will be resolved on next refreshHoldings or init
342
+ tick: saved.tick,
343
+ actionCounts: { ...EMPTY_COUNTS, ...saved.actionCounts },
344
+ holdings: new Map(Object.entries(saved.holdings)),
345
+ activeLoans: new Set(saved.activeLoans),
346
+ founded: [...saved.founded],
347
+ rallied: new Set(saved.rallied),
348
+ voted: new Set(saved.voted),
349
+ sentiment: new Map(Object.entries(saved.sentiment)),
350
+ recentHistory: [...saved.recentHistory],
351
+ personalitySummary: saved.personalitySummary,
352
+ totalSolSpent: saved.totalSolSpent,
353
+ totalSolReceived: saved.totalSolReceived,
354
+ initialized: true,
355
+ }
356
+ }
357
+ }