@conquest-eth/tools 0.0.0

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -0
  3. package/dist/cli-tool-generator.d.ts +22 -0
  4. package/dist/cli-tool-generator.d.ts.map +1 -0
  5. package/dist/cli-tool-generator.js +217 -0
  6. package/dist/cli-tool-generator.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +76 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/contracts/space-info.d.ts +7 -0
  12. package/dist/contracts/space-info.d.ts.map +1 -0
  13. package/dist/contracts/space-info.js +34 -0
  14. package/dist/contracts/space-info.js.map +1 -0
  15. package/dist/fleet/index.d.ts +4 -0
  16. package/dist/fleet/index.d.ts.map +1 -0
  17. package/dist/fleet/index.js +7 -0
  18. package/dist/fleet/index.js.map +1 -0
  19. package/dist/fleet/manager.d.ts +70 -0
  20. package/dist/fleet/manager.d.ts.map +1 -0
  21. package/dist/fleet/manager.js +92 -0
  22. package/dist/fleet/manager.js.map +1 -0
  23. package/dist/fleet/resolve.d.ts +51 -0
  24. package/dist/fleet/resolve.d.ts.map +1 -0
  25. package/dist/fleet/resolve.js +140 -0
  26. package/dist/fleet/resolve.js.map +1 -0
  27. package/dist/fleet/send.d.ts +29 -0
  28. package/dist/fleet/send.d.ts.map +1 -0
  29. package/dist/fleet/send.js +81 -0
  30. package/dist/fleet/send.js.map +1 -0
  31. package/dist/helpers/index.d.ts +14 -0
  32. package/dist/helpers/index.d.ts.map +1 -0
  33. package/dist/helpers/index.js +28 -0
  34. package/dist/helpers/index.js.map +1 -0
  35. package/dist/index.d.ts +36 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +143 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/planet/acquire.d.ts +16 -0
  40. package/dist/planet/acquire.d.ts.map +1 -0
  41. package/dist/planet/acquire.js +27 -0
  42. package/dist/planet/acquire.js.map +1 -0
  43. package/dist/planet/exit.d.ts +17 -0
  44. package/dist/planet/exit.d.ts.map +1 -0
  45. package/dist/planet/exit.js +56 -0
  46. package/dist/planet/exit.js.map +1 -0
  47. package/dist/planet/index.d.ts +4 -0
  48. package/dist/planet/index.d.ts.map +1 -0
  49. package/dist/planet/index.js +6 -0
  50. package/dist/planet/index.js.map +1 -0
  51. package/dist/planet/manager.d.ts +106 -0
  52. package/dist/planet/manager.d.ts.map +1 -0
  53. package/dist/planet/manager.js +253 -0
  54. package/dist/planet/manager.js.map +1 -0
  55. package/dist/storage/interface.d.ts +93 -0
  56. package/dist/storage/interface.d.ts.map +1 -0
  57. package/dist/storage/interface.js +2 -0
  58. package/dist/storage/interface.js.map +1 -0
  59. package/dist/storage/json-storage.d.ts +28 -0
  60. package/dist/storage/json-storage.d.ts.map +1 -0
  61. package/dist/storage/json-storage.js +148 -0
  62. package/dist/storage/json-storage.js.map +1 -0
  63. package/dist/tools/acquire_planets.d.ts +7 -0
  64. package/dist/tools/acquire_planets.d.ts.map +1 -0
  65. package/dist/tools/acquire_planets.js +63 -0
  66. package/dist/tools/acquire_planets.js.map +1 -0
  67. package/dist/tools/exit_planets.d.ts +5 -0
  68. package/dist/tools/exit_planets.d.ts.map +1 -0
  69. package/dist/tools/exit_planets.js +31 -0
  70. package/dist/tools/exit_planets.js.map +1 -0
  71. package/dist/tools/get_my_planets.d.ts +5 -0
  72. package/dist/tools/get_my_planets.d.ts.map +1 -0
  73. package/dist/tools/get_my_planets.js +30 -0
  74. package/dist/tools/get_my_planets.js.map +1 -0
  75. package/dist/tools/get_pending_exits.d.ts +3 -0
  76. package/dist/tools/get_pending_exits.d.ts.map +1 -0
  77. package/dist/tools/get_pending_exits.js +37 -0
  78. package/dist/tools/get_pending_exits.js.map +1 -0
  79. package/dist/tools/get_pending_fleets.d.ts +3 -0
  80. package/dist/tools/get_pending_fleets.d.ts.map +1 -0
  81. package/dist/tools/get_pending_fleets.js +41 -0
  82. package/dist/tools/get_pending_fleets.js.map +1 -0
  83. package/dist/tools/get_planets_around.d.ts +7 -0
  84. package/dist/tools/get_planets_around.d.ts.map +1 -0
  85. package/dist/tools/get_planets_around.js +41 -0
  86. package/dist/tools/get_planets_around.js.map +1 -0
  87. package/dist/tools/index.d.ts +10 -0
  88. package/dist/tools/index.d.ts.map +1 -0
  89. package/dist/tools/index.js +11 -0
  90. package/dist/tools/index.js.map +1 -0
  91. package/dist/tools/resolve_fleet.d.ts +5 -0
  92. package/dist/tools/resolve_fleet.d.ts.map +1 -0
  93. package/dist/tools/resolve_fleet.js +37 -0
  94. package/dist/tools/resolve_fleet.js.map +1 -0
  95. package/dist/tools/send_fleet.d.ts +16 -0
  96. package/dist/tools/send_fleet.d.ts.map +1 -0
  97. package/dist/tools/send_fleet.js +62 -0
  98. package/dist/tools/send_fleet.js.map +1 -0
  99. package/dist/tools/verify_exit_status.d.ts +5 -0
  100. package/dist/tools/verify_exit_status.d.ts.map +1 -0
  101. package/dist/tools/verify_exit_status.js +39 -0
  102. package/dist/tools/verify_exit_status.js.map +1 -0
  103. package/dist/types.d.ts +126 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +34 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/util/hashing.d.ts +33 -0
  108. package/dist/util/hashing.d.ts.map +1 -0
  109. package/dist/util/hashing.js +38 -0
  110. package/dist/util/hashing.js.map +1 -0
  111. package/dist/util/time.d.ts +43 -0
  112. package/dist/util/time.d.ts.map +1 -0
  113. package/dist/util/time.js +55 -0
  114. package/dist/util/time.js.map +1 -0
  115. package/package.json +78 -0
  116. package/src/cli-tool-generator.ts +287 -0
  117. package/src/cli.ts +109 -0
  118. package/src/contracts/space-info.ts +41 -0
  119. package/src/fleet/index.ts +8 -0
  120. package/src/fleet/manager.ts +140 -0
  121. package/src/fleet/resolve.ts +187 -0
  122. package/src/fleet/send.ts +112 -0
  123. package/src/helpers/index.ts +59 -0
  124. package/src/index.ts +181 -0
  125. package/src/planet/acquire.ts +41 -0
  126. package/src/planet/exit.ts +71 -0
  127. package/src/planet/index.ts +6 -0
  128. package/src/planet/manager.ts +335 -0
  129. package/src/storage/interface.ts +111 -0
  130. package/src/storage/json-storage.ts +184 -0
  131. package/src/tools/acquire_planets.ts +81 -0
  132. package/src/tools/exit_planets.ts +35 -0
  133. package/src/tools/get_my_planets.ts +30 -0
  134. package/src/tools/get_pending_exits.ts +37 -0
  135. package/src/tools/get_pending_fleets.ts +41 -0
  136. package/src/tools/get_planets_around.ts +44 -0
  137. package/src/tools/index.ts +10 -0
  138. package/src/tools/resolve_fleet.ts +37 -0
  139. package/src/tools/send_fleet.ts +68 -0
  140. package/src/tools/verify_exit_status.ts +43 -0
  141. package/src/types.ts +178 -0
  142. package/src/util/hashing.ts +60 -0
  143. package/src/util/time.ts +66 -0
