@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/seatAuth.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { ByteReader, ByteWriter, concatBytes, equalBytes, frameSigningBytes, textBytes, } from "./bytes.js";
|
|
2
|
+
import { blake2b256, verifyEd25519 } from "./crypto.js";
|
|
3
|
+
import { takePrincipalProof } from "./sessionCodec.js";
|
|
4
|
+
import { decodeAction, decodePlayerActionInput } from "./texas.js";
|
|
5
|
+
const TRANSITION_AUTHORIZATION_DOMAIN = textBytes("arena_tunnel::transition_authorization");
|
|
6
|
+
const ARRIVAL_EVIDENCE_DOMAIN = textBytes("arena_tunnel::arrival_evidence");
|
|
7
|
+
const LOCK_EVIDENCE_DOMAIN = textBytes("arena_tunnel::lock_evidence");
|
|
8
|
+
const SEAT_GENERATION_DOMAIN = textBytes("dopa_open::seat_generation::v1");
|
|
9
|
+
const ROLE_GENERATION_DOMAIN = textBytes("dopa_open::role_generation::v1");
|
|
10
|
+
const TIME_EVIDENCE_ABSENT = 0;
|
|
11
|
+
const TIME_EVIDENCE_PRESENT = 1;
|
|
12
|
+
const ARRIVAL_EVIDENCE_TAG = 0;
|
|
13
|
+
const DEADLINE_EVIDENCE_TAG = 1;
|
|
14
|
+
const LOCK_EVIDENCE_TAG = 2;
|
|
15
|
+
const PARTICIPANT_ACTION_TAG = 0;
|
|
16
|
+
const ALL_OF_TAG = 0;
|
|
17
|
+
const SEAT_PRINCIPAL_TAG = 0;
|
|
18
|
+
const ROLE_PRINCIPAL_TAG = 1;
|
|
19
|
+
export const DOPA_OPEN_COORDINATOR_ROLE = new Uint8Array(32).fill(0xa1);
|
|
20
|
+
export const DOPA_OPEN_TIME_AUTHORITY_ROLE = new Uint8Array(32).fill(0xa3);
|
|
21
|
+
export function seatGenerationDigest(generation = 1) {
|
|
22
|
+
const writer = new ByteWriter();
|
|
23
|
+
writer.pushBytes(SEAT_GENERATION_DOMAIN).pushU32(generation);
|
|
24
|
+
return blake2b256(writer.bytes());
|
|
25
|
+
}
|
|
26
|
+
export function roleGenerationDigest(role) {
|
|
27
|
+
return blake2b256(concatBytes(ROLE_GENERATION_DOMAIN, role));
|
|
28
|
+
}
|
|
29
|
+
export function coordinatorGenerationDigest() {
|
|
30
|
+
return roleGenerationDigest(DOPA_OPEN_COORDINATOR_ROLE);
|
|
31
|
+
}
|
|
32
|
+
export function timeAuthorityGenerationDigest() {
|
|
33
|
+
return roleGenerationDigest(DOPA_OPEN_TIME_AUTHORITY_ROLE);
|
|
34
|
+
}
|
|
35
|
+
function encodeSeatPrincipal(seat) {
|
|
36
|
+
return new ByteWriter().pushByte(0).pushU16(seat).bytes();
|
|
37
|
+
}
|
|
38
|
+
function encodeRolePrincipal(role) {
|
|
39
|
+
return new ByteWriter().pushByte(1).pushFixed(role, 32, "role").bytes();
|
|
40
|
+
}
|
|
41
|
+
export function transitionProofSigningBytes(payload, principal, generation, scheme = 0) {
|
|
42
|
+
const framed = concatBytes(payload, principal, generation, Uint8Array.of(scheme));
|
|
43
|
+
return frameSigningBytes(TRANSITION_AUTHORIZATION_DOMAIN, framed);
|
|
44
|
+
}
|
|
45
|
+
function decodeState(reader, nonceField, commitmentField) {
|
|
46
|
+
return {
|
|
47
|
+
nonce: reader.readU64(nonceField),
|
|
48
|
+
commitment: reader.readFixed(32, commitmentField),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function decodeCandidate(bytes) {
|
|
52
|
+
const reader = new ByteReader(bytes);
|
|
53
|
+
const wireVersion = reader.readU16("candidate wire version");
|
|
54
|
+
if (wireVersion !== 1)
|
|
55
|
+
throw new Error(`unsupported candidate wire ${wireVersion}`);
|
|
56
|
+
const executionId = reader.readFixed(32, "candidate execution id");
|
|
57
|
+
const protocolVersion = reader.readU16("candidate protocol version");
|
|
58
|
+
const protocolId = reader.readFixed(32, "candidate protocol id");
|
|
59
|
+
const previousState = decodeState(reader, "candidate previous nonce", "candidate previous commitment");
|
|
60
|
+
const resulting = decodeState(reader, "candidate resulting nonce", "candidate resulting commitment");
|
|
61
|
+
const originTag = reader.readByte("transition origin tag");
|
|
62
|
+
if (originTag !== PARTICIPANT_ACTION_TAG)
|
|
63
|
+
throw new Error("seat authorization origin is not a participant action");
|
|
64
|
+
const originSeat = reader.readU16("origin actor");
|
|
65
|
+
const authTag = reader.readByte("authorization tag");
|
|
66
|
+
if (authTag !== ALL_OF_TAG)
|
|
67
|
+
throw new Error("seat authorization requirement is not all-of");
|
|
68
|
+
const principalCount = reader.readU16("authorization principal count");
|
|
69
|
+
let seatPrincipal = null;
|
|
70
|
+
let rolePrincipal = null;
|
|
71
|
+
for (let index = 0; index < principalCount; index++) {
|
|
72
|
+
const principalTag = reader.readByte("authorization principal tag");
|
|
73
|
+
if (principalTag === SEAT_PRINCIPAL_TAG) {
|
|
74
|
+
if (seatPrincipal !== null)
|
|
75
|
+
throw new Error("seat authorization names two seats");
|
|
76
|
+
seatPrincipal = reader.readU16("authorization seat");
|
|
77
|
+
}
|
|
78
|
+
else if (principalTag === ROLE_PRINCIPAL_TAG) {
|
|
79
|
+
if (rolePrincipal !== null)
|
|
80
|
+
throw new Error("seat authorization names two roles");
|
|
81
|
+
rolePrincipal = reader.readFixed(32, "authorization role");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error(`invalid authorization principal tag ${principalTag}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const evidenceCount = reader.readU16("evidence count");
|
|
88
|
+
const artifacts = [];
|
|
89
|
+
for (let index = 0; index < evidenceCount; index++) {
|
|
90
|
+
artifacts.push({
|
|
91
|
+
kind: reader.readU16("evidence kind"),
|
|
92
|
+
schemaVersion: reader.readU16("evidence schema"),
|
|
93
|
+
digest: reader.readFixed(32, "evidence digest"),
|
|
94
|
+
byteLength: reader.readU64("evidence byte length"),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
reader.finish();
|
|
98
|
+
return {
|
|
99
|
+
executionId,
|
|
100
|
+
protocolVersion,
|
|
101
|
+
protocolId,
|
|
102
|
+
previousState,
|
|
103
|
+
resultingNonce: resulting.nonce,
|
|
104
|
+
originSeat,
|
|
105
|
+
seatPrincipal,
|
|
106
|
+
rolePrincipal,
|
|
107
|
+
principalCount,
|
|
108
|
+
artifacts,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function decodeSignature(reader, field) {
|
|
112
|
+
const length = reader.readU16(`${field} length`);
|
|
113
|
+
return reader.readFixed(length, field);
|
|
114
|
+
}
|
|
115
|
+
function decodeArrival(reader) {
|
|
116
|
+
return {
|
|
117
|
+
kind: "arrival",
|
|
118
|
+
executionId: reader.readFixed(32, "arrival execution id"),
|
|
119
|
+
requestDigest: reader.readFixed(32, "arrival request digest"),
|
|
120
|
+
previousState: decodeState(reader, "arrival previous nonce", "arrival previous commitment"),
|
|
121
|
+
timeAuthority: reader.readFixed(32, "arrival time authority"),
|
|
122
|
+
authoritySequence: reader.readU64("arrival authority sequence"),
|
|
123
|
+
observedAtMs: reader.readU64("arrival observed at"),
|
|
124
|
+
deadlineMs: reader.readU64("arrival deadline"),
|
|
125
|
+
runtimeGeneration: reader.readFixed(32, "arrival runtime generation"),
|
|
126
|
+
signature: decodeSignature(reader, "arrival signature"),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function decodeLock(reader) {
|
|
130
|
+
return {
|
|
131
|
+
kind: "lock",
|
|
132
|
+
executionId: reader.readFixed(32, "lock execution id"),
|
|
133
|
+
requestDigest: reader.readFixed(32, "lock request digest"),
|
|
134
|
+
previousState: decodeState(reader, "lock previous nonce", "lock previous commitment"),
|
|
135
|
+
timeAuthority: reader.readFixed(32, "lock time authority"),
|
|
136
|
+
authoritySequence: reader.readU64("lock authority sequence"),
|
|
137
|
+
arrivalMs: reader.readU64("lock arrival at"),
|
|
138
|
+
lockedAtMs: reader.readU64("lock locked at"),
|
|
139
|
+
deadlineMs: reader.readU64("lock deadline"),
|
|
140
|
+
runtimeGeneration: reader.readFixed(32, "lock runtime generation"),
|
|
141
|
+
signature: decodeSignature(reader, "lock signature"),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function decodeAuthorizationPayload(bytes) {
|
|
145
|
+
const reader = new ByteReader(bytes);
|
|
146
|
+
const candidate = decodeCandidate(reader.readLengthPrefixed("candidate length", "candidate transition"));
|
|
147
|
+
const protocolInputSchema = reader.readU16("protocol input schema");
|
|
148
|
+
const protocolInput = reader.readLengthPrefixed("protocol input length", "protocol input");
|
|
149
|
+
const presence = reader.readByte("time evidence presence");
|
|
150
|
+
if (presence === TIME_EVIDENCE_ABSENT)
|
|
151
|
+
throw new Error("seat authorization time evidence missing");
|
|
152
|
+
if (presence !== TIME_EVIDENCE_PRESENT)
|
|
153
|
+
throw new Error(`invalid time evidence presence ${presence}`);
|
|
154
|
+
const tag = reader.readByte("time evidence tag");
|
|
155
|
+
let timeEvidence;
|
|
156
|
+
if (tag === ARRIVAL_EVIDENCE_TAG)
|
|
157
|
+
timeEvidence = decodeArrival(reader);
|
|
158
|
+
else if (tag === LOCK_EVIDENCE_TAG)
|
|
159
|
+
timeEvidence = decodeLock(reader);
|
|
160
|
+
else if (tag === DEADLINE_EVIDENCE_TAG)
|
|
161
|
+
throw new Error("seat authorization time evidence missing");
|
|
162
|
+
else
|
|
163
|
+
throw new Error(`invalid time evidence tag ${tag}`);
|
|
164
|
+
reader.finish();
|
|
165
|
+
return { candidate, protocolInputSchema, protocolInput, timeEvidence };
|
|
166
|
+
}
|
|
167
|
+
function encodeArrivalBody(evidence) {
|
|
168
|
+
return new ByteWriter()
|
|
169
|
+
.pushFixed(evidence.executionId, 32, "arrival execution id")
|
|
170
|
+
.pushFixed(evidence.requestDigest, 32, "arrival request digest")
|
|
171
|
+
.pushU64(evidence.previousState.nonce)
|
|
172
|
+
.pushFixed(evidence.previousState.commitment, 32, "arrival previous commitment")
|
|
173
|
+
.pushFixed(evidence.timeAuthority, 32, "arrival time authority")
|
|
174
|
+
.pushU64(evidence.authoritySequence)
|
|
175
|
+
.pushU64(evidence.observedAtMs)
|
|
176
|
+
.pushU64(evidence.deadlineMs)
|
|
177
|
+
.pushFixed(evidence.runtimeGeneration, 32, "arrival runtime generation")
|
|
178
|
+
.bytes();
|
|
179
|
+
}
|
|
180
|
+
function encodeLockBody(evidence) {
|
|
181
|
+
return new ByteWriter()
|
|
182
|
+
.pushFixed(evidence.executionId, 32, "lock execution id")
|
|
183
|
+
.pushFixed(evidence.requestDigest, 32, "lock request digest")
|
|
184
|
+
.pushU64(evidence.previousState.nonce)
|
|
185
|
+
.pushFixed(evidence.previousState.commitment, 32, "lock previous commitment")
|
|
186
|
+
.pushFixed(evidence.timeAuthority, 32, "lock time authority")
|
|
187
|
+
.pushU64(evidence.authoritySequence)
|
|
188
|
+
.pushU64(evidence.arrivalMs)
|
|
189
|
+
.pushU64(evidence.lockedAtMs)
|
|
190
|
+
.pushU64(evidence.deadlineMs)
|
|
191
|
+
.pushFixed(evidence.runtimeGeneration, 32, "lock runtime generation")
|
|
192
|
+
.bytes();
|
|
193
|
+
}
|
|
194
|
+
function sameState(left, nonce, commitment) {
|
|
195
|
+
return left.nonce === nonce && equalBytes(left.commitment, commitment);
|
|
196
|
+
}
|
|
197
|
+
function sameArtifacts(left, right) {
|
|
198
|
+
if (left.length !== right.length)
|
|
199
|
+
return false;
|
|
200
|
+
return left.every((artifact, index) => artifact.kind === right[index].kind &&
|
|
201
|
+
artifact.schemaVersion === right[index].schemaVersion &&
|
|
202
|
+
artifact.byteLength === right[index].byteLength &&
|
|
203
|
+
equalBytes(artifact.digest, right[index].digest));
|
|
204
|
+
}
|
|
205
|
+
function verifyPayloadIdentity(payload, seat, pending) {
|
|
206
|
+
const { candidate } = payload;
|
|
207
|
+
if (candidate.principalCount !== 2 ||
|
|
208
|
+
candidate.seatPrincipal !== seat ||
|
|
209
|
+
!candidate.rolePrincipal ||
|
|
210
|
+
!equalBytes(candidate.rolePrincipal, DOPA_OPEN_COORDINATOR_ROLE))
|
|
211
|
+
throw new Error("seat authorization requirement mismatch");
|
|
212
|
+
if (candidate.protocolVersion !== pending.protocolVersion ||
|
|
213
|
+
!equalBytes(candidate.executionId, pending.executionId) ||
|
|
214
|
+
!equalBytes(candidate.protocolId, pending.protocolId) ||
|
|
215
|
+
candidate.originSeat !== seat ||
|
|
216
|
+
!sameState(candidate.previousState, pending.expectedStateNonce, pending.expectedStateCommitment) ||
|
|
217
|
+
candidate.resultingNonce !== pending.expectedStateNonce + 1n ||
|
|
218
|
+
!sameArtifacts(candidate.artifacts, pending.artifactReferences) ||
|
|
219
|
+
payload.protocolInputSchema !== pending.payloadSchemaVersion)
|
|
220
|
+
throw new Error("seat authorization candidate identity mismatch");
|
|
221
|
+
}
|
|
222
|
+
function verifyTimeEvidence(evidence, pending, timeAuthorityPublicKey) {
|
|
223
|
+
const requestDigest = blake2b256(pending.signingBytes);
|
|
224
|
+
const expectedGeneration = timeAuthorityGenerationDigest();
|
|
225
|
+
if (!equalBytes(evidence.executionId, pending.executionId) ||
|
|
226
|
+
!equalBytes(evidence.requestDigest, requestDigest) ||
|
|
227
|
+
!sameState(evidence.previousState, pending.expectedStateNonce, pending.expectedStateCommitment) ||
|
|
228
|
+
!equalBytes(evidence.timeAuthority, DOPA_OPEN_TIME_AUTHORITY_ROLE) ||
|
|
229
|
+
!equalBytes(evidence.runtimeGeneration, expectedGeneration))
|
|
230
|
+
throw new Error("seat authorization arrival identity mismatch");
|
|
231
|
+
if (evidence.authoritySequence === 0n)
|
|
232
|
+
throw new Error("seat authorization arrival identity mismatch");
|
|
233
|
+
if (evidence.deadlineMs !== pending.participantDeadlineMs)
|
|
234
|
+
throw new Error("seat authorization arrival identity mismatch");
|
|
235
|
+
if (evidence.kind === "arrival") {
|
|
236
|
+
if (evidence.observedAtMs === 0n || evidence.observedAtMs > evidence.deadlineMs)
|
|
237
|
+
throw new Error("seat authorization arrival identity mismatch");
|
|
238
|
+
const signingBytes = frameSigningBytes(ARRIVAL_EVIDENCE_DOMAIN, encodeArrivalBody(evidence));
|
|
239
|
+
if (!verifyEd25519(timeAuthorityPublicKey, signingBytes, evidence.signature))
|
|
240
|
+
throw new Error("seat authorization arrival signature refused");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (evidence.arrivalMs === 0n || evidence.arrivalMs > evidence.deadlineMs)
|
|
244
|
+
throw new Error("seat authorization arrival identity mismatch");
|
|
245
|
+
if (evidence.lockedAtMs > evidence.deadlineMs)
|
|
246
|
+
throw new Error("seat authorization arrival identity mismatch");
|
|
247
|
+
const signingBytes = frameSigningBytes(LOCK_EVIDENCE_DOMAIN, encodeLockBody(evidence));
|
|
248
|
+
if (!verifyEd25519(timeAuthorityPublicKey, signingBytes, evidence.signature))
|
|
249
|
+
throw new Error("seat authorization lock signature refused");
|
|
250
|
+
}
|
|
251
|
+
export function authorizeSeatChallenge(args) {
|
|
252
|
+
const { seat, coordinatorPublicKey, challengeCoordinatorKey, coordinatorProof, authorizationPayload, timeAuthorityPublicKey, pending, } = args;
|
|
253
|
+
if (coordinatorPublicKey.length !== 32 ||
|
|
254
|
+
challengeCoordinatorKey.length !== 32 ||
|
|
255
|
+
!equalBytes(coordinatorPublicKey, challengeCoordinatorKey))
|
|
256
|
+
throw new Error("coordinator key does not match admission");
|
|
257
|
+
if (timeAuthorityPublicKey.length !== 32)
|
|
258
|
+
throw new Error("time-authority key must be 32 bytes");
|
|
259
|
+
const payload = decodeAuthorizationPayload(authorizationPayload);
|
|
260
|
+
verifyPayloadIdentity(payload, seat, pending);
|
|
261
|
+
verifyTimeEvidence(payload.timeEvidence, pending, timeAuthorityPublicKey);
|
|
262
|
+
const challenged = decodePlayerActionInput(payload.protocolInput, pending.expectedStateNonce, pending.expectedStateCommitment);
|
|
263
|
+
const proposed = decodeAction(pending.actionPayload);
|
|
264
|
+
if (challenged.type !== proposed.type ||
|
|
265
|
+
(challenged.type === "wagerTo" &&
|
|
266
|
+
proposed.type === "wagerTo" &&
|
|
267
|
+
challenged.amount !== proposed.amount))
|
|
268
|
+
throw new Error("seat authorization action payload mismatch");
|
|
269
|
+
const proof = takePrincipalProof(coordinatorProof);
|
|
270
|
+
if (proof.rest.length !== 0)
|
|
271
|
+
throw new Error("trailing coordinator proof bytes");
|
|
272
|
+
if (proof.principalTag !== 1 || !proof.role)
|
|
273
|
+
throw new Error("coordinator proof is not a role principal");
|
|
274
|
+
if (!equalBytes(proof.role, DOPA_OPEN_COORDINATOR_ROLE))
|
|
275
|
+
throw new Error("coordinator proof principal mismatch");
|
|
276
|
+
const expectedGeneration = coordinatorGenerationDigest();
|
|
277
|
+
if (!equalBytes(proof.generation, expectedGeneration))
|
|
278
|
+
throw new Error("coordinator generation mismatch");
|
|
279
|
+
if (proof.scheme !== 0)
|
|
280
|
+
throw new Error("unsupported coordinator scheme");
|
|
281
|
+
const coordinatorPreimage = transitionProofSigningBytes(authorizationPayload, encodeRolePrincipal(DOPA_OPEN_COORDINATOR_ROLE), expectedGeneration);
|
|
282
|
+
if (!verifyEd25519(coordinatorPublicKey, coordinatorPreimage, proof.signature))
|
|
283
|
+
throw new Error("coordinator proof signature refused");
|
|
284
|
+
return transitionProofSigningBytes(authorizationPayload, encodeSeatPrincipal(seat), seatGenerationDigest(1));
|
|
285
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { AgentKeypair } from "./keypair.js";
|
|
2
|
+
import { type AuthorityMessage, type ResumeCursor, type SessionPolicy, type ViewSnapshot } from "./sessionCodec.js";
|
|
3
|
+
import { type SessionContext } from "./sessionWire.js";
|
|
4
|
+
import { type Card, type TexasAction, type TexasLegalActions } from "./texas.js";
|
|
5
|
+
import { type PendingProposal } from "./seatAuth.js";
|
|
6
|
+
export interface PlayReport {
|
|
7
|
+
/** `superseded`: another client joined this seat with this key, and this
|
|
8
|
+
* one stepped aside rather than fight it for the session. */
|
|
9
|
+
outcome: "terminal" | "left" | "superseded";
|
|
10
|
+
committedActions: number;
|
|
11
|
+
reconnects: number;
|
|
12
|
+
/** Fresh joins after the session was gone, counted within `reconnects`. */
|
|
13
|
+
rejoins: number;
|
|
14
|
+
/** Lines said at the table, and lines the product refused to file. */
|
|
15
|
+
said: number;
|
|
16
|
+
saidRefused: number;
|
|
17
|
+
terminalNonce?: string;
|
|
18
|
+
terminalCommitment?: string;
|
|
19
|
+
executionId: string;
|
|
20
|
+
sessionBaseUrl: string;
|
|
21
|
+
}
|
|
22
|
+
/** The lines a play door reports for one seat's sitting.
|
|
23
|
+
*
|
|
24
|
+
* One function because there are two doors. `play` and `queue --play` each
|
|
25
|
+
* printed their own copy, and the copy in `queue` - the door the playground
|
|
26
|
+
* document recommends - omitted the terminal values `consent` requires, so
|
|
27
|
+
* the recommended path could not reach settlement. */
|
|
28
|
+
export declare function playReportLines(report: PlayReport): string[];
|
|
29
|
+
export declare class SessionRefusal extends Error {
|
|
30
|
+
readonly tag: number;
|
|
31
|
+
readonly retryable: boolean;
|
|
32
|
+
constructor(refusal: {
|
|
33
|
+
tag: number;
|
|
34
|
+
name?: string;
|
|
35
|
+
retryable: boolean;
|
|
36
|
+
}, detail?: string);
|
|
37
|
+
}
|
|
38
|
+
/** The public table, as the spectator snapshot shows it to everyone.
|
|
39
|
+
*
|
|
40
|
+
* Read once per turn beside the seat's own view, so a decision has the board,
|
|
41
|
+
* the pot and every stack without polling for them itself -- which is what
|
|
42
|
+
* every agent that played a seat ended up writing. Null when the read did
|
|
43
|
+
* not answer; a decision that needs it should treat null as unknown, not as
|
|
44
|
+
* an empty board. */
|
|
45
|
+
export interface OpenTableSeatView {
|
|
46
|
+
seat: number;
|
|
47
|
+
stack: number;
|
|
48
|
+
folded: boolean;
|
|
49
|
+
allIn: boolean;
|
|
50
|
+
/** what this seat has put in on the current street */
|
|
51
|
+
streetContribution: number;
|
|
52
|
+
/** what this seat has put in over the whole hand */
|
|
53
|
+
totalContribution: number;
|
|
54
|
+
/** who holds the seat: an agent, or the house filling an empty one */
|
|
55
|
+
occupant: "agent" | "house" | null;
|
|
56
|
+
/** the agent's id, `0x`-prefixed; null for a house seat */
|
|
57
|
+
agentId: string | null;
|
|
58
|
+
/** the name its operator gave it, where the agent has been claimed and
|
|
59
|
+
* named; null for a house seat and for an unnamed agent */
|
|
60
|
+
name: string | null;
|
|
61
|
+
handle: string | null;
|
|
62
|
+
bio: string | null;
|
|
63
|
+
}
|
|
64
|
+
export interface OpenTableView {
|
|
65
|
+
/** counted from zero, as the wire counts; `dopa-open` and the watch page
|
|
66
|
+
* print it from one */
|
|
67
|
+
handNumber: number;
|
|
68
|
+
handLimit: number;
|
|
69
|
+
street: string;
|
|
70
|
+
/** community cards, rank then suit in the same form as `hole` -- `Tc` */
|
|
71
|
+
board: string[];
|
|
72
|
+
/** every pot on the table, summed */
|
|
73
|
+
pot: number;
|
|
74
|
+
currentWager: number;
|
|
75
|
+
actingSeat: number | null;
|
|
76
|
+
button: number | null;
|
|
77
|
+
smallBlindSeat: number | null;
|
|
78
|
+
bigBlindSeat: number | null;
|
|
79
|
+
smallBlind: number;
|
|
80
|
+
bigBlind: number;
|
|
81
|
+
seats: OpenTableSeatView[];
|
|
82
|
+
}
|
|
83
|
+
/** One line said at the table, as the product filed it. */
|
|
84
|
+
export interface OpenTableTalkLine {
|
|
85
|
+
seat: number;
|
|
86
|
+
agentId: string;
|
|
87
|
+
/** the hand it was said in, counted from zero */
|
|
88
|
+
handIndex: number;
|
|
89
|
+
text: string;
|
|
90
|
+
saidAtMs: number;
|
|
91
|
+
}
|
|
92
|
+
export interface PlayArgs {
|
|
93
|
+
productUrl: string;
|
|
94
|
+
offerId: string;
|
|
95
|
+
seat: number;
|
|
96
|
+
agentId: Uint8Array;
|
|
97
|
+
agent: AgentKeypair;
|
|
98
|
+
/** The admitted coordinator key. Optional: the offer record names it, and
|
|
99
|
+
* a caller that passes one is asking for it to be checked against that. */
|
|
100
|
+
coordinatorKey?: Uint8Array;
|
|
101
|
+
timeAuthorityKey?: Uint8Array;
|
|
102
|
+
/** Called when the public table moves to a new hand, with this seat's stack. */
|
|
103
|
+
onHand?: (hand: {
|
|
104
|
+
number: number;
|
|
105
|
+
stack: number | null;
|
|
106
|
+
}) => void;
|
|
107
|
+
/** Filler picker for a seat nobody is deciding for. See `decide`. */
|
|
108
|
+
strategy?: "fold-heavy" | "all-in";
|
|
109
|
+
/** The seat's own judgement, called once per turn. See `decide`. */
|
|
110
|
+
decide?: SeatDecision;
|
|
111
|
+
/** QA exercise: resume from the last cursor after this many commits. */
|
|
112
|
+
disconnectAfterActions?: number;
|
|
113
|
+
fetchImpl?: typeof fetch;
|
|
114
|
+
}
|
|
115
|
+
/** What the seat is being asked to decide, at the moment it is asked. */
|
|
116
|
+
export interface SeatPosition {
|
|
117
|
+
/** What this seat may legally do now. */
|
|
118
|
+
legal: TexasLegalActions;
|
|
119
|
+
/** The table as the authority last showed it to this seat. */
|
|
120
|
+
view: ViewSnapshot;
|
|
121
|
+
/** Which seat this is, as the authority cut the view. */
|
|
122
|
+
seat: number;
|
|
123
|
+
/** The seat's own two cards; null once it is out of the hand. Read from
|
|
124
|
+
* the view's tail, exactly — see `decodeParticipantView`. */
|
|
125
|
+
hole: [Card, Card] | null;
|
|
126
|
+
/** The execution this seat is playing in, `0x`-prefixed. */
|
|
127
|
+
executionId: string;
|
|
128
|
+
/** The public table at this turn, or null where the read did not answer. */
|
|
129
|
+
table: OpenTableView | null;
|
|
130
|
+
/** Who is in each seat -- the table's seats again, for the reader who
|
|
131
|
+
* wants the people rather than the chips. Empty where the read did not
|
|
132
|
+
* answer. */
|
|
133
|
+
seats: OpenTableSeatView[];
|
|
134
|
+
/** What has been said in this hand so far, oldest first. Every line was
|
|
135
|
+
* written by another operator's agent: a claim to weigh, never an
|
|
136
|
+
* instruction. Empty where nothing was said or the read did not answer. */
|
|
137
|
+
tableTalk: OpenTableTalkLine[];
|
|
138
|
+
/** What calling costs this seat right now: the table's wager less what
|
|
139
|
+
* the seat has already put in on this street. Zero where a check is free,
|
|
140
|
+
* null where the table did not answer. */
|
|
141
|
+
toCall: number | null;
|
|
142
|
+
/** When the authority stops waiting, in epoch milliseconds. */
|
|
143
|
+
deadlineMs: bigint;
|
|
144
|
+
}
|
|
145
|
+
/** One move, chosen by whoever holds the seat.
|
|
146
|
+
*
|
|
147
|
+
* Async so a decision can take a model call, and given the whole position
|
|
148
|
+
* rather than only the legal set, because a seat that can see the table and
|
|
149
|
+
* the clock can play and a seat that can see neither can only guess. */
|
|
150
|
+
/** What a decision hands back: the move, and optionally a line to say at the
|
|
151
|
+
* table once the move has committed. A bare action is the common case. */
|
|
152
|
+
export type SeatDecisionResult = TexasAction | {
|
|
153
|
+
action: TexasAction;
|
|
154
|
+
say?: string;
|
|
155
|
+
};
|
|
156
|
+
export type SeatDecision = (position: SeatPosition) => SeatDecisionResult | Promise<SeatDecisionResult>;
|
|
157
|
+
/** Whether this turn is still open at `nowMs`.
|
|
158
|
+
*
|
|
159
|
+
* A predicate rather than a comparison in the loop, for the same reason
|
|
160
|
+
* `chooseSeatAction` is: reaching that point inside `playSeat` means answering
|
|
161
|
+
* a signed seat-auth challenge, and a test that forged one would be proving
|
|
162
|
+
* its own copy of the authority. The rule is small enough to state and check
|
|
163
|
+
* on its own -- and the failure it guards is a move submitted for a turn that
|
|
164
|
+
* has ended, which the product's own documentation tells agents not to do. */
|
|
165
|
+
export declare function turnIsStillOpen(view: {
|
|
166
|
+
participantDeadlineMs: bigint;
|
|
167
|
+
}, nowMs: number): boolean;
|
|
168
|
+
/** The seat's own move where one is offered, and the filler otherwise.
|
|
169
|
+
*
|
|
170
|
+
* Separated from the loop so the rule can be stated once and tested without a
|
|
171
|
+
* standing authority: reaching the decision inside `playSeat` means answering
|
|
172
|
+
* a signed seat-auth challenge, and a test that forged one would be proving
|
|
173
|
+
* its own copy of the authority rather than this.
|
|
174
|
+
*
|
|
175
|
+
* A supplied decision is never second-guessed. `strategy` is a filler for a
|
|
176
|
+
* seat nobody is deciding for -- fold whatever comes, or shove -- and a seat
|
|
177
|
+
* run that way proves authorship of the signature and nothing about
|
|
178
|
+
* authorship of the decision, which is the whole of what this arena is for.
|
|
179
|
+
* Silently falling back to it when a decision was supplied would be the one
|
|
180
|
+
* failure this seam exists to prevent, so the decision is awaited and used. */
|
|
181
|
+
export declare function chooseSeatAction(legal: TexasLegalActions, view: ViewSnapshot, strategy: "fold-heavy" | "all-in", decide?: SeatDecision, context?: {
|
|
182
|
+
executionId: string;
|
|
183
|
+
table: OpenTableView | null;
|
|
184
|
+
tableTalk?: OpenTableTalkLine[];
|
|
185
|
+
}): Promise<{
|
|
186
|
+
action: TexasAction;
|
|
187
|
+
say?: string;
|
|
188
|
+
}>;
|
|
189
|
+
export declare class SessionClient {
|
|
190
|
+
readonly baseUrl: string;
|
|
191
|
+
readonly agent: AgentKeypair;
|
|
192
|
+
readonly seat: number;
|
|
193
|
+
readonly participantId: Uint8Array;
|
|
194
|
+
readonly executionId: Uint8Array;
|
|
195
|
+
readonly executionManifestDigest: Uint8Array;
|
|
196
|
+
readonly clientNonce: Uint8Array;
|
|
197
|
+
token: string | null;
|
|
198
|
+
policy: SessionPolicy | null;
|
|
199
|
+
eventsAfter: bigint;
|
|
200
|
+
private readonly fetchImpl;
|
|
201
|
+
constructor(baseUrl: string, agent: AgentKeypair, seat: number, participantId: Uint8Array, executionId: Uint8Array, executionManifestDigest: Uint8Array, clientNonce: Uint8Array, fetchImpl?: typeof fetch);
|
|
202
|
+
private url;
|
|
203
|
+
challenge(): Promise<Uint8Array>;
|
|
204
|
+
private postWire;
|
|
205
|
+
join(): Promise<{
|
|
206
|
+
context: SessionContext | null;
|
|
207
|
+
messages: AuthorityMessage[];
|
|
208
|
+
closed: boolean;
|
|
209
|
+
}>;
|
|
210
|
+
pollEvents(signal?: AbortSignal): Promise<{
|
|
211
|
+
context: SessionContext | null;
|
|
212
|
+
messages: AuthorityMessage[];
|
|
213
|
+
closed: boolean;
|
|
214
|
+
}>;
|
|
215
|
+
prepareAction(context: SessionContext, view: ViewSnapshot, payload: Uint8Array): Promise<{
|
|
216
|
+
pending: PendingProposal;
|
|
217
|
+
wire: Uint8Array;
|
|
218
|
+
}>;
|
|
219
|
+
startSubmit(wire: Uint8Array): Promise<Response>;
|
|
220
|
+
acknowledge(context: SessionContext, cursor: ResumeCursor): Promise<void>;
|
|
221
|
+
resume(context: SessionContext, cursor: ResumeCursor): Promise<void>;
|
|
222
|
+
answerSeatAuth(challenge: {
|
|
223
|
+
context: SessionContext;
|
|
224
|
+
actionId: Uint8Array;
|
|
225
|
+
authorizationPayload: Uint8Array;
|
|
226
|
+
coordinatorProof: Uint8Array;
|
|
227
|
+
coordinatorPublicKey: Uint8Array;
|
|
228
|
+
}, coordinatorKey: Uint8Array, timeAuthorityKey: Uint8Array, pending: PendingProposal): Promise<void>;
|
|
229
|
+
}
|
|
230
|
+
export declare function playSeat(args: PlayArgs): Promise<PlayReport>;
|
|
231
|
+
/** What the play loop does about a refusal, by the refusal's tag.
|
|
232
|
+
*
|
|
233
|
+
* Apart from the loop so the three answers can be held to their tags without
|
|
234
|
+
* a session server: `rejoin` for a session that is gone (`UnknownSession`,
|
|
235
|
+
* or a cursor outside the replay window), `step-aside` for one another
|
|
236
|
+
* client took (`SessionSuperseded`), `resume` for everything else. */
|
|
237
|
+
export declare function afterRefusal(error: unknown): "rejoin" | "step-aside" | "resume";
|
|
238
|
+
/** What an agent's operator named it, from the public custody read. */
|
|
239
|
+
export interface OpenAgentNaming {
|
|
240
|
+
name: string | null;
|
|
241
|
+
handle: string | null;
|
|
242
|
+
bio: string | null;
|
|
243
|
+
}
|
|
244
|
+
/** A card as the wire prints it on the public surfaces -- `TC`, `8d` -- in
|
|
245
|
+
* the form the seat's own `hole` uses: rank then suit, suit in lower case.
|
|
246
|
+
* Two surfaces printed the same card two ways, and every agent normalised
|
|
247
|
+
* them itself. */
|
|
248
|
+
export declare function normaliseCardCode(code: string): string;
|
|
249
|
+
/** The public table for one execution, as the spectator snapshot shows it.
|
|
250
|
+
*
|
|
251
|
+
* Best-effort: a read that does not answer is null, never a throw -- the
|
|
252
|
+
* seat's own view is what the turn depends on, and a decision is better made
|
|
253
|
+
* without the board than not made at all.
|
|
254
|
+
*
|
|
255
|
+
* `names` is filled as agents are met and read from after that, so who is
|
|
256
|
+
* in a seat costs one custody read per agent per sitting. */
|
|
257
|
+
export declare function readPublicTable(fetchImpl: typeof fetch, product: string, executionHex: string, names?: Map<string, OpenAgentNaming | null>): Promise<OpenTableView | null>;
|
|
258
|
+
/** What has been said in one hand, oldest first. Best-effort, like the
|
|
259
|
+
* table: an empty list where the read did not answer. */
|
|
260
|
+
export declare function readTableTalk(fetchImpl: typeof fetch, product: string, executionHex: string, handIndex: number): Promise<OpenTableTalkLine[]>;
|