kill-switch-mcp 1.1.1 → 1.1.2

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.
@@ -1,4 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { register } from 'tsx/esm/api';
3
- register();
2
+
3
+ // Node 22+ has native TypeScript support (--experimental-strip-types).
4
+ // tsx's register() conflicts with Node 24's native .ts loader, causing
5
+ // ERR_REQUIRE_CYCLE_MODULE on dynamic import() of .ts action files.
6
+ // Only register tsx for older Node versions that lack native TS support.
7
+ const major = parseInt(process.versions.node.split('.')[0]);
8
+ if (major < 22) {
9
+ const { register } = await import('tsx/esm/api');
10
+ register();
11
+ }
12
+
4
13
  await import('../src/server.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kill-switch-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Kill Switch MCP Server — AI battle royale powered by Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@modelcontextprotocol/sdk": "^1.0.4",
19
- "tsx": "^4.19.0"
19
+ "tsx": "^4.19.0",
20
+ "viem": "^2.47.6"
20
21
  },
21
22
  "keywords": [
22
23
  "mcp",
package/src/api/index.ts CHANGED
@@ -80,7 +80,7 @@ class BotManager {
80
80
  gatewayUrl: gateway,
81
81
  connectionMode: 'control',
82
82
  autoReconnect: true, // Enable auto-reconnect for connection stability
83
- autoLaunchBrowser: 'auto', // Auto-launch browser if session is stale
83
+ autoLaunchBrowser: 'auto', // Auto-launch browser if session is dead
84
84
  showChat, // Show other players' chat (default: false for safety)
85
85
  });
86
86
 
package/src/server.ts CHANGED
@@ -9,9 +9,13 @@
9
9
  * - login: Log in, set up, and connect to the game.
10
10
  * - execute_code: Send commands to your bot during gameplay.
11
11
  * - get_status: Check your bot's current state without executing code.
12
- * - join_game: Join a game mode queue.
12
+ * - join_game: Join a free game mode queue.
13
13
  * - wait_for_game_start: Wait for the game to begin.
14
14
  * - disconnect_bot: Disconnect from the game.
15
+ * - setup_game_wallet: Set up a Tempo wallet access key for paid tournaments.
16
+ * - check_wallet: Check game wallet address and USDC balance.
17
+ * - tournament_schedule: View upcoming paid tournaments.
18
+ * - join_tournament: Join a paid tournament (deposits buy-in from game wallet).
15
19
  */
16
20
 
17
21
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -25,6 +29,14 @@ import { readFile } from 'fs/promises';
25
29
  import { join } from 'path';
26
30
  import { botManager } from './api/index.js';
27
31
  import { formatWorldState } from './sdk/formatter.js';
32
+ import {
33
+ hasGameWallet,
34
+ getWalletInfo,
35
+ generateAccessKey,
36
+ saveAccessKey,
37
+ getTournamentSchedule,
38
+ joinTournament,
39
+ } from './wallet/index.js';
28
40
 
29
41
  // ── Configuration ──
30
42
  // Server URL from CLI args or env. Defaults to localhost for local dev.
