@totalis/hip4-client 0.0.0-stage → 0.65.2
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/CHANGELOG.md +1541 -0
- package/LICENSE +202 -0
- package/README.md +143 -2
- package/dist/api-keys.d.ts +118 -0
- package/dist/api-keys.js +104 -0
- package/dist/audience-contract.d.ts +52 -0
- package/dist/audience-contract.js +1 -0
- package/dist/base.d.ts +41 -0
- package/dist/base.js +60 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +15 -0
- package/dist/controller.d.ts +50 -0
- package/dist/controller.js +60 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +3 -0
- package/dist/flow-errors.d.ts +61 -0
- package/dist/flow-errors.js +70 -0
- package/dist/generated/account-read.d.ts +2009 -0
- package/dist/generated/account-read.js +1 -0
- package/dist/generated/api-keys.d.ts +1303 -0
- package/dist/generated/api-keys.js +1 -0
- package/dist/generated/browse.d.ts +938 -0
- package/dist/generated/browse.js +1 -0
- package/dist/generated/client.d.ts +73 -0
- package/dist/generated/client.js +1 -0
- package/dist/generated/errors.d.ts +404 -0
- package/dist/generated/errors.js +1342 -0
- package/dist/generated/maker.d.ts +47 -0
- package/dist/generated/maker.js +1 -0
- package/dist/generated/openapi.d.ts +8044 -0
- package/dist/generated/openapi.js +1 -0
- package/dist/generated/operator.d.ts +7 -0
- package/dist/generated/operator.js +1 -0
- package/dist/generated/realtime.d.ts +1416 -0
- package/dist/generated/realtime.js +1 -0
- package/dist/generated/vault-abi.d.ts +39 -0
- package/dist/generated/vault-abi.js +56 -0
- package/dist/generated/version.d.ts +23 -0
- package/dist/generated/version.js +143 -0
- package/dist/hypercore.d.ts +111 -0
- package/dist/hypercore.js +132 -0
- package/dist/maker.d.ts +205 -0
- package/dist/maker.js +608 -0
- package/dist/money.d.ts +59 -0
- package/dist/money.js +66 -0
- package/dist/operator.d.ts +4 -0
- package/dist/operator.js +4 -0
- package/dist/pagination.d.ts +20 -0
- package/dist/pagination.js +27 -0
- package/dist/partner.d.ts +59 -0
- package/dist/partner.js +57 -0
- package/dist/realtime.d.ts +19 -0
- package/dist/realtime.js +10 -0
- package/dist/request.d.ts +112 -0
- package/dist/request.js +187 -0
- package/dist/signer.d.ts +51 -0
- package/dist/signer.js +90 -0
- package/dist/stream.d.ts +133 -0
- package/dist/stream.js +318 -0
- package/dist/totalis.d.ts +656 -0
- package/dist/totalis.js +1160 -0
- package/dist/typed-data.d.ts +330 -0
- package/dist/typed-data.js +340 -0
- package/dist/ui.d.ts +54 -0
- package/dist/ui.js +77 -0
- package/dist/wallet-session.d.ts +45 -0
- package/dist/wallet-session.js +61 -0
- package/examples/browser-wallet.html +69 -0
- package/examples/browser-wallet.mjs +57 -0
- package/examples/node-bot.mjs +57 -0
- package/examples/onboard.mjs +132 -0
- package/examples/partner-frontend.mjs +44 -0
- package/examples/partner-server.mjs +22 -0
- package/package.json +104 -4
package/dist/maker.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { audienceClient, headerRecord } from "./base.js";
|
|
2
|
+
import { ApiError, randomBytes32, resendable, sendOnce, singleFetch, sleep, unwrap, uuidv7 } from "./request.js";
|
|
3
|
+
import { toTotalisSigner } from "./signer.js";
|
|
4
|
+
import { items } from "./pagination.js";
|
|
5
|
+
import { TotalisStream } from "./stream.js";
|
|
6
|
+
import { follow } from "./totalis.js";
|
|
7
|
+
import { address, cashoutTransferTypedData, hashTypedData, quoteTypedData, sellBackTypedData } from "./typed-data.js";
|
|
8
|
+
import { calldata, controllerTransactions } from "./controller.js";
|
|
9
|
+
export { controllerTransactions, encodeCall, openMakerVaultTransaction } from "./controller.js";
|
|
10
|
+
export function createMakerClient(environment, options) {
|
|
11
|
+
return audienceClient(environment, options);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `quote` or `bid` on an RFQ where a taker's acceptance of this maker's quote
|
|
15
|
+
* awaits its answer. Publishing would replace that quote, which the venue
|
|
16
|
+
* refuses with 409 `STATE_CONFLICT`. Nothing was published: answer the
|
|
17
|
+
* acceptance (`confirm`, or `onAcceptance`) first, or let its deadline pass.
|
|
18
|
+
*/
|
|
19
|
+
export class AcceptancePendingError extends Error {
|
|
20
|
+
rfqId;
|
|
21
|
+
quoteId;
|
|
22
|
+
deadline;
|
|
23
|
+
name = "AcceptancePendingError";
|
|
24
|
+
code = "ACCEPTANCE_PENDING";
|
|
25
|
+
constructor(rfqId, quoteId, deadline) {
|
|
26
|
+
super(`an acceptance of quote ${quoteId} on RFQ ${rfqId} awaits this maker's answer until ${deadline}`);
|
|
27
|
+
this.rfqId = rfqId;
|
|
28
|
+
this.quoteId = quoteId;
|
|
29
|
+
this.deadline = deadline;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// An opening event older than this arrived by replay, not live fanout.
|
|
33
|
+
const REPLAYED_AFTER_MS = 2_000;
|
|
34
|
+
/** An in-memory store that drops entries a minute after they expire. */
|
|
35
|
+
export function memoryPublished() {
|
|
36
|
+
const entries = new Map();
|
|
37
|
+
return {
|
|
38
|
+
record(digest, expiry) {
|
|
39
|
+
const stale = Date.now() / 1000 - 60;
|
|
40
|
+
for (const [known, until] of entries)
|
|
41
|
+
if (until < stale)
|
|
42
|
+
entries.delete(known);
|
|
43
|
+
entries.set(digest, expiry);
|
|
44
|
+
},
|
|
45
|
+
forget: (digest) => void entries.delete(digest),
|
|
46
|
+
expiry: (digest) => entries.get(digest),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const randomSalt = () => BigInt(randomBytes32()).toString();
|
|
50
|
+
const expiryIn = (seconds) => String(Math.floor(Date.now() / 1000) + seconds);
|
|
51
|
+
/**
|
|
52
|
+
* The maker SDK over the public maker surface (the one Totalis MM uses):
|
|
53
|
+
* publish unsigned entry and cash-out quotes on one route, then confirm the
|
|
54
|
+
* exact terms a taker accepted when QUOTE_ACCEPTED arrives.
|
|
55
|
+
*/
|
|
56
|
+
export function createMaker(options) {
|
|
57
|
+
const { environment, apiKey, makerId, signer: signerInput, stream: streamOptions, streamBaseUrl, ...clientOptions } = options;
|
|
58
|
+
const client = createMakerClient(environment, { ...clientOptions, accessToken: apiKey });
|
|
59
|
+
const recoveryFetch = singleFetch(clientOptions.fetch);
|
|
60
|
+
const signer = toTotalisSigner(signerInput);
|
|
61
|
+
const stream = new TotalisStream({
|
|
62
|
+
environment, accessToken: apiKey, headers: headerRecord(clientOptions.headers), ...streamOptions,
|
|
63
|
+
subscription: { ...streamOptions?.subscription, type: "maker", maker_id: makerId },
|
|
64
|
+
...(clientOptions.baseUrl === undefined ? {} : { baseUrl: clientOptions.baseUrl }),
|
|
65
|
+
...(streamBaseUrl === undefined ? {} : { streamBaseUrl }),
|
|
66
|
+
});
|
|
67
|
+
let deploymentRead;
|
|
68
|
+
// Acceptances still before their deadline and not yet answered, from the
|
|
69
|
+
// snapshot, the acceptances GET and QUOTE_ACCEPTED, so a handler attached
|
|
70
|
+
// after the stream is live still answers what it already delivered.
|
|
71
|
+
const pending = new Map();
|
|
72
|
+
/** This maker's unanswered acceptance on the RFQ, if any. Replacing or cancelling its quote would withdraw it. */
|
|
73
|
+
const awaiting = (rfqId) => {
|
|
74
|
+
for (const [id, acceptance] of pending) {
|
|
75
|
+
// Deadlines are venue time: a fast local clock must not drop a live acceptance.
|
|
76
|
+
if (Date.parse(acceptance.deadline) <= stream.serverNow())
|
|
77
|
+
pending.delete(id);
|
|
78
|
+
else if (acceptance.rfq_id === rfqId)
|
|
79
|
+
return acceptance;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
};
|
|
83
|
+
const refuseWhileAwaiting = (rfqId) => {
|
|
84
|
+
const acceptance = awaiting(rfqId);
|
|
85
|
+
if (acceptance !== undefined)
|
|
86
|
+
throw new AcceptancePendingError(rfqId, acceptance.quote_id, acceptance.deadline);
|
|
87
|
+
};
|
|
88
|
+
// Resolves once the acceptances a truncated snapshot left out have been paged
|
|
89
|
+
// in, and rejects while that read has failed: until then they are not known.
|
|
90
|
+
let acceptancesPaged = Promise.resolve();
|
|
91
|
+
let pagingFailed = false;
|
|
92
|
+
const pageAcceptances = () => {
|
|
93
|
+
pagingFailed = false;
|
|
94
|
+
const paging = continueAcceptances();
|
|
95
|
+
acceptancesPaged = paging;
|
|
96
|
+
paging.catch((error) => {
|
|
97
|
+
if (acceptancesPaged === paging)
|
|
98
|
+
pagingFailed = true;
|
|
99
|
+
handlers.forEach(({ failed }) => failed(error));
|
|
100
|
+
});
|
|
101
|
+
return paging;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Pending acceptances are known: the stream is live and any truncated
|
|
105
|
+
* acceptances are paged in. A failed page read is read again here, and
|
|
106
|
+
* rejects again if it fails, so nothing is quoted while acceptances are unknown.
|
|
107
|
+
*/
|
|
108
|
+
const acceptancesKnown = async () => {
|
|
109
|
+
await stream.live();
|
|
110
|
+
if (pagingFailed)
|
|
111
|
+
pageAcceptances();
|
|
112
|
+
await acceptancesPaged;
|
|
113
|
+
};
|
|
114
|
+
// Called with an RFQ whose acceptance was answered, so a held RFQ is offered again at once.
|
|
115
|
+
const released = new Set();
|
|
116
|
+
// Answered acceptances until their deadline, so a copy still being replayed or paged does not hold the RFQ again.
|
|
117
|
+
const answeredUntil = new Map();
|
|
118
|
+
/** `filled`: the answer filled the RFQ, so it is no longer open to quote. */
|
|
119
|
+
const answered = (acceptance, filled = false) => {
|
|
120
|
+
answeredUntil.set(acceptance.quote_id, Date.parse(acceptance.deadline));
|
|
121
|
+
if (pending.delete(acceptance.quote_id))
|
|
122
|
+
released.forEach((release) => release(acceptance.rfq_id, filled));
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Record a digest as signable before publishing it (a lost response may
|
|
126
|
+
* still have published it), and forget it when the venue definitely rejects it.
|
|
127
|
+
*/
|
|
128
|
+
async function publish(rfqId, digest, expiry, post) {
|
|
129
|
+
// An acceptance may have arrived while the terms were prepared.
|
|
130
|
+
refuseWhileAwaiting(rfqId);
|
|
131
|
+
await published.record(digest, expiry);
|
|
132
|
+
try {
|
|
133
|
+
await post();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!resendable(error))
|
|
137
|
+
await published.forget(digest);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const published = options.published ?? memoryPublished();
|
|
142
|
+
const deployment = () => (deploymentRead ??= client.GET("/v1/deployment").then(unwrap).catch((error) => {
|
|
143
|
+
deploymentRead = undefined;
|
|
144
|
+
throw error;
|
|
145
|
+
}));
|
|
146
|
+
// Fees can change by governance and accepts require the current ones, so
|
|
147
|
+
// every quote reads them; only the deployment identity is cached.
|
|
148
|
+
async function vaultFees() {
|
|
149
|
+
const { fee_bps, taker_fee_bps } = unwrap(await client.GET("/v1/deployment"));
|
|
150
|
+
if (fee_bps === null || taker_fee_bps === null)
|
|
151
|
+
throw new Error("the venue has not published the vault's fees yet");
|
|
152
|
+
return { fee_bps, taker_fee_bps };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Publish an unsigned quote on an entry RFQ; it replaces this maker's
|
|
156
|
+
* previous quote there. Nothing is signed until a taker accepts it. The
|
|
157
|
+
* fees are the vault's current ones, read from GET /v1/deployment for this quote.
|
|
158
|
+
* Throws `AcceptancePendingError`, publishing nothing, while a taker's
|
|
159
|
+
* acceptance of this maker's quote on the RFQ awaits an answer: replacing
|
|
160
|
+
* the quote would withdraw that acceptance. It starts the maker stream if
|
|
161
|
+
* needed and waits until it is live, so the pending acceptances are known.
|
|
162
|
+
*/
|
|
163
|
+
async function quote(rfq, terms) {
|
|
164
|
+
await acceptancesKnown();
|
|
165
|
+
refuseWhileAwaiting(rfq.rfq_id);
|
|
166
|
+
const [chain, fees] = await Promise.all([deployment(), vaultFees()]);
|
|
167
|
+
const entry = {
|
|
168
|
+
maker_id: makerId, legs: [...rfq.legs].sort((a, b) => Number(a.outcome_id) - Number(b.outcome_id)), stake: rfq.stake, payout: String(terms.payout),
|
|
169
|
+
fee_bps: fees.fee_bps, taker_fee_bps: fees.taker_fee_bps, taker: rfq.taker.toLowerCase(),
|
|
170
|
+
expiry: expiryIn(terms.lifetimeSeconds ?? 20), salt: terms.salt ?? randomSalt(),
|
|
171
|
+
};
|
|
172
|
+
const quote_id = hashTypedData(quoteTypedData(chain, entry));
|
|
173
|
+
await publish(rfq.rfq_id, quote_id, Number(entry.expiry), async () => unwrap(await client.POST("/v1/rfqs/{rfq_id}/quotes", {
|
|
174
|
+
params: { path: { rfq_id: rfq.rfq_id }, header: { "Idempotency-Key": uuidv7() } }, body: { quote_id, entry },
|
|
175
|
+
})));
|
|
176
|
+
return { quoteId: quote_id, entry };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Publish an unsigned cash-out quote on the same route as entry quotes: a
|
|
180
|
+
* sell-back when this maker wrote the position (`rfq.maker_id` is this
|
|
181
|
+
* maker), paid from the maker vault; otherwise a transfer to this maker's
|
|
182
|
+
* signer, paid from that wallet's own account balance in the vault, not the
|
|
183
|
+
* maker vault. The venue refuses a transfer that balance cannot pay with
|
|
184
|
+
* `INVALID_REQUEST` naming `transfer.price`, so bid only on your own
|
|
185
|
+
* positions unless it is funded. The owner signs it before accepting, so
|
|
186
|
+
* confirming it executes it. Throws `AcceptancePendingError` like `quote`.
|
|
187
|
+
*/
|
|
188
|
+
async function bid(rfq, terms) {
|
|
189
|
+
await acceptancesKnown();
|
|
190
|
+
refuseWhileAwaiting(rfq.rfq_id);
|
|
191
|
+
const chain = await deployment();
|
|
192
|
+
const common = { position_id: rfq.position_id, price: String(terms.price), expiry: expiryIn(terms.lifetimeSeconds ?? 45), salt: terms.salt ?? randomSalt(), leg_state: rfq.leg_state };
|
|
193
|
+
let command;
|
|
194
|
+
if (rfq.maker_id === makerId) {
|
|
195
|
+
const sell_back = { ...common, owner: rfq.owner };
|
|
196
|
+
command = { quote_id: hashTypedData(sellBackTypedData(chain, sell_back)), sell_back };
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const transfer = { ...common, seller: rfq.owner, buyer: await signer.address() };
|
|
200
|
+
command = { quote_id: hashTypedData(cashoutTransferTypedData(chain, transfer)), transfer };
|
|
201
|
+
}
|
|
202
|
+
await publish(rfq.rfq_id, command.quote_id, Number(common.expiry), async () => unwrap(await client.POST("/v1/rfqs/{rfq_id}/quotes", {
|
|
203
|
+
params: { path: { rfq_id: rfq.rfq_id }, header: { "Idempotency-Key": uuidv7() } }, body: command,
|
|
204
|
+
})));
|
|
205
|
+
return { quoteId: command.quote_id, ...command };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Cancel a published quote. `ACCEPTED` means this maker already confirmed it:
|
|
209
|
+
* it is filled, and there is nothing left to answer. While a taker's
|
|
210
|
+
* acceptance of it awaits an answer the venue refuses the cancel with 409
|
|
211
|
+
* `STATE_CONFLICT`: turn the acceptance down with
|
|
212
|
+
* `confirm(acceptance, "DECLINE")` instead.
|
|
213
|
+
*/
|
|
214
|
+
async function cancelQuote(quoteId) {
|
|
215
|
+
const response = await client.DELETE("/v1/quotes/{quote_id}", { params: { path: { quote_id: quoteId } } });
|
|
216
|
+
// A quote this maker already confirmed answers 409 with {status: "ACCEPTED"}: a result, not a failure.
|
|
217
|
+
const body = response.error;
|
|
218
|
+
if (response.response.status === 409 && typeof body === "object" && body !== null && "status" in body && body.status === "ACCEPTED")
|
|
219
|
+
return { status: "ACCEPTED" };
|
|
220
|
+
const result = unwrap(response);
|
|
221
|
+
// Cancelled or replaced can no longer be accepted, so it must no longer be signable.
|
|
222
|
+
if (result.status !== "ACCEPTED") {
|
|
223
|
+
await published.forget(quoteId);
|
|
224
|
+
const acceptance = pending.get(quoteId);
|
|
225
|
+
if (acceptance !== undefined)
|
|
226
|
+
answered(acceptance);
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Arm or renew this maker's quote lease: its open quotes stand only while the
|
|
232
|
+
* lease is live. If `leaseMs` passes without another renewal, the venue
|
|
233
|
+
* cancels every open quote of this maker that no taker is accepting, and each
|
|
234
|
+
* one it publishes after, until the lease is renewed or released. Takers see
|
|
235
|
+
* QUOTE_REMOVED with status CANCELLED. Without a lease nothing changes.
|
|
236
|
+
*/
|
|
237
|
+
async function renewQuoteLease(leaseMs) {
|
|
238
|
+
return unwrap(await client.PUT("/v1/makers/{maker_id}/quote-lease", { params: { path: { maker_id: makerId } }, body: { lease_ms: leaseMs } }));
|
|
239
|
+
}
|
|
240
|
+
/** Disarm the quote lease: open quotes then stand until their own expiry. `released` is false if none was armed. */
|
|
241
|
+
async function releaseQuoteLease() {
|
|
242
|
+
return unwrap(await client.DELETE("/v1/makers/{maker_id}/quote-lease", { params: { path: { maker_id: makerId } } }));
|
|
243
|
+
}
|
|
244
|
+
let stopLease;
|
|
245
|
+
/**
|
|
246
|
+
* Renew the quote lease now and every third of `leaseMs` until the returned
|
|
247
|
+
* stop, `close()` or another `keepQuoteLease` call. If this process dies or
|
|
248
|
+
* loses the venue, the lease lapses and its quotes are withdrawn. Stopping
|
|
249
|
+
* leaves the lease to lapse; `releaseQuoteLease()` keeps quotes standing
|
|
250
|
+
* without one. `onError` gets each failed renewal; renewals continue.
|
|
251
|
+
*/
|
|
252
|
+
function keepQuoteLease(leaseMs, onError = () => undefined) {
|
|
253
|
+
stopLease?.();
|
|
254
|
+
let timer;
|
|
255
|
+
let stopped = false;
|
|
256
|
+
const renew = () => void renewQuoteLease(leaseMs).catch(onError).finally(() => {
|
|
257
|
+
if (!stopped)
|
|
258
|
+
timer = setTimeout(renew, leaseMs / 3);
|
|
259
|
+
});
|
|
260
|
+
const stop = () => {
|
|
261
|
+
stopped = true;
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
if (stopLease === stop)
|
|
264
|
+
stopLease = undefined;
|
|
265
|
+
};
|
|
266
|
+
stopLease = stop;
|
|
267
|
+
renew();
|
|
268
|
+
return stop;
|
|
269
|
+
}
|
|
270
|
+
/** The exact terms an acceptance asks this maker to sign, checked against the quote ID. */
|
|
271
|
+
async function termsFor(acceptance) {
|
|
272
|
+
const chain = await deployment();
|
|
273
|
+
const typedData = acceptance.kind === "ENTRY" ? quoteTypedData(chain, acceptance.entry)
|
|
274
|
+
: acceptance.kind === "SELL_BACK" ? sellBackTypedData(chain, acceptance.sell_back)
|
|
275
|
+
: cashoutTransferTypedData(chain, acceptance.transfer);
|
|
276
|
+
if (hashTypedData(typedData) !== acceptance.quote_id)
|
|
277
|
+
throw new Error("acceptance terms do not match their quote ID");
|
|
278
|
+
// The quote signer signs only terms this maker published and that are still live.
|
|
279
|
+
const expiry = await published.expiry(acceptance.quote_id);
|
|
280
|
+
if (expiry === undefined || expiry * 1000 <= Date.now())
|
|
281
|
+
throw new Error("acceptance is not for a live quote this maker published");
|
|
282
|
+
return typedData;
|
|
283
|
+
}
|
|
284
|
+
// Each delivery also goes to every handler.
|
|
285
|
+
const handlers = new Set();
|
|
286
|
+
const deliver = (acceptance) => {
|
|
287
|
+
for (const [id, { deadline }] of pending)
|
|
288
|
+
if (Date.parse(deadline) <= stream.serverNow())
|
|
289
|
+
pending.delete(id);
|
|
290
|
+
for (const [id, deadline] of answeredUntil)
|
|
291
|
+
if (deadline <= stream.serverNow())
|
|
292
|
+
answeredUntil.delete(id);
|
|
293
|
+
if (Date.parse(acceptance.deadline) > stream.serverNow() && !answeredUntil.has(acceptance.quote_id))
|
|
294
|
+
pending.set(acceptance.quote_id, acceptance);
|
|
295
|
+
handlers.forEach(({ dispatch }) => dispatch(acceptance));
|
|
296
|
+
};
|
|
297
|
+
const continueAcceptances = async () => {
|
|
298
|
+
const read = (query) => client.GET("/v1/makers/{maker_id}/acceptances", { params: { path: { maker_id: makerId }, query: { ...query, limit: 200 } } }).then(unwrap);
|
|
299
|
+
for await (const acceptance of items(read, (page) => page.acceptances))
|
|
300
|
+
deliver(acceptance);
|
|
301
|
+
};
|
|
302
|
+
stream.on("reset", () => pending.clear());
|
|
303
|
+
// A resumed stream replays events with nothing marking their end (a maker
|
|
304
|
+
// stream has no publication freshness), so it is live before any replayed
|
|
305
|
+
// QUOTE_ACCEPTED is applied. Without a snapshot on this connection, the
|
|
306
|
+
// pending acceptances are read in full before anything is quoted.
|
|
307
|
+
let snapshotted = false;
|
|
308
|
+
stream.on("status", (status) => {
|
|
309
|
+
if (status !== "live")
|
|
310
|
+
snapshotted = false;
|
|
311
|
+
else if (!snapshotted)
|
|
312
|
+
void pageAcceptances().catch(() => undefined);
|
|
313
|
+
});
|
|
314
|
+
stream.on("snapshot", (snapshot) => {
|
|
315
|
+
snapshotted = true;
|
|
316
|
+
(snapshot.resources.acceptances ?? []).forEach(deliver);
|
|
317
|
+
if (!(snapshot.truncated ?? []).some((truncation) => truncation.resource === "acceptances"))
|
|
318
|
+
return;
|
|
319
|
+
void pageAcceptances().catch(() => undefined);
|
|
320
|
+
});
|
|
321
|
+
stream.on("event", (event) => void (event.event_type === "QUOTE_ACCEPTED" && deliver(event.payload)));
|
|
322
|
+
// Each acceptance's answer is decided, signed and built once, registered before any await so
|
|
323
|
+
// concurrent callers share it; asking again resends those exact bytes.
|
|
324
|
+
const answers = new Map();
|
|
325
|
+
function answer(acceptance, decide) {
|
|
326
|
+
let entry = answers.get(acceptance.quote_id);
|
|
327
|
+
if (entry === undefined) {
|
|
328
|
+
const built = (async () => {
|
|
329
|
+
const decision = await decide();
|
|
330
|
+
const command = decision === "DECLINE"
|
|
331
|
+
? { decision }
|
|
332
|
+
: { decision, maker_signature: await signer.signTypedData(await termsFor(acceptance)) };
|
|
333
|
+
// A quote has at most one acceptance, so the quote ID alone keys the answer.
|
|
334
|
+
const params = { path: { quote_id: acceptance.quote_id } };
|
|
335
|
+
return async () => {
|
|
336
|
+
const result = await sendOnce(() => client.POST("/v1/quotes/{quote_id}/confirm", { params, body: command }));
|
|
337
|
+
// Answered: unless it filled the RFQ, its quotes may change again.
|
|
338
|
+
answered(acceptance, result.status === "ACCEPTED" || result.status === "FILLED");
|
|
339
|
+
return result;
|
|
340
|
+
};
|
|
341
|
+
})();
|
|
342
|
+
entry = { until: Date.parse(acceptance.deadline), built };
|
|
343
|
+
answers.set(acceptance.quote_id, entry);
|
|
344
|
+
// Nothing was sent if deciding or signing failed; a later delivery may decide again.
|
|
345
|
+
built.catch(() => answers.delete(acceptance.quote_id));
|
|
346
|
+
for (const [id, { until }] of answers)
|
|
347
|
+
if (until < Date.now() - 60_000)
|
|
348
|
+
answers.delete(id);
|
|
349
|
+
}
|
|
350
|
+
return entry.built.then((send) => send());
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Answer an acceptance before its deadline. CONFIRM signs the exact accepted
|
|
354
|
+
* terms with the quote signer. The first answer for a quote is final:
|
|
355
|
+
* calling again, or `NetworkError.resend()`, resends the identical request,
|
|
356
|
+
* which the venue answers with the stored result.
|
|
357
|
+
*/
|
|
358
|
+
function confirm(acceptance, decision = "CONFIRM") {
|
|
359
|
+
return answer(acceptance, () => decision);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Handle every pending acceptance: from QUOTE_ACCEPTED, live or replayed by
|
|
363
|
+
* the stream after a reconnect with a cursor, and the snapshot's
|
|
364
|
+
* `acceptances` (paging GET /v1/makers/{maker_id}/acceptances when the
|
|
365
|
+
* snapshot truncated them), including acceptances the stream delivered
|
|
366
|
+
* before it was attached. The handler decides once per quote; if every send
|
|
367
|
+
* of the answer is lost, a redelivery before the deadline resends that same
|
|
368
|
+
* answer. `onError` receives the acceptance, or none when paging the
|
|
369
|
+
* truncated acceptances failed.
|
|
370
|
+
*/
|
|
371
|
+
function onAcceptance(handler, onError = () => undefined) {
|
|
372
|
+
const busy = new Set();
|
|
373
|
+
const answered = new Set();
|
|
374
|
+
const dispatch = (request) => {
|
|
375
|
+
const id = request.quote_id;
|
|
376
|
+
if (busy.has(id) || answered.has(id) || Date.parse(request.deadline) <= Date.now())
|
|
377
|
+
return;
|
|
378
|
+
busy.add(id);
|
|
379
|
+
void answer(request, () => handler(request)).then(() => answered.add(id), (error) => {
|
|
380
|
+
// A lost or backed-off answer is resent unchanged by the next delivery.
|
|
381
|
+
if (!resendable(error))
|
|
382
|
+
answered.add(id);
|
|
383
|
+
onError(error, request);
|
|
384
|
+
}).finally(() => busy.delete(id));
|
|
385
|
+
};
|
|
386
|
+
const entry = { dispatch, failed: (error) => onError(error) };
|
|
387
|
+
handlers.add(entry);
|
|
388
|
+
stream.start();
|
|
389
|
+
[...pending.values()].forEach(dispatch);
|
|
390
|
+
return () => void handlers.delete(entry);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Every open RFQ once per subscription: from RFQ_CREATED, the snapshot's
|
|
394
|
+
* open RFQs, and a paged `GET /v1/rfqs` after a reconnect that replayed
|
|
395
|
+
* instead of snapshotting (or whose snapshot was truncated). Each call keeps
|
|
396
|
+
* its own in-memory handled set, so another subscription or a restart
|
|
397
|
+
* delivers the still-open RFQs again; quoting one again replaces this
|
|
398
|
+
* maker's previous quote on it, so a repeat cannot fill twice. An RFQ where
|
|
399
|
+
* a taker's acceptance of this maker's quote awaits an answer is held back
|
|
400
|
+
* until that acceptance is answered or its deadline passes, so quoting it
|
|
401
|
+
* again does not withdraw an acceptance this maker already knows of.
|
|
402
|
+
*/
|
|
403
|
+
function onRfq(handler, onError = () => undefined) {
|
|
404
|
+
const seen = new Set();
|
|
405
|
+
// RFQs that ended since a snapshot or held delivery captured them open.
|
|
406
|
+
const ended = new Set();
|
|
407
|
+
const held = new Map();
|
|
408
|
+
const release = (rfqId, filled = false) => {
|
|
409
|
+
if (filled)
|
|
410
|
+
ended.add(rfqId);
|
|
411
|
+
const entry = held.get(rfqId);
|
|
412
|
+
if (entry === undefined)
|
|
413
|
+
return;
|
|
414
|
+
clearTimeout(entry.timer);
|
|
415
|
+
held.delete(rfqId);
|
|
416
|
+
call(entry.rfq);
|
|
417
|
+
};
|
|
418
|
+
const call = (rfq) => {
|
|
419
|
+
if (stream.status === "closed" || ended.has(rfq.rfq_id) || seen.has(rfq.rfq_id) || held.has(rfq.rfq_id) || !(Date.parse(rfq.expires_at) > stream.serverNow()))
|
|
420
|
+
return;
|
|
421
|
+
const acceptance = awaiting(rfq.rfq_id);
|
|
422
|
+
if (acceptance !== undefined) {
|
|
423
|
+
// Offered again once the acceptance is answered, or when its deadline passes.
|
|
424
|
+
held.set(rfq.rfq_id, { rfq, timer: setTimeout(() => release(rfq.rfq_id), Math.max(Date.parse(acceptance.deadline) - stream.serverNow(), 0) + 50) });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
seen.add(rfq.rfq_id);
|
|
428
|
+
if (seen.size > 10_000)
|
|
429
|
+
seen.delete(seen.values().next().value);
|
|
430
|
+
// A failed handler leaves the RFQ deliverable again by a later snapshot or recovery read.
|
|
431
|
+
const failed = (error) => {
|
|
432
|
+
seen.delete(rfq.rfq_id);
|
|
433
|
+
onError(error);
|
|
434
|
+
};
|
|
435
|
+
try {
|
|
436
|
+
void Promise.resolve(handler(rfq)).catch(failed);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
failed(error);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
// A live RFQ_CREATED carries the whole open RFQ; its event type is the
|
|
443
|
+
// status. A replayed one may have been accepted or cancelled since, so it is
|
|
444
|
+
// read once more and delivered only if still open.
|
|
445
|
+
const opened = async (event) => {
|
|
446
|
+
if (event.event_type !== "RFQ_CREATED")
|
|
447
|
+
return;
|
|
448
|
+
const now = stream.serverNow();
|
|
449
|
+
if (!(Date.parse(event.payload.expires_at) > now) || seen.has(event.payload.rfq_id))
|
|
450
|
+
return;
|
|
451
|
+
if (now - Date.parse(event.occurred_at) <= REPLAYED_AFTER_MS)
|
|
452
|
+
return call({ ...event.payload, status: "OPEN" });
|
|
453
|
+
const current = unwrap(await client.GET("/v1/rfqs/{rfq_id}", { params: { path: { rfq_id: event.payload.rfq_id } } }));
|
|
454
|
+
if (current.status === "OPEN")
|
|
455
|
+
call(current);
|
|
456
|
+
};
|
|
457
|
+
let covered = false;
|
|
458
|
+
let stopped = false;
|
|
459
|
+
const afterAcceptances = (deliver) => {
|
|
460
|
+
if (stopped)
|
|
461
|
+
return;
|
|
462
|
+
void acceptancesKnown().then(() => {
|
|
463
|
+
if (!stopped)
|
|
464
|
+
deliver();
|
|
465
|
+
}, (error) => {
|
|
466
|
+
if (stopped)
|
|
467
|
+
return;
|
|
468
|
+
onError(error);
|
|
469
|
+
if (stream.status !== "closed")
|
|
470
|
+
setTimeout(() => afterAcceptances(deliver), 1_000);
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
const recover = async () => {
|
|
474
|
+
for await (const rfq of items((query) => client.GET("/v1/rfqs", { params: { query: { ...query, limit: 100 } } }).then(unwrap), (page) => page.rfqs)) {
|
|
475
|
+
if (rfq.status === "OPEN")
|
|
476
|
+
call(rfq);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const offs = [
|
|
480
|
+
stream.on("event", (event) => {
|
|
481
|
+
if (event.event_type === "RFQ_ACCEPTED" || event.event_type === "RFQ_CANCELLED" || event.event_type === "RFQ_FAILED") {
|
|
482
|
+
ended.add(event.payload.rfq_id);
|
|
483
|
+
if (ended.size > 10_000)
|
|
484
|
+
ended.delete(ended.values().next().value);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
afterAcceptances(() => void opened(event).catch(onError));
|
|
488
|
+
}),
|
|
489
|
+
stream.on("snapshot", (snapshot) => {
|
|
490
|
+
const resources = snapshot.resources;
|
|
491
|
+
// The acceptance listener ran first; a truncated snapshot's other acceptances are paged in before any RFQ is offered.
|
|
492
|
+
afterAcceptances(() => (resources.open_rfqs ?? []).forEach(call));
|
|
493
|
+
covered = !(snapshot.truncated ?? []).some((truncation) => truncation.resource === "open_rfqs");
|
|
494
|
+
}),
|
|
495
|
+
stream.on("status", (status) => {
|
|
496
|
+
if (status === "live" && !covered)
|
|
497
|
+
afterAcceptances(() => void recover().catch(onError));
|
|
498
|
+
if (status !== "live")
|
|
499
|
+
covered = false;
|
|
500
|
+
}),
|
|
501
|
+
];
|
|
502
|
+
stream.start();
|
|
503
|
+
if (stream.status === "live")
|
|
504
|
+
afterAcceptances(() => void recover().catch(onError));
|
|
505
|
+
released.add(release);
|
|
506
|
+
return () => {
|
|
507
|
+
stopped = true;
|
|
508
|
+
offs.forEach((off) => off());
|
|
509
|
+
released.delete(release);
|
|
510
|
+
held.forEach(({ timer }) => clearTimeout(timer));
|
|
511
|
+
held.clear();
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/** Vault transactions for this maker, for its controller wallet to sign and send. */
|
|
515
|
+
const controller = async () => {
|
|
516
|
+
const { owner } = unwrap(await client.GET("/v1/makers/{maker_id}/capital", { params: { path: { maker_id: makerId } } }));
|
|
517
|
+
return controllerTransactions(await deployment(), makerId, owner);
|
|
518
|
+
};
|
|
519
|
+
/**
|
|
520
|
+
* The pending vault withdrawal, if any, and what would block executing it
|
|
521
|
+
* now. Send `controller()`'s `executeWithdraw` once `pending.executable`.
|
|
522
|
+
*/
|
|
523
|
+
const withdrawal = async () => unwrap(await client.GET("/v1/makers/{maker_id}/withdrawal", { params: { path: { maker_id: makerId } } }));
|
|
524
|
+
const settled = new Set(["NO_IMPROVEMENT", "FAILED"]);
|
|
525
|
+
const operationDone = new Set(["COMMITTED", "REVERTED", "EXPIRED_UNEXECUTED"]);
|
|
526
|
+
/**
|
|
527
|
+
* Ask for a collateral reduction and follow the job on the maker stream until
|
|
528
|
+
* it needs nothing more from the maker: NO_IMPROVEMENT, FAILED, or DURABLE
|
|
529
|
+
* with the attestation transaction ready (or already executed). A job stays
|
|
530
|
+
* QUEUED until `next_eligible_at`. With `signTransaction`, a wallet that
|
|
531
|
+
* holds HYPE for the gas (the maker wallet, or any other) signs the returned
|
|
532
|
+
* call as a raw EIP-1559 transaction, and it is submitted through the
|
|
533
|
+
* self-funded route. Without it, the job is returned with its `transaction`
|
|
534
|
+
* and `idempotencyKey`: sign the transaction yourself and submit it to
|
|
535
|
+
* `POST /v1/makers/{maker_id}/operations/{operation_id}/self-funded-transaction`
|
|
536
|
+
* under that same `Idempotency-Key`, the only one the route admits for this job.
|
|
537
|
+
*/
|
|
538
|
+
async function reduceCollateral(request = {}) {
|
|
539
|
+
const path = { maker_id: makerId };
|
|
540
|
+
// The self-funded route admits the attestation only under the key that created the job.
|
|
541
|
+
const header = { "Idempotency-Key": uuidv7() };
|
|
542
|
+
const { job_id } = await sendOnce(() => client.POST("/v1/makers/{maker_id}/collateral-reductions", { params: { path, header } }));
|
|
543
|
+
const read = async (signal) => unwrap(await client.GET("/v1/makers/{maker_id}/collateral-reductions/{job_id}", { params: { path: { maker_id: makerId, job_id } }, signal, fetch: recoveryFetch }));
|
|
544
|
+
const ready = (job) => settled.has(job.status) ||
|
|
545
|
+
(job.status === "DURABLE" && (job.transaction !== null || (job.operation_status !== null && operationDone.has(job.operation_status))));
|
|
546
|
+
const job = await follow(stream, read, ready, (event) => (event.event_type === "COLLATERAL_REDUCTION_UPDATED" && event.payload.job_id === job_id) ||
|
|
547
|
+
event.event_type === "OPERATION_UPDATED", request.signal, clientOptions.retry || undefined);
|
|
548
|
+
const idempotencyKey = header["Idempotency-Key"];
|
|
549
|
+
if (job.transaction === null || job.operation_id === null || request.signTransaction === undefined)
|
|
550
|
+
return { job, idempotencyKey };
|
|
551
|
+
const { chain_id, to, data } = job.transaction;
|
|
552
|
+
const signed_transaction = (await request.signTransaction({ chain_id, to: address(to), data: calldata(data), value: "0" })).toLowerCase();
|
|
553
|
+
const submitted = await sendOnce(() => client.POST("/v1/makers/{maker_id}/operations/{operation_id}/self-funded-transaction", {
|
|
554
|
+
params: { path: { maker_id: makerId, operation_id: job.operation_id }, header }, body: { signed_transaction },
|
|
555
|
+
}));
|
|
556
|
+
return { job, idempotencyKey, submitted };
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
client, stream, deployment, quote, bid, cancelQuote, renewQuoteLease, releaseQuoteLease, keepQuoteLease, confirm, onAcceptance, onRfq, controller, withdrawal, reduceCollateral,
|
|
560
|
+
close: () => {
|
|
561
|
+
stopLease?.();
|
|
562
|
+
stream.close();
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Read the account's maker (`GET /v1/me/maker`) and, once indexed or
|
|
568
|
+
* `PENDING`, admit it (`POST /v1/me/maker`, unless `admit: false`). Admission needs no signature
|
|
569
|
+
* and repeats safely. The POST's 404 (no maker indexed yet) is `NOT_INDEXED`,
|
|
570
|
+
* not an error.
|
|
571
|
+
*/
|
|
572
|
+
export async function makerReadiness(client, options = {}) {
|
|
573
|
+
const init = options.signal === undefined ? {} : { signal: options.signal };
|
|
574
|
+
let { ownership } = unwrap(await client.GET("/v1/me/maker", init));
|
|
575
|
+
// A self-service PENDING profile is activated by the same POST; an operator decision keeps it as it is.
|
|
576
|
+
if ((ownership.state === "SETUP_REQUIRED" || ownership.state === "PENDING") && options.admit !== false) {
|
|
577
|
+
try {
|
|
578
|
+
({ ownership } = unwrap(await client.POST("/v1/me/maker", init)));
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
if (error instanceof ApiError && error.status === 404)
|
|
582
|
+
return { state: "NOT_INDEXED", ownership };
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
if (ownership.state === "SETUP_REQUIRED")
|
|
586
|
+
return { state: "NOT_INDEXED", ownership };
|
|
587
|
+
}
|
|
588
|
+
switch (ownership.state) {
|
|
589
|
+
case "READY":
|
|
590
|
+
case "PENDING":
|
|
591
|
+
case "RESTRICTED":
|
|
592
|
+
return { state: ownership.state, makerId: ownership.maker.maker_id, ownership };
|
|
593
|
+
case "NOT_CREATED":
|
|
594
|
+
return { state: "NOT_INDEXED", ownership };
|
|
595
|
+
default:
|
|
596
|
+
return { state: ownership.state, ownership };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/** `makerReadiness` again every `intervalMs` (default 3000) while `NOT_INDEXED`; resolves with the first other state. */
|
|
600
|
+
export async function waitForMaker(client, options = {}) {
|
|
601
|
+
const { intervalMs = 3_000, ...read } = options;
|
|
602
|
+
for (;;) {
|
|
603
|
+
const readiness = await makerReadiness(client, read);
|
|
604
|
+
if (readiness.state !== "NOT_INDEXED")
|
|
605
|
+
return readiness;
|
|
606
|
+
await sleep(intervalMs, options.signal);
|
|
607
|
+
}
|
|
608
|
+
}
|
package/dist/money.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { components } from "./generated/openapi.js";
|
|
2
|
+
type Schemas = components["schemas"];
|
|
3
|
+
type Balance = Schemas["Balance"];
|
|
4
|
+
type EntryTerms = Pick<Schemas["schemas-QuoteTerms"], "stake" | "payout" | "fee_bps" | "taker_fee_bps">;
|
|
5
|
+
type CashoutTerms = {
|
|
6
|
+
readonly kind: "SELL_BACK";
|
|
7
|
+
readonly sell_back: Pick<Schemas["schemas-SellBackTerms"], "price">;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "TRANSFER";
|
|
10
|
+
readonly transfer: Pick<Schemas["schemas-TransferTerms"], "price">;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Spendable Totalis cash from `GET /v1/me/balances`, in USDC base units, as
|
|
14
|
+
* the SDK plans entries and withdrawals with it. `null` means unknown (the
|
|
15
|
+
* component is not `CURRENT`), never 0: show it as unknown and read again.
|
|
16
|
+
* - `vault`: free vault cash (`VAULT_FREE`).
|
|
17
|
+
* - `wallet`: wallet USDC (`WALLET_USDC`) less `pending.wallet_reserved`
|
|
18
|
+
* (open withdrawals, stake fundings and wallet deposits not yet committed),
|
|
19
|
+
* floored at 0.
|
|
20
|
+
* - `total`: the venue's spendable `amount`; it subtracts withdrawals and
|
|
21
|
+
* stake fundings but not a pending wallet deposit, which moves into the
|
|
22
|
+
* vault, so it can exceed `vault + wallet` while a deposit is pending.
|
|
23
|
+
*/
|
|
24
|
+
export declare function spendableCash(balance: Pick<Balance, "amount" | "state" | "components" | "pending">): {
|
|
25
|
+
readonly vault: bigint | null;
|
|
26
|
+
readonly wallet: bigint | null;
|
|
27
|
+
readonly total: bigint | null;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* What the owner receives from a gross close amount (a settlement payout or a
|
|
31
|
+
* sell-back price), as the vault pays it: the profit fee is
|
|
32
|
+
* `floor((amount - netStake) * feeBps / 10000)` on the part above the
|
|
33
|
+
* position's net stake, and nothing at or below it. `feeBps` and `netStake`
|
|
34
|
+
* are the position's own (`fee_bps`, `net_stake`), fixed when it opened.
|
|
35
|
+
*/
|
|
36
|
+
export declare function netOfProfitFee(amount: bigint | string, netStake: bigint | string, feeBps: number): bigint;
|
|
37
|
+
/** A quote's net stake: the stake less the entry fee, `floor(stake * taker_fee_bps / 10000)`. */
|
|
38
|
+
export declare function netStake(terms: Pick<EntryTerms, "stake" | "taker_fee_bps">): bigint;
|
|
39
|
+
/**
|
|
40
|
+
* What the taker receives if every leg wins, net of the profit fee: the
|
|
41
|
+
* figure to show next to an entry quote, whose `payout` is gross. A fractional
|
|
42
|
+
* or partial result pays less; `netOfProfitFee` applies to whatever the
|
|
43
|
+
* position settles at. Example: stake 3 USDC at 20 bps entry fee and payout
|
|
44
|
+
* 9.18 USDC at 100 bps profit fee nets 9.118140 USDC.
|
|
45
|
+
*/
|
|
46
|
+
export declare function netPayout(quote: {
|
|
47
|
+
readonly entry: EntryTerms;
|
|
48
|
+
} | EntryTerms): bigint;
|
|
49
|
+
/**
|
|
50
|
+
* What the owner receives from a cash-out quote. A `TRANSFER` pays its price
|
|
51
|
+
* in full; a `SELL_BACK` withholds the position's profit fee on the price
|
|
52
|
+
* above its net stake. `position` is the position read (`GET
|
|
53
|
+
* /v1/me/positions/{position_id}`); `null` when its `net_stake` is unknown.
|
|
54
|
+
*/
|
|
55
|
+
export declare function netCashout(bid: CashoutTerms, position: {
|
|
56
|
+
readonly net_stake: string | null;
|
|
57
|
+
readonly fee_bps: number;
|
|
58
|
+
}): bigint | null;
|
|
59
|
+
export {};
|