@totalis/hip4-client 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -2
- package/dist/client.d.ts +1 -1
- package/dist/totalis.d.ts +39 -3
- package/dist/totalis.js +135 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
`@totalis/hip4-client` follows [semantic versioning](https://semver.org).
|
|
4
4
|
The contract version the generated types come from is `sdkVersion` in `./version`.
|
|
5
5
|
|
|
6
|
+
## 0.77.0
|
|
7
|
+
|
|
8
|
+
`openPosition` chooses its quote last and moves on to the next quote when an
|
|
9
|
+
accept is refused, declined or times out (TT-2578, TT-2574). Makers replace
|
|
10
|
+
quotes as they renew them, and an accept that took seconds to sign often found
|
|
11
|
+
its quote gone (`QUOTE_REPLACED`). Additive: 0.76 code runs unchanged, but
|
|
12
|
+
`openPosition` now sends up to `maxAttempts` accepts where it sent one or two.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `prepareEntryFunding({ stake, core, cash, sourceDex, hypercoreNonce })`
|
|
17
|
+
plans and signs an entry's funding (the wallet's USDC authorization and any
|
|
18
|
+
bridge from Core) before a quote is chosen. `signEntry({ quote, prepared })`
|
|
19
|
+
then signs only the quote, so the accept can go straight out. A prepared
|
|
20
|
+
funding is for one accept.
|
|
21
|
+
- `SignedEntry.refused`: the code of an accept the venue refused outright.
|
|
22
|
+
`recoverAcceptance` answers such a record `NOT_ADMITTED` from the record
|
|
23
|
+
alone, without reading the acceptance or cancelling the RFQ.
|
|
24
|
+
- `openPosition` options `minPayout`, `refreshMs` (default 10 s) and
|
|
25
|
+
`maxAttempts` (default 4).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `openPosition` signs the funding first and chooses the quote after, then
|
|
30
|
+
signs it and sends the accept at once. Signing the funding counts against
|
|
31
|
+
`collectMs`.
|
|
32
|
+
- On `QUOTE_REPLACED` or `QUOTE_EXPIRED`, `openPosition` hands `onSigned` the
|
|
33
|
+
record marked `refused`, then prepares the funding again and signs the best
|
|
34
|
+
other quote paying at least `minPayout` (left out: the first chosen quote's
|
|
35
|
+
payout). It keeps going until an accept is taken, `refreshMs` passes or
|
|
36
|
+
`maxAttempts` accepts were sent, and throws the last refusal. Before, it
|
|
37
|
+
retried once and took any payout.
|
|
38
|
+
- A `DECLINED` or `TIMED_OUT` accept is followed the same way, within the
|
|
39
|
+
same `refreshMs` and `maxAttempts`: the maker's answer admitted nothing and
|
|
40
|
+
the RFQ stays open. With no other quote at or above `minPayout` in time,
|
|
41
|
+
`openPosition` returns that answer, as before.
|
|
42
|
+
|
|
6
43
|
## 0.76.0
|
|
7
44
|
|
|
8
45
|
Vault agents (TT-2554), one Combo leg per question (TT-2605) and no legs on
|
package/README.md
CHANGED
|
@@ -66,8 +66,9 @@ if (opened.operationId) {
|
|
|
66
66
|
totalis.close();
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
`openPosition` creates the RFQ,
|
|
70
|
-
|
|
69
|
+
`openPosition` creates the RFQ, signs the funding for any shortfall from the wallet, then takes the best quote
|
|
70
|
+
and signs the acceptance. If the maker replaced the quote first, declined it or didn't answer, it signs the best
|
|
71
|
+
other quote paying at least the first one's payout (`minPayout`) and tries again. Only `COMMITTED` means the trade happened. A decided position settles and pays out on its own.
|
|
71
72
|
|
|
72
73
|
## Make markets
|
|
73
74
|
|
package/dist/client.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { components, operations, paths } from "./generated/client.js";
|
|
|
5
5
|
export type { AccessToken, ApiClientOptions, PublicEnvironment } from "./base.js";
|
|
6
6
|
/** The typed HTTP client for the `client` audience. `createTotalis` adds the flows. */
|
|
7
7
|
export declare function createClient(environment: PublicEnvironment, options?: ApiClientOptions): import("openapi-fetch").Client<paths, `${string}/${string}`>;
|
|
8
|
-
export { bestBid, bestQuote, createTotalis, EntryError, entryFailure, follow, type Acceptance, type AcceptanceResult, type Amount, type BridgeState, type CashoutBid, type Chooser, type Deployment, type DepositAccepted, type EntryCash, type EntryFailure, type EntryQuote, type CoreExit, type LegacySignedEntry, type Leg, type NotAdmitted, type RecordListener, type Operation, type Quote, type ResponseOf, type SellBackQuote, type SignedAcceptance, type SignedCashout, type SignedCoreExit, type SignedDeposit, type SignedEntry, type SignedReturnToCore, type Totalis, type TotalisOptions, type TransferQuote, type WalletCall, type Withdrawal, } from "./totalis.js";
|
|
8
|
+
export { bestBid, bestQuote, createTotalis, EntryError, entryFailure, follow, type Acceptance, type AcceptanceResult, type Amount, type BridgeState, type CashoutBid, type Chooser, type Deployment, type DepositAccepted, type EntryCash, type EntryFailure, type EntryQuote, type CoreExit, type LegacySignedEntry, type Leg, type PreparedEntryFunding, type NotAdmitted, type RecordListener, type Operation, type Quote, type ResponseOf, type SellBackQuote, type SignedAcceptance, type SignedCashout, type SignedCoreExit, type SignedDeposit, type SignedEntry, type SignedReturnToCore, type Totalis, type TotalisOptions, type TransferQuote, type WalletCall, type Withdrawal, } from "./totalis.js";
|
|
9
9
|
export { bridgeAction, bridgeGasBuffer, classifyHypercoreAnswer, hyperevmUsdcBridge, hyperliquidExchange, hyperliquidNetwork, hypercoreNonce, hypercoreSignature, sendAssetTypedData, settleHypercore, signSendAsset, submitHypercore, usdcText, type BridgedCash, type HypercoreAnswer, type HypercoreSignature, type HypercoreTransport, type HyperliquidNetwork, type SendAssetAction, type SignedSendAsset, } from "./hypercore.js";
|
|
10
10
|
export { ApiError, envelope, NetworkError, randomBytes32, retryingFetch, sendOnce, unwrap, uuidv7, type Fetch, type Repaired, type RetryPolicy } from "./request.js";
|
|
11
11
|
export { items, pages, type Page, type PageQuery } from "./pagination.js";
|
package/dist/totalis.d.ts
CHANGED
|
@@ -102,6 +102,27 @@ export type SignedEntry = RecordScope & {
|
|
|
102
102
|
readonly command: AcceptCommand;
|
|
103
103
|
readonly bridge?: SignedSendAsset;
|
|
104
104
|
readonly bridgeState?: BridgeState;
|
|
105
|
+
/**
|
|
106
|
+
* Set when the venue refused this accept outright (`QUOTE_REPLACED`,
|
|
107
|
+
* `QUOTE_EXPIRED`): nothing was admitted, so `recoverAcceptance` answers
|
|
108
|
+
* `NOT_ADMITTED` without reading or cancelling the RFQ, which a later
|
|
109
|
+
* attempt may be using.
|
|
110
|
+
*/
|
|
111
|
+
readonly refused?: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* An entry's funding, signed before its quote is chosen
|
|
115
|
+
* (`prepareEntryFunding`). Pass it to `signEntry` as `prepared`, so only the
|
|
116
|
+
* quote is left to sign once it is chosen. It funds one accept: prepare again
|
|
117
|
+
* for another.
|
|
118
|
+
*/
|
|
119
|
+
export type PreparedEntryFunding = {
|
|
120
|
+
readonly stake: string;
|
|
121
|
+
readonly account: Hex;
|
|
122
|
+
readonly chainId: string;
|
|
123
|
+
readonly vault: Hex;
|
|
124
|
+
readonly funding?: Funding;
|
|
125
|
+
readonly bridge?: SignedSendAsset;
|
|
105
126
|
};
|
|
106
127
|
/** An entry record from SDK 0.63 or older, which sent its bridge before the accept: `recoverAcceptance` reads it and sends nothing, and `sendAcceptance` refuses it. */
|
|
107
128
|
export type LegacySignedEntry = Omit<SignedEntry, "version" | "bridgeState">;
|
|
@@ -246,7 +267,7 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
246
267
|
readonly legs: readonly Leg[];
|
|
247
268
|
readonly stake: Amount;
|
|
248
269
|
readonly choose?: Chooser<EntryQuote>;
|
|
249
|
-
/** How long quotes may keep arriving after the first one `choose` would take, before it picks; 0 takes it at once. */
|
|
270
|
+
/** How long quotes may keep arriving after the first one `choose` would take, before it picks; 0 takes it at once. Time spent signing the funding counts. */
|
|
250
271
|
readonly collectMs?: number;
|
|
251
272
|
/** `"wallet"` (default) plans the shortfall from the wallet and `core`; `"none"` never funds; or pass a funding command. */
|
|
252
273
|
readonly funding?: "wallet" | "none" | Funding;
|
|
@@ -254,9 +275,9 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
254
275
|
readonly core?: Amount | null;
|
|
255
276
|
readonly sourceDex?: "spot" | "";
|
|
256
277
|
readonly transport?: HypercoreTransport;
|
|
257
|
-
/** The cash to plan from (see `signEntry`); left out, the balances are read while the
|
|
278
|
+
/** The cash to plan from (see `signEntry`); left out, the balances are read while the RFQ is created, and again for each refresh. */
|
|
258
279
|
readonly cash?: EntryCash;
|
|
259
|
-
/** Awaited with the signed record before anything is sent, then with each change to its bridge (`onRecord`): persist it to `recoverAcceptance` after a restart. */
|
|
280
|
+
/** Awaited with the signed record before anything is sent, then with each change to its bridge (`onRecord`) or its refusal: persist it to `recoverAcceptance` after a restart. */
|
|
260
281
|
readonly onSigned?: RecordListener;
|
|
261
282
|
/** Automatic-quote intent identity, as the Totalis app sends it. */
|
|
262
283
|
readonly intent?: {
|
|
@@ -264,6 +285,12 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
264
285
|
readonly intent_generation: string;
|
|
265
286
|
};
|
|
266
287
|
readonly quoteTimeoutMs?: number;
|
|
288
|
+
/** The least payout a quote may pay, in USDC base units. Left out, it is the first chosen quote's: a refresh never takes less. */
|
|
289
|
+
readonly minPayout?: Amount;
|
|
290
|
+
/** How long after the first quote is chosen a refused, declined or timed-out accept may still be followed by another (default 10 s). */
|
|
291
|
+
readonly refreshMs?: number;
|
|
292
|
+
/** The most accepts sent, the first included (default 4); 1 never refreshes. */
|
|
293
|
+
readonly maxAttempts?: number;
|
|
267
294
|
readonly signal?: AbortSignal;
|
|
268
295
|
}) => Promise<{
|
|
269
296
|
entry: {
|
|
@@ -297,6 +324,14 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
297
324
|
readonly quoteTimeoutMs?: number;
|
|
298
325
|
readonly signal?: AbortSignal;
|
|
299
326
|
}) => Promise<AcceptanceResult>;
|
|
327
|
+
prepareEntryFunding: (request: {
|
|
328
|
+
readonly stake: Amount;
|
|
329
|
+
readonly core?: Amount | null | undefined;
|
|
330
|
+
readonly cash?: EntryCash | undefined;
|
|
331
|
+
readonly sourceDex?: "spot" | "" | undefined;
|
|
332
|
+
readonly hypercoreNonce?: number | undefined;
|
|
333
|
+
readonly signal?: AbortSignal | undefined;
|
|
334
|
+
}) => Promise<PreparedEntryFunding>;
|
|
300
335
|
signEntry: (request: {
|
|
301
336
|
readonly quote: EntryQuote;
|
|
302
337
|
readonly core?: Amount | null;
|
|
@@ -304,6 +339,7 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
304
339
|
readonly sourceDex?: "spot" | "";
|
|
305
340
|
readonly acceptanceId?: string;
|
|
306
341
|
readonly hypercoreNonce?: number;
|
|
342
|
+
readonly prepared?: PreparedEntryFunding;
|
|
307
343
|
}) => Promise<SignedEntry>;
|
|
308
344
|
signCashout: (request: {
|
|
309
345
|
readonly quote: CashoutBid;
|
package/dist/totalis.js
CHANGED
|
@@ -87,6 +87,11 @@ const minimumDeposit = 1000000n;
|
|
|
87
87
|
* best of them win instead of the fastest.
|
|
88
88
|
*/
|
|
89
89
|
const defaultCollectMs = 1_000;
|
|
90
|
+
const defaultRefreshMs = 10_000;
|
|
91
|
+
const defaultMaxAttempts = 4;
|
|
92
|
+
/** Accept refusals that mean the chosen quote went away before the accept, not that the position cannot open. */
|
|
93
|
+
const refreshCodes = new Set(["QUOTE_EXPIRED", "QUOTE_REPLACED"]);
|
|
94
|
+
const passedOn = new Set(["DECLINED", "TIMED_OUT"]);
|
|
90
95
|
const timerSlackMs = 250;
|
|
91
96
|
function termsOf(quote) {
|
|
92
97
|
return quote.kind === "ENTRY" ? quote.entry : quote.kind === "SELL_BACK" ? quote.sell_back : quote.transfer;
|
|
@@ -641,11 +646,46 @@ export function createTotalis(options) {
|
|
|
641
646
|
throw new EntryError("INSUFFICIENT_FUNDS", `${bridge - (routable > 0n ? routable : 0n)} USDC base units short for this stake`);
|
|
642
647
|
return { wallet: shortfall, bridge };
|
|
643
648
|
}
|
|
649
|
+
function signBridge(signer, chain, amount, request) {
|
|
650
|
+
return amount > 0n ? signSendAsset(signer, bridgeAction(hyperliquidNetwork(chain.chain_id), amount, {
|
|
651
|
+
...(request.hypercoreNonce === undefined ? {} : { nonce: request.hypercoreNonce }),
|
|
652
|
+
...(request.sourceDex === undefined ? {} : { sourceDex: request.sourceDex }),
|
|
653
|
+
})) : undefined;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Plan and sign an entry's funding before choosing its quote: the wallet's
|
|
657
|
+
* USDC authorization and any bridge from `core`, planned from `cash` or a
|
|
658
|
+
* fresh balances read as `signEntry` plans them. Neither names a quote, so
|
|
659
|
+
* once one is chosen `signEntry({ quote, prepared })` signs only the quote
|
|
660
|
+
* and the accept can go straight out.
|
|
661
|
+
*/
|
|
662
|
+
async function prepareEntryFunding(request) {
|
|
663
|
+
const { signer, account, chain, scope } = await recordScope();
|
|
664
|
+
const stake = BigInt(request.stake);
|
|
665
|
+
const core = request.core === undefined || request.core === null ? request.core : BigInt(request.core);
|
|
666
|
+
const plan = planEntry(stake, core, request.cash ?? await readSpendableCash(request.signal));
|
|
667
|
+
const [bridge, funding] = await Promise.all([
|
|
668
|
+
signBridge(signer, chain, plan.bridge, request),
|
|
669
|
+
plan.wallet > 0n ? depositAuthorization(chain, account, plan.wallet) : undefined,
|
|
670
|
+
]);
|
|
671
|
+
return { stake: stake.toString(), ...scope, ...(funding === undefined ? {} : { funding }), ...(bridge === undefined ? {} : { bridge }) };
|
|
672
|
+
}
|
|
644
673
|
async function signEntryWith(request, funding) {
|
|
645
|
-
const { quote, cash } = request;
|
|
674
|
+
const { quote, cash, prepared } = request;
|
|
646
675
|
const { signer, vaultSigner, account, chain, scope } = await recordScope();
|
|
647
676
|
if (quote.entry.taker.toLowerCase() !== account)
|
|
648
677
|
throw new Error("the quote is for another taker");
|
|
678
|
+
const record = (taker_signature, fundingCommand, bridge) => ({
|
|
679
|
+
kind: "ENTRY", version: 2, acceptanceId: request.acceptanceId ?? uuidv7(), rfqId: quote.rfq_id, quoteId: quote.quote_id, ...scope, expiry: quote.entry.expiry,
|
|
680
|
+
command: { taker_signature, ...(fundingCommand === undefined ? {} : { funding: fundingCommand }) },
|
|
681
|
+
...(bridge === undefined ? {} : { bridge }),
|
|
682
|
+
});
|
|
683
|
+
if (prepared !== undefined) {
|
|
684
|
+
if (prepared.stake !== quote.entry.stake || prepared.account !== scope.account || prepared.chainId !== scope.chainId || prepared.vault !== scope.vault) {
|
|
685
|
+
throw new Error("the prepared funding is for another stake, account or deployment");
|
|
686
|
+
}
|
|
687
|
+
return record(await vaultSigner.signTypedData(takerAcceptTypedData(chain, quote.entry)), prepared.funding, prepared.bridge);
|
|
688
|
+
}
|
|
649
689
|
const stake = BigInt(quote.entry.stake);
|
|
650
690
|
const core = request.core === undefined || request.core === null ? request.core : BigInt(request.core);
|
|
651
691
|
// Known cash is planned before any prompt, so a plan that fails asks the wallet for nothing.
|
|
@@ -664,18 +704,11 @@ export function createTotalis(options) {
|
|
|
664
704
|
const walletAmount = plan?.wallet ?? 0n;
|
|
665
705
|
const [taker_signature, bridge, walletFunding] = await Promise.all([
|
|
666
706
|
signing,
|
|
667
|
-
|
|
668
|
-
...(request.hypercoreNonce === undefined ? {} : { nonce: request.hypercoreNonce }),
|
|
669
|
-
...(request.sourceDex === undefined ? {} : { sourceDex: request.sourceDex }),
|
|
670
|
-
})) : undefined,
|
|
707
|
+
signBridge(signer, chain, bridgeAmount, request),
|
|
671
708
|
walletAmount > 0n ? depositAuthorization(chain, account, walletAmount) : undefined,
|
|
672
709
|
]);
|
|
673
710
|
const fundingCommand = plan === undefined ? (funding === "none" || funding === "plan" ? undefined : funding) : walletFunding;
|
|
674
|
-
return
|
|
675
|
-
kind: "ENTRY", version: 2, acceptanceId: request.acceptanceId ?? uuidv7(), rfqId: quote.rfq_id, quoteId: quote.quote_id, ...scope, expiry: quote.entry.expiry,
|
|
676
|
-
command: { taker_signature, ...(fundingCommand === undefined ? {} : { funding: fundingCommand }) },
|
|
677
|
-
...(bridge === undefined ? {} : { bridge }),
|
|
678
|
-
};
|
|
711
|
+
return record(taker_signature, fundingCommand, bridge);
|
|
679
712
|
}
|
|
680
713
|
/**
|
|
681
714
|
* Sign a Combo entry for a quote without sending it. Funding is planned
|
|
@@ -691,7 +724,8 @@ export function createTotalis(options) {
|
|
|
691
724
|
* shows them in turn. `sourceDex: ""` bridges from perps instead of spot.
|
|
692
725
|
* Persist the record, then `sendAcceptance` it; after a restart,
|
|
693
726
|
* `recoverAcceptance` it. Pass `hypercoreNonce` if your app reserves
|
|
694
|
-
* Hyperliquid nonces itself.
|
|
727
|
+
* Hyperliquid nonces itself. With `prepared` (`prepareEntryFunding`), only
|
|
728
|
+
* the quote is signed, and its funding is the prepared one.
|
|
695
729
|
*/
|
|
696
730
|
function signEntry(request) {
|
|
697
731
|
return signEntryWith(request, "plan");
|
|
@@ -773,9 +807,13 @@ export function createTotalis(options) {
|
|
|
773
807
|
* record from SDK 0.63 or older is only read. One still `PENDING_CONFIRM`
|
|
774
808
|
* is read again after the maker's deadline, or every 250 ms until 2 s past
|
|
775
809
|
* it while a bridge is unsent; `wait: false` returns it as it stands instead.
|
|
810
|
+
* A record marked `refused` is `NOT_ADMITTED`, read from the record alone.
|
|
776
811
|
*/
|
|
777
812
|
async function recoverAcceptance(signed, options = {}) {
|
|
778
813
|
await checkScope(signed);
|
|
814
|
+
if (signed.kind === "ENTRY" && "refused" in signed && signed.refused !== undefined) {
|
|
815
|
+
return resultOf(signed, { status: "NOT_ADMITTED", quote_id: signed.quoteId, rfq_id: signed.rfqId }, {});
|
|
816
|
+
}
|
|
779
817
|
const clock = venueClock();
|
|
780
818
|
const outcome = await fenceAcceptance(signed, clock, undefined, options.wait ?? true, bridgeUnsent(signed));
|
|
781
819
|
return resultOf(signed, outcome, await moveBridge(signed, outcome, clock, options));
|
|
@@ -791,21 +829,30 @@ export function createTotalis(options) {
|
|
|
791
829
|
}
|
|
792
830
|
}
|
|
793
831
|
/**
|
|
794
|
-
* Open a combo position: create the RFQ,
|
|
795
|
-
*
|
|
796
|
-
*
|
|
832
|
+
* Open a combo position: create the RFQ, sign its funding
|
|
833
|
+
* (`prepareEntryFunding`), then take a quote from those the stream pushes,
|
|
834
|
+
* sign it (`signEntry`), hand the record to `onSigned` to persist, and
|
|
835
|
+
* `sendAcceptance` at once: the quote is chosen last, so the one signed is
|
|
836
|
+
* as fresh as the book allows. Returns the acceptance's status (`ACCEPTED` with
|
|
797
837
|
* `operationId`; `FILLED` or `FAILED`, also with it, when a lost answer was
|
|
798
838
|
* read after the trade settled; `DECLINED` / `TIMED_OUT` / `CANCELLED`; or
|
|
799
839
|
* `NOT_ADMITTED` when a lost accept never arrived). The position opens
|
|
800
840
|
* under `positionId`, the quote's ID, once `waitForOperation(operationId)`
|
|
801
841
|
* resolves `COMMITTED`; pass its operation and the result's `record` to
|
|
802
842
|
* `entryFailure` to name a failure. Any vault shortfall is funded from the
|
|
803
|
-
* wallet, and from `core` by bridge, unless `funding` says otherwise.
|
|
804
|
-
*
|
|
805
|
-
*
|
|
843
|
+
* wallet, and from `core` by bridge, unless `funding` says otherwise.
|
|
844
|
+
*
|
|
845
|
+
* Makers replace their quotes as they renew them, so an accept can find its
|
|
846
|
+
* quote gone (`QUOTE_REPLACED`, `QUOTE_EXPIRED`); then `onSigned` gets the
|
|
847
|
+
* record marked `refused`. A maker can also turn its quote down (`DECLINED`)
|
|
848
|
+
* or not answer in time (`TIMED_OUT`). Either way nothing was admitted, so
|
|
849
|
+
* the best other quote paying at least `minPayout` is signed and sent,
|
|
850
|
+
* funding first, until one is taken, `refreshMs` has passed since the first
|
|
851
|
+
* quote was chosen, or `maxAttempts` accepts were sent. Then the last
|
|
852
|
+
* refusal is thrown, or the last decline or timeout returned.
|
|
806
853
|
*/
|
|
807
854
|
async function openPosition(request) {
|
|
808
|
-
const { legs, stake, choose = bestQuote(), collectMs = defaultCollectMs, funding = "wallet", intent, quoteTimeoutMs = 20_000, signal } = request;
|
|
855
|
+
const { legs, stake, choose = bestQuote(), collectMs = defaultCollectMs, funding = "wallet", intent, quoteTimeoutMs = 20_000, refreshMs = defaultRefreshMs, maxAttempts = defaultMaxAttempts, signal } = request;
|
|
809
856
|
const reading = funding === "wallet" && request.cash === undefined ? readSpendableCash(signal) : undefined;
|
|
810
857
|
reading?.catch(() => undefined);
|
|
811
858
|
const account = await (await requireSigner()).address();
|
|
@@ -814,20 +861,32 @@ export function createTotalis(options) {
|
|
|
814
861
|
const wanted = { taker: account, stake: String(stake), legs: [...legs].sort((a, b) => Number(a.outcome_id) - Number(b.outcome_id)).map(({ outcome_id, side }) => ({ outcome_id, side })) };
|
|
815
862
|
const entry = { stake: wanted.stake, legs: wanted.legs };
|
|
816
863
|
const rfqId = await createRfq(intent === undefined ? entry : { ...entry, ...intent });
|
|
817
|
-
|
|
818
|
-
|
|
864
|
+
const createdAt = performance.now();
|
|
865
|
+
let floor = request.minPayout === undefined ? undefined : BigInt(request.minPayout);
|
|
866
|
+
const refused = new Set();
|
|
867
|
+
const offered = (quote) => quote.kind === "ENTRY" && quote.rfq_id === rfqId &&
|
|
819
868
|
quote.entry.taker.toLowerCase() === wanted.taker && quote.entry.stake === wanted.stake &&
|
|
820
|
-
JSON.stringify(quote.entry.legs.map(({ outcome_id, side }) => ({ outcome_id, side }))) === JSON.stringify(wanted.legs)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
869
|
+
JSON.stringify(quote.entry.legs.map(({ outcome_id, side }) => ({ outcome_id, side }))) === JSON.stringify(wanted.legs) &&
|
|
870
|
+
!refused.has(quote.quote_id) && (floor === undefined || BigInt(quote.entry.payout) >= floor);
|
|
871
|
+
// Counted from the first choice, not from its signature, so a slow wallet cannot stretch the window.
|
|
872
|
+
let refreshUntil;
|
|
873
|
+
const take = () => signedOrCancel(rfqId, async () => {
|
|
874
|
+
const prepared = funding === "wallet"
|
|
875
|
+
? await prepareEntryFunding({ stake: wanted.stake, core: request.core, cash: request.cash ?? await (refreshUntil === undefined ? reading : readSpendableCash(signal)), sourceDex: request.sourceDex, signal })
|
|
876
|
+
: undefined;
|
|
877
|
+
const selected = refreshUntil === undefined
|
|
878
|
+
? await awaitOffer(rfqId, offered, choose, Math.max(0, Math.round(collectMs - (performance.now() - createdAt))), quoteTimeoutMs, signal)
|
|
879
|
+
: await awaitOffer(rfqId, offered, choose, 0, Math.max(0, Math.round(refreshUntil - performance.now())), signal);
|
|
880
|
+
refreshUntil ??= performance.now() + refreshMs;
|
|
824
881
|
if (!offered(selected))
|
|
825
882
|
throw new Error("the chosen quote is not for the requested position");
|
|
826
|
-
const signed = await signEntryWith({ quote: selected,
|
|
883
|
+
const signed = await signEntryWith({ quote: selected, prepared }, funding === "wallet" ? "plan" : funding);
|
|
827
884
|
await request.onSigned?.(signed);
|
|
828
885
|
return { selected, signed };
|
|
829
886
|
});
|
|
830
887
|
let { selected, signed } = await take();
|
|
888
|
+
floor ??= BigInt(selected.entry.payout);
|
|
889
|
+
const refreshEnds = refreshUntil ?? 0;
|
|
831
890
|
let record = signed;
|
|
832
891
|
const send = () => sendAcceptance(signed, {
|
|
833
892
|
...(request.transport === undefined ? {} : { transport: request.transport }),
|
|
@@ -837,18 +896,56 @@ export function createTotalis(options) {
|
|
|
837
896
|
await request.onSigned?.(next);
|
|
838
897
|
},
|
|
839
898
|
});
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
899
|
+
const settle = async () => {
|
|
900
|
+
for (let attempt = 1;; attempt += 1) {
|
|
901
|
+
// The maker turned the quote down, or never answered: nothing was admitted and the RFQ stays open.
|
|
902
|
+
let lost;
|
|
903
|
+
try {
|
|
904
|
+
lost = await send();
|
|
905
|
+
if (!passedOn.has(lost.status))
|
|
906
|
+
return lost;
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
if (!(error instanceof ApiError) || !refreshCodes.has(error.code))
|
|
910
|
+
throw error;
|
|
911
|
+
// Refused outright, so nothing was admitted and sendAcceptance discarded any bridge.
|
|
912
|
+
lost = error;
|
|
913
|
+
record = { ...record, refused: error.code };
|
|
914
|
+
await request.onSigned?.(record);
|
|
915
|
+
}
|
|
916
|
+
refused.add(signed.quoteId);
|
|
917
|
+
const last = record;
|
|
918
|
+
const end = async () => {
|
|
919
|
+
if (record !== last) {
|
|
920
|
+
record = last;
|
|
921
|
+
await request.onSigned?.(last);
|
|
922
|
+
}
|
|
923
|
+
if (lost instanceof ApiError)
|
|
924
|
+
throw lost;
|
|
925
|
+
return lost;
|
|
926
|
+
};
|
|
927
|
+
if (attempt >= maxAttempts || performance.now() >= refreshEnds)
|
|
928
|
+
return end();
|
|
929
|
+
let next;
|
|
930
|
+
try {
|
|
931
|
+
next = await take();
|
|
932
|
+
}
|
|
933
|
+
catch (retried) {
|
|
934
|
+
// No fresh quote at or above the floor in time leaves this outcome standing.
|
|
935
|
+
if (retried instanceof Error && retried.name === "TimeoutError")
|
|
936
|
+
return end();
|
|
937
|
+
throw retried;
|
|
938
|
+
}
|
|
939
|
+
// Signing and persisting can outlast the window: a record signed too late is never sent, and the last sent one is persisted again.
|
|
940
|
+
if (performance.now() >= refreshEnds) {
|
|
941
|
+
record = next.signed;
|
|
942
|
+
return end();
|
|
943
|
+
}
|
|
944
|
+
({ selected, signed } = next);
|
|
945
|
+
record = signed;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
const result = await settle();
|
|
852
949
|
const { bridged: _bridged, ...settled } = result;
|
|
853
950
|
return {
|
|
854
951
|
...settled,
|
|
@@ -1265,6 +1362,7 @@ export function createTotalis(options) {
|
|
|
1265
1362
|
spendableCash: readSpendableCash,
|
|
1266
1363
|
openPosition,
|
|
1267
1364
|
cashOut,
|
|
1365
|
+
prepareEntryFunding,
|
|
1268
1366
|
signEntry,
|
|
1269
1367
|
signCashout,
|
|
1270
1368
|
sendAcceptance,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalis/hip4-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "Totalis HIP-4 SDK: typed HTTP clients, signer-agnostic typed data, a retrying request layer, pagination, a realtime stream and taker/maker flows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|