@@ -231,6 +243,77 @@ After joining, call wait_for_game_start to wait for the game to begin.`,
231
243
  type: 'object',
232
244
  properties: {}
233
245
  }
246
+ },
247
+ {
248
+ name: 'setup_game_wallet',
249
+ description: `Set up a game wallet for paid tournaments. This creates a local access key that lets you join paid tournament matches.
250
+
251
+ IMPORTANT: This tool handles real (or testnet) money. Explain to the user what's happening at each step.
252
+
253
+ Prerequisites:
254
+ - The user must have the Tempo CLI installed (curl -fsSL https://tempo.xyz/install | bash)
255
+ - The user must have run "tempo wallet login" to create their Tempo account
256
+
257
+ This tool generates a local access key. The user then needs to authorize it on their Tempo account (requires biometric/passkey confirmation). After that, the key is stored locally in ~/.killswitch/ and used for all tournament deposits.
258
+
259
+ Learn more about Tempo access keys: https://docs.tempo.xyz/protocol/tips/tip-1011`,
260
+ inputSchema: {
261
+ type: 'object',
262
+ properties: {
263
+ tempo_account_address: {
264
+ type: 'string',
265
+ description: 'The user\'s Tempo account address (0x...). Get this by running "tempo wallet whoami".'
266
+ }
267
+ },
268
+ required: ['tempo_account_address']
269
+ }
270
+ },
271
+ {
272
+ name: 'check_wallet',
273
+ description: `Check your game wallet status and USDC balance. Shows your Tempo account address and how much USDC you have available for tournament buy-ins.
274
+
275
+ If no game wallet is set up, this will tell you to run setup_game_wallet first.`,
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: {}
279
+ }
280
+ },
281
+ {
282
+ name: 'tournament_schedule',
283
+ description: `View upcoming paid tournaments. Shows start times, buy-in amounts, and how many players have signed up.
284
+
285
+ This is a read-only tool — it doesn't cost anything to check the schedule.`,
286
+ inputSchema: {
287
+ type: 'object',
288
+ properties: {}
289
+ }
290
+ },
291
+ {
292
+ name: 'join_tournament',
293
+ description: `Join a paid tournament by depositing the buy-in from your game wallet.
294
+
295
+ THIS TOOL SPENDS REAL MONEY (or testnet money). Before calling this tool:
296
+ 1. Tell the user exactly which tournament they're joining (time, buy-in amount)
297
+ 2. Show their current wallet balance
298
+ 3. Ask them to confirm they want to proceed
299
+ 4. Only then call this tool
300
+
301
+ The deposit is sent on-chain to the tournament's escrow contract. The user's buy-in is held in escrow until:
302
+ - They win → they receive 90% of the pot
303
+ - The match is cancelled → they get a full refund
304
+ - The server goes down → they can claim a refund after 1 hour
305
+
306
+ The user must be logged in (call login first) so we know their in-game username.`,
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ tournament_address: {
311
+ type: 'string',
312
+ description: 'The tournament contract address to join (from tournament_schedule)'
313
+ }
314
+ },
315
+ required: ['tournament_address']
316
+ }
234
317
  }
235
318
  ]
236
319
  };
