@slvr-labs/sdk 0.1.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/LICENSE +21 -0
- package/README.md +849 -0
- package/dist/connect.d.ts +47 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +56 -0
- package/dist/connect.js.map +1 -0
- package/dist/contracts/autoCommit.d.ts +151 -0
- package/dist/contracts/autoCommit.d.ts.map +1 -0
- package/dist/contracts/autoCommit.js +426 -0
- package/dist/contracts/autoCommit.js.map +1 -0
- package/dist/contracts/hub.d.ts +50 -0
- package/dist/contracts/hub.d.ts.map +1 -0
- package/dist/contracts/hub.js +118 -0
- package/dist/contracts/hub.js.map +1 -0
- package/dist/contracts/index.d.ts +8 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +18 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/jackpot.d.ts +21 -0
- package/dist/contracts/jackpot.d.ts.map +1 -0
- package/dist/contracts/jackpot.js +44 -0
- package/dist/contracts/jackpot.js.map +1 -0
- package/dist/contracts/lottery.d.ts +256 -0
- package/dist/contracts/lottery.d.ts.map +1 -0
- package/dist/contracts/lottery.js +767 -0
- package/dist/contracts/lottery.js.map +1 -0
- package/dist/contracts/registry.d.ts +53 -0
- package/dist/contracts/registry.d.ts.map +1 -0
- package/dist/contracts/registry.js +143 -0
- package/dist/contracts/registry.js.map +1 -0
- package/dist/contracts/staking.d.ts +101 -0
- package/dist/contracts/staking.d.ts.map +1 -0
- package/dist/contracts/staking.js +306 -0
- package/dist/contracts/staking.js.map +1 -0
- package/dist/contracts/token.d.ts +95 -0
- package/dist/contracts/token.d.ts.map +1 -0
- package/dist/contracts/token.js +273 -0
- package/dist/contracts/token.js.map +1 -0
- package/dist/deployments.d.ts +117 -0
- package/dist/deployments.d.ts.map +1 -0
- package/dist/deployments.js +74 -0
- package/dist/deployments.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +67 -0
- package/dist/errors.js.map +1 -0
- package/dist/ev.d.ts +123 -0
- package/dist/ev.d.ts.map +1 -0
- package/dist/ev.js +111 -0
- package/dist/ev.js.map +1 -0
- package/dist/events.d.ts +418 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +87 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +436 -0
- package/dist/index.js.map +1 -0
- package/dist/oracle.d.ts +48 -0
- package/dist/oracle.d.ts.map +1 -0
- package/dist/oracle.js +77 -0
- package/dist/oracle.js.map +1 -0
- package/dist/price.d.ts +52 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +78 -0
- package/dist/price.js.map +1 -0
- package/dist/transaction.d.ts +54 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +105 -0
- package/dist/transaction.js.map +1 -0
- package/dist/types.d.ts +162 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +58 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +110 -0
- package/dist/utils.js.map +1 -0
- package/examples/.env.example +22 -0
- package/examples/README.md +417 -0
- package/examples/automated-betting.ts +232 -0
- package/examples/combined-strategy.ts +227 -0
- package/examples/constants.ts +35 -0
- package/examples/custom-strategy-example.ts +231 -0
- package/examples/expected-value-strategy.ts +255 -0
- package/examples/fixed-squares-strategy.ts +176 -0
- package/examples/index.ts +30 -0
- package/examples/least-allocated-strategy.ts +224 -0
- package/examples/local-test.ts +172 -0
- package/examples/quickstart-bet.ts +72 -0
- package/examples/quickstart-read.ts +87 -0
- package/examples/simple-example.ts +122 -0
- package/examples/strategy-base.ts +283 -0
- package/package.json +71 -0
- package/skills/slvr-bot/SKILL.md +193 -0
- package/skills/slvr-bot/references/api.md +117 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Expected-value grid-mining bot
|
|
3
|
+
*
|
|
4
|
+
* This bot only bets when the round is **+EV** — i.e. the expected value of
|
|
5
|
+
* mining SLVR (plus the jackpot term) exceeds the ETH you bleed to the protocol
|
|
6
|
+
* fee. It uses the SDK's built-in grid-mining calculator (`computeGridMiningEv` /
|
|
7
|
+
* `sdk.estimateRoundEv`) and the SDK's SLVR price reader (`sdk.price`), so the
|
|
8
|
+
* math matches the protocol's own edge calculator.
|
|
9
|
+
*
|
|
10
|
+
* How the decision works (see the docs on `computeGridMiningEv` for the full
|
|
11
|
+
* model):
|
|
12
|
+
* - SLVR mined/round ≈ (stake / pot) * emissionPerRound
|
|
13
|
+
* - ETH bleed/round = feeFraction * stake (you get the rest of the pot back pro-rata)
|
|
14
|
+
* - net EV/round = slvrValue - bleed + jackpotEV
|
|
15
|
+
* Because both terms scale with your stake, the edge depends on **pot vs SLVR
|
|
16
|
+
* price**, not on bet size: mining is profitable while the pot is *below* the
|
|
17
|
+
* break-even pot. This bot bets when net EV per round clears a threshold.
|
|
18
|
+
*
|
|
19
|
+
* When it bets, it spreads the stake across all 25 squares in proportion to the
|
|
20
|
+
* current pot — the coverage the EV model assumes, so you hold a `stake/pot`
|
|
21
|
+
* slice of whichever square wins. (Tilting toward the least-allocated squares —
|
|
22
|
+
* see `least-allocated-strategy.ts` — would raise SLVR yield *above* this
|
|
23
|
+
* estimate, so treating this EV as a floor is conservative.)
|
|
24
|
+
*
|
|
25
|
+
* Run: set PRIVATE_KEY and `ts-node expected-value-strategy.ts` (or import the
|
|
26
|
+
* class). To adapt into your own project, change the `../src` import to
|
|
27
|
+
* `@slvr-labs/sdk`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { createPublicClient, createWalletClient, http, parseEther, formatEther } from 'viem';
|
|
31
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
32
|
+
import { SlvrSDK, robinhoodChain, deployments, GRID_SIZE, type GridMiningEv } from '../src';
|
|
33
|
+
|
|
34
|
+
export interface ExpectedValueBotOptions {
|
|
35
|
+
/** ETH to commit per round. Default: 0.1 ETH. */
|
|
36
|
+
stakeEth?: number;
|
|
37
|
+
/** Minimum net EV (in ETH per round) required to bet. Default: 0 (bet on any +EV round). */
|
|
38
|
+
minNetEth?: number;
|
|
39
|
+
/** Value SLVR net of the 10% refining fee (cashing out) vs at full price (holding). Default: false (hold). */
|
|
40
|
+
cashOut?: boolean;
|
|
41
|
+
/** ETH currently in the jackpot pool, to include the jackpot term. Default: 0. */
|
|
42
|
+
jackpotPool?: number;
|
|
43
|
+
/** Jackpot odds (1-in-N). Default: the protocol default (625). */
|
|
44
|
+
jackpotOdds?: number;
|
|
45
|
+
/**
|
|
46
|
+
* ETH/USD price override, for logging values in USD. If omitted, the bot uses
|
|
47
|
+
* the SDK's Chainlink feed (`sdk.ethUsd`) — which is wired for Robinhood Chain,
|
|
48
|
+
* so USD figures show automatically. Set this only to override the feed or on a
|
|
49
|
+
* chain that lacks one.
|
|
50
|
+
*/
|
|
51
|
+
ethUsd?: number;
|
|
52
|
+
/** How often to check, in milliseconds. Default: 5000. */
|
|
53
|
+
checkInterval?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A bot that mines the grid only when the expected value is positive.
|
|
58
|
+
*/
|
|
59
|
+
export class ExpectedValueBot {
|
|
60
|
+
private sdk: SlvrSDK;
|
|
61
|
+
private stakeEth: number;
|
|
62
|
+
private minNetEth: number;
|
|
63
|
+
private cashOut: boolean;
|
|
64
|
+
private jackpotPool: number;
|
|
65
|
+
private jackpotOdds: number | undefined;
|
|
66
|
+
private ethUsd: number | undefined;
|
|
67
|
+
private checkInterval: number;
|
|
68
|
+
private isRunning = false;
|
|
69
|
+
private intervalId?: ReturnType<typeof setInterval>;
|
|
70
|
+
private lastBetRound?: bigint;
|
|
71
|
+
|
|
72
|
+
constructor(sdk: SlvrSDK, options: ExpectedValueBotOptions = {}) {
|
|
73
|
+
this.sdk = sdk;
|
|
74
|
+
this.stakeEth = options.stakeEth ?? 0.1;
|
|
75
|
+
this.minNetEth = options.minNetEth ?? 0;
|
|
76
|
+
this.cashOut = options.cashOut ?? false;
|
|
77
|
+
this.jackpotPool = options.jackpotPool ?? 0;
|
|
78
|
+
this.jackpotOdds = options.jackpotOdds;
|
|
79
|
+
this.ethUsd = options.ethUsd;
|
|
80
|
+
this.checkInterval = options.checkInterval ?? 5000;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Evaluate the current round once: log the EV breakdown and bet if it clears the threshold. */
|
|
84
|
+
async checkOnce(): Promise<GridMiningEv | null> {
|
|
85
|
+
const roundId = await this.sdk.lottery.currentRoundId();
|
|
86
|
+
|
|
87
|
+
// SLVR price in both ETH and USD (USD needs an ETH/USD source — the SDK's
|
|
88
|
+
// Chainlink feed if configured, else the `ethUsd` option; otherwise null).
|
|
89
|
+
const price = await this.sdk.getSlvrPrice({ ethUsd: this.ethUsd });
|
|
90
|
+
console.log(
|
|
91
|
+
`SLVR price: ${price.eth.toExponential(4)} ETH` +
|
|
92
|
+
(price.usd !== null ? ` · $${price.usd.toFixed(4)}` : ' · (USD unavailable — no ETH/USD source)')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Compute the expected value for this round (pulls pot, emission and SLVR price on-chain).
|
|
96
|
+
const ev = await this.sdk.estimateRoundEv({
|
|
97
|
+
stake: this.stakeEth,
|
|
98
|
+
roundId,
|
|
99
|
+
cashOut: this.cashOut,
|
|
100
|
+
jackpotPool: this.jackpotPool,
|
|
101
|
+
jackpotOdds: this.jackpotOdds,
|
|
102
|
+
slvrPriceEth: price.eth, // reuse the price we just read
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Derive the ETH/USD we ended up using, to also show net EV in USD.
|
|
106
|
+
const ethUsd = price.usd !== null ? price.usd / price.eth : null;
|
|
107
|
+
const netUsd = ethUsd !== null ? ` (${ev.netEth * ethUsd >= 0 ? '+' : ''}$${(ev.netEth * ethUsd).toFixed(4)})` : '';
|
|
108
|
+
|
|
109
|
+
console.log(
|
|
110
|
+
`Round ${roundId}: share ${(ev.share * 100).toFixed(2)}% · ` +
|
|
111
|
+
`mine ${ev.slvrMined.toFixed(4)} SLVR (${ev.slvrValueEth.toFixed(6)} ETH) · ` +
|
|
112
|
+
`bleed ${ev.ethBleed.toFixed(6)} ETH · jackpot +${ev.jackpotEvEth.toFixed(6)} ETH · ` +
|
|
113
|
+
`NET ${ev.netEth >= 0 ? '+' : ''}${ev.netEth.toFixed(6)} ETH/round${netUsd} ` +
|
|
114
|
+
`(edge ${(ev.edgeRatio * 100).toFixed(2)}%, break-even pot ${ev.breakEvenPot.toFixed(4)} ETH)`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (this.lastBetRound === roundId) {
|
|
118
|
+
console.log(' already bet this round — skipping');
|
|
119
|
+
return ev;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (ev.netEth < this.minNetEth) {
|
|
123
|
+
console.log(
|
|
124
|
+
` skip: net EV ${ev.netEth.toFixed(6)} < threshold ${this.minNetEth} ETH ` +
|
|
125
|
+
`(pot above break-even — wait for it to shrink or SLVR to rise)`
|
|
126
|
+
);
|
|
127
|
+
return ev;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!(await this.sdk.lottery.roundOpen(roundId))) {
|
|
131
|
+
console.log(' skip: round is not open for betting');
|
|
132
|
+
return ev;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await this.placeGridBet(roundId);
|
|
136
|
+
this.lastBetRound = roundId;
|
|
137
|
+
return ev;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Spread the stake across all {@link GRID_SIZE} squares in proportion to the
|
|
142
|
+
* current pot (with a floor so every square is covered and we always hold the
|
|
143
|
+
* winner). This is the coverage the grid-mining EV model assumes.
|
|
144
|
+
*/
|
|
145
|
+
private async placeGridBet(roundId: bigint): Promise<void> {
|
|
146
|
+
const stakeWei = parseEther(this.stakeEth.toString());
|
|
147
|
+
|
|
148
|
+
const squaresData = await this.sdk.lottery.getRoundSquares(roundId);
|
|
149
|
+
const totals: bigint[] = Array.from(
|
|
150
|
+
{ length: GRID_SIZE },
|
|
151
|
+
(_, i) => squaresData.find((s) => s.square === i)?.total ?? 0n
|
|
152
|
+
);
|
|
153
|
+
const sumTotals = totals.reduce((a, b) => a + b, 0n);
|
|
154
|
+
|
|
155
|
+
// Floor keeps empty squares covered so we never miss the winning square.
|
|
156
|
+
const floor = sumTotals / BigInt(GRID_SIZE) + 1n;
|
|
157
|
+
const weights = totals.map((t) => t + floor);
|
|
158
|
+
const sumWeights = weights.reduce((a, b) => a + b, 0n);
|
|
159
|
+
|
|
160
|
+
const amounts = weights.map((w) => (stakeWei * w) / sumWeights);
|
|
161
|
+
// Push any rounding dust onto the first square so the amounts sum to exactly stakeWei.
|
|
162
|
+
const allocated = amounts.reduce((a, b) => a + b, 0n);
|
|
163
|
+
amounts[0] = (amounts[0] ?? 0n) + (stakeWei - allocated);
|
|
164
|
+
|
|
165
|
+
const squares = Array.from({ length: GRID_SIZE }, (_, i) => i);
|
|
166
|
+
|
|
167
|
+
console.log(` betting ${formatEther(stakeWei)} ETH across all ${GRID_SIZE} squares…`);
|
|
168
|
+
const txHash = await this.sdk.lottery.bet({ roundId, squares, amounts });
|
|
169
|
+
console.log(` ✅ bet placed: ${txHash}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Start the polling loop. */
|
|
173
|
+
async start(): Promise<void> {
|
|
174
|
+
if (this.isRunning) {
|
|
175
|
+
console.log('Bot is already running');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!this.sdk.getWalletClient()) {
|
|
179
|
+
throw new Error('Wallet client required for expected-value betting');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.isRunning = true;
|
|
183
|
+
console.log('📈 Expected-value bot started');
|
|
184
|
+
console.log(
|
|
185
|
+
`Stake ${this.stakeEth} ETH/round · bet when net EV ≥ ${this.minNetEth} ETH · ` +
|
|
186
|
+
`mode ${this.cashOut ? 'cash-out (−10% refine)' : 'hold'}`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const tick = async () => {
|
|
190
|
+
try {
|
|
191
|
+
await this.checkOnce();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(' error during check:', err instanceof Error ? err.message : err);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
await tick();
|
|
198
|
+
this.intervalId = setInterval(tick, this.checkInterval);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Stop the polling loop. */
|
|
202
|
+
stop(): void {
|
|
203
|
+
if (this.intervalId) clearInterval(this.intervalId);
|
|
204
|
+
this.isRunning = false;
|
|
205
|
+
console.log('🛑 Expected-value bot stopped');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Runnable entry point: builds a wallet-backed SDK on Robinhood Chain (using the
|
|
211
|
+
* shipped `deployments` + `robinhoodChain`) and starts the bot.
|
|
212
|
+
*/
|
|
213
|
+
export async function exampleExpectedValueBot(): Promise<void> {
|
|
214
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
215
|
+
if (!privateKey) {
|
|
216
|
+
throw new Error('PRIVATE_KEY environment variable is required');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
|
220
|
+
const publicClient = createPublicClient({ chain: robinhoodChain, transport: http() });
|
|
221
|
+
const walletClient = createWalletClient({ chain: robinhoodChain, transport: http(), account });
|
|
222
|
+
|
|
223
|
+
// deployments.robinhood.addresses includes slvrEthPair, so sdk.price is available.
|
|
224
|
+
const sdk = new SlvrSDK({
|
|
225
|
+
publicClient,
|
|
226
|
+
walletClient,
|
|
227
|
+
addresses: deployments.robinhood.addresses,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const bot = new ExpectedValueBot(sdk, {
|
|
231
|
+
stakeEth: 0.1, // commit 0.1 ETH per +EV round
|
|
232
|
+
minNetEth: 0, // bet on any positive-EV round
|
|
233
|
+
cashOut: false, // value mined SLVR at full price (holding/staking it)
|
|
234
|
+
// USD comes from the SDK's Chainlink ETH/USD feed automatically on Robinhood
|
|
235
|
+
// Chain. Set ETH_USD to override it (e.g. `ETH_USD=1815 ts-node …`).
|
|
236
|
+
ethUsd: process.env.ETH_USD ? Number(process.env.ETH_USD) : undefined,
|
|
237
|
+
checkInterval: 5000,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await bot.start();
|
|
241
|
+
|
|
242
|
+
process.on('SIGINT', () => {
|
|
243
|
+
console.log('\nReceived SIGINT, stopping bot…');
|
|
244
|
+
bot.stop();
|
|
245
|
+
process.exit(0);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Run directly: `PRIVATE_KEY=0x... ts-node expected-value-strategy.ts`
|
|
250
|
+
if (require.main === module) {
|
|
251
|
+
exampleExpectedValueBot().catch((err) => {
|
|
252
|
+
console.error(err);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Fixed Squares Strategy
|
|
3
|
+
*
|
|
4
|
+
* This strategy bets on a fixed set of squares every round
|
|
5
|
+
* when the total wager is below the threshold.
|
|
6
|
+
*
|
|
7
|
+
* Perfect for users who want to consistently bet on specific squares.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BettingStrategy } from './strategy-base';
|
|
11
|
+
import { parseEther } from 'viem';
|
|
12
|
+
import { SlvrSDK } from '../src';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for fixed squares strategy
|
|
16
|
+
*/
|
|
17
|
+
export interface FixedSquaresConfig {
|
|
18
|
+
/** Squares to bet on (0-24) */
|
|
19
|
+
squares: number[];
|
|
20
|
+
/** Amount to bet per square (in wei) */
|
|
21
|
+
amountPerSquare: bigint;
|
|
22
|
+
/** Optional: different amount for each square (overrides amountPerSquare) */
|
|
23
|
+
amounts?: bigint[];
|
|
24
|
+
/** Threshold in wei - only bet when round total wager is below this */
|
|
25
|
+
threshold?: bigint;
|
|
26
|
+
/** How often to check rounds (in milliseconds) */
|
|
27
|
+
checkInterval?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Strategy that bets on fixed squares every round
|
|
32
|
+
*/
|
|
33
|
+
export class FixedSquaresStrategy extends BettingStrategy {
|
|
34
|
+
private squares: number[];
|
|
35
|
+
private amountPerSquare: bigint;
|
|
36
|
+
private amounts?: bigint[];
|
|
37
|
+
|
|
38
|
+
constructor(sdk: SlvrSDK, config: FixedSquaresConfig) {
|
|
39
|
+
super(sdk, {
|
|
40
|
+
threshold: config.threshold,
|
|
41
|
+
checkInterval: config.checkInterval,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Validate squares
|
|
45
|
+
if (!config.squares || config.squares.length === 0) {
|
|
46
|
+
throw new Error('At least one square must be specified');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const square of config.squares) {
|
|
50
|
+
if (square < 0 || square >= 25) {
|
|
51
|
+
throw new Error(`Invalid square: ${square}. Must be 0-24`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for duplicates
|
|
56
|
+
const unique = new Set(config.squares);
|
|
57
|
+
if (unique.size !== config.squares.length) {
|
|
58
|
+
throw new Error('Duplicate squares not allowed');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.squares = config.squares;
|
|
62
|
+
this.amountPerSquare = config.amountPerSquare;
|
|
63
|
+
this.amounts = config.amounts;
|
|
64
|
+
|
|
65
|
+
// Validate amounts array if provided
|
|
66
|
+
if (this.amounts) {
|
|
67
|
+
if (this.amounts.length !== this.squares.length) {
|
|
68
|
+
throw new Error('Amounts array length must match squares array length');
|
|
69
|
+
}
|
|
70
|
+
for (const amount of this.amounts) {
|
|
71
|
+
if (amount <= 0n) {
|
|
72
|
+
throw new Error('All amounts must be greater than 0');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else if (this.amountPerSquare <= 0n) {
|
|
76
|
+
throw new Error('Amount per square must be greater than 0');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Always bet on the fixed squares
|
|
82
|
+
*/
|
|
83
|
+
protected async selectSquares(): Promise<number[]> {
|
|
84
|
+
return this.squares;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Use configured amounts
|
|
89
|
+
*/
|
|
90
|
+
protected async calculateAmounts(): Promise<bigint[]> {
|
|
91
|
+
if (this.amounts) {
|
|
92
|
+
return this.amounts;
|
|
93
|
+
}
|
|
94
|
+
return this.squares.map(() => this.amountPerSquare);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Log configuration
|
|
99
|
+
*/
|
|
100
|
+
protected logConfig(): void {
|
|
101
|
+
super.logConfig();
|
|
102
|
+
console.log(` Squares: ${this.squares.join(', ')}`);
|
|
103
|
+
const total = this.amounts
|
|
104
|
+
? this.amounts.reduce((sum, amt) => sum + amt, 0n)
|
|
105
|
+
: this.amountPerSquare * BigInt(this.squares.length);
|
|
106
|
+
console.log(` Amount per square: ${SlvrSDK.formatToken(this.amountPerSquare)} ETH`);
|
|
107
|
+
console.log(` Total bet per round: ${SlvrSDK.formatToken(total)} ETH`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Example usage
|
|
113
|
+
*/
|
|
114
|
+
export async function exampleFixedSquaresStrategy() {
|
|
115
|
+
const { createPublicClient, createWalletClient, http } = await import('viem');
|
|
116
|
+
const { defineChain } = await import('viem');
|
|
117
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
118
|
+
const { SlvrSDK } = await import('../src');
|
|
119
|
+
const { CONTRACTS, ROBINHOOD_CHAIN } = await import('./constants');
|
|
120
|
+
|
|
121
|
+
// Create clients
|
|
122
|
+
const robinhood = defineChain(ROBINHOOD_CHAIN);
|
|
123
|
+
const publicClient = createPublicClient({
|
|
124
|
+
chain: robinhood,
|
|
125
|
+
transport: http(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
129
|
+
if (!privateKey) {
|
|
130
|
+
throw new Error('PRIVATE_KEY environment variable is required');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
|
134
|
+
const walletClient = createWalletClient({
|
|
135
|
+
chain: robinhood,
|
|
136
|
+
transport: http(),
|
|
137
|
+
account,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Initialize SDK
|
|
141
|
+
const sdk = new SlvrSDK({
|
|
142
|
+
publicClient,
|
|
143
|
+
walletClient,
|
|
144
|
+
addresses: {
|
|
145
|
+
lottery: CONTRACTS.LOTTERY,
|
|
146
|
+
staking: CONTRACTS.STAKING,
|
|
147
|
+
token: CONTRACTS.TOKEN,
|
|
148
|
+
autoCommit: CONTRACTS.AUTO_COMMIT !== '0x...' ? CONTRACTS.AUTO_COMMIT : undefined,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Create strategy: bet on squares 0, 5, 10, 15, 20 every round
|
|
153
|
+
const strategy = new FixedSquaresStrategy(sdk, {
|
|
154
|
+
squares: [0, 5, 10, 15, 20], // Bet on these 5 squares
|
|
155
|
+
amountPerSquare: parseEther('1'), // 1 ETH per square
|
|
156
|
+
threshold: parseEther('100'), // Only bet when round has < 100 ETH
|
|
157
|
+
checkInterval: 5000, // Check every 5 seconds
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Start the strategy
|
|
161
|
+
await strategy.start();
|
|
162
|
+
|
|
163
|
+
// Handle graceful shutdown
|
|
164
|
+
process.on('SIGINT', () => {
|
|
165
|
+
console.log('\nReceived SIGINT, stopping strategy...');
|
|
166
|
+
strategy.stop();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
process.on('SIGTERM', () => {
|
|
171
|
+
console.log('\nReceived SIGTERM, stopping strategy...');
|
|
172
|
+
strategy.stop();
|
|
173
|
+
process.exit(0);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example exports
|
|
3
|
+
*
|
|
4
|
+
* Import and use these examples to understand how to use the Slvr SDK
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Base strategy class for creating custom strategies
|
|
8
|
+
export { BettingStrategy, StrategyConfig, BetDecision } from './strategy-base';
|
|
9
|
+
|
|
10
|
+
// Pre-built strategies
|
|
11
|
+
export { AutomatedBettingBot, exampleAutomatedBetting } from './automated-betting';
|
|
12
|
+
export { LeastAllocatedStrategy, exampleLeastAllocatedStrategy } from './least-allocated-strategy';
|
|
13
|
+
export { CombinedBettingBot, exampleCombinedStrategy } from './combined-strategy';
|
|
14
|
+
export { ExpectedValueBot, exampleExpectedValueBot } from './expected-value-strategy';
|
|
15
|
+
export type { ExpectedValueBotOptions } from './expected-value-strategy';
|
|
16
|
+
export { FixedSquaresStrategy, exampleFixedSquaresStrategy, FixedSquaresConfig } from './fixed-squares-strategy';
|
|
17
|
+
|
|
18
|
+
// Custom strategy examples
|
|
19
|
+
export {
|
|
20
|
+
LeastAllocatedCustomStrategy,
|
|
21
|
+
RandomSquaresStrategy,
|
|
22
|
+
MostBettorsStrategy,
|
|
23
|
+
WeightedAllocationStrategy,
|
|
24
|
+
ConditionalStrategy,
|
|
25
|
+
} from './custom-strategy-example';
|
|
26
|
+
|
|
27
|
+
// Utilities
|
|
28
|
+
export { simpleExample } from './simple-example';
|
|
29
|
+
export { CONTRACTS, ROBINHOOD_CHAIN } from './constants';
|
|
30
|
+
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Betting strategy that targets squares with least allocation
|
|
3
|
+
*
|
|
4
|
+
* This example shows how to:
|
|
5
|
+
* 1. Get all squares and their current allocations
|
|
6
|
+
* 2. Identify squares with the least funds
|
|
7
|
+
* 3. Distribute bets across these squares
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { SlvrSDK } from '../src';
|
|
11
|
+
import { parseEther } from 'viem';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Strategy that bets on squares with the least allocation
|
|
15
|
+
*/
|
|
16
|
+
export class LeastAllocatedStrategy {
|
|
17
|
+
private sdk: SlvrSDK;
|
|
18
|
+
|
|
19
|
+
constructor(sdk: SlvrSDK) {
|
|
20
|
+
this.sdk = sdk;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get squares sorted by allocation (least to most)
|
|
25
|
+
*/
|
|
26
|
+
async getSquaresByAllocation(roundId: bigint): Promise<
|
|
27
|
+
Array<{ square: number; total: bigint; bettors: bigint; percentage: number }>
|
|
28
|
+
> {
|
|
29
|
+
const squares = await this.sdk.lottery.getRoundSquares(roundId);
|
|
30
|
+
|
|
31
|
+
// Calculate total across all squares
|
|
32
|
+
const totalAllocation = squares.reduce((sum, sq) => sum + sq.total, 0n);
|
|
33
|
+
|
|
34
|
+
// Add percentage and sort by total (ascending)
|
|
35
|
+
const withPercentage = squares
|
|
36
|
+
.map(({ square, total, bettors }) => ({
|
|
37
|
+
square,
|
|
38
|
+
total,
|
|
39
|
+
bettors,
|
|
40
|
+
percentage: totalAllocation > 0n
|
|
41
|
+
? Number((total * 10000n) / totalAllocation) / 100
|
|
42
|
+
: 0,
|
|
43
|
+
}))
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
if (a.total < b.total) return -1;
|
|
46
|
+
if (a.total > b.total) return 1;
|
|
47
|
+
return 0;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return withPercentage;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the N squares with least allocation
|
|
55
|
+
*/
|
|
56
|
+
async getLeastAllocatedSquares(
|
|
57
|
+
roundId: bigint,
|
|
58
|
+
count: number = 5
|
|
59
|
+
): Promise<number[]> {
|
|
60
|
+
const sorted = await this.getSquaresByAllocation(roundId);
|
|
61
|
+
return sorted.slice(0, count).map(({ square }) => square);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calculate bet amounts for least allocated squares
|
|
66
|
+
* Distributes total bet amount evenly across selected squares
|
|
67
|
+
*/
|
|
68
|
+
calculateBetAmounts(
|
|
69
|
+
totalAmount: bigint,
|
|
70
|
+
squareCount: number
|
|
71
|
+
): bigint[] {
|
|
72
|
+
const amountPerSquare = totalAmount / BigInt(squareCount);
|
|
73
|
+
const remainder = totalAmount % BigInt(squareCount);
|
|
74
|
+
|
|
75
|
+
const amounts = Array(squareCount).fill(amountPerSquare);
|
|
76
|
+
|
|
77
|
+
// Add remainder to first square to ensure exact total
|
|
78
|
+
if (remainder > 0n) {
|
|
79
|
+
amounts[0] += remainder;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return amounts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Place a bet on least allocated squares
|
|
87
|
+
*/
|
|
88
|
+
async betOnLeastAllocated(
|
|
89
|
+
roundId: bigint,
|
|
90
|
+
totalBetAmount: bigint,
|
|
91
|
+
squareCount: number = 5
|
|
92
|
+
): Promise<`0x${string}`> {
|
|
93
|
+
if (!this.sdk.getWalletClient()) {
|
|
94
|
+
throw new Error('Wallet client required for betting');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get squares with least allocation
|
|
98
|
+
const squares = await this.getLeastAllocatedSquares(roundId, squareCount);
|
|
99
|
+
|
|
100
|
+
if (squares.length === 0) {
|
|
101
|
+
throw new Error('No squares available');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Calculate amounts
|
|
105
|
+
const amounts = this.calculateBetAmounts(totalBetAmount, squares.length);
|
|
106
|
+
|
|
107
|
+
console.log(`🎯 Betting on ${squares.length} least allocated squares:`);
|
|
108
|
+
squares.forEach((square, i) => {
|
|
109
|
+
console.log(` Square ${square}: ${SlvrSDK.formatToken(amounts[i])} ETH`);
|
|
110
|
+
});
|
|
111
|
+
console.log(`💰 Total: ${SlvrSDK.formatToken(totalBetAmount)} ETH`);
|
|
112
|
+
|
|
113
|
+
// Place the bet
|
|
114
|
+
return await this.sdk.lottery.bet({
|
|
115
|
+
roundId,
|
|
116
|
+
squares,
|
|
117
|
+
amounts,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get detailed analysis of square allocations
|
|
123
|
+
*/
|
|
124
|
+
async analyzeRound(roundId: bigint): Promise<{
|
|
125
|
+
roundId: bigint;
|
|
126
|
+
totalAllocation: bigint;
|
|
127
|
+
squares: Array<{
|
|
128
|
+
square: number;
|
|
129
|
+
total: bigint;
|
|
130
|
+
bettors: bigint;
|
|
131
|
+
percentage: number;
|
|
132
|
+
rank: number;
|
|
133
|
+
}>;
|
|
134
|
+
leastAllocated: number[];
|
|
135
|
+
mostAllocated: number[];
|
|
136
|
+
}> {
|
|
137
|
+
const sorted = await this.getSquaresByAllocation(roundId);
|
|
138
|
+
const totalAllocation = sorted.reduce((sum, sq) => sum + sq.total, 0n);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
roundId,
|
|
142
|
+
totalAllocation,
|
|
143
|
+
squares: sorted.map((sq, index) => ({
|
|
144
|
+
...sq,
|
|
145
|
+
rank: index + 1,
|
|
146
|
+
})),
|
|
147
|
+
leastAllocated: sorted.slice(0, 5).map(({ square }) => square),
|
|
148
|
+
mostAllocated: sorted.slice(-5).reverse().map(({ square }) => square),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Example usage
|
|
155
|
+
*
|
|
156
|
+
* Set PRIVATE_KEY environment variable to enable betting
|
|
157
|
+
*/
|
|
158
|
+
export async function exampleLeastAllocatedStrategy() {
|
|
159
|
+
const { createPublicClient, createWalletClient, http } = await import('viem');
|
|
160
|
+
const { defineChain } = await import('viem');
|
|
161
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
162
|
+
const { parseEther } = await import('viem');
|
|
163
|
+
const { SlvrSDK } = await import('../src');
|
|
164
|
+
const { CONTRACTS, ROBINHOOD_CHAIN } = await import('./constants');
|
|
165
|
+
|
|
166
|
+
// Create clients
|
|
167
|
+
const robinhood = defineChain(ROBINHOOD_CHAIN);
|
|
168
|
+
const publicClient = createPublicClient({
|
|
169
|
+
chain: robinhood,
|
|
170
|
+
transport: http(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
174
|
+
let walletClient: ReturnType<typeof createWalletClient> | undefined;
|
|
175
|
+
|
|
176
|
+
if (privateKey) {
|
|
177
|
+
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
|
178
|
+
walletClient = createWalletClient({
|
|
179
|
+
chain: robinhood,
|
|
180
|
+
transport: http(),
|
|
181
|
+
account,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Initialize SDK
|
|
186
|
+
const sdk = new SlvrSDK({
|
|
187
|
+
publicClient,
|
|
188
|
+
walletClient,
|
|
189
|
+
addresses: {
|
|
190
|
+
lottery: CONTRACTS.LOTTERY,
|
|
191
|
+
staking: CONTRACTS.STAKING,
|
|
192
|
+
token: CONTRACTS.TOKEN,
|
|
193
|
+
autoCommit: CONTRACTS.AUTO_COMMIT !== '0x...' ? CONTRACTS.AUTO_COMMIT : undefined,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const strategy = new LeastAllocatedStrategy(sdk);
|
|
198
|
+
const roundId = await sdk.lottery.currentRoundId();
|
|
199
|
+
|
|
200
|
+
// Analyze the round
|
|
201
|
+
const analysis = await strategy.analyzeRound(roundId);
|
|
202
|
+
console.log('Round Analysis:', {
|
|
203
|
+
totalAllocation: SlvrSDK.formatToken(analysis.totalAllocation),
|
|
204
|
+
leastAllocated: analysis.leastAllocated,
|
|
205
|
+
mostAllocated: analysis.mostAllocated,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Get squares with least allocation
|
|
209
|
+
const leastAllocated = await strategy.getLeastAllocatedSquares(roundId, 5);
|
|
210
|
+
console.log('Least allocated squares:', leastAllocated);
|
|
211
|
+
|
|
212
|
+
// Place bet on these squares (if wallet client is available)
|
|
213
|
+
if (walletClient) {
|
|
214
|
+
const txHash = await strategy.betOnLeastAllocated(
|
|
215
|
+
roundId,
|
|
216
|
+
parseEther('5'), // 5 ETH total
|
|
217
|
+
5 // Bet on 5 squares
|
|
218
|
+
);
|
|
219
|
+
console.log('Bet placed:', txHash);
|
|
220
|
+
} else {
|
|
221
|
+
console.log('Cannot place bet - set PRIVATE_KEY environment variable');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|