@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/dist/policy.js CHANGED
@@ -15,6 +15,14 @@ export const CURVE_BPS = 7500n;
15
15
  export const LP_FEE_BPS = 30;
16
16
  /** Protocol fee of an ordinary DEX pool (anyone's pool), in basis points of the quote side: the flat model (§3.1). */
17
17
  export const PROTOCOL_FEE_BPS = 100;
18
+ /**
19
+ * The LP fee tiers the create-pool form offers for an ordinary pool (docs/dex.md decision 2): 0.25%
20
+ * (the default, for bridged pairs), 1% and 2% (memecoins). The program takes any value from 0 to 90%;
21
+ * three tiers keep the number of duplicate pools per pair down (§3.5).
22
+ */
23
+ export const POOL_FEE_TIERS_BPS = [25, 100, 200];
24
+ export const POOL_FEE_TIER_DEFAULT_BPS = 25;
25
+ export const isPoolFeeTier = (bps) => POOL_FEE_TIERS_BPS.includes(bps);
18
26
  /**
19
27
  * Bordrless's take on a launch pool (its curve and the same pool after graduation), the share
20
28
  * model of §3.1 (2026-10-07: "we earn 25% of their total fees, not 0.25%"): a quarter of what the
@@ -26,6 +34,8 @@ export const PROTOCOL_FEE_BPS = 100;
26
34
  export const LAUNCH_PROTOCOL_SHARE_BPS = 2_500;
27
35
  /** The largest share the DEX config may set (`MAX_PROTOCOL_SHARE_BPS`): all of the cuts. */
28
36
  export const MAX_PROTOCOL_SHARE_BPS = 10_000;
37
+ /** The largest flat protocol fee the DEX config may set (`bordrless_swap::constants::MAX_PROTOCOL_FEE_BPS`): 10% of the quote side. */
38
+ export const MAX_PROTOCOL_FEE_BPS = 1_000;
29
39
  /** Largest creator fee a launch may choose. */
30
40
  export const MAX_CREATOR_FEE_BPS = 200;
31
41
  /** The creator fee choices offered on the launch form. */
@@ -370,30 +380,37 @@ export const COMPANION_MAX_VEST_DAYS = 365;
370
380
  export const LAUNCHER_SPLIT = { buybackBps: 0, holdersBps: 0, beneficiaryBps: 10_000 };
371
381
  // ---- lottery coins (docs/games.md) -----------------------------------------------------------------
372
382
  /**
373
- * The companion program's game limits (`constants.rs`; `GAME_LIMITS` in the SDK, the same numbers):
374
- * a round's length, the minimum pot, the prize's least share, a claim window's bounds, the most
375
- * attempts, and the half of a round the attempts may take. The launch form holds its choices to
376
- * them and the backend refuses anything outside them (`lotteryProblem`).
383
+ * The Jackpot's limits as the site offers them: the round's length (the programs take rounds from 5
384
+ * minutes since the five-minute rounds upgrade of 2026-10-12; the owner set the site's floor at 15
385
+ * minutes, which is also its default), the minimum pot, the prize's least share, a claim window's
386
+ * bounds (`MIN_CLAIM_WINDOW` in the companion, a minute), the most attempts, and the half of a round
387
+ * the attempts may take. The launch form holds its choices to them and the backend refuses anything
388
+ * outside them (`lotteryProblem`). `GAME_LIMITS` in the SDK is the programs' own.
377
389
  */
378
390
  export const LOTTERY_LIMITS = {
379
- minRoundSecs: 3_600,
391
+ minRoundSecs: 900,
380
392
  maxRoundSecs: 30 * DAY_SECS,
381
393
  minMinPotLamports: 100000000n,
382
394
  maxMinPotLamports: 1000000000000n,
383
395
  minPrizeBps: 1_000,
384
- minClaimWindowSecs: 300,
396
+ minClaimWindowSecs: 60,
385
397
  maxClaimWindowSecs: DAY_SECS,
386
398
  maxAttempts: 16,
387
399
  claimsPerRound: 2,
400
+ /** A streak's epoch and its claim window keep the programs' older floors (`MIN_EPOCH_SECS`, `MIN_EPOCH_CLAIM_WINDOW`). */
401
+ minEpochSecs: 3_600,
402
+ minEpochClaimWindowSecs: 300,
388
403
  /** The most a pot holds while its hook is not audited: 10 SOL, the rest of the pot's share bought back. */
389
404
  potCapLamports: 10000000000n,
390
405
  /** Lamports the setup pays for the hook's state and registry and the game's account (mainnet rent, 2026-10-08). */
391
406
  rentLamports: 3515360n + 3119120n,
392
407
  };