package/src/index.ts ADDED
@@ -0,0 +1,181 @@
1
+ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import pkg from '../package.json' with {type: 'json'};
3
+ import {Implementation} from '@modelcontextprotocol/sdk/types.js';
4
+ import {Chain} from 'viem';
5
+ import {ServerOptions} from '@modelcontextprotocol/sdk/server';
6
+ import {createServer as createMCPEthereumServer} from 'tools-ethereum';
7
+ import {getClients} from 'tools-ethereum/helpers';
8
+ import {createSpaceInfo} from './contracts/space-info.js';
9
+ import {JsonFleetStorage} from './storage/json-storage.js';
10
+ import {FleetManager} from './fleet/manager.js';
11
+ import {PlanetManager} from './planet/manager.js';
12
+ import type {ClientsWithOptionalWallet, ContractConfig, GameContract} from './types.js';
13
+ import {SpaceInfo} from 'conquest-eth-v0-contracts';
14
+
15
+ // Import refactored tools
16
+ import * as tools from './tools/index.js';
17
+ import {registerTool, stringifyWithBigInt} from './helpers/index.js';
18
+ import {Abi_IOuterSpace} from 'conquest-eth-v0-contracts/abis/IOuterSpace.js';
19
+
20
+ /**
21
+ * Create and configure an MCP server for Conquest.eth game interactions
22
+ *
23
+ * @param params - Configuration parameters for the server
24
+ * @param params.chain - The blockchain chain to connect to
25
+ * @param params.privateKey - Optional private key for signing transactions (wallet operations)
26
+ * @param params.gameContract - The game contract address
27
+ * @param options - Optional server configuration
28
+ * @param options.ethereum - Whether to include Ethereum MCP tools (default: false)
29
+ * @param options.rpcURL - Optional custom RPC URL
30
+ * @param options.serverOptions - Optional MCP server options
31
+ * @param options.serverInfo - Optional server metadata to override defaults
32
+ * @param options.storageConfig - Storage configuration for fleets and exits
33
+ * @param options.storageConfig.type - Storage type ('json' or 'sqlite')
34
+ * @param options.storageConfig.dataDir - Optional data directory path
35
+ * @returns Configured MCP server instance with Conquest game tools registered
36
+ */
37
+ export function createServer(
38
+ params: {chain: Chain; privateKey?: `0x${string}`; gameContract: `0x${string}`},
39
+ options?: {
40
+ ethereum?: boolean;
41
+ rpcURL?: string;
42
+ serverOptions?: ServerOptions;
43
+ serverInfo?: Implementation;
44
+ storageConfig?: {type: 'json' | 'sqlite'; dataDir?: string};
45
+ },
46
+ ) {
47
+ const {gameContract: gameContractAddress, ...mcpEthereumParams} = params;
48
+ const clients = getClients(params, options) as ClientsWithOptionalWallet;
49
+
50
+ const gameContract: GameContract = {
51
+ address: gameContractAddress,
52
+ abi: Abi_IOuterSpace,
53
+ };
54
+
55
+ const name = `mcp-conquest-eth-v0`;
56
+ const server = options?.ethereum
57
+ ? createMCPEthereumServer(mcpEthereumParams, {
58
+ ...options,
59
+ serverInfo: {name, version: pkg.version, ...options?.serverInfo},
60
+ })
61
+ : new McpServer(
62
+ options?.serverInfo || {
63
+ name,
64
+ version: pkg.version,
65
+ },
66
+ options?.serverOptions || {capabilities: {logging: {}}},
67
+ );
68
+
69
+ // Initialize SpaceInfo and contractConfig
70
+ let spaceInfo: SpaceInfo | null = null;
71
+ let contractConfig: ContractConfig | null = null;
72
+
73
+ const initSpaceInfo = async () => {
74
+ if (!spaceInfo || !contractConfig) {
75
+ const result = await createSpaceInfo(clients, gameContract);
76
+ spaceInfo = result.spaceInfo;
77
+ contractConfig = result.contractConfig;
78
+ }
79
+ return {spaceInfo, contractConfig};
80
+ };
81
+
82
+ // Initialize storage
83
+ const storageConfig = options?.storageConfig || {type: 'json', dataDir: './data'};
84
+ const storage = new JsonFleetStorage(storageConfig.dataDir || './data');
85
+
86
+ // Initialize managers (will be initialized after spaceInfo is ready)
87
+ let fleetManager: FleetManager | null = null;
88
+ let planetManager: PlanetManager | null = null;
89
+
90
+ // Helper to ensure managers are initialized
91
+ const ensureManagersInitialized = async () => {
92
+ const {spaceInfo: si, contractConfig: cc} = await initSpaceInfo();
93
+
94
+ // Initialize fleetManager even without walletClient for read-only operations
95
+ if (!fleetManager && si && cc) {
96
+ fleetManager = new FleetManager(clients, gameContract, si, cc, storage);
97
+ }
98
+
99
+ // Initialize planetManager even without walletClient for read-only operations
100
+ if (!planetManager && si && cc) {
101
+ planetManager = new PlanetManager(clients, gameContract, si, cc, storage);
102
+ }
103
+
104
+ if (!fleetManager) {
105
+ throw new Error('Fleet manager not initialized');
106
+ }
107
+ if (!planetManager) {
108
+ throw new Error('Planet manager not initialized');
109
+ }
110
+
111
+ return {fleetManager, planetManager};
112
+ };
113
+
114
+ // Auto-register all tools
115
+ for (const [name, tool] of Object.entries(tools)) {
116
+ // Skip the file that's not a tool
117
+ if (name === 'default') continue;
118
+
119
+ server.registerTool(
120
+ name,
121
+ {
122
+ description: tool.description,
123
+ inputSchema: tool.schema,
124
+ },
125
+ async (args: unknown) => {
126
+ try {
127
+ const {fleetManager, planetManager} = await ensureManagersInitialized();
128
+
129
+ const env = {
130
+ sendStatus: async (_message: string) => {
131
+ // TODO: Implement progress notifications when sessionId is available
132
+ },
133
+ fleetManager,
134
+ planetManager,
135
+ };
136
+
137
+ const result = await tool.execute(env, args as any);
138
+
139
+ // Convert ToolResult to CallToolResult
140
+ if (result.success === false) {
141
+ return {
142
+ content: [
143
+ {
144
+ type: 'text' as const,
145
+ text: stringifyWithBigInt({
146
+ error: result.error,
147
+ ...(result.stack ? {stack: result.stack} : {}),
148
+ }),
149
+ },
150
+ ],
151
+ isError: true,
152
+ };
153
+ }
154
+
155
+ return {
156
+ content: [
157
+ {
158
+ type: 'text' as const,
159
+ text: stringifyWithBigInt(result.result, 2),
160
+ },
161
+ ],
162
+ };
163
+ } catch (error) {
164
+ return {
165
+ content: [
166
+ {
167
+ type: 'text' as const,
168
+ text: stringifyWithBigInt({
169
+ error: error instanceof Error ? error.message : String(error),
170
+ }),
171
+ },
172
+ ],
173
+ isError: true,
174
+ };
175
+ }
176
+ },
177
+ );
178
+ }
179
+
180
+ return server;
181
+ }
@@ -0,0 +1,41 @@
1
+ import {Clients, GameContract} from '../types.js';
2
+
3
+ /**
4
+ * Acquire (stake) multiple planets
5
+ *
6
+ * @param clients - Viem clients (publicClient and walletClient)
7
+ * @param gameContract - The game contract instance with address and ABI
8
+ * @param planetIds - Array of planet location IDs to acquire
9
+ * @param amountToMint - Amount of native token to spend
10
+ * @param tokenAmount - Amount of staking token to spend
11
+ * @returns Transaction hash and list of planets acquired
12
+ */
13
+ export async function acquirePlanets(
14
+ clients: Clients,
15
+ gameContract: GameContract,
16
+ planetIds: bigint[],
17
+ amountToMint: bigint,
18
+ tokenAmount: bigint,
19
+ ): Promise<{hash: `0x${string}`; planetsAcquired: bigint[]}> {
20
+ const sender = clients.walletClient.account!.address;
21
+
22
+ const nativeTokenAmount = (amountToMint * 1000000000000000000n) / 1000000000000000000000n; // TODO BigInt((PlayToken.linkedData as any).numTokensPerNativeTokenAt18Decimals);
23
+
24
+ console.log(
25
+ `Acquiring ${planetIds.length} planets with ${amountToMint} native tokens and ${tokenAmount} staking tokens using ${nativeTokenAmount} native tokens`,
26
+ );
27
+
28
+ // Get the contract acquireMultipleViaNativeTokenAndStakingToken function signature
29
+ const simulation = await clients.publicClient.simulateContract({
30
+ ...gameContract,
31
+ functionName: 'acquireMultipleViaNativeTokenAndStakingToken',
32
+ args: [planetIds, amountToMint, tokenAmount],
33
+ account: sender,
34
+ value: nativeTokenAmount,
35
+ });
36
+
37
+ // Send the transaction
38
+ const hash = await clients.walletClient.writeContract(simulation.request);
39
+
40
+ return {hash, planetsAcquired: planetIds};
41
+ }
@@ -0,0 +1,71 @@
1
+ import {getCurrentTimestamp} from '../util/time.js';
2
+ import type {FleetStorage} from '../storage/interface.js';
3
+ import {Clients, GameContract, PendingExit} from '../types.js';
4
+
5
+ /**
6
+ * Exit (unstake) multiple planets to retrieve staked tokens
7
+ *
8
+ * @param clients - Viem clients (publicClient and walletClient)
9
+ * @param gameContract - The game contract instance with address and ABI
10
+ * @param planetIds - Array of planet location IDs to exit
11
+ * @param exitDuration - Duration of the exit process in seconds (from contract config)
12
+ * @param storage - Storage instance for tracking pending exits
13
+ * @returns Transaction hash and list of planet IDs for which exits were initiated
14
+ */
15
+ export async function exitPlanets(
16
+ clients: Clients,
17
+ gameContract: GameContract,
18
+ planetIds: bigint[],
19
+ exitDuration: bigint,
20
+ storage: FleetStorage,
21
+ ): Promise<{hash: `0x${string}`; exitsInitiated: bigint[]}> {
22
+ const sender = clients.walletClient.account!.address;
23
+ const currentTime = getCurrentTimestamp();
24
+
25
+ // Get planet states to verify ownership
26
+ const result = await clients.publicClient.readContract({
27
+ ...gameContract,
28
+ functionName: 'getPlanetStates',
29
+ args: [planetIds],
30
+ });
31
+ const states = result[0];
32
+
33
+ // Create pending exit records for each planet
34
+ const exitsInitiated: bigint[] = [];
35
+ for (let i = 0; i < planetIds.length; i++) {
36
+ const planetId = planetIds[i];
37
+ const state = states[i];
38
+
39
+ // Only create exit record for planets owned by the sender
40
+ if (state.owner && state.owner.toLowerCase() === sender.toLowerCase()) {
41
+ const exit: PendingExit = {
42
+ planetId,
43
+ player: sender,
44
+ exitStartTime: currentTime,
45
+ exitDuration: Number(exitDuration),
46
+ exitCompleteTime: currentTime + Number(exitDuration),
47
+ numSpaceships: state.numSpaceships,
48
+ owner: state.owner,
49
+ completed: false,
50
+ interrupted: false,
51
+ lastCheckedAt: currentTime,
52
+ };
53
+
54
+ await storage.savePendingExit(exit);
55
+ exitsInitiated.push(planetId);
56
+ }
57
+ }
58
+
59
+ // Get the contract exitMultipleFor function signature
60
+ const simulation = await clients.publicClient.simulateContract({
61
+ ...gameContract,
62
+ functionName: 'exitMultipleFor',
63
+ args: [sender, planetIds],
64
+ account: sender,
65
+ });
66
+
67
+ // Send the transaction
68
+ const hash = await clients.walletClient.writeContract(simulation.request);
69
+
70
+ return {hash, exitsInitiated};
71
+ }
@@ -0,0 +1,6 @@
1
+ // Planet Manager and exports
2
+ export {PlanetManager} from './manager.js';
3
+
4
+ // Planet operations
5
+ export {acquirePlanets} from './acquire.js';
6
+ export {exitPlanets} from './exit.js';
@@ -0,0 +1,335 @@
1
+ import {zeroAddress, type Address} from 'viem';
2
+ import type {SpaceInfo} from 'conquest-eth-v0-contracts';
3
+ import type {PlanetInfo, PlanetState} from 'conquest-eth-v0-contracts';
4
+ import {acquirePlanets} from './acquire.js';
5
+ import {exitPlanets} from './exit.js';
6
+ import type {FleetStorage} from '../storage/interface.js';
7
+ import type {
8
+ Clients,
9
+ ClientsWithOptionalWallet,
10
+ ContractConfig,
11
+ ExternalPlanet,
12
+ GameContract,
13
+ PendingExit,
14
+ } from '../types.js';
15
+
16
+ /**
17
+ * PlanetManager manages planet-related operations in the Conquest game
18
+ * including acquiring new planets and initiating exit processes
19
+ */
20
+ export class PlanetManager {
21
+ constructor(
22
+ private readonly clients: ClientsWithOptionalWallet,
23
+ private readonly gameContract: GameContract,
24
+ private readonly spaceInfo: SpaceInfo,
25
+ private readonly contractConfig: ContractConfig,
26
+ private readonly storage: FleetStorage,
27
+ ) {}
28
+
29
+ /**
30
+ * Ensure walletClient is available for operations that require it
31
+ */
32
+ private requireWalletClient(): Clients {
33
+ if (!this.clients.walletClient) {
34
+ throw new Error(
35
+ 'Wallet client is required for this operation. Please provide a PRIVATE_KEY environment variable.',
36
+ );
37
+ }
38
+ return this.clients as Clients;
39
+ }
40
+
41
+ /**
42
+ * Acquire (stake) multiple planets
43
+ */
44
+ async acquire(
45
+ planetIds: bigint[],
46
+ amountToMint: bigint,
47
+ tokenAmount: bigint,
48
+ ): Promise<{hash: `0x${string}`; planetsAcquired: bigint[]}> {
49
+ return acquirePlanets(
50
+ this.requireWalletClient(),
51
+ this.gameContract,
52
+ planetIds,
53
+ amountToMint,
54
+ tokenAmount,
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Acquire (stake) multiple planets with automatic cost calculation
60
+ */
61
+ async acquireWithAutoCalc(planetIds: bigint[]): Promise<{
62
+ hash: `0x${string}`;
63
+ planetsAcquired: bigint[];
64
+ costs: {amountToMint: bigint; tokenAmount: bigint};
65
+ }> {
66
+ const costs = this.calculateAcquisitionCosts(planetIds);
67
+ const result = await acquirePlanets(
68
+ this.requireWalletClient(),
69
+ this.gameContract,
70
+ planetIds,
71
+ costs.amountToMint,
72
+ costs.tokenAmount,
73
+ );
74
+ return {...result, costs};
75
+ }
76
+
77
+ /**
78
+ * Calculate acquisition costs for planets based on their stats
79
+ *
80
+ * @param planetIds - Array of planet location IDs
81
+ * @returns Object with total amountToMint and tokenAmount
82
+ */
83
+ calculateAcquisitionCosts(planetIds: bigint[]): {amountToMint: bigint; tokenAmount: bigint} {
84
+ const DECIMAL_14 = 100000000000000n;
85
+ let totalTokenAmount = 0n;
86
+ for (const planetId of planetIds) {
87
+ const planet = this.getPlanetInfo(planetId);
88
+ if (!planet) {
89
+ throw new Error(`Planet ${planetId} not found`);
90
+ }
91
+ // Use the planet's stake value from its statistics
92
+ // Multiply by DECIMAL_14 as the contract does
93
+ totalTokenAmount += BigInt(planet.stats.stake) * DECIMAL_14;
94
+ }
95
+
96
+ const amountToMint = totalTokenAmount;
97
+
98
+ // When using native token, we set tokenAmount to 0
99
+ return {amountToMint, tokenAmount: 0n};
100
+ }
101
+
102
+ /**
103
+ * Exit (unstake) multiple planets
104
+ */
105
+ async exit(planetIds: bigint[]): Promise<{hash: `0x${string}`; exitsInitiated: bigint[]}> {
106
+ return exitPlanets(
107
+ this.requireWalletClient(),
108
+ this.gameContract,
109
+ planetIds,
110
+ this.contractConfig.exitDuration,
111
+ this.storage,
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Get planet info by location ID
117
+ */
118
+ getPlanetInfo(planetId: bigint): PlanetInfo | undefined {
119
+ return this.spaceInfo.getPlanetInfoViaId(planetId);
120
+ }
121
+
122
+ /**
123
+ * Get planet ID by x,y coordinates
124
+ * @param x - X coordinate
125
+ * @param y - Y coordinate
126
+ * @returns Planet location ID as bigint, or undefined if no planet exists at coordinates
127
+ */
128
+ getPlanetIdByCoordinates(x: number, y: number): bigint | undefined {
129
+ const planet = this.spaceInfo.getPlanetInfo(x, y);
130
+ return planet?.location.id;
131
+ }
132
+
133
+ /**
134
+ * Get multiple planet infos
135
+ */
136
+ getPlanetInfos(planetIds: bigint[]): (PlanetInfo | undefined)[] {
137
+ return planetIds.map((id) => this.getPlanetInfo(id));
138
+ }
139
+
140
+ /**
141
+ * Get planets around a center point within a radius
142
+ */
143
+ async getPlanetsAround(
144
+ centerX: number,
145
+ centerY: number,
146
+ radius: number,
147
+ ): Promise<{info: PlanetInfo; state: PlanetState}[]> {
148
+ // Get planet infos from SpaceInfo within the bounding box
149
+ const planetsInRect: PlanetInfo[] = [];
150
+ for (const planet of this.spaceInfo.yieldPlanetsFromRect(
151
+ centerX - radius,
152
+ centerY - radius,
153
+ centerX + radius,
154
+ centerY + radius,
155
+ )) {
156
+ // Calculate actual distance to filter by radius
157
+ const dx = planet.location.x - centerX;
158
+ const dy = planet.location.y - centerY;
159
+ const distance = Math.sqrt(dx * dx + dy * dy);
160
+ if (distance <= radius) {
161
+ planetsInRect.push(planet);
162
+ }
163
+ }
164
+
165
+ // Batch query contract for planet states
166
+ const planetIds = planetsInRect.map((p) => p.location.id);
167
+ const result = await this.clients.publicClient.readContract({
168
+ address: this.gameContract.address,
169
+ abi: this.gameContract.abi,
170
+ functionName: 'getPlanetStates',
171
+ args: [planetIds],
172
+ });
173
+
174
+ const states = result[0];
175
+
176
+ // Get current time for computing latest state
177
+ const currentTime = Math.floor(Date.now() / 1000);
178
+
179
+ // Combine info with states and compute latest state
180
+ return planetsInRect.map((planet, index) => {
181
+ const rawState = states[index];
182
+ // Create a mutable copy of the state to compute updates
183
+ const stateCopy = {
184
+ owner: rawState.owner === zeroAddress ? undefined : rawState.owner,
185
+ ownerYakuzaSubscriptionEndTime: 0, // TODO
186
+ lastUpdatedSaved: rawState.lastUpdated,
187
+ startExitTime: rawState.exitStartTime,
188
+ numSpaceships: rawState.numSpaceships,
189
+ flagTime: 0, // TODO
190
+ travelingUpkeep: 0, // TODO
191
+ overflow: 0, // TODO
192
+ active: rawState.active,
193
+ exiting: false, // will be populated
194
+ exitTimeLeft: 0, // will be populated
195
+ natives: false, // will be populated
196
+ capturing: false, // will be populated
197
+ inReach: false, // will be populated
198
+ rewardGiver: '', // will be populated
199
+ metadata: {},
200
+ };
201
+ // Compute the latest state using SpaceInfo
202
+ this.spaceInfo.computePlanetUpdateForTimeElapsed(stateCopy, planet, currentTime);
203
+ return {
204
+ info: planet,
205
+ state: stateCopy,
206
+ };
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Get my planets (owned by the current wallet)
212
+ */
213
+ async getMyPlanets(
214
+ radius: number = 100,
215
+ ): Promise<Array<{info: PlanetInfo; state: ExternalPlanet}>> {
216
+ const sender = this.requireWalletClient().walletClient.account!.address;
217
+
218
+ // For now, use a simple approach: get all planets in area and filter by owner
219
+ // A better approach would be to use an index or The Graph
220
+ const planetsInRect: PlanetInfo[] = [];
221
+
222
+ // Get planets from 0,0 out to radius
223
+ for (const planet of this.spaceInfo.yieldPlanetsFromRect(-radius, -radius, radius, radius)) {
224
+ planetsInRect.push(planet);
225
+ }
226
+
227
+ // Batch query contract for planet states
228
+ const planetIds = planetsInRect.map((p) => p.location.id);
229
+ const result = await this.clients.publicClient.readContract({
230
+ address: this.gameContract.address,
231
+ abi: this.gameContract.abi,
232
+ functionName: 'getPlanetStates',
233
+ args: [planetIds],
234
+ });
235
+ const states = result[0];
236
+
237
+ // Get current time for computing latest state
238
+ const currentTime = Math.floor(Date.now() / 1000);
239
+
240
+ // Filter by owner and compute latest state
241
+ const myPlanets: Array<{info: PlanetInfo; state: ExternalPlanet}> = [];
242
+ for (let i = 0; i < planetsInRect.length; i++) {
243
+ const rawState = states[i];
244
+ if (rawState && rawState.owner && rawState.owner.toLowerCase() === sender.toLowerCase()) {
245
+ // Create a mutable copy of the state to compute updates
246
+ const stateCopy: any = {...rawState};
247
+ // Compute the latest state using SpaceInfo
248
+ this.spaceInfo.computePlanetUpdateForTimeElapsed(stateCopy, planetsInRect[i], currentTime);
249
+ myPlanets.push({info: planetsInRect[i], state: stateCopy});
250
+ }
251
+ }
252
+
253
+ return myPlanets;
254
+ }
255
+
256
+ /**
257
+ * Get pending exits for the current player
258
+ */
259
+ async getMyPendingExits(): Promise<PendingExit[]> {
260
+ const sender = this.requireWalletClient().walletClient.account!.address;
261
+ return this.storage.getPendingExitsByPlayer(sender);
262
+ }
263
+
264
+ /**
265
+ * Verify exit status for a planet
266
+ */
267
+ async verifyExitStatus(
268
+ planetId: bigint,
269
+ ): Promise<{exit: PendingExit; interrupted: boolean; newOwner?: Address}> {
270
+ const exit = await this.storage.getPendingExit(planetId);
271
+ if (!exit) {
272
+ throw new Error(`No pending exit found for planet ${planetId}`);
273
+ }
274
+
275
+ // Query contract for current planet state
276
+ const result = await this.clients.publicClient.readContract({
277
+ ...this.gameContract,
278
+ functionName: 'getPlanetStates',
279
+ args: [[planetId]],
280
+ });
281
+ const states = result[0];
282
+
283
+ if (states.length === 0) {
284
+ throw new Error(`Could not get planet state for ${planetId}`);
285
+ }
286
+
287
+ const currentState = states[0];
288
+ const currentTime = Math.floor(Date.now() / 1000);
289
+
290
+ // Check if exit was interrupted by an attack
291
+ let interrupted = false;
292
+ if (currentState.owner && currentState.owner.toLowerCase() !== exit.player.toLowerCase()) {
293
+ interrupted = true;
294
+ await this.storage.markExitInterrupted(planetId, currentTime, currentState.owner);
295
+ }
296
+
297
+ // Check if exit is complete
298
+ if (!currentState.active && currentTime >= exit.exitCompleteTime) {
299
+ await this.storage.markExitCompleted(planetId, currentTime);
300
+ }
301
+
302
+ const updatedExit = await this.storage.getPendingExit(planetId);
303
+ if (!updatedExit) {
304
+ throw new Error('Exit was cleaned up during verification');
305
+ }
306
+
307
+ return {
308
+ exit: updatedExit,
309
+ interrupted,
310
+ newOwner: currentState.owner,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Clean up old completed exits
316
+ */
317
+ async cleanupOldCompletedExits(olderThanDays: number = 7): Promise<void> {
318
+ const olderThan = Math.floor(Date.now() / 1000) - olderThanDays * 24 * 60 * 60;
319
+ await this.storage.cleanupOldCompletedExits(olderThan);
320
+ }
321
+
322
+ /**
323
+ * Calculate distance between two planets
324
+ */
325
+ calculateDistance(fromPlanetId: bigint, toPlanetId: bigint): number | undefined {
326
+ const fromPlanet = this.getPlanetInfo(fromPlanetId);
327
+ const toPlanet = this.getPlanetInfo(toPlanetId);
328
+
329
+ if (!fromPlanet || !toPlanet) {
330
+ return undefined;
331
+ }
332
+
333
+ return this.spaceInfo.distance(fromPlanet, toPlanet);
334
+ }
335
+ }