@peddles/sdk 0.2.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.
Files changed (124) hide show
  1. package/CLAUDE_PROMPT.md +159 -0
  2. package/LICENSE +21 -0
  3. package/README.md +186 -0
  4. package/dist/abis.d.ts +19 -0
  5. package/dist/abis.d.ts.map +1 -0
  6. package/dist/abis.generated.d.ts +2029 -0
  7. package/dist/abis.generated.d.ts.map +1 -0
  8. package/dist/abis.generated.js +2723 -0
  9. package/dist/abis.generated.js.map +1 -0
  10. package/dist/abis.js +19 -0
  11. package/dist/abis.js.map +1 -0
  12. package/dist/artDex.d.ts +937 -0
  13. package/dist/artDex.d.ts.map +1 -0
  14. package/dist/artDex.js +155 -0
  15. package/dist/artDex.js.map +1 -0
  16. package/dist/client.d.ts +10 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +2 -0
  19. package/dist/client.js.map +1 -0
  20. package/dist/clog.d.ts +75 -0
  21. package/dist/clog.d.ts.map +1 -0
  22. package/dist/clog.js +91 -0
  23. package/dist/clog.js.map +1 -0
  24. package/dist/clogInflow.d.ts +136 -0
  25. package/dist/clogInflow.d.ts.map +1 -0
  26. package/dist/clogInflow.js +177 -0
  27. package/dist/clogInflow.js.map +1 -0
  28. package/dist/deployments.d.ts +30 -0
  29. package/dist/deployments.d.ts.map +1 -0
  30. package/dist/deployments.generated.d.ts +92 -0
  31. package/dist/deployments.generated.d.ts.map +1 -0
  32. package/dist/deployments.generated.js +93 -0
  33. package/dist/deployments.generated.js.map +1 -0
  34. package/dist/deployments.js +52 -0
  35. package/dist/deployments.js.map +1 -0
  36. package/dist/feeSplit.d.ts +54 -0
  37. package/dist/feeSplit.d.ts.map +1 -0
  38. package/dist/feeSplit.js +69 -0
  39. package/dist/feeSplit.js.map +1 -0
  40. package/dist/feeTerms.d.ts +46 -0
  41. package/dist/feeTerms.d.ts.map +1 -0
  42. package/dist/feeTerms.js +80 -0
  43. package/dist/feeTerms.js.map +1 -0
  44. package/dist/index.d.ts +102 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +108 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/launch/abi.generated.d.ts +2667 -0
  49. package/dist/launch/abi.generated.d.ts.map +1 -0
  50. package/dist/launch/abi.generated.js +2472 -0
  51. package/dist/launch/abi.generated.js.map +1 -0
  52. package/dist/launch/addresses.d.ts +24 -0
  53. package/dist/launch/addresses.d.ts.map +1 -0
  54. package/dist/launch/addresses.js +35 -0
  55. package/dist/launch/addresses.js.map +1 -0
  56. package/dist/launch/feeTerms.d.ts +52 -0
  57. package/dist/launch/feeTerms.d.ts.map +1 -0
  58. package/dist/launch/feeTerms.js +72 -0
  59. package/dist/launch/feeTerms.js.map +1 -0
  60. package/dist/launch/index.d.ts +33 -0
  61. package/dist/launch/index.d.ts.map +1 -0
  62. package/dist/launch/index.js +24 -0
  63. package/dist/launch/index.js.map +1 -0
  64. package/dist/launch/revert.d.ts +23 -0
  65. package/dist/launch/revert.d.ts.map +1 -0
  66. package/dist/launch/revert.js +169 -0
  67. package/dist/launch/revert.js.map +1 -0
  68. package/dist/launch/salt.d.ts +90 -0
  69. package/dist/launch/salt.d.ts.map +1 -0
  70. package/dist/launch/salt.js +153 -0
  71. package/dist/launch/salt.js.map +1 -0
  72. package/dist/launch/stockCall.d.ts +122 -0
  73. package/dist/launch/stockCall.d.ts.map +1 -0
  74. package/dist/launch/stockCall.js +144 -0
  75. package/dist/launch/stockCall.js.map +1 -0
  76. package/dist/launch/tickMath.d.ts +39 -0
  77. package/dist/launch/tickMath.d.ts.map +1 -0
  78. package/dist/launch/tickMath.js +109 -0
  79. package/dist/launch/tickMath.js.map +1 -0
  80. package/dist/launch/variants.d.ts +64 -0
  81. package/dist/launch/variants.d.ts.map +1 -0
  82. package/dist/launch/variants.js +131 -0
  83. package/dist/launch/variants.js.map +1 -0
  84. package/dist/launch/wethCall.d.ts +133 -0
  85. package/dist/launch/wethCall.d.ts.map +1 -0
  86. package/dist/launch/wethCall.js +173 -0
  87. package/dist/launch/wethCall.js.map +1 -0
  88. package/dist/launch/wethPlan.d.ts +71 -0
  89. package/dist/launch/wethPlan.d.ts.map +1 -0
  90. package/dist/launch/wethPlan.js +146 -0
  91. package/dist/launch/wethPlan.js.map +1 -0
  92. package/dist/perps/abi.generated.d.ts +794 -0
  93. package/dist/perps/abi.generated.d.ts.map +1 -0
  94. package/dist/perps/abi.generated.js +738 -0
  95. package/dist/perps/abi.generated.js.map +1 -0
  96. package/dist/perps/index.d.ts +160 -0
  97. package/dist/perps/index.d.ts.map +1 -0
  98. package/dist/perps/index.js +147 -0
  99. package/dist/perps/index.js.map +1 -0
  100. package/package.json +105 -0
  101. package/src/abis.generated.ts +2738 -0
  102. package/src/abis.ts +35 -0
  103. package/src/artDex.ts +254 -0
  104. package/src/client.ts +10 -0
  105. package/src/clog.ts +171 -0
  106. package/src/clogInflow.ts +217 -0
  107. package/src/deployments.generated.ts +98 -0
  108. package/src/deployments.ts +60 -0
  109. package/src/feeSplit.ts +76 -0
  110. package/src/feeTerms.ts +137 -0
  111. package/src/index.ts +233 -0
  112. package/src/launch/abi.generated.ts +2481 -0
  113. package/src/launch/addresses.ts +58 -0
  114. package/src/launch/feeTerms.ts +93 -0
  115. package/src/launch/index.ts +93 -0
  116. package/src/launch/revert.ts +188 -0
  117. package/src/launch/salt.ts +216 -0
  118. package/src/launch/stockCall.ts +223 -0
  119. package/src/launch/tickMath.ts +139 -0
  120. package/src/launch/variants.ts +183 -0
  121. package/src/launch/wethCall.ts +240 -0
  122. package/src/launch/wethPlan.ts +257 -0
  123. package/src/perps/abi.generated.ts +741 -0
  124. package/src/perps/index.ts +263 -0
package/src/abis.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The ABI fragments the SDK's public surface needs — and nothing else.
3
+ *
4
+ * DELIBERATELY PARTIAL. These are narrowed to the functions this package
5
+ * exposes, not dumps of the full artifacts. A published ABI is API: every
6
+ * fragment shipped here is one this package promises to keep working, so the
7
+ * ones an integrator has no business calling (owner setters, one-shot bootstrap
8
+ * calls, indexer hooks) are absent rather than available-but-undocumented.
9
+ *
10
+ * NARROWED, NOT HAND-TYPED. Which names ship is decided in
11
+ * `script/generate-abis.mjs`; every fragment's shape is copied from the
12
+ * compiler's own output (`contracts/out`) into `abis.generated.ts`.
13
+ *
14
+ * `PeddlesCreatorFeeHook` is intentionally absent: it is not part of the
15
+ * fixed-terms fee model. Every launch pool's terms are read from
16
+ * `PeddlesFeeHook` (`feeHookAbi`).
17
+ */
18
+ export {
19
+ factoryAbi,
20
+ clogVaultAbi,
21
+ clogVaultFactoryAbi,
22
+ erc20Abi,
23
+ tokenV20Abi,
24
+ feeHookAbi,
25
+ stockLaunchpadAbi,
26
+ holderRewardsAbi,
27
+ nftFactoryAbi,
28
+ nftCollectionAbi,
29
+ nftFeeDistributorFactoryAbi,
30
+ nftFeeDistributorAbi,
31
+ nftBondingGraduationOrchestratorAbi,
32
+ nftStockGraduationOrchestratorAbi,
33
+ liquidityExecutorAbi,
34
+ routeSwapRouterAbi,
35
+ } from './abis.generated.js';
package/src/artDex.ts ADDED
@@ -0,0 +1,254 @@
1
+ import type { ReadClient } from './client.js';
2
+ import type { Address, Hex } from 'viem';
3
+ import { encodeAbiParameters, keccak256 } from 'viem';
4
+ import {
5
+ nftBondingGraduationOrchestratorAbi,
6
+ nftCollectionAbi,
7
+ nftFactoryAbi,
8
+ nftFeeDistributorAbi,
9
+ nftFeeDistributorFactoryAbi,
10
+ nftStockGraduationOrchestratorAbi,
11
+ } from './abis.js';
12
+
13
+ /**
14
+ * Art->DEX: a Peddles NFT collection launches ONE coin, paired to WETH or a tokenised stock, and may
15
+ * commit — permanently — a share of the creator's fee streams to its NFT holders.
16
+ *
17
+ * The commitment is the collection's write-once `holderRewards` binding to its canonical
18
+ * `PeddlesNftFeeDistributor` (made by `PeddlesNftFeeDistributorFactory`), plus the `holderShareBps`
19
+ * the creator passes at launch (0..10000). Nobody can change either afterwards.
20
+ *
21
+ * Addresses are parameters here rather than looked up from `DEPLOYMENTS`: the distributor factory ships
22
+ * with the Art->DEX deployment, and an address book that predates it has no entry to look up.
23
+ */
24
+
25
+ const ZERO = /^0x0{40}$/i;
26
+ const BPS = 10_000;
27
+
28
+ /** Bps bound shared by every holder-share argument. Throws rather than letting the chain revert. */
29
+ function assertBps(name: string, v: number): void {
30
+ if (!Number.isInteger(v) || v < 0 || v > BPS) throw new RangeError(`${name} must be an integer 0..10000, got ${v}`);
31
+ }
32
+
33
+ /* ------------------------------ relayed stock launch consent ------------------------------ */
34
+
35
+ export interface RelayParams {
36
+ readonly chainId: number;
37
+ /** The PeddlesNftStockGraduationOrchestrator the consent is for. */
38
+ readonly orchestrator: Address;
39
+ readonly collection: Address;
40
+ readonly name: string;
41
+ readonly symbol: string;
42
+ readonly quote: Address;
43
+ readonly holderShareBps: number;
44
+ }
45
+
46
+ /**
47
+ * `relayParamsHash` computed off chain — `keccak256(abi.encode(chainid, orchestrator, collection, name,
48
+ * symbol, quote, holderShareBps))`, exactly as the orchestrator hashes it. The collection owner signs
49
+ * `setStockGraduationOptIn(collection, hash)`; a relay whose parameters hash to anything else reverts.
50
+ * Chain id and orchestrator are part of it, so consent never carries across chains or deployments.
51
+ */
52
+ export function relayParamsHash(p: RelayParams): Hex {
53
+ assertBps('holderShareBps', p.holderShareBps);
54
+ return keccak256(
55
+ encodeAbiParameters(
56
+ [{ type: 'uint256' }, { type: 'address' }, { type: 'address' }, { type: 'string' }, { type: 'string' }, { type: 'address' }, { type: 'uint16' }],
57
+ [BigInt(p.chainId), p.orchestrator, p.collection, p.name, p.symbol, p.quote, p.holderShareBps],
58
+ ),
59
+ );
60
+ }
61
+
62
+ /** Request for `setStockGraduationOptIn(collection, relayParamsHash(p))` (collection owner only). Zero revokes. */
63
+ export function stockGraduationOptInRequest(p: RelayParams) {
64
+ return {
65
+ address: p.orchestrator,
66
+ abi: nftStockGraduationOrchestratorAbi,
67
+ functionName: 'setStockGraduationOptIn',
68
+ args: [p.collection, relayParamsHash(p)],
69
+ } as const;
70
+ }
71
+
72
+ /* ------------------------------ launch requests ------------------------------ */
73
+
74
+ export interface StockTermsLaunch {
75
+ readonly orchestrator: Address;
76
+ readonly collection: Address;
77
+ readonly name: string;
78
+ readonly symbol: string;
79
+ readonly quote: Address;
80
+ /** All-in pool tax, bps (100..1000). Permanent. */
81
+ readonly creatorTaxBps: number;
82
+ /** Creator's share of the tax above 1.00%, bps. Permanent. */
83
+ readonly excessToCreatorBps: number;
84
+ /** Share of the creator streams paid to NFT holders, bps. 0 unless the collection is committed. Permanent. */
85
+ readonly holderShareBps: number;
86
+ /** Quote base units for the in-launch dev buy (0 = none); pulled from the caller, so approve first. */
87
+ readonly devBuyQuoteIn: bigint;
88
+ readonly minTokensOut: bigint;
89
+ /** The launchpad's native launch fee, forwarded as msg.value. */
90
+ readonly launchFee: bigint;
91
+ }
92
+
93
+ /** Request for `graduateToStockWithTerms` (collection owner only). */
94
+ export function graduateToStockWithTermsRequest(l: StockTermsLaunch) {
95
+ assertBps('holderShareBps', l.holderShareBps);
96
+ assertBps('excessToCreatorBps', l.excessToCreatorBps);
97
+ return {
98
+ address: l.orchestrator,
99
+ abi: nftStockGraduationOrchestratorAbi,
100
+ functionName: 'graduateToStockWithTerms',
101
+ args: [
102
+ {
103
+ collection: l.collection,
104
+ name: l.name,
105
+ symbol: l.symbol,
106
+ quote: l.quote,
107
+ creatorTaxBps: l.creatorTaxBps,
108
+ excessToCreatorBps: l.excessToCreatorBps,
109
+ holderShareBps: l.holderShareBps,
110
+ devBuyQuoteIn: l.devBuyQuoteIn,
111
+ minTokensOut: l.minTokensOut,
112
+ },
113
+ ],
114
+ value: l.launchFee,
115
+ } as const;
116
+ }
117
+
118
+ /**
119
+ * Request for `graduateAndBuy(input, holderShareBps, devBuyValue, minTokensOut)` — the ONLY WETH-path
120
+ * entry since 94d6564 (`graduate` was removed). A plain launch is `holderShareBps = 0, devBuyValue = 0,
121
+ * minTokensOut = 0`. `value` must cover the orchestrator's `launchFee()` (read it live —
122
+ * `readOrchestratorLaunchFee` in `@peddles/sdk/launch`) `+ input.factoryValue + devBuyValue`; a relay of
123
+ * a committed graduation carries exactly `launchFee()` (`RELAY_FEE_ONLY`).
124
+ *
125
+ * `input` is the orchestrator's `GraduateInput` tuple, built by the launch kit exactly as for any V20
126
+ * launch; this helper only positions the Art->DEX arguments around it.
127
+ */
128
+ export function graduateAndBuyRequest(p: {
129
+ readonly orchestrator: Address;
130
+ readonly input: unknown;
131
+ readonly holderShareBps: number;
132
+ readonly devBuyValue: bigint;
133
+ readonly minTokensOut: bigint;
134
+ readonly value: bigint;
135
+ }) {
136
+ assertBps('holderShareBps', p.holderShareBps);
137
+ if (p.devBuyValue > p.value) throw new RangeError('devBuyValue exceeds value');
138
+ return {
139
+ address: p.orchestrator,
140
+ abi: nftBondingGraduationOrchestratorAbi,
141
+ functionName: 'graduateAndBuy',
142
+ args: [p.input, p.holderShareBps, p.devBuyValue, p.minTokensOut],
143
+ value: p.value,
144
+ } as const;
145
+ }
146
+
147
+ /* ------------------------------ reads ------------------------------ */
148
+
149
+ /** The canonical distributor address for `collection` (CREATE2; exists or not). */
150
+ export async function predictDistributor(client: ReadClient, distributorFactory: Address, collection: Address): Promise<Address> {
151
+ return (await client.readContract({ address: distributorFactory, abi: nftFeeDistributorFactoryAbi, functionName: 'predict', args: [collection] })) as Address;
152
+ }
153
+
154
+ export interface HolderCommitment {
155
+ /** True when the collection is bound to its canonical Peddles distributor. */
156
+ readonly committed: boolean;
157
+ /** The bound `holderRewards` (zero = none). */
158
+ readonly holderRewards: Address;
159
+ /** True when the Peddles NFT factory made this collection. */
160
+ readonly peddlesCollection: boolean;
161
+ }
162
+
163
+ /**
164
+ * Whether a collection's creator streams are committed to its holders — the same three reads the
165
+ * orchestrators make: `holderRewards()`, `factory.isDistributor(d)`, `d.collection() == collection`.
166
+ * Also reports factory provenance, which the orchestrators do NOT check.
167
+ */
168
+ export async function getHolderCommitment(
169
+ client: ReadClient,
170
+ a: { nftFactory: Address; distributorFactory: Address; collection: Address },
171
+ ): Promise<HolderCommitment> {
172
+ const [holderRewards, peddlesCollection] = await Promise.all([
173
+ client.readContract({ address: a.collection, abi: nftCollectionAbi, functionName: 'holderRewards' }) as Promise<Address>,
174
+ client.readContract({ address: a.nftFactory, abi: nftFactoryAbi, functionName: 'isPeddlesCollection', args: [a.collection] }) as Promise<boolean>,
175
+ ]);
176
+ if (ZERO.test(holderRewards)) return { committed: false, holderRewards, peddlesCollection };
177
+ const isDistributor = (await client.readContract({
178
+ address: a.distributorFactory,
179
+ abi: nftFeeDistributorFactoryAbi,
180
+ functionName: 'isDistributor',
181
+ args: [holderRewards],
182
+ })) as boolean;
183
+ if (!isDistributor) return { committed: false, holderRewards, peddlesCollection };
184
+ const bound = (await client.readContract({ address: holderRewards, abi: nftFeeDistributorAbi, functionName: 'collection' })) as Address;
185
+ return { committed: bound.toLowerCase() === a.collection.toLowerCase(), holderRewards, peddlesCollection };
186
+ }
187
+
188
+ export interface DistributorState {
189
+ readonly distributor: Address;
190
+ readonly collection: Address;
191
+ /** Zero until launch. */
192
+ readonly quote: Address;
193
+ readonly token: Address;
194
+ readonly holderShareBps: number;
195
+ readonly assetsInitialized: boolean;
196
+ readonly creatorShareOwed: { readonly quote: bigint; readonly token: bigint };
197
+ readonly owedToHolders: { readonly quote: bigint; readonly token: bigint };
198
+ /** False while minted pieces are unmirrored — holder distribution waits until they are synced. */
199
+ readonly mirrorComplete: boolean;
200
+ }
201
+
202
+ export async function getDistributorState(client: ReadClient, distributor: Address): Promise<DistributorState> {
203
+ const base = { address: distributor, abi: nftFeeDistributorAbi } as const;
204
+ const [collection, quote, token, holderShareBps, assetsInitialized, creatorShareOwed, owedToHolders, mirrorComplete] = await client.multicall({
205
+ allowFailure: false,
206
+ contracts: [
207
+ { ...base, functionName: 'collection' },
208
+ { ...base, functionName: 'quote' },
209
+ { ...base, functionName: 'token' },
210
+ { ...base, functionName: 'holderShareBps' },
211
+ { ...base, functionName: 'assetsInitialized' },
212
+ { ...base, functionName: 'creatorShareOwed' },
213
+ { ...base, functionName: 'owedToHolders' },
214
+ { ...base, functionName: 'mirrorComplete' },
215
+ ],
216
+ });
217
+ const [cq, ct] = creatorShareOwed as readonly [bigint, bigint];
218
+ const [hq, ht] = owedToHolders as readonly [bigint, bigint];
219
+ return {
220
+ distributor,
221
+ collection: collection as Address,
222
+ quote: quote as Address,
223
+ token: token as Address,
224
+ holderShareBps: Number(holderShareBps),
225
+ assetsInitialized: assetsInitialized as boolean,
226
+ creatorShareOwed: { quote: cq, token: ct },
227
+ owedToHolders: { quote: hq, token: ht },
228
+ mirrorComplete: mirrorComplete as boolean,
229
+ };
230
+ }
231
+
232
+ export interface HolderClaimable {
233
+ readonly quote: bigint;
234
+ readonly token: bigint;
235
+ /** Shares the distributor has mirrored for the holder. */
236
+ readonly shares: bigint;
237
+ /** Pieces the holder holds on the collection. */
238
+ readonly pieces: bigint;
239
+ /** `shares != pieces`: claims under-accrue until `syncAccount(holder)` runs. */
240
+ readonly needsSync: boolean;
241
+ }
242
+
243
+ export async function holderClaimable(client: ReadClient, distributor: Address, collection: Address, holder: Address): Promise<HolderClaimable> {
244
+ const [pending, shares, pieces] = await client.multicall({
245
+ allowFailure: false,
246
+ contracts: [
247
+ { address: distributor, abi: nftFeeDistributorAbi, functionName: 'pending', args: [holder] },
248
+ { address: distributor, abi: nftFeeDistributorAbi, functionName: 'sharesOf', args: [holder] },
249
+ { address: collection, abi: nftCollectionAbi, functionName: 'balanceOf', args: [holder] },
250
+ ],
251
+ });
252
+ const [quote, token] = pending as readonly [bigint, bigint];
253
+ return { quote, token, shares: shares as bigint, pieces: pieces as bigint, needsSync: (shares as bigint) !== (pieces as bigint) };
254
+ }
package/src/client.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { PublicClient } from 'viem';
2
+
3
+ /**
4
+ * The client every SDK read takes: any viem client that can `readContract`, `multicall` and
5
+ * `getBalance`. Narrower than `PublicClient` on purpose — a `PublicClient` built for a chain with its
6
+ * own formatters (Base and every OP-stack chain add `deposit` transactions) is NOT assignable to the
7
+ * generic `PublicClient`, so typing parameters that way made `createPublicClient({ chain: base })`
8
+ * a compile error for every Base integrator. Only the three methods the SDK calls are required.
9
+ */
10
+ export type ReadClient = Pick<PublicClient, 'readContract' | 'multicall' | 'getBalance'>;
package/src/clog.ts ADDED
@@ -0,0 +1,171 @@
1
+ import type { ReadClient } from './client.js';
2
+ import type { Address } from 'viem';
3
+ import { clogVaultAbi, clogVaultFactoryAbi, erc20Abi, factoryAbi } from './abis.js';
4
+ import { addressOf } from './deployments.js';
5
+
6
+ /**
7
+ * The Clog launch type, read from the chain.
8
+ *
9
+ * A clog withholds a share of a launch's supply in a `PeddlesClogVault` and
10
+ * sells it into that launch's own pool a capped slice at a time. The proceeds
11
+ * (native) STAY IN THE VAULT until the creator calls `withdrawProceeds`, and a
12
+ * creator-set release floor bounds the price a release may sell at. It funds marketing without the creator putting up
13
+ * capital — and it only ever extracts from volume that actually arrived, since
14
+ * a sale into an empty book returns nothing and reverts.
15
+ */
16
+
17
+ /** A launch type's declared economics. Write-once at variant registration. */
18
+ export interface VariantAllocation {
19
+ readonly liquidityBps: number;
20
+ readonly airdropBps: number;
21
+ readonly vestingBps: number;
22
+ readonly burnBps: number;
23
+ readonly clogBps: number;
24
+ /** Per-release cap, in bps OF TOTAL SUPPLY — not of the allocation. */
25
+ readonly clogSliceBps: number;
26
+ readonly clogMinIntervalSeconds: bigint;
27
+ }
28
+
29
+ /** One clog vault's live state. */
30
+ export interface ClogState {
31
+ readonly vault: Address;
32
+ readonly token: Address;
33
+ readonly creator: Address;
34
+ readonly router: Address;
35
+ /** Raw units still held, i.e. not yet sold. */
36
+ readonly held: bigint;
37
+ /** Raw units sold to date, across every release. */
38
+ readonly totalSold: bigint;
39
+ /** Max raw units one release may sell. */
40
+ readonly sliceCap: bigint;
41
+ readonly minIntervalSeconds: bigint;
42
+ /** What `release` would sell right now. */
43
+ readonly nextSlice: bigint;
44
+ /** Unix seconds at which `release` stops reverting `TooSoon`. */
45
+ readonly readyAt: bigint;
46
+ /** Native proceeds held by the vault, owed to the creator (`withdrawProceeds`). */
47
+ readonly proceedsOwed: bigint;
48
+ /** The creator's minimum release price, 1e18-scaled quote per token; 0 = none. */
49
+ readonly releaseFloorX18: bigint;
50
+ }
51
+
52
+ /**
53
+ * Read a launch type's allocation.
54
+ *
55
+ * `registerVariant` is write-once, so this is the deal a creator and every
56
+ * buyer of their token accepted — it cannot be changed under them afterwards.
57
+ * Read it rather than assuming: the shares differ per launch type, and a clog
58
+ * type is not 100% liquidity.
59
+ */
60
+ export async function getVariantAllocation(
61
+ client: ReadClient,
62
+ chainId: number,
63
+ variant: number,
64
+ ): Promise<VariantAllocation> {
65
+ const a = (await client.readContract({
66
+ address: addressOf(chainId, 'PeddlesFactoryV20'),
67
+ abi: factoryAbi,
68
+ functionName: 'variantAllocation',
69
+ args: [variant],
70
+ })) as {
71
+ liquidityBps: number;
72
+ airdropBps: number;
73
+ vestingBps: number;
74
+ burnBps: number;
75
+ clogBps: number;
76
+ clogSliceBps: number;
77
+ clogMinInterval: bigint;
78
+ };
79
+
80
+ return {
81
+ liquidityBps: a.liquidityBps,
82
+ airdropBps: a.airdropBps,
83
+ vestingBps: a.vestingBps,
84
+ burnBps: a.burnBps,
85
+ clogBps: a.clogBps,
86
+ clogSliceBps: a.clogSliceBps,
87
+ clogMinIntervalSeconds: a.clogMinInterval,
88
+ };
89
+ }
90
+
91
+ /** The clog vault a launch's orchestrator created for `token`, or null. */
92
+ export async function getClogVault(
93
+ client: ReadClient,
94
+ chainId: number,
95
+ token: Address,
96
+ ): Promise<Address | null> {
97
+ const vault = (await client.readContract({
98
+ address: addressOf(chainId, 'PeddlesClogVaultFactory'),
99
+ abi: clogVaultFactoryAbi,
100
+ functionName: 'vaultOf',
101
+ args: [addressOf(chainId, 'PeddlesLaunchOrchestratorV20'), token],
102
+ })) as Address;
103
+ return /^0x0{40}$/i.test(vault) ? null : vault;
104
+ }
105
+
106
+ /** Everything about one clog vault, in a single multicall. */
107
+ export async function getClogState(client: ReadClient, vault: Address): Promise<ClogState> {
108
+ const base = { address: vault, abi: clogVaultAbi } as const;
109
+ const [token, creator, router, sliceCap, minInterval, totalSold, preview, releaseFloorX18] = await client.multicall({
110
+ allowFailure: false,
111
+ contracts: [
112
+ { ...base, functionName: 'token' },
113
+ { ...base, functionName: 'creator' },
114
+ { ...base, functionName: 'router' },
115
+ { ...base, functionName: 'sliceCap' },
116
+ { ...base, functionName: 'minInterval' },
117
+ { ...base, functionName: 'totalSold' },
118
+ { ...base, functionName: 'previewRelease' },
119
+ { ...base, functionName: 'releaseFloorX18' },
120
+ ],
121
+ });
122
+
123
+ const [held, proceedsOwed] = await Promise.all([
124
+ client.readContract({ address: token as Address, abi: erc20Abi, functionName: 'balanceOf', args: [vault] }) as Promise<bigint>,
125
+ client.getBalance({ address: vault }),
126
+ ]);
127
+
128
+ const [nextSlice, readyAt] = preview as readonly [bigint, bigint];
129
+
130
+ return {
131
+ vault,
132
+ token: token as Address,
133
+ creator: creator as Address,
134
+ router: router as Address,
135
+ held,
136
+ totalSold: totalSold as bigint,
137
+ sliceCap: sliceCap as bigint,
138
+ minIntervalSeconds: minInterval as bigint,
139
+ nextSlice,
140
+ readyAt,
141
+ proceedsOwed,
142
+ releaseFloorX18: releaseFloorX18 as bigint,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * What share of INFLOWS a clog routes to the creator, given its share of supply.
148
+ *
149
+ * THESE ARE NOT THE SAME NUMBER, and the second is the one that matters to a
150
+ * buyer. The allocation is a share of supply, but it is sold into the pool the
151
+ * buyer is buying from, so it captures a much larger share of the money coming
152
+ * in. Surface it wherever the allocation is shown: a launch page that says "5%"
153
+ * and stops has told the buyer the smaller of two true numbers.
154
+ *
155
+ * The model (exact, bigint, documented in `clogInflow.ts`) lives in its own
156
+ * import-free module and is re-exported here.
157
+ */
158
+ export {
159
+ CLOG_MODEL_FLOW_MULTIPLE,
160
+ CLOG_TABLE_SLICE_BPS,
161
+ clogShareOfInflows,
162
+ clogShareOfInflowsFromBps,
163
+ clogInflowShareBps,
164
+ inflowShareBps,
165
+ } from './clogInflow.js';
166
+ export type { ShareOfInflows, ClogModelInput } from './clogInflow.js';
167
+
168
+ /** True when `release` would not revert `TooSoon`. */
169
+ export function isReleaseReady(state: ClogState, nowSeconds: bigint): boolean {
170
+ return nowSeconds >= state.readyAt;
171
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * What share of the money buyers put in a clog routes to the creator.
3
+ *
4
+ * A clog allocation is a share of SUPPLY, but it is sold into the pool buyers are
5
+ * buying from, so it captures a far larger share of the money coming in. That
6
+ * second number is the one a buyer is accepting, and it must be stated wherever
7
+ * the allocation is. This module derives it; nothing here is a lookup table.
8
+ *
9
+ * Pure: no imports, bigint only, no floating point at any step. Every step is
10
+ * exact rational arithmetic, so the result is a fraction `num / den`; the only
11
+ * rounding is in `clogInflowShareBps`, once, at the end.
12
+ *
13
+ * SINGLE SOURCE OF TRUTH. `apps/web/src/features/launch/clogInflow.ts` carries a
14
+ * copy (the web app does not depend on this package). `__tests__/clogInflow.test.ts`
15
+ * imports that copy and holds the two to identical fractions over a grid, so a
16
+ * change to one without the other fails the SDK's tests.
17
+ *
18
+ * ── THE MODEL ───────────────────────────────────────────────────────────────
19
+ * Pure buy flow — nobody but the clog ever sells. That is the most favourable
20
+ * case for the design: any real selling makes the clog sell into a weaker pool.
21
+ *
22
+ * The pool. A Peddles launch opens a single-sided position from the launch
23
+ * price upward holding the liquidity share `x0 = supply - clog`. For such a
24
+ * position, token reserve `x` and quote `y` satisfy
25
+ *
26
+ * x · (y + x0·p0) = x0² · p0 (a constant product, k)
27
+ *
28
+ * i.e. constant product against a virtual quote reserve `x0·p0`. Write
29
+ * `q = y + x0·p0`, so `q0 = x0·p0` and `k = x0·q0`.
30
+ *
31
+ * One round. Buyers spend `B` (a buy never changes k): q ← q + B
32
+ * then the clog sells `s = min(slice, remaining)` tokens:
33
+ *
34
+ * q ← k · q / (k + s · q) (x ← x + s, q = k / x)
35
+ *
36
+ * Rounds repeat until the clog is empty. After `n` rounds buyers have put in
37
+ * `n·B`, and the pool holds `q_n − q0` of it; everything else was paid out to
38
+ * the creator. So, exactly:
39
+ *
40
+ * share of inflows = 1 − (q_n − q0) / (n · B)
41
+ *
42
+ * Buy flow per round — THE ONE ASSUMPTION. `B = flowMultiple × slice × p0`:
43
+ * buyers spend between releases `flowMultiple` times what a slice was worth at
44
+ * the launch price. Default `CLOG_MODEL_FLOW_MULTIPLE` (100).
45
+ *
46
+ * p0 cancels. Measuring quote in units of `p0` makes every term an integer
47
+ * count of tokens (`q0 = x0`, `k = x0²`, `B = flowMultiple × slice`), so the
48
+ * result depends only on (supply, clog, slice) and the flow multiple — never
49
+ * on the launch price, which is why it can be stated before the pool exists.
50
+ *
51
+ * The floor. A single-sided position holds no quote below its launch price, so
52
+ * a release can never take the price under `p0`. If a round's sale would, the
53
+ * model does not apply and the functions return null rather than a number from
54
+ * a pool that cannot exist. At flowMultiple = 100 this cannot happen anywhere
55
+ * in the range the factory accepts (clog ≤ `PeddlesFactoryV20.CLOG_BPS_MAX`,
56
+ * 25%): a round starts at `q ≥ q0`, its buys take at least `x0·B / (x0 + B)`
57
+ * tokens out of the pool, and a sale `s ≤ clog` fits back under the floor
58
+ * whenever `99·x0 ≥ 100·s` — true for every `x0 ≥ 75%` and `s ≤ 25%`.
59
+ *
60
+ * ── REFERENCE ───────────────────────────────────────────────────────────────
61
+ * With slices of 1% of supply (`CLOG_TABLE_SLICE_BPS`) and flowMultiple = 100
62
+ * this reproduces the policy table in CLAUDE.md at its 0.1% resolution
63
+ * (5% → 16.5%, 10% → 37.1%, 15% → 54.6%, 25% → 75.0%). A launch type's real
64
+ * slice is smaller — the live Sepolia Clog type sells 5% in 0.25% slices, which
65
+ * is ≈14.2% — so pass the slice the chain reports, never the table's.
66
+ */
67
+
68
+ /**
69
+ * Buyer spend between two clog releases, as a multiple of what one slice was
70
+ * worth at the launch price. The one assumption in the model; see the header.
71
+ */
72
+ export const CLOG_MODEL_FLOW_MULTIPLE = 100n;
73
+
74
+ /** The slice (1% of supply) at which the model reproduces the CLAUDE.md policy table. */
75
+ export const CLOG_TABLE_SLICE_BPS = 100n;
76
+
77
+ /** More rounds than any registrable launch type can need (25% in 1-unit slices of 10,000). */
78
+ const MAX_ROUNDS = 100_000n;
79
+
80
+ const BPS = 10_000n;
81
+
82
+ export interface ShareOfInflows {
83
+ /**
84
+ * Share of all buyer money paid out to the creator, as `num / den` (0 ≤ share < 1).
85
+ * Not reduced to lowest terms — compare two shares by cross-multiplying.
86
+ */
87
+ readonly num: bigint;
88
+ readonly den: bigint;
89
+ /** How many releases it takes to sell the whole clog. */
90
+ readonly releases: bigint;
91
+ }
92
+
93
+ export interface ClogModelInput {
94
+ /** Total supply, in any unit — basis points (10000) or raw token units. */
95
+ readonly supply: bigint;
96
+ /** The clog allocation, in the same unit. */
97
+ readonly clog: bigint;
98
+ /** The most one release sells, in the same unit. */
99
+ readonly slice: bigint;
100
+ /** Defaults to `CLOG_MODEL_FLOW_MULTIPLE`. */
101
+ readonly flowMultiple?: bigint;
102
+ }
103
+
104
+ /**
105
+ * The share of buyer money a clog routes to the creator, as an exact fraction,
106
+ * or null when the model does not apply (a malformed input, or a release that
107
+ * would breach the floor). A zero clog captures nothing:
108
+ * `{ num: 0n, den: 1n, releases: 0n }`.
109
+ */
110
+ export function clogShareOfInflows(input: ClogModelInput): ShareOfInflows | null {
111
+ const { supply, clog, slice } = input;
112
+ const flowMultiple = input.flowMultiple ?? CLOG_MODEL_FLOW_MULTIPLE;
113
+
114
+ if (supply <= 0n || clog < 0n || clog >= supply) return null;
115
+ if (clog === 0n) return { num: 0n, den: 1n, releases: 0n };
116
+ if (slice <= 0n || flowMultiple <= 0n) return null;
117
+
118
+ const x0 = supply - clog;
119
+ const k = x0 * x0; // q0 = x0 in units of p0
120
+ const buy = flowMultiple * slice;
121
+
122
+ // q = qn / qd, kept as an unreduced exact fraction.
123
+ let qn = x0;
124
+ let qd = 1n;
125
+ let remaining = clog;
126
+ let rounds = 0n;
127
+
128
+ while (remaining > 0n) {
129
+ if (rounds >= MAX_ROUNDS) return null;
130
+ rounds += 1n;
131
+
132
+ // Buyers spend: q ← q + B.
133
+ qn += buy * qd;
134
+
135
+ // The clog sells s tokens: q ← k·q / (k + s·q).
136
+ const sold = remaining < slice ? remaining : slice;
137
+ const nextN = k * qn;
138
+ const nextD = k * qd + sold * qn;
139
+
140
+ // Floor: q must not fall below q0 (= x0), i.e. nextN / nextD ≥ x0.
141
+ if (nextN < x0 * nextD) return null;
142
+
143
+ // Left unreduced. The fraction almost never reduces, and a gcd on numbers
144
+ // that grow a few dozen bits per round is what makes a 2,500-round type slow.
145
+ qn = nextN;
146
+ qd = nextD;
147
+ remaining -= sold;
148
+ }
149
+
150
+ // share = 1 − (q − q0) / (n·B) = (n·B·qd − (qn − x0·qd)) / (n·B·qd)
151
+ const inflow = rounds * buy;
152
+ const den = inflow * qd;
153
+ const num = den - (qn - x0 * qd);
154
+ return { num, den, releases: rounds };
155
+ }
156
+
157
+ /** The same model, stated in basis points of supply — the unit a launch type declares. */
158
+ export function clogShareOfInflowsFromBps(
159
+ clogBps: bigint,
160
+ sliceBps: bigint,
161
+ flowMultiple: bigint = CLOG_MODEL_FLOW_MULTIPLE,
162
+ ): ShareOfInflows | null {
163
+ return clogShareOfInflows({ supply: BPS, clog: clogBps, slice: sliceBps, flowMultiple });
164
+ }
165
+
166
+ /** `num / den` scaled by `unit`, rounded half up. `num ≥ 0`, `den > 0`. */
167
+ function roundHalfUp(num: bigint, den: bigint, unit: bigint): bigint {
168
+ return (num * unit * 2n + den) / (den * 2n);
169
+ }
170
+
171
+ /**
172
+ * Share of inflows, in basis points, for a clog of `clogBps` of supply sold in
173
+ * slices of `sliceBps` of supply.
174
+ *
175
+ * Pass the launch type's own `clogSliceBps` (from `getVariantAllocation`), not
176
+ * the policy table's 1%: the slice moves the answer (5% in 1% slices is 1649 bps;
177
+ * in the live 0.25% slices it is 1417 bps).
178
+ *
179
+ * @param clogBps Clog allocation, bps of total supply.
180
+ * @param sliceBps Per-release cap, bps of total supply.
181
+ * @param flowMultiple Buyer spend between releases, in slices at the launch
182
+ * price. The model's one assumption; defaults to
183
+ * `CLOG_MODEL_FLOW_MULTIPLE` (100).
184
+ * @returns Nearest basis point, rounded half up — the only rounding in the model
185
+ * (use `clogShareOfInflows` for the exact fraction) — or null when the
186
+ * model does not apply.
187
+ */
188
+ export function clogInflowShareBps(
189
+ clogBps: bigint,
190
+ sliceBps: bigint,
191
+ flowMultiple: bigint = CLOG_MODEL_FLOW_MULTIPLE,
192
+ ): bigint | null {
193
+ const share = clogShareOfInflowsFromBps(clogBps, sliceBps, flowMultiple);
194
+ return share === null ? null : roundHalfUp(share.num, share.den, BPS);
195
+ }
196
+
197
+ /**
198
+ * What share of INFLOWS a clog routes to the creator, given its share of supply —
199
+ * AT THE POLICY TABLE'S 1% SLICE AND 0.1% RESOLUTION.
200
+ *
201
+ * @deprecated Kept for existing callers. It answers for the CLAUDE.md table's
202
+ * assumed slice, not for any real launch type's, so it overstates the live Sepolia
203
+ * Clog type (5%: 1650 here vs 1417 at its real 0.25% slice). Use
204
+ * `clogInflowShareBps(clogBps, allocation.clogSliceBps)`.
205
+ *
206
+ * Output is unchanged for the four table rows (500 → 1650, 1000 → 3710,
207
+ * 1500 → 5460, 2500 → 7500). It is now derived from the model rather than looked
208
+ * up, so any integer clog in [0, 10000) returns a value (a multiple of 10) where
209
+ * it used to return null; non-integer or out-of-range input still returns null.
210
+ */
211
+ export function inflowShareBps(clogBps: number): number | null {
212
+ if (!Number.isSafeInteger(clogBps)) return null;
213
+ const share = clogShareOfInflowsFromBps(BigInt(clogBps), CLOG_TABLE_SLICE_BPS);
214
+ if (share === null) return null;
215
+ // Table resolution is 0.1% = 10 bps: round once at that resolution, then restate in bps.
216
+ return Number(roundHalfUp(share.num, share.den, BPS / 10n) * 10n);
217
+ }