393
- /** The lottery the launch form starts with (docs/games.md, spec example 1): 6-hour rounds, 70% of the fee to the pot, 30% bought back, draws from 0.5 SOL paying the whole pot, 8 claim attempts of 10 minutes. */
394
- export const LOTTERY_DEFAULTS = { kind: 'lottery', roundSecs: 21_600, potBps: 7_000, minPotLamports: '500000000', prizeBps: 10_000, claimWindowSecs: 600, maxAttempts: 8 };
408
+ /** The lottery the launch form starts with (the owner, 2026-10-12): 15-minute rounds, 70% of the fee to the pot, 30% bought back, draws from 0.5 SOL paying the whole pot, 3 claim attempts of 2 minutes (`lotteryClaimsFor`). */
409
+ export const LOTTERY_DEFAULTS = { kind: 'lottery', roundSecs: 900, potBps: 7_000, minPotLamports: '500000000', prizeBps: 10_000, claimWindowSecs: 120, maxAttempts: 3 };
395
410
  /** The round lengths the form offers (seconds), each with its label. */
396
411
  export const LOTTERY_ROUND_CHOICES = [
412
+ { secs: 900, label: '15 min' },
413
+ { secs: 1_800, label: '30 min' },
397
414
  { secs: 3_600, label: '1 hour' },
398
415
  { secs: 21_600, label: '6 hours' },
399
416
  { secs: DAY_SECS, label: '1 day' },
@@ -411,11 +428,19 @@ export const LOTTERY_PRIZE_CHOICES_BPS = [10_000, 5_000, 2_500];
411
428
  /** The pot's share moves in steps of 10%, like the Split control's. */
412
429
  export const LOTTERY_POT_STEP_BPS = 1_000;
413
430
  /**
414
- * The claim windows and attempts the form fixes for a round length: 8 attempts of 10 minutes for a
415
- * round of 6 hours or more, 6 of 5 minutes for an hour (the attempts must fit in half a round).
431
+ * The claim windows and attempts the form fixes for a round length (the attempts must fit in half a
432
+ * round, `claimsPerRound`): 8 attempts of 10 minutes from 6 hours, 6 of 5 minutes from an hour, 3 of
433
+ * 2 minutes from 15 minutes, and 2 of a minute under that (the programs' 5-minute rounds, which the
434
+ * site does not offer).
416
435
  */
417
436
  export function lotteryClaimsFor(roundSecs) {
418
- return roundSecs >= 21_600 ? { claimWindowSecs: 600, maxAttempts: 8 } : { claimWindowSecs: 300, maxAttempts: 6 };
437
+ if (roundSecs >= 21_600)
438
+ return { claimWindowSecs: 600, maxAttempts: 8 };
439
+ if (roundSecs >= 3_600)
440
+ return { claimWindowSecs: 300, maxAttempts: 6 };
441
+ if (roundSecs >= 900)
442
+ return { claimWindowSecs: 120, maxAttempts: 3 };
443
+ return { claimWindowSecs: 60, maxAttempts: 2 };
419
444
  }
420
445
  const isWhole = (v, min, max) => typeof v === 'number' && Number.isInteger(v) && v >= min && v <= max;
421
446
  /** The lottery a launch request's `companion.game` names, each field a whole number; null when it is malformed (bounds are `lotteryProblem`'s). */
@@ -449,14 +474,14 @@ export function lotteryProblem(game, split, rules, creatorFeeBps) {
449
474
  if (!(game.potBps > 0) || split.buybackBps + split.holdersBps + split.beneficiaryBps + game.potBps !== 10_000)
450
475
  return 'The pot’s part and the split must add up to 10,000 basis points, with a pot.';
451
476
  if (game.roundSecs < L.minRoundSecs || game.roundSecs > L.maxRoundSecs)
452
- return 'A round lasts an hour to 30 days.';
477
+ return 'A round lasts 15 minutes to 30 days.';
453
478
  const minPot = BigInt(game.minPotLamports);
454
479
  if (minPot < L.minMinPotLamports || minPot > L.maxMinPotLamports)
455
480
  return 'The minimum pot is 0.1 to 1,000 SOL.';
456
481
  if (game.prizeBps < L.minPrizeBps || game.prizeBps > 10_000)
457
482
  return 'A draw pays 10% to 100% of the pot.';
458
483
  if (game.claimWindowSecs < L.minClaimWindowSecs || game.claimWindowSecs > L.maxClaimWindowSecs)
459
- return 'A claim window lasts 5 minutes to a day.';
484
+ return 'A claim window lasts a minute to a day.';
460
485
  if (!(game.maxAttempts >= 1 && game.maxAttempts <= L.maxAttempts))
461
486
  return 'A draw makes 1 to 16 attempts.';
462
487
  if (game.claimWindowSecs * game.maxAttempts * L.claimsPerRound > game.roundSecs)
@@ -465,22 +490,126 @@ export function lotteryProblem(game, split, rules, creatorFeeBps) {
465
490
  }
466
491
  /** The copy rules of a lottery coin (docs/games.md): said on the launch form, the review step and the token page, never "provably fair", "guaranteed" or "audited". */
467
492
  export const LOTTERY_COPY = {
468
- held: 'The pot is held by the companion program and paid out by code. Randomness by ORAO VRF.',
469
- odds: 'Your odds = your tokens in the round. Sell before the payout and you forfeit.',
470
- stop: 'Bordrless can stop this game: its pot would be bought back and burned. Nobody is paid.',
471
- cap: 'Pot capped at 10 SOL until the game’s hook is audited.',
472
- oracle: 'Randomness by ORAO VRF: ORAO’s three signers produce it. They could withhold or bias a draw; Bordrless can’t.',
493
+ held: 'The pot is held by code, not by a person. Each round, one holder is picked at random and paid in SOL.',
494
+ odds: 'Your odds are your share of the tokens held through the round. Tokens you buy count from the next round, and selling during a round takes you out of it.',
495
+ stop: 'Bordrless can stop the game. If it does, the pot buys back and burns the token and no one is paid.',
496
+ cap: 'The pot holds at most 10 SOL for now. Anything above that buys back and burns the token.',
497
+ oracle: 'The random numbers come from ORAO, an outside service. Neither Bordrless nor the launcher can pick the winner.',
498
+ };
499
+ // ---- jackpot and streak coins (docs/games.md "Phase 2"), on game hooks made in Studio -----------------
500
+ /** The jackpot's timer and the streak's minimum streak: the companion's bounds (`GAME_LIMITS` in the SDK). */
501
+ export const GAME_KIND_LIMITS = { minTimerSecs: 300, maxTimerSecs: 30 * DAY_SECS, maxMinStreakSecs: 365 * DAY_SECS };
502
+ /** A jackpot as the launch form starts it (spec example 2, `JACKPOT_DEFAULTS` in the SDK): 70% of the fee to the pot, 30% bought back, half the pot to the last buyer from 0.5 SOL. */
503
+ export const JACKPOT_REQUEST_DEFAULTS = { kind: 'jackpot', potBps: 7_000, minPotLamports: '500000000', prizeBps: 5_000 };
504
+ /** A streak as the launch form starts it (spec example 3, `STREAK_DEFAULTS` in the SDK): 70% of the fee to the pot, 30% bought back, the whole pot shared each epoch from 0.1 SOL, an hour at least for claims. */
505
+ export const STREAK_REQUEST_DEFAULTS = { kind: 'streak', potBps: 7_000, minPotLamports: '100000000', prizeBps: 10_000, claimWindowSecs: 3_600 };
506
+ /** The least a streak's close leaves for claims, as the form offers it (seconds); each must fit half the hook's epoch. */
507
+ export const STREAK_CLAIM_WINDOW_CHOICES = [
508
+ { secs: 1_800, label: '30 min' },
509
+ { secs: 3_600, label: '1 hour' },
510
+ { secs: 21_600, label: '6 hours' },
511
+ { secs: DAY_SECS, label: '1 day' },
512
+ ];
513
+ /** The game a launch request's `companion.game` names, of any kind, each field a whole number; null when malformed (bounds are `gameProblem`'s). */
514
+ export function gameRequestOf(raw) {
515
+ if (typeof raw !== 'object' || raw === null)
516
+ return null;
517
+ const r = raw;
518
+ if (r.kind === 'lottery')
519
+ return lotteryRequestOf(raw);
520
+ if (r.kind !== 'jackpot' && r.kind !== 'streak')
521
+ return null;
522
+ if (!isWhole(r.potBps, 0, 10_000) || !isWhole(r.prizeBps, 0, 10_000))
523
+ return null;
524
+ if (typeof r.minPotLamports !== 'string' || !/^\d{1,20}$/.test(r.minPotLamports))
525
+ return null;
526
+ if (r.kind === 'jackpot')
527
+ return { kind: 'jackpot', potBps: r.potBps, minPotLamports: r.minPotLamports, prizeBps: r.prizeBps };
528
+ if (!isWhole(r.claimWindowSecs, 0, 2 ** 32 - 1))
529
+ return null;
530
+ return { kind: 'streak', potBps: r.potBps, minPotLamports: r.minPotLamports, prizeBps: r.prizeBps, claimWindowSecs: r.claimWindowSecs };
531
+ }
532
+ /**
533
+ * Why `create_game_v2` (or the launch) would refuse this game with this split on these rules at
534
+ * this fee, with `hook` the Studio game hook the config names (`ConfigInspection.game`; null: the
535
+ * config names none), in one sentence; null when it would not. A lottery is `lotteryProblem`'s.
536
+ * The program's own checks (`gameArgsProblem` in the SDK, the same sentences) and what a game
537
+ * coin's launch needs besides: a creator fee, no token rules, no holders' share, and a hook of the
538
+ * game's kind.
539
+ */
540
+ export function gameProblem(game, split, rules, creatorFeeBps, hook = null) {
541
+ if (game.kind === 'lottery')
542
+ return lotteryProblem(game, split, rules, creatorFeeBps);
543
+ const L = LOTTERY_LIMITS;
544
+ const word = GAME_KIND_NAMES[game.kind];
545
+ if (!hook)
546
+ return `A ${word} coin launches from a game hook made in Studio: open your ${word} project there and press Launch with it.`;
547
+ if (hook.kind !== game.kind)
548
+ return `This config’s hook keeps a ${GAME_KIND_NAMES[hook.kind]}’s score, not a ${word}’s.`;
549
+ if (creatorFeeBps === 0)
550
+ return 'There is no creator fee to fill the pot with: set one.';
551
+ if (Object.keys(NO_RULES).some((k) => rules[k] !== 0))
552
+ return `A ${word} coin has no other token rules: its hook keeps the score.`;
553
+ if (split.holdersBps !== 0)
554
+ return 'A game coin runs its own hook, so it has no holder rewards: no holders’ part.';
555
+ if (!(game.potBps > 0) || split.buybackBps + split.holdersBps + split.beneficiaryBps + game.potBps !== 10_000)
556
+ return 'The pot’s part and the split must add up to 10,000 basis points, with a pot.';
557
+ const minPot = BigInt(game.minPotLamports);
558
+ if (minPot < L.minMinPotLamports || minPot > L.maxMinPotLamports)
559
+ return 'The minimum pot is 0.1 to 1,000 SOL.';
560
+ if (game.prizeBps < L.minPrizeBps || game.prizeBps > 10_000)
561
+ return 'A game pays 10% to 100% of the pot at a time.';
562
+ if (game.kind === 'jackpot') {
563
+ if (hook.timerSecs < GAME_KIND_LIMITS.minTimerSecs || hook.timerSecs > GAME_KIND_LIMITS.maxTimerSecs)
564
+ return 'A jackpot’s timer lasts 5 minutes to 30 days.';
565
+ if (BigInt(hook.minTokens) < 1n)
566
+ return 'A qualifying buy is at least one base unit.';
567
+ return null;
568
+ }
569
+ if (hook.roundSecs < L.minEpochSecs || hook.roundSecs > L.maxRoundSecs)
570
+ return 'An epoch lasts an hour to 30 days.';
571
+ if (game.claimWindowSecs < L.minEpochClaimWindowSecs || game.claimWindowSecs > L.maxClaimWindowSecs || game.claimWindowSecs * L.claimsPerRound > hook.roundSecs)
572
+ return 'A streak leaves claims 5 minutes to a day, at most half an epoch.';
573
+ if (hook.minStreakSecs > GAME_KIND_LIMITS.maxMinStreakSecs)
574
+ return 'A streak asks at most a year without sending.';
575
+ if (BigInt(hook.minWeight) < 1n)
576
+ return 'The least weight that shares is at least one base unit.';
577
+ return null;
578
+ }
579
+ /** Each game kind in a word: "jackpot". */
580
+ export const GAME_KIND_NAMES = { lottery: 'lottery', jackpot: 'jackpot', streak: 'streak', strategy: 'strategy' };
581
+ /**
582
+ * The copy rules of a jackpot and a streak coin (docs/games.md "Phase 2", "Site"): said on the
583
+ * launch form, the review step and the token page. The pot is held by the companion and paid by
584
+ * code (no randomness in either kind), the 10 SOL cap until the hook is audited, "Bordrless can
585
+ * stop this game"; never "provably fair", "guaranteed" or "audited".
586
+ */
587
+ export const JACKPOT_COPY = {
588
+ held: 'The pot is held by code, not by a person. No luck involved: the last buyer wins.',
589
+ rule: 'The last buyer wins when the timer runs out, as long as they still hold what they bought. Selling or sending any of it forfeits.',
590
+ unfunded: 'If the pot is under its minimum when the timer runs out, no prize is paid and the pot keeps growing.',
591
+ curve: 'The game runs while the coin is on its curve and ends at graduation. A round under way is still paid.',
592
+ cap: LOTTERY_COPY.cap,
593
+ stop: LOTTERY_COPY.stop,
594
+ };
595
+ export const STREAK_COPY = {
596
+ held: 'The pot is held by code, not by a person. No luck involved: holders share it in proportion to what they held.',
597
+ rule: 'Hold through the epoch without sending anything. Any sale or transfer drops you out of this epoch and the last one.',
598
+ weight: 'Your share is based on what you held when the epoch began. Tokens you buy during an epoch count from the next one.',
599
+ claims: 'Shares are paid out during the next epoch. Most are paid automatically; small ones are claimed here. Unclaimed shares stay in the pot.',
600
+ cap: LOTTERY_COPY.cap,
601
+ stop: LOTTERY_COPY.stop,
473
602
  };
474
603
  /** Why a round paid no prize, in a few words each (`GameRolloverReason`). */
475
604
  export const ROLLOVER_WORDS = {
476
- noTickets: 'Nobody held tickets',
477
- roundForgotten: 'The round was forgotten',
478
- noClaim: 'No winner claimed in time',
479
- oracleSilent: 'ORAO never answered',
605
+ noTickets: 'No tickets yet: tokens count from the round after they are bought',
606
+ roundForgotten: 'The round was skipped',
607
+ noClaim: 'The winner did not claim in time',
608
+ oracleSilent: 'The random number never arrived',
480
609
  blocked: 'Stopped by Bordrless',
481
610
  late: 'Drawn too late',
482
- oracleUnreadable: 'ORAO’s answer unreadable',
483
- oracleUnpaid: 'The pot could not pay ORAO',
611
+ oracleUnreadable: 'The random number could not be read',
612
+ oracleUnpaid: 'The pot could not pay for the random number',
484
613
  };
485
614
  /** Whether a split is the whole fee to the launcher (no companion needed). */
486
615
  export const isLauncherSplit = (s) => s.buybackBps === 0 && s.holdersBps === 0 && s.beneficiaryBps === 10_000;
@@ -532,6 +661,132 @@ export function companionSplitProblem(split, rules, creatorFeeBps) {
532
661
  return 'A share of the creator fee for holders needs holder rewards on.';
533
662
  return null;
534
663
  }
664
+ export const SNOWBALL_PRESETS = {
665
+ conservative: { buyStepBps: 300, sellStepBps: 300, buyBps: 1_000, sellBps: 1_000, poolBps: 100, interval: 60, flatTrades: true },
666
+ normal: { buyStepBps: 200, sellStepBps: 200, buyBps: 2_500, sellBps: 2_500, poolBps: 200, interval: 30, flatTrades: true },
667
+ aggressive: { buyStepBps: 100, sellStepBps: 100, buyBps: 5_000, sellBps: 5_000, poolBps: 500, interval: 10, flatTrades: true },
668
+ };
669
+ export const SNOWBALL_PRESET_LIST = ['conservative', 'normal', 'aggressive'];
670
+ export const SNOWBALL_DEFAULT_PRESET = 'normal';
671
+ /** The custom controls' choices. The program takes anything within `SNOWBALL_BOUNDS`. */
672
+ export const SNOWBALL_STEP_CHOICES_BPS = [50, 100, 200, 300, 500, 1_000];
673
+ export const SNOWBALL_SIZE_CHOICES_BPS = [1_000, 2_500, 5_000, 7_500, 10_000];
674
+ export const SNOWBALL_POOL_CHOICES_BPS = [100, 200, 500, 1_000];
675
+ export const SNOWBALL_INTERVAL_CHOICES_SECS = [10, 30, 60, 300];
676
+ /** The program's bounds (`create_snowball`). */
677
+ export const SNOWBALL_BOUNDS = { minStepBps: 50, maxStepBps: 2_000, minSizeBps: 100, maxSizeBps: 10_000, minPoolBps: 25, maxPoolBps: 1_000, minInterval: 10, maxInterval: 86_400 };
678
+ /** The Snowball a launch request's `companion` names; null without one; `'malformed'` when it is there and not whole numbers and a flag. */
679
+ export function snowballRequestOf(companion) {
680
+ if (typeof companion !== 'object' || companion === null)
681
+ return null;
682
+ const raw = companion.snowball;
683
+ if (raw === undefined || raw === null)
684
+ return null;
685
+ if (typeof raw !== 'object')
686
+ return 'malformed';
687
+ const r = raw;
688
+ if (![r.potBps, r.buyStepBps, r.sellStepBps, r.buyBps, r.sellBps, r.poolBps].every(isShare))
689
+ return 'malformed';
690
+ if (typeof r.interval !== 'number' || !Number.isInteger(r.interval) || typeof r.flatTrades !== 'boolean')
691
+ return 'malformed';
692
+ return { potBps: r.potBps, buyStepBps: r.buyStepBps, sellStepBps: r.sellStepBps, buyBps: r.buyBps, sellBps: r.sellBps, poolBps: r.poolBps, interval: r.interval, flatTrades: r.flatTrades };
693
+ }
694
+ /**
695
+ * Why a Snowball with this split can't run these rules at this fee, in the words the launch form and
696
+ * the backend use (`companion_rules`); null when it can: the market maker's part with the split adds up
697
+ * to 10,000, its terms are within the program's bounds, and what any companion needs
698
+ * (`companionSplitProblem`'s rules).
699
+ */
700
+ export function snowballProblem(s, split, rules, creatorFeeBps) {
701
+ const B = SNOWBALL_BOUNDS;
702
+ const within = (v, lo, hi) => Number.isInteger(v) && v >= lo && v <= hi;
703
+ if (!(s.potBps > 0) || split.buybackBps + split.holdersBps + split.beneficiaryBps + s.potBps !== 10_000)
704
+ return 'The market maker’s part and the rest of the creator fee must add up to 100%.';
705
+ if (!within(s.buyStepBps, B.minStepBps, B.maxStepBps) || !within(s.sellStepBps, B.minStepBps, B.maxStepBps))
706
+ return 'A buy or a sell triggers on a move of 0.5% to 20%.';
707
+ if (!within(s.buyBps, B.minSizeBps, B.maxSizeBps) || !within(s.sellBps, B.minSizeBps, B.maxSizeBps))
708
+ return 'Each trade uses 1% to 100% of its SOL or its tokens.';
709
+ if (!within(s.poolBps, B.minPoolBps, B.maxPoolBps))
710
+ return 'A trade can be up to 0.25% to 10% of the pool.';
711
+ if (!within(s.interval, B.minInterval, B.maxInterval))
712
+ return 'Trades are 10 seconds to a day apart.';
713
+ if (rules.creatorLockDays > 0)
714
+ return 'A creator fee run by a program can’t come with the creator wallet lock: the program vests your first buy instead.';
715
+ if (creatorFeeBps === 0)
716
+ return 'There is no creator fee to fill the market maker with: set one.';
717
+ if (splitPaysHolders(split) && !rewardsOn(rules))
718
+ return 'A share of the creator fee for holders needs holder rewards on.';
719
+ return null;
720
+ }
721
+ // ---- Custom companions (2026-10-12): a Workshop script spends the pot (docs/scripts.md) --------------
722
+ /** The companion program's script bounds (`create_script`): steps 10 seconds to a day apart, at most 512 bytes of init data, at most 10,240 bytes of state. */
723
+ export const SCRIPT_BOUNDS = { minInterval: 10, maxInterval: 86_400, maxInitLen: 512, maxStateLen: 10_240 };
724
+ /** The interval choices the launch form offers (seconds): a minute, five, fifteen, an hour, a day. */
725
+ export const SCRIPT_INTERVAL_CHOICES_SECS = [60, 300, 900, 3_600, 86_400];
726
+ const BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
727
+ const HEX_BYTES = /^(?:0x)?(?:[0-9a-fA-F]{2})*$/;
728
+ const BASE64_TEXT = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
729
+ /**
730
+ * How many bytes a script's init data holds, as the request writes it: hex (an even number of hex
731
+ * digits, `0x` or not) or base64; the empty string is no data. Null when it is neither. Hex is tried
732
+ * first: a text that reads as both (`abcd`) is read as hex.
733
+ */
734
+ export function scriptInitDataLength(raw) {
735
+ if (raw === '')
736
+ return 0;
737
+ if (HEX_BYTES.test(raw))
738
+ return (raw.startsWith('0x') ? raw.length - 2 : raw.length) / 2;
739
+ if (BASE64_TEXT.test(raw))
740
+ return (raw.length / 4) * 3 - (raw.endsWith('==') ? 2 : raw.endsWith('=') ? 1 : 0);
741
+ return null;
742
+ }
743
+ /** The custom companion's script a launch request's `companion` names; null without one; `'malformed'` when it is there and not the fields `ScriptRequest` takes. */
744
+ export function scriptRequestOf(companion) {
745
+ if (typeof companion !== 'object' || companion === null)
746
+ return null;
747
+ const raw = companion.script;
748
+ if (raw === undefined || raw === null)
749
+ return null;
750
+ if (typeof raw !== 'object')
751
+ return 'malformed';
752
+ const r = raw;
753
+ if (!isShare(r.potBps) || typeof r.program !== 'string' || !BASE58_ADDRESS.test(r.program))
754
+ return 'malformed';
755
+ if (typeof r.interval !== 'number' || !Number.isInteger(r.interval) || typeof r.stateLen !== 'number' || !Number.isInteger(r.stateLen))
756
+ return 'malformed';
757
+ const initData = r.initData === undefined || r.initData === null ? '' : r.initData;
758
+ if (typeof initData !== 'string' || scriptInitDataLength(initData) === null)
759
+ return 'malformed';
760
+ return { potBps: r.potBps, program: r.program, interval: r.interval, initData, stateLen: r.stateLen };
761
+ }
762
+ /**
763
+ * Why a custom companion with this split can't run these rules at this fee, in the words the launch
764
+ * form and the backend use (`companion_rules`); null when it can: the pot's part with the split adds
765
+ * up to 10,000, the interval, the init data and the state are within the program's bounds, and what
766
+ * any companion needs (`companionSplitProblem`'s rules). Whether the script is approved is the
767
+ * backend's to check (`script_not_approved`).
768
+ */
769
+ export function scriptProblem(s, split, rules, creatorFeeBps) {
770
+ const B = SCRIPT_BOUNDS;
771
+ if (!(s.potBps > 0) || split.buybackBps + split.holdersBps + split.beneficiaryBps + s.potBps !== 10_000)
772
+ return 'The script’s part and the rest of the creator fee must add up to 100%.';
773
+ if (!Number.isInteger(s.interval) || s.interval < B.minInterval || s.interval > B.maxInterval)
774
+ return 'Steps are 10 seconds to a day apart.';
775
+ const initLen = scriptInitDataLength(s.initData);
776
+ if (initLen === null)
777
+ return 'The script’s init data is hex or base64.';
778
+ if (initLen > B.maxInitLen)
779
+ return `The script’s init data is at most ${B.maxInitLen} bytes.`;
780
+ if (!Number.isInteger(s.stateLen) || s.stateLen < 0 || s.stateLen > B.maxStateLen)
781
+ return `The script’s state is 0 to ${B.maxStateLen.toLocaleString('en-US')} bytes.`;
782
+ if (rules.creatorLockDays > 0)
783
+ return 'A creator fee run by a program can’t come with the creator wallet lock: the program vests your first buy instead.';
784
+ if (creatorFeeBps === 0)
785
+ return 'There is no creator fee for the script to spend: set one.';
786
+ if (splitPaysHolders(split) && !rewardsOn(rules))
787
+ return 'A share of the creator fee for holders needs holder rewards on.';
788
+ return null;
789
+ }
535
790
  /** The template whose split this is, or null for any other split. */
536
791
  export function companionTemplateOf(split) {
537
792
  return COMPANION_TEMPLATE_LIST.find((t) => {
@@ -22,7 +22,25 @@ export declare const PROGRAM_IDS: {
22
22
  * (2026-10-08): companion v2 and this hook go live together, after their audit.
23
23
  */
24
24
  readonly lotteryHook: "HqFWsCBQ416DAfevJ9TspyT5yXGGoYTCpcreiGkCgWcr";
25
+ /**
26
+ * `hook_timelock` (phase 3a, bordrless-programs docs/phase3a.md §3): a hook's or a strategy's
27
+ * upgrade authority behind a public delay of at least 3 days; its only exit is "immutable". Not
28
+ * deployed yet (2026-10-09).
29
+ */
30
+ readonly hookTimelock: "BBUzaamchPWZpKENmn7bopiuQWvRGm2Vg8TqLLgzgGGZ";
31
+ /**
32
+ * `hook_vault` (phase 3a, §5): where a hook's cuts go, each slot with a policy fixed before the
33
+ * launch (burn, sell for SOL to a fixed wallet, sell and buy-and-burn another token), run by a
34
+ * permissionless crank under the companion's buyback guards. Not deployed yet (2026-10-09).
35
+ */
36
+ readonly hookVault: "5cojoUStG7WFhHJiSncCUEwTqDuhDLKu4a9BqTbF8jzG";
25
37
  };
38
+ /**
39
+ * Bordrless Studio's attester (phase 3a, docs/phase3a.md §6): a hot key of Studio's build worker,
40
+ * the only signer of the companion's `HookAttestation` ("Bordrless rebuilt this source, its hash is
41
+ * the code on chain, the checks, the simulator and the review passed"). Not Studio's upgrade key.
42
+ */
43
+ export declare const STUDIO_ATTESTER = "3uGLsTJNse7vE3pgRkAf6aKKBoKmCyPUcNNu1bw3KvP2";
26
44
  /**
27
45
  * The lottery hook's fixed facts (programs/lottery_hook/README.md): the flags a `LaunchConfig`
28
46
  * names for it (`BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA`, exactly what a companion game
package/dist/programs.js CHANGED
@@ -22,7 +22,25 @@ export const PROGRAM_IDS = {
22
22
  * (2026-10-08): companion v2 and this hook go live together, after their audit.
23
23
  */
24
24
  lotteryHook: 'HqFWsCBQ416DAfevJ9TspyT5yXGGoYTCpcreiGkCgWcr',
25
+ /**
26
+ * `hook_timelock` (phase 3a, bordrless-programs docs/phase3a.md §3): a hook's or a strategy's
27
+ * upgrade authority behind a public delay of at least 3 days; its only exit is "immutable". Not
28
+ * deployed yet (2026-10-09).
29
+ */
30
+ hookTimelock: 'BBUzaamchPWZpKENmn7bopiuQWvRGm2Vg8TqLLgzgGGZ',
31
+ /**
32
+ * `hook_vault` (phase 3a, §5): where a hook's cuts go, each slot with a policy fixed before the
33
+ * launch (burn, sell for SOL to a fixed wallet, sell and buy-and-burn another token), run by a
34
+ * permissionless crank under the companion's buyback guards. Not deployed yet (2026-10-09).
35
+ */
36
+ hookVault: '5cojoUStG7WFhHJiSncCUEwTqDuhDLKu4a9BqTbF8jzG',
25
37
  };
38
+ /**
39
+ * Bordrless Studio's attester (phase 3a, docs/phase3a.md §6): a hot key of Studio's build worker,
40
+ * the only signer of the companion's `HookAttestation` ("Bordrless rebuilt this source, its hash is
41
+ * the code on chain, the checks, the simulator and the review passed"). Not Studio's upgrade key.
42
+ */
43
+ export const STUDIO_ATTESTER = '3uGLsTJNse7vE3pgRkAf6aKKBoKmCyPUcNNu1bw3KvP2';
26
44
  /**
27
45
  * The lottery hook's fixed facts (programs/lottery_hook/README.md): the flags a `LaunchConfig`
28
46
  * names for it (`BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA`, exactly what a companion game