agent-outlier-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +298 -0
- package/package.json +29 -0
package/index.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Outlier SDK — Play the game in 3 function calls.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const { OutlierPlayer } = require('agent-outlier-sdk');
|
|
6
|
+
* const player = new OutlierPlayer(signer, { exoTokenId: 1 });
|
|
7
|
+
* const { roundId, hash } = await player.commit(0, [10, 20, 30]); // NANO tier
|
|
8
|
+
* // ... wait for reveal phase ...
|
|
9
|
+
* await player.reveal(0, [10, 20, 30]);
|
|
10
|
+
* // ... wait for finalize phase ...
|
|
11
|
+
* await player.finalize(0);
|
|
12
|
+
* await player.claim();
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { ethers } = require("ethers");
|
|
16
|
+
|
|
17
|
+
const GAME_ADDRESS = "0x8F7403D5809Dd7245dF268ab9D596B3299A84B5C";
|
|
18
|
+
const EXO_ADDRESS = "0x8241BDD5009ed3F6C99737D2415994B58296Da0d";
|
|
19
|
+
|
|
20
|
+
const GAME_ABI = [
|
|
21
|
+
"function commit(uint8 tier, bytes32 commitHash, uint256 exoTokenId) external payable",
|
|
22
|
+
"function revealSelf(uint8 tier, uint256[] picks, bytes32 salt) external",
|
|
23
|
+
"function finalizeRound(uint8 tier) external",
|
|
24
|
+
"function claimWinnings() external",
|
|
25
|
+
"function getCurrentRoundId(uint8 tier) view returns (uint256)",
|
|
26
|
+
"function getRoundPhase(uint256 roundId) view returns (uint8)",
|
|
27
|
+
"function getRoundStartTime(uint256 roundId) view returns (uint256)",
|
|
28
|
+
"function getCurrentRound(uint8 tier) view returns (uint256 roundId, uint8 phase, uint256 startTime, uint256 commitDeadline, uint256 revealDeadline, uint256 totalPot, uint256 rolloverPot, uint256 playerCount, uint256 maxRange)",
|
|
29
|
+
"function getRoundResult(uint256 roundId) view returns (bool finalized, address winner, uint256 winningNumber, uint256 totalPot)",
|
|
30
|
+
"function getRoundPlayers(uint256 roundId) view returns (address[])",
|
|
31
|
+
"function getPlayerReveals(uint256 roundId, address player) view returns (uint256[])",
|
|
32
|
+
"function hasPlayerRevealed(uint256 roundId, address player) view returns (bool)",
|
|
33
|
+
"function getPlayerStats(address player) view returns (uint256 eloRating, uint256 gamesPlayed, uint256 epochGames, uint256 claimable)",
|
|
34
|
+
"function getPlayerElo(address player) view returns (uint256)",
|
|
35
|
+
"function getTierConfig(uint8 tier) view returns (tuple(uint256 entryFee, uint256 maxRange, uint8 numPicks, uint256 eloMinimum, uint256 eloCeiling, uint256 minPlayers, uint256 maxPlayers))",
|
|
36
|
+
"function claimableWinnings(address) view returns (uint256)",
|
|
37
|
+
"function deploymentTimestamp() view returns (uint256)",
|
|
38
|
+
"function paused() view returns (bool)",
|
|
39
|
+
"function getNumberCount(uint256 roundId, uint256 number) view returns (uint256)",
|
|
40
|
+
"function getPlayerCommitment(uint256 roundId, address player) view returns (bytes32 commitHash, uint8 picksCount, uint256 entryAmount, uint256 exoTokenId)",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const TIER = { NANO: 0, MICRO: 1, STANDARD: 2, HIGH: 3 };
|
|
44
|
+
const PHASE = { COMMIT: 0, REVEAL: 1, FINALIZED: 2 };
|
|
45
|
+
|
|
46
|
+
const ROUND_DURATION = 20 * 60;
|
|
47
|
+
const COMMIT_DURATION = 12 * 60;
|
|
48
|
+
const REVEAL_DURATION = 4 * 60;
|
|
49
|
+
|
|
50
|
+
class OutlierPlayer {
|
|
51
|
+
/**
|
|
52
|
+
* @param {ethers.Signer} signer — Wallet that will play
|
|
53
|
+
* @param {Object} options
|
|
54
|
+
* @param {number} options.exoTokenId — Exoskeleton token ID to play with
|
|
55
|
+
* @param {string} [options.gameAddress] — Override game contract address
|
|
56
|
+
*/
|
|
57
|
+
constructor(signer, options = {}) {
|
|
58
|
+
if (!signer) throw new Error("Signer required");
|
|
59
|
+
if (!options.exoTokenId && options.exoTokenId !== 0) throw new Error("exoTokenId required");
|
|
60
|
+
|
|
61
|
+
this.signer = signer;
|
|
62
|
+
this.exoTokenId = options.exoTokenId;
|
|
63
|
+
this.gameAddress = options.gameAddress || GAME_ADDRESS;
|
|
64
|
+
this.game = new ethers.Contract(this.gameAddress, GAME_ABI, signer);
|
|
65
|
+
|
|
66
|
+
// Store salts for reveal
|
|
67
|
+
this._salts = {};
|
|
68
|
+
this._picks = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a random salt
|
|
73
|
+
*/
|
|
74
|
+
_generateSalt() {
|
|
75
|
+
return ethers.hexlify(ethers.randomBytes(32));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate commit hash matching the contract's keccak256(abi.encode(...))
|
|
80
|
+
*/
|
|
81
|
+
_generateCommitHash(address, roundId, picks, salt) {
|
|
82
|
+
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
|
83
|
+
const encoded = abiCoder.encode(
|
|
84
|
+
["address", "uint256", "uint256[]", "bytes32"],
|
|
85
|
+
[address, roundId, picks, salt]
|
|
86
|
+
);
|
|
87
|
+
return ethers.keccak256(encoded);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Commit picks to a tier. Stores salt internally for reveal.
|
|
92
|
+
* @param {number} tier — 0=NANO, 1=MICRO, 2=STANDARD, 3=HIGH
|
|
93
|
+
* @param {number[]} picks — Array of numbers to pick
|
|
94
|
+
* @returns {{ roundId, hash, salt, tx }}
|
|
95
|
+
*/
|
|
96
|
+
async commit(tier, picks) {
|
|
97
|
+
const config = await this.game.getTierConfig(tier);
|
|
98
|
+
const totalCost = config.entryFee * BigInt(config.numPicks);
|
|
99
|
+
const roundId = await this.game.getCurrentRoundId(tier);
|
|
100
|
+
const address = await this.signer.getAddress();
|
|
101
|
+
|
|
102
|
+
// Validate picks
|
|
103
|
+
if (picks.length !== Number(config.numPicks)) {
|
|
104
|
+
throw new Error(`Tier requires ${config.numPicks} picks, got ${picks.length}`);
|
|
105
|
+
}
|
|
106
|
+
for (const p of picks) {
|
|
107
|
+
if (p < 1 || p > Number(config.maxRange)) {
|
|
108
|
+
throw new Error(`Pick ${p} out of range 1-${config.maxRange}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const salt = this._generateSalt();
|
|
113
|
+
const hash = this._generateCommitHash(address, roundId, picks, salt);
|
|
114
|
+
|
|
115
|
+
const tx = await this.game.commit(tier, hash, this.exoTokenId, { value: totalCost });
|
|
116
|
+
const receipt = await tx.wait();
|
|
117
|
+
|
|
118
|
+
// Store for reveal
|
|
119
|
+
const key = `${tier}-${roundId.toString()}`;
|
|
120
|
+
this._salts[key] = salt;
|
|
121
|
+
this._picks[key] = picks;
|
|
122
|
+
|
|
123
|
+
return { roundId, hash, salt, picks, tx: receipt };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reveal picks for the current round of a tier.
|
|
128
|
+
* Uses the stored salt from commit().
|
|
129
|
+
* @param {number} tier
|
|
130
|
+
* @param {number[]} [picks] — Override picks (must match commit). Uses stored picks if omitted.
|
|
131
|
+
* @param {string} [salt] — Override salt. Uses stored salt if omitted.
|
|
132
|
+
*/
|
|
133
|
+
async reveal(tier, picks, salt) {
|
|
134
|
+
const roundId = await this.game.getCurrentRoundId(tier);
|
|
135
|
+
const key = `${tier}-${roundId.toString()}`;
|
|
136
|
+
|
|
137
|
+
picks = picks || this._picks[key];
|
|
138
|
+
salt = salt || this._salts[key];
|
|
139
|
+
|
|
140
|
+
if (!picks || !salt) {
|
|
141
|
+
throw new Error("No stored commit found for this tier/round. Pass picks and salt manually.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const tx = await this.game.revealSelf(tier, picks, salt);
|
|
145
|
+
return tx.wait();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Finalize the current round for a tier. Anyone can call this.
|
|
150
|
+
* @param {number} tier
|
|
151
|
+
*/
|
|
152
|
+
async finalize(tier) {
|
|
153
|
+
const tx = await this.game.finalizeRound(tier);
|
|
154
|
+
return tx.wait();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Claim any accumulated winnings.
|
|
159
|
+
*/
|
|
160
|
+
async claim() {
|
|
161
|
+
const tx = await this.game.claimWinnings();
|
|
162
|
+
return tx.wait();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============ READ FUNCTIONS ============
|
|
166
|
+
|
|
167
|
+
/** Get current round info for a tier */
|
|
168
|
+
async getRound(tier) {
|
|
169
|
+
const data = await this.game.getCurrentRound(tier);
|
|
170
|
+
return {
|
|
171
|
+
roundId: data.roundId,
|
|
172
|
+
phase: Number(data.phase),
|
|
173
|
+
phaseName: ["COMMIT", "REVEAL", "FINALIZED"][Number(data.phase)],
|
|
174
|
+
startTime: Number(data.startTime),
|
|
175
|
+
commitDeadline: Number(data.commitDeadline),
|
|
176
|
+
revealDeadline: Number(data.revealDeadline),
|
|
177
|
+
totalPot: data.totalPot,
|
|
178
|
+
rolloverPot: data.rolloverPot,
|
|
179
|
+
playerCount: Number(data.playerCount),
|
|
180
|
+
maxRange: Number(data.maxRange),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Get player stats (ELO, games, claimable) */
|
|
185
|
+
async getStats(address) {
|
|
186
|
+
address = address || (await this.signer.getAddress());
|
|
187
|
+
const data = await this.game.getPlayerStats(address);
|
|
188
|
+
return {
|
|
189
|
+
elo: Number(data.eloRating),
|
|
190
|
+
gamesPlayed: Number(data.gamesPlayed),
|
|
191
|
+
epochGames: Number(data.epochGames),
|
|
192
|
+
claimable: data.claimable,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Get tier configuration */
|
|
197
|
+
async getTierConfig(tier) {
|
|
198
|
+
const config = await this.game.getTierConfig(tier);
|
|
199
|
+
return {
|
|
200
|
+
entryFee: config.entryFee,
|
|
201
|
+
maxRange: Number(config.maxRange),
|
|
202
|
+
numPicks: Number(config.numPicks),
|
|
203
|
+
eloMinimum: Number(config.eloMinimum),
|
|
204
|
+
eloCeiling: Number(config.eloCeiling),
|
|
205
|
+
minPlayers: Number(config.minPlayers),
|
|
206
|
+
maxPlayers: Number(config.maxPlayers),
|
|
207
|
+
totalCost: config.entryFee * BigInt(config.numPicks),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Get round result */
|
|
212
|
+
async getResult(roundId) {
|
|
213
|
+
const data = await this.game.getRoundResult(roundId);
|
|
214
|
+
return {
|
|
215
|
+
finalized: data.finalized,
|
|
216
|
+
winner: data.winner,
|
|
217
|
+
winningNumber: Number(data.winningNumber),
|
|
218
|
+
totalPot: data.totalPot,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Check if game is paused */
|
|
223
|
+
async isPaused() {
|
|
224
|
+
return this.game.paused();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Get claimable balance */
|
|
228
|
+
async getClaimable(address) {
|
|
229
|
+
address = address || (await this.signer.getAddress());
|
|
230
|
+
return this.game.claimableWinnings(address);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Wait for a specific phase, polling every `interval` ms.
|
|
235
|
+
* @param {number} tier
|
|
236
|
+
* @param {number} targetPhase — 0=COMMIT, 1=REVEAL, 2=FINALIZED
|
|
237
|
+
* @param {number} [interval=5000] — Poll interval in ms
|
|
238
|
+
* @param {number} [timeout=1500000] — Max wait in ms (default 25 min)
|
|
239
|
+
*/
|
|
240
|
+
async waitForPhase(tier, targetPhase, interval = 5000, timeout = 1500000) {
|
|
241
|
+
const start = Date.now();
|
|
242
|
+
while (Date.now() - start < timeout) {
|
|
243
|
+
const round = await this.getRound(tier);
|
|
244
|
+
if (round.phase === targetPhase) return round;
|
|
245
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Timeout waiting for phase ${targetPhase}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Play a complete round: commit → wait → reveal → wait → finalize → claim.
|
|
252
|
+
* This is the simplest way to play — one function call does everything.
|
|
253
|
+
* @param {number} tier
|
|
254
|
+
* @param {number[]} picks
|
|
255
|
+
* @returns {{ roundId, result, claimed }}
|
|
256
|
+
*/
|
|
257
|
+
async playRound(tier, picks) {
|
|
258
|
+
// 1. Wait for commit phase
|
|
259
|
+
await this.waitForPhase(tier, PHASE.COMMIT);
|
|
260
|
+
const { roundId } = await this.commit(tier, picks);
|
|
261
|
+
console.log(`Committed to round ${roundId} with picks [${picks}]`);
|
|
262
|
+
|
|
263
|
+
// 2. Wait for reveal phase
|
|
264
|
+
await this.waitForPhase(tier, PHASE.REVEAL);
|
|
265
|
+
await this.reveal(tier);
|
|
266
|
+
console.log("Revealed picks");
|
|
267
|
+
|
|
268
|
+
// 3. Wait for finalize phase
|
|
269
|
+
await this.waitForPhase(tier, PHASE.FINALIZED);
|
|
270
|
+
|
|
271
|
+
// 4. Try to finalize (may already be finalized by someone else)
|
|
272
|
+
try {
|
|
273
|
+
await this.finalize(tier);
|
|
274
|
+
console.log("Finalized round");
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.log("Round already finalized");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 5. Check result
|
|
280
|
+
const result = await this.getResult(roundId);
|
|
281
|
+
const myAddress = await this.signer.getAddress();
|
|
282
|
+
const won = result.winner.toLowerCase() === myAddress.toLowerCase();
|
|
283
|
+
console.log(won ? `Won! Number ${result.winningNumber}` : "Did not win this round");
|
|
284
|
+
|
|
285
|
+
// 6. Claim if we have winnings
|
|
286
|
+
let claimed = 0n;
|
|
287
|
+
const claimable = await this.getClaimable();
|
|
288
|
+
if (claimable > 0n) {
|
|
289
|
+
await this.claim();
|
|
290
|
+
claimed = claimable;
|
|
291
|
+
console.log(`Claimed ${ethers.formatEther(claimable)} ETH`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { roundId, result, won, claimed };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = { OutlierPlayer, GAME_ABI, GAME_ADDRESS, EXO_ADDRESS, TIER, PHASE, ROUND_DURATION, COMMIT_DURATION, REVEAL_DURATION };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-outlier-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Play Agent Outlier in 3 function calls — SDK for AI agents on Base",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent",
|
|
8
|
+
"outlier",
|
|
9
|
+
"base",
|
|
10
|
+
"onchain",
|
|
11
|
+
"game",
|
|
12
|
+
"ai-agent",
|
|
13
|
+
"exoskeleton",
|
|
14
|
+
"commit-reveal",
|
|
15
|
+
"strategy"
|
|
16
|
+
],
|
|
17
|
+
"author": "potdealer",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Potdealer/agent-outlier"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"ethers": "^6.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|