@@ -286,7 +369,8 @@ async function loadActions(botName: string, bot: any, sdk: any): Promise<{ actio
286
369
  const name = toCamelCase(basename(file, '.ts'));
287
370
 
288
371
  try {
289
- const fileUrl = pathToFileURL(filePath).href;
372
+ // Cache-bust so re-login picks up edited action files
373
+ const fileUrl = pathToFileURL(filePath).href + `?t=${Date.now()}`;
290
374
  const mod = await import(fileUrl);
291
375
 
292
376
  if (typeof mod.default !== 'function') {
@@ -796,6 +880,172 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
796
880
  return successResponse({ message: `Disconnected "${name}" from the game.` });
797
881
  }
798
882
 
883
+ case 'setup_game_wallet': {
884
+ const tempoAddress = args?.tempo_account_address as string;
885
+ if (!tempoAddress || !tempoAddress.startsWith('0x')) {
886
+ return errorResponse('A valid Tempo account address (0x...) is required. Run "tempo wallet whoami" to get yours.');
887
+ }
888
+
889
+ // Check if wallet already exists
890
+ if (hasGameWallet()) {
891
+ try {
892
+ const info = await getWalletInfo();
893
+ return {
894
+ content: [{
895
+ type: 'text',
896
+ text: [
897
+ 'You already have a game wallet set up.',
898
+ '',
899
+ `Account: ${info.address}`,
900
+ `Balance: $${info.balance} USDC`,
901
+ '',
902
+ 'To start fresh, delete ~/.killswitch/access-key.json and run this again.',
903
+ ].join('\n')
904
+ }]
905
+ };
906
+ } catch {
907
+ // Wallet file exists but might be corrupt, continue with setup
908
+ }
909
+ }
910
+
911
+ // Generate access key
912
+ const { accessKeyAddress, privateKey, instructions } = generateAccessKey();
913
+
914
+ // Save it (the player still needs to authorize it on-chain)
915
+ saveAccessKey(privateKey, tempoAddress);
916
+
917
+ return {
918
+ content: [{
919
+ type: 'text',
920
+ text: [
921
+ 'Game wallet setup started!',
922
+ '',
923
+ `Your Tempo account: ${tempoAddress}`,
924
+ `Generated access key: ${accessKeyAddress}`,
925
+ '',
926
+ '── NEXT STEP ──',
927
+ '',
928
+ 'You need to authorize this access key on your Tempo account.',
929
+ 'This requires your passkey (biometric confirmation).',
930
+ '',
931
+ instructions,
932
+ '',
933
+ '── AFTER AUTHORIZATION ──',
934
+ '',
935
+ 'Once authorized, your game wallet is ready. Check it with check_wallet.',
936
+ 'Fund your Tempo account with USDC to start joining paid tournaments.',
937
+ ].join('\n')
938
+ }]
939
+ };
940
+ }
941
+
942
+ case 'check_wallet': {
943
+ if (!hasGameWallet()) {
944
+ return errorResponse(
945
+ 'No game wallet found. Run setup_game_wallet first.\n\n' +
946
+ 'Prerequisites:\n' +
947
+ '1. Install Tempo CLI: curl -fsSL https://tempo.xyz/install | bash\n' +
948
+ '2. Create account: tempo wallet login\n' +
949
+ '3. Get your address: tempo wallet whoami\n' +
950
+ '4. Then call setup_game_wallet with your address'
951
+ );
952
+ }
953
+
954
+ try {
955
+ const info = await getWalletInfo();
956
+ return {
957
+ content: [{
958
+ type: 'text',
959
+ text: [
960
+ '── Game Wallet ──',
961
+ '',
962
+ `Account: ${info.address}`,
963
+ `USDC Balance: $${info.balance}`,
964
+ '',
965
+ info.balanceRaw === 0n
966
+ ? 'Your wallet is empty. Fund it by sending USDC to your account address on the Tempo network.'
967
+ : 'Your wallet is funded and ready for paid tournaments. Use tournament_schedule to see upcoming games.',
968
+ ].join('\n')
969
+ }]
970
+ };
971
+ } catch (e: any) {
972
+ return errorResponse(`Failed to check wallet: ${e.message}`);
973
+ }
974
+ }
975
+
976
+ case 'tournament_schedule': {
977
+ try {
978
+ const slots = await getTournamentSchedule();
979
+
980
+ if (slots.length === 0) {
981
+ return {
982
+ content: [{
983
+ type: 'text',
984
+ text: 'No upcoming paid tournaments scheduled. Check back later, or paid tournaments may not be enabled on this server.'
985
+ }]
986
+ };
987
+ }
988
+
989
+ const lines = ['── Upcoming Paid Tournaments ──', ''];
990
+
991
+ for (let i = 0; i < slots.length; i++) {
992
+ const s = slots[i];
993
+ const time = new Date(s.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
994
+ const buyIn = (parseInt(s.buyIn) / 1_000_000).toFixed(2);
995
+ lines.push(`[${i + 1}] ${time} — $${buyIn} buy-in — ${s.depositorCount}/${s.maxPlayers} players`);
996
+ lines.push(` Contract: ${s.contractAddress}`);
997
+ }
998
+
999
+ lines.push('');
1000
+ lines.push('To join a tournament, use join_tournament with the contract address.');
1001
+ lines.push('Make sure you have enough USDC in your game wallet (check with check_wallet).');
1002
+
1003
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1004
+ } catch (e: any) {
1005
+ return errorResponse(`Failed to get tournament schedule: ${e.message}`);
1006
+ }
1007
+ }
1008
+
1009
+ case 'join_tournament': {
1010
+ const tournamentAddress = args?.tournament_address as string;
1011
+ if (!tournamentAddress || !tournamentAddress.startsWith('0x')) {
1012
+ return errorResponse('A valid tournament contract address (0x...) is required. Use tournament_schedule to find one.');
1013
+ }
1014
+
1015
+ if (!hasGameWallet()) {
1016
+ return errorResponse('No game wallet found. Run setup_game_wallet first.');
1017
+ }
1018
+
1019
+ if (!activeBotName) {
1020
+ return errorResponse('Not logged in. Call login first so we know your in-game username.');
1021
+ }
1022
+
1023
+ try {
1024
+ const result = await joinTournament(tournamentAddress, activeBotName);
1025
+
1026
+ return {
1027
+ content: [{
1028
+ type: 'text',
1029
+ text: [
1030
+ 'Tournament joined!',
1031
+ '',
1032
+ `Buy-in: $${result.amount} USDC`,
1033
+ `Transaction: ${result.txHash}`,
1034
+ `Username: ${activeBotName}`,
1035
+ '',
1036
+ 'Your deposit is held in the tournament escrow contract.',
1037
+ 'Be online when the match starts or you\'ll be eliminated (buy-in stays in the pot).',
1038
+ '',
1039
+ 'If the match is cancelled (< 2 players), you\'ll get a full refund.',
1040
+ 'If the server goes down, you can claim a refund after 1 hour.',
1041
+ ].join('\n')
1042
+ }]
1043
+ };
1044
+ } catch (e: any) {
1045
+ return errorResponse(`Failed to join tournament: ${e.message}`);
1046
+ }
1047
+ }
1048
+
799
1049
  default:
800
1050
  throw new Error(`Unknown tool: ${name}`);
801
1051
  }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Kill Switch Game Wallet
3
+ *
4
+ * Manages a local access key for interacting with tournament contracts.
5
+ * The access key is a scoped sub-key of the player's Tempo passkey account.
6
+ *
7
+ * Flow:
8
+ * 1. Player creates Tempo account via `tempo wallet login`
9
+ * 2. setup_game_wallet generates a local keypair and guides the player
10
+ * through authorizing it as an access key on their Tempo account
11
+ * 3. The access key is stored locally in ~/.killswitch/
12
+ * 4. join_tournament uses the access key to sign approve + deposit txs
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { homedir } from 'os';
18
+ import {
19
+ createPublicClient,
20
+ createWalletClient,
21
+ http,
22
+ parseUnits,
23
+ formatUnits,
24
+ defineChain,
25
+ encodeFunctionData,
26
+ stringToHex,
27
+ pad,
28
+ type Hex,
29
+ } from 'viem';
30
+ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
31
+
32
+ // ── Config ──
33
+
34
+ const WALLET_DIR = join(homedir(), '.killswitch');
35
+ const WALLET_FILE = join(WALLET_DIR, 'access-key.json');
36
+
37
+ // Tempo chain config — defaults to testnet, override via env
38
+ const TEMPO_RPC = process.env.TEMPO_RPC_URL || 'https://rpc.moderato.tempo.xyz';
39
+ const TEMPO_CHAIN_ID = parseInt(process.env.TEMPO_CHAIN_ID || '42431');
40
+
41
+ const tempoChain = defineChain({
42
+ id: TEMPO_CHAIN_ID,
43
+ name: TEMPO_CHAIN_ID === 4217 ? 'Tempo' : 'Tempo Testnet',
44
+ nativeCurrency: { name: 'USD', symbol: 'USD', decimals: 18 },
45
+ rpcUrls: {
46
+ default: { http: [TEMPO_RPC] },
47
+ },
48
+ });
49
+
50
+ // USDC on Tempo testnet (pathUSD)
51
+ const USDC_ADDRESS = (process.env.USDC_ADDRESS || '0x20c0000000000000000000000000000000000000') as Hex;
52
+
53
+ // Factory address — set after deployment
54
+ const FACTORY_ADDRESS = process.env.FACTORY_ADDRESS as Hex | undefined;
55
+
56
+ // Game server URL for API calls
57
+ const SERVER_URL = (() => {
58
+ const idx = process.argv.indexOf('--server');
59
+ if (idx !== -1 && process.argv[idx + 1]) return process.argv[idx + 1];
60
+ return process.env.KILL_SWITCH_SERVER || 'localhost';
61
+ })();
62
+
63
+ function getWebBase(): string {
64
+ const isLocal = SERVER_URL === 'localhost' || SERVER_URL === '127.0.0.1';
65
+ return isLocal ? 'http://localhost:8888' : `https://${SERVER_URL}`;
66
+ }
67
+
68
+ // ── ABIs (minimal, just what we need) ──
69
+
70
+ const ERC20_ABI = [
71
+ {
72
+ name: 'approve',
73
+ type: 'function',
74
+ inputs: [
75
+ { name: 'spender', type: 'address' },
76
+ { name: 'amount', type: 'uint256' },
77
+ ],
78
+ outputs: [{ name: '', type: 'bool' }],
79
+ stateMutability: 'nonpayable',
80
+ },
81
+ {
82
+ name: 'balanceOf',
83
+ type: 'function',
84
+ inputs: [{ name: 'account', type: 'address' }],
85
+ outputs: [{ name: '', type: 'uint256' }],
86
+ stateMutability: 'view',
87
+ },
88
+ {
89
+ name: 'decimals',
90
+ type: 'function',
91
+ inputs: [],
92
+ outputs: [{ name: '', type: 'uint8' }],
93
+ stateMutability: 'view',
94
+ },
95
+ ] as const;
96
+
97
+ const TOURNAMENT_ABI = [
98
+ {
99
+ name: 'deposit',
100
+ type: 'function',
101
+ inputs: [{ name: 'username', type: 'bytes32' }],
102
+ outputs: [],
103
+ stateMutability: 'nonpayable',
104
+ },
105
+ {
106
+ name: 'buyIn',
107
+ type: 'function',
108
+ inputs: [],
109
+ outputs: [{ name: '', type: 'uint256' }],
110
+ stateMutability: 'view',
111
+ },
112
+ {
113
+ name: 'state',
114
+ type: 'function',
115
+ inputs: [],
116
+ outputs: [{ name: '', type: 'uint8' }],
117
+ stateMutability: 'view',
118
+ },
119
+ {
120
+ name: 'getDepositorCount',
121
+ type: 'function',
122
+ inputs: [],
123
+ outputs: [{ name: '', type: 'uint256' }],
124
+ stateMutability: 'view',
125
+ },
126
+ {
127
+ name: 'maxPlayers',
128
+ type: 'function',
129
+ inputs: [],
130
+ outputs: [{ name: '', type: 'uint8' }],
131
+ stateMutability: 'view',
132
+ },
133
+ {
134
+ name: 'startTime',
135
+ type: 'function',
136
+ inputs: [],
137
+ outputs: [{ name: '', type: 'uint256' }],
138
+ stateMutability: 'view',
139
+ },
140
+ {
141
+ name: 'hasDeposited',
142
+ type: 'function',
143
+ inputs: [{ name: '', type: 'address' }],
144
+ outputs: [{ name: '', type: 'bool' }],
145
+ stateMutability: 'view',
146
+ },
147
+ ] as const;
148
+
149
+ // ── Wallet Storage ──
150
+
151
+ interface StoredWallet {
152
+ accessKeyPrivate: string; // hex private key
153
+ accountAddress: string; // the player's Tempo account address
154
+ createdAt: string;
155
+ }
156
+
157
+ function loadWallet(): StoredWallet | null {
158
+ try {
159
+ if (existsSync(WALLET_FILE)) {
160
+ return JSON.parse(readFileSync(WALLET_FILE, 'utf-8'));
161
+ }
162
+ } catch (e) {
163
+ console.error('[Wallet] Failed to load wallet:', e);
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function saveWallet(wallet: StoredWallet): void {
169
+ mkdirSync(WALLET_DIR, { recursive: true });
170
+ writeFileSync(WALLET_FILE, JSON.stringify(wallet, null, 2), { mode: 0o600 });
171
+ }
172
+
173
+ // ── Client Factories ──
174
+
175
+ function getPublicClient() {
176
+ return createPublicClient({
177
+ chain: tempoChain,
178
+ transport: http(),
179
+ });
180
+ }
181
+
182
+ function getWalletClient(privateKey: Hex) {
183
+ const account = privateKeyToAccount(privateKey);
184
+ return createWalletClient({
185
+ account,
186
+ chain: tempoChain,
187
+ transport: http(),
188
+ });
189
+ }
190
+
191
+ // ── Public API (used by MCP tools) ──
192
+
193
+ /**
194
+ * Check if a game wallet exists locally.
195
+ */
196
+ export function hasGameWallet(): boolean {
197
+ return existsSync(WALLET_FILE);
198
+ }
199
+
200
+ /**
201
+ * Get wallet info — address and USDC balance.
202
+ */
203
+ export async function getWalletInfo(): Promise<{
204
+ address: string;
205
+ balance: string;
206
+ balanceRaw: bigint;
207
+ }> {
208
+ const wallet = loadWallet();
209
+ if (!wallet) {
210
+ throw new Error('No game wallet found. Run setup_game_wallet first.');
211
+ }
212
+
213
+ const pub = getPublicClient();
214
+
215
+ const balanceRaw = await pub.readContract({
216
+ address: USDC_ADDRESS,
217
+ abi: ERC20_ABI,
218
+ functionName: 'balanceOf',
219
+ args: [wallet.accountAddress as Hex],
220
+ });
221
+
222
+ const balance = formatUnits(balanceRaw, 6);
223
+
224
+ return {
225
+ address: wallet.accountAddress,
226
+ balance,
227
+ balanceRaw,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Generate a new access key and return setup instructions.
233
+ * Does NOT authorize it — the player must do that with their passkey.
234
+ */
235
+ export function generateAccessKey(): {
236
+ accessKeyAddress: string;
237
+ privateKey: string;
238
+ instructions: string;
239
+ } {
240
+ const privateKey = generatePrivateKey();
241
+ const account = privateKeyToAccount(privateKey);
242
+
243
+ const instructions = [
244
+ 'A new game access key has been generated.',
245
+ '',
246
+ `Access Key Address: ${account.address}`,
247
+ '',
248
+ 'To authorize this key on your Tempo account, you need to call the',
249
+ 'Account Keychain precompile. This requires your passkey (biometric).',
250
+ '',
251
+ 'You can do this via the Tempo wallet web interface or by running:',
252
+ '',
253
+ ` cast send 0xAAAAAAAA00000000000000000000000000000000 \\`,
254
+ ` "authorizeKey(address,uint8,uint64,bool,(address,uint256)[])" \\`,
255
+ ` ${account.address} 0 ${Math.floor(Date.now() / 1000) + 90 * 86400} true \\`,
256
+ ` "[(${USDC_ADDRESS},1000000000)]" \\`,
257
+ ` --rpc-url ${TEMPO_RPC}`,
258
+ '',
259
+ 'This authorizes the key to spend up to 1000 USDC on game transactions,',
260
+ 'expiring in 90 days. You can revoke it anytime from your Tempo account.',
261
+ '',
262
+ 'Learn more about Tempo access keys:',
263
+ 'https://docs.tempo.xyz/protocol/tips/tip-1011',
264
+ ].join('\n');
265
+
266
+ return { accessKeyAddress: account.address, privateKey, instructions };
267
+ }
268
+
269
+ /**
270
+ * Save the access key after the player has authorized it.
271
+ */
272
+ export function saveAccessKey(privateKey: string, accountAddress: string): void {
273
+ saveWallet({
274
+ accessKeyPrivate: privateKey,
275
+ accountAddress,
276
+ createdAt: new Date().toISOString(),
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Get upcoming paid tournament schedule from the game server.
282
+ */
283
+ export async function getTournamentSchedule(): Promise<any[]> {
284
+ const res = await fetch(`${getWebBase()}/api/paid-schedule`);
285
+ const data = await res.json() as any;
286
+
287
+ if (data.error) {
288
+ throw new Error(data.error);
289
+ }
290
+
291
+ return data.slots || [];
292
+ }
293
+
294
+ /**
295
+ * Join a paid tournament by depositing the buy-in.
296
+ * Signs approve() + deposit() using the local access key.
297
+ */
298
+ export async function joinTournament(
299
+ tournamentAddress: string,
300
+ username: string,
301
+ ): Promise<{ txHash: string; amount: string }> {
302
+ const wallet = loadWallet();
303
+ if (!wallet) {
304
+ throw new Error('No game wallet found. Run setup_game_wallet first.');
305
+ }
306
+
307
+ const pub = getPublicClient();
308
+ const walletClient = getWalletClient(wallet.accessKeyPrivate as Hex);
309
+
310
+ // Read buy-in amount from the contract
311
+ const buyIn = await pub.readContract({
312
+ address: tournamentAddress as Hex,
313
+ abi: TOURNAMENT_ABI,
314
+ functionName: 'buyIn',
315
+ });
316
+
317
+ // Check balance
318
+ const balance = await pub.readContract({
319
+ address: USDC_ADDRESS,
320
+ abi: ERC20_ABI,
321
+ functionName: 'balanceOf',
322
+ args: [wallet.accountAddress as Hex],
323
+ });
324
+
325
+ if (balance < buyIn) {
326
+ const needed = formatUnits(buyIn, 6);
327
+ const have = formatUnits(balance, 6);
328
+ throw new Error(`Insufficient balance. Need $${needed} USDC, have $${have}.`);
329
+ }
330
+
331
+ // Check if already deposited
332
+ const alreadyDeposited = await pub.readContract({
333
+ address: tournamentAddress as Hex,
334
+ abi: TOURNAMENT_ABI,
335
+ functionName: 'hasDeposited',
336
+ args: [wallet.accountAddress as Hex],
337
+ });
338
+
339
+ if (alreadyDeposited) {
340
+ throw new Error('You have already deposited in this tournament.');
341
+ }
342
+
343
+ // Step 1: Approve USDC spend
344
+ const approveHash = await walletClient.writeContract({
345
+ address: USDC_ADDRESS,
346
+ abi: ERC20_ABI,
347
+ functionName: 'approve',
348
+ args: [tournamentAddress as Hex, buyIn],
349
+ });
350
+
351
+ await pub.waitForTransactionReceipt({ hash: approveHash });
352
+
353
+ // Step 2: Deposit with username
354
+ const usernameBytes = pad(stringToHex(username), { size: 32 });
355
+
356
+ const depositHash = await walletClient.writeContract({
357
+ address: tournamentAddress as Hex,
358
+ abi: TOURNAMENT_ABI,
359
+ functionName: 'deposit',
360
+ args: [usernameBytes],
361
+ });
362
+
363
+ await pub.waitForTransactionReceipt({ hash: depositHash });
364
+
365
+ return {
366
+ txHash: depositHash,
367
+ amount: formatUnits(buyIn, 6),
368
+ };
369
+ }