@dopamint-fun/open-sdk 0.1.0-dev.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 +42 -0
- package/dist/acceptance.d.ts +24 -0
- package/dist/acceptance.js +48 -0
- package/dist/agentHttp.d.ts +52 -0
- package/dist/agentHttp.js +139 -0
- package/dist/bytes.d.ts +40 -0
- package/dist/bytes.js +166 -0
- package/dist/channel.d.ts +38 -0
- package/dist/channel.js +86 -0
- package/dist/claim.d.ts +39 -0
- package/dist/claim.js +91 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1006 -0
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +20 -0
- package/dist/identity.d.ts +32 -0
- package/dist/identity.js +100 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +22 -0
- package/dist/keypair.d.ts +25 -0
- package/dist/keypair.js +69 -0
- package/dist/offer.d.ts +36 -0
- package/dist/offer.js +138 -0
- package/dist/registration.d.ts +53 -0
- package/dist/registration.js +104 -0
- package/dist/seatAuth.d.ts +30 -0
- package/dist/seatAuth.js +285 -0
- package/dist/session.d.ts +260 -0
- package/dist/session.js +840 -0
- package/dist/sessionCodec.d.ts +139 -0
- package/dist/sessionCodec.js +373 -0
- package/dist/sessionWire.d.ts +80 -0
- package/dist/sessionWire.js +118 -0
- package/dist/settlement.d.ts +56 -0
- package/dist/settlement.js +117 -0
- package/dist/texas.d.ts +52 -0
- package/dist/texas.js +217 -0
- package/dist/tour.d.ts +101 -0
- package/dist/tour.js +129 -0
- package/package.json +35 -0
package/dist/tour.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Entering a tour and playing the seat it gives you, on a self-held key.
|
|
2
|
+
*
|
|
3
|
+
* This is the tour door, and it is not the offer door `session.ts` drives. An
|
|
4
|
+
* offer is a match somebody else already composed and invited this agent into,
|
|
5
|
+
* so it needs a table owner in the loop and it is played over the Participant
|
|
6
|
+
* Session at the authority. A tour seats the agent from a queue with nobody
|
|
7
|
+
* proposing anything, and it is played over the Product API - so there is no
|
|
8
|
+
* session base url, coordinator key or time-authority key to obtain here.
|
|
9
|
+
*
|
|
10
|
+
* Every request carries a freshly minted `AgentHttpCapability`. The nonce is
|
|
11
|
+
* single-use and the server records it, so a capability is never reused across
|
|
12
|
+
* two requests even inside its own validity window.
|
|
13
|
+
*/
|
|
14
|
+
import { AGENT_HTTP_CAPABILITY_HEADER, mintAgentHttpCapability, } from "./agentHttp.js";
|
|
15
|
+
import { textBytes } from "./bytes.js";
|
|
16
|
+
/** One signed Product API call. `target` is origin-form, exactly as the server
|
|
17
|
+
* will reconstruct it - a leading-slash path plus any query string. */
|
|
18
|
+
async function signedFetch(client, method, target, body) {
|
|
19
|
+
const payload = body === undefined ? new Uint8Array() : textBytes(JSON.stringify(body));
|
|
20
|
+
const binding = {
|
|
21
|
+
method,
|
|
22
|
+
requestTarget: target,
|
|
23
|
+
body: payload,
|
|
24
|
+
};
|
|
25
|
+
const { header } = await mintAgentHttpCapability(client.agent, client.agentId, binding);
|
|
26
|
+
const headers = {
|
|
27
|
+
[AGENT_HTTP_CAPABILITY_HEADER]: header,
|
|
28
|
+
};
|
|
29
|
+
if (body !== undefined)
|
|
30
|
+
headers["content-type"] = "application/json";
|
|
31
|
+
const response = await fetch(`${client.productUrl.replace(/\/$/, "")}${target}`, {
|
|
32
|
+
method,
|
|
33
|
+
headers,
|
|
34
|
+
// The signature covers these exact bytes, so the body must be sent
|
|
35
|
+
// verbatim rather than re-serialized by fetch from an object.
|
|
36
|
+
body: body === undefined ? undefined : payload,
|
|
37
|
+
});
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
return {
|
|
40
|
+
status: response.status,
|
|
41
|
+
json: text.length === 0 ? undefined : JSON.parse(text),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function enterTour(client, tour) {
|
|
45
|
+
const target = tour === "playground"
|
|
46
|
+
? "/open/v1/tours/playground/entries"
|
|
47
|
+
: "/open/v1/tours/tournament/entries";
|
|
48
|
+
const { status, json } = await signedFetch(client, "POST", target);
|
|
49
|
+
if (status !== 200)
|
|
50
|
+
throw new Error(`tour entry refused (${status}): ${JSON.stringify(json)}`);
|
|
51
|
+
return json;
|
|
52
|
+
}
|
|
53
|
+
/** Enter and poll until seated.
|
|
54
|
+
*
|
|
55
|
+
* Waiting is the normal answer, not a failure: the playground holds a place
|
|
56
|
+
* for real opponents and only house-fills the leftover seats once the wait
|
|
57
|
+
* elapses, so an agent that gives up early is the reason a table stays short.
|
|
58
|
+
* The default timeout therefore outlasts the server's own fill wait. */
|
|
59
|
+
export async function queueUntilSeated(client, tour, options = {}) {
|
|
60
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
61
|
+
const pollMs = options.pollMs ?? 3_000;
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
for (;;) {
|
|
64
|
+
const entry = await enterTour(client, tour);
|
|
65
|
+
if (entry.state === "seated" || entry.state === "offered")
|
|
66
|
+
return entry;
|
|
67
|
+
if (entry.state !== "waiting")
|
|
68
|
+
throw new Error(`tour is not open: ${JSON.stringify(entry)}`);
|
|
69
|
+
options.onWaiting?.(entry);
|
|
70
|
+
if (Date.now() >= deadline)
|
|
71
|
+
throw new Error(`still waiting for a seat after ${timeoutMs} ms; the tour fills at ${entry.fillAtMs}`);
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function readPosition(client, tableId) {
|
|
76
|
+
const { status, json } = await signedFetch(client, "GET", `/open/v1/tables/${tableId}/decision`);
|
|
77
|
+
if (status !== 200)
|
|
78
|
+
throw new Error(`decision read refused (${status}): ${JSON.stringify(json)}`);
|
|
79
|
+
return json;
|
|
80
|
+
}
|
|
81
|
+
export async function act(client, tableId, move) {
|
|
82
|
+
const { status, json } = await signedFetch(client, "POST", `/open/v1/tables/${tableId}/actions`, move);
|
|
83
|
+
if (status !== 200)
|
|
84
|
+
throw new Error(`action refused (${status}): ${JSON.stringify(json)}`);
|
|
85
|
+
return json;
|
|
86
|
+
}
|
|
87
|
+
/** The reference strategy: never wager, call only what is free. It exists so
|
|
88
|
+
* the journey can be demonstrated end to end, not because it plays well - an
|
|
89
|
+
* agent that reasons about the hand replaces `decide`. */
|
|
90
|
+
export function foldHeavy(turn) {
|
|
91
|
+
if (turn.canCheck)
|
|
92
|
+
return { action: "check" };
|
|
93
|
+
if (turn.callAmount === 0 && turn.canCall)
|
|
94
|
+
return { action: "call" };
|
|
95
|
+
return { action: turn.canFold ? "fold" : "check" };
|
|
96
|
+
}
|
|
97
|
+
/** Drive a seated tour table to a terminal disposition.
|
|
98
|
+
*
|
|
99
|
+
* A lapsed turn is not an error: the authority acts for a silent seat on its
|
|
100
|
+
* own clock, so a slow decision costs the hand rather than the match, and the
|
|
101
|
+
* loop keeps reading. Only a refusal the server calls terminal stops it. */
|
|
102
|
+
export async function playTour(client, tableId, decide = foldHeavy, options = {}) {
|
|
103
|
+
const pollMs = options.pollMs ?? 1_000;
|
|
104
|
+
let committedActions = 0;
|
|
105
|
+
const hands = new Set();
|
|
106
|
+
for (;;) {
|
|
107
|
+
const turn = await readPosition(client, tableId);
|
|
108
|
+
if (turn.state === "complete" || turn.state === "eliminated")
|
|
109
|
+
return { outcome: turn.state, committedActions, hands: hands.size };
|
|
110
|
+
if (turn.state !== "your_turn") {
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (turn.handIndex !== undefined)
|
|
115
|
+
hands.add(turn.handIndex);
|
|
116
|
+
const move = decide(turn);
|
|
117
|
+
options.onTurn?.(turn, move);
|
|
118
|
+
try {
|
|
119
|
+
await act(client, tableId, move);
|
|
120
|
+
committedActions += 1;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// 409 is the turn clock having resolved this seat already. Re-read
|
|
124
|
+
// rather than abort: the match continues without this action.
|
|
125
|
+
if (!(error instanceof Error) || !error.message.includes("(409)"))
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dopamint-fun/open-sdk",
|
|
3
|
+
"version": "0.1.0-dev.0",
|
|
4
|
+
"description": "DOPA-OPEN client SDK: generate and hold your own agent key, sign registrations, and play a Participant Session",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/CommandOSSLabs/dopamint-arena.git",
|
|
9
|
+
"directory": "libs/dopa-open-client-ts"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"dopa-open": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc --project tsconfig.build.json",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "node --import tsx --test --test-isolation=none \"src/**/*.test.ts\""
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@mysten/sui": "^2.20.1",
|
|
28
|
+
"@noble/hashes": "^2.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.19.43",
|
|
32
|
+
"tsx": "^4.22.4",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
}
|
|
35
|
+
}
|