@ickb/sdk 1000.0.26 → 1000.0.43

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/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { ccc, type Epoch } from "@ckb-ccc/core";
1
+ import { ccc } from "@ckb-ccc/core";
2
2
  import {
3
- arrayFrom,
3
+ collect,
4
4
  epochCompare,
5
5
  CapacityManager,
6
6
  SmartTransaction,
@@ -8,34 +8,36 @@ import {
8
8
  type UdtCell,
9
9
  type CapacityCell,
10
10
  epochAdd,
11
- type SmartTransactionLike,
11
+ asyncBinarySearch,
12
+ RestrictedTransaction,
13
+ type ExchangeRatio,
12
14
  } from "@ickb/utils";
13
15
  import {
14
16
  convert,
15
17
  ICKB_DEPOSIT_CAP,
16
18
  ickbExchangeRatio,
17
- IckbManager,
19
+ IckbUdtManager,
18
20
  LogicManager,
19
21
  OwnedOwnerManager,
20
22
  type IckbDepositCell,
21
23
  type ReceiptCell,
22
- type WithdrawalGroups,
24
+ type WithdrawalGroup,
23
25
  } from "@ickb/core";
24
- import { DaoManager } from "@ickb/dao";
26
+ import { DaoManager, type DaoCell } from "@ickb/dao";
25
27
  import {
26
28
  Info,
27
29
  OrderManager,
28
- Ratio,
29
30
  type OrderCell,
30
31
  type OrderGroup,
31
32
  } from "@ickb/order";
32
33
 
33
34
  export class IckbController {
34
35
  constructor(
35
- public ickbManager: IckbManager,
36
- public logicManager: LogicManager,
37
- public ownedOwnerManager: OwnedOwnerManager,
38
- public orderManager: OrderManager,
36
+ public readonly ickbUdtManager: IckbUdtManager,
37
+ public readonly logicManager: LogicManager,
38
+ public readonly ownedOwnerManager: OwnedOwnerManager,
39
+ public readonly orderManager: OrderManager,
40
+ public readonly daoManager: DaoManager,
39
41
  ) {}
40
42
 
41
43
  static fromDeps({
@@ -65,190 +67,205 @@ export class IckbController {
65
67
  ),
66
68
  cellDeps,
67
69
  };
68
- const ickbManager = IckbManager.fromDeps(ickbXudt, daoManager);
70
+ const ickbUdtManager = IckbUdtManager.fromDeps(ickbXudt, daoManager);
69
71
 
70
72
  return new IckbController(
71
- ickbManager,
72
- LogicManager.fromDeps(ickbLogic, daoManager, ickbManager),
73
- OwnedOwnerManager.fromDeps(ownedOwner, daoManager, ickbManager),
74
- OrderManager.fromDeps(order, ickbManager),
73
+ ickbUdtManager,
74
+ LogicManager.fromDeps(ickbLogic, daoManager, ickbUdtManager),
75
+ OwnedOwnerManager.fromDeps(ownedOwner, daoManager, ickbUdtManager),
76
+ OrderManager.fromDeps(order, ickbUdtManager),
77
+ daoManager,
75
78
  );
76
79
  }
77
80
 
78
- previewConversion(
79
- isCkb2Udt: boolean,
80
- amount: ccc.FixedPoint,
81
- options: ({ ratio: Ratio } | { tip: ccc.ClientBlockHeader }) & {
82
- fee?: {
83
- numerator: ccc.Num;
84
- denominator: ccc.Num;
81
+ // fee examples:
82
+ // 100000n => 0.001%
83
+ // 10000n => 0.01%
84
+ // 1000n => 0.1%
85
+ // 100n => 1%
86
+ // 10n => 10%
87
+ // 1n => 100%
88
+ // less than 1n => 0%
89
+ static previewConversion(conversion: {
90
+ isCkb2Udt: boolean;
91
+ amount: ccc.FixedPoint;
92
+ rate: ExchangeRatio | ccc.ClientBlockHeader;
93
+ fee: ccc.Num;
94
+ }): { rate: ExchangeRatio; convertedAmount: ccc.FixedPoint } {
95
+ const { isCkb2Udt, amount, fee } = conversion;
96
+ let rate = conversion.rate;
97
+ rate = "dao" in rate ? ickbExchangeRatio(rate) : rate;
98
+
99
+ if (fee > ccc.Zero) {
100
+ const { ckbScale, udtScale } = rate;
101
+ rate = {
102
+ ckbScale,
103
+ // Worst case scenario where fee applies to the full amount
104
+ udtScale: udtScale + (isCkb2Udt ? 1n : -1n) * (udtScale / fee),
85
105
  };
86
- },
87
- ): ccc.FixedPoint {
88
- // TODO: make compatible with logic.convert(isCkb2Udt, amount, options)!!!
89
-
90
- const { ckbScale, udtScale } =
91
- "ratio" in options ? options.ratio : ickbExchangeRatio(options.tip);
92
-
93
- let convertedAmount = isCkb2Udt
94
- ? (amount * ckbScale) / udtScale
95
- : (amount * udtScale) / ckbScale;
96
-
97
- if (options.fee) {
98
- const { numerator, denominator } = options.fee;
99
- convertedAmount -= (convertedAmount * numerator) / denominator;
100
106
  }
101
107
 
102
- return convertedAmount;
108
+ const convertedAmount =
109
+ amount > ccc.Zero ? convert(isCkb2Udt, amount, rate) : ccc.Zero;
110
+
111
+ return { rate, convertedAmount };
103
112
  }
104
113
 
105
- // TODO streamline parameters with previewConversion
106
114
  async convert(options: {
115
+ conversion: Parameters<typeof IckbController.previewConversion>[0] & {
116
+ ckbMinMatchLog?: number;
117
+ ckbMinFee?: ccc.Num;
118
+ };
107
119
  signer: ccc.Signer;
108
- isCkb2Udt: boolean;
109
- amount: ccc.FixedPoint;
110
120
  userCells: UserCells;
111
- minCkbChange: ccc.FixedPoint;
112
121
  system: SystemState;
113
- balance: Balance;
114
- fee?: {
115
- numerator: ccc.Num;
116
- denominator: ccc.Num;
122
+ filters: {
123
+ isReadyOnly: boolean;
124
+ isFulfilledOnly: boolean;
117
125
  };
118
126
  }): Promise<Conversion | undefined> {
119
- const { isCkb2Udt, amount, system, signer } = options;
120
- // TODO: RatioLike and specify fee!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
121
- // Or let outside specify adjusted rate...
122
- const info = Info.create(isCkb2Udt, Ratio.from(system.exchangeRatio));
123
- const address = await signer.getRecommendedAddressObj();
127
+ // Get user lock
128
+ const { system, signer } = options;
129
+ const lock = (await signer.getRecommendedAddressObj()).script;
130
+
131
+ // Normalize rate
132
+ const { rate } = IckbController.previewConversion({
133
+ ...options.conversion,
134
+ amount: ccc.Zero,
135
+ });
136
+ const { isCkb2Udt, amount, ckbMinMatchLog } = options.conversion;
137
+ const info = Info.create(isCkb2Udt, rate, ckbMinMatchLog);
138
+ // feeRate is a good approximation of the bot tx size matching the order
139
+ const ckbMinFee = options.conversion.ckbMinFee ?? 10n * system.feeRate;
140
+ const conversion = {
141
+ isCkb2Udt,
142
+ amount,
143
+ rate,
144
+ info,
145
+ ckbMinFee,
146
+ lock,
147
+ };
148
+
124
149
  const N = isCkb2Udt
125
150
  ? Number(amount / system.ckbDepositCap)
126
- : system.deposits.slice(0, 30).length;
151
+ : system.depositPool.slice(0, 30).length;
152
+
127
153
  const cache = Array<ReturnType<IckbController["convert"]>>(N);
128
- const attempt = (n: number): ReturnType<IckbController["convert"]> => {
154
+ const cachedAttempt = (
155
+ n: number,
156
+ ): ReturnType<IckbController["convert"]> => {
129
157
  n = N - n;
130
- return (cache[n] =
131
- cache[n] ?? this.attemptConversion(n, info, address, options));
158
+ return (cache[n] = cache[n] ?? this.attempt(n, conversion, options));
132
159
  };
133
- return attempt(
160
+
161
+ return cachedAttempt(
134
162
  await asyncBinarySearch(
135
163
  N,
136
- async (n: number) => (await attempt(n)) !== undefined,
164
+ async (n: number) => (await cachedAttempt(n)) !== undefined,
137
165
  ),
138
166
  );
139
167
  }
140
168
 
141
- private async attemptConversion(
169
+ private async attempt(
142
170
  n: number,
143
- info: Info,
144
- address: ccc.Address,
145
- {
146
- signer,
147
- isCkb2Udt,
148
- amount,
149
- userCells,
150
- system: { feeRate, tip, deposits, ckbDepositCap, exchangeRatio },
151
- }: Parameters<IckbController["convert"]>[0],
171
+ conversion: {
172
+ isCkb2Udt: boolean;
173
+ amount: bigint;
174
+ rate: ExchangeRatio;
175
+ info: Info;
176
+ ckbMinFee: ccc.Num;
177
+ lock: ccc.Script;
178
+ },
179
+ options: Omit<Parameters<IckbController["convert"]>[0], "conversion">,
152
180
  ): ReturnType<IckbController["convert"]> {
153
- let tx: SmartTransaction = OfflineTransaction.default();
154
- const maturities: Epoch[] = [];
155
- let ckbFee = ccc.Zero;
156
-
157
- if (n > 0) {
158
- if (isCkb2Udt) {
159
- amount -= ckbDepositCap * BigInt(n);
160
- if (amount < 0n) {
161
- // Too many Deposits respectfully to the amount
162
- return undefined;
163
- }
164
- this.logicManager.deposit(tx, n, ckbDepositCap, address.script);
165
- } else {
166
- const myDeposits = deposits.slice(0, n);
167
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168
- amount -= myDeposits.slice(-1)[0]!.udtCumulative;
169
- if (amount < 0n) {
170
- // Too many Withdrawal Requests respectfully to the amount
171
- return undefined;
181
+ try {
182
+ const { isCkb2Udt, rate, info, ckbMinFee, lock } = conversion;
183
+ const { signer, userCells, system, filters } = options;
184
+ const { feeRate, depositPool, ckbDepositCap, exchangeRatio } = system;
185
+
186
+ const tx = SmartTransaction.default();
187
+ const maturities: ccc.Epoch[] = [];
188
+ let ckbFee = ccc.Zero;
189
+ let amount = conversion.amount;
190
+
191
+ // Core protocol
192
+ if (n > 0) {
193
+ if (isCkb2Udt) {
194
+ amount -= ckbDepositCap * BigInt(n);
195
+ if (amount < ccc.Zero) {
196
+ // Too many Deposits respectfully to the amount
197
+ return undefined;
198
+ }
199
+ this.logicManager.deposit(tx, n, ckbDepositCap, lock);
200
+ } else {
201
+ const myDeposits = depositPool.slice(0, n);
202
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
203
+ amount -= myDeposits.slice(-1)[0]!.udtCumulative;
204
+ if (amount < ccc.Zero) {
205
+ // Too many Withdrawal Requests respectfully to the amount
206
+ return undefined;
207
+ }
208
+ this.ownedOwnerManager.requestWithdrawal(tx, myDeposits, lock);
209
+ maturities.push(...myDeposits.map((d) => d.maturity));
172
210
  }
173
- this.ownedOwnerManager.requestWithdrawal(
174
- tx,
175
- myDeposits,
176
- address.script,
211
+ }
212
+
213
+ // Order protocol
214
+ if (amount > ccc.Zero) {
215
+ this.orderManager.mint(tx, lock, info, {
216
+ ckbValue: isCkb2Udt ? amount : ccc.Zero,
217
+ udtValue: isCkb2Udt ? ccc.Zero : amount,
218
+ });
219
+
220
+ const convertedAmount = convert(isCkb2Udt, amount, rate);
221
+ ckbFee = isCkb2Udt
222
+ ? amount - convert(false, convertedAmount, exchangeRatio)
223
+ : convert(true, amount, exchangeRatio) - convertedAmount;
224
+
225
+ maturities.push(
226
+ // TODO: use real world data from deposits, open orders and bot balances
227
+ orderMaturityEstimate(isCkb2Udt, amount, system.tip),
177
228
  );
178
- maturities.push(...myDeposits.map((d) => d.maturity));
179
229
  }
180
- }
181
230
 
182
- if (amount > 0n) {
183
- this.orderManager.mint(
184
- tx,
185
- address.script,
186
- info,
187
- isCkb2Udt ? amount : ccc.Zero,
188
- isCkb2Udt ? ccc.Zero : amount,
189
- );
231
+ // Check that order provides enough fee to the bot for being matched
232
+ if (ckbFee < ckbMinFee) {
233
+ return undefined;
234
+ }
190
235
 
191
- const ratio = isCkb2Udt ? info.ckbToUdt : info.udtToCkb;
192
- const convertedAmount = convert(isCkb2Udt, amount, ratio);
193
- ckbFee = isCkb2Udt
194
- ? amount - convert(false, convertedAmount, exchangeRatio)
195
- : convert(true, amount, exchangeRatio) - convertedAmount;
236
+ const { receipts, withdrawalGroups, orders, udts, capacities } =
237
+ userCells;
196
238
 
197
- maturities.push(
198
- // TODO: use real world data from deposits, open orders and bot balances
199
- orderMaturityEstimate(isCkb2Udt, amount, tip),
200
- );
201
- }
239
+ this.logicManager.completeDeposit(tx, receipts);
240
+ this.ownedOwnerManager.withdraw(tx, withdrawalGroups, filters);
241
+ this.orderManager.melt(tx, orders, filters);
242
+ this.ickbUdtManager.addUdts(tx, udts);
243
+ CapacityManager.addCapacities(tx, capacities);
202
244
 
203
- // Check that order provides enough fee to the bot for being matched
204
- // feeRate is a good approximation of the bot tx size matching the order
205
- if (10n * (feeRate - ckbFee) > ckbFee) {
206
- return undefined;
207
- }
245
+ // Throw an error if inputs do not cover for outputs
246
+ await new RestrictedTransaction(tx).completeFeeBy(signer, feeRate);
208
247
 
209
- const { receipts, withdrawalGroups, orders, udts, capacities } = userCells;
248
+ // Find the maturity most distant in the future
249
+ const maturity = maturities.reduce((a, b) =>
250
+ epochCompare(a, b) === -1 ? b : a,
251
+ );
210
252
 
211
- this.logicManager.completeDeposit(tx, receipts);
212
- this.ownedOwnerManager.withdraw(tx, withdrawalGroups); //TODO: ignore not isReady cells in method!!!
213
- for (const order of orders) {
214
- this.orderManager.melt(tx, order); //TODO: allow melting multiple groups and give choices!!!
215
- }
216
- // const outputLen = tx.inputs.length;??
217
- // Give choice that let user add this cells the way he prefer!! (Either here or at completeFeeBy)
218
- this.ickbManager.addUdts(tx, udts);
219
- CapacityManager.addCapacities(tx, capacities);
220
- const outputLen = tx.outputs.length;
253
+ return {
254
+ tx,
255
+ ckbFee,
256
+ maturity,
257
+ };
221
258
 
222
- try {
223
- await tx.completeFeeBy(signer, feeRate);
224
- // TODO: check in SmartTransaction for 64 output cells in DAO transactions
225
- if (tx.outputs.length > 64) {
226
- return undefined;
227
- }
228
259
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
229
260
  } catch (_) {
230
261
  return undefined;
231
262
  }
232
-
233
- // Revert any change cells
234
- tx.outputs = tx.outputs.slice(outputLen);
235
- tx = SmartTransaction.from(tx);
236
-
237
- const maturity = maturities.reduce((a, b) =>
238
- epochCompare(a, b) === -1 ? b : a,
239
- );
240
-
241
- return {
242
- tx,
243
- ckbFee,
244
- maturity,
245
- };
246
263
  }
247
264
 
248
- // TODO: Allow filtering which cells are fetched
249
265
  async getL1State(
250
266
  client: ccc.Client,
251
- locks: ccc.ScriptLike[],
267
+ filter: Set<keyof UserCells | "depositPool">,
268
+ locks: ccc.Script[],
252
269
  options?: {
253
270
  minLockUp?: ccc.Epoch;
254
271
  maxLockUp?: ccc.Epoch;
@@ -268,25 +285,42 @@ export class IckbController {
268
285
  receipts,
269
286
  withdrawalGroups,
270
287
  orders,
288
+ deposits,
289
+ withdrawalRequests,
271
290
  ] = await Promise.all([
272
- // TODO: fetch user NervosDAO cells
273
- // TODO: fetch bot cells for estimating timings
274
291
  client.getFeeRate(),
275
- arrayFrom(this.logicManager.findDeposits(client, opts)),
276
- arrayFrom(CapacityManager.findCapacities(client, locks, opts)),
277
- arrayFrom(this.ickbManager.findUdts(client, locks, opts)),
278
- arrayFrom(this.logicManager.findReceipts(client, locks, opts)),
279
- arrayFrom(
280
- this.ownedOwnerManager.findWithdrawalGroups(client, locks, opts),
281
- ),
282
- this.orderManager.findOrders(client),
292
+ filter.has("depositPool")
293
+ ? collect(this.logicManager.findDeposits(client, opts))
294
+ : [],
295
+ filter.has("capacities")
296
+ ? collect(CapacityManager.findCapacities(client, locks, opts))
297
+ : [],
298
+ filter.has("udts")
299
+ ? collect(this.ickbUdtManager.findUdts(client, locks, opts))
300
+ : [],
301
+ filter.has("receipts")
302
+ ? collect(this.logicManager.findReceipts(client, locks, opts))
303
+ : [],
304
+ filter.has("withdrawalGroups")
305
+ ? collect(
306
+ this.ownedOwnerManager.findWithdrawalGroups(client, locks, opts),
307
+ )
308
+ : [],
309
+ filter.has("orders") ? collect(this.orderManager.findOrders(client)) : [],
310
+ filter.has("deposits")
311
+ ? collect(this.daoManager.findDeposits(client, locks, opts))
312
+ : [],
313
+ filter.has("withdrawalRequests")
314
+ ? collect(this.daoManager.findWithdrawalRequests(client, locks, opts))
315
+ : [],
316
+ // TODO: fetch bot cells for estimating timings
283
317
  ]);
284
318
 
285
319
  // Sort depositPool by maturity
286
320
  depositPool.sort((a, b) => epochCompare(a.maturity, b.maturity));
287
321
  // Calculate cumulative value of deposits in the pool
288
322
  let udtCumulative = 0n;
289
- const cumulativeDepositPool: SystemState["deposits"] = [];
323
+ const cumulativeDepositPool: SystemState["depositPool"] = [];
290
324
  for (const deposit of depositPool) {
291
325
  udtCumulative += deposit.udtValue;
292
326
  cumulativeDepositPool.push({ ...deposit, udtCumulative });
@@ -303,7 +337,7 @@ export class IckbController {
303
337
  }
304
338
 
305
339
  const exchangeRatio = ickbExchangeRatio(tip);
306
- const ckbDepositCap = convert(false, ICKB_DEPOSIT_CAP, exchangeRatio); // TODO: Remove export from core
340
+ const ckbDepositCap = convert(false, ICKB_DEPOSIT_CAP, exchangeRatio);
307
341
 
308
342
  return {
309
343
  system: {
@@ -312,8 +346,8 @@ export class IckbController {
312
346
  exchangeRatio,
313
347
  ckbDepositCap,
314
348
  udtDepositCap: ICKB_DEPOSIT_CAP,
315
- deposits: cumulativeDepositPool,
316
- orders: nonUserOrders,
349
+ depositPool: cumulativeDepositPool,
350
+ orderPool: nonUserOrders,
317
351
  },
318
352
  user: {
319
353
  capacities,
@@ -321,45 +355,11 @@ export class IckbController {
321
355
  receipts,
322
356
  withdrawalGroups,
323
357
  orders: userOrders,
358
+ deposits,
359
+ withdrawalRequests,
324
360
  },
325
361
  };
326
362
  }
327
-
328
- // TODO: Allow filtering which cells are accounted for
329
- static getBalance({
330
- capacities,
331
- udts,
332
- receipts,
333
- withdrawalGroups,
334
- orders,
335
- }: UserCells): Balance {
336
- const cells: { cell: ccc.Cell; udtValue?: ccc.FixedPoint }[] = [
337
- capacities,
338
- udts,
339
- receipts,
340
- ].flat();
341
- for (const { owner, owned } of withdrawalGroups) {
342
- cells.push(owned, owner);
343
- }
344
- for (const { master, order } of orders) {
345
- cells.push(master, order);
346
- }
347
- let ckb = ccc.Zero;
348
- let udt = ccc.Zero;
349
- for (const cell of cells) {
350
- ckb += cell.cell.cellOutput.capacity;
351
- udt += cell.udtValue ?? ccc.Zero;
352
- }
353
- return {
354
- ckb,
355
- udt,
356
- };
357
- }
358
- }
359
-
360
- export interface Balance {
361
- ckb: ccc.FixedPoint;
362
- udt: ccc.FixedPoint;
363
363
  }
364
364
 
365
365
  export interface SystemState {
@@ -371,22 +371,24 @@ export interface SystemState {
371
371
  };
372
372
  ckbDepositCap: ccc.FixedPoint;
373
373
  udtDepositCap: ccc.FixedPoint;
374
- deposits: (IckbDepositCell & { udtCumulative: bigint })[];
375
- orders: OrderCell[];
374
+ depositPool: (IckbDepositCell & { udtCumulative: bigint })[];
375
+ orderPool: OrderCell[];
376
376
  }
377
377
 
378
378
  export interface UserCells {
379
379
  capacities: CapacityCell[];
380
380
  udts: UdtCell[];
381
381
  receipts: ReceiptCell[];
382
- withdrawalGroups: WithdrawalGroups[];
382
+ withdrawalGroups: WithdrawalGroup[];
383
383
  orders: OrderGroup[];
384
+ deposits: DaoCell[];
385
+ withdrawalRequests: DaoCell[];
384
386
  }
385
387
 
386
388
  export interface Conversion {
387
389
  tx: SmartTransaction;
388
390
  ckbFee: ccc.FixedPoint;
389
- maturity: Epoch;
391
+ maturity: ccc.Epoch;
390
392
  }
391
393
 
392
394
  // Estimate bot ability to fulfill orders:
@@ -403,77 +405,3 @@ function orderMaturityEstimate(
403
405
  4n * 60n,
404
406
  ]);
405
407
  }
406
-
407
- class OfflineTransaction extends SmartTransaction {
408
- override clone(): OfflineTransaction {
409
- const result = super.clone();
410
- // Keep inputs metadata when cloning, so it will not refetch that data
411
- result.inputs = [...this.inputs];
412
- return new OfflineTransaction(result);
413
- }
414
-
415
- // eslint-disable-next-line @typescript-eslint/require-await
416
- override async completeInputs<T>(
417
- _0: never,
418
- _1: never,
419
- _2: never,
420
- init: T,
421
- ): Promise<{ addedCount: number; accumulated?: T }> {
422
- // Disable completeInputs, so it will not fetch additional cells
423
- return {
424
- addedCount: 0,
425
- accumulated: init,
426
- };
427
- }
428
-
429
- // Reimplement the rest of transformations
430
-
431
- constructor(tx: SmartTransaction) {
432
- super(
433
- tx.version,
434
- tx.cellDeps,
435
- tx.headerDeps,
436
- tx.inputs,
437
- tx.outputs,
438
- tx.outputsData,
439
- tx.witnesses,
440
- tx.udtHandlers,
441
- tx.headers,
442
- );
443
- }
444
-
445
- static override default(): OfflineTransaction {
446
- return new OfflineTransaction(super.default());
447
- }
448
-
449
- static override fromLumosSkeleton(
450
- skeleton: ccc.LumosTransactionSkeletonType,
451
- ): OfflineTransaction {
452
- return new OfflineTransaction(super.fromLumosSkeleton(skeleton));
453
- }
454
-
455
- static override from(txLike: SmartTransactionLike): OfflineTransaction {
456
- return new OfflineTransaction(SmartTransaction.from(txLike));
457
- }
458
- }
459
-
460
- // TODO: utils.binarySearch is sync, need an async one for await the result!!!!!!!!!!!!
461
- async function asyncBinarySearch(
462
- n: number,
463
- f: (i: number) => Promise<boolean>,
464
- ): Promise<number> {
465
- // Define f(-1) == false and f(n) == true.
466
- // Invariant: f(i-1) == false, f(j) == true.
467
- let [i, j] = [0, n];
468
- while (i < j) {
469
- const h = Math.trunc((i + j) / 2);
470
- // i ≤ h < j
471
- if (!(await f(h))) {
472
- i = h + 1; // preserves f(i-1) == false
473
- } else {
474
- j = h; // preserves f(j) == true
475
- }
476
- }
477
- // i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
478
- return i;
479
- }