@bordrless/shared 0.5.0 → 0.6.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/dist/api.d.ts CHANGED
@@ -107,7 +107,63 @@ export interface CustomHookInfo {
107
107
  upgradeAuthority: Address | null;
108
108
  /** True: `upgradeAuthority` can upgrade it. False: nobody can (final). Null: not read, so nothing is claimed either way. */
109
109
  upgradeable: boolean | null;
110
+ /** What the hook does, for a hook Studio built (`HookAbout`); null (or absent) for any other, which the site describes from its flags. */
111
+ about?: HookAbout | null;
110
112
  }
113
+ /**
114
+ * Bordrless's official summary of a hook Studio deployed: written once by the assistant from the
115
+ * deployed source (comments and strings blanked), its flags and its review, then never changed and
116
+ * never the author's to edit. A new build is a new program, with a summary of its own.
117
+ */
118
+ export interface HookSummary {
119
+ /** At most `HOOK_SUMMARY_LIMITS.title` characters. */
120
+ title: string;
121
+ /** One or two plain sentences: what the hook does to transfers, buys and sells. */
122
+ text: string;
123
+ /** The build it was written for (sha256 of the deployed binary); null when the build had none. */
124
+ buildHash: string | null;
125
+ at: number;
126
+ }
127
+ /** Figures the behaviour simulator measured on a hook (numbers, never model text). Reserved: not filled yet. */
128
+ export interface HookMeasured {
129
+ /** The largest cut seen on one transfer, bps of it. */
130
+ maxCutBps: number | null;
131
+ /** Transfers it refused, of `runs`. */
132
+ refusals: number | null;
133
+ runs: number | null;
134
+ }
135
+ /**
136
+ * What a token hook Studio built does (`GET /v1/hooks/:program/about`, and on `CustomHookInfo.about`,
137
+ * `LaunchConfigData.customHook.about`, `MarketplaceListing.hookAbout`): Bordrless's official summary
138
+ * first, the author's own words second. Null for a hook Studio didn't build.
139
+ */
140
+ export interface HookAbout {
141
+ program: Address;
142
+ /** The Studio project's name, as its author called it. */
143
+ title: string;
144
+ /** Bordrless's summary; null until it is written (the site then says what the flags allow). */
145
+ summary: HookSummary | null;
146
+ /** "What your hook does", in the author's words (at most `HOOK_ABOUT_CHARS`); empty when none was written. */
147
+ about: string;
148
+ studioProject: string;
149
+ /** The worst verdict of Studio's automated checks of the deployed source (not an audit); null when none is kept. */
150
+ reviewVerdict: 'pass' | 'warn' | null;
151
+ /** sha256 of the deployed binary. */
152
+ buildHash: string | null;
153
+ /** Studio keeps its source for anyone to read. */
154
+ sourceAvailable: boolean;
155
+ /** The marketplace listing that shows its source (its Studio config, when listed); null when not listed. */
156
+ listing: Address | null;
157
+ /** Reserved for the behaviour simulator's figures; absent for now. */
158
+ measured?: HookMeasured | null;
159
+ }
160
+ /** "What your hook does": the most an author writes, in characters (the Studio field and what is served). */
161
+ export declare const HOOK_ABOUT_CHARS = 280;
162
+ /** The official summary's limits, in characters. */
163
+ export declare const HOOK_SUMMARY_LIMITS: {
164
+ readonly title: 40;
165
+ readonly text: 320;
166
+ };
111
167
  /** How a pool's protocol fee is taken (§3.1): a flat rate of the quote (`protocolFeeBps`, ordinary pools), or a share of what the hooks cut (`protocolShareBps`, launch pools). */
112
168
  export type ProtocolModel = 'flat' | 'share';
113
169
  /**
@@ -120,7 +176,8 @@ export interface LaunchConfigData {
120
176
  creatorFeeBps: number;
121
177
  customHook: {
122
178
  program: Address;
123
- flags: number;
179
+ flags: number; /** As `CustomHookInfo.about`. */
180
+ about?: HookAbout | null;
124
181
  } | null;
125
182
  label: string;
126
183
  }
@@ -281,6 +338,8 @@ export interface LaunchDetail extends LaunchSummary {
281
338
  ruleStats: RuleStats;
282
339
  /** The bridged-SOL holding holder rewards are paid from; null without holder rewards. */
283
340
  holderVault: Address | null;
341
+ /** The program that is this launch's creator (docs/companions.md), read live; null for a launch whose creator is a wallet. */
342
+ companion: CompanionSummary | null;
284
343
  }
285
344
  /** `GET /v1/launches?sort=`: newest first by default; `rewards24h` most paid to holders in 24 h first; `fees` lowest buy-then-sell fee first. */
286
345
  export type LaunchSort = 'new' | 'mcap' | 'volume' | 'trending' | 'graduated' | 'rewards24h' | 'fees';
