@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/cli.ts ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {createServer} from './index.js';
4
+ import {Command} from 'commander';
5
+ import pkg from '../package.json' with {type: 'json'};
6
+ import {getChain} from 'tools-ethereum/helpers';
7
+ import {loadEnv} from 'ldenv';
8
+ import * as tools from './tools/index.js';
9
+ import {registerAllToolCommands} from './cli-tool-generator.js';
10
+
11
+ loadEnv();
12
+
13
+ const program = new Command();
14
+
15
+ // Get the binary name from package.json
16
+ const binName = Object.keys(pkg.bin || {})[0];
17
+
18
+ program
19
+ .name(binName)
20
+ .description(pkg.description || 'Conquest.eth CLI - MCP server and direct tool execution')
21
+ .version(pkg.version)
22
+ // Global options available to all commands
23
+ .option('--rpc-url <url>', 'RPC URL for the Ethereum network', process.env.RPC_URL || '')
24
+ .option(
25
+ '--game-contract <address>',
26
+ 'Contract address of the game',
27
+ process.env.GAME_CONTRACT || '',
28
+ )
29
+ .option('--storage <type>', 'Storage backend: json or sqlite', process.env.STORAGE_TYPE || 'json')
30
+ .option(
31
+ '--storage-path <path>',
32
+ 'Path to storage directory',
33
+ process.env.STORAGE_PATH || './data',
34
+ )
35
+ .option(
36
+ '--private-key <key>',
37
+ 'Private key for sending transactions',
38
+ process.env.PRIVATE_KEY || '',
39
+ )
40
+ .action(() => {
41
+ program.help();
42
+ });
43
+
44
+ // MCP subcommand - starts the MCP server
45
+ program
46
+ .command('mcp')
47
+ .description('Start the MCP server')
48
+ .option(
49
+ '--ethereum',
50
+ 'Whether to also provide tools-ethereum tools',
51
+ process.env.ETHEREUM_TOOLS === 'true',
52
+ )
53
+ .action(async () => {
54
+ const options = program.opts();
55
+ const mcpOptions = program.commands.find((cmd) => cmd.name() === 'mcp')?.opts() || {};
56
+
57
+ const rpcUrl = options.rpcUrl;
58
+ const gameContract = options.gameContract;
59
+ const ethereum = mcpOptions.ethereum ?? process.env.ETHEREUM_TOOLS === 'true';
60
+ const privateKey = options.privateKey;
61
+ const storage = options.storage;
62
+ const storagePath = options.storagePath;
63
+
64
+ // Validate required options
65
+ if (!rpcUrl) {
66
+ console.error('Error: --rpc-url option or RPC_URL environment variable is required');
67
+ process.exit(1);
68
+ }
69
+
70
+ if (!gameContract) {
71
+ console.error(
72
+ 'Error: --game-contract option or GAME_CONTRACT environment variable is required',
73
+ );
74
+ process.exit(1);
75
+ }
76
+
77
+ // Warn if private key is not provided for write operations
78
+ if (!privateKey) {
79
+ console.warn(
80
+ 'Warning: PRIVATE_KEY environment variable is required for sending transactions',
81
+ );
82
+ } else if (!privateKey.startsWith('0x')) {
83
+ console.error('Error: PRIVATE_KEY must start with 0x');
84
+ process.exit(1);
85
+ }
86
+
87
+ const chain = await getChain(rpcUrl);
88
+ const transport = new StdioServerTransport();
89
+ const server = createServer(
90
+ {
91
+ chain,
92
+ privateKey: privateKey as `0x${string}`,
93
+ gameContract: gameContract as `0x${string}`,
94
+ },
95
+ {
96
+ ethereum,
97
+ storageConfig: {
98
+ type: storage as 'json' | 'sqlite',
99
+ dataDir: storagePath,
100
+ },
101
+ },
102
+ );
103
+ await server.connect(transport);
104
+ });
105
+
106
+ // Register all tool commands dynamically
107
+ registerAllToolCommands(program, tools);
108
+
109
+ program.parse(process.argv);
@@ -0,0 +1,41 @@
1
+ import {SpaceInfo} from 'conquest-eth-v0-contracts';
2
+ import type {ClientsWithOptionalWallet, ContractConfig, GameContract} from '../types.js';
3
+
4
+ export async function createSpaceInfo(
5
+ clients: ClientsWithOptionalWallet,
6
+ gameContract: GameContract,
7
+ ): Promise<{spaceInfo: SpaceInfo; contractConfig: ContractConfig}> {
8
+ // Fetch config from contract
9
+ const config = await clients.publicClient.readContract({
10
+ ...gameContract,
11
+ functionName: 'getConfig',
12
+ });
13
+
14
+ const contractConfig: ContractConfig = {
15
+ genesis: BigInt(config.genesis),
16
+ resolveWindow: BigInt(config.resolveWindow),
17
+ timePerDistance: BigInt(config.timePerDistance),
18
+ exitDuration: BigInt(config.exitDuration),
19
+ acquireNumSpaceships: Number(config.acquireNumSpaceships),
20
+ };
21
+
22
+ // Create SpaceInfo instance with config
23
+ const spaceInfo = new SpaceInfo({
24
+ genesis: config.genesis as `0x${string}`,
25
+ resolveWindow: Number(config.resolveWindow),
26
+ timePerDistance: Number(config.timePerDistance),
27
+ exitDuration: Number(config.exitDuration),
28
+ acquireNumSpaceships: Number(config.acquireNumSpaceships),
29
+ productionSpeedUp: Number(config.productionSpeedUp),
30
+ productionCapAsDuration: Number(config.productionCapAsDuration),
31
+ upkeepProductionDecreaseRatePer10000th: Number(config.upkeepProductionDecreaseRatePer10000th),
32
+ fleetSizeFactor6: Number(config.fleetSizeFactor6),
33
+ giftTaxPer10000: Number(config.giftTaxPer10000),
34
+ stakeRange: config.stakeRange,
35
+ stakeMultiplier10000th: Number(config.stakeMultiplier10000th),
36
+ bootstrapSessionEndTime: Number(config.bootstrapSessionEndTime),
37
+ infinityStartTime: Number(config.infinityStartTime),
38
+ });
39
+
40
+ return {spaceInfo, contractConfig};
41
+ }
@@ -0,0 +1,8 @@
1
+ // Fleet Manager and exports
2
+ export {FleetManager} from './manager.js';
3
+
4
+ // Send functions
5
+ export {sendFleet} from './send.js';
6
+
7
+ // Resolve functions
8
+ export {resolveFleet, resolveFleetWithSpaceInfo, getResolvableFleets} from './resolve.js';
@@ -0,0 +1,140 @@
1
+ import type {Address} from 'viem';
2
+ import type {SpaceInfo} from 'conquest-eth-v0-contracts';
3
+ import type {FleetStorage} from '../storage/interface.js';
4
+ import type {
5
+ Clients,
6
+ ClientsWithOptionalWallet,
7
+ ContractConfig,
8
+ GameContract,
9
+ PendingFleet,
10
+ } from '../types.js';
11
+ import {getResolvableFleets, resolveFleetWithSpaceInfo} from './resolve.js';
12
+ import {sendFleet} from './send.js';
13
+ /**
14
+ * FleetManager manages the lifecycle of fleets in the Conquest game
15
+ * including sending new fleets and resolving existing ones
16
+ */
17
+ export class FleetManager {
18
+ constructor(
19
+ private readonly clients: ClientsWithOptionalWallet,
20
+ private readonly gameContract: GameContract,
21
+ private readonly spaceInfo: SpaceInfo,
22
+ private readonly contractConfig: ContractConfig,
23
+ private readonly storage: FleetStorage,
24
+ // private readonly contractAddress: Address,
25
+ ) {}
26
+
27
+ /**
28
+ * Ensure walletClient is available for operations that require it
29
+ */
30
+ private requireWalletClient(): Clients {
31
+ if (!this.clients.walletClient) {
32
+ throw new Error(
33
+ 'Wallet client is required for this operation. Please provide a PRIVATE_KEY environment variable.',
34
+ );
35
+ }
36
+ return this.clients as Clients;
37
+ }
38
+
39
+ /**
40
+ * Send a fleet to a destination planet
41
+ */
42
+ async send(
43
+ fromPlanetId: bigint,
44
+ toPlanetId: bigint,
45
+ quantity: number,
46
+ options?: {
47
+ gift?: boolean;
48
+ specific?: Address;
49
+ arrivalTimeWanted?: bigint;
50
+ secret?: `0x${string}`;
51
+ },
52
+ ): Promise<PendingFleet> {
53
+ return sendFleet(
54
+ this.requireWalletClient(),
55
+ this.gameContract,
56
+ fromPlanetId,
57
+ toPlanetId,
58
+ quantity,
59
+ this.spaceInfo,
60
+ this.contractConfig,
61
+ this.storage,
62
+ options,
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Resolve (reveal) a fleet to complete its journey
68
+ */
69
+ async resolve(
70
+ fleetId: string,
71
+ ): Promise<{resolved: true; fleet: PendingFleet} | {resolved: false; reason: string}> {
72
+ return resolveFleetWithSpaceInfo(
73
+ this.requireWalletClient(),
74
+ this.gameContract,
75
+ this.spaceInfo,
76
+ fleetId,
77
+ this.storage,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Get fleets that can be resolved (not yet resolved and past resolve window)
83
+ */
84
+ async getResolvableFleets(): Promise<PendingFleet[]> {
85
+ return getResolvableFleets(this.storage, this.contractConfig.resolveWindow);
86
+ }
87
+
88
+ /**
89
+ * Get all pending fleets for the current sender
90
+ */
91
+ async getMyPendingFleets(): Promise<PendingFleet[]> {
92
+ const sender = this.requireWalletClient().walletClient.account!.address;
93
+ return this.storage.getPendingFleetsBySender(sender);
94
+ }
95
+
96
+ /**
97
+ * Get a specific fleet by ID
98
+ */
99
+ async getFleet(fleetId: string): Promise<PendingFleet | null> {
100
+ return this.storage.getFleet(fleetId);
101
+ }
102
+
103
+ /**
104
+ * Get all fleets in storage
105
+ */
106
+ async getAllFleets(): Promise<PendingFleet[]> {
107
+ return this.storage.getAllFleets();
108
+ }
109
+
110
+ /**
111
+ * Resolve all fleets that are ready (batch operation)
112
+ */
113
+ async resolveAllReady(): Promise<{
114
+ successful: PendingFleet[];
115
+ failed: Array<{fleetId: string; reason: string}>;
116
+ }> {
117
+ const readyFleets = await this.getResolvableFleets();
118
+ const successful: PendingFleet[] = [];
119
+ const failed: Array<{fleetId: string; reason: string}> = [];
120
+
121
+ for (const fleet of readyFleets) {
122
+ const result = await this.resolve(fleet.fleetId);
123
+ if (result.resolved) {
124
+ successful.push(result.fleet);
125
+ } else {
126
+ failed.push({fleetId: fleet.fleetId, reason: result.reason});
127
+ }
128
+ }
129
+
130
+ return {successful, failed};
131
+ }
132
+
133
+ /**
134
+ * Clean up old resolved fleets from storage
135
+ */
136
+ async cleanupOldResolvedFleets(olderThanDays: number = 7): Promise<void> {
137
+ const olderThan = Math.floor(Date.now() / 1000) - olderThanDays * 24 * 60 * 60;
138
+ await this.storage.cleanupOldResolvedFleets(olderThan);
139
+ }
140
+ }
@@ -0,0 +1,187 @@
1
+ import type {Address, WalletClient} from 'viem';
2
+ import type {SpaceInfo} from 'conquest-eth-v0-contracts';
3
+ import type {FleetStorage} from '../storage/interface.js';
4
+ import {Clients, FleetResolution, GameContract, PendingFleet} from '../types.js';
5
+ import {getCurrentTimestamp} from '../util/time.js';
6
+
7
+ /**
8
+ * Resolve (reveal) a fleet to complete its journey and trigger combat
9
+ *
10
+ * @param walletClient - Viem wallet client for signing transactions
11
+ * @param fleetsRevealContract - The fleets reveal contract instance with address, abi, and clients
12
+ * @param fleetId - The fleet ID to resolve
13
+ * @param storage - Storage instance for tracking pending fleets
14
+ * @returns The resolution result with either resolved fleet or reason for failure
15
+ */
16
+ export async function resolveFleet(
17
+ walletClient: WalletClient,
18
+ fleetsRevealContract: {
19
+ address: Address;
20
+ abi: readonly unknown[];
21
+ publicClient: unknown;
22
+ walletClient: WalletClient | undefined;
23
+ },
24
+ fleetId: string,
25
+ storage: FleetStorage,
26
+ ): Promise<{resolved: true; fleet: PendingFleet} | {resolved: false; reason: string}> {
27
+ // Get the pending fleet from storage
28
+ const pendingFleet = await storage.getFleet(fleetId);
29
+
30
+ if (!pendingFleet) {
31
+ return {resolved: false, reason: `Fleet ${fleetId} not found in storage`};
32
+ }
33
+
34
+ if (pendingFleet.resolved) {
35
+ return {resolved: false, reason: `Fleet ${fleetId} has already been resolved`};
36
+ }
37
+
38
+ // The operator must be the same as the wallet client account
39
+ const operator = walletClient.account!.address;
40
+ if (pendingFleet.operator !== operator) {
41
+ return {resolved: false, reason: `Only the operator can resolve this fleet`};
42
+ }
43
+
44
+ // Build the FleetResolution struct
45
+ const resolution: FleetResolution = {
46
+ from: pendingFleet.fromPlanetId,
47
+ to: pendingFleet.toPlanetId,
48
+ distance: 0n, // Will be calculated by SpaceInfo
49
+ arrivalTimeWanted: pendingFleet.arrivalTimeWanted,
50
+ gift: pendingFleet.gift,
51
+ specific: pendingFleet.specific,
52
+ secret: pendingFleet.secret,
53
+ fleetSender: pendingFleet.fleetSender,
54
+ operator: pendingFleet.operator,
55
+ };
56
+
57
+ // Get the contract resolveFleet function signature
58
+ const publicClient = fleetsRevealContract.publicClient as any;
59
+ const request = await publicClient.simulateContract({
60
+ address: fleetsRevealContract.address,
61
+ abi: fleetsRevealContract.abi,
62
+ functionName: 'resolveFleet',
63
+ args: [BigInt('0x' + fleetId), resolution],
64
+ account: operator,
65
+ });
66
+
67
+ // Send the transaction
68
+ const hash = await walletClient.writeContract(request);
69
+
70
+ // Mark fleet as resolved in storage
71
+ const resolvedAt = getCurrentTimestamp();
72
+ await storage.markResolved(fleetId, resolvedAt);
73
+
74
+ // Update the fleet record
75
+ const resolvedFleet: PendingFleet = {
76
+ ...pendingFleet,
77
+ resolved: true,
78
+ resolvedAt,
79
+ };
80
+
81
+ return {resolved: true, fleet: resolvedFleet};
82
+ }
83
+
84
+ /**
85
+ * Resolve a fleet with custom SpaceInfo for distance calculation
86
+ *
87
+ * @param clients - Viem clients (publicClient and walletClient)
88
+ * @param gameContract - The game contract instance with address and ABI
89
+ * @param spaceInfo - SpaceInfo instance for distance calculation
90
+ * @param fleetId - The fleet ID to resolve
91
+ * @param storage - Storage instance for tracking pending fleets
92
+ * @returns The resolution result with either resolved fleet or reason for failure
93
+ */
94
+ export async function resolveFleetWithSpaceInfo(
95
+ clients: Clients,
96
+ gameContract: GameContract,
97
+ spaceInfo: SpaceInfo,
98
+ fleetId: string,
99
+ storage: FleetStorage,
100
+ ): Promise<{resolved: true; fleet: PendingFleet} | {resolved: false; reason: string}> {
101
+ // Get the pending fleet from storage
102
+ const pendingFleet = await storage.getFleet(fleetId);
103
+
104
+ if (!pendingFleet) {
105
+ return {resolved: false, reason: `Fleet ${fleetId} not found in storage`};
106
+ }
107
+
108
+ if (pendingFleet.resolved) {
109
+ return {resolved: false, reason: `Fleet ${fleetId} has already been resolved`};
110
+ }
111
+
112
+ // The operator must be the same as the wallet client account
113
+ const operator = clients.walletClient.account!.address;
114
+ if (pendingFleet.operator !== operator) {
115
+ return {resolved: false, reason: `Only the operator can resolve this fleet`};
116
+ }
117
+
118
+ // Get planet info for distance calculation
119
+ const fromPlanet = spaceInfo.getPlanetInfoViaId(pendingFleet.fromPlanetId);
120
+ const toPlanet = spaceInfo.getPlanetInfoViaId(pendingFleet.toPlanetId);
121
+
122
+ if (!fromPlanet || !toPlanet) {
123
+ return {resolved: false, reason: 'Could not get planet info for one or both planets'};
124
+ }
125
+
126
+ // Calculate distance using SpaceInfo
127
+ const distance = spaceInfo.distance(fromPlanet, toPlanet);
128
+
129
+ // Build the FleetResolution struct
130
+ const resolution: FleetResolution = {
131
+ from: pendingFleet.fromPlanetId,
132
+ to: pendingFleet.toPlanetId,
133
+ distance: BigInt(distance),
134
+ arrivalTimeWanted: pendingFleet.arrivalTimeWanted,
135
+ gift: pendingFleet.gift,
136
+ specific: pendingFleet.specific,
137
+ secret: pendingFleet.secret,
138
+ fleetSender: pendingFleet.fleetSender,
139
+ operator: pendingFleet.operator,
140
+ };
141
+
142
+ // Get the contract resolveFleet function signature
143
+ const simulation = await clients.publicClient.simulateContract({
144
+ address: gameContract.address,
145
+ abi: gameContract.abi,
146
+ functionName: 'resolveFleet',
147
+ args: [BigInt(fleetId), resolution],
148
+ account: operator,
149
+ });
150
+
151
+ // Send the transaction
152
+ const hash = await clients.walletClient.writeContract(simulation.request);
153
+
154
+ // Mark fleet as resolved in storage
155
+ const resolvedAt = getCurrentTimestamp();
156
+ await storage.markResolved(fleetId, resolvedAt);
157
+
158
+ // Update the fleet record
159
+ const resolvedFleet: PendingFleet = {
160
+ ...pendingFleet,
161
+ resolved: true,
162
+ resolvedAt,
163
+ };
164
+
165
+ return {resolved: true, fleet: resolvedFleet};
166
+ }
167
+
168
+ /**
169
+ * Get fleets that can be resolved (not yet resolved and past resolve window)
170
+ *
171
+ * @param storage - Storage instance for tracking pending fleets
172
+ * @param resolveWindow - The resolve window duration in seconds from contract config
173
+ * @returns List of fleets that can be resolved
174
+ */
175
+ export async function getResolvableFleets(
176
+ storage: FleetStorage,
177
+ resolveWindow: bigint,
178
+ ): Promise<PendingFleet[]> {
179
+ const currentTime = getCurrentTimestamp();
180
+ const fleets = await storage.getResolvableFleets();
181
+
182
+ return fleets.filter((fleet) => {
183
+ // Check if fleet is past the resolve window
184
+ const resolveWindowOpen = fleet.estimatedArrivalTime + Number(resolveWindow);
185
+ return !fleet.resolved && currentTime >= resolveWindowOpen;
186
+ });
187
+ }
@@ -0,0 +1,112 @@
1
+ import {zeroAddress, type Address} from 'viem';
2
+ import type {SpaceInfo} from 'conquest-eth-v0-contracts';
3
+ import type {FleetStorage} from '../storage/interface.js';
4
+ import type {Clients, ContractConfig, GameContract, PendingFleet} from '../types.js';
5
+ import {computeFleetId, computeToHash, generateSecret} from '../util/hashing.js';
6
+ import {calculateEstimatedArrivalTime, getCurrentTimestamp} from '../util/time.js';
7
+
8
+ /**
9
+ * Send a fleet to a destination planet
10
+ *
11
+ * @param clients - Viem clients (publicClient and walletClient)
12
+ * @param gameContract - The game contract instance with address and ABI
13
+ * @param fromPlanetId - Source planet location ID
14
+ * @param toPlanetId - Destination planet location ID
15
+ * @param quantity - Number of spaceships to send
16
+ * @param spaceInfo - SpaceInfo instance for distance calculation
17
+ * @param contractConfig - Contract config for time calculations
18
+ * @param storage - Storage instance for tracking pending fleets
19
+ * @param options - Optional parameters
20
+ * @param options.gift - Whether the fleet is a gift (no combat)
21
+ * @param options.specific - Specific target address (advanced feature)
22
+ * @param options.arrivalTimeWanted - Preferred arrival time (advanced feature)
23
+ * @param options.secret - Random secret for hash commitment (auto-generated if not provided)
24
+ * @returns The pending fleet information
25
+ */
26
+ export async function sendFleet(
27
+ clients: Clients,
28
+ gameContract: GameContract,
29
+ fromPlanetId: bigint,
30
+ toPlanetId: bigint,
31
+ quantity: number,
32
+ spaceInfo: SpaceInfo,
33
+ contractConfig: ContractConfig,
34
+ storage: FleetStorage,
35
+ options?: {
36
+ gift?: boolean;
37
+ specific?: Address;
38
+ arrivalTimeWanted?: bigint;
39
+ secret?: `0x${string}`;
40
+ },
41
+ ): Promise<PendingFleet> {
42
+ const fleetSender = clients.walletClient.account!.address;
43
+ const operator = fleetSender; // Default to same address
44
+
45
+ // Get planet info for distance calculation
46
+ const fromPlanet = spaceInfo.getPlanetInfoViaId(fromPlanetId);
47
+ const toPlanet = spaceInfo.getPlanetInfoViaId(toPlanetId);
48
+
49
+ if (!fromPlanet || !toPlanet) {
50
+ throw new Error('Could not get planet info for one or both planets');
51
+ }
52
+
53
+ // Calculate distance
54
+ const distance = spaceInfo.distance(fromPlanet, toPlanet);
55
+
56
+ // Generate secret if not provided
57
+ const secret = options?.secret || generateSecret();
58
+
59
+ // Calculate estimated arrival time using contract config
60
+ const estimatedArrivalTime = calculateEstimatedArrivalTime({
61
+ startTime: BigInt(getCurrentTimestamp()),
62
+ distance: BigInt(distance),
63
+ timePerDistance: contractConfig.timePerDistance,
64
+ });
65
+
66
+ const parameters = {
67
+ gift: options?.gift ?? false,
68
+ specific: options?.specific ?? zeroAddress,
69
+ arrivalTimeWanted: options?.arrivalTimeWanted || 0n,
70
+ };
71
+ const toHash = computeToHash(toPlanetId, secret, parameters);
72
+
73
+ // Get the contract send function signature
74
+ const simulation = await clients.publicClient.simulateContract({
75
+ address: gameContract.address,
76
+ abi: gameContract.abi,
77
+ functionName: 'send',
78
+ args: [fromPlanetId, quantity, toHash],
79
+ account: fleetSender,
80
+ });
81
+
82
+ // Compute fleet ID
83
+ const fleetId = computeFleetId(toHash, fromPlanetId, fleetSender, operator);
84
+
85
+ // Create pending fleet record
86
+ const pendingFleet: PendingFleet = {
87
+ fleetId,
88
+ fromPlanetId,
89
+ toPlanetId,
90
+ quantity,
91
+ secret,
92
+ gift: parameters.gift,
93
+ specific: parameters.specific,
94
+ arrivalTimeWanted: parameters.arrivalTimeWanted,
95
+ fleetSender,
96
+ operator,
97
+ committedAt: getCurrentTimestamp(),
98
+ estimatedArrivalTime,
99
+ resolved: false,
100
+ };
101
+
102
+ // we always save to storage first in case an error happen while the tx is still broadcasted
103
+ await storage.saveFleet(pendingFleet);
104
+
105
+ // Send the transaction
106
+ const hash = await clients.walletClient.writeContract(simulation.request);
107
+
108
+ pendingFleet.hash = hash;
109
+ await storage.saveFleet(pendingFleet);
110
+
111
+ return pendingFleet;
112
+ }
@@ -0,0 +1,59 @@
1
+ import type {Tool, ToolEnvironment} from '../types.js';
2
+ import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type {FleetManager} from '../fleet/manager.js';
4
+ import type {PlanetManager} from '../planet/manager.js';
5
+ import {convertToCallToolResult} from '../types.js';
6
+
7
+ // Helper function to handle BigInt serialization in JSON.stringify
8
+ export function stringifyWithBigInt(obj: any, space?: number): string {
9
+ return JSON.stringify(
10
+ obj,
11
+ (_key, value) => (typeof value === 'bigint' ? value.toString() : value),
12
+ space,
13
+ );
14
+ }
15
+
16
+ // Create tool environment with sendStatus
17
+ export function createToolEnvironment(
18
+ server: McpServer,
19
+ fleetManager: FleetManager,
20
+ planetManager: PlanetManager,
21
+ ): ToolEnvironment {
22
+ return {
23
+ sendStatus: async (_message: string) => {
24
+ // TODO: Implement progress notifications when sessionId is available
25
+ // For now, this is a no-op since we don't have sessionId in the current architecture
26
+ },
27
+ fleetManager,
28
+ planetManager,
29
+ };
30
+ }
31
+
32
+ // Register tool with MCP server
33
+ export function registerTool({
34
+ server,
35
+ name,
36
+ tool,
37
+ fleetManager,
38
+ planetManager,
39
+ }: {
40
+ server: McpServer;
41
+ name: string;
42
+ tool: Tool<any>;
43
+ fleetManager: FleetManager;
44
+ planetManager: PlanetManager;
45
+ }): void {
46
+ server.registerTool(
47
+ name,
48
+ {
49
+ description: tool.description,
50
+ inputSchema: tool.schema as any,
51
+ },
52
+ async (params: unknown) => {
53
+ const env = createToolEnvironment(server, fleetManager, planetManager);
54
+
55
+ const result = await tool.execute(env, params as any);
56
+ return convertToCallToolResult(result);
57
+ },
58
+ );
59
+ }