@logswap/sdk 0.5.52 → 0.5.54
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/fpool.d.ts +42 -7
- package/dist/fpool.js +52 -14
- package/dist/indexer.d.ts +15 -0
- package/dist/indexer.js +57 -34
- package/dist/liquidity.d.ts +43 -15
- package/dist/liquidity.js +46 -25
- package/dist/pools.d.ts +2 -0
- package/dist/pools.js +6 -14
- package/dist/write.d.ts +8 -0
- package/dist/write.js +16 -1
- package/package.json +1 -1
- package/src/fpool.ts +60 -15
- package/src/indexer.ts +63 -41
- package/src/liquidity.ts +82 -37
- package/src/pools.ts +8 -13
- package/src/write.ts +17 -1
package/src/liquidity.ts
CHANGED
|
@@ -46,8 +46,10 @@ import {
|
|
|
46
46
|
resolveRecipient,
|
|
47
47
|
sendRouterWrite,
|
|
48
48
|
sendRouterWriteWithPermits,
|
|
49
|
+
withSlippage,
|
|
49
50
|
type WriteOptions,
|
|
50
51
|
} from "./write.js";
|
|
52
|
+
import { quoteExactIn, quoteExactOut } from "./swap.js";
|
|
51
53
|
|
|
52
54
|
/* mint ====================================================================== */
|
|
53
55
|
|
|
@@ -58,8 +60,14 @@ export interface MintArgs extends WriteOptions {
|
|
|
58
60
|
cap?: bigint;
|
|
59
61
|
/** Exposure to add. */
|
|
60
62
|
L: bigint;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The most the mint may pull, per token. REQUIRED: the pulled amounts depend on spot when the
|
|
65
|
+
* transaction lands, so an unbounded mint can be sandwiched into depositing far more than the
|
|
66
|
+
* preview showed. Derive them from the preview with {@link withSlippage} (`"input"`); pass
|
|
67
|
+
* {@link UNBOUNDED} only where the price cannot move under you.
|
|
68
|
+
*/
|
|
69
|
+
maxBaseIn: bigint;
|
|
70
|
+
maxQuoteIn: bigint;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
/**
|
|
@@ -73,7 +81,6 @@ export interface MintArgs extends WriteOptions {
|
|
|
73
81
|
* costs zero quote too — that is what makes single-asset entry possible in both directions.
|
|
74
82
|
*/
|
|
75
83
|
export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
|
|
76
|
-
const MAX = (1n << 256n) - 1n;
|
|
77
84
|
return sendRouterWriteWithPermits(c, {
|
|
78
85
|
abi: logswapRouterAbi,
|
|
79
86
|
functionName: "mintBlended",
|
|
@@ -82,8 +89,8 @@ export async function mint(c: LogswapClient, a: MintArgs): Promise<Hash> {
|
|
|
82
89
|
a.targetFloor,
|
|
83
90
|
a.cap ?? NO_CAP,
|
|
84
91
|
a.L,
|
|
85
|
-
a.maxBaseIn
|
|
86
|
-
a.maxQuoteIn
|
|
92
|
+
a.maxBaseIn,
|
|
93
|
+
a.maxQuoteIn,
|
|
87
94
|
resolveRecipient(c, a),
|
|
88
95
|
resolveDeadline(a),
|
|
89
96
|
],
|
|
@@ -98,8 +105,9 @@ export interface CreateMarketArgs extends WriteOptions {
|
|
|
98
105
|
targetFloor: bigint;
|
|
99
106
|
/** The founding exposure; must exceed the manager's `minBackstopL` lock. */
|
|
100
107
|
L: bigint;
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
/** Required, as on {@link mint}. Creation is atomic at `x0`, so the flows are fixed by the arguments; {@link UNBOUNDED} is safe here only for that reason. */
|
|
109
|
+
maxBaseIn: bigint;
|
|
110
|
+
maxQuoteIn: bigint;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
/**
|
|
@@ -111,7 +119,6 @@ export interface CreateMarketArgs extends WriteOptions {
|
|
|
111
119
|
* uncapped.
|
|
112
120
|
*/
|
|
113
121
|
export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promise<Hash> {
|
|
114
|
-
const MAX = (1n << 256n) - 1n;
|
|
115
122
|
return sendRouterWriteWithPermits(c, {
|
|
116
123
|
abi: logswapRouterAbi,
|
|
117
124
|
functionName: "createMarket",
|
|
@@ -120,8 +127,8 @@ export async function createMarket(c: LogswapClient, a: CreateMarketArgs): Promi
|
|
|
120
127
|
a.x0,
|
|
121
128
|
a.targetFloor,
|
|
122
129
|
a.L,
|
|
123
|
-
a.maxBaseIn
|
|
124
|
-
a.maxQuoteIn
|
|
130
|
+
a.maxBaseIn,
|
|
131
|
+
a.maxQuoteIn,
|
|
125
132
|
resolveRecipient(c, a),
|
|
126
133
|
resolveDeadline(a),
|
|
127
134
|
],
|
|
@@ -133,12 +140,13 @@ export interface ZapArgs extends WriteOptions {
|
|
|
133
140
|
/** Fund entirely from base (true) or entirely from quote (false). */
|
|
134
141
|
fundWithBase: boolean;
|
|
135
142
|
amountIn: bigint;
|
|
136
|
-
/** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s.
|
|
137
|
-
minSwapOut
|
|
143
|
+
/** The least the swap leg may return (contracts 7c82eed, decisions 043): the zap's own slippage bound, as `swapExactIn`'s. Required. */
|
|
144
|
+
minSwapOut: bigint;
|
|
138
145
|
targetFloor: bigint;
|
|
139
146
|
cap?: bigint;
|
|
140
147
|
L: bigint;
|
|
141
|
-
|
|
148
|
+
/** The most of the funding token the zap may pull. Required. */
|
|
149
|
+
maxIn: bigint;
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
/**
|
|
@@ -157,11 +165,11 @@ export async function zapIn(c: LogswapClient, a: ZapArgs): Promise<Hash> {
|
|
|
157
165
|
a.key,
|
|
158
166
|
a.fundWithBase,
|
|
159
167
|
a.amountIn,
|
|
160
|
-
a.minSwapOut
|
|
168
|
+
a.minSwapOut,
|
|
161
169
|
a.targetFloor,
|
|
162
170
|
a.cap ?? NO_CAP,
|
|
163
171
|
a.L,
|
|
164
|
-
a.maxIn
|
|
172
|
+
a.maxIn,
|
|
165
173
|
resolveRecipient(c, a),
|
|
166
174
|
resolveDeadline(a),
|
|
167
175
|
],
|
|
@@ -178,13 +186,13 @@ export interface UpdateArgs extends WriteOptions {
|
|
|
178
186
|
toCap?: bigint;
|
|
179
187
|
/** New exposure. `0n` exits the position entirely. */
|
|
180
188
|
newL: bigint;
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
/** Required: see {@link MintArgs}. {@link previewEdit} gives the exact flows to bound. */
|
|
190
|
+
maxBaseIn: bigint;
|
|
191
|
+
maxQuoteIn: bigint;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
/** The primitive, through the router's named entry: resize and reposition in one call, in kind. */
|
|
186
195
|
export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
|
|
187
|
-
const MAX = (1n << 256n) - 1n;
|
|
188
196
|
return sendRouterWriteWithPermits(c, {
|
|
189
197
|
abi: logswapRouterAbi,
|
|
190
198
|
functionName: "update",
|
|
@@ -195,8 +203,8 @@ export async function update(c: LogswapClient, a: UpdateArgs): Promise<Hash> {
|
|
|
195
203
|
a.toFloor,
|
|
196
204
|
a.toCap ?? NO_CAP,
|
|
197
205
|
a.newL,
|
|
198
|
-
a.maxBaseIn
|
|
199
|
-
a.maxQuoteIn
|
|
206
|
+
a.maxBaseIn,
|
|
207
|
+
a.maxQuoteIn,
|
|
200
208
|
resolveRecipient(c, a),
|
|
201
209
|
resolveDeadline(a),
|
|
202
210
|
],
|
|
@@ -209,8 +217,9 @@ export interface MoveArgs extends WriteOptions {
|
|
|
209
217
|
shares: bigint;
|
|
210
218
|
toFloor: bigint;
|
|
211
219
|
toCap?: bigint;
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
/** Required: see {@link MintArgs}. */
|
|
221
|
+
maxBaseIn: bigint;
|
|
222
|
+
maxQuoteIn: bigint;
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
/**
|
|
@@ -218,11 +227,9 @@ export interface MoveArgs extends WriteOptions {
|
|
|
218
227
|
*
|
|
219
228
|
* **Bound it.** The router used to settle this entry with no limit, which meant a reposition that
|
|
220
229
|
* priced differently by inclusion time could pull the whole allowance (security-review2 M-01).
|
|
221
|
-
* The
|
|
222
|
-
* `maxQuoteIn` from a preview for anything user-facing.
|
|
230
|
+
* The bounds are required; {@link UNBOUNDED} is an explicit, visible opt-out.
|
|
223
231
|
*/
|
|
224
232
|
export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
|
|
225
|
-
const MAX = (1n << 256n) - 1n;
|
|
226
233
|
return sendRouterWrite(c, {
|
|
227
234
|
abi: logswapRouterAbi,
|
|
228
235
|
functionName: "move",
|
|
@@ -232,8 +239,8 @@ export async function move(c: LogswapClient, a: MoveArgs): Promise<Hash> {
|
|
|
232
239
|
a.shares,
|
|
233
240
|
a.toFloor,
|
|
234
241
|
a.toCap ?? NO_CAP,
|
|
235
|
-
a.maxBaseIn
|
|
236
|
-
a.maxQuoteIn
|
|
242
|
+
a.maxBaseIn,
|
|
243
|
+
a.maxQuoteIn,
|
|
237
244
|
resolveRecipient(c, a),
|
|
238
245
|
resolveDeadline(a),
|
|
239
246
|
],
|
|
@@ -252,6 +259,13 @@ export interface EditArgs extends WriteOptions, PairBounds {
|
|
|
252
259
|
shares: bigint;
|
|
253
260
|
/** Default `kind`. */
|
|
254
261
|
settleIn?: SettleIn;
|
|
262
|
+
/**
|
|
263
|
+
* Tolerance in basis points, applied to the flows the edit is expected to move (the lens's
|
|
264
|
+
* exact preview, plus a quote of the netting swap when `settleIn` is one token). Required: an
|
|
265
|
+
* edit's flows depend on spot when it lands. An explicit bound in `PairBounds` overrides the one
|
|
266
|
+
* derived for that token.
|
|
267
|
+
*/
|
|
268
|
+
slippageBps: number;
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
export interface EditPreview {
|
|
@@ -277,11 +291,27 @@ export async function previewEdit(
|
|
|
277
291
|
return { dBase: r[0], dQuote: r[1], fees: r[2] };
|
|
278
292
|
}
|
|
279
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Bounds for a pair of signed flows (+ = the caller pays, − = the caller receives), at `bps` of
|
|
296
|
+
* tolerance: a payment is capped from above, a receipt floored from below, and a token that does
|
|
297
|
+
* not move in that direction is bound to zero.
|
|
298
|
+
*/
|
|
299
|
+
export function boundsFromFlows(dBase: bigint, dQuote: bigint, bps: number): Required<PairBounds> {
|
|
300
|
+
const inb = (d: bigint) => (d > 0n ? withSlippage(d, bps, "input") : 0n);
|
|
301
|
+
const outb = (d: bigint) => (d < 0n ? withSlippage(-d, bps, "output") : 0n);
|
|
302
|
+
return { maxBaseIn: inb(dBase), maxQuoteIn: inb(dQuote), minBaseOut: outb(dBase), minQuoteOut: outb(dQuote) };
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
/**
|
|
281
306
|
* The general edit as a batch: `[update]`, plus — when `settleIn` is one token — the one swap that
|
|
282
307
|
* nets the other. The other side's flow is read off the preview: released → sold with an exact-in
|
|
283
308
|
* of `OPEN_DELTA`; owed → bought with an exact-out of `OPEN_DELTA`. The settlement then binds the
|
|
284
309
|
* other token to zero both ways, so a stale preview fails loudly rather than moving it.
|
|
310
|
+
*
|
|
311
|
+
* **Bounded by default.** The preview gives the edit's exact flows at today's state; `slippageBps`
|
|
312
|
+
* turns them into the settlement's bounds. With `settleIn` one token, the netting swap is quoted
|
|
313
|
+
* on today's curve, before the update changes the active liquidity — close for a small edit, and
|
|
314
|
+
* for a large one a bound that reverts rather than one that lets a sandwich through.
|
|
285
315
|
*/
|
|
286
316
|
export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; toCap?: bigint; newL: bigint }): Promise<Hash> {
|
|
287
317
|
const { key } = a;
|
|
@@ -290,25 +320,39 @@ export async function edit(c: LogswapClient, a: EditArgs & { toFloor: bigint; to
|
|
|
290
320
|
updateAction({ key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL }),
|
|
291
321
|
];
|
|
292
322
|
const settleIn = a.settleIn ?? "kind";
|
|
323
|
+
const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
|
|
293
324
|
let settle: Settle;
|
|
294
325
|
if (settleIn === "kind") {
|
|
295
|
-
settle = settleInKind(key, recipient, a);
|
|
326
|
+
settle = settleInKind(key, recipient, { ...boundsFromFlows(p.dBase, p.dQuote, a.slippageBps), ...defined(a) });
|
|
296
327
|
} else {
|
|
297
|
-
const p = await previewEdit(c, { key, fromId: a.fromId, shares: a.shares, toFloor: a.toFloor, toCap: a.toCap, newL: a.newL });
|
|
298
328
|
// the swept fees ride INSIDE dQuote (the manager burns by owner and nets them; decisions 018),
|
|
299
329
|
// so quote's open delta after the update is dQuote itself — `fees` is reported for display only
|
|
300
330
|
const otherFlow = settleIn === "base" ? p.dQuote : p.dBase; // + = owed, − = released
|
|
301
331
|
const otherIsBase = settleIn === "quote";
|
|
302
|
-
|
|
303
|
-
|
|
332
|
+
let own = settleIn === "base" ? p.dBase : p.dQuote;
|
|
333
|
+
if (otherFlow < 0n) {
|
|
334
|
+
actions.push(swapExactInAction({ key, baseIn: otherIsBase, amountIn: OPEN_DELTA }));
|
|
335
|
+
own -= await quoteExactIn(c, key, otherIsBase, -otherFlow);
|
|
336
|
+
} else if (otherFlow > 0n) {
|
|
337
|
+
actions.push(swapExactOutAction({ key, baseOut: otherIsBase, amountOut: OPEN_DELTA }));
|
|
338
|
+
own += await quoteExactOut(c, key, otherIsBase, otherFlow);
|
|
339
|
+
}
|
|
340
|
+
const b = settleIn === "base" ? boundsFromFlows(own, 0n, a.slippageBps) : boundsFromFlows(0n, own, a.slippageBps);
|
|
304
341
|
settle = settleOnly(key, recipient, settleIn, {
|
|
305
|
-
maxIn: settleIn === "base" ? a.maxBaseIn : a.maxQuoteIn,
|
|
306
|
-
minOut: settleIn === "base" ? a.minBaseOut : a.minQuoteOut,
|
|
342
|
+
maxIn: settleIn === "base" ? (a.maxBaseIn ?? b.maxBaseIn) : (a.maxQuoteIn ?? b.maxQuoteIn),
|
|
343
|
+
minOut: settleIn === "base" ? (a.minBaseOut ?? b.minBaseOut) : (a.minQuoteOut ?? b.minQuoteOut),
|
|
307
344
|
});
|
|
308
345
|
}
|
|
309
346
|
return execute(c, { actions, settle, deadline: a.deadline, recipient });
|
|
310
347
|
}
|
|
311
348
|
|
|
349
|
+
/** The explicit bounds a caller passed, without the undefined ones (so they do not erase derived ones). */
|
|
350
|
+
function defined(b: PairBounds): PairBounds {
|
|
351
|
+
return Object.fromEntries(
|
|
352
|
+
(["maxBaseIn", "maxQuoteIn", "minBaseOut", "minQuoteOut"] as const).filter((k) => b[k] !== undefined).map((k) => [k, b[k]]),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
312
356
|
/** The class behind `shares` of `id`: its floor, cap, and the exposure those shares carry. */
|
|
313
357
|
async function classOf(c: LogswapClient, key: PoolKey, id: bigint, shares: bigint) {
|
|
314
358
|
const [d, cls] = await Promise.all([describeId(c, id, key.tickSpacing), getPositionClass(c, id)]);
|
|
@@ -405,8 +449,9 @@ export interface BurnArgs extends WriteOptions {
|
|
|
405
449
|
key: PoolKey;
|
|
406
450
|
ids: readonly bigint[];
|
|
407
451
|
shares: readonly bigint[];
|
|
408
|
-
|
|
409
|
-
|
|
452
|
+
/** The least of each token the burn must return. Required: a burn's legs depend on spot. */
|
|
453
|
+
minBaseOut: bigint;
|
|
454
|
+
minQuoteOut: bigint;
|
|
410
455
|
}
|
|
411
456
|
|
|
412
457
|
/**
|
|
@@ -424,8 +469,8 @@ export async function burn(c: LogswapClient, a: BurnArgs): Promise<Hash> {
|
|
|
424
469
|
a.key,
|
|
425
470
|
a.ids,
|
|
426
471
|
a.shares,
|
|
427
|
-
a.minBaseOut
|
|
428
|
-
a.minQuoteOut
|
|
472
|
+
a.minBaseOut,
|
|
473
|
+
a.minQuoteOut,
|
|
429
474
|
resolveRecipient(c, a),
|
|
430
475
|
resolveDeadline(a),
|
|
431
476
|
],
|
package/src/pools.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { formatUnits, type Address, type Hex } from "viem";
|
|
16
16
|
import { logswapLensAbi, cPoolManagerAbi } from "./generated.js";
|
|
17
17
|
import type { LogswapClient } from "./client.js";
|
|
18
|
+
import { signerFor } from "./write.js";
|
|
18
19
|
import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
|
|
19
20
|
import { poolId, type PoolKey } from "./keys.js";
|
|
20
21
|
|
|
@@ -104,6 +105,8 @@ export interface DiscoveredMarket {
|
|
|
104
105
|
x0: bigint;
|
|
105
106
|
blockNumber: bigint;
|
|
106
107
|
transactionHash: Hex;
|
|
108
|
+
/** The manager that emitted the `Initialize` — set when the row came from the indexer. */
|
|
109
|
+
manager?: Address;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/**
|
|
@@ -188,18 +191,10 @@ export async function marketDecimals(
|
|
|
188
191
|
c: LogswapClient,
|
|
189
192
|
key: PoolKey,
|
|
190
193
|
): Promise<{ base: number; quote: number }> {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
return Number(await c.public.readContract({ address: token, abi, functionName: "decimals" }));
|
|
197
|
-
} catch {
|
|
198
|
-
return 18; // same fallback the manager uses when the token does not implement it
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
const [base, quote] = await Promise.all([read(key.base), read(key.quote)]);
|
|
202
|
-
return { base, quote };
|
|
194
|
+
// It used to fall back to 18 on ANY failure, a dropped request included — a 6-decimal quote read
|
|
195
|
+
// as 18 is every amount 10^12 off. `marketTokens` retries and then refuses; so does this.
|
|
196
|
+
const { base, quote } = await marketTokens(c, key);
|
|
197
|
+
return { base: base.decimals, quote: quote.decimals };
|
|
203
198
|
}
|
|
204
199
|
|
|
205
200
|
/** A token as a UI needs it: address, symbol and decimals, all read from the chain. */
|
|
@@ -391,7 +386,7 @@ export async function initializeMarket(c: LogswapClient, a: { key: PoolKey; x0:
|
|
|
391
386
|
abi: cPoolManagerAbi,
|
|
392
387
|
functionName: "initialize",
|
|
393
388
|
args: [a.key, a.x0],
|
|
394
|
-
account: a.account,
|
|
389
|
+
account: signerFor(c, a.account),
|
|
395
390
|
} as never);
|
|
396
391
|
return c.wallet!.writeContract(request as never);
|
|
397
392
|
}
|
package/src/write.ts
CHANGED
|
@@ -46,6 +46,21 @@ export function resolveDeadline(o: WriteOptions | undefined, now = Date.now()):
|
|
|
46
46
|
* until someone uses a key the node does not have — a bot, a script, CI, or a real user's wallet.
|
|
47
47
|
* Passing the object lets viem pick the correct path for both kinds.
|
|
48
48
|
*/
|
|
49
|
+
/**
|
|
50
|
+
* The wallet's account OBJECT for a write the caller named by address — see {@link requireWallet}
|
|
51
|
+
* for why the object and never the address. The F helpers take `account: Address` in their
|
|
52
|
+
* arguments; passed straight to viem it became a JSON-RPC account and the NODE was asked to sign,
|
|
53
|
+
* which works only on a dev node holding the key (every agent on the devnet did exactly that).
|
|
54
|
+
* A named account that is not the wallet's is refused: nothing else could sign for it.
|
|
55
|
+
*/
|
|
56
|
+
export function signerFor(c: LogswapClient, address: Address): NonNullable<LogswapClient["wallet"]>["account"] {
|
|
57
|
+
const { account } = requireWallet(c);
|
|
58
|
+
if (account!.address.toLowerCase() !== address.toLowerCase()) {
|
|
59
|
+
throw new Error(`logswap: the write names ${address} but the wallet signs as ${account!.address}`);
|
|
60
|
+
}
|
|
61
|
+
return account;
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
export function requireWallet(c: LogswapClient): { account: NonNullable<LogswapClient["wallet"]>["account"]; address: Address } {
|
|
50
65
|
const account = c.wallet?.account;
|
|
51
66
|
if (!c.wallet || !account) {
|
|
@@ -68,7 +83,8 @@ export function resolveRecipient(c: LogswapClient, o?: WriteOptions): Address {
|
|
|
68
83
|
export function withSlippage(quoted: bigint, bps: number, side: "output" | "input"): bigint {
|
|
69
84
|
if (bps < 0 || bps > 10_000) throw new Error(`logswap: slippage bps out of range: ${bps}`);
|
|
70
85
|
const b = BigInt(Math.round(bps));
|
|
71
|
-
|
|
86
|
+
// an input bound rounds UP: flooring it would put a wei-exact quote at bps = 0 one wei short
|
|
87
|
+
return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b) + 9_999n) / 10_000n;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
/**
|