@@ -500,19 +559,94 @@ export interface LaunchPrepareRequest {
500
559
  */
501
560
  config?: Address;
502
561
  /**
503
- * A companion (docs/companions.md): the launch's creator is a program, so its creator fees are
504
- * bought back and burned, streamed to holders or paid to the launcher by code, with no keeper.
505
- * The launcher is the beneficiary; a first buy is then the companion's, vesting to them over
506
- * `vestDays`. Not with the creator wallet lock or a custom hook; the holders' templates need
507
- * holder rewards.
562
+ * Where the creator fee goes when it isn't all paid to the launcher (docs/companions.md): the
563
+ * launch's creator is then a program, a companion, that buys the token back and burns it,
564
+ * streams to holders or pays the launcher, by `split` (basis points summing to 10,000), with no
565
+ * keeper. A `template` names one of the three preset splits (kept for older clients). The
566
+ * launcher is the beneficiary; a first buy is then the companion's, vesting to them over
567
+ * `vestDays` (0 to 365, default 30). Refused (`companion_rules`) with the creator wallet lock, a
568
+ * launch config (listed or not, with or without a hook), no creator fee, a share for holders
569
+ * without holder rewards, or a split that doesn't add up.
508
570
  */
509
- companion?: {
510
- template: CompanionTemplate;
511
- vestDays?: number;
512
- };
571
+ companion?: CompanionRequest;
513
572
  }
514
- /** The companion templates (`COMPANION_TEMPLATES` in the SDK). */
573
+ /** A launch's companion as the launch request names it: a split of the creator fee, or one of the preset splits by name. */
574
+ export type CompanionRequest = {
575
+ split: CompanionSplitBps;
576
+ vestDays?: number;
577
+ } | {
578
+ template: CompanionTemplate;
579
+ vestDays?: number;
580
+ };
581
+ /** The companion templates (`COMPANION_SPLITS`; `COMPANION_TEMPLATES` in the SDK). */
515
582
  export type CompanionTemplate = 'buysItself' | 'rugProofDev' | 'buybackAndReward';
583
+ /** What a companion does with every creator fee it claims, in basis points summing to 10,000. */
584
+ export interface CompanionSplitBps {
585
+ /** Bought back on the token's own pool and burned. */
586
+ buybackBps: number;
587
+ /** Streamed to holders through the kit (the launch has holder rewards). */
588
+ holdersBps: number;
589
+ /** Paid to the launcher (the beneficiary), in SOL. */
590
+ beneficiaryBps: number;
591
+ }
592
+ /** A launch's companion as its token page shows it (`LaunchDetail.companion`): lamports and token base units as strings. */
593
+ export interface CompanionSummary {
594
+ /** The companion's account, `PDA(["companion", mint])` under the companion program. */
595
+ address: Address;
596
+ /** The wallet that launched it: paid its part of the fees, and the dev bag as it vests. */
597
+ beneficiary: Address;
598
+ /** The template whose split this is; null for another split. */
599
+ template: CompanionTemplate | null;
600
+ split: CompanionSplitBps;
601
+ /** Creator fees the companion has claimed, all time. */
602
+ claimed: Amount;
603
+ /** Spent buying the token back on its pool. */
604
+ boughtBack: Amount;
605
+ /** Tokens those buybacks burned. */
606
+ burned: Amount;
607
+ /** Shared with holders through the kit. */
608
+ shared: Amount;
609
+ /** Paid to the launcher. */
610
+ paidToLauncher: Amount;
611
+ /** Claimed and waiting for its step: the next buyback, the next share, the launcher's next payout. */
612
+ pending: {
613
+ buyback: Amount;
614
+ holders: Amount;
615
+ launcher: Amount;
616
+ };
617
+ /** The launcher's first buy, held by the companion: its tokens, the part vested now, the part sent to the launcher, and when the vest ends; null without one. */
618
+ devBag: {
619
+ total: Amount;
620
+ vested: Amount;
621
+ released: Amount;
622
+ vestEndsAt: number;
623
+ } | null;
624
+ /** When the next buyback may run while SOL waits for one (it also waits while the price runs above its reference); null otherwise. */
625
+ nextBuybackAt: number | null;
626
+ }
627
+ /**
628
+ * `GET /v1/companion/:mint`: a mint's companion as a launch that may not have finished needs it.
629
+ * `none`: no companion was set up for the mint; `set-up`: set up, its launch not made (its funding
630
+ * can be taken back, `refundable`); `launched`.
631
+ */
632
+ export interface CompanionStatus {
633
+ mint: Address;
634
+ state: 'none' | 'set-up' | 'launched';
635
+ /** The wallet it pays; null with none. */
636
+ beneficiary: Address | null;
637
+ /** Lamports a refund returns now; null unless `set-up`. */
638
+ refundable: Amount | null;
639
+ }
640
+ /** `POST /v1/companion/refund/prepare`: a companion's funding back to the wallet that set it up, for a launch that never happened. The mint's key signs it after the wallet. */
641
+ export interface CompanionRefundRequest {
642
+ wallet: Address;
643
+ mint: Address;
644
+ }
645
+ export interface CompanionRefundResponse {
646
+ transaction: PreparedTx;
647
+ /** Lamports it returns. */
648
+ refundable: Amount;
649
+ }
516
650
  export interface LaunchPrepareResponse {
517
651
  intentId: string;
518
652
  mint: Address;
@@ -619,6 +753,13 @@ export interface BridgeAsset {
619
753
  * opened. Absent from backends that predate it.
620
754
  */
621
755
  pool?: BridgePool | null;
756
+ /**
757
+ * The platform token on the overview only: base units bought back from protocol revenue and held
758
+ * by the treasury (docs page /docs/tokenomics): its holding of the bridged version, plus the SPL
759
+ * version it holds that is still to be bridged. Read live from the chain; null when it could not
760
+ * be read. Absent from backends that predate it and from any other asset.
761
+ */
762
+ boughtBack?: Amount | null;
622
763
  }
623
764
  /** A bridged token's pool on the DEX (`BridgeAsset.pool`): where it is, since when, and its last price. */
624
765
  export interface BridgePool {
package/dist/api.js CHANGED
@@ -3,4 +3,7 @@
3
3
  * (`Amount`), dollar figures as numbers, times as unix seconds. A figure the backend could not
4
4
  * read is null, never zero.
5
5
  */
6
- export {};
6
+ /** "What your hook does": the most an author writes, in characters (the Studio field and what is served). */
7
+ export const HOOK_ABOUT_CHARS = 280;
8
+ /** The official summary's limits, in characters. */
9
+ export const HOOK_SUMMARY_LIMITS = { title: 40, text: 320 };
package/dist/policy.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * `programs/tests/vectors/launch-fees.json`, which the Rust tests render from the reference the
7
7
  * programs are held to, and requires exact equality.
8
8
  */
9
- import type { LaunchRules, LaunchRulesInput, Side } from './api.ts';
9
+ import type { CompanionSplitBps, CompanionTemplate, LaunchRules, LaunchRulesInput, Side } from './api.ts';
10
10
  export declare const BPS = 10000n;
11
11
  export declare const WAD: bigint;
12
12
  /** The largest u64, where the programs' checked arithmetic stops. */
@@ -270,6 +270,44 @@ export declare const RULE_PATHS: readonly RulePath[];
270
270
  export declare const DEFAULT_RULE_PRESET: RulePresetName;
271
271
  /** The preset of this name. */
272
272
  export declare function presetNamed(name: RulePresetName): RulePreset;
273
+ /**
274
+ * The companion templates' splits of every creator fee claim: the one list the launch form, the
275
+ * token page and the backend read (`COMPANION_TEMPLATES` in the SDK is this object).
276
+ */
277
+ export declare const COMPANION_SPLITS: Readonly<Record<CompanionTemplate, Readonly<CompanionSplitBps>>>;
278
+ /** The templates in the order the launch form offers them. */
279
+ export declare const COMPANION_TEMPLATE_LIST: readonly CompanionTemplate[];
280
+ /** How long the launcher's first buy vests over, as the launch form offers it (days), and the default (the backend's too). */
281
+ export declare const COMPANION_VEST_CHOICES_DAYS: readonly [7, 30, 90];
282
+ export declare const COMPANION_DEFAULT_VEST_DAYS = 30;
283
+ /** The longest a first buy may vest over, days (the program's limit is a year). */
284
+ export declare const COMPANION_MAX_VEST_DAYS = 365;
285
+ /** The whole creator fee to the launcher: an ordinary launch, with no companion. */
286
+ export declare const LAUNCHER_SPLIT: Readonly<CompanionSplitBps>;
287
+ /** Whether a split is the whole fee to the launcher (no companion needed). */
288
+ export declare const isLauncherSplit: (s: CompanionSplitBps) => boolean;
289
+ /** A split with a share for holders needs holder rewards on. */
290
+ export declare const splitPaysHolders: (s: CompanionSplitBps) => boolean;
291
+ /** Whether a split takes a first buy: every fee bought back and burned means no dev at all. */
292
+ export declare const splitTakesFirstBuy: (s: CompanionSplitBps) => boolean;
293
+ /** A template that pays holders needs holder rewards on. */
294
+ export declare const companionPaysHolders: (template: CompanionTemplate) => boolean;
295
+ /** Whether a template takes a first buy: the token that buys itself has no dev at all. */
296
+ export declare const companionTakesFirstBuy: (template: CompanionTemplate) => boolean;
297
+ /**
298
+ * The split a launch request's `companion` names: its own `split` (each share a whole number of
299
+ * basis points, 0 to 10,000; their sum is checked by `companionSplitProblem`), or a template's;
300
+ * `null` when it names neither (a malformed request).
301
+ */
302
+ export declare function companionRequestSplit(raw: unknown): CompanionSplitBps | null;
303
+ /**
304
+ * Why a companion with this split can't run these rules, in the words the launch form and the
305
+ * backend use (`companion_rules`); null when it can. The program's own checks (`BadSplit`,
306
+ * `CreatorLockUnsupported`, `HolderRewardsOff`), and a creator fee for it to run.
307
+ */
308
+ export declare function companionSplitProblem(split: CompanionSplitBps, rules: Pick<LaunchRulesInput, 'holderFeeBuyBps' | 'holderFeeSellBps' | 'creatorLockDays'>, creatorFeeBps: number): string | null;
309
+ /** The template whose split this is, or null for any other split. */
310
+ export declare function companionTemplateOf(split: CompanionSplitBps): CompanionTemplate | null;
273
311
  /** Kit module bits (§4.2). */
274
312
  export declare const KIT_MODULES: {
275
313
  readonly HOLDER_REWARDS: 1;
package/dist/policy.js CHANGED
@@ -346,6 +346,85 @@ export const DEFAULT_RULE_PRESET = 'Plain';
346
346
  export function presetNamed(name) {
347
347
  return RULE_PRESETS.find((p) => p.name === name) ?? RULE_PRESETS[0];
348
348
  }
349
+ // ---- companions (docs/companions.md) -----------------------------------------------------------------
350
+ /**
351
+ * The companion templates' splits of every creator fee claim: the one list the launch form, the
352
+ * token page and the backend read (`COMPANION_TEMPLATES` in the SDK is this object).
353
+ */
354
+ export const COMPANION_SPLITS = {
355
+ /** No dev at all: every creator fee buys the token back and burns it. */
356
+ buysItself: { buybackBps: 10_000, holdersBps: 0, beneficiaryBps: 0 },
357
+ /** Half to holders through the kit, half to the launcher, whose first buy vests (holder rewards needed). */
358
+ rugProofDev: { buybackBps: 0, holdersBps: 5_000, beneficiaryBps: 5_000 },
359
+ /** Half bought back and burned, half to holders (holder rewards needed). */
360
+ buybackAndReward: { buybackBps: 5_000, holdersBps: 5_000, beneficiaryBps: 0 },
361
+ };
362
+ /** The templates in the order the launch form offers them. */
363
+ export const COMPANION_TEMPLATE_LIST = ['buysItself', 'rugProofDev', 'buybackAndReward'];
364
+ /** How long the launcher's first buy vests over, as the launch form offers it (days), and the default (the backend's too). */
365
+ export const COMPANION_VEST_CHOICES_DAYS = [7, 30, 90];
366
+ export const COMPANION_DEFAULT_VEST_DAYS = 30;
367
+ /** The longest a first buy may vest over, days (the program's limit is a year). */
368
+ export const COMPANION_MAX_VEST_DAYS = 365;
369
+ /** The whole creator fee to the launcher: an ordinary launch, with no companion. */
370
+ export const LAUNCHER_SPLIT = { buybackBps: 0, holdersBps: 0, beneficiaryBps: 10_000 };
371
+ /** Whether a split is the whole fee to the launcher (no companion needed). */
372
+ export const isLauncherSplit = (s) => s.buybackBps === 0 && s.holdersBps === 0 && s.beneficiaryBps === 10_000;
373
+ /** A split with a share for holders needs holder rewards on. */
374
+ export const splitPaysHolders = (s) => s.holdersBps > 0;
375
+ /** Whether a split takes a first buy: every fee bought back and burned means no dev at all. */
376
+ export const splitTakesFirstBuy = (s) => s.buybackBps < 10_000;
377
+ /** A template that pays holders needs holder rewards on. */
378
+ export const companionPaysHolders = (template) => splitPaysHolders(COMPANION_SPLITS[template]);
379
+ /** Whether a template takes a first buy: the token that buys itself has no dev at all. */
380
+ export const companionTakesFirstBuy = (template) => splitTakesFirstBuy(COMPANION_SPLITS[template]);
381
+ const isShare = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 10_000;
382
+ /**
383
+ * The split a launch request's `companion` names: its own `split` (each share a whole number of
384
+ * basis points, 0 to 10,000; their sum is checked by `companionSplitProblem`), or a template's;
385
+ * `null` when it names neither (a malformed request).
386
+ */
387
+ export function companionRequestSplit(raw) {
388
+ if (typeof raw !== 'object' || raw === null)
389
+ return null;
390
+ const r = raw;
391
+ if (r.split !== undefined) {
392
+ if (typeof r.split !== 'object' || r.split === null)
393
+ return null;
394
+ const s = r.split;
395
+ if (!isShare(s.buybackBps) || !isShare(s.holdersBps) || !isShare(s.beneficiaryBps))
396
+ return null;
397
+ return { buybackBps: s.buybackBps, holdersBps: s.holdersBps, beneficiaryBps: s.beneficiaryBps };
398
+ }
399
+ if (typeof r.template === 'string' && Object.hasOwn(COMPANION_SPLITS, r.template))
400
+ return { ...COMPANION_SPLITS[r.template] };
401
+ return null;
402
+ }
403
+ /**
404
+ * Why a companion with this split can't run these rules, in the words the launch form and the
405
+ * backend use (`companion_rules`); null when it can. The program's own checks (`BadSplit`,
406
+ * `CreatorLockUnsupported`, `HolderRewardsOff`), and a creator fee for it to run.
407
+ */
408
+ export function companionSplitProblem(split, rules, creatorFeeBps) {
409
+ if (![split.buybackBps, split.holdersBps, split.beneficiaryBps].every(isShare))
410
+ return 'Each share of the creator fee is a whole number of basis points, 0 to 10,000.';
411
+ if (split.buybackBps + split.holdersBps + split.beneficiaryBps !== 10_000)
412
+ return 'The creator fee’s shares must add up to 100% (10,000 basis points).';
413
+ if (rules.creatorLockDays > 0)
414
+ return 'A creator fee run by a program can’t come with the creator wallet lock: the program vests your first buy instead.';
415
+ if (creatorFeeBps === 0)
416
+ return 'There is no creator fee for a program to run: set one.';
417
+ if (splitPaysHolders(split) && !rewardsOn(rules))
418
+ return 'A share of the creator fee for holders needs holder rewards on.';
419
+ return null;
420
+ }
421
+ /** The template whose split this is, or null for any other split. */
422
+ export function companionTemplateOf(split) {
423
+ return COMPANION_TEMPLATE_LIST.find((t) => {
424
+ const s = COMPANION_SPLITS[t];
425
+ return s.buybackBps === split.buybackBps && s.holdersBps === split.holdersBps && s.beneficiaryBps === split.beneficiaryBps;
426
+ }) ?? null;
427
+ }
349
428
  /** Kit module bits (§4.2). */
350
429
  export const KIT_MODULES = { HOLDER_REWARDS: 1, MAX_WALLET: 2, CREATOR_LOCK: 4, EARLY_LOCK: 8 };
351
430
  /** The kit modules a launch with these rules installs (§5.1); 0 means no hook at all (a burn alone runs in the pool hook). */
@@ -16,6 +16,25 @@ export declare const PROGRAM_IDS: {
16
16
  readonly halfLife: "53SpmtkdPWQ63mWoDeXk8P9tuwiT4ed2Wx4fwfy5NSF8";
17
17
  /** Companions (programs/bordrless_companion, docs/companions.md): a launch whose creator is a program, its fees bought back, shared with holders or vested by code. */
18
18
  readonly companion: "6ZUM1gWBH9hBBNoJoaVAGwSftyZ6CUda6vUZTW9MsJuo";
19
+ /**
20
+ * The lottery hook (programs/lottery_hook, docs/companions.md "Games"): a lottery coin's token
21
+ * hook under the game ticket standard; its companion holds the pot and draws. Not deployed yet
22
+ * (2026-10-08): companion v2 and this hook go live together, after their audit.
23
+ */
24
+ readonly lotteryHook: "HqFWsCBQ416DAfevJ9TspyT5yXGGoYTCpcreiGkCgWcr";
25
+ };
26
+ /**
27
+ * The lottery hook's fixed facts (programs/lottery_hook/README.md): the flags a `LaunchConfig`
28
+ * names for it (`BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA`, exactly what a companion game
29
+ * launch accepts), the token program's signer of its callbacks (`["hook-authority", hook]` under
30
+ * the token program) and its own `["hook-authority"]`, which signs `write_hook_data` in `enter`.
31
+ */
32
+ export declare const LOTTERY_HOOK: {
33
+ readonly program: "HqFWsCBQ416DAfevJ9TspyT5yXGGoYTCpcreiGkCgWcr";
34
+ readonly flags: 145;
35
+ readonly tokenHookSigner: "CFyuaxvKmpgnSMCoNqDKCcwxnTUeW8Mm1go1t3UMsvLH";
36
+ readonly hookAuthority: "6oZ9LkAfgmhmPYjXj3okjYK5sderddEo8fp8MR4H6Gx1";
37
+ readonly readme: "https://github.com/BordrlessDex/bordrless-programs/tree/main/programs/lottery_hook";
19
38
  };
20
39
  /**
21
40
  * Half-Life, Bordrless's own token hook (programs/half_life/README.md): the `LaunchConfig` the
package/dist/programs.js CHANGED
@@ -16,6 +16,25 @@ export const PROGRAM_IDS = {
16
16
  halfLife: '53SpmtkdPWQ63mWoDeXk8P9tuwiT4ed2Wx4fwfy5NSF8',
17
17
  /** Companions (programs/bordrless_companion, docs/companions.md): a launch whose creator is a program, its fees bought back, shared with holders or vested by code. */
18
18
  companion: '6ZUM1gWBH9hBBNoJoaVAGwSftyZ6CUda6vUZTW9MsJuo',
19
+ /**
20
+ * The lottery hook (programs/lottery_hook, docs/companions.md "Games"): a lottery coin's token
21
+ * hook under the game ticket standard; its companion holds the pot and draws. Not deployed yet
22
+ * (2026-10-08): companion v2 and this hook go live together, after their audit.
23
+ */
24
+ lotteryHook: 'HqFWsCBQ416DAfevJ9TspyT5yXGGoYTCpcreiGkCgWcr',
25
+ };
26
+ /**
27
+ * The lottery hook's fixed facts (programs/lottery_hook/README.md): the flags a `LaunchConfig`
28
+ * names for it (`BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA`, exactly what a companion game
29
+ * launch accepts), the token program's signer of its callbacks (`["hook-authority", hook]` under
30
+ * the token program) and its own `["hook-authority"]`, which signs `write_hook_data` in `enter`.
31
+ */
32
+ export const LOTTERY_HOOK = {
33
+ program: PROGRAM_IDS.lotteryHook,
34
+ flags: 145,
35
+ tokenHookSigner: 'CFyuaxvKmpgnSMCoNqDKCcwxnTUeW8Mm1go1t3UMsvLH',
36
+ hookAuthority: '6oZ9LkAfgmhmPYjXj3okjYK5sderddEo8fp8MR4H6Gx1',
37
+ readme: 'https://github.com/BordrlessDex/bordrless-programs/tree/main/programs/lottery_hook',
19
38
  };
20
39
  /**
21
40
  * Half-Life, Bordrless's own token hook (programs/half_life/README.md): the `LaunchConfig` the
package/dist/studio.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * for a new mint without knowing its code. The backend refuses source that leaves the template's
14
14
  * limits (`StaticCheck`) before it is built, reviewed or paid for.
15
15
  */
16
- import type { Address, PreparedTx } from './api.ts';
16
+ import type { Address, HookAbout, PreparedTx } from './api.ts';
17
17
  /** One source file of a hook. Only `src/**.rs` files; `Cargo.toml` is the template's and never sent. */
18
18
  export interface StudioFile {
19
19
  /** Relative path, e.g. `src/lib.rs`. */
@@ -167,9 +167,15 @@ export interface ReviewFinding {
167
167
  /** `src/lib.rs:42`, when it points at a line. */
168
168
  location: string | null;
169
169
  }
170
- /** `POST /v1/studio/projects/:id/review`: the static checks, then the assistant's security review. */
170
+ /**
171
+ * `POST /v1/studio/projects/:id/review`: the static checks, then the automated security review (two
172
+ * independent model runs, the worse kept). Every run of a source (and the hook flags read from it,
173
+ * `detectHookFlags`) is kept: the review shown is the worst of them, its findings merged, and a
174
+ * source that failed once stays failed (only a change to the source clears it). Automated checks,
175
+ * not an audit.
176
+ */
171
177
  export interface StudioReview {
172
- /** `fail`: a static check failed or a critical/high finding; deploy is refused. */
178
+ /** `fail`: a static check failed or a critical/high finding; deploy is refused. The worst of every run of this source and these flags. */
173
179
  verdict: 'pass' | 'warn' | 'fail';
174
180
  checks: StaticCheck[];
175
181
  findings: ReviewFinding[];
@@ -177,6 +183,10 @@ export interface StudioReview {
177
183
  summary: string;
178
184
  /** sha256 of the source reviewed. */
179
185
  sourceHash: string;
186
+ /** The hook flags the source was reviewed with, read from the source itself (`detectHookFlags`). Absent on reviews made before flags were recorded. */
187
+ hookFlags?: number;
188
+ /** How many review runs of this source and these flags the verdict and findings merge. */
189
+ runs?: number;
180
190
  at: number;
181
191
  }
182
192
  /** `POST /v1/studio/projects/:id/build`: compiled by the build worker in the verifiable-build image. */
@@ -191,6 +201,21 @@ export interface StudioBuild {
191
201
  log: string;
192
202
  sourceHash: string;
193
203
  at: number;
204
+ /** Studio's trading test of what was built (the worker's simulator): a deploy needs `pass`. Absent on builds from before it. */
205
+ sim?: StudioSim | null;
206
+ }
207
+ /** The simulator's verdict on a build: launched, bought, sold and sent by several wallets over a simulated year. */
208
+ export interface StudioSim {
209
+ pass: boolean;
210
+ /** Why it failed, in the simulator's words (the first few). */
211
+ reasons: string[];
212
+ /** The most the hook took from one transfer, in bps, and the cap it was held to. */
213
+ cutMaxBps: number | null;
214
+ capBps: number | null;
215
+ /** Operations the hook refused. */
216
+ refusedTotal: number;
217
+ /** What the test can't see (amount triggers, wallets it never used, after a year). */
218
+ notes: string[];
194
219
  }
195
220
  /** `POST /v1/studio/projects/:id/deploy/quote`: what deploying the built hook costs, in lamports. */
196
221
  export interface StudioDeployQuote {
@@ -272,8 +297,13 @@ export interface MarketplaceListing {
272
297
  studioSource: boolean;
273
298
  /** Who can upgrade the hook now (null: final, or no hook). */
274
299
  hookUpgradeAuthority: Address | null;
275
- /** The source passed review when it was deployed. */
276
- reviewVerdict: 'pass' | 'warn' | null;
300
+ /**
301
+ * The worst verdict of the automated checks over every review run of the deployed source (not an
302
+ * audit, and not a seal); null when Studio did not build the hook or has no review of it.
303
+ */
304
+ reviewVerdict: 'pass' | 'warn' | 'fail' | null;
305
+ /** What its hook does, when Studio built it: Bordrless's summary and the author's words (`HookAbout`); null or absent otherwise. */
306
+ hookAbout?: HookAbout | null;
277
307
  launches: number;
278
308
  /** Paid to the author so far, lamports. */
279
309
  authorEarned: string;
@@ -326,6 +356,25 @@ export declare const PROTOCOL_UPGRADE_AUTHORITY = "5xsibKwtiN6ruxsYrEyWVpV3Kcwuz
326
356
  * may hold a custom hook's upgrade authority for `create_config` to accept it (docs/hooks-v2.md §5.8).
327
357
  */
328
358
  export declare const HOOK_UPGRADE_AUTHORITIES: readonly string[];
359
+ /** The one sentence the site gives a config whose hook someone outside Bordrless can upgrade. */
360
+ export declare const HOOK_AUTHORITY_PROBLEM = "This config\u2019s hook can be upgraded by someone outside Bordrless, so it can\u2019t launch here.";
361
+ /** The error code the backend refuses such a config with (`POST /v1/launch/prepare`). */
362
+ export declare const HOOK_AUTHORITY_CODE = "hook_upgrade_authority";
363
+ /** The Solana CLI command that makes a deployed hook immutable (signed by its current upgrade authority): the way a hook built outside Studio can launch. */
364
+ export declare const hookFinalCommand: (program?: string) => string;
365
+ /**
366
+ * 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`), or when its upgrade
368
+ * authority is one of `HOOK_UPGRADE_AUTHORITIES` (Bordrless Studio's key or the protocol's). Only a
369
+ * hook some other key is known to be able to upgrade is refused: one whose upgrade info could not be
370
+ * read is not (the launch program checked it when the config was made; the SDK's
371
+ * `hookAuthorityProblem` and the marketplace watch read it the same way). A config without a hook
372
+ * (`customHook` null: the launchpad's and the kit's own programs) passes.
373
+ */
374
+ export declare function hookAuthorityAccepted(customHook: string | null, hook: {
375
+ upgradeAuthority: string | null;
376
+ upgradeable: boolean | null;
377
+ } | null): boolean;
329
378
  /** `bordrless_launch::constants::MAX_AUTHOR_SHARE_BPS`: half the creator fee, the most the program lets a listed config pay its author. */
330
379
  export declare const MAX_AUTHOR_SHARE_BPS = 5000;
331
380
  /**
@@ -357,3 +406,16 @@ export declare const STUDIO_STARTERS: readonly {
357
406
  name: string;
358
407
  blurb: string;
359
408
  }[];
409
+ /**
410
+ * The token hook flags a Studio hook needs, read from its own source (never chosen by hand): each
411
+ * callback whose body the author changed from the template's stub; cuts (TRANSFER_RETURNS_DELTA)
412
+ * when before_transfer runs and the code builds a `Delta` or fills `deltas`; hook data
413
+ * (WRITES_HOOK_DATA) when the code sets a holding's `source_hook_data`/`destination_hook_data`
414
+ * (reading `args.…_hook_data` needs no flag). It leans to including a bit: a flag the code doesn't
415
+ * use is harmless, a missing one makes the token program refuse the hook's answer. 0: the hook does
416
+ * nothing yet.
417
+ */
418
+ export declare function detectHookFlags(files: readonly {
419
+ path: string;
420
+ content: string;
421
+ }[]): number;
package/dist/studio.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { TOKEN_HOOK_FLAGS as HOOK_FLAG_BITS } from "./programs.js";
1
2
  /** The message a wallet signs to use Studio: no transaction, nothing moves. */
2
3
  export function studioSignInMessage(wallet, nonce, issuedAt) {
3
4
  return ['Sign in to Bordrless Studio', '', 'This signature proves you own this wallet. It does not move funds or approve any transaction.', '', `Wallet: ${wallet}`, `Nonce: ${nonce}`, `Issued: ${issuedAt}`].join('\n');
@@ -14,6 +15,26 @@ export const PROTOCOL_UPGRADE_AUTHORITY = '5xsibKwtiN6ruxsYrEyWVpV3KcwuzSPbQd1n2
14
15
  * may hold a custom hook's upgrade authority for `create_config` to accept it (docs/hooks-v2.md §5.8).
15
16
  */
16
17
  export const HOOK_UPGRADE_AUTHORITIES = [STUDIO_UPGRADE_AUTHORITY, PROTOCOL_UPGRADE_AUTHORITY];
18
+ /** The one sentence the site gives a config whose hook someone outside Bordrless can upgrade. */
19
+ export const HOOK_AUTHORITY_PROBLEM = 'This config’s hook can be upgraded by someone outside Bordrless, so it can’t launch here.';
20
+ /** The error code the backend refuses such a config with (`POST /v1/launch/prepare`). */
21
+ export const HOOK_AUTHORITY_CODE = 'hook_upgrade_authority';
22
+ /** The Solana CLI command that makes a deployed hook immutable (signed by its current upgrade authority): the way a hook built outside Studio can launch. */
23
+ export const hookFinalCommand = (program = '<PROGRAM_ID>') => `solana program set-upgrade-authority ${program} --final`;
24
+ /**
25
+ * 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`), or when its upgrade
27
+ * authority is one of `HOOK_UPGRADE_AUTHORITIES` (Bordrless Studio's key or the protocol's). Only a
28
+ * hook some other key is known to be able to upgrade is refused: one whose upgrade info could not be
29
+ * read is not (the launch program checked it when the config was made; the SDK's
30
+ * `hookAuthorityProblem` and the marketplace watch read it the same way). A config without a hook
31
+ * (`customHook` null: the launchpad's and the kit's own programs) passes.
32
+ */
33
+ export function hookAuthorityAccepted(customHook, hook) {
34
+ if (customHook === null || hook === null || hook.upgradeable !== true || hook.upgradeAuthority === null)
35
+ return true;
36
+ return HOOK_UPGRADE_AUTHORITIES.includes(hook.upgradeAuthority);
37
+ }
17
38
  /** `bordrless_launch::constants::MAX_AUTHOR_SHARE_BPS`: half the creator fee, the most the program lets a listed config pay its author. */
18
39
  export const MAX_AUTHOR_SHARE_BPS = 5_000;
19
40
  /**
@@ -38,3 +59,112 @@ export const STUDIO_STARTERS = [
38
59
  { 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.' },
39
60
  { id: 'cooldown', name: 'Transfer cooldown', blurb: 'A holding that received tokens cannot send them on for a while, stamped in its hook data.' },
40
61
  ];
62
+ // ---- the hook's flags, read from its source ---------------------------------------------------------
63
+ /** Comments, string and char literals blanked (newlines kept), so only code is read. */
64
+ function codeOnly(src) {
65
+ let out = '';
66
+ let i = 0;
67
+ while (i < src.length) {
68
+ const c = src[i];
69
+ const n = src[i + 1];
70
+ if (c === '/' && n === '/') {
71
+ while (i < src.length && src[i] !== '\n')
72
+ i += 1;
73
+ }
74
+ else if (c === '/' && n === '*') {
75
+ let depth = 1;
76
+ i += 2;
77
+ while (i < src.length && depth > 0) {
78
+ if (src[i] === '/' && src[i + 1] === '*')
79
+ (depth += 1), (i += 2);
80
+ else if (src[i] === '*' && src[i + 1] === '/')
81
+ (depth -= 1), (i += 2);
82
+ else {
83
+ if (src[i] === '\n')
84
+ out += '\n';
85
+ i += 1;
86
+ }
87
+ }
88
+ }
89
+ else if (c === 'r' && (n === '"' || n === '#') && !/[A-Za-z0-9_]/.test(src[i - 1] ?? '')) {
90
+ let j = i + 1;
91
+ let hashes = 0;
92
+ while (src[j] === '#')
93
+ (hashes += 1), (j += 1);
94
+ if (src[j] !== '"') {
95
+ out += c;
96
+ i += 1;
97
+ continue;
98
+ }
99
+ const end = '"' + '#'.repeat(hashes);
100
+ const close = src.indexOf(end, j + 1);
101
+ i = close < 0 ? src.length : close + end.length;
102
+ out += ' ';
103
+ }
104
+ else if (c === '"') {
105
+ i += 1;
106
+ while (i < src.length && src[i] !== '"')
107
+ i += src[i] === '\\' ? 2 : 1;
108
+ i += 1;
109
+ out += ' ';
110
+ }
111
+ else {
112
+ out += c;
113
+ i += 1;
114
+ }
115
+ }
116
+ return out;
117
+ }
118
+ /** A function's body (between its braces), or null when it isn't there. */
119
+ function bodyOf(code, name) {
120
+ const m = new RegExp(`\\bfn\\s+${name}\\s*\\(`).exec(code);
121
+ if (!m)
122
+ return null;
123
+ const open = code.indexOf('{', m.index);
124
+ if (open < 0)
125
+ return null;
126
+ let depth = 0;
127
+ for (let k = open; k < code.length; k += 1) {
128
+ if (code[k] === '{')
129
+ depth += 1;
130
+ else if (code[k] === '}') {
131
+ depth -= 1;
132
+ if (depth === 0)
133
+ return code.slice(open + 1, k);
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+ /** The Studio template's untouched callbacks: a callback left like this does nothing, so it isn't subscribed. */
139
+ const STUB_BODIES = new Set(['check_call(&ctx,&args)?;Ok(HookReturn::default())', 'check_call(&ctx,&args)']);
140
+ const CALLBACK_FLAGS = [
141
+ ['before_transfer', HOOK_FLAG_BITS.BEFORE_TRANSFER],
142
+ ['after_transfer', HOOK_FLAG_BITS.AFTER_TRANSFER],
143
+ ['before_mint', HOOK_FLAG_BITS.BEFORE_MINT],
144
+ ['after_mint', HOOK_FLAG_BITS.AFTER_MINT],
145
+ ['before_burn', HOOK_FLAG_BITS.BEFORE_BURN],
146
+ ['after_burn', HOOK_FLAG_BITS.AFTER_BURN],
147
+ ];
148
+ /**
149
+ * The token hook flags a Studio hook needs, read from its own source (never chosen by hand): each
150
+ * callback whose body the author changed from the template's stub; cuts (TRANSFER_RETURNS_DELTA)
151
+ * when before_transfer runs and the code builds a `Delta` or fills `deltas`; hook data
152
+ * (WRITES_HOOK_DATA) when the code sets a holding's `source_hook_data`/`destination_hook_data`
153
+ * (reading `args.…_hook_data` needs no flag). It leans to including a bit: a flag the code doesn't
154
+ * use is harmless, a missing one makes the token program refuse the hook's answer. 0: the hook does
155
+ * nothing yet.
156
+ */
157
+ export function detectHookFlags(files) {
158
+ const code = files.filter((f) => f.path.endsWith('.rs')).map((f) => codeOnly(f.content)).join('\n');
159
+ let flags = 0;
160
+ for (const [name, bit] of CALLBACK_FLAGS) {
161
+ const body = bodyOf(code, name);
162
+ if (body !== null && !STUB_BODIES.has(body.replace(/\s+/g, '')))
163
+ flags |= bit;
164
+ }
165
+ if (flags & HOOK_FLAG_BITS.BEFORE_TRANSFER && (/\bDelta\s*\{/.test(code) || /\bdeltas\s*:/.test(code) || /\.deltas\b/.test(code)))
166
+ flags |= HOOK_FLAG_BITS.TRANSFER_RETURNS_DELTA;
167
+ if (flags && /(?<!args\.)\b(source|destination)_hook_data\b/.test(code))
168
+ flags |= HOOK_FLAG_BITS.WRITES_HOOK_DATA;
169
+ return flags;
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bordrless/shared",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Bordrless policy (the same math as crates/bordrless-core), program addresses and the API types shared by the server and the site. No Solana dependencies.",
6
6
  "license": "Apache-2.0",