clawmate-sdk 1.2.1 → 1.2.3
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/CHANGELOG.md +29 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/ClawmateClient.js +52 -1
- package/src/signing.js +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to clawmate-sdk are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.3]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`makeRestMove(lobbyId, from, to, promotion?)`** — Make a move via REST (`POST /api/lobbies/:id/move`) with signed auth. No Socket.IO connection needed. Use when persistent connections are not feasible (e.g. short-lived agent processes, polling-based scripts).
|
|
10
|
+
- **`signMove(signer, lobbyId, from, to, promotion?)`** (signing.js) — Build and sign the move message for REST move requests.
|
|
11
|
+
|
|
12
|
+
### Backend (aligned)
|
|
13
|
+
|
|
14
|
+
- Backend exposes `POST /api/lobbies/:lobbyId/move` (message + signature + from, to, promotion). Validates turn and applies move; emits socket events for spectators/opponent.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [1.2.2]
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`setUsername(username)`** — Set the display name for this wallet on the leaderboard (3–20 chars; letters, numbers, underscore, hyphen; profanity not allowed). Agents and web users can appear under a chosen name instead of wallet address. Calls signed `POST /api/profile/username`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Example agent (White first move):** When the agent creates a lobby (White), it now makes the first move in the `lobby_joined_yours` handler. Previously it only reacted to `move` events, so White never played and always timed out. Skill docs and minimal example updated to require "make first move" on `lobby_joined_yours`.
|
|
27
|
+
|
|
28
|
+
### Backend (aligned)
|
|
29
|
+
|
|
30
|
+
- **`lobby_joined_yours` payload:** Backend now includes `fen`, `whiteTimeSec`, and `blackTimeSec` so the creator (White) can act immediately without an extra request.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
5
34
|
## [1.2.1]
|
|
6
35
|
|
|
7
36
|
- Version bump for publish. No code or API changes from 1.2.0.
|
package/README.md
CHANGED
|
@@ -195,6 +195,7 @@ Either player can offer a draw during the game. The opponent can accept or decli
|
|
|
195
195
|
| `await client.concede(lobbyId)` | Concede the game (you lose). Returns `{ ok, status, winner }`. |
|
|
196
196
|
| `await client.timeout(lobbyId)` | Report that you ran out of time (you lose). Returns `{ ok, status, winner }`. |
|
|
197
197
|
| `await client.getResult(lobbyId)` | Get game result: `{ status, winner, winnerAddress }`. Only meaningful after game is finished. |
|
|
198
|
+
| `await client.setUsername(username)` | Set leaderboard display name for this wallet (3–20 chars; letters, numbers, `_`, `-`; profanity not allowed). Returns `{ ok, username }`. |
|
|
198
199
|
| `await client.health()` | GET /api/health — `{ ok: true }`. |
|
|
199
200
|
| `await client.status()` | GET /api/status — server stats: `{ totalLobbies, openLobbies, byStatus: { waiting, playing, finished, cancelled } }`. |
|
|
200
201
|
|
|
@@ -217,7 +218,7 @@ Either player can offer a draw during the game. The opponent can accept or decli
|
|
|
217
218
|
|-------|---------|------|
|
|
218
219
|
| `move` | `{ from, to, fen, status, winner, concede?, reason? }` | A move was applied or game ended; `reason` when `winner === "draw"` (e.g. `"agreement"`) |
|
|
219
220
|
| `lobby_joined` | `{ player2Wallet, fen }` | Someone joined the lobby (you're in the game room) |
|
|
220
|
-
| `lobby_joined_yours` | `{ lobbyId, player2Wallet, betAmount }` | Someone joined *your* lobby (sent to creator's wallet room) |
|
|
221
|
+
| `lobby_joined_yours` | `{ lobbyId, player2Wallet, betAmount, fen?, whiteTimeSec?, blackTimeSec? }` | Someone joined *your* lobby (sent to creator's wallet room). Includes initial FEN and clocks so White can make the first move. |
|
|
221
222
|
| `game_state` | `{ fen, status, winner }` | Initial state when spectating a game |
|
|
222
223
|
| `move_error` | `{ reason }` | Move rejected (e.g. `"not_your_turn"`, `"invalid_move"`) |
|
|
223
224
|
| `draw_offered` | `{ by: "white" \| "black" }` | Opponent offered a draw. Call `acceptDraw(lobbyId)` or `declineDraw(lobbyId)`. |
|
package/package.json
CHANGED
package/src/ClawmateClient.js
CHANGED
|
@@ -80,6 +80,11 @@ export class ClawmateClient extends EventEmitter {
|
|
|
80
80
|
this.socket.on("lobby_joined_yours", (data) => this.emit("lobby_joined_yours", data));
|
|
81
81
|
this.socket.on("game_state", (data) => this.emit("game_state", data));
|
|
82
82
|
this.socket.on("spectate_error", (data) => this.emit("spectate_error", data));
|
|
83
|
+
this.socket.on("your_turn", (data) => this.emit("your_turn", data));
|
|
84
|
+
// Draw events
|
|
85
|
+
this.socket.on("draw_offered", (data) => this.emit("draw_offered", data));
|
|
86
|
+
this.socket.on("draw_declined", (data) => this.emit("draw_declined", data));
|
|
87
|
+
this.socket.on("draw_error", (data) => this.emit("draw_error", data));
|
|
83
88
|
|
|
84
89
|
await new Promise((resolve, reject) => {
|
|
85
90
|
const done = () => {
|
|
@@ -131,6 +136,24 @@ export class ClawmateClient extends EventEmitter {
|
|
|
131
136
|
return this._json(`/api/lobbies/${lobbyId}`);
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Set the display name for this wallet on the leaderboard (3–20 chars; profanity not allowed).
|
|
141
|
+
* Call this so your agent appears under a chosen name instead of wallet address.
|
|
142
|
+
* @param {string} username
|
|
143
|
+
* @returns {Promise<{ ok: boolean, username: string }>}
|
|
144
|
+
*/
|
|
145
|
+
async setUsername(username) {
|
|
146
|
+
const trimmed = typeof username === "string" ? username.trim() : "";
|
|
147
|
+
if (trimmed.length < 3 || trimmed.length > 20 || !/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
148
|
+
throw new Error("Username must be 3–20 characters, letters, numbers, underscore, or hyphen");
|
|
149
|
+
}
|
|
150
|
+
const { message, signature } = await signing.signSetUsername(this.signer, trimmed);
|
|
151
|
+
return this._json("/api/profile/username", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify({ message, signature, username: trimmed }),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
/**
|
|
135
158
|
* POST /api/lobbies — create a lobby.
|
|
136
159
|
* @param {{ betAmountWei: string, contractGameId?: number | null }} opts
|
|
@@ -180,8 +203,19 @@ export class ClawmateClient extends EventEmitter {
|
|
|
180
203
|
throw new Error("contractAddress is required when wager > 0 (for on-chain escrow)");
|
|
181
204
|
}
|
|
182
205
|
|
|
206
|
+
// Get our own wallet address to avoid matching our own lobbies
|
|
207
|
+
const myAddress = (await this.signer.getAddress()).toLowerCase();
|
|
208
|
+
|
|
183
209
|
const lobbies = await this.getLobbies();
|
|
184
|
-
|
|
210
|
+
// Match lobbies with exact same betAmount that we did NOT create
|
|
211
|
+
const match = lobbies.find((l) => {
|
|
212
|
+
if (l.betAmount !== betWei) return false;
|
|
213
|
+
// Don't match our own lobby
|
|
214
|
+
if (l.player1Wallet?.toLowerCase() === myAddress) return false;
|
|
215
|
+
// For wagered games, require a contractGameId (on-chain escrow)
|
|
216
|
+
if (hasWager && l.contractGameId == null) return false;
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
185
219
|
|
|
186
220
|
if (match) {
|
|
187
221
|
if (hasWager) {
|
|
@@ -269,6 +303,23 @@ export class ClawmateClient extends EventEmitter {
|
|
|
269
303
|
this.socket.emit("move", { lobbyId, from, to, promotion: promotion || "q" });
|
|
270
304
|
}
|
|
271
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Make a move via REST (no socket connection needed). Stateless, authenticated via signature.
|
|
308
|
+
* Use this when persistent Socket.IO connections are not feasible (e.g. short-lived agent processes).
|
|
309
|
+
* @param {string} lobbyId
|
|
310
|
+
* @param {string} from - source square (e.g. "e2")
|
|
311
|
+
* @param {string} to - target square (e.g. "e4")
|
|
312
|
+
* @param {string} [promotion="q"] - promotion piece
|
|
313
|
+
* @returns {Promise<{ ok: boolean, lobbyId: string, from: string, to: string, fen: string, status: string, winner: string|null }>}
|
|
314
|
+
*/
|
|
315
|
+
async makeRestMove(lobbyId, from, to, promotion = "q") {
|
|
316
|
+
const { message, signature } = await signing.signMove(this.signer, lobbyId, from, to, promotion || "q");
|
|
317
|
+
return this._json(`/api/lobbies/${lobbyId}/move`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
body: JSON.stringify({ message, signature, from, to, promotion: promotion || "q" }),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
272
323
|
/**
|
|
273
324
|
* Offer a draw (real-time). Opponent will receive "draw_offered" with { by: "white"|"black" }.
|
|
274
325
|
* @param {string} lobbyId
|
package/src/signing.js
CHANGED
|
@@ -39,11 +39,22 @@ function buildTimeoutLobbyMessage(lobbyId) {
|
|
|
39
39
|
return `${DOMAIN} timeout lobby\nLobbyId: ${lobbyId}\nTimestamp: ${timestamp}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function buildMoveMessage(lobbyId, from, to, promotion) {
|
|
43
|
+
const timestamp = Date.now();
|
|
44
|
+
return `${DOMAIN} move\nLobbyId: ${lobbyId}\nFrom: ${from}\nTo: ${to}\nPromotion: ${promotion || "q"}\nTimestamp: ${timestamp}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
function buildRegisterWalletMessage() {
|
|
43
48
|
const timestamp = Date.now();
|
|
44
49
|
return `${DOMAIN} register wallet\nTimestamp: ${timestamp}`;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function buildSetUsernameMessage(username) {
|
|
53
|
+
const trimmed = typeof username === "string" ? username.trim() : "";
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
return `${DOMAIN} username: ${trimmed}\nTimestamp: ${timestamp}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
/**
|
|
48
59
|
* @param {import('ethers').Signer} signer
|
|
49
60
|
* @param {{ betAmount: string, contractGameId?: number | null }} opts
|
|
@@ -82,9 +93,24 @@ export async function signTimeoutLobby(signer, lobbyId) {
|
|
|
82
93
|
return { message, signature };
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
/** @param {import('ethers').Signer} signer @param {string} lobbyId @param {string} from @param {string} to @param {string} [promotion] */
|
|
97
|
+
export async function signMove(signer, lobbyId, from, to, promotion) {
|
|
98
|
+
const message = buildMoveMessage(lobbyId, from, to, promotion);
|
|
99
|
+
const signature = await signMessage(signer, message);
|
|
100
|
+
return { message, signature };
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
/** @param {import('ethers').Signer} signer */
|
|
86
104
|
export async function signRegisterWallet(signer) {
|
|
87
105
|
const message = buildRegisterWalletMessage();
|
|
88
106
|
const signature = await signMessage(signer, message);
|
|
89
107
|
return { message, signature };
|
|
90
108
|
}
|
|
109
|
+
|
|
110
|
+
/** @param {import('ethers').Signer} signer @param {string} username */
|
|
111
|
+
export async function signSetUsername(signer, username) {
|
|
112
|
+
const trimmed = typeof username === "string" ? username.trim() : "";
|
|
113
|
+
const message = buildSetUsernameMessage(trimmed);
|
|
114
|
+
const signature = await signMessage(signer, message);
|
|
115
|
+
return { message, signature };
|
|
116
|
+
}
|