@bordrless/shared 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/api.d.ts +719 -6
- package/dist/api.js +23 -5
- package/dist/explorer.d.ts +21 -0
- package/dist/explorer.js +94 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/policy.d.ts +139 -15
- package/dist/policy.js +279 -24
- package/dist/programs.d.ts +18 -0
- package/dist/programs.js +18 -0
- package/dist/studio.d.ts +180 -7
- package/dist/studio.js +200 -4
- package/dist/workshop.d.ts +352 -0
- package/dist/workshop.js +21 -0
- package/package.json +1 -1
package/dist/studio.d.ts
CHANGED
|
@@ -20,8 +20,70 @@ export interface StudioFile {
|
|
|
20
20
|
path: string;
|
|
21
21
|
content: string;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* What a project becomes: a config of existing rules alone, or a config with its own hook program.
|
|
25
|
+
* A hook project whose state starts with the game ticket standard's header is a game hook
|
|
26
|
+
* (docs/games.md "Phase 2"): read from its source (`detectGameSettings`), never chosen, and its
|
|
27
|
+
* config launches a jackpot, streak or lottery coin through the companion.
|
|
28
|
+
*
|
|
29
|
+
* `script` (2026-10-12, docs/scripts.md): a Workshop companion's script, the program a companion
|
|
30
|
+
* calls to spend its pot. Only the Workshop makes one (`POST /v1/workshop/submit` with kind
|
|
31
|
+
* companion); it goes through the same pipeline with a script's own checks, review and build (no
|
|
32
|
+
* trading test), is deployed the same way, and is approved on chain instead of listed as a config.
|
|
33
|
+
*/
|
|
34
|
+
export type StudioKind = 'config' | 'hook' | 'script';
|
|
35
|
+
/** The kinds of game a game hook keeps the score of (the companion's `GameKind`). */
|
|
36
|
+
export type StudioGameKind = 'lottery' | 'jackpot' | 'streak';
|
|
37
|
+
export declare const STUDIO_GAME_KINDS: readonly StudioGameKind[];
|
|
38
|
+
/**
|
|
39
|
+
* A game hook's companion draft (docs/games.md "The companion's game", phase 2): what the launch
|
|
40
|
+
* fixes with `create_game_v2` besides the hook's own constants. The split with the pot: the pot's
|
|
41
|
+
* part and the buyback's, the launcher's what is left (no holders' part: a game coin has no kit).
|
|
42
|
+
* The kind's settings (the timer, the minimum buy; the epoch, the minimum streak and weight; a
|
|
43
|
+
* lottery's rounds) are the hook's constants, read from its source (`detectGameSettings`), never
|
|
44
|
+
* typed: the companion checks them against the hook's kind header.
|
|
45
|
+
*/
|
|
46
|
+
export interface StudioGameDraft {
|
|
47
|
+
potBps: number;
|
|
48
|
+
buybackBps: number;
|
|
49
|
+
/** No settle, close or draw while the pot holds less: 0.1 to 1,000 SOL. */
|
|
50
|
+
minPotLamports: string;
|
|
51
|
+
/** The part of the pot a settle, an epoch or a draw pays: 10% to 100%. */
|
|
52
|
+
prizeBps: number;
|
|
53
|
+
/** Streak: the least a close leaves for claims (5 minutes to a day, at most half an epoch); lottery: each attempt's window; 0 for a jackpot. */
|
|
54
|
+
claimWindowSecs: number;
|
|
55
|
+
/** Lottery only: attempts per draw (1 to 16); 0 otherwise. */
|
|
56
|
+
maxAttempts: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A game hook's kind and settings as its source fixes them (p2-n: the starters' settings are their
|
|
60
|
+
* code's): the state struct's first field is the crate's `GameHeader`, its second the kind header
|
|
61
|
+
* (`JackpotHeader` / `StreakHeader`; neither: a lottery), and the settings are what the headers are
|
|
62
|
+
* made with (`JackpotHeader::new(timer, min_tokens)`, `StreakHeader::new(min_streak, min_weight)`,
|
|
63
|
+
* `GameHeader::new(mint, round_secs, now)`). Amounts are base units as strings.
|
|
64
|
+
*/
|
|
65
|
+
export interface StudioGameSettings {
|
|
66
|
+
kind: StudioGameKind;
|
|
67
|
+
/** Jackpot: a round ends this long after its last qualifying buy. 0 otherwise. */
|
|
68
|
+
timerSecs: number;
|
|
69
|
+
/** Jackpot: the least a qualifying buy delivers. '0' otherwise. */
|
|
70
|
+
minTokens: string;
|
|
71
|
+
/** Streak: the epoch; lottery: the round. 0 for a jackpot. */
|
|
72
|
+
roundSecs: number;
|
|
73
|
+
/** Streak: by an epoch's end a holding must have sent nothing for this long. 0 otherwise. */
|
|
74
|
+
minStreakSecs: number;
|
|
75
|
+
/** Streak: the least weight that shares. '0' otherwise. */
|
|
76
|
+
minWeight: string;
|
|
77
|
+
}
|
|
78
|
+
/** The hook flags every game hook runs on: `BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA` (`GameKind::hook_flags`, 145), the only set the companion's launch accepts. */
|
|
79
|
+
export declare const GAME_HOOK_FLAGS = 145;
|
|
80
|
+
/**
|
|
81
|
+
* The companion draft a game hook starts with (the docs' examples: 70% of the fee to the pot, 30%
|
|
82
|
+
* bought back; a jackpot paying half the pot from 0.5 SOL; a streak sharing the whole pot from 0.1
|
|
83
|
+
* SOL with an hour at least for claims; a lottery paying the whole pot from 0.5 SOL, 8 attempts of
|
|
84
|
+
* 10 minutes).
|
|
85
|
+
*/
|
|
86
|
+
export declare function gameDraftDefaults(kind: StudioGameKind): StudioGameDraft;
|
|
25
87
|
/** The launch config a project makes (the arguments of `create_config` / `create_listed_config`). */
|
|
26
88
|
export interface StudioConfigDraft {
|
|
27
89
|
/** At most 32 bytes. */
|
|
@@ -38,8 +100,10 @@ export interface StudioConfigDraft {
|
|
|
38
100
|
earlyLockSecs: number;
|
|
39
101
|
/** `TOKEN_HOOK_FLAGS` the hook runs on, for a `hook` project; 0 for a `config` project. */
|
|
40
102
|
hookFlags: number;
|
|
41
|
-
/** List it on the marketplace (made with `create_listed_config`). */
|
|
103
|
+
/** List it on the marketplace (made with `create_listed_config`). Never for a game hook: the companion refuses a config that pays an author. */
|
|
42
104
|
listed: boolean;
|
|
105
|
+
/** A game hook's companion draft (the split with the pot, the minimum pot, the prize, the claim window); null or absent otherwise. The backend fills it with the kind's defaults when the source becomes a game hook. */
|
|
106
|
+
game?: StudioGameDraft | null;
|
|
43
107
|
/**
|
|
44
108
|
* The author's share of the creator fee on others' launches, bps of it. Not the author's to choose:
|
|
45
109
|
* the backend holds it at `AUTHOR_SHARE_BPS` when listed and 0 when not, whatever is sent.
|
|
@@ -65,6 +129,12 @@ export interface StudioProject extends StudioProjectSummary {
|
|
|
65
129
|
description: string;
|
|
66
130
|
files: StudioFile[];
|
|
67
131
|
draft: StudioConfigDraft;
|
|
132
|
+
/**
|
|
133
|
+
* A hook project's game, as its source reads (`detectGameSettings`): its kind and the constants
|
|
134
|
+
* the companion will check against the hook's kind header. Null when the source is not a game
|
|
135
|
+
* hook (no `GameHeader` state, or settings that can't be read); absent for a config project.
|
|
136
|
+
*/
|
|
137
|
+
game?: StudioGameSettings | null;
|
|
68
138
|
/** The last review of the current source; null when the source changed since. */
|
|
69
139
|
review: StudioReview | null;
|
|
70
140
|
/** The last build of the current source; null when the source changed since. */
|
|
@@ -75,7 +145,7 @@ export interface StudioProject extends StudioProjectSummary {
|
|
|
75
145
|
export interface StudioProjectCreateRequest {
|
|
76
146
|
name: string;
|
|
77
147
|
kind: StudioKind;
|
|
78
|
-
/** Start from a template (`STUDIO_STARTERS`), or blank. */
|
|
148
|
+
/** Start from a template (`STUDIO_STARTERS`), or blank; the game starters make a game hook. */
|
|
79
149
|
starter?: string;
|
|
80
150
|
}
|
|
81
151
|
/** A save: any of the fields; files replace the whole set. Changing files drops the review and the build. */
|
|
@@ -203,6 +273,12 @@ export interface StudioBuild {
|
|
|
203
273
|
at: number;
|
|
204
274
|
/** Studio's trading test of what was built (the worker's simulator): a deploy needs `pass`. Absent on builds from before it. */
|
|
205
275
|
sim?: StudioSim | null;
|
|
276
|
+
/**
|
|
277
|
+
* The Studio template's commit the worker built against (40 hex characters; the worker's
|
|
278
|
+
* `STUDIO_TEMPLATE_COMMIT`). Studio's attestation records it, so anyone can rebuild the hook.
|
|
279
|
+
* Absent on builds from before it, or from a worker that does not report it.
|
|
280
|
+
*/
|
|
281
|
+
templateCommit?: string | null;
|
|
206
282
|
}
|
|
207
283
|
/** The simulator's verdict on a build: launched, bought, sold and sent by several wallets over a simulated year. */
|
|
208
284
|
export interface StudioSim {
|
|
@@ -214,8 +290,80 @@ export interface StudioSim {
|
|
|
214
290
|
capBps: number | null;
|
|
215
291
|
/** Operations the hook refused. */
|
|
216
292
|
refusedTotal: number;
|
|
293
|
+
/** The simulator's version (recorded in Studio's attestation); absent from workers that don't report it. */
|
|
294
|
+
version?: number | null;
|
|
217
295
|
/** What the test can't see (amount triggers, wallets it never used, after a year). */
|
|
218
296
|
notes: string[];
|
|
297
|
+
/** A game hook's scenarios (the jackpot's or the streak's, docs/games-handover.md phase 2 item 4): absent on a hook that is not a game's, or on builds from before them. */
|
|
298
|
+
game?: StudioSimGame | null;
|
|
299
|
+
}
|
|
300
|
+
/** The simulator's game scenarios on a game hook: the header and the marks or weights checked against a plain model of the rules, and the fairness of the payouts the rules would make. */
|
|
301
|
+
export interface StudioSimGame {
|
|
302
|
+
kind: StudioGameKind;
|
|
303
|
+
pass: boolean;
|
|
304
|
+
/** Each check that failed, in the simulator's words (the first few). */
|
|
305
|
+
failed: string[];
|
|
306
|
+
/** Rounds (a jackpot) or epochs (a streak) the scenario ran. */
|
|
307
|
+
periods: number;
|
|
308
|
+
/** The largest difference between a wallet's share of the payouts and its share of what it held, in bps, over the fairness run; null when it did not run. */
|
|
309
|
+
fairnessMaxBps: number | null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* `POST /v1/studio/verify` (docs/phase3a-handover.md item 6; the open CLI sends it without the
|
|
313
|
+
* site's key): a program checked against Studio's build record of it and the code on chain. `files`
|
|
314
|
+
* is the caller's copy of the source (`src/**.rs`, as a Studio project keeps them), hashed and
|
|
315
|
+
* compared with the source Studio built; `commit` the template commit the caller built against.
|
|
316
|
+
* `source` is the CLI's directory name, kept for the log only.
|
|
317
|
+
*/
|
|
318
|
+
export interface StudioVerifyRequest {
|
|
319
|
+
kind?: 'hook' | 'strategy';
|
|
320
|
+
program: Address;
|
|
321
|
+
commit?: string;
|
|
322
|
+
source?: string;
|
|
323
|
+
files?: StudioFile[];
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The verdict: `verified` when the code on chain is Studio's build of the source Studio holds (and,
|
|
327
|
+
* when sent, the caller's source and commit are that build's); `mismatch` when something Studio
|
|
328
|
+
* holds differs from the chain or from what the caller sent; `unverifiable` when nothing can be
|
|
329
|
+
* said: no Studio build record for this program, or no code on chain. `rebuilt` is false: this route
|
|
330
|
+
* compares records, it does not yet rebuild the source at the commit (the handover's full
|
|
331
|
+
* semantics), so a program Studio never built is never `verified` here.
|
|
332
|
+
*/
|
|
333
|
+
export interface StudioVerifyResponse {
|
|
334
|
+
program: Address;
|
|
335
|
+
verdict: 'verified' | 'mismatch' | 'unverifiable';
|
|
336
|
+
/** Why, in sentences the CLI prints. */
|
|
337
|
+
reasons: string[];
|
|
338
|
+
/** `solana-verify`'s executable hash of the code on chain; null when the program is not deployed through the upgradeable loader. */
|
|
339
|
+
onChainHash: string | null;
|
|
340
|
+
/** Studio's build record of this program id, when there is one. */
|
|
341
|
+
build: {
|
|
342
|
+
hash: string;
|
|
343
|
+
sourceHash: string;
|
|
344
|
+
templateCommit: string | null;
|
|
345
|
+
at: number;
|
|
346
|
+
project: string;
|
|
347
|
+
stage: string;
|
|
348
|
+
} | null;
|
|
349
|
+
/** sha256 of the `files` sent (null without files). */
|
|
350
|
+
sourceHash: string | null;
|
|
351
|
+
/** Whether the source sent is the source Studio built (null without files or without a build). */
|
|
352
|
+
sourceMatchesBuild: boolean | null;
|
|
353
|
+
/** Whether the commit sent is the template commit the build recorded (null without a commit, a build, or a recorded commit). */
|
|
354
|
+
commitMatchesBuild: boolean | null;
|
|
355
|
+
/** Studio's on-chain `HookAttestation` of the program, when there is one. */
|
|
356
|
+
attestation: {
|
|
357
|
+
buildHash: string;
|
|
358
|
+
sourceHash: string;
|
|
359
|
+
templateCommit: string;
|
|
360
|
+
kind: string;
|
|
361
|
+
review: 'pass' | 'warn';
|
|
362
|
+
revoked: boolean;
|
|
363
|
+
attestedAt: number;
|
|
364
|
+
matchesChain: boolean;
|
|
365
|
+
} | null;
|
|
366
|
+
rebuilt: false;
|
|
219
367
|
}
|
|
220
368
|
/** `POST /v1/studio/projects/:id/deploy/quote`: what deploying the built hook costs, in lamports. */
|
|
221
369
|
export interface StudioDeployQuote {
|
|
@@ -364,8 +512,10 @@ export declare const HOOK_AUTHORITY_CODE = "hook_upgrade_authority";
|
|
|
364
512
|
export declare const hookFinalCommand: (program?: string) => string;
|
|
365
513
|
/**
|
|
366
514
|
* The launch program's rule for a config's own hook (`create_config`, docs/hooks-v2.md §5.8), on its
|
|
367
|
-
* upgrade info: accepted when nobody can upgrade it (`upgradeable: false`),
|
|
368
|
-
* authority is one of `HOOK_UPGRADE_AUTHORITIES` (Bordrless Studio's key or the protocol's)
|
|
515
|
+
* upgrade info: accepted when nobody can upgrade it (`upgradeable: false`), when its upgrade
|
|
516
|
+
* authority is one of `HOOK_UPGRADE_AUTHORITIES` (Bordrless Studio's key or the protocol's), or when
|
|
517
|
+
* it is its own `hook_timelock` account (`timelocked`, phase 3a: the caller derives
|
|
518
|
+
* `PDA(["timelock", hook], hook_timelock)` and compares; the launch program also checks the delay). Only a
|
|
369
519
|
* hook some other key is known to be able to upgrade is refused: one whose upgrade info could not be
|
|
370
520
|
* read is not (the launch program checked it when the config was made; the SDK's
|
|
371
521
|
* `hookAuthorityProblem` and the marketplace watch read it the same way). A config without a hook
|
|
@@ -374,6 +524,7 @@ export declare const hookFinalCommand: (program?: string) => string;
|
|
|
374
524
|
export declare function hookAuthorityAccepted(customHook: string | null, hook: {
|
|
375
525
|
upgradeAuthority: string | null;
|
|
376
526
|
upgradeable: boolean | null;
|
|
527
|
+
timelocked?: boolean;
|
|
377
528
|
} | null): boolean;
|
|
378
529
|
/** `bordrless_launch::constants::MAX_AUTHOR_SHARE_BPS`: half the creator fee, the most the program lets a listed config pay its author. */
|
|
379
530
|
export declare const MAX_AUTHOR_SHARE_BPS = 5000;
|
|
@@ -400,12 +551,20 @@ export declare const STUDIO_LIMITS: {
|
|
|
400
551
|
readonly bytes: 120000;
|
|
401
552
|
readonly messageChars: 8000;
|
|
402
553
|
};
|
|
403
|
-
/**
|
|
554
|
+
/**
|
|
555
|
+
* Starters a new hook project can begin from (the template's `src/lib.rs` variants). Those with
|
|
556
|
+
* `game` make a game hook (the reference game hooks of docs/games.md "Studio game hooks", written on
|
|
557
|
+
* the `bordrless-game` crate). A Studio lottery starter is not written yet (it would be
|
|
558
|
+
* `lottery_hook`'s rules with the standard `prepare` and `ROUND_SECS` as a constant).
|
|
559
|
+
*/
|
|
404
560
|
export declare const STUDIO_STARTERS: readonly {
|
|
405
561
|
id: string;
|
|
406
562
|
name: string;
|
|
407
563
|
blurb: string;
|
|
564
|
+
game?: StudioGameKind;
|
|
408
565
|
}[];
|
|
566
|
+
/** What each game kind is called on the site: "Jackpot coin". */
|
|
567
|
+
export declare const GAME_KIND_WORDS: Readonly<Record<StudioGameKind | 'strategy', string>>;
|
|
409
568
|
/**
|
|
410
569
|
* The token hook flags a Studio hook needs, read from its own source (never chosen by hand): each
|
|
411
570
|
* callback whose body the author changed from the template's stub; cuts (TRANSFER_RETURNS_DELTA)
|
|
@@ -419,3 +578,17 @@ export declare function detectHookFlags(files: readonly {
|
|
|
419
578
|
path: string;
|
|
420
579
|
content: string;
|
|
421
580
|
}[]): number;
|
|
581
|
+
/**
|
|
582
|
+
* A game hook's kind and settings from its source (docs/games.md "Studio game hooks", p2-n):
|
|
583
|
+
* the state struct whose first field is the crate's `GameHeader` names the kind by its second
|
|
584
|
+
* field (`JackpotHeader`, `StreakHeader`; neither: a lottery), and the settings are the arguments
|
|
585
|
+
* the headers are made with in `prepare`: `JackpotHeader::new(timer_secs, min_tokens)`,
|
|
586
|
+
* `StreakHeader::new(min_streak_secs, min_weight)` and `GameHeader::new(mint, round_secs, now)`,
|
|
587
|
+
* each a constant expression (`const` names followed). Null when the source is not a game hook, or
|
|
588
|
+
* a setting can't be read from it (the backend then refuses the config: the companion would check
|
|
589
|
+
* the setting against the hook's header and refuse the setup).
|
|
590
|
+
*/
|
|
591
|
+
export declare function detectGameSettings(files: readonly {
|
|
592
|
+
path: string;
|
|
593
|
+
content: string;
|
|
594
|
+
}[]): StudioGameSettings | null;
|
package/dist/studio.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { TOKEN_HOOK_FLAGS as HOOK_FLAG_BITS } from "./programs.js";
|
|
2
|
+
export const STUDIO_GAME_KINDS = ['lottery', 'jackpot', 'streak'];
|
|
3
|
+
/** The hook flags every game hook runs on: `BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA` (`GameKind::hook_flags`, 145), the only set the companion's launch accepts. */
|
|
4
|
+
export const GAME_HOOK_FLAGS = 145;
|
|
5
|
+
/**
|
|
6
|
+
* The companion draft a game hook starts with (the docs' examples: 70% of the fee to the pot, 30%
|
|
7
|
+
* bought back; a jackpot paying half the pot from 0.5 SOL; a streak sharing the whole pot from 0.1
|
|
8
|
+
* SOL with an hour at least for claims; a lottery paying the whole pot from 0.5 SOL, 8 attempts of
|
|
9
|
+
* 10 minutes).
|
|
10
|
+
*/
|
|
11
|
+
export function gameDraftDefaults(kind) {
|
|
12
|
+
if (kind === 'jackpot')
|
|
13
|
+
return { potBps: 7_000, buybackBps: 3_000, minPotLamports: '500000000', prizeBps: 5_000, claimWindowSecs: 0, maxAttempts: 0 };
|
|
14
|
+
if (kind === 'streak')
|
|
15
|
+
return { potBps: 7_000, buybackBps: 3_000, minPotLamports: '100000000', prizeBps: 10_000, claimWindowSecs: 3_600, maxAttempts: 0 };
|
|
16
|
+
return { potBps: 7_000, buybackBps: 3_000, minPotLamports: '500000000', prizeBps: 10_000, claimWindowSecs: 600, maxAttempts: 8 };
|
|
17
|
+
}
|
|
2
18
|
/** The message a wallet signs to use Studio: no transaction, nothing moves. */
|
|
3
19
|
export function studioSignInMessage(wallet, nonce, issuedAt) {
|
|
4
|
-
return ['Sign in to Bordrless
|
|
20
|
+
return ['Sign in to the Bordrless Workshop', '', 'This signature proves you own this wallet. It does not move funds or approve any transaction.', '', `Wallet: ${wallet}`, `Nonce: ${nonce}`, `Issued: ${issuedAt}`].join('\n');
|
|
5
21
|
}
|
|
6
22
|
/** The header the session token travels in, browser → site proxy → backend. */
|
|
7
23
|
export const STUDIO_SESSION_HEADER = 'x-studio-session';
|
|
@@ -23,8 +39,10 @@ export const HOOK_AUTHORITY_CODE = 'hook_upgrade_authority';
|
|
|
23
39
|
export const hookFinalCommand = (program = '<PROGRAM_ID>') => `solana program set-upgrade-authority ${program} --final`;
|
|
24
40
|
/**
|
|
25
41
|
* The launch program's rule for a config's own hook (`create_config`, docs/hooks-v2.md §5.8), on its
|
|
26
|
-
* upgrade info: accepted when nobody can upgrade it (`upgradeable: false`),
|
|
27
|
-
* authority is one of `HOOK_UPGRADE_AUTHORITIES` (Bordrless Studio's key or the protocol's)
|
|
42
|
+
* upgrade info: accepted when nobody can upgrade it (`upgradeable: false`), when its upgrade
|
|
43
|
+
* authority is one of `HOOK_UPGRADE_AUTHORITIES` (Bordrless Studio's key or the protocol's), or when
|
|
44
|
+
* it is its own `hook_timelock` account (`timelocked`, phase 3a: the caller derives
|
|
45
|
+
* `PDA(["timelock", hook], hook_timelock)` and compares; the launch program also checks the delay). Only a
|
|
28
46
|
* hook some other key is known to be able to upgrade is refused: one whose upgrade info could not be
|
|
29
47
|
* read is not (the launch program checked it when the config was made; the SDK's
|
|
30
48
|
* `hookAuthorityProblem` and the marketplace watch read it the same way). A config without a hook
|
|
@@ -33,6 +51,9 @@ export const hookFinalCommand = (program = '<PROGRAM_ID>') => `solana program se
|
|
|
33
51
|
export function hookAuthorityAccepted(customHook, hook) {
|
|
34
52
|
if (customHook === null || hook === null || hook.upgradeable !== true || hook.upgradeAuthority === null)
|
|
35
53
|
return true;
|
|
54
|
+
// Phase 3a: a hook behind its own `hook_timelock` (a public delay of at least 3 days) is accepted too.
|
|
55
|
+
if (hook.timelocked === true)
|
|
56
|
+
return true;
|
|
36
57
|
return HOOK_UPGRADE_AUTHORITIES.includes(hook.upgradeAuthority);
|
|
37
58
|
}
|
|
38
59
|
/** `bordrless_launch::constants::MAX_AUTHOR_SHARE_BPS`: half the creator fee, the most the program lets a listed config pay its author. */
|
|
@@ -53,12 +74,21 @@ export const AUTHOR_SHARE_BPS = 5_000;
|
|
|
53
74
|
export const STUDIO_PREPARE = { instruction: 'prepare', stateSeed: 'state' };
|
|
54
75
|
/** Limits of a project's source. */
|
|
55
76
|
export const STUDIO_LIMITS = { files: 8, bytes: 120_000, messageChars: 8_000 };
|
|
56
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Starters a new hook project can begin from (the template's `src/lib.rs` variants). Those with
|
|
79
|
+
* `game` make a game hook (the reference game hooks of docs/games.md "Studio game hooks", written on
|
|
80
|
+
* the `bordrless-game` crate). A Studio lottery starter is not written yet (it would be
|
|
81
|
+
* `lottery_hook`'s rules with the standard `prepare` and `ROUND_SECS` as a constant).
|
|
82
|
+
*/
|
|
57
83
|
export const STUDIO_STARTERS = [
|
|
58
84
|
{ id: 'blank', name: 'Blank hook', blurb: 'The template: every callback stubbed, nothing taken, nothing refused.' },
|
|
59
85
|
{ id: 'sell-tax', name: 'Sell tax to a wallet', blurb: 'A cut of every sell sent to a wallet you choose; buys and transfers pass free.' },
|
|
60
86
|
{ id: 'cooldown', name: 'Transfer cooldown', blurb: 'A holding that received tokens cannot send them on for a while, stamped in its hook data.' },
|
|
87
|
+
{ id: 'jackpot', game: 'jackpot', name: 'Last-buyer jackpot', blurb: 'Every buy of at least MIN_TOKENS restarts a TIMER_SECS countdown; when it runs out, the last buyer who still holds is paid a share of the pot.' },
|
|
88
|
+
{ id: 'streak', game: 'streak', name: 'Diamond-hands streak', blurb: 'Each epoch, part of the pot is shared among the holders who held through it without sending a token, in proportion to what they held.' },
|
|
61
89
|
];
|
|
90
|
+
/** What each game kind is called on the site: "Jackpot coin". */
|
|
91
|
+
export const GAME_KIND_WORDS = { lottery: 'Lottery coin', jackpot: 'Jackpot coin', streak: 'Streak coin', strategy: 'Strategy coin' };
|
|
62
92
|
// ---- the hook's flags, read from its source ---------------------------------------------------------
|
|
63
93
|
/** Comments, string and char literals blanked (newlines kept), so only code is read. */
|
|
64
94
|
function codeOnly(src) {
|
|
@@ -168,3 +198,169 @@ export function detectHookFlags(files) {
|
|
|
168
198
|
flags |= HOOK_FLAG_BITS.WRITES_HOOK_DATA;
|
|
169
199
|
return flags;
|
|
170
200
|
}
|
|
201
|
+
// ---- a game hook's kind and settings, read from its source --------------------------------------------
|
|
202
|
+
/** The index just past the bracket matching the one at `open`, or the end. */
|
|
203
|
+
function closeOf(code, open) {
|
|
204
|
+
let depth = 0;
|
|
205
|
+
for (let j = open; j < code.length; j += 1) {
|
|
206
|
+
const ch = code[j];
|
|
207
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
208
|
+
depth += 1;
|
|
209
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
210
|
+
depth -= 1;
|
|
211
|
+
if (depth === 0)
|
|
212
|
+
return j + 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return code.length;
|
|
216
|
+
}
|
|
217
|
+
/** The arguments of the call whose opening parenthesis is at `open`, as written (split on commas at depth 0). */
|
|
218
|
+
function argsOf(code, open) {
|
|
219
|
+
const inner = code.slice(open + 1, closeOf(code, open) - 1);
|
|
220
|
+
const out = [];
|
|
221
|
+
let depth = 0;
|
|
222
|
+
let from = 0;
|
|
223
|
+
for (let j = 0; j < inner.length; j += 1) {
|
|
224
|
+
const ch = inner[j];
|
|
225
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
226
|
+
depth += 1;
|
|
227
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
228
|
+
depth -= 1;
|
|
229
|
+
else if (ch === ',' && depth === 0) {
|
|
230
|
+
out.push(inner.slice(from, j));
|
|
231
|
+
from = j + 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (inner.slice(from).trim())
|
|
235
|
+
out.push(inner.slice(from));
|
|
236
|
+
return out.map((a) => a.trim());
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* A constant expression of the source, evaluated: integer literals (with `_` and a type suffix),
|
|
240
|
+
* `+ - * /` and parentheses, and the names of other `const`s (followed up to a depth of 8). Null
|
|
241
|
+
* for anything else (a call, a runtime value, an unknown name, a division by zero).
|
|
242
|
+
*/
|
|
243
|
+
function evalConst(code, expr, depth = 0) {
|
|
244
|
+
const text = expr.replace(/\s+/g, '');
|
|
245
|
+
if (!text || depth > 8)
|
|
246
|
+
return null;
|
|
247
|
+
let i = 0;
|
|
248
|
+
const peek = () => text[i] ?? '';
|
|
249
|
+
const primary = () => {
|
|
250
|
+
if (peek() === '(') {
|
|
251
|
+
i += 1;
|
|
252
|
+
const v = sum();
|
|
253
|
+
if (peek() !== ')')
|
|
254
|
+
return null;
|
|
255
|
+
i += 1;
|
|
256
|
+
return v;
|
|
257
|
+
}
|
|
258
|
+
if (peek() === '-') {
|
|
259
|
+
i += 1;
|
|
260
|
+
const v = primary();
|
|
261
|
+
return v === null ? null : -v;
|
|
262
|
+
}
|
|
263
|
+
const lit = /^(0x[0-9a-fA-F_]+|\d[\d_]*)(u8|u16|u32|u64|u128|i8|i16|i32|i64|i128|usize|isize)?/.exec(text.slice(i));
|
|
264
|
+
if (lit) {
|
|
265
|
+
i += lit[0].length;
|
|
266
|
+
try {
|
|
267
|
+
return BigInt(lit[1].replace(/_/g, ''));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const name = /^(?:(?:crate|self|super)::)*([A-Za-z_][A-Za-z0-9_]*)/.exec(text.slice(i));
|
|
274
|
+
if (!name)
|
|
275
|
+
return null;
|
|
276
|
+
i += name[0].length;
|
|
277
|
+
const def = new RegExp(`\\bconst\\s+${name[1]}\\s*:\\s*[A-Za-z0-9_:<>]+\\s*=([^;]*);`).exec(code);
|
|
278
|
+
if (!def)
|
|
279
|
+
return null;
|
|
280
|
+
return evalConst(code, def[1], depth + 1);
|
|
281
|
+
};
|
|
282
|
+
const product = () => {
|
|
283
|
+
let v = primary();
|
|
284
|
+
while (v !== null && (peek() === '*' || peek() === '/')) {
|
|
285
|
+
const op = peek();
|
|
286
|
+
i += 1;
|
|
287
|
+
const r = primary();
|
|
288
|
+
if (r === null)
|
|
289
|
+
return null;
|
|
290
|
+
if (op === '/') {
|
|
291
|
+
if (r === 0n)
|
|
292
|
+
return null;
|
|
293
|
+
v /= r;
|
|
294
|
+
}
|
|
295
|
+
else
|
|
296
|
+
v *= r;
|
|
297
|
+
}
|
|
298
|
+
return v;
|
|
299
|
+
};
|
|
300
|
+
const sum = () => {
|
|
301
|
+
let v = product();
|
|
302
|
+
while (v !== null && (peek() === '+' || peek() === '-')) {
|
|
303
|
+
const op = peek();
|
|
304
|
+
i += 1;
|
|
305
|
+
const r = product();
|
|
306
|
+
if (r === null)
|
|
307
|
+
return null;
|
|
308
|
+
v = op === '+' ? v + r : v - r;
|
|
309
|
+
}
|
|
310
|
+
return v;
|
|
311
|
+
};
|
|
312
|
+
const v = sum();
|
|
313
|
+
return i === text.length ? v : null;
|
|
314
|
+
}
|
|
315
|
+
/** The arguments `Name::new(…)` is called with in `code` (the first call), evaluated; null when there is none or one can't be read. */
|
|
316
|
+
function newArgs(code, name, count) {
|
|
317
|
+
const m = new RegExp(`\\b${name}\\s*::\\s*new\\s*\\(`).exec(code);
|
|
318
|
+
if (!m)
|
|
319
|
+
return null;
|
|
320
|
+
const args = argsOf(code, m.index + m[0].length - 1);
|
|
321
|
+
if (args.length !== count)
|
|
322
|
+
return null;
|
|
323
|
+
return args.map((a) => evalConst(code, a));
|
|
324
|
+
}
|
|
325
|
+
const within = (v, max) => v !== null && v >= 0n && v <= max;
|
|
326
|
+
const U32 = 0xffffffffn;
|
|
327
|
+
const U64 = 0xffffffffffffffffn;
|
|
328
|
+
/**
|
|
329
|
+
* A game hook's kind and settings from its source (docs/games.md "Studio game hooks", p2-n):
|
|
330
|
+
* the state struct whose first field is the crate's `GameHeader` names the kind by its second
|
|
331
|
+
* field (`JackpotHeader`, `StreakHeader`; neither: a lottery), and the settings are the arguments
|
|
332
|
+
* the headers are made with in `prepare`: `JackpotHeader::new(timer_secs, min_tokens)`,
|
|
333
|
+
* `StreakHeader::new(min_streak_secs, min_weight)` and `GameHeader::new(mint, round_secs, now)`,
|
|
334
|
+
* each a constant expression (`const` names followed). Null when the source is not a game hook, or
|
|
335
|
+
* a setting can't be read from it (the backend then refuses the config: the companion would check
|
|
336
|
+
* the setting against the hook's header and refuse the setup).
|
|
337
|
+
*/
|
|
338
|
+
export function detectGameSettings(files) {
|
|
339
|
+
const code = files.filter((f) => f.path.endsWith('.rs')).map((f) => codeOnly(f.content)).join('\n');
|
|
340
|
+
const state = /\bstruct\s+\w+\s*(?:<[^>{]*>)?\s*\{\s*(?:#\s*\[[^\]]*\]\s*)*(?:pub(?:\s*\([^)]*\))?\s+)?\w+\s*:\s*(?:(?:::)?bordrless_game\s*::\s*)?GameHeader\s*,\s*(?:#\s*\[[^\]]*\]\s*)*(?:(?:pub(?:\s*\([^)]*\))?\s+)?\w+\s*:\s*(?:(?:::)?bordrless_game\s*::\s*)?(JackpotHeader|StreakHeader)\b)?/.exec(code);
|
|
341
|
+
if (!state)
|
|
342
|
+
return null;
|
|
343
|
+
const kind = state[1] === 'JackpotHeader' ? 'jackpot' : state[1] === 'StreakHeader' ? 'streak' : 'lottery';
|
|
344
|
+
const header = newArgs(code, 'GameHeader', 3);
|
|
345
|
+
if (!header)
|
|
346
|
+
return null;
|
|
347
|
+
const roundSecs = header[1] ?? null;
|
|
348
|
+
if (!within(roundSecs, U32))
|
|
349
|
+
return null;
|
|
350
|
+
const out = { kind, timerSecs: 0, minTokens: '0', roundSecs: Number(roundSecs), minStreakSecs: 0, minWeight: '0' };
|
|
351
|
+
if (kind === 'jackpot') {
|
|
352
|
+
const j = newArgs(code, 'JackpotHeader', 2);
|
|
353
|
+
if (!j || !within(j[0] ?? null, U32) || !within(j[1] ?? null, U64))
|
|
354
|
+
return null;
|
|
355
|
+
out.timerSecs = Number(j[0]);
|
|
356
|
+
out.minTokens = j[1].toString();
|
|
357
|
+
}
|
|
358
|
+
else if (kind === 'streak') {
|
|
359
|
+
const s = newArgs(code, 'StreakHeader', 2);
|
|
360
|
+
if (!s || !within(s[0] ?? null, U32) || !within(s[1] ?? null, U64))
|
|
361
|
+
return null;
|
|
362
|
+
out.minStreakSecs = Number(s[0]);
|
|
363
|
+
out.minWeight = s[1].toString();
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|