@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.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/cli-tool-generator.d.ts +22 -0
- package/dist/cli-tool-generator.d.ts.map +1 -0
- package/dist/cli-tool-generator.js +217 -0
- package/dist/cli-tool-generator.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts/space-info.d.ts +7 -0
- package/dist/contracts/space-info.d.ts.map +1 -0
- package/dist/contracts/space-info.js +34 -0
- package/dist/contracts/space-info.js.map +1 -0
- package/dist/fleet/index.d.ts +4 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +7 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/manager.d.ts +70 -0
- package/dist/fleet/manager.d.ts.map +1 -0
- package/dist/fleet/manager.js +92 -0
- package/dist/fleet/manager.js.map +1 -0
- package/dist/fleet/resolve.d.ts +51 -0
- package/dist/fleet/resolve.d.ts.map +1 -0
- package/dist/fleet/resolve.js +140 -0
- package/dist/fleet/resolve.js.map +1 -0
- package/dist/fleet/send.d.ts +29 -0
- package/dist/fleet/send.d.ts.map +1 -0
- package/dist/fleet/send.js +81 -0
- package/dist/fleet/send.js.map +1 -0
- package/dist/helpers/index.d.ts +14 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +28 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/planet/acquire.d.ts +16 -0
- package/dist/planet/acquire.d.ts.map +1 -0
- package/dist/planet/acquire.js +27 -0
- package/dist/planet/acquire.js.map +1 -0
- package/dist/planet/exit.d.ts +17 -0
- package/dist/planet/exit.d.ts.map +1 -0
- package/dist/planet/exit.js +56 -0
- package/dist/planet/exit.js.map +1 -0
- package/dist/planet/index.d.ts +4 -0
- package/dist/planet/index.d.ts.map +1 -0
- package/dist/planet/index.js +6 -0
- package/dist/planet/index.js.map +1 -0
- package/dist/planet/manager.d.ts +106 -0
- package/dist/planet/manager.d.ts.map +1 -0
- package/dist/planet/manager.js +253 -0
- package/dist/planet/manager.js.map +1 -0
- package/dist/storage/interface.d.ts +93 -0
- package/dist/storage/interface.d.ts.map +1 -0
- package/dist/storage/interface.js +2 -0
- package/dist/storage/interface.js.map +1 -0
- package/dist/storage/json-storage.d.ts +28 -0
- package/dist/storage/json-storage.d.ts.map +1 -0
- package/dist/storage/json-storage.js +148 -0
- package/dist/storage/json-storage.js.map +1 -0
- package/dist/tools/acquire_planets.d.ts +7 -0
- package/dist/tools/acquire_planets.d.ts.map +1 -0
- package/dist/tools/acquire_planets.js +63 -0
- package/dist/tools/acquire_planets.js.map +1 -0
- package/dist/tools/exit_planets.d.ts +5 -0
- package/dist/tools/exit_planets.d.ts.map +1 -0
- package/dist/tools/exit_planets.js +31 -0
- package/dist/tools/exit_planets.js.map +1 -0
- package/dist/tools/get_my_planets.d.ts +5 -0
- package/dist/tools/get_my_planets.d.ts.map +1 -0
- package/dist/tools/get_my_planets.js +30 -0
- package/dist/tools/get_my_planets.js.map +1 -0
- package/dist/tools/get_pending_exits.d.ts +3 -0
- package/dist/tools/get_pending_exits.d.ts.map +1 -0
- package/dist/tools/get_pending_exits.js +37 -0
- package/dist/tools/get_pending_exits.js.map +1 -0
- package/dist/tools/get_pending_fleets.d.ts +3 -0
- package/dist/tools/get_pending_fleets.d.ts.map +1 -0
- package/dist/tools/get_pending_fleets.js +41 -0
- package/dist/tools/get_pending_fleets.js.map +1 -0
- package/dist/tools/get_planets_around.d.ts +7 -0
- package/dist/tools/get_planets_around.d.ts.map +1 -0
- package/dist/tools/get_planets_around.js +41 -0
- package/dist/tools/get_planets_around.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +11 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/resolve_fleet.d.ts +5 -0
- package/dist/tools/resolve_fleet.d.ts.map +1 -0
- package/dist/tools/resolve_fleet.js +37 -0
- package/dist/tools/resolve_fleet.js.map +1 -0
- package/dist/tools/send_fleet.d.ts +16 -0
- package/dist/tools/send_fleet.d.ts.map +1 -0
- package/dist/tools/send_fleet.js +62 -0
- package/dist/tools/send_fleet.js.map +1 -0
- package/dist/tools/verify_exit_status.d.ts +5 -0
- package/dist/tools/verify_exit_status.d.ts.map +1 -0
- package/dist/tools/verify_exit_status.js +39 -0
- package/dist/tools/verify_exit_status.js.map +1 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -0
- package/dist/util/hashing.d.ts +33 -0
- package/dist/util/hashing.d.ts.map +1 -0
- package/dist/util/hashing.js +38 -0
- package/dist/util/hashing.js.map +1 -0
- package/dist/util/time.d.ts +43 -0
- package/dist/util/time.d.ts.map +1 -0
- package/dist/util/time.js +55 -0
- package/dist/util/time.js.map +1 -0
- package/package.json +78 -0
- package/src/cli-tool-generator.ts +287 -0
- package/src/cli.ts +109 -0
- package/src/contracts/space-info.ts +41 -0
- package/src/fleet/index.ts +8 -0
- package/src/fleet/manager.ts +140 -0
- package/src/fleet/resolve.ts +187 -0
- package/src/fleet/send.ts +112 -0
- package/src/helpers/index.ts +59 -0
- package/src/index.ts +181 -0
- package/src/planet/acquire.ts +41 -0
- package/src/planet/exit.ts +71 -0
- package/src/planet/index.ts +6 -0
- package/src/planet/manager.ts +335 -0
- package/src/storage/interface.ts +111 -0
- package/src/storage/json-storage.ts +184 -0
- package/src/tools/acquire_planets.ts +81 -0
- package/src/tools/exit_planets.ts +35 -0
- package/src/tools/get_my_planets.ts +30 -0
- package/src/tools/get_pending_exits.ts +37 -0
- package/src/tools/get_pending_fleets.ts +41 -0
- package/src/tools/get_planets_around.ts +44 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/resolve_fleet.ts +37 -0
- package/src/tools/send_fleet.ts +68 -0
- package/src/tools/verify_exit_status.ts +43 -0
- package/src/types.ts +178 -0
- package/src/util/hashing.ts +60 -0
- 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,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
|
+
}
|