clarityxo-sdk 0.1.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/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # ClarityXO SDK
2
+
3
+ A TypeScript SDK and CLI for interacting with the ClarityXO decentralized Tic-Tac-Toe game on the Stacks blockchain.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install clarityxo-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createClient } from 'clarityxo-sdk';
15
+
16
+ const client = createClient({
17
+ network: 'testnet',
18
+ contractAddress: 'ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1',
19
+ });
20
+
21
+ const gameState = await client.getFullGameState();
22
+ console.log(gameState.board);
23
+ ```
24
+
25
+ ## API Reference
26
+
27
+ ### ClarityXOClient
28
+
29
+ The main client class for interacting with ClarityXO.
30
+
31
+ #### Constructor
32
+
33
+ ```typescript
34
+ new ClarityXOClient(config: ClarityXOConfig)
35
+ ```
36
+
37
+ #### Game State Methods
38
+
39
+ - `getBoardState(): Promise<Board>` - Get the current board state
40
+ - `getGameStatus(): Promise<GameStatus>` - Get the game status ('active', 'finished', 'not-started')
41
+ - `getWinner(): Promise<'player' | 'ai' | 'draw' | null>` - Get the winner
42
+ - `getCurrentTurn(): Promise<Turn>` - Get whose turn it is ('player' or 'ai')
43
+ - `isValidMove(row: 0|1|2, col: 0|1|2): Promise<boolean>` - Check if a move is valid
44
+ - `getFullGameState(): Promise<GameState>` - Get all game state at once
45
+
46
+ #### Transaction Methods
47
+
48
+ These require `senderKey` and `senderAddress` in config.
49
+
50
+ - `startNewGame(): Promise<{ txId: string }>` - Start a new game
51
+ - `makeMove(row: 0|1|2, col: 0|1|2): Promise<{ txId: string }>` - Make a move
52
+ - `resignGame(): Promise<{ txId: string }>` - Resign the current game
53
+
54
+ #### Leaderboard Methods
55
+
56
+ - `getLeaderboard(month: string): Promise<LeaderboardMonth>` - Get leaderboard for a month
57
+ - `submitResult(result: GameResult): Promise<void>` - Submit a game result
58
+ - `syncLeaderboard(): Promise<void>` - Sync leaderboard data
59
+ - `healthCheck(): Promise<boolean>` - Check if leaderboard API is healthy
60
+
61
+ ### Types
62
+
63
+ - `Network`: 'mainnet' | 'testnet'
64
+ - `Board`: 3x3 array of CellValue
65
+ - `CellValue`: 'X' | 'O' | null
66
+ - `GameStatus`: 'active' | 'finished' | 'not-started'
67
+ - `Turn`: 'player' | 'ai'
68
+ - `GameState`: { board, status, winner, currentTurn }
69
+ - `LeaderboardEntry`: { player, wins, losses, draws, points, rank }
70
+ - `LeaderboardMonth`: { month, entries }
71
+ - `GameResult`: { player, outcome, month }
72
+
73
+ ## CLI
74
+
75
+ The package includes a CLI tool called `clarityxo`.
76
+
77
+ ### Global Options
78
+
79
+ - `--contract <address>`: Contract address (required for most commands)
80
+ - `--network <mainnet|testnet>`: Network (default: testnet)
81
+ - `--api <url>`: Leaderboard API URL
82
+
83
+ ### Commands
84
+
85
+ #### `board`
86
+
87
+ Display the current game board.
88
+
89
+ ```bash
90
+ clarityxo board --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1
91
+ ```
92
+
93
+ Example output:
94
+
95
+ ```
96
+ ClarityXO — Testnet
97
+ Contract: ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1
98
+
99
+ X │ · │ O
100
+ ───┼───┼───
101
+ · │ X │ ·
102
+ ───┼───┼───
103
+ O │ · │ X
104
+
105
+ Turn: AI Status: active Winner: —
106
+ ```
107
+
108
+ #### `status`
109
+
110
+ Display game status and current turn.
111
+
112
+ ```bash
113
+ clarityxo status --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1
114
+ ```
115
+
116
+ #### `winner`
117
+
118
+ Display the winner.
119
+
120
+ ```bash
121
+ clarityxo winner --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1
122
+ ```
123
+
124
+ #### `leaderboard`
125
+
126
+ Display the leaderboard for a month.
127
+
128
+ ```bash
129
+ clarityxo leaderboard --month 2023-10 --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1
130
+ ```
131
+
132
+ ## Network Configuration
133
+
134
+ - **Mainnet**: Use 'mainnet' network, mainnet contract addresses
135
+ - **Testnet**: Use 'testnet' network, testnet contract addresses
136
+
137
+ Default leaderboard API is `https://clarityxo.onrender.com`, but can be overridden.
138
+
139
+ ## Error Handling
140
+
141
+ All methods throw descriptive errors. Handle them appropriately:
142
+
143
+ ```typescript
144
+ try {
145
+ const tx = await client.startNewGame();
146
+ console.log('Transaction ID:', tx.txId);
147
+ } catch (error) {
148
+ console.error('Failed to start game:', error.message);
149
+ }
150
+ ```
151
+
152
+ ## Contributing
153
+
154
+ 1. Fork the repository
155
+ 2. Create a feature branch
156
+ 3. Make your changes
157
+ 4. Run tests: `npm test`
158
+ 5. Lint: `npm run lint`
159
+ 6. Format: `npm run format`
160
+ 7. Submit a PR
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,318 @@
1
+ // src/constants.ts
2
+ var CONTRACT_NAME = "tictactoe";
3
+ var MAINNET_API = "https://api.mainnet.hiro.so";
4
+ var TESTNET_API = "https://api.testnet.hiro.so";
5
+ var DEFAULT_LEADERBOARD_API = "https://clarityxo.onrender.com";
6
+ var CONTRACT_FUNCTIONS = {
7
+ START_NEW_GAME: "start-new-game",
8
+ MAKE_MOVE: "make-move",
9
+ RESIGN_GAME: "resign-game",
10
+ GET_BOARD_STATE: "get-board-state",
11
+ GET_GAME_STATUS: "get-game-status",
12
+ GET_WINNER: "get-winner",
13
+ GET_CURRENT_TURN: "get-current-turn",
14
+ IS_VALID_MOVE: "is-valid-move"
15
+ };
16
+
17
+ // src/contract/read.ts
18
+ import { callReadOnlyFunction, uintCV } from "@stacks/transactions";
19
+
20
+ // src/utils/network.ts
21
+ import { StacksMainnet, StacksTestnet } from "@stacks/network";
22
+ function getStacksNetwork(network) {
23
+ return network === "mainnet" ? new StacksMainnet() : new StacksTestnet();
24
+ }
25
+
26
+ // src/utils/cv.ts
27
+ function parseBoardCV(cv) {
28
+ const rows = cv;
29
+ const board = [
30
+ [null, null, null],
31
+ [null, null, null],
32
+ [null, null, null]
33
+ ];
34
+ for (let i = 0; i < 3; i++) {
35
+ const row = rows.list[i].list;
36
+ for (let j = 0; j < 3; j++) {
37
+ const cell = row[j];
38
+ if (cell.type === "some") {
39
+ board[i][j] = cell.value.value;
40
+ } else {
41
+ board[i][j] = null;
42
+ }
43
+ }
44
+ }
45
+ return board;
46
+ }
47
+ function parseGameStatusCV(cv) {
48
+ const value = cv.value;
49
+ if (value === "active") return "active";
50
+ if (value === "finished") return "finished";
51
+ if (value === "not-started") return "not-started";
52
+ throw new Error(`Invalid game status: ${value}`);
53
+ }
54
+ function parseWinnerCV(cv) {
55
+ const value = cv.value;
56
+ if (value === "player") return "player";
57
+ if (value === "ai") return "ai";
58
+ if (value === "draw") return "draw";
59
+ return null;
60
+ }
61
+ function parseTurnCV(cv) {
62
+ const value = cv.value;
63
+ if (value === "player") return "player";
64
+ if (value === "ai") return "ai";
65
+ throw new Error(`Invalid turn: ${value}`);
66
+ }
67
+
68
+ // src/contract/read.ts
69
+ async function getBoardState(config) {
70
+ const network = getStacksNetwork(config.network);
71
+ const contractName = config.contractName || CONTRACT_NAME;
72
+ const cv = await callReadOnlyFunction({
73
+ network,
74
+ contractAddress: config.contractAddress,
75
+ contractName,
76
+ functionName: CONTRACT_FUNCTIONS.GET_BOARD_STATE,
77
+ functionArgs: [],
78
+ senderAddress: config.contractAddress
79
+ // arbitrary
80
+ });
81
+ return parseBoardCV(cv);
82
+ }
83
+ async function getGameStatus(config) {
84
+ const network = getStacksNetwork(config.network);
85
+ const contractName = config.contractName || CONTRACT_NAME;
86
+ const cv = await callReadOnlyFunction({
87
+ network,
88
+ contractAddress: config.contractAddress,
89
+ contractName,
90
+ functionName: CONTRACT_FUNCTIONS.GET_GAME_STATUS,
91
+ functionArgs: [],
92
+ senderAddress: config.contractAddress
93
+ });
94
+ return parseGameStatusCV(cv);
95
+ }
96
+ async function getWinner(config) {
97
+ const network = getStacksNetwork(config.network);
98
+ const contractName = config.contractName || CONTRACT_NAME;
99
+ const cv = await callReadOnlyFunction({
100
+ network,
101
+ contractAddress: config.contractAddress,
102
+ contractName,
103
+ functionName: CONTRACT_FUNCTIONS.GET_WINNER,
104
+ functionArgs: [],
105
+ senderAddress: config.contractAddress
106
+ });
107
+ return parseWinnerCV(cv);
108
+ }
109
+ async function getCurrentTurn(config) {
110
+ const network = getStacksNetwork(config.network);
111
+ const contractName = config.contractName || CONTRACT_NAME;
112
+ const cv = await callReadOnlyFunction({
113
+ network,
114
+ contractAddress: config.contractAddress,
115
+ contractName,
116
+ functionName: CONTRACT_FUNCTIONS.GET_CURRENT_TURN,
117
+ functionArgs: [],
118
+ senderAddress: config.contractAddress
119
+ });
120
+ return parseTurnCV(cv);
121
+ }
122
+ async function isValidMove(config, row, col) {
123
+ const network = getStacksNetwork(config.network);
124
+ const contractName = config.contractName || CONTRACT_NAME;
125
+ const cv = await callReadOnlyFunction({
126
+ network,
127
+ contractAddress: config.contractAddress,
128
+ contractName,
129
+ functionName: CONTRACT_FUNCTIONS.IS_VALID_MOVE,
130
+ functionArgs: [uintCV(row), uintCV(col)],
131
+ senderAddress: config.contractAddress
132
+ });
133
+ return cv.value;
134
+ }
135
+ async function getFullGameState(config) {
136
+ const [board, status, winner, currentTurn] = await Promise.all([
137
+ getBoardState(config),
138
+ getGameStatus(config),
139
+ getWinner(config),
140
+ getCurrentTurn(config)
141
+ ]);
142
+ return { board, status, winner, currentTurn };
143
+ }
144
+
145
+ // src/contract/write.ts
146
+ import { makeContractCall, broadcastTransaction, uintCV as uintCV2 } from "@stacks/transactions";
147
+ async function startNewGame(config) {
148
+ if (!config.senderKey || !config.senderAddress) {
149
+ throw new Error("senderKey and senderAddress are required for write operations");
150
+ }
151
+ const network = getStacksNetwork(config.network);
152
+ const contractName = config.contractName || CONTRACT_NAME;
153
+ const tx = await makeContractCall({
154
+ network,
155
+ contractAddress: config.contractAddress,
156
+ contractName,
157
+ functionName: CONTRACT_FUNCTIONS.START_NEW_GAME,
158
+ functionArgs: [],
159
+ senderKey: config.senderKey,
160
+ anchorMode: "any"
161
+ });
162
+ const broadcastResponse = await broadcastTransaction(tx, network);
163
+ return { txId: broadcastResponse.txid };
164
+ }
165
+ async function makeMove(config, row, col) {
166
+ if (!config.senderKey || !config.senderAddress) {
167
+ throw new Error("senderKey and senderAddress are required for write operations");
168
+ }
169
+ const network = getStacksNetwork(config.network);
170
+ const contractName = config.contractName || CONTRACT_NAME;
171
+ const tx = await makeContractCall({
172
+ network,
173
+ contractAddress: config.contractAddress,
174
+ contractName,
175
+ functionName: CONTRACT_FUNCTIONS.MAKE_MOVE,
176
+ functionArgs: [uintCV2(row), uintCV2(col)],
177
+ senderKey: config.senderKey,
178
+ anchorMode: "any"
179
+ });
180
+ const broadcastResponse = await broadcastTransaction(tx, network);
181
+ return { txId: broadcastResponse.txid };
182
+ }
183
+ async function resignGame(config) {
184
+ if (!config.senderKey || !config.senderAddress) {
185
+ throw new Error("senderKey and senderAddress are required for write operations");
186
+ }
187
+ const network = getStacksNetwork(config.network);
188
+ const contractName = config.contractName || CONTRACT_NAME;
189
+ const tx = await makeContractCall({
190
+ network,
191
+ contractAddress: config.contractAddress,
192
+ contractName,
193
+ functionName: CONTRACT_FUNCTIONS.RESIGN_GAME,
194
+ functionArgs: [],
195
+ senderKey: config.senderKey,
196
+ anchorMode: "any"
197
+ });
198
+ const broadcastResponse = await broadcastTransaction(tx, network);
199
+ return { txId: broadcastResponse.txid };
200
+ }
201
+
202
+ // src/leaderboard/api.ts
203
+ function getBaseUrl(config) {
204
+ return config.leaderboardApiUrl || DEFAULT_LEADERBOARD_API;
205
+ }
206
+ async function getLeaderboard(config, month) {
207
+ const baseUrl = getBaseUrl(config);
208
+ const response = await fetch(`${baseUrl}/api/leaderboard?month=${month}`);
209
+ if (!response.ok) {
210
+ throw new Error(`Failed to fetch leaderboard: ${response.statusText}`);
211
+ }
212
+ return response.json();
213
+ }
214
+ async function submitResult(config, result) {
215
+ const baseUrl = getBaseUrl(config);
216
+ const response = await fetch(`${baseUrl}/api/leaderboard/result`, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json" },
219
+ body: JSON.stringify(result)
220
+ });
221
+ if (!response.ok) {
222
+ throw new Error(`Failed to submit result: ${response.statusText}`);
223
+ }
224
+ }
225
+ async function syncLeaderboard(config) {
226
+ const baseUrl = getBaseUrl(config);
227
+ const response = await fetch(`${baseUrl}/api/sync`, {
228
+ method: "POST"
229
+ });
230
+ if (!response.ok) {
231
+ throw new Error(`Failed to sync leaderboard: ${response.statusText}`);
232
+ }
233
+ }
234
+ async function healthCheck(config) {
235
+ const baseUrl = getBaseUrl(config);
236
+ try {
237
+ const response = await fetch(`${baseUrl}/health`);
238
+ return response.ok;
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
244
+ // src/client.ts
245
+ var ClarityXOClient = class {
246
+ constructor(config) {
247
+ this.config = config;
248
+ }
249
+ config;
250
+ // Game state
251
+ getBoardState() {
252
+ return getBoardState(this.config);
253
+ }
254
+ getGameStatus() {
255
+ return getGameStatus(this.config);
256
+ }
257
+ getWinner() {
258
+ return getWinner(this.config);
259
+ }
260
+ getCurrentTurn() {
261
+ return getCurrentTurn(this.config);
262
+ }
263
+ isValidMove(row, col) {
264
+ return isValidMove(this.config, row, col);
265
+ }
266
+ getFullGameState() {
267
+ return getFullGameState(this.config);
268
+ }
269
+ // Transactions
270
+ startNewGame() {
271
+ return startNewGame(this.config);
272
+ }
273
+ makeMove(row, col) {
274
+ return makeMove(this.config, row, col);
275
+ }
276
+ resignGame() {
277
+ return resignGame(this.config);
278
+ }
279
+ // Leaderboard
280
+ getLeaderboard(month) {
281
+ return getLeaderboard(this.config, month);
282
+ }
283
+ submitResult(result) {
284
+ return submitResult(this.config, result);
285
+ }
286
+ syncLeaderboard() {
287
+ return syncLeaderboard(this.config);
288
+ }
289
+ healthCheck() {
290
+ return healthCheck(this.config);
291
+ }
292
+ };
293
+ function createClient(config) {
294
+ return new ClarityXOClient(config);
295
+ }
296
+
297
+ export {
298
+ CONTRACT_NAME,
299
+ MAINNET_API,
300
+ TESTNET_API,
301
+ DEFAULT_LEADERBOARD_API,
302
+ CONTRACT_FUNCTIONS,
303
+ getBoardState,
304
+ getGameStatus,
305
+ getWinner,
306
+ getCurrentTurn,
307
+ isValidMove,
308
+ getFullGameState,
309
+ startNewGame,
310
+ makeMove,
311
+ resignGame,
312
+ getLeaderboard,
313
+ submitResult,
314
+ syncLeaderboard,
315
+ healthCheck,
316
+ ClarityXOClient,
317
+ createClient
318
+ };