@swarm-tips/client 0.1.1 → 0.2.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.
Files changed (35) hide show
  1. package/README.md +5 -0
  2. package/SBOM.spdx.json +5 -5
  3. package/dist/coordination-game/api.d.ts +276 -0
  4. package/dist/coordination-game/api.js +146 -0
  5. package/dist/coordination-game/evm/abi.d.ts +409 -0
  6. package/dist/coordination-game/evm/abi.js +371 -0
  7. package/dist/coordination-game/evm/cross-chain.d.ts +33 -0
  8. package/dist/coordination-game/evm/cross-chain.js +76 -0
  9. package/dist/coordination-game/evm/index.d.ts +6 -0
  10. package/dist/coordination-game/evm/index.js +6 -0
  11. package/dist/coordination-game/evm/rules.d.ts +112 -0
  12. package/dist/coordination-game/evm/rules.js +128 -0
  13. package/dist/coordination-game/evm/session-funding.d.ts +111 -0
  14. package/dist/coordination-game/evm/session-funding.js +407 -0
  15. package/dist/coordination-game/evm/session.d.ts +58 -0
  16. package/dist/coordination-game/evm/session.js +165 -0
  17. package/dist/coordination-game/evm/transactions.d.ts +137 -0
  18. package/dist/coordination-game/evm/transactions.js +211 -0
  19. package/dist/coordination-game/index.d.ts +1 -0
  20. package/dist/coordination-game/index.js +1 -0
  21. package/dist/coordination-game/protocol.d.ts +58 -0
  22. package/dist/coordination-game/protocol.js +120 -0
  23. package/dist/coordination-game/solana.d.ts +84 -0
  24. package/dist/coordination-game/solana.js +262 -0
  25. package/dist/errors.d.ts +19 -0
  26. package/dist/errors.js +20 -0
  27. package/dist/http.d.ts +21 -0
  28. package/dist/http.js +101 -0
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -1
  31. package/dist/shillbot/api.d.ts +72 -0
  32. package/dist/shillbot/api.js +91 -0
  33. package/dist/swarm/index.d.ts +84 -0
  34. package/dist/swarm/index.js +71 -0
  35. package/package.json +26 -1
