@bordrless/shared 0.4.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
  }
@@ -244,6 +301,8 @@ export interface LaunchSummary {
244
301
  protocolModel: ProtocolModel;
245
302
  /** Under the share model, Bordrless's share of the cuts in basis points (2,500 = a quarter); null under the flat model. */
246
303
  protocolShareBps: number | null;
304
+ /** The pool's LP fee (the sniper fee included) goes to Bordrless in SOL: a launch pool created at or after the DEX upgrade of 2026-10-08 (`LP_FEE_TO_PROTOCOL_FROM_SLOT`). False on one created before it, whose LP fee stays in the pool. */
305
+ lpFeeToProtocol: boolean;
247
306
  /** Total fee of a buy by the §7.3 formula, outside the sniper window. */
248
307
  buyFeeBps: number;
249
308
  /** Total fee of a sell by the §7.3 formula, outside the sniper window. */
@@ -279,6 +338,8 @@ export interface LaunchDetail extends LaunchSummary {
279
338
  ruleStats: RuleStats;
280
339
  /** The bridged-SOL holding holder rewards are paid from; null without holder rewards. */
281
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;
282
343
  }
283
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. */
284
345
  export type LaunchSort = 'new' | 'mcap' | 'volume' | 'trending' | 'graduated' | 'rewards24h' | 'fees';
@@ -447,7 +508,8 @@ export interface PreparedTx {
447
508
  stage: number;
448
509
  label: string;
449
510
  /** Keys the browser holds that must also sign (a launch's fresh mint). */
450
- extraSigners: ('mint')[];
511
+ /** Keys the browser holds that sign after the wallet: a launch's fresh mint, a new launch config. */
512
+ extraSigners: ('mint' | 'config')[];
451
513
  }
452
514
  export interface SwapPrepareResponse {
453
515
  transactions: PreparedTx[];
@@ -496,6 +558,94 @@ export interface LaunchPrepareRequest {
496
558
  * (what the site showed is what launches), and a custom hook's accounts are resolved for `mint`.
497
559
  */
498
560
  config?: Address;
561
+ /**
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.
570
+ */
571
+ companion?: CompanionRequest;
572
+ }
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). */
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;
499
649
  }
