@swarm-tips/client 0.1.1 → 0.2.1
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 +5 -0
- package/SBOM.spdx.json +5 -5
- package/dist/coordination-game/api.d.ts +276 -0
- package/dist/coordination-game/api.js +146 -0
- package/dist/coordination-game/evm/abi.d.ts +409 -0
- package/dist/coordination-game/evm/abi.js +371 -0
- package/dist/coordination-game/evm/cross-chain.d.ts +33 -0
- package/dist/coordination-game/evm/cross-chain.js +76 -0
- package/dist/coordination-game/evm/index.d.ts +6 -0
- package/dist/coordination-game/evm/index.js +6 -0
- package/dist/coordination-game/evm/rules.d.ts +112 -0
- package/dist/coordination-game/evm/rules.js +128 -0
- package/dist/coordination-game/evm/session-funding.d.ts +111 -0
- package/dist/coordination-game/evm/session-funding.js +407 -0
- package/dist/coordination-game/evm/session.d.ts +58 -0
- package/dist/coordination-game/evm/session.js +165 -0
- package/dist/coordination-game/evm/transactions.d.ts +137 -0
- package/dist/coordination-game/evm/transactions.js +211 -0
- package/dist/coordination-game/index.d.ts +1 -0
- package/dist/coordination-game/index.js +1 -0
- package/dist/coordination-game/protocol.d.ts +58 -0
- package/dist/coordination-game/protocol.js +120 -0
- package/dist/coordination-game/solana.d.ts +100 -0
- package/dist/coordination-game/solana.js +309 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +20 -0
- package/dist/http.d.ts +21 -0
- package/dist/http.js +101 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/shillbot/api.d.ts +76 -0
- package/dist/shillbot/api.js +114 -0
- package/dist/swarm/index.d.ts +84 -0
- package/dist/swarm/index.js +71 -0
- 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,100 @@
|
|
|
1
|
+
import { BN, Program } from "@coral-xyz/anchor";
|
|
2
|
+
import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, type Commitment, 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 const SESSION_FEE_HEADROOM_LAMPORTS = 10000000;
|
|
23
|
+
export declare function sessionFundLamports(stakeLamports: bigint): number;
|
|
24
|
+
export declare function sessionBalanceFloor(stakeLamports: bigint): number;
|
|
25
|
+
export declare function generateSessionKeypair(): Keypair;
|
|
26
|
+
export declare function readStoredSession(storage: SolanaSessionStorage, walletAddress: string, now?: () => number): Keypair | null;
|
|
27
|
+
export declare function storeSession(storage: SolanaSessionStorage, session: Keypair, walletAddress: string, now?: () => number): void;
|
|
28
|
+
export declare function clearSession(storage: SolanaSessionStorage): void;
|
|
29
|
+
export declare class CoordinationGameSolanaClient {
|
|
30
|
+
readonly connection: Connection;
|
|
31
|
+
readonly wallet?: SolanaWalletSigner;
|
|
32
|
+
readonly tournamentId: bigint;
|
|
33
|
+
readonly commitment: Commitment;
|
|
34
|
+
constructor(options: CoordinationGameSolanaClientOptions);
|
|
35
|
+
program(wallet?: SolanaWalletSigner | undefined): Program<CoordinationGame>;
|
|
36
|
+
sessionProgram(session: Keypair): Program<CoordinationGame>;
|
|
37
|
+
private requireWallet;
|
|
38
|
+
liveStakeRemainingAccounts(): {
|
|
39
|
+
pubkey: PublicKey;
|
|
40
|
+
isSigner: boolean;
|
|
41
|
+
isWritable: boolean;
|
|
42
|
+
}[];
|
|
43
|
+
fetchTournament(tournamentId?: bigint): Promise<TournamentData | null>;
|
|
44
|
+
fetchGlobalConfig(): Promise<GlobalConfigData>;
|
|
45
|
+
buildCreatePlayerSessionInstruction(sessionPublicKey: PublicKey): Promise<TransactionInstruction>;
|
|
46
|
+
buildSessionSetupTransaction(input: {
|
|
47
|
+
sessionPublicKey: PublicKey;
|
|
48
|
+
lamports: number;
|
|
49
|
+
memoNonce?: string;
|
|
50
|
+
}): Promise<Transaction>;
|
|
51
|
+
waitForSignature(signature: string, options?: {
|
|
52
|
+
attempts?: number;
|
|
53
|
+
intervalMs?: number;
|
|
54
|
+
sleep?: (ms: number) => Promise<void>;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
fetchPlayerProfiles(tournamentId?: bigint): Promise<Array<{
|
|
57
|
+
account: unknown;
|
|
58
|
+
}>>;
|
|
59
|
+
depositStake(tournament: PublicKey, session?: Keypair | null): Promise<string>;
|
|
60
|
+
buildCreateGameTransaction(input: {
|
|
61
|
+
stake: BN;
|
|
62
|
+
matchupCommitment: number[];
|
|
63
|
+
tournament: PublicKey;
|
|
64
|
+
escrow: PublicKey;
|
|
65
|
+
globalConfig: PublicKey;
|
|
66
|
+
matchmaker: PublicKey;
|
|
67
|
+
playerProfile: PublicKey;
|
|
68
|
+
gameCounter: PublicKey;
|
|
69
|
+
game: PublicKey;
|
|
70
|
+
session?: Keypair;
|
|
71
|
+
player?: PublicKey;
|
|
72
|
+
}): Promise<Transaction>;
|
|
73
|
+
buildJoinGameTransaction(input: {
|
|
74
|
+
game: PublicKey;
|
|
75
|
+
tournament: PublicKey;
|
|
76
|
+
escrow: PublicKey;
|
|
77
|
+
preInstructions?: TransactionInstruction[];
|
|
78
|
+
session?: Keypair;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
transaction: Transaction;
|
|
81
|
+
payer: PublicKey;
|
|
82
|
+
matchmaker: PublicKey;
|
|
83
|
+
}>;
|
|
84
|
+
applyCosignature(transaction: Transaction, signer: PublicKey, signatureBase64: string): Promise<Transaction>;
|
|
85
|
+
messageBase64(transaction: Transaction): string;
|
|
86
|
+
signBroadcastAndConfirm(transaction: Transaction, session?: Keypair): Promise<string>;
|
|
87
|
+
commitGuess(commitment: number[], game: PublicKey, session?: Keypair | null): Promise<string>;
|
|
88
|
+
revealGuess(r: number[], rMatchup: number[] | null, accounts: Record<string, PublicKey>, session?: Keypair | null): Promise<string>;
|
|
89
|
+
closeGame(game: PublicKey, session?: Keypair | null): Promise<string>;
|
|
90
|
+
resolveTimeout(gameId: bigint, session?: Keypair | null): Promise<string>;
|
|
91
|
+
closeSessionByDelegate(session: Keypair, player: PublicKey): Promise<string>;
|
|
92
|
+
deriveAddresses(gameId: bigint, player: PublicKey): {
|
|
93
|
+
globalConfig: PublicKey;
|
|
94
|
+
gameCounter: PublicKey;
|
|
95
|
+
game: PublicKey;
|
|
96
|
+
tournament: PublicKey;
|
|
97
|
+
playerProfile: PublicKey;
|
|
98
|
+
escrow: PublicKey;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { AnchorProvider, Program } from "@coral-xyz/anchor";
|
|
2
|
+
import { Buffer } from "buffer";
|
|
3
|
+
import bs58 from "bs58";
|
|
4
|
+
import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, } from "@solana/web3.js";
|
|
5
|
+
import { COORDINATION_GAME_IDL } from "../contracts/index.js";
|
|
6
|
+
import { SwarmClientError } from "../errors.js";
|
|
7
|
+
import { decodeGlobalConfig, decodeTournament, escrowPda, gameCounterPda, gamePda, globalConfigPda, playerProfilePda, playerSessionPda, tournamentPda, u64LE, } from "./protocol.js";
|
|
8
|
+
const SESSION_STORAGE_KEY = "coordination-session";
|
|
9
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const MEMO_PROGRAM_ID = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
|
|
11
|
+
export const SESSION_FEE_HEADROOM_LAMPORTS = 10_000_000;
|
|
12
|
+
export function sessionFundLamports(stakeLamports) {
|
|
13
|
+
return Number(stakeLamports) + SESSION_FEE_HEADROOM_LAMPORTS;
|
|
14
|
+
}
|
|
15
|
+
export function sessionBalanceFloor(stakeLamports) {
|
|
16
|
+
return Number(stakeLamports) + SESSION_FEE_HEADROOM_LAMPORTS / 2;
|
|
17
|
+
}
|
|
18
|
+
function walletForSession(session) {
|
|
19
|
+
return {
|
|
20
|
+
publicKey: session.publicKey,
|
|
21
|
+
async signTransaction(transaction) {
|
|
22
|
+
if (transaction instanceof Transaction)
|
|
23
|
+
transaction.partialSign(session);
|
|
24
|
+
else
|
|
25
|
+
transaction.sign([session]);
|
|
26
|
+
return transaction;
|
|
27
|
+
},
|
|
28
|
+
async signAllTransactions(transactions) {
|
|
29
|
+
for (const transaction of transactions)
|
|
30
|
+
await this.signTransaction(transaction);
|
|
31
|
+
return transactions;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function readonlyWallet() {
|
|
36
|
+
const keypair = Keypair.generate();
|
|
37
|
+
return {
|
|
38
|
+
publicKey: keypair.publicKey,
|
|
39
|
+
signTransaction: async (transaction) => transaction,
|
|
40
|
+
signAllTransactions: async (transactions) => transactions,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function anchorProgram(connection, wallet, commitment) {
|
|
44
|
+
const provider = new AnchorProvider(connection, wallet, { commitment, skipPreflight: false });
|
|
45
|
+
return new Program(COORDINATION_GAME_IDL, provider);
|
|
46
|
+
}
|
|
47
|
+
export function generateSessionKeypair() { return Keypair.generate(); }
|
|
48
|
+
export function readStoredSession(storage, walletAddress, now = Date.now) {
|
|
49
|
+
const raw = storage.getItem(SESSION_STORAGE_KEY);
|
|
50
|
+
if (!raw)
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const value = JSON.parse(raw);
|
|
54
|
+
if (!Array.isArray(value.secretKey) || value.secretKey.length !== 64 || typeof value.createdAt !== "number" || value.wallet !== walletAddress || now() - value.createdAt > SESSION_TTL_MS) {
|
|
55
|
+
storage.removeItem(SESSION_STORAGE_KEY);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return Keypair.fromSecretKey(Uint8Array.from(value.secretKey));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
storage.removeItem(SESSION_STORAGE_KEY);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function storeSession(storage, session, walletAddress, now = Date.now) {
|
|
66
|
+
storage.setItem(SESSION_STORAGE_KEY, JSON.stringify({
|
|
67
|
+
secretKey: Array.from(session.secretKey),
|
|
68
|
+
createdAt: now(),
|
|
69
|
+
wallet: walletAddress,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
export function clearSession(storage) {
|
|
73
|
+
storage.removeItem(SESSION_STORAGE_KEY);
|
|
74
|
+
}
|
|
75
|
+
export class CoordinationGameSolanaClient {
|
|
76
|
+
connection;
|
|
77
|
+
wallet;
|
|
78
|
+
tournamentId;
|
|
79
|
+
commitment;
|
|
80
|
+
constructor(options) {
|
|
81
|
+
if (!options.connection && !options.rpcUrl) {
|
|
82
|
+
throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.constructor", message: "connection or rpcUrl is required" });
|
|
83
|
+
}
|
|
84
|
+
this.connection = options.connection ?? new Connection(options.rpcUrl, options.commitment ?? "confirmed");
|
|
85
|
+
this.wallet = options.wallet;
|
|
86
|
+
this.tournamentId = options.tournamentId ?? 1n;
|
|
87
|
+
this.commitment = options.commitment ?? "confirmed";
|
|
88
|
+
}
|
|
89
|
+
program(wallet = this.wallet) {
|
|
90
|
+
return anchorProgram(this.connection, wallet ?? readonlyWallet(), this.commitment);
|
|
91
|
+
}
|
|
92
|
+
sessionProgram(session) {
|
|
93
|
+
return anchorProgram(this.connection, walletForSession(session), this.commitment);
|
|
94
|
+
}
|
|
95
|
+
requireWallet() {
|
|
96
|
+
if (!this.wallet)
|
|
97
|
+
throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.wallet", message: "A wallet signer is required" });
|
|
98
|
+
return this.wallet;
|
|
99
|
+
}
|
|
100
|
+
liveStakeRemainingAccounts() {
|
|
101
|
+
return [{ pubkey: globalConfigPda()[0], isSigner: false, isWritable: false }];
|
|
102
|
+
}
|
|
103
|
+
async fetchTournament(tournamentId = this.tournamentId) {
|
|
104
|
+
const info = await this.connection.getAccountInfo(tournamentPda(tournamentId)[0]);
|
|
105
|
+
return info ? decodeTournament(new Uint8Array(info.data)) : null;
|
|
106
|
+
}
|
|
107
|
+
async fetchGlobalConfig() {
|
|
108
|
+
const info = await this.connection.getAccountInfo(globalConfigPda()[0]);
|
|
109
|
+
if (!info)
|
|
110
|
+
throw new SwarmClientError({ code: "INVALID_RESPONSE", operation: "solana.fetchGlobalConfig", message: "GlobalConfig account not found on-chain" });
|
|
111
|
+
return decodeGlobalConfig(new Uint8Array(info.data));
|
|
112
|
+
}
|
|
113
|
+
async buildCreatePlayerSessionInstruction(sessionPublicKey) {
|
|
114
|
+
const wallet = this.requireWallet();
|
|
115
|
+
return this.program(wallet).methods.createPlayerSession().accountsPartial({ sessionKey: sessionPublicKey }).instruction();
|
|
116
|
+
}
|
|
117
|
+
async buildSessionSetupTransaction(input) {
|
|
118
|
+
const wallet = this.requireWallet();
|
|
119
|
+
const transaction = new Transaction().add(SystemProgram.transfer({
|
|
120
|
+
fromPubkey: wallet.publicKey,
|
|
121
|
+
toPubkey: input.sessionPublicKey,
|
|
122
|
+
lamports: input.lamports,
|
|
123
|
+
}));
|
|
124
|
+
if (input.memoNonce) {
|
|
125
|
+
transaction.add(new TransactionInstruction({
|
|
126
|
+
keys: [],
|
|
127
|
+
programId: MEMO_PROGRAM_ID,
|
|
128
|
+
data: Buffer.from(input.memoNonce, "utf8"),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
const authority = playerSessionPda(wallet.publicKey, input.sessionPublicKey)[0];
|
|
132
|
+
if ((await this.connection.getAccountInfo(authority)) === null) {
|
|
133
|
+
transaction.add(await this.buildCreatePlayerSessionInstruction(input.sessionPublicKey));
|
|
134
|
+
}
|
|
135
|
+
return transaction;
|
|
136
|
+
}
|
|
137
|
+
async waitForSignature(signature, options = {}) {
|
|
138
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => globalThis.setTimeout(resolve, ms)));
|
|
139
|
+
const attempts = options.attempts ?? 6;
|
|
140
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
141
|
+
await sleep(options.intervalMs ?? 1_500);
|
|
142
|
+
const status = await this.connection.getSignatureStatus(signature);
|
|
143
|
+
const confirmation = status.value?.confirmationStatus;
|
|
144
|
+
if (confirmation === "confirmed" || confirmation === "finalized")
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
throw new SwarmClientError({ code: "TIMEOUT", operation: "solana.waitForSignature", message: `transaction ${signature} did not confirm after ${attempts} polls`, retryable: true });
|
|
148
|
+
}
|
|
149
|
+
async fetchPlayerProfiles(tournamentId = this.tournamentId) {
|
|
150
|
+
const profiles = await this.program().account.playerProfile.all([{
|
|
151
|
+
memcmp: { offset: 40, bytes: bs58.encode(u64LE(tournamentId)) },
|
|
152
|
+
}]);
|
|
153
|
+
return profiles;
|
|
154
|
+
}
|
|
155
|
+
async depositStake(tournament, session) {
|
|
156
|
+
const wallet = this.requireWallet();
|
|
157
|
+
const escrow = escrowPda(this.tournamentId, wallet.publicKey)[0];
|
|
158
|
+
if (session) {
|
|
159
|
+
return this.sessionProgram(session).methods.depositStakeSession().accountsPartial({
|
|
160
|
+
player: wallet.publicKey,
|
|
161
|
+
sessionAuthority: playerSessionPda(wallet.publicKey, session.publicKey)[0],
|
|
162
|
+
tournament,
|
|
163
|
+
escrow,
|
|
164
|
+
}).remainingAccounts(this.liveStakeRemainingAccounts()).rpc();
|
|
165
|
+
}
|
|
166
|
+
return this.program(wallet).methods.depositStake().accountsPartial({ tournament, escrow }).remainingAccounts(this.liveStakeRemainingAccounts()).rpc();
|
|
167
|
+
}
|
|
168
|
+
async buildCreateGameTransaction(input) {
|
|
169
|
+
const wallet = this.requireWallet();
|
|
170
|
+
if (input.session) {
|
|
171
|
+
const player = input.player ?? wallet.publicKey;
|
|
172
|
+
return this.sessionProgram(input.session).methods.createGameSession(input.stake, input.matchupCommitment).accountsPartial({
|
|
173
|
+
game: input.game,
|
|
174
|
+
gameCounter: input.gameCounter,
|
|
175
|
+
playerProfile: input.playerProfile,
|
|
176
|
+
escrow: input.escrow,
|
|
177
|
+
tournament: input.tournament,
|
|
178
|
+
globalConfig: input.globalConfig,
|
|
179
|
+
matchmaker: input.matchmaker,
|
|
180
|
+
player,
|
|
181
|
+
sessionAuthority: playerSessionPda(player, input.session.publicKey)[0],
|
|
182
|
+
sessionSigner: input.session.publicKey,
|
|
183
|
+
systemProgram: SystemProgram.programId,
|
|
184
|
+
}).transaction();
|
|
185
|
+
}
|
|
186
|
+
return this.program(wallet).methods.createGame(input.stake, input.matchupCommitment).accountsPartial({
|
|
187
|
+
game: input.game,
|
|
188
|
+
gameCounter: input.gameCounter,
|
|
189
|
+
playerProfile: input.playerProfile,
|
|
190
|
+
escrow: input.escrow,
|
|
191
|
+
tournament: input.tournament,
|
|
192
|
+
globalConfig: input.globalConfig,
|
|
193
|
+
matchmaker: input.matchmaker,
|
|
194
|
+
player: wallet.publicKey,
|
|
195
|
+
systemProgram: SystemProgram.programId,
|
|
196
|
+
}).transaction();
|
|
197
|
+
}
|
|
198
|
+
async buildJoinGameTransaction(input) {
|
|
199
|
+
const wallet = this.requireWallet();
|
|
200
|
+
const config = await this.fetchGlobalConfig();
|
|
201
|
+
const globalConfig = globalConfigPda()[0];
|
|
202
|
+
const program = input.session ? this.sessionProgram(input.session) : this.program(wallet);
|
|
203
|
+
const builder = input.session
|
|
204
|
+
? program.methods.joinGameSession().accountsPartial({
|
|
205
|
+
player: wallet.publicKey,
|
|
206
|
+
sessionAuthority: playerSessionPda(wallet.publicKey, input.session.publicKey)[0],
|
|
207
|
+
game: input.game,
|
|
208
|
+
tournament: input.tournament,
|
|
209
|
+
escrow: input.escrow,
|
|
210
|
+
globalConfig,
|
|
211
|
+
matchmaker: config.matchmaker,
|
|
212
|
+
})
|
|
213
|
+
: program.methods.joinGame().accountsPartial({
|
|
214
|
+
game: input.game,
|
|
215
|
+
tournament: input.tournament,
|
|
216
|
+
escrow: input.escrow,
|
|
217
|
+
globalConfig,
|
|
218
|
+
matchmaker: config.matchmaker,
|
|
219
|
+
});
|
|
220
|
+
const transaction = await builder.preInstructions(input.preInstructions ?? []).transaction();
|
|
221
|
+
const latest = await this.connection.getLatestBlockhash(this.commitment);
|
|
222
|
+
transaction.recentBlockhash = latest.blockhash;
|
|
223
|
+
transaction.lastValidBlockHeight = latest.lastValidBlockHeight;
|
|
224
|
+
const payer = input.session?.publicKey ?? wallet.publicKey;
|
|
225
|
+
transaction.feePayer = payer;
|
|
226
|
+
transaction.signatures = [
|
|
227
|
+
{ publicKey: payer, signature: null },
|
|
228
|
+
{ publicKey: config.matchmaker, signature: null },
|
|
229
|
+
];
|
|
230
|
+
return { transaction, payer, matchmaker: config.matchmaker };
|
|
231
|
+
}
|
|
232
|
+
async applyCosignature(transaction, signer, signatureBase64) {
|
|
233
|
+
const index = transaction.signatures.findIndex((entry) => entry.publicKey.equals(signer));
|
|
234
|
+
if (index < 0)
|
|
235
|
+
throw new SwarmClientError({ code: "TRANSACTION_MISMATCH", operation: "solana.applyCosignature", message: "Cosigner is not a required signer" });
|
|
236
|
+
transaction.signatures[index] = { publicKey: signer, signature: Buffer.from(signatureBase64, "base64") };
|
|
237
|
+
return transaction;
|
|
238
|
+
}
|
|
239
|
+
messageBase64(transaction) {
|
|
240
|
+
return Buffer.from(transaction.serializeMessage()).toString("base64");
|
|
241
|
+
}
|
|
242
|
+
async signBroadcastAndConfirm(transaction, session) {
|
|
243
|
+
const wallet = this.requireWallet();
|
|
244
|
+
let signed;
|
|
245
|
+
if (session) {
|
|
246
|
+
transaction.partialSign(session);
|
|
247
|
+
signed = transaction;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
signed = await wallet.signTransaction(transaction);
|
|
251
|
+
}
|
|
252
|
+
const signature = await this.connection.sendRawTransaction(signed.serialize());
|
|
253
|
+
await this.connection.confirmTransaction(signature, this.commitment);
|
|
254
|
+
return signature;
|
|
255
|
+
}
|
|
256
|
+
async commitGuess(commitment, game, session) {
|
|
257
|
+
const wallet = this.requireWallet();
|
|
258
|
+
if (session)
|
|
259
|
+
return this.sessionProgram(session).methods.commitGuessSession(commitment).accountsPartial({ player: wallet.publicKey, sessionAuthority: playerSessionPda(wallet.publicKey, session.publicKey)[0], game }).rpc();
|
|
260
|
+
return this.program(wallet).methods.commitGuess(commitment).accountsPartial({ game }).rpc();
|
|
261
|
+
}
|
|
262
|
+
async revealGuess(r, rMatchup, accounts, session) {
|
|
263
|
+
const wallet = this.requireWallet();
|
|
264
|
+
if (session)
|
|
265
|
+
return this.sessionProgram(session).methods.revealGuessSession(r, rMatchup).accountsPartial({ ...accounts, player: wallet.publicKey, sessionAuthority: playerSessionPda(wallet.publicKey, session.publicKey)[0] }).rpc();
|
|
266
|
+
return this.program(wallet).methods.revealGuess(r, rMatchup).accountsPartial(accounts).rpc();
|
|
267
|
+
}
|
|
268
|
+
async closeGame(game, session) {
|
|
269
|
+
const wallet = this.requireWallet();
|
|
270
|
+
if (session)
|
|
271
|
+
return this.sessionProgram(session).methods.closeGame().accountsPartial({ game, caller: session.publicKey }).rpc();
|
|
272
|
+
return this.program(wallet).methods.closeGame().accountsPartial({ game }).rpc();
|
|
273
|
+
}
|
|
274
|
+
async resolveTimeout(gameId, session) {
|
|
275
|
+
const wallet = this.requireWallet();
|
|
276
|
+
const program = session ? this.sessionProgram(session) : this.program(wallet);
|
|
277
|
+
const gameKey = gamePda(gameId)[0];
|
|
278
|
+
const game = await program.account.game.fetch(gameKey);
|
|
279
|
+
const config = await this.fetchGlobalConfig();
|
|
280
|
+
const tournamentId = BigInt(game.tournamentId.toString());
|
|
281
|
+
return program.methods.resolveTimeout().accountsPartial({
|
|
282
|
+
game: gameKey,
|
|
283
|
+
p1Profile: playerProfilePda(tournamentId, game.playerOne)[0],
|
|
284
|
+
p2Profile: playerProfilePda(tournamentId, game.playerTwo)[0],
|
|
285
|
+
tournament: tournamentPda(tournamentId)[0],
|
|
286
|
+
globalConfig: globalConfigPda()[0],
|
|
287
|
+
treasury: config.treasury,
|
|
288
|
+
playerOneWallet: game.playerOne,
|
|
289
|
+
playerTwoWallet: game.playerTwo,
|
|
290
|
+
caller: session?.publicKey ?? wallet.publicKey,
|
|
291
|
+
}).rpc();
|
|
292
|
+
}
|
|
293
|
+
async closeSessionByDelegate(session, player) {
|
|
294
|
+
return this.sessionProgram(session).methods.closeSessionByDelegate().accountsPartial({
|
|
295
|
+
sessionAuthority: playerSessionPda(player, session.publicKey)[0],
|
|
296
|
+
sessionSigner: session.publicKey,
|
|
297
|
+
}).rpc();
|
|
298
|
+
}
|
|
299
|
+
deriveAddresses(gameId, player) {
|
|
300
|
+
return {
|
|
301
|
+
globalConfig: globalConfigPda()[0],
|
|
302
|
+
gameCounter: gameCounterPda()[0],
|
|
303
|
+
game: gamePda(gameId)[0],
|
|
304
|
+
tournament: tournamentPda(this.tournamentId)[0],
|
|
305
|
+
playerProfile: playerProfilePda(this.tournamentId, player)[0],
|
|
306
|
+
escrow: escrowPda(this.tournamentId, player)[0],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|