@@ -0,0 +1,120 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import { COORDINATION_GAME_PROGRAM_ID } from "../contracts/index.js";
3
+ const encoder = new TextEncoder();
4
+ export const COORDINATION_GAME_TOURNAMENT_ID = 1n;
5
+ export const GUESS_UNREVEALED = 255;
6
+ export const REVEAL_TIMEOUT_SLOTS = 14400n;
7
+ export function toGuessBit(value) {
8
+ if (value === 0 || value === 1)
9
+ return value;
10
+ throw new RangeError(`Expected 0 or 1, got ${value}`);
11
+ }
12
+ export function u64LE(value) {
13
+ if (value < 0n || value > 0xffffffffffffffffn) {
14
+ throw new RangeError("u64 value is out of range");
15
+ }
16
+ const bytes = new Uint8Array(8);
17
+ let remaining = value;
18
+ for (let index = 0; index < bytes.length; index += 1) {
19
+ bytes[index] = Number(remaining & 0xffn);
20
+ remaining >>= 8n;
21
+ }
22
+ return bytes;
23
+ }
24
+ function programId(programId) {
25
+ if (programId instanceof PublicKey)
26
+ return programId;
27
+ return programId
28
+ ? new PublicKey(programId)
29
+ : COORDINATION_GAME_PROGRAM_ID;
30
+ }
31
+ export function globalConfigPda(id) {
32
+ return PublicKey.findProgramAddressSync([encoder.encode("global_config")], programId(id));
33
+ }
34
+ export function gameCounterPda(id) {
35
+ return PublicKey.findProgramAddressSync([encoder.encode("game_counter")], programId(id));
36
+ }
37
+ export function gamePda(gameId, id) {
38
+ return PublicKey.findProgramAddressSync([encoder.encode("game"), u64LE(gameId)], programId(id));
39
+ }
40
+ export function tournamentPda(tournamentId, id) {
41
+ return PublicKey.findProgramAddressSync([encoder.encode("tournament"), u64LE(tournamentId)], programId(id));
42
+ }
43
+ export function playerProfilePda(tournamentId, wallet, id) {
44
+ return PublicKey.findProgramAddressSync([encoder.encode("player"), u64LE(tournamentId), wallet.toBytes()], programId(id));
45
+ }
46
+ export function escrowPda(tournamentId, wallet, id) {
47
+ return PublicKey.findProgramAddressSync([encoder.encode("escrow"), u64LE(tournamentId), wallet.toBytes()], programId(id));
48
+ }
49
+ export function playerSessionPda(wallet, session, id) {
50
+ return PublicKey.findProgramAddressSync([encoder.encode("game_session"), wallet.toBytes(), session.toBytes()], programId(id));
51
+ }
52
+ function viewOf(data) {
53
+ return new DataView(data.buffer, data.byteOffset, data.byteLength);
54
+ }
55
+ export function decodeGlobalConfig(data) {
56
+ const stakeOffset = 107;
57
+ if (data.length < stakeOffset + 8) {
58
+ throw new RangeError(`GlobalConfig is the pre-migration ${data.length}-byte layout and carries no stake_lamports`);
59
+ }
60
+ return {
61
+ authority: new PublicKey(data.subarray(8, 40)),
62
+ matchmaker: new PublicKey(data.subarray(40, 72)),
63
+ treasury: new PublicKey(data.subarray(72, 104)),
64
+ treasurySplitBps: data[104] | (data[105] << 8),
65
+ stakeLamports: new DataView(data.buffer, data.byteOffset + stakeOffset, 8).getBigUint64(0, true),
66
+ };
67
+ }
68
+ export function decodeTournament(data) {
69
+ const minimumLength = 8 + 8 + 32 + 8 + 8 + 8 + 8 + 1;
70
+ if (data.length < minimumLength) {
71
+ throw new RangeError(`Tournament account data too short: ${data.length} bytes`);
72
+ }
73
+ const view = viewOf(data);
74
+ return {
75
+ tournamentId: view.getBigUint64(8, true),
76
+ startTime: view.getBigInt64(48, true),
77
+ endTime: view.getBigInt64(56, true),
78
+ prizeLamports: view.getBigUint64(64, true),
79
+ gameCount: view.getBigUint64(72, true),
80
+ finalized: data[80] !== 0,
81
+ };
82
+ }
83
+ export function decodeTournamentStats(data) {
84
+ const tournament = decodeTournament(data);
85
+ return { gameCount: Number(tournament.gameCount), prizeLamports: tournament.prizeLamports };
86
+ }
87
+ function hasCommitted(commit) {
88
+ return Array.from(commit).some((byte) => byte !== 0);
89
+ }
90
+ export function deriveSolanaResumeState(game, wallet) {
91
+ if (!["active", "committing", "revealing"].includes(game.state))
92
+ return null;
93
+ const iAmP1 = game.playerOne === wallet;
94
+ const iAmP2 = game.playerTwo === wallet;
95
+ if (!iAmP1 && !iAmP2)
96
+ return null;
97
+ if ((iAmP1 ? game.p1Guess : game.p2Guess) !== GUESS_UNREVEALED)
98
+ return null;
99
+ const mine = hasCommitted(iAmP1 ? game.p1Commit : game.p2Commit);
100
+ const bothCommitted = hasCommitted(game.p1Commit) && hasCommitted(game.p2Commit);
101
+ return {
102
+ phase: !mine ? "active" : bothCommitted ? "revealing" : "committing",
103
+ iAmP1,
104
+ bothCommitted,
105
+ };
106
+ }
107
+ export function timeoutDeadlineSlot(game) {
108
+ const later = game.p1CommitSlot > game.p2CommitSlot ? game.p1CommitSlot : game.p2CommitSlot;
109
+ if (game.state === "active")
110
+ return game.activatedAtSlot + game.commitTimeoutSlots;
111
+ if (game.state === "committing")
112
+ return later + game.commitTimeoutSlots;
113
+ if (game.state === "revealing")
114
+ return later + REVEAL_TIMEOUT_SLOTS;
115
+ return null;
116
+ }
117
+ export function timeoutClaimableAtSlot(game, currentSlot) {
118
+ const deadline = timeoutDeadlineSlot(game);
119
+ return deadline !== null && currentSlot >= deadline;
120
+ }
@@ -0,0 +1,84 @@
1
+ import { BN, Program } from "@coral-xyz/anchor";
2
+ import { Connection, Keypair, PublicKey, Transaction, type Commitment, type TransactionInstruction, type VersionedTransaction } from "@solana/web3.js";
3
+ import { type CoordinationGame } from "../contracts/index.js";
4
+ import { type GlobalConfigData, type TournamentData } from "./protocol.js";
5
+ export interface SolanaWalletSigner {
6
+ publicKey: PublicKey;
7
+ signTransaction<T extends Transaction | VersionedTransaction>(transaction: T): Promise<T>;
8
+ signAllTransactions?<T extends Transaction | VersionedTransaction>(transactions: T[]): Promise<T[]>;
9
+ }
10
+ export interface SolanaSessionStorage {
11
+ getItem(key: string): string | null;
12
+ setItem(key: string, value: string): void;
13
+ removeItem(key: string): void;
14
+ }
15
+ export interface CoordinationGameSolanaClientOptions {
16
+ connection?: Connection;
17
+ rpcUrl?: string;
18
+ wallet?: SolanaWalletSigner;
19
+ tournamentId?: bigint;
20
+ commitment?: Commitment;
21
+ }
22
+ export declare function generateSessionKeypair(): Keypair;
23
+ export declare function readStoredSession(storage: SolanaSessionStorage, walletAddress: string, now?: () => number): Keypair | null;
24
+ export declare function storeSession(storage: SolanaSessionStorage, session: Keypair, walletAddress: string, now?: () => number): void;
25
+ export declare function clearSession(storage: SolanaSessionStorage): void;
26
+ export declare class CoordinationGameSolanaClient {
27
+ readonly connection: Connection;
28
+ readonly wallet?: SolanaWalletSigner;
29
+ readonly tournamentId: bigint;
30
+ readonly commitment: Commitment;
31
+ constructor(options: CoordinationGameSolanaClientOptions);
32
+ program(wallet?: SolanaWalletSigner | undefined): Program<CoordinationGame>;
33
+ sessionProgram(session: Keypair): Program<CoordinationGame>;
34
+ private requireWallet;
35
+ liveStakeRemainingAccounts(): {
36
+ pubkey: PublicKey;
37
+ isSigner: boolean;
38
+ isWritable: boolean;
39
+ }[];
40
+ fetchTournament(tournamentId?: bigint): Promise<TournamentData | null>;
41
+ fetchGlobalConfig(): Promise<GlobalConfigData>;
42
+ buildCreatePlayerSessionInstruction(sessionPublicKey: PublicKey): Promise<TransactionInstruction>;
43
+ depositStake(tournament: PublicKey, session?: Keypair | null): Promise<string>;
44
+ buildCreateGameTransaction(input: {
45
+ stake: BN;
46
+ matchupCommitment: number[];
47
+ tournament: PublicKey;
48
+ escrow: PublicKey;
49
+ globalConfig: PublicKey;
50
+ matchmaker: PublicKey;
51
+ playerProfile: PublicKey;
52
+ gameCounter: PublicKey;
53
+ game: PublicKey;
54
+ session?: Keypair;
55
+ player?: PublicKey;
56
+ }): Promise<Transaction>;
57
+ buildJoinGameTransaction(input: {
58
+ game: PublicKey;
59
+ tournament: PublicKey;
60
+ escrow: PublicKey;
61
+ preInstructions?: TransactionInstruction[];
62
+ session?: Keypair;
63
+ }): Promise<{
64
+ transaction: Transaction;
65
+ payer: PublicKey;
66
+ matchmaker: PublicKey;
67
+ }>;
68
+ applyCosignature(transaction: Transaction, signer: PublicKey, signatureBase64: string): Promise<Transaction>;
69
+ messageBase64(transaction: Transaction): string;
70
+ signBroadcastAndConfirm(transaction: Transaction, session?: Keypair): Promise<string>;
71
+ commitGuess(commitment: number[], game: PublicKey, session?: Keypair | null): Promise<string>;
72
+ revealGuess(r: number[], rMatchup: number[] | null, accounts: Record<string, PublicKey>, session?: Keypair | null): Promise<string>;
73
+ closeGame(game: PublicKey, session?: Keypair | null): Promise<string>;
74
+ resolveTimeout(gameId: bigint, session?: Keypair | null): Promise<string>;
75
+ closeSessionByDelegate(session: Keypair, player: PublicKey): Promise<string>;
76
+ deriveAddresses(gameId: bigint, player: PublicKey): {
77
+ globalConfig: PublicKey;
78
+ gameCounter: PublicKey;
79
+ game: PublicKey;
80
+ tournament: PublicKey;
81
+ playerProfile: PublicKey;
82
+ escrow: PublicKey;
83
+ };
84
+ }
@@ -0,0 +1,262 @@
1
+ import { AnchorProvider, Program } from "@coral-xyz/anchor";
2
+ import { Buffer } from "buffer";
3
+ import { Connection, Keypair, SystemProgram, Transaction, } from "@solana/web3.js";
4
+ import { COORDINATION_GAME_IDL } from "../contracts/index.js";
5
+ import { SwarmClientError } from "../errors.js";
6
+ import { decodeGlobalConfig, decodeTournament, escrowPda, gameCounterPda, gamePda, globalConfigPda, playerProfilePda, playerSessionPda, tournamentPda, } from "./protocol.js";
7
+ const SESSION_STORAGE_KEY = "coordination-session";
8
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
9
+ function walletForSession(session) {
10
+ return {
11
+ publicKey: session.publicKey,
12
+ async signTransaction(transaction) {
13
+ if (transaction instanceof Transaction)
14
+ transaction.partialSign(session);
15
+ else
16
+ transaction.sign([session]);
17
+ return transaction;
18
+ },
19
+ async signAllTransactions(transactions) {
20
+ for (const transaction of transactions)
21
+ await this.signTransaction(transaction);
22
+ return transactions;
23
+ },
24
+ };
25
+ }
26
+ function readonlyWallet() {
27
+ const keypair = Keypair.generate();
28
+ return {
29
+ publicKey: keypair.publicKey,
30
+ signTransaction: async (transaction) => transaction,
31
+ signAllTransactions: async (transactions) => transactions,
32
+ };
33
+ }
34
+ function anchorProgram(connection, wallet, commitment) {
35
+ const provider = new AnchorProvider(connection, wallet, { commitment, skipPreflight: false });
36
+ return new Program(COORDINATION_GAME_IDL, provider);
37
+ }
38
+ export function generateSessionKeypair() { return Keypair.generate(); }
39
+ export function readStoredSession(storage, walletAddress, now = Date.now) {
40
+ const raw = storage.getItem(SESSION_STORAGE_KEY);
41
+ if (!raw)
42
+ return null;
43
+ try {
44
+ const value = JSON.parse(raw);
45
+ if (!Array.isArray(value.secretKey) || value.secretKey.length !== 64 || typeof value.createdAt !== "number" || value.wallet !== walletAddress || now() - value.createdAt > SESSION_TTL_MS) {
46
+ storage.removeItem(SESSION_STORAGE_KEY);
47
+ return null;
48
+ }
49
+ return Keypair.fromSecretKey(Uint8Array.from(value.secretKey));
50
+ }
51
+ catch {
52
+ storage.removeItem(SESSION_STORAGE_KEY);
53
+ return null;
54
+ }
55
+ }
56
+ export function storeSession(storage, session, walletAddress, now = Date.now) {
57
+ storage.setItem(SESSION_STORAGE_KEY, JSON.stringify({
58
+ secretKey: Array.from(session.secretKey),
59
+ createdAt: now(),
60
+ wallet: walletAddress,
61
+ }));
62
+ }
63
+ export function clearSession(storage) {
64
+ storage.removeItem(SESSION_STORAGE_KEY);
65
+ }
66
+ export class CoordinationGameSolanaClient {
67
+ connection;
68
+ wallet;
69
+ tournamentId;
70
+ commitment;
71
+ constructor(options) {
72
+ if (!options.connection && !options.rpcUrl) {
73
+ throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.constructor", message: "connection or rpcUrl is required" });
74
+ }
75
+ this.connection = options.connection ?? new Connection(options.rpcUrl, options.commitment ?? "confirmed");
76
+ this.wallet = options.wallet;
77
+ this.tournamentId = options.tournamentId ?? 1n;
78
+ this.commitment = options.commitment ?? "confirmed";
79
+ }
80
+ program(wallet = this.wallet) {
81
+ return anchorProgram(this.connection, wallet ?? readonlyWallet(), this.commitment);
82
+ }
83
+ sessionProgram(session) {
84
+ return anchorProgram(this.connection, walletForSession(session), this.commitment);
85
+ }
86
+ requireWallet() {
87
+ if (!this.wallet)
88
+ throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.wallet", message: "A wallet signer is required" });
89
+ return this.wallet;
90
+ }
91
+ liveStakeRemainingAccounts() {
92
+ return [{ pubkey: globalConfigPda()[0], isSigner: false, isWritable: false }];
93
+ }
94
+ async fetchTournament(tournamentId = this.tournamentId) {
95
+ const info = await this.connection.getAccountInfo(tournamentPda(tournamentId)[0]);
96
+ return info ? decodeTournament(new Uint8Array(info.data)) : null;
97
+ }
98
+ async fetchGlobalConfig() {
99
+ const info = await this.connection.getAccountInfo(globalConfigPda()[0]);
100
+ if (!info)
101
+ throw new SwarmClientError({ code: "INVALID_RESPONSE", operation: "solana.fetchGlobalConfig", message: "GlobalConfig account not found on-chain" });
102
+ return decodeGlobalConfig(new Uint8Array(info.data));
103
+ }
104
+ async buildCreatePlayerSessionInstruction(sessionPublicKey) {
105
+ const wallet = this.requireWallet();
106
+ return this.program(wallet).methods.createPlayerSession().accountsPartial({ sessionKey: sessionPublicKey }).instruction();
107
+ }
108
+ async depositStake(tournament, session) {
109
+ const wallet = this.requireWallet();
110
+ const escrow = escrowPda(this.tournamentId, wallet.publicKey)[0];
111
+ if (session) {
112
+ return this.sessionProgram(session).methods.depositStakeSession().accountsPartial({
113
+ player: wallet.publicKey,
114
+ sessionAuthority: playerSessionPda(wallet.publicKey, session.publicKey)[0],
115
+ tournament,
116
+ escrow,
117
+ }).remainingAccounts(this.liveStakeRemainingAccounts()).rpc();
118
+ }
119
+ return this.program(wallet).methods.depositStake().accountsPartial({ tournament, escrow }).remainingAccounts(this.liveStakeRemainingAccounts()).rpc();
120
+ }
121
+ async buildCreateGameTransaction(input) {
122
+ const wallet = this.requireWallet();
123
+ if (input.session) {
124
+ const player = input.player ?? wallet.publicKey;
125
+ return this.sessionProgram(input.session).methods.createGameSession(input.stake, input.matchupCommitment).accountsPartial({
126
+ game: input.game,
127
+ gameCounter: input.gameCounter,
128
+ playerProfile: input.playerProfile,
129
+ escrow: input.escrow,
130
+ tournament: input.tournament,
131
+ globalConfig: input.globalConfig,
132
+ matchmaker: input.matchmaker,
133
+ player,
134
+ sessionAuthority: playerSessionPda(player, input.session.publicKey)[0],
135
+ sessionSigner: input.session.publicKey,
136
+ systemProgram: SystemProgram.programId,
137
+ }).transaction();
138
+ }
139
+ return this.program(wallet).methods.createGame(input.stake, input.matchupCommitment).accountsPartial({
140
+ game: input.game,
141
+ gameCounter: input.gameCounter,
142
+ playerProfile: input.playerProfile,
143
+ escrow: input.escrow,
144
+ tournament: input.tournament,
145
+ globalConfig: input.globalConfig,
146
+ matchmaker: input.matchmaker,
147
+ player: wallet.publicKey,
148
+ systemProgram: SystemProgram.programId,
149
+ }).transaction();
150
+ }
151
+ async buildJoinGameTransaction(input) {
152
+ const wallet = this.requireWallet();
153
+ const config = await this.fetchGlobalConfig();
154
+ const globalConfig = globalConfigPda()[0];
155
+ const program = input.session ? this.sessionProgram(input.session) : this.program(wallet);
156
+ const builder = input.session
157
+ ? program.methods.joinGameSession().accountsPartial({
158
+ player: wallet.publicKey,
159
+ sessionAuthority: playerSessionPda(wallet.publicKey, input.session.publicKey)[0],
160
+ game: input.game,
161
+ tournament: input.tournament,
162
+ escrow: input.escrow,
163
+ globalConfig,
164
+ matchmaker: config.matchmaker,
165
+ })
166
+ : program.methods.joinGame().accountsPartial({
167
+ game: input.game,
168
+ tournament: input.tournament,
169
+ escrow: input.escrow,
170
+ globalConfig,
171
+ matchmaker: config.matchmaker,
172
+ });
173
+ const transaction = await builder.preInstructions(input.preInstructions ?? []).transaction();
174
+ const latest = await this.connection.getLatestBlockhash(this.commitment);
175
+ transaction.recentBlockhash = latest.blockhash;
176
+ transaction.lastValidBlockHeight = latest.lastValidBlockHeight;
177
+ const payer = input.session?.publicKey ?? wallet.publicKey;
178
+ transaction.feePayer = payer;
179
+ transaction.signatures = [
180
+ { publicKey: payer, signature: null },
181
+ { publicKey: config.matchmaker, signature: null },
182
+ ];
183
+ return { transaction, payer, matchmaker: config.matchmaker };
184
+ }
185
+ async applyCosignature(transaction, signer, signatureBase64) {
186
+ const index = transaction.signatures.findIndex((entry) => entry.publicKey.equals(signer));
187
+ if (index < 0)
188
+ throw new SwarmClientError({ code: "TRANSACTION_MISMATCH", operation: "solana.applyCosignature", message: "Cosigner is not a required signer" });
189
+ transaction.signatures[index] = { publicKey: signer, signature: Buffer.from(signatureBase64, "base64") };
190
+ return transaction;
191
+ }
192
+ messageBase64(transaction) {
193
+ return Buffer.from(transaction.serializeMessage()).toString("base64");
194
+ }
195
+ async signBroadcastAndConfirm(transaction, session) {
196
+ const wallet = this.requireWallet();
197
+ let signed;
198
+ if (session) {
199
+ transaction.partialSign(session);
200
+ signed = transaction;
201
+ }
202
+ else {
203
+ signed = await wallet.signTransaction(transaction);
204
+ }
205
+ const signature = await this.connection.sendRawTransaction(signed.serialize());
206
+ await this.connection.confirmTransaction(signature, this.commitment);
207
+ return signature;
208
+ }
209
+ async commitGuess(commitment, game, session) {
210
+ const wallet = this.requireWallet();
211
+ if (session)
212
+ return this.sessionProgram(session).methods.commitGuessSession(commitment).accountsPartial({ player: wallet.publicKey, sessionAuthority: playerSessionPda(wallet.publicKey, session.publicKey)[0], game }).rpc();
213
+ return this.program(wallet).methods.commitGuess(commitment).accountsPartial({ game }).rpc();
214
+ }
215
+ async revealGuess(r, rMatchup, accounts, session) {
216
+ const wallet = this.requireWallet();
217
+ if (session)
218
+ return this.sessionProgram(session).methods.revealGuessSession(r, rMatchup).accountsPartial({ ...accounts, player: wallet.publicKey, sessionAuthority: playerSessionPda(wallet.publicKey, session.publicKey)[0] }).rpc();
219
+ return this.program(wallet).methods.revealGuess(r, rMatchup).accountsPartial(accounts).rpc();
220
+ }
221
+ async closeGame(game, session) {
222
+ const wallet = this.requireWallet();
223
+ if (session)
224
+ return this.sessionProgram(session).methods.closeGame().accountsPartial({ game, caller: session.publicKey }).rpc();
225
+ return this.program(wallet).methods.closeGame().accountsPartial({ game }).rpc();
226
+ }
227
+ async resolveTimeout(gameId, session) {
228
+ const wallet = this.requireWallet();
229
+ const program = session ? this.sessionProgram(session) : this.program(wallet);
230
+ const gameKey = gamePda(gameId)[0];
231
+ const game = await program.account.game.fetch(gameKey);
232
+ const config = await this.fetchGlobalConfig();
233
+ const tournamentId = BigInt(game.tournamentId.toString());
234
+ return program.methods.resolveTimeout().accountsPartial({
235
+ game: gameKey,
236
+ p1Profile: playerProfilePda(tournamentId, game.playerOne)[0],
237
+ p2Profile: playerProfilePda(tournamentId, game.playerTwo)[0],
238
+ tournament: tournamentPda(tournamentId)[0],
239
+ globalConfig: globalConfigPda()[0],
240
+ treasury: config.treasury,
241
+ playerOneWallet: game.playerOne,
242
+ playerTwoWallet: game.playerTwo,
243
+ caller: session?.publicKey ?? wallet.publicKey,
244
+ }).rpc();
245
+ }
246
+ async closeSessionByDelegate(session, player) {
247
+ return this.sessionProgram(session).methods.closeSessionByDelegate().accountsPartial({
248
+ sessionAuthority: playerSessionPda(player, session.publicKey)[0],
249
+ sessionSigner: session.publicKey,
250
+ }).rpc();
251
+ }
252
+ deriveAddresses(gameId, player) {
253
+ return {
254
+ globalConfig: globalConfigPda()[0],
255
+ gameCounter: gameCounterPda()[0],
256
+ game: gamePda(gameId)[0],
257
+ tournament: tournamentPda(this.tournamentId)[0],
258
+ playerProfile: playerProfilePda(this.tournamentId, player)[0],
259
+ escrow: escrowPda(this.tournamentId, player)[0],
260
+ };
261
+ }
262
+ }
@@ -0,0 +1,19 @@
1
+ export type SwarmClientErrorCode = "INVALID_ARGUMENT" | "INVALID_RESPONSE" | "NETWORK_ERROR" | "HTTP_ERROR" | "TIMEOUT" | "TRANSACTION_MISMATCH" | "TRANSACTION_FAILED" | "STORAGE_ERROR" | "WEBSOCKET_ERROR";
2
+ export interface SwarmClientErrorOptions {
3
+ code: SwarmClientErrorCode;
4
+ operation: string;
5
+ message: string;
6
+ retryable?: boolean;
7
+ cause?: unknown;
8
+ status?: number;
9
+ }
10
+ /** Stable error boundary shared by every headless client entrypoint. */
11
+ export declare class SwarmClientError extends Error {
12
+ readonly code: SwarmClientErrorCode;
13
+ readonly operation: string;
14
+ readonly retryable: boolean;
15
+ readonly cause?: unknown;
16
+ readonly status?: number;
17
+ constructor(options: SwarmClientErrorOptions);
18
+ }
19
+ export declare function errorMessage(error: unknown, fallback: string): string;
package/dist/errors.js ADDED
@@ -0,0 +1,20 @@
1
+ /** Stable error boundary shared by every headless client entrypoint. */
2
+ export class SwarmClientError extends Error {
3
+ code;
4
+ operation;
5
+ retryable;
6
+ cause;
7
+ status;
8
+ constructor(options) {
9
+ super(options.message);
10
+ this.name = "SwarmClientError";
11
+ this.code = options.code;
12
+ this.operation = options.operation;
13
+ this.retryable = options.retryable ?? false;
14
+ this.cause = options.cause;
15
+ this.status = options.status;
16
+ }
17
+ }
18
+ export function errorMessage(error, fallback) {
19
+ return error instanceof Error && error.message ? error.message : fallback;
20
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
2
+ export interface HttpClientOptions {
3
+ baseUrl: string;
4
+ fetch?: FetchLike;
5
+ network?: string;
6
+ getToken?: () => string | null | undefined;
7
+ }
8
+ export declare class HttpClient {
9
+ readonly baseUrl: string;
10
+ readonly network?: string;
11
+ private readonly fetchImpl;
12
+ private readonly getToken?;
13
+ constructor(options: HttpClientOptions);
14
+ withNetwork(path: string): string;
15
+ request<T>(path: string, init?: RequestInit & {
16
+ token?: string | null;
17
+ }): Promise<T>;
18
+ get<T>(path: string, token?: string | null): Promise<T>;
19
+ post<T>(path: string, body?: unknown, token?: string | null): Promise<T>;
20
+ patch<T>(path: string, body?: unknown, token?: string | null): Promise<T>;
21
+ }
package/dist/http.js ADDED
@@ -0,0 +1,101 @@
1
+ import { SwarmClientError, errorMessage } from "./errors.js";
2
+ export class HttpClient {
3
+ baseUrl;
4
+ network;
5
+ fetchImpl;
6
+ getToken;
7
+ constructor(options) {
8
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
9
+ this.network = options.network;
10
+ const candidate = options.fetch ?? globalThis.fetch;
11
+ if (!candidate)
12
+ throw new SwarmClientError({
13
+ code: "INVALID_ARGUMENT",
14
+ operation: "http.constructor",
15
+ message: "A fetch implementation is required",
16
+ });
17
+ this.fetchImpl = candidate.bind(globalThis);
18
+ this.getToken = options.getToken;
19
+ }
20
+ withNetwork(path) {
21
+ if (!this.network || this.network === "mainnet")
22
+ return path;
23
+ const separator = path.includes("?") ? "&" : "?";
24
+ return `${path}${separator}network=${encodeURIComponent(this.network)}`;
25
+ }
26
+ async request(path, init = {}) {
27
+ const operation = `${init.method ?? "GET"} ${path}`;
28
+ const token = init.token ?? this.getToken?.();
29
+ const headers = {
30
+ "Content-Type": "application/json",
31
+ ...init.headers,
32
+ };
33
+ if (token)
34
+ headers.Authorization = `Bearer ${token}`;
35
+ let response;
36
+ try {
37
+ response = await this.fetchImpl(`${this.baseUrl}${this.withNetwork(path)}`, {
38
+ ...init,
39
+ headers,
40
+ });
41
+ }
42
+ catch (cause) {
43
+ throw new SwarmClientError({
44
+ code: "NETWORK_ERROR",
45
+ operation,
46
+ message: errorMessage(cause, "Network request failed"),
47
+ retryable: true,
48
+ cause,
49
+ });
50
+ }
51
+ if (!response.ok) {
52
+ const raw = await response.text().catch(() => "");
53
+ let message = `HTTP ${response.status}`;
54
+ try {
55
+ const body = JSON.parse(raw);
56
+ message = body.message ?? body.error ?? message;
57
+ }
58
+ catch {
59
+ if (raw.trim())
60
+ message = raw.slice(0, 500);
61
+ }
62
+ throw new SwarmClientError({
63
+ code: "HTTP_ERROR",
64
+ operation,
65
+ message,
66
+ retryable: response.status === 408 || response.status === 429 || response.status >= 500,
67
+ status: response.status,
68
+ });
69
+ }
70
+ if (response.status === 204)
71
+ return undefined;
72
+ try {
73
+ return (await response.json());
74
+ }
75
+ catch (cause) {
76
+ throw new SwarmClientError({
77
+ code: "INVALID_RESPONSE",
78
+ operation,
79
+ message: "Response was not valid JSON",
80
+ cause,
81
+ });
82
+ }
83
+ }
84
+ get(path, token) {
85
+ return this.request(path, { method: "GET", token });
86
+ }
87
+ post(path, body, token) {
88
+ return this.request(path, {
89
+ method: "POST",
90
+ body: body === undefined ? undefined : JSON.stringify(body),
91
+ token,
92
+ });
93
+ }
94
+ patch(path, body, token) {
95
+ return this.request(path, {
96
+ method: "PATCH",
97
+ body: body === undefined ? undefined : JSON.stringify(body),
98
+ token,
99
+ });
100
+ }
101
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  /** The unified client package version. Functional APIs live in typed subpaths. */
2
- export declare const CLIENT_VERSION: "0.1.0";
2
+ export declare const CLIENT_VERSION: "0.2.0";
3
3
  export declare const CLIENT_MANIFEST_SCHEMA: "swarm.client-manifest/v1";
4
+ export { SwarmClientError } from "./errors.js";
5
+ export type { SwarmClientErrorCode, SwarmClientErrorOptions } from "./errors.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  /** The unified client package version. Functional APIs live in typed subpaths. */
2
- export const CLIENT_VERSION = "0.1.0";
2
+ export const CLIENT_VERSION = "0.2.0";
3
3
  export const CLIENT_MANIFEST_SCHEMA = "swarm.client-manifest/v1";
4
+ export { SwarmClientError } from "./errors.js";