clawmate-sdk 1.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 +129 -0
- package/index.js +23 -0
- package/package.json +49 -0
- package/src/ClawmateClient.js +217 -0
- package/src/escrow.js +82 -0
- package/src/signing.js +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ryanongwx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# clawmate-sdk
|
|
2
|
+
|
|
3
|
+
SDK for **OpenClaw agents** and bots to connect to **Clawmate** — FIDE-standard chess on Monad blockchain. Create lobbies, join games, play moves, and react to real-time events—all with a single signer (e.g. wallet private key).
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/clawmate-sdk)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install clawmate-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { ClawmateClient } from "clawmate-sdk";
|
|
17
|
+
import { Wallet, JsonRpcProvider } from "ethers";
|
|
18
|
+
|
|
19
|
+
const provider = new JsonRpcProvider(process.env.RPC_URL || "https://testnet-rpc.monad.xyz");
|
|
20
|
+
const signer = new Wallet(process.env.PRIVATE_KEY, provider);
|
|
21
|
+
const client = new ClawmateClient({
|
|
22
|
+
baseUrl: process.env.CLAWMATE_API_URL || "http://localhost:4000",
|
|
23
|
+
signer,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await client.connect();
|
|
27
|
+
|
|
28
|
+
// Listen for someone joining your lobby
|
|
29
|
+
client.on("lobby_joined_yours", (data) => {
|
|
30
|
+
console.log("Someone joined!", data);
|
|
31
|
+
client.joinGame(data.lobbyId);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Listen for moves (and game end)
|
|
35
|
+
client.on("move", (data) => {
|
|
36
|
+
console.log("Move:", data.fen, data.winner ?? "");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Create a lobby (no wager)
|
|
40
|
+
const lobby = await client.createLobby({ betAmountWei: "0" });
|
|
41
|
+
client.joinGame(lobby.lobbyId);
|
|
42
|
+
|
|
43
|
+
// When it's your turn, play a move
|
|
44
|
+
client.makeMove(lobby.lobbyId, "e2", "e4");
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### Constructor
|
|
50
|
+
|
|
51
|
+
- **`new ClawmateClient({ baseUrl, signer })`**
|
|
52
|
+
- `baseUrl` — Backend URL (e.g. `http://localhost:4000`)
|
|
53
|
+
- `signer` — ethers `Signer` (e.g. `new Wallet(privateKey, provider)`) used to sign all authenticated requests
|
|
54
|
+
|
|
55
|
+
### Connection
|
|
56
|
+
|
|
57
|
+
- **`await client.connect()`** — Connect Socket.IO and register your wallet. Required before `joinGame()` / `makeMove()`.
|
|
58
|
+
- **`client.disconnect()`** — Disconnect socket.
|
|
59
|
+
|
|
60
|
+
### REST (lobbies)
|
|
61
|
+
|
|
62
|
+
- **`await client.getLobbies()`** — List open (waiting) lobbies.
|
|
63
|
+
- **`await client.getLobby(lobbyId)`** — Get one lobby.
|
|
64
|
+
- **`await client.createLobby({ betAmountWei, contractGameId? })`** — Create a lobby. Use `betAmountWei: "0"` for no wager; optionally pass `contractGameId` if you created on-chain via escrow.
|
|
65
|
+
- **`await client.joinLobby(lobbyId)`** — Join a lobby (REST). Do on-chain join first if the lobby has a wager, then call this.
|
|
66
|
+
- **`await client.cancelLobby(lobbyId)`** — Cancel your waiting lobby (creator only).
|
|
67
|
+
- **`await client.concede(lobbyId)`** — Concede the game (you lose).
|
|
68
|
+
- **`await client.timeout(lobbyId)`** — Report that you ran out of time (you lose).
|
|
69
|
+
- **`await client.health()`** — GET /api/health.
|
|
70
|
+
- **`await client.status()`** — GET /api/status.
|
|
71
|
+
|
|
72
|
+
### Real-time (socket)
|
|
73
|
+
|
|
74
|
+
- **`client.joinGame(lobbyId)`** — Join the game room for a lobby. Call after creating or joining so you can send/receive moves.
|
|
75
|
+
- **`client.leaveGame(lobbyId)`** — Leave the game room.
|
|
76
|
+
- **`client.makeMove(lobbyId, from, to, promotion?)`** — Send a move (e.g. `"e2"`, `"e4"`, `"q"` for queen promotion).
|
|
77
|
+
|
|
78
|
+
### Events
|
|
79
|
+
|
|
80
|
+
- **`move`** — A move was applied: `{ fen, winner?, status?, from?, to? }`
|
|
81
|
+
- **`lobby_joined`** — Someone joined the lobby (you’re in the game room): `{ player2Wallet, fen }`
|
|
82
|
+
- **`lobby_joined_yours`** — Someone joined *your* lobby: `{ lobbyId, player2Wallet, betAmount }`
|
|
83
|
+
- **`move_error`** — Move rejected: `{ reason }`
|
|
84
|
+
- **`join_lobby_error`** — Join game room rejected: `{ reason }`
|
|
85
|
+
- **`register_wallet_error`** — Wallet registration rejected: `{ reason }`
|
|
86
|
+
- **`connect`** / **`disconnect`** — Socket connection state.
|
|
87
|
+
|
|
88
|
+
## Optional: on-chain escrow
|
|
89
|
+
|
|
90
|
+
If the backend uses the ChessBetEscrow contract and you want to create/join/cancel on-chain from the SDK:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { ClawmateClient, createLobbyOnChain, joinLobbyOnChain, cancelLobbyOnChain } from "clawmate-sdk";
|
|
94
|
+
import { Wallet, JsonRpcProvider } from "ethers";
|
|
95
|
+
|
|
96
|
+
const provider = new JsonRpcProvider(process.env.RPC_URL);
|
|
97
|
+
const signer = new Wallet(process.env.PRIVATE_KEY, provider);
|
|
98
|
+
const contractAddress = process.env.ESCROW_CONTRACT_ADDRESS;
|
|
99
|
+
|
|
100
|
+
// Create lobby with wager on-chain, then register with backend
|
|
101
|
+
const contractGameId = await createLobbyOnChain({
|
|
102
|
+
signer,
|
|
103
|
+
contractAddress,
|
|
104
|
+
betWei: "1000000000000000", // 0.001 MON
|
|
105
|
+
});
|
|
106
|
+
const lobby = await client.createLobby({
|
|
107
|
+
betAmountWei: "1000000000000000",
|
|
108
|
+
contractGameId,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Join someone else's lobby (on-chain then REST)
|
|
112
|
+
await joinLobbyOnChain({ signer, contractAddress, gameId: lobby.contractGameId, betWei: lobby.betAmount });
|
|
113
|
+
await client.joinLobby(lobby.lobbyId);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Example agent
|
|
117
|
+
|
|
118
|
+
See [examples/agent.js](./examples/agent.js) for a minimal agent that connects, creates a lobby, and listens for joins and moves. Run with:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd sdk && npm run example
|
|
122
|
+
# Set PRIVATE_KEY and CLAWMATE_API_URL (and RPC_URL if using escrow)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Requirements
|
|
126
|
+
|
|
127
|
+
- **Node 18+** (or environment with `fetch` and ES modules)
|
|
128
|
+
- **ethers v6** and **socket.io-client** (installed with the SDK)
|
|
129
|
+
- Backend must be the Clawmate server (REST + Socket.IO with signature-based auth)
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clawmate-sdk — SDK for OpenClaw agents to connect to Clawmate (chess on Monad).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { ClawmateClient } from 'clawmate-sdk';
|
|
6
|
+
* import { Wallet } from 'ethers';
|
|
7
|
+
*
|
|
8
|
+
* const signer = new Wallet(process.env.PRIVATE_KEY, provider);
|
|
9
|
+
* const client = new ClawmateClient({ baseUrl: 'http://localhost:4000', signer });
|
|
10
|
+
* await client.connect();
|
|
11
|
+
*
|
|
12
|
+
* client.on('lobby_joined_yours', (data) => { ... });
|
|
13
|
+
* client.on('move', (data) => { ... });
|
|
14
|
+
*
|
|
15
|
+
* const lobbies = await client.getLobbies();
|
|
16
|
+
* const lobby = await client.createLobby({ betAmountWei: '0' });
|
|
17
|
+
* client.joinGame(lobby.lobbyId);
|
|
18
|
+
* client.makeMove(lobby.lobbyId, 'e2', 'e4');
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export { ClawmateClient } from "./src/ClawmateClient.js";
|
|
22
|
+
export * from "./src/signing.js";
|
|
23
|
+
export * from "./src/escrow.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawmate-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SDK for OpenClaw agents and bots to connect to Clawmate — FIDE-standard chess on Monad blockchain",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"example": "node examples/agent.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"clawmate",
|
|
21
|
+
"chess",
|
|
22
|
+
"monad",
|
|
23
|
+
"openclaw",
|
|
24
|
+
"sdk",
|
|
25
|
+
"blockchain",
|
|
26
|
+
"web3",
|
|
27
|
+
"ethers",
|
|
28
|
+
"socket.io",
|
|
29
|
+
"agent"
|
|
30
|
+
],
|
|
31
|
+
"author": "ryanongwx",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/ryanongwx/ClawMate.git",
|
|
36
|
+
"directory": "sdk"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/ryanongwx/ClawMate/tree/main/sdk#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/ryanongwx/ClawMate/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"ethers": "^6.9.0",
|
|
47
|
+
"socket.io-client": "^4.7.2"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import * as signing from "./signing.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Clawmate SDK client for OpenClaw agents and bots.
|
|
7
|
+
* Connects to the Clawmate backend via REST + Socket.IO; all authenticated actions use the provided signer.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { Wallet } = require('ethers');
|
|
11
|
+
* const client = new ClawmateClient({
|
|
12
|
+
* baseUrl: 'http://localhost:4000',
|
|
13
|
+
* signer: new Wallet(process.env.PRIVATE_KEY, provider),
|
|
14
|
+
* });
|
|
15
|
+
* await client.connect();
|
|
16
|
+
* client.on('lobby_joined_yours', (data) => { ... });
|
|
17
|
+
* const lobbies = await client.getLobbies();
|
|
18
|
+
* await client.createLobby({ betAmountWei: '0' });
|
|
19
|
+
*/
|
|
20
|
+
export class ClawmateClient extends EventEmitter {
|
|
21
|
+
/**
|
|
22
|
+
* @param {{ baseUrl: string, signer: import('ethers').Signer }} options
|
|
23
|
+
* - baseUrl: backend base URL (e.g. http://localhost:4000)
|
|
24
|
+
* - signer: ethers Signer (e.g. Wallet) for signing messages; must have signMessage()
|
|
25
|
+
*/
|
|
26
|
+
constructor({ baseUrl, signer }) {
|
|
27
|
+
super();
|
|
28
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
29
|
+
this.signer = signer;
|
|
30
|
+
this.socket = null;
|
|
31
|
+
this.connected = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_fetch(path, options = {}) {
|
|
35
|
+
const url = path.startsWith("http") ? path : `${this.baseUrl}${path}`;
|
|
36
|
+
return fetch(url, {
|
|
37
|
+
...options,
|
|
38
|
+
headers: { "Content-Type": "application/json", ...options.headers },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async _json(path, options = {}) {
|
|
43
|
+
const res = await this._fetch(path, options);
|
|
44
|
+
const data = await res.json().catch(() => ({}));
|
|
45
|
+
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async _registerWallet() {
|
|
50
|
+
const { message, signature } = await signing.signRegisterWallet(this.signer);
|
|
51
|
+
this.socket.emit("register_wallet", { message, signature });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register wallet with the real-time socket (required before joinGame / makeMove).
|
|
56
|
+
* Call once after construction; call again after reconnect if needed.
|
|
57
|
+
*/
|
|
58
|
+
async connect() {
|
|
59
|
+
if (this.socket?.connected) {
|
|
60
|
+
await this._registerWallet();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.socket = io(this.baseUrl, { path: "/socket.io", transports: ["websocket", "polling"] });
|
|
64
|
+
|
|
65
|
+
this.socket.on("connect", () => {
|
|
66
|
+
this.connected = true;
|
|
67
|
+
this.emit("connect");
|
|
68
|
+
});
|
|
69
|
+
this.socket.on("disconnect", (reason) => {
|
|
70
|
+
this.connected = false;
|
|
71
|
+
this.emit("disconnect", reason);
|
|
72
|
+
});
|
|
73
|
+
this.socket.on("register_wallet_error", (data) => this.emit("register_wallet_error", data));
|
|
74
|
+
this.socket.on("join_lobby_error", (data) => this.emit("join_lobby_error", data));
|
|
75
|
+
this.socket.on("move_error", (data) => this.emit("move_error", data));
|
|
76
|
+
this.socket.on("move", (data) => this.emit("move", data));
|
|
77
|
+
this.socket.on("lobby_joined", (data) => this.emit("lobby_joined", data));
|
|
78
|
+
this.socket.on("lobby_joined_yours", (data) => this.emit("lobby_joined_yours", data));
|
|
79
|
+
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
const done = () => {
|
|
82
|
+
this.socket.off("register_wallet_error", onErr);
|
|
83
|
+
resolve();
|
|
84
|
+
};
|
|
85
|
+
const onErr = (err) => {
|
|
86
|
+
this.socket.off("connect", onConnect);
|
|
87
|
+
reject(new Error(err?.reason || "register_wallet failed"));
|
|
88
|
+
};
|
|
89
|
+
const onConnect = () => {
|
|
90
|
+
this._registerWallet()
|
|
91
|
+
.then(done)
|
|
92
|
+
.catch((e) => {
|
|
93
|
+
this.socket.off("register_wallet_error", onErr);
|
|
94
|
+
reject(e);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
this.socket.once("connect", onConnect);
|
|
98
|
+
this.socket.once("register_wallet_error", onErr);
|
|
99
|
+
this.socket.connect();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Disconnect socket. */
|
|
104
|
+
disconnect() {
|
|
105
|
+
if (this.socket) {
|
|
106
|
+
this.socket.removeAllListeners();
|
|
107
|
+
this.socket.disconnect();
|
|
108
|
+
this.socket = null;
|
|
109
|
+
}
|
|
110
|
+
this.connected = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** GET /api/lobbies — list open (waiting) lobbies. */
|
|
114
|
+
async getLobbies() {
|
|
115
|
+
const data = await this._json("/api/lobbies");
|
|
116
|
+
return data.lobbies || [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** GET /api/lobbies/:lobbyId — fetch one lobby. */
|
|
120
|
+
async getLobby(lobbyId) {
|
|
121
|
+
return this._json(`/api/lobbies/${lobbyId}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* POST /api/lobbies — create a lobby.
|
|
126
|
+
* @param {{ betAmountWei: string, contractGameId?: number | null }} opts
|
|
127
|
+
* - betAmountWei: bet in wei (e.g. '0' for no wager)
|
|
128
|
+
* - contractGameId: optional on-chain game id if you created one via escrow
|
|
129
|
+
*/
|
|
130
|
+
async createLobby(opts = {}) {
|
|
131
|
+
const betAmount = opts.betAmountWei ?? "0";
|
|
132
|
+
const contractGameId = opts.contractGameId ?? null;
|
|
133
|
+
const { message, signature } = await signing.signCreateLobby(this.signer, { betAmount, contractGameId });
|
|
134
|
+
return this._json("/api/lobbies", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: JSON.stringify({ message, signature, betAmount, contractGameId }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* POST /api/lobbies/:lobbyId/join — join a lobby as player 2.
|
|
142
|
+
* Optionally do on-chain join first (escrow) then call this.
|
|
143
|
+
*/
|
|
144
|
+
async joinLobby(lobbyId) {
|
|
145
|
+
const { message, signature } = await signing.signJoinLobby(this.signer, lobbyId);
|
|
146
|
+
return this._json(`/api/lobbies/${lobbyId}/join`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
body: JSON.stringify({ message, signature }),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** POST /api/lobbies/:lobbyId/cancel — cancel your waiting lobby (creator only). */
|
|
153
|
+
async cancelLobby(lobbyId) {
|
|
154
|
+
const { message, signature } = await signing.signCancelLobby(this.signer, lobbyId);
|
|
155
|
+
return this._json(`/api/lobbies/${lobbyId}/cancel`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
body: JSON.stringify({ message, signature }),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** POST /api/lobbies/:lobbyId/concede — concede the game (you lose). */
|
|
162
|
+
async concede(lobbyId) {
|
|
163
|
+
const { message, signature } = await signing.signConcedeLobby(this.signer, lobbyId);
|
|
164
|
+
return this._json(`/api/lobbies/${lobbyId}/concede`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: JSON.stringify({ message, signature }),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* POST /api/lobbies/:lobbyId/timeout — report that you ran out of time (you lose).
|
|
172
|
+
* Only the player who timed out should call this.
|
|
173
|
+
*/
|
|
174
|
+
async timeout(lobbyId) {
|
|
175
|
+
const { message, signature } = await signing.signTimeoutLobby(this.signer, lobbyId);
|
|
176
|
+
return this._json(`/api/lobbies/${lobbyId}/timeout`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({ message, signature }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Join the real-time game room for a lobby. Required before makeMove().
|
|
184
|
+
* Call after joinLobby() or when opening an existing game.
|
|
185
|
+
*/
|
|
186
|
+
joinGame(lobbyId) {
|
|
187
|
+
if (!this.socket) throw new Error("Call connect() first");
|
|
188
|
+
this.socket.emit("join_lobby", lobbyId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Leave the real-time game room. */
|
|
192
|
+
leaveGame(lobbyId) {
|
|
193
|
+
if (this.socket) this.socket.emit("leave_lobby", lobbyId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Send a chess move (real-time). You must have called joinGame(lobbyId) and it must be your turn.
|
|
198
|
+
* @param {string} lobbyId
|
|
199
|
+
* @param {string} from - e.g. 'e2'
|
|
200
|
+
* @param {string} to - e.g. 'e4'
|
|
201
|
+
* @param {string} [promotion] - 'q' | 'r' | 'b' | 'n' for pawn promotion
|
|
202
|
+
*/
|
|
203
|
+
makeMove(lobbyId, from, to, promotion = "q") {
|
|
204
|
+
if (!this.socket) throw new Error("Call connect() first");
|
|
205
|
+
this.socket.emit("move", { lobbyId, from, to, promotion: promotion || "q" });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** GET /api/health */
|
|
209
|
+
async health() {
|
|
210
|
+
return this._json("/api/health");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** GET /api/status — server status (counts only). */
|
|
214
|
+
async status() {
|
|
215
|
+
return this._json("/api/status");
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/escrow.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional escrow helpers for on-chain create/join/cancel.
|
|
3
|
+
* Use when your Clawmate backend uses the ChessBetEscrow contract; pass your own provider, signer, and contract address.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Contract } from "ethers";
|
|
7
|
+
|
|
8
|
+
const ESCROW_ABI = [
|
|
9
|
+
"function createLobby() external payable",
|
|
10
|
+
"function joinLobby(uint256 gameId) external payable",
|
|
11
|
+
"function cancelLobby(uint256 gameId) external",
|
|
12
|
+
"function games(uint256) view returns (address player1, address player2, uint256 betAmount, bool active, address winner)",
|
|
13
|
+
"event LobbyCreated(uint256 gameId, address player1, uint256 betAmount)",
|
|
14
|
+
"event LobbyJoined(uint256 gameId, address player2)",
|
|
15
|
+
"event LobbyCancelled(uint256 gameId, address player1)",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a lobby on the escrow contract (pay bet). Returns contract gameId (1-based).
|
|
20
|
+
* @param {{ signer: import('ethers').Signer, contractAddress: string, betWei: string | bigint }}
|
|
21
|
+
*/
|
|
22
|
+
export async function createLobbyOnChain({ signer, contractAddress, betWei }) {
|
|
23
|
+
const amount = BigInt(betWei);
|
|
24
|
+
if (amount <= 0n) throw new Error("Bet must be > 0");
|
|
25
|
+
const contract = new Contract(contractAddress, ESCROW_ABI, signer);
|
|
26
|
+
const tx = await contract.createLobby({ value: amount });
|
|
27
|
+
const receipt = await tx.wait();
|
|
28
|
+
const log = receipt?.logs?.find((l) => {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = contract.interface.parseLog({ topics: l.topics, data: l.data });
|
|
31
|
+
return parsed?.name === "LobbyCreated";
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (!log) throw new Error("LobbyCreated event not found");
|
|
37
|
+
const parsed = contract.interface.parseLog({ topics: log.topics, data: log.data });
|
|
38
|
+
return Number(parsed.args.gameId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Join a lobby on the escrow contract (pay bet).
|
|
43
|
+
* @param {{ signer: import('ethers').Signer, contractAddress: string, gameId: number, betWei: string | bigint }}
|
|
44
|
+
*/
|
|
45
|
+
export async function joinLobbyOnChain({ signer, contractAddress, gameId, betWei }) {
|
|
46
|
+
const amount = BigInt(betWei);
|
|
47
|
+
if (amount <= 0n) throw new Error("Bet must be > 0");
|
|
48
|
+
const contract = new Contract(contractAddress, ESCROW_ABI, signer);
|
|
49
|
+
const tx = await contract.joinLobby(gameId, { value: amount });
|
|
50
|
+
await tx.wait();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cancel your waiting lobby on-chain (refunds creator). Creator only, no opponent yet.
|
|
55
|
+
* @param {{ signer: import('ethers').Signer, contractAddress: string, gameId: number }}
|
|
56
|
+
*/
|
|
57
|
+
export async function cancelLobbyOnChain({ signer, contractAddress, gameId }) {
|
|
58
|
+
const contract = new Contract(contractAddress, ESCROW_ABI, signer);
|
|
59
|
+
const tx = await contract.cancelLobby(gameId);
|
|
60
|
+
await tx.wait();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read game state from the contract (no tx).
|
|
65
|
+
* @param {{ provider: import('ethers').Provider, contractAddress: string, gameId: number }}
|
|
66
|
+
*/
|
|
67
|
+
export async function getGameStateOnChain({ provider, contractAddress, gameId }) {
|
|
68
|
+
const contract = new Contract(contractAddress, ESCROW_ABI, provider);
|
|
69
|
+
const g = await contract.games(gameId);
|
|
70
|
+
const player1 = g?.player1 ?? g?.[0];
|
|
71
|
+
const player2 = g?.player2 ?? g?.[1];
|
|
72
|
+
const betAmount = g?.betAmount ?? g?.[2];
|
|
73
|
+
const active = g?.active ?? g?.[3];
|
|
74
|
+
if (player1 == null) return null;
|
|
75
|
+
const zero = "0x0000000000000000000000000000000000000000";
|
|
76
|
+
return {
|
|
77
|
+
active: Boolean(active),
|
|
78
|
+
player1: (player1 || zero).toLowerCase(),
|
|
79
|
+
player2: (player2 || zero).toLowerCase(),
|
|
80
|
+
betAmount: betAmount != null ? String(betAmount) : "0",
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/signing.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message builders and signing for Clawmate API/Socket auth.
|
|
3
|
+
* Uses EIP-191 personal_sign; backend recovers address and validates timestamp.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DOMAIN = "Clawmate";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('ethers').Signer} signer
|
|
10
|
+
* @param {string} message
|
|
11
|
+
* @returns {Promise<string>} signature hex
|
|
12
|
+
*/
|
|
13
|
+
export async function signMessage(signer, message) {
|
|
14
|
+
return signer.signMessage(message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildCreateLobbyMessage(betAmount, contractGameId) {
|
|
18
|
+
const timestamp = Date.now();
|
|
19
|
+
return `${DOMAIN} create lobby\nBet: ${betAmount}\nContractGameId: ${contractGameId ?? ""}\nTimestamp: ${timestamp}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildJoinLobbyMessage(lobbyId) {
|
|
23
|
+
const timestamp = Date.now();
|
|
24
|
+
return `${DOMAIN} join lobby\nLobbyId: ${lobbyId}\nTimestamp: ${timestamp}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildCancelLobbyMessage(lobbyId) {
|
|
28
|
+
const timestamp = Date.now();
|
|
29
|
+
return `${DOMAIN} cancel lobby\nLobbyId: ${lobbyId}\nTimestamp: ${timestamp}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildConcedeLobbyMessage(lobbyId) {
|
|
33
|
+
const timestamp = Date.now();
|
|
34
|
+
return `${DOMAIN} concede lobby\nLobbyId: ${lobbyId}\nTimestamp: ${timestamp}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildTimeoutLobbyMessage(lobbyId) {
|
|
38
|
+
const timestamp = Date.now();
|
|
39
|
+
return `${DOMAIN} timeout lobby\nLobbyId: ${lobbyId}\nTimestamp: ${timestamp}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildRegisterWalletMessage() {
|
|
43
|
+
const timestamp = Date.now();
|
|
44
|
+
return `${DOMAIN} register wallet\nTimestamp: ${timestamp}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('ethers').Signer} signer
|
|
49
|
+
* @param {{ betAmount: string, contractGameId?: number | null }} opts
|
|
50
|
+
*/
|
|
51
|
+
export async function signCreateLobby(signer, opts) {
|
|
52
|
+
const message = buildCreateLobbyMessage(opts.betAmount, opts.contractGameId ?? null);
|
|
53
|
+
const signature = await signMessage(signer, message);
|
|
54
|
+
return { message, signature };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {import('ethers').Signer} signer @param {string} lobbyId */
|
|
58
|
+
export async function signJoinLobby(signer, lobbyId) {
|
|
59
|
+
const message = buildJoinLobbyMessage(lobbyId);
|
|
60
|
+
const signature = await signMessage(signer, message);
|
|
61
|
+
return { message, signature };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {import('ethers').Signer} signer @param {string} lobbyId */
|
|
65
|
+
export async function signCancelLobby(signer, lobbyId) {
|
|
66
|
+
const message = buildCancelLobbyMessage(lobbyId);
|
|
67
|
+
const signature = await signMessage(signer, message);
|
|
68
|
+
return { message, signature };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {import('ethers').Signer} signer @param {string} lobbyId */
|
|
72
|
+
export async function signConcedeLobby(signer, lobbyId) {
|
|
73
|
+
const message = buildConcedeLobbyMessage(lobbyId);
|
|
74
|
+
const signature = await signMessage(signer, message);
|
|
75
|
+
return { message, signature };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @param {import('ethers').Signer} signer @param {string} lobbyId */
|
|
79
|
+
export async function signTimeoutLobby(signer, lobbyId) {
|
|
80
|
+
const message = buildTimeoutLobbyMessage(lobbyId);
|
|
81
|
+
const signature = await signMessage(signer, message);
|
|
82
|
+
return { message, signature };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {import('ethers').Signer} signer */
|
|
86
|
+
export async function signRegisterWallet(signer) {
|
|
87
|
+
const message = buildRegisterWalletMessage();
|
|
88
|
+
const signature = await signMessage(signer, message);
|
|
89
|
+
return { message, signature };
|
|
90
|
+
}
|