500
650
  export interface LaunchPrepareResponse {
501
651
  intentId: string;
@@ -539,6 +689,24 @@ export interface RewardsPrepareRequest {
539
689
  /** Unwrap the bridged SOL to SOL and close the temporary holding. */
540
690
  unwrap: boolean;
541
691
  }
692
+ /**
693
+ * Send `amount` of `mint` from the owner's holding to `to`'s (made on the way when missing, the
694
+ * sender paying its rent). Wallets cannot build this themselves: the token program's transfer takes
695
+ * the mint's hook accounts, which the backend resolves. A holding under the creator lock or the
696
+ * early-buyer lock is refused with the lock (code `locked`), as the program would refuse it.
697
+ */
698
+ export interface TransferPrepareRequest {
699
+ owner: Address;
700
+ mint: Address;
701
+ to: Address;
702
+ /** Base units of the token. */
703
+ amount: Amount;
704
+ }
705
+ export interface TransferPrepareResponse {
706
+ transactions: PreparedTx[];
707
+ /** Lamports the sender pays for the recipient's holding; 0 when it already exists. */
708
+ rentLamports: Amount;
709
+ }
542
710
  /** Claim the creator fees of launches the owner created: up to about 4 mints per transaction, paid as SOL. */
543
711
  export interface CreatorPrepareRequest {
544
712
  owner: Address;
@@ -585,6 +753,13 @@ export interface BridgeAsset {
585
753
  * opened. Absent from backends that predate it.
586
754
  */
587
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;
588
763
  }
589
764
  /** A bridged token's pool on the DEX (`BridgeAsset.pool`): where it is, since when, and its last price. */
590
765
  export interface BridgePool {
@@ -600,6 +775,8 @@ export interface BridgePool {
600
775
  lpFeeBps: number;
601
776
  /** The flat protocol rate of the quote on this pool (ordinary pools; 0 under the share model). */
602
777
  protocolFeeBps: number;
778
+ /** The LP fee goes to Bordrless in SOL (a launch pool since the upgrade of 2026-10-08, `LP_FEE_TO_PROTOCOL_FROM_SLOT`); false when it stays in the pool, as an ordinary pool's always does. */
779
+ lpFeeToProtocol: boolean;
603
780
  baseReserve: Amount;
604
781
  quoteReserve: Amount;
605
782
  volume24hUsd: number | null;
@@ -639,6 +816,8 @@ export interface PoolSummary {
639
816
  protocolModel?: ProtocolModel;
640
817
  /** Under the share model, Bordrless's share of what the hooks cut, in basis points; null (or absent) under the flat model. */
641
818
  protocolShareBps?: number | null;
819
+ /** The LP fee goes to Bordrless in SOL (a launch pool since the upgrade of 2026-10-08); false when it stays in the pool. Absent from backends that predate it. */
820
+ lpFeeToProtocol?: boolean;
642
821
  hookProgram: Address | null;
643
822
  /** The base token's creator-written hook (§5.8), for a launch pool whose token has one; null or absent otherwise. */
644
823
  customHook?: CustomHookInfo | null;
@@ -681,3 +860,264 @@ export interface ApiErrorBody {
681
860
  error: string;
682
861
  code?: string;
683
862
  }
863
+ /** Which of the standard's programs emitted an event: the five of Bordrless, or any hook program (`hook`). */
864
+ export type ExplorerProgram = 'token' | 'swap' | 'bridge' | 'launch' | 'kit' | 'hook';
865
+ /** What an address turned out to be, in the order the backend looks: a mint, a pool, a bridge wrapper, a holding, a program, a wallet, a launch config, or nothing it knows. */
866
+ export type ExplorerKind = 'mint' | 'pool' | 'wrapper' | 'holding' | 'program' | 'wallet' | 'config' | 'unknown';
867
+ export interface ExplorerStats {
868
+ /** Mints of the standard the indexer has seen. */
869
+ mints: number;
870
+ /** Holdings with a balance. */
871
+ holdings: number;
872
+ /** Transactions indexed (distinct signatures), all time and in the last 24 hours. */
873
+ transactions: number;
874
+ transactions24h: number;
875
+ /** The newest slot indexed; null before the first event. */
876
+ newestSlot: number | null;
877
+ }
878
+ /** A token an event's amounts are in. */
879
+ export interface ExplorerToken {
880
+ mint: Address;
881
+ symbol: string;
882
+ decimals: number;
883
+ }
884
+ /** One decoded event of `chain_events`, as the explorer shows it. */
885
+ export interface ExplorerEvent {
886
+ signature: string;
887
+ ordinal: number;
888
+ slot: number;
889
+ blockTime: number | null;
890
+ /** The program id that emitted it. */
891
+ program: Address;
892
+ /** The IDL's event name in PascalCase (`Transferred`, `Swapped`, `KitInstalled`). */
893
+ name: string;
894
+ /** The account the event is mostly about (the mint, the pool, the holding, ...); null when it has none. */
895
+ subject: Address | null;
896
+ /** The decoded fields: keys as the SDK's coder gives them (camelCase), pubkeys as base58, integers as decimal strings. */
897
+ payload: Record<string, unknown>;
898
+ /** The tokens its amounts are in, where the indexer knows them: the event's own mint (or the pool's base), and the pool's quote. */
899
+ base: ExplorerToken | null;
900
+ quote: ExplorerToken | null;
901
+ }
902
+ export interface ExplorerEvents {
903
+ events: ExplorerEvent[];
904
+ /** The cursor for the page after this one (`before=`); null on the last page. */
905
+ next: string | null;
906
+ }
907
+ /** `GET /v1/explorer/tx/:signature`: a transaction the indexer has, with every event it decoded, in order. */
908
+ export interface ExplorerTx {
909
+ signature: string;
910
+ slot: number;
911
+ blockTime: number | null;
912
+ /** The program ids its events came from, first seen first. */
913
+ programs: Address[];
914
+ events: ExplorerEvent[];
915
+ }
916
+ export interface ExplorerMint {
917
+ mint: Address;
918
+ name: string;
919
+ symbol: string;
920
+ image: string | null;
921
+ decimals: number;
922
+ supply: Amount;
923
+ maxSupply: Amount;
924
+ creator: Address;
925
+ createdAt: number;
926
+ kind: 'launch' | 'bridged' | 'lp' | 'other';
927
+ mintAuthority: Address | null;
928
+ freezeAuthority: Address | null;
929
+ hookAuthority: Address | null;
930
+ metadataAuthority: Address | null;
931
+ hookProgram: Address | null;
932
+ hookFlags: number;
933
+ /** Holdings with a balance. */
934
+ holders: number;
935
+ }
936
+ export interface ExplorerHolder {
937
+ owner: Address;
938
+ holding: Address;
939
+ amount: Amount;
940
+ /** Of the supply, 0 to 1. */
941
+ share: number;
942
+ }
943
+ /** A pool as the explorer lists it on a mint's page, or shows it on its own. */
944
+ export interface ExplorerPool {
945
+ pool: Address;
946
+ baseMint: Address;
947
+ quoteMint: Address;
948
+ baseSymbol: string;
949
+ quoteSymbol: string;
950
+ baseDecimals: number;
951
+ quoteDecimals: number;
952
+ baseReserve: Amount;
953
+ quoteReserve: Amount;
954
+ lpMint: Address;
955
+ lpSupply: Amount;
956
+ lpFeeBps: number;
957
+ protocolFeeBps: number;
958
+ protocolModel: ProtocolModel;
959
+ protocolShareBps: number | null;
960
+ /** The LP fee goes to Bordrless in SOL (a launch pool since the upgrade of 2026-10-08); false when it stays in the pool. */
961
+ lpFeeToProtocol: boolean;
962
+ hookProgram: Address | null;
963
+ hookFlags: number;
964
+ curve: boolean;
965
+ creator: Address;
966
+ createdAt: number;
967
+ swapCount: number;
968
+ volume24hUsd: number | null;
969
+ /** The launch this is the pool of, if any. */
970
+ launchMint: Address | null;
971
+ }
972
+ export interface ExplorerHolding {
973
+ holding: Address;
974
+ mint: Address;
975
+ owner: Address;
976
+ amount: Amount;
977
+ symbol: string;
978
+ name: string;
979
+ image: string | null;
980
+ decimals: number;
981
+ /** Dollars, where the token has a pool against bridged SOL and SOL has a price. */
982
+ valueUsd: number | null;
983
+ }
984
+ export interface ExplorerProgramInfo extends ProgramInfo {
985
+ /** The program's name on the standard ("Token", "DEX", "Half-Life"); a hook the site does not know is "Hook". */
986
+ name: string;
987
+ /** Events indexed, by event name, most first. */
988
+ counts: {
989
+ name: string;
990
+ count: number;
991
+ }[];
992
+ }
993
+ export interface ExplorerWrapper {
994
+ wrapper: Address;
995
+ underlyingMint: Address;
996
+ underlyingProgram: Address;
997
+ wrappedMint: Address;
998
+ vault: Address;
999
+ decimals: number;
1000
+ native: boolean;
1001
+ totalWrapped: Amount;
1002
+ name: string;
1003
+ symbol: string;
1004
+ image: string | null;
1005
+ registrar: Address;
1006
+ registeredAt: number;
1007
+ }
1008
+ /** A `LaunchConfig` (docs/hooks-v2.md §5.7) as its `LaunchConfigCreated` event told it, and the launches made from it. */
1009
+ export interface ExplorerConfig {
1010
+ config: Address;
1011
+ creator: Address | null;
1012
+ label: string | null;
1013
+ creatorFeeBps: number | null;
1014
+ customHook: Address | null;
1015
+ customHookFlags: number | null;
1016
+ rules: Record<string, unknown> | null;
1017
+ launches: {
1018
+ mint: Address;
1019
+ name: string;
1020
+ symbol: string;
1021
+ image: string | null;
1022
+ }[];
1023
+ }
1024
+ /** A short reference to a token, for lists. */
1025
+ export interface ExplorerTokenRef {
1026
+ mint: Address;
1027
+ name: string;
1028
+ symbol: string;
1029
+ image: string | null;
1030
+ }
1031
+ /** `GET /v1/explorer/address/:address`: what lives at an address, by kind. Every kind carries the newest events touching it. */
1032
+ export type ExplorerAddress = {
1033
+ address: Address;
1034
+ events: ExplorerEvent[];
1035
+ } & ({
1036
+ kind: 'mint';
1037
+ mint: ExplorerMint;
1038
+ launch: {
1039
+ status: LaunchStatus;
1040
+ } | null;
1041
+ wrapper: {
1042
+ underlyingMint: Address;
1043
+ native: boolean;
1044
+ } | null;
1045
+ pools: ExplorerPool[];
1046
+ holders: ExplorerHolder[];
1047
+ } | {
1048
+ kind: 'pool';
1049
+ pool: ExplorerPool;
1050
+ trades: Trade[];
1051
+ } | {
1052
+ kind: 'wrapper';
1053
+ wrapper: ExplorerWrapper;
1054
+ } | {
1055
+ kind: 'holding';
1056
+ holding: ExplorerHolding;
1057
+ } | {
1058
+ kind: 'program';
1059
+ program: ExplorerProgramInfo;
1060
+ } | {
1061
+ kind: 'wallet';
1062
+ holdings: ExplorerHolding[];
1063
+ totalUsd: number | null;
1064
+ created: ExplorerTokenRef[];
1065
+ } | {
1066
+ kind: 'config';
1067
+ config: ExplorerConfig;
1068
+ } | {
1069
+ kind: 'unknown';
1070
+ });
1071
+ /** One answer to a search: an address resolved to its kind, a transaction, or a mint whose name or ticker matched. */
1072
+ export interface ExplorerHit {
1073
+ kind: ExplorerKind | 'tx';
1074
+ address: Address;
1075
+ name: string | null;
1076
+ symbol: string | null;
1077
+ image: string | null;
1078
+ }
1079
+ export interface ExplorerSearch {
1080
+ query: string;
1081
+ hits: ExplorerHit[];
1082
+ }
1083
+ /**
1084
+ * `POST /v1/config/prepare`: a `LaunchConfig` made from the launch page (Build your own, §5.7), for a
1085
+ * hook program of the creator's own. `config` is the address of a keypair the browser made; it signs
1086
+ * after the wallet. The kit's rules can't go with a hook of one's own, so a config carries burn and a
1087
+ * creator fee only.
1088
+ */
1089
+ export interface ConfigPrepareRequest {
1090
+ creator: Address;
1091
+ config: Address;
1092
+ customHook: Address;
1093
+ /** `TOKEN_HOOK_FLAGS`: at least one callback, no unknown bit. */
1094
+ customHookFlags: number;
1095
+ burnBuyBps: number;
1096
+ burnSellBps: number;
1097
+ creatorFeeBps: number;
1098
+ /** At most 32 bytes. */
1099
+ label: string;
1100
+ }
1101
+ export interface ConfigPrepareResponse {
1102
+ config: Address;
1103
+ transactions: PreparedTx[];
1104
+ }
1105
+ /** `GET /v1/hooks/:program?mint=`: a hook program as the launch page checks it, and whether it is prepared for the mint. */
1106
+ export interface HookInspection {
1107
+ program: Address;
1108
+ /** Deployed as a program on this cluster. */
1109
+ executable: boolean;
1110
+ /** None of Bordrless's own programs. */
1111
+ ownProgram: boolean;
1112
+ upgradeAuthority: Address | null;
1113
+ upgradeable: boolean | null;
1114
+ /** With a mint: the registry it must write for that mint, `["bordrless-hook-accounts", mint]` under the program. */
1115
+ registry: Address | null;
1116
+ /** With a mint: whether that registry exists and decodes. */
1117
+ registryReady: boolean | null;
1118
+ /** The registry's extra accounts, once it exists; a launch fits at most `maxExtras`. */
1119
+ extras: number | null;
1120
+ maxExtras: number;
1121
+ /** Why it can't be launched with yet, one sentence each. */
1122
+ problems: string[];
1123
+ }
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). */
@@ -14,6 +14,27 @@ export declare const PROGRAM_IDS: {
14
14
  readonly kit: "14RJQXPdJfkehit6ezktjd3xujamf8nVSKw2shKamaEH";
15
15
  /** Half-Life (programs/half_life): an exit fee that halves every six hours a token is held, burned. */
16
16
  readonly halfLife: "53SpmtkdPWQ63mWoDeXk8P9tuwiT4ed2Wx4fwfy5NSF8";
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
+ 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";
17
38
  };
18
39
  /**
19
40
  * Half-Life, Bordrless's own token hook (programs/half_life/README.md): the `LaunchConfig` the
@@ -39,6 +60,26 @@ export declare const HALF_LIFE: {
39
60
  * (20% at 0, halved every six hours, linear within each, 0 from 48 h).
40
61
  */
41
62
  export declare function halfLifeFeePpm(ageSecs: number): number;
63
+ /**
64
+ * The DEX upgrade of 2026-10-08 (docs/hooks-v2.md §3.1, "The LP fee of a launch pool is
65
+ * Bordrless's"): from this slot on mainnet a launch pool's LP fee (the sniper fee included) is
66
+ * taken in SOL and paid to Bordrless with the protocol fee; a launch pool created before it keeps
67
+ * its LP fee in the pool, compounding, as its traders were told. The slot is the DEX program's
68
+ * last upgrade, read from its ProgramData account; the time is that slot's block time
69
+ * (`getBlockTime`, unix seconds), which the backend compares a pool's `created_at` against (the
70
+ * pools table keeps a timestamp, not a slot). Devnet and localnet were deployed after the rule:
71
+ * every launch pool there pays Bordrless.
72
+ */
73
+ export declare const LP_FEE_TO_PROTOCOL_FROM_SLOT = 454439142;
74
+ export declare const LP_FEE_TO_PROTOCOL_FROM_TIME = 1791434238;
75
+ /**
76
+ * Whether a pool's LP fee goes to Bordrless (`LaunchSummary.lpFeeToProtocol`): every launch pool's
77
+ * (the share model), whenever it was created. The upgraded DEX keys the fee's destination on the
78
+ * pool's fee model alone, so a launch pool opened before the upgrade compounded its LP fee until
79
+ * that slot and pays Bordrless since. An ordinary pool's LP fee is its liquidity's. `createdAt`
80
+ * and `cluster` are kept for the record of when the rule began (the constants above).
81
+ */
82
+ export declare function lpFeeToProtocol(shareModel: boolean, _createdAt: number, _cluster: 'mainnet' | 'devnet' | 'localnet'): boolean;
42
83
  /** The wSOL mint, which stands for native SOL on the bridge. */
43
84
  export declare const NATIVE_MINT = "So11111111111111111111111111111111111111112";
44
85
  export declare const TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -89,9 +130,11 @@ export declare const FIXED_ADDRESSES: {
89
130
  readonly kitEventAuthority: "9abhxVTuwck5Ux79act3e2Vkfem7Q4zBwATMctUvHvuE";
90
131
  /** `["hook-authority"]` under the kit: it signs the token program's `write_hook_data` in `claim`. */
91
132
  readonly kitHookAuthority: "2repKA1JgDkBo4AffscVee342dcBcH6c2yfpUTRrAiEN";
133
+ /** `["__event_authority"]` under the companion program. */
134
+ readonly companionEventAuthority: "7QuoYRuD9MHzX5EN7qqj62iwNPVqfy74RzmwJ524cgDt";
92
135
  };
93
136
  /**
94
- * The protocol lookup table (docs/hooks-v2.md §6; programs-summary §2.7): the 18 fixed addresses
137
+ * The protocol lookup table (docs/hooks-v2.md §6; programs-summary §2.7): the 22 fixed addresses
95
138
  * no top-level instruction invokes, in the order the programs' tests load them and `pnpm admin
96
139
  * init` must write them (a v0 message names table entries by index). Programs a transaction
97
140
  * invokes at top level (the kit for claims and shares) are kept in the static keys by the v0
package/dist/programs.js CHANGED
@@ -14,6 +14,27 @@ export const PROGRAM_IDS = {
14
14
  kit: '14RJQXPdJfkehit6ezktjd3xujamf8nVSKw2shKamaEH',
15
15
  /** Half-Life (programs/half_life): an exit fee that halves every six hours a token is held, burned. */
16
16
  halfLife: '53SpmtkdPWQ63mWoDeXk8P9tuwiT4ed2Wx4fwfy5NSF8',
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
+ 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',
17
38
  };
18
39
  /**
19
40
  * Half-Life, Bordrless's own token hook (programs/half_life/README.md): the `LaunchConfig` the
@@ -49,6 +70,28 @@ export function halfLifeFeePpm(ageSecs) {
49
70
  const lo = Math.floor(HALF_LIFE.maxFeePpm / 2 ** (halvings + 1));
50
71
  return hi - Math.floor(((hi - lo) * into) / HALF_LIFE.halfLifeSecs);
51
72
  }
73
+ /**
74
+ * The DEX upgrade of 2026-10-08 (docs/hooks-v2.md §3.1, "The LP fee of a launch pool is
75
+ * Bordrless's"): from this slot on mainnet a launch pool's LP fee (the sniper fee included) is
76
+ * taken in SOL and paid to Bordrless with the protocol fee; a launch pool created before it keeps
77
+ * its LP fee in the pool, compounding, as its traders were told. The slot is the DEX program's
78
+ * last upgrade, read from its ProgramData account; the time is that slot's block time
79
+ * (`getBlockTime`, unix seconds), which the backend compares a pool's `created_at` against (the
80
+ * pools table keeps a timestamp, not a slot). Devnet and localnet were deployed after the rule:
81
+ * every launch pool there pays Bordrless.
82
+ */
83
+ export const LP_FEE_TO_PROTOCOL_FROM_SLOT = 454_439_142;
84
+ export const LP_FEE_TO_PROTOCOL_FROM_TIME = 1_791_434_238;
85
+ /**
86
+ * Whether a pool's LP fee goes to Bordrless (`LaunchSummary.lpFeeToProtocol`): every launch pool's
87
+ * (the share model), whenever it was created. The upgraded DEX keys the fee's destination on the
88
+ * pool's fee model alone, so a launch pool opened before the upgrade compounded its LP fee until
89
+ * that slot and pays Bordrless since. An ordinary pool's LP fee is its liquidity's. `createdAt`
90
+ * and `cluster` are kept for the record of when the rule began (the constants above).
91
+ */
92
+ export function lpFeeToProtocol(shareModel, _createdAt, _cluster) {
93
+ return shareModel;
94
+ }
52
95
  /** The wSOL mint, which stands for native SOL on the bridge. */
53
96
  export const NATIVE_MINT = 'So11111111111111111111111111111111111111112';
54
97
  export const TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
@@ -99,9 +142,11 @@ export const FIXED_ADDRESSES = {
99
142
  kitEventAuthority: '9abhxVTuwck5Ux79act3e2Vkfem7Q4zBwATMctUvHvuE',
100
143
  /** `["hook-authority"]` under the kit: it signs the token program's `write_hook_data` in `claim`. */
101
144
  kitHookAuthority: '2repKA1JgDkBo4AffscVee342dcBcH6c2yfpUTRrAiEN',
145
+ /** `["__event_authority"]` under the companion program. */
146
+ companionEventAuthority: '7QuoYRuD9MHzX5EN7qqj62iwNPVqfy74RzmwJ524cgDt',
102
147
  };
103
148
  /**
104
- * The protocol lookup table (docs/hooks-v2.md §6; programs-summary §2.7): the 18 fixed addresses
149
+ * The protocol lookup table (docs/hooks-v2.md §6; programs-summary §2.7): the 22 fixed addresses
105
150
  * no top-level instruction invokes, in the order the programs' tests load them and `pnpm admin
106
151
  * init` must write them (a v0 message names table entries by index). Programs a transaction
107
152
  * invokes at top level (the kit for claims and shares) are kept in the static keys by the v0
@@ -126,6 +171,12 @@ export const PROTOCOL_LOOKUP_TABLE_ADDRESSES = [
126
171
  FIXED_ADDRESSES.bridgedSolMint,
127
172
  SYSTEM_PROGRAM_ID,
128
173
  ASSOCIATED_TOKEN_PROGRAM_ID,
174
+ // Appended 2026-10-08 for launches through a companion (docs/companions.md): the companion's event
175
+ // authority and the programs it invokes, so its launch transaction fits (1,142 bytes instead of 1,266).
176
+ FIXED_ADDRESSES.companionEventAuthority,
177
+ PROGRAM_IDS.launch,
178
+ PROGRAM_IDS.swap,
179
+ PROGRAM_IDS.token,
129
180
  ];
130
181
  /** Token hook flags (`Mint.hookFlags`). */
131
182
  export const TOKEN_HOOK_FLAGS = {
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.4.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",