@whetstone-research/doppler-sdk 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,6 +88,19 @@ const params = new StaticAuctionBuilder()
88
88
  // Optional: specify multiple recipients and amounts
89
89
  // recipients: ['0xTeam...', '0xAdvisor...'],
90
90
  // amounts: [parseEther('50000000'), parseEther('50000000')]
91
+ // Optional: define per-beneficiary vesting allocations on the DERC20 V2 path
92
+ // allocations: [
93
+ // {
94
+ // recipient: '0xTeam...',
95
+ // amount: parseEther('50000000'),
96
+ // schedule: { duration: BigInt(180 * 24 * 60 * 60), cliffDuration: 30 * 24 * 60 * 60 },
97
+ // },
98
+ // {
99
+ // recipient: '0xAdvisor...',
100
+ // amount: parseEther('50000000'),
101
+ // schedule: { duration: BigInt(365 * 24 * 60 * 60), cliffDuration: 90 * 24 * 60 * 60 },
102
+ // },
103
+ // ]
91
104
  })
92
105
  .withMigration({ type: 'uniswapV2' })
93
106
  .withUserAddress('0x...')
@@ -98,7 +111,9 @@ console.log('Pool address:', result.poolAddress);
98
111
  console.log('Token address:', result.tokenAddress);
99
112
  ```
100
113
 
101
- If you set `cliffDuration > 0`, the SDK now automatically uses the DERC20 V2 factory and exposes schedule-aware token reads via `sdk.getDerc20V2(tokenAddress)`.
114
+ If you set `cliffDuration > 0` or provide `allocations`, the SDK automatically uses the DERC20 V2 factory and exposes schedule-aware token reads via `sdk.getDerc20V2(tokenAddress)`. When `allocations` is provided, the SDK dedupes identical schedules internally and maps each recipient to the correct on-chain schedule.
115
+
116
+ For a runnable example, see [examples/multicurve-per-beneficiary-vesting.ts](./examples/multicurve-per-beneficiary-vesting.ts).
102
117
 
103
118
  > **Tick spacing reminder:** When you provide ticks manually via `poolByTicks`, make sure both `startTick` and `endTick` are exact multiples of the fee tier's tick spacing (100→1, 500→10, 3000→60, 10000→200). The SDK now validates this locally and will fail fast if the ticks are misaligned.
104
119
 
@@ -5282,6 +5282,7 @@ var erc20BalanceOfAbi = [
5282
5282
  ];
5283
5283
  var TOKEN_FACTORY_80_ADDRESS2 = "0xf0b5141dd9096254b2ca624dff26024f46087229";
5284
5284
  var DERC20_V2_MIN_VESTING_DURATION = 24 * 60 * 60;
5285
+ var MAX_UINT64 = (1n << 64n) - 1n;
5285
5286
  var DopplerFactory = class {
5286
5287
  publicClient;
5287
5288
  walletClient;
@@ -5293,13 +5294,29 @@ var DopplerFactory = class {
5293
5294
  this.walletClient = walletClient;
5294
5295
  this.chainId = chainId;
5295
5296
  }
5297
+ hasCustomV2Schedules(vesting) {
5298
+ return (vesting?.allocations?.length ?? 0) > 0;
5299
+ }
5296
5300
  usesDerc20V2Vesting(vesting) {
5297
- return (vesting?.cliffDuration ?? 0) > 0;
5301
+ if (!vesting) {
5302
+ return false;
5303
+ }
5304
+ return this.hasCustomV2Schedules(vesting) || (vesting.cliffDuration ?? 0) > 0;
5298
5305
  }
5299
5306
  resolveVestingAllocations(args) {
5300
5307
  if (!args.vesting) {
5301
5308
  return { recipients: [], amounts: [] };
5302
5309
  }
5310
+ if (args.vesting.allocations) {
5311
+ return {
5312
+ recipients: args.vesting.allocations.map(
5313
+ (allocation) => allocation.recipient
5314
+ ),
5315
+ amounts: args.vesting.allocations.map(
5316
+ (allocation) => allocation.amount
5317
+ )
5318
+ };
5319
+ }
5303
5320
  if (args.vesting.recipients && args.vesting.amounts) {
5304
5321
  return {
5305
5322
  recipients: args.vesting.recipients,
@@ -5311,6 +5328,56 @@ var DopplerFactory = class {
5311
5328
  amounts: [args.sale.initialSupply - args.sale.numTokensToSell]
5312
5329
  };
5313
5330
  }
5331
+ validateUint64LikeNumber(value, fieldPath, options = {}) {
5332
+ const { allowZero = true } = options;
5333
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
5334
+ throw new Error(`${fieldPath} must be a finite integer`);
5335
+ }
5336
+ if (!Number.isSafeInteger(value)) {
5337
+ throw new Error(`${fieldPath} must be a safe integer`);
5338
+ }
5339
+ if (value < 0) {
5340
+ throw new Error(`${fieldPath} cannot be negative`);
5341
+ }
5342
+ if (!allowZero && value === 0) {
5343
+ throw new Error(`${fieldPath} must be greater than zero`);
5344
+ }
5345
+ if (BigInt(value) > MAX_UINT64) {
5346
+ throw new Error(`${fieldPath} must fit in uint64`);
5347
+ }
5348
+ }
5349
+ resolveV2VestingSchedules(args) {
5350
+ if (!args.vesting) {
5351
+ return { schedules: [], scheduleIds: [] };
5352
+ }
5353
+ if (!this.hasCustomV2Schedules(args.vesting)) {
5354
+ return {
5355
+ schedules: [
5356
+ {
5357
+ cliff: BigInt(args.vesting.cliffDuration ?? 0),
5358
+ duration: BigInt(args.vesting.duration ?? 0)
5359
+ }
5360
+ ],
5361
+ scheduleIds: Array.from({ length: args.recipientCount }, () => 0n)
5362
+ };
5363
+ }
5364
+ const schedules = [];
5365
+ const scheduleIds = [];
5366
+ const scheduleIdsByKey = /* @__PURE__ */ new Map();
5367
+ for (const allocation of args.vesting.allocations ?? []) {
5368
+ const cliff = BigInt(allocation.schedule.cliffDuration ?? 0);
5369
+ const duration = BigInt(allocation.schedule.duration ?? 0);
5370
+ const key = `${cliff}:${duration}`;
5371
+ let scheduleId = scheduleIdsByKey.get(key);
5372
+ if (scheduleId === void 0) {
5373
+ scheduleId = BigInt(schedules.length);
5374
+ schedules.push({ cliff, duration });
5375
+ scheduleIdsByKey.set(key, scheduleId);
5376
+ }
5377
+ scheduleIds.push(scheduleId);
5378
+ }
5379
+ return { schedules, scheduleIds };
5380
+ }
5314
5381
  resolveStandardTokenFactoryMode(args) {
5315
5382
  if (this.usesDerc20V2Vesting(args.vesting)) {
5316
5383
  return "v2";
@@ -5352,12 +5419,10 @@ var DopplerFactory = class {
5352
5419
  "DERC20 V2 implementation address not configured for this chain."
5353
5420
  );
5354
5421
  }
5355
- const schedules = args.vesting === void 0 ? [] : [
5356
- {
5357
- cliff: BigInt(args.vesting.cliffDuration ?? 0),
5358
- duration: BigInt(args.vesting.duration ?? 0)
5359
- }
5360
- ];
5422
+ const { schedules, scheduleIds } = this.resolveV2VestingSchedules({
5423
+ vesting: args.vesting,
5424
+ recipientCount: recipients.length
5425
+ });
5361
5426
  return {
5362
5427
  kind: "v2",
5363
5428
  name: args.token.name,
@@ -5367,7 +5432,7 @@ var DopplerFactory = class {
5367
5432
  yearlyMintRate,
5368
5433
  schedules,
5369
5434
  beneficiaries: recipients,
5370
- scheduleIds: recipients.map(() => 0n),
5435
+ scheduleIds,
5371
5436
  amounts,
5372
5437
  tokenURI: args.token.tokenURI,
5373
5438
  implementation
@@ -8154,19 +8219,49 @@ var DopplerFactory = class {
8154
8219
  }
8155
8220
  const cliffDuration = vesting.cliffDuration ?? 0;
8156
8221
  const duration = vesting.duration ?? 0;
8157
- if (cliffDuration < 0) {
8158
- throw new Error("Vesting cliff duration cannot be negative");
8159
- }
8160
- if (duration < 0) {
8161
- throw new Error("Vesting duration cannot be negative");
8162
- }
8163
- if (cliffDuration > duration) {
8164
- throw new Error("Vesting cliff duration cannot exceed vesting duration");
8165
- }
8166
- if (cliffDuration > 0 && duration > 0 && duration < DERC20_V2_MIN_VESTING_DURATION) {
8222
+ const hasCustomSchedules = this.hasCustomV2Schedules(vesting);
8223
+ if (hasCustomSchedules && (cliffDuration > 0 || duration > 0 || vesting.recipients !== void 0 || vesting.amounts !== void 0)) {
8167
8224
  throw new Error(
8168
- `Vesting duration must be 0 or at least ${DERC20_V2_MIN_VESTING_DURATION} seconds when using cliffs`
8225
+ "Use vesting.allocations instead of top-level duration/cliffDuration/recipients/amounts when configuring per-beneficiary vesting"
8226
+ );
8227
+ }
8228
+ const availableForVesting = sale.initialSupply - sale.numTokensToSell;
8229
+ if (vesting.allocations) {
8230
+ if (vesting.allocations.length === 0) {
8231
+ throw new Error("Vesting allocations array cannot be empty");
8232
+ }
8233
+ const totalVested = vesting.allocations.reduce(
8234
+ (sum, allocation) => sum + allocation.amount,
8235
+ 0n
8169
8236
  );
8237
+ if (totalVested > availableForVesting) {
8238
+ throw new Error(
8239
+ `Total vesting amount (${totalVested}) exceeds available tokens (${availableForVesting})`
8240
+ );
8241
+ }
8242
+ for (const [index, allocation] of vesting.allocations.entries()) {
8243
+ const scheduleCliff = allocation.schedule.cliffDuration ?? 0;
8244
+ const scheduleDuration = allocation.schedule.duration ?? 0;
8245
+ this.validateUint64LikeNumber(
8246
+ scheduleCliff,
8247
+ `Vesting allocations[${index}].schedule.cliffDuration`
8248
+ );
8249
+ this.validateUint64LikeNumber(
8250
+ scheduleDuration,
8251
+ `Vesting allocations[${index}].schedule.duration`
8252
+ );
8253
+ if (scheduleCliff > scheduleDuration) {
8254
+ throw new Error(
8255
+ `Vesting allocations[${index}].schedule.cliffDuration cannot exceed duration`
8256
+ );
8257
+ }
8258
+ if (scheduleCliff > 0 && scheduleDuration > 0 && scheduleDuration < DERC20_V2_MIN_VESTING_DURATION) {
8259
+ throw new Error(
8260
+ `Vesting allocations[${index}].schedule.duration must be 0 or at least ${DERC20_V2_MIN_VESTING_DURATION} seconds when using cliffs`
8261
+ );
8262
+ }
8263
+ }
8264
+ return;
8170
8265
  }
8171
8266
  if (vesting.recipients && vesting.amounts) {
8172
8267
  if (vesting.recipients.length !== vesting.amounts.length) {
@@ -8178,17 +8273,30 @@ var DopplerFactory = class {
8178
8273
  throw new Error("Vesting recipients array cannot be empty");
8179
8274
  }
8180
8275
  const totalVested = vesting.amounts.reduce((sum, amt) => sum + amt, 0n);
8181
- const availableForVesting = sale.initialSupply - sale.numTokensToSell;
8182
8276
  if (totalVested > availableForVesting) {
8183
8277
  throw new Error(
8184
8278
  `Total vesting amount (${totalVested}) exceeds available tokens (${availableForVesting})`
8185
8279
  );
8186
8280
  }
8187
- return;
8281
+ } else {
8282
+ const vestedAmount = sale.initialSupply - sale.numTokensToSell;
8283
+ if (vestedAmount <= 0n) {
8284
+ throw new Error("No tokens available for vesting");
8285
+ }
8188
8286
  }
8189
- const vestedAmount = sale.initialSupply - sale.numTokensToSell;
8190
- if (vestedAmount <= 0n) {
8191
- throw new Error("No tokens available for vesting");
8287
+ if (cliffDuration < 0) {
8288
+ throw new Error("Vesting cliff duration cannot be negative");
8289
+ }
8290
+ if (duration < 0) {
8291
+ throw new Error("Vesting duration cannot be negative");
8292
+ }
8293
+ if (cliffDuration > duration) {
8294
+ throw new Error("Vesting cliff duration cannot exceed vesting duration");
8295
+ }
8296
+ if (cliffDuration > 0 && duration > 0 && duration < DERC20_V2_MIN_VESTING_DURATION) {
8297
+ throw new Error(
8298
+ `Vesting duration must be 0 or at least ${DERC20_V2_MIN_VESTING_DURATION} seconds when using cliffs`
8299
+ );
8192
8300
  }
8193
8301
  }
8194
8302
  validateStaticAuctionParams(params) {
@@ -8229,7 +8337,6 @@ var DopplerFactory = class {
8229
8337
  throw new Error("Cannot sell more tokens than initial supply");
8230
8338
  }
8231
8339
  this.validateVestingConfig(params.sale, params.vesting);
8232
- this.validateVestingConfig(params.sale, params.vesting);
8233
8340
  if (params.migration.type === "dopplerHook") {
8234
8341
  throw new Error(
8235
8342
  "dopplerHook migration is only supported for dynamic auctions"
@@ -13424,6 +13531,26 @@ var Eth = class {
13424
13531
  };
13425
13532
 
13426
13533
  // src/evm/builders/shared.ts
13534
+ var MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
13535
+ function normalizeBuilderVestingScheduleDuration(value, fieldPath) {
13536
+ const duration = value ?? 0n;
13537
+ if (duration < 0n) {
13538
+ throw new RangeError(`${fieldPath} cannot be negative`);
13539
+ }
13540
+ if (duration > MAX_SAFE_INTEGER_BIGINT) {
13541
+ throw new RangeError(`${fieldPath} must be a safe integer`);
13542
+ }
13543
+ return Number(duration);
13544
+ }
13545
+ function normalizeBuilderVestingSchedule(schedule, fieldPath) {
13546
+ return {
13547
+ duration: normalizeBuilderVestingScheduleDuration(
13548
+ schedule.duration,
13549
+ `${fieldPath}.duration`
13550
+ ),
13551
+ cliffDuration: schedule.cliffDuration ?? 0
13552
+ };
13553
+ }
13427
13554
  function computeTicks(priceRange, tickSpacing) {
13428
13555
  const startTick = Math.floor(
13429
13556
  Math.log(priceRange.startPrice) / Math.log(1.0001) / tickSpacing
@@ -13719,6 +13846,19 @@ var StaticAuctionBuilder = class _StaticAuctionBuilder {
13719
13846
  this.vesting = void 0;
13720
13847
  return this;
13721
13848
  }
13849
+ if (params.allocations) {
13850
+ this.vesting = {
13851
+ allocations: params.allocations.map((allocation, index) => ({
13852
+ recipient: allocation.recipient,
13853
+ amount: allocation.amount,
13854
+ schedule: normalizeBuilderVestingSchedule(
13855
+ allocation.schedule,
13856
+ `Vesting allocations[${index}].schedule`
13857
+ )
13858
+ }))
13859
+ };
13860
+ return this;
13861
+ }
13722
13862
  this.vesting = {
13723
13863
  duration: Number(params.duration ?? DEFAULT_V3_VESTING_DURATION),
13724
13864
  cliffDuration: params.cliffDuration ?? 0,
@@ -14051,6 +14191,19 @@ var DynamicAuctionBuilder = class _DynamicAuctionBuilder {
14051
14191
  this.vesting = void 0;
14052
14192
  return this;
14053
14193
  }
14194
+ if (params.allocations) {
14195
+ this.vesting = {
14196
+ allocations: params.allocations.map((allocation, index) => ({
14197
+ recipient: allocation.recipient,
14198
+ amount: allocation.amount,
14199
+ schedule: normalizeBuilderVestingSchedule(
14200
+ allocation.schedule,
14201
+ `Vesting allocations[${index}].schedule`
14202
+ )
14203
+ }))
14204
+ };
14205
+ return this;
14206
+ }
14054
14207
  this.vesting = {
14055
14208
  duration: Number(params.duration ?? 0n),
14056
14209
  cliffDuration: params.cliffDuration ?? 0,
@@ -14657,6 +14810,19 @@ var MulticurveBuilder = class _MulticurveBuilder {
14657
14810
  this.vesting = void 0;
14658
14811
  return this;
14659
14812
  }
14813
+ if (params.allocations) {
14814
+ this.vesting = {
14815
+ allocations: params.allocations.map((allocation, index) => ({
14816
+ recipient: allocation.recipient,
14817
+ amount: allocation.amount,
14818
+ schedule: normalizeBuilderVestingSchedule(
14819
+ allocation.schedule,
14820
+ `Vesting allocations[${index}].schedule`
14821
+ )
14822
+ }))
14823
+ };
14824
+ return this;
14825
+ }
14660
14826
  this.vesting = {
14661
14827
  duration: Number(params.duration ?? 0n),
14662
14828
  cliffDuration: params.cliffDuration ?? 0,
@@ -15046,6 +15212,19 @@ var OpeningAuctionBuilder = class _OpeningAuctionBuilder {
15046
15212
  this.vesting = void 0;
15047
15213
  return this;
15048
15214
  }
15215
+ if (params.allocations) {
15216
+ this.vesting = {
15217
+ allocations: params.allocations.map((allocation, index) => ({
15218
+ recipient: allocation.recipient,
15219
+ amount: allocation.amount,
15220
+ schedule: normalizeBuilderVestingSchedule(
15221
+ allocation.schedule,
15222
+ `Vesting allocations[${index}].schedule`
15223
+ )
15224
+ }))
15225
+ };
15226
+ return this;
15227
+ }
15049
15228
  this.vesting = {
15050
15229
  duration: Number(params.duration ?? 0n),
15051
15230
  cliffDuration: params.cliffDuration ?? 0,