@totalis/hip4-client 0.75.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 +74 -0
- package/README.md +3 -2
- package/dist/client.d.ts +1 -1
- package/dist/generated/openapi.d.ts +2 -2
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/totalis.d.ts +69 -3
- package/dist/totalis.js +194 -46
- package/dist/typed-data.d.ts +31 -1
- package/dist/typed-data.js +24 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,80 @@
|
|
|
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
|
+
|
|
43
|
+
## 0.76.0
|
|
44
|
+
|
|
45
|
+
Vault agents (TT-2554), one Combo leg per question (TT-2605) and no legs on
|
|
46
|
+
unpriced markets (TT-2606). Generated from contract
|
|
47
|
+
`205.0.0-device-vault-agents`. Additive: 0.75 code runs unchanged, but
|
|
48
|
+
`createRfq` now refuses two legs from one question and legs with a null
|
|
49
|
+
`chance`.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Vault agents (TT-2554). `agent` in `createTotalis` takes a key the account
|
|
54
|
+
granted; it then signs the account's `TakerAccept`, `SellBack`,
|
|
55
|
+
`CashoutTransfer` and Exits to HyperCore or the account's wallet instead of
|
|
56
|
+
`signer`, so those need no wallet prompt. `signer` still names the account
|
|
57
|
+
and signs USDC authorizations, HyperCore actions, grants and run-off claims.
|
|
58
|
+
- `grantVaultAgent({ agent, validUntil })` has the account sign an
|
|
59
|
+
`AgentGrant` and relays it (`POST /v1/me/vault-agents`); `revokeVaultAgent`
|
|
60
|
+
sends `validUntil` 0, and `vaultAgents()` iterates the live agents.
|
|
61
|
+
- `agentGrantTypedData` in `typed-data`, checked against the vault's vectors.
|
|
62
|
+
- A grant or revoke's default nonce rises past any nonce this client sent
|
|
63
|
+
explicitly for that agent, so it isn't refused for not exceeding it.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- `createRfq` refuses a Combo with two legs from one native question
|
|
68
|
+
(TT-2605): the second such leg is named in `field_violations` as
|
|
69
|
+
`/legs/{i}/outcome_id` with reason `SAME_QUESTION` (`400
|
|
70
|
+
INVALID_REQUEST`). Makers never quoted these legs, so such an RFQ used
|
|
71
|
+
to open and get no quotes. Allow one leg per `native_question_id` in your
|
|
72
|
+
slip; standalone markets have none and always combine.
|
|
73
|
+
- `createRfq` refuses a leg whose market has a side with a null `chance`
|
|
74
|
+
(an empty, dust or one-sided book with no recent evidence), with reason
|
|
75
|
+
`CHANCE_UNAVAILABLE` on `/legs/{i}/outcome_id` (`400 INVALID_REQUEST`).
|
|
76
|
+
Makers price from the book, and on such a book one small order sets the
|
|
77
|
+
price. Show `chance`, not `value`, and treat `chance: null` as unpriced
|
|
78
|
+
(TT-2606).
|
|
79
|
+
|
|
6
80
|
## 0.75.0
|
|
7
81
|
|
|
8
82
|
Maker capital on the stream (TT-2584), a stream that cannot hang in
|
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";
|
|
@@ -2843,7 +2843,7 @@ export type components = {
|
|
|
2843
2843
|
readonly CreateRfqCommand: {
|
|
2844
2844
|
/** @description Stake in atomic USDC (6 decimals), before taker fees; must be positive. Quotes carry the same stake. */
|
|
2845
2845
|
readonly stake: components["schemas"]["$defs-Amount"];
|
|
2846
|
-
/** @description 1 to 5 legs in strictly increasing outcome_id order. */
|
|
2846
|
+
/** @description 1 to 5 legs in strictly increasing outcome_id order, at most one per native question. Every leg's market must have eligibility ENABLED and combo OPEN, and a chance on each side. */
|
|
2847
2847
|
readonly legs: readonly components["schemas"]["$defs-Leg"][];
|
|
2848
2848
|
/** @description Optional partner attribution code, from any credential. From an account a partner admitted, it must be one of that partner's active codes and needs no signature; with none, the partner's account code is recorded. From any other account it needs builder_authorization_expiry and builder_signature from the partner's registered signer and is honoured only from the code's registered browser origin, so an API-key request (no Origin) from such an account cannot send one. A refused code returns INVALID_REQUEST naming /builder_code. Revocation rejects new RFQs without vetoing an already-created RFQ. Partners get codes from Totalis. */
|
|
2849
2849
|
readonly builder_code?: string;
|
|
@@ -9558,7 +9558,7 @@ export interface operations {
|
|
|
9558
9558
|
readonly "application/json": components["schemas"]["RfqCreated"];
|
|
9559
9559
|
};
|
|
9560
9560
|
};
|
|
9561
|
-
/** @description `INVALID_REQUEST`: the body is malformed, or a leg's market refuses new positions now. A refused leg is named in `field_violations` as `/legs/{i}/outcome_id`, and the message gives the reason, such as `REFERENCE_UNAVAILABLE` or `
|
|
9561
|
+
/** @description `INVALID_REQUEST`: the body is malformed, or a leg's market refuses new positions now. A refused leg is named in `field_violations` as `/legs/{i}/outcome_id`, and the message gives the reason, such as `REFERENCE_UNAVAILABLE`, `OUTSIDE_WINDOW`, `CHANCE_UNAVAILABLE` for a side whose `chance` is null, or `SAME_QUESTION` for a leg whose `native_question_id` an earlier leg already has; the market's `eligibility` and `combo` show when it clears. */
|
|
9562
9562
|
readonly 400: components["responses"]["Error"];
|
|
9563
9563
|
readonly 401: components["responses"]["Error"];
|
|
9564
9564
|
readonly 403: components["responses"]["Error"];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const sdkVersion = "205.0.0-device-vault-agents";
|
|
2
|
-
export declare const openapiSha256 = "
|
|
2
|
+
export declare const openapiSha256 = "93141e0bf50949fc37ebceef7c64555eb50a70f69c71c89c8fdc12780e0dd168";
|
|
3
3
|
export declare const realtimeSha256 = "1cac740e108e84ad1979f6ccc71089efd840404cb72d1612a86edc02769637e3";
|
|
4
4
|
export declare const jsonSchemaSha256: {
|
|
5
5
|
readonly "account.schema.json": "5106e2d53abef190990eff113f6692dd27df5848439cf5a80a42c11e89f2bef6";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/generate.mjs; do not edit.
|
|
2
2
|
export const sdkVersion = "205.0.0-device-vault-agents";
|
|
3
|
-
export const openapiSha256 = "
|
|
3
|
+
export const openapiSha256 = "93141e0bf50949fc37ebceef7c64555eb50a70f69c71c89c8fdc12780e0dd168";
|
|
4
4
|
export const realtimeSha256 = "1cac740e108e84ad1979f6ccc71089efd840404cb72d1612a86edc02769637e3";
|
|
5
5
|
export const jsonSchemaSha256 = {
|
|
6
6
|
"account.schema.json": "5106e2d53abef190990eff113f6692dd27df5848439cf5a80a42c11e89f2bef6",
|
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">;
|
|
@@ -185,6 +206,15 @@ export type TotalisOptions = Omit<ApiClientOptions, "accessToken"> & {
|
|
|
185
206
|
readonly accessToken: AccessToken;
|
|
186
207
|
/** The account's signer (GET /v1/me `wallet`); required for flows that sign. Never stored. */
|
|
187
208
|
readonly signer?: SignerInput;
|
|
209
|
+
/**
|
|
210
|
+
* A key the account granted as a vault agent (`grantVaultAgent`). When set,
|
|
211
|
+
* it signs the account's TakerAccept, SellBack, CashoutTransfer and Exits to
|
|
212
|
+
* its wallet or HyperCore instead of `signer`, so those need no wallet
|
|
213
|
+
* prompt. `signer` still names the account and signs everything else: USDC
|
|
214
|
+
* authorizations, HyperCore actions, grants and run-off claims. The venue
|
|
215
|
+
* refuses an agent whose grant ends before the signed expiry.
|
|
216
|
+
*/
|
|
217
|
+
readonly agent?: SignerInput;
|
|
188
218
|
/** Stream options; `subscription` adds account subscription fields such as `open_rfqs`. */
|
|
189
219
|
readonly stream?: Omit<StreamOptions, "subscription" | "accessToken" | "environment" | "baseUrl" | "streamBaseUrl"> & {
|
|
190
220
|
readonly subscription?: Omit<Extract<StreamSubscription, {
|
|
@@ -237,7 +267,7 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
237
267
|
readonly legs: readonly Leg[];
|
|
238
268
|
readonly stake: Amount;
|
|
239
269
|
readonly choose?: Chooser<EntryQuote>;
|
|
240
|
-
/** 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. */
|
|
241
271
|
readonly collectMs?: number;
|
|
242
272
|
/** `"wallet"` (default) plans the shortfall from the wallet and `core`; `"none"` never funds; or pass a funding command. */
|
|
243
273
|
readonly funding?: "wallet" | "none" | Funding;
|
|
@@ -245,9 +275,9 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
245
275
|
readonly core?: Amount | null;
|
|
246
276
|
readonly sourceDex?: "spot" | "";
|
|
247
277
|
readonly transport?: HypercoreTransport;
|
|
248
|
-
/** 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. */
|
|
249
279
|
readonly cash?: EntryCash;
|
|
250
|
-
/** 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. */
|
|
251
281
|
readonly onSigned?: RecordListener;
|
|
252
282
|
/** Automatic-quote intent identity, as the Totalis app sends it. */
|
|
253
283
|
readonly intent?: {
|
|
@@ -255,6 +285,12 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
255
285
|
readonly intent_generation: string;
|
|
256
286
|
};
|
|
257
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;
|
|
258
294
|
readonly signal?: AbortSignal;
|
|
259
295
|
}) => Promise<{
|
|
260
296
|
entry: {
|
|
@@ -288,6 +324,14 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
288
324
|
readonly quoteTimeoutMs?: number;
|
|
289
325
|
readonly signal?: AbortSignal;
|
|
290
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>;
|
|
291
335
|
signEntry: (request: {
|
|
292
336
|
readonly quote: EntryQuote;
|
|
293
337
|
readonly core?: Amount | null;
|
|
@@ -295,6 +339,7 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
295
339
|
readonly sourceDex?: "spot" | "";
|
|
296
340
|
readonly acceptanceId?: string;
|
|
297
341
|
readonly hypercoreNonce?: number;
|
|
342
|
+
readonly prepared?: PreparedEntryFunding;
|
|
298
343
|
}) => Promise<SignedEntry>;
|
|
299
344
|
signCashout: (request: {
|
|
300
345
|
readonly quote: CashoutBid;
|
|
@@ -638,6 +683,27 @@ export declare function createTotalis(options: TotalisOptions): {
|
|
|
638
683
|
readonly signal?: AbortSignal;
|
|
639
684
|
}) => Promise<CoreExit>;
|
|
640
685
|
directDepositCalls: (amount: Amount) => Promise<readonly WalletCall[]>;
|
|
686
|
+
grantVaultAgent: (request: {
|
|
687
|
+
readonly agent: string;
|
|
688
|
+
readonly validUntil: number | string;
|
|
689
|
+
readonly nonce?: number | string;
|
|
690
|
+
readonly idempotencyKey?: string;
|
|
691
|
+
}) => Promise<{
|
|
692
|
+
readonly operation_id: import("./generated/openapi.js").components["schemas"]["Hash32"];
|
|
693
|
+
}>;
|
|
694
|
+
revokeVaultAgent: (request: {
|
|
695
|
+
readonly agent: string;
|
|
696
|
+
readonly nonce?: number | string;
|
|
697
|
+
readonly idempotencyKey?: string;
|
|
698
|
+
}) => Promise<{
|
|
699
|
+
readonly operation_id: import("./generated/openapi.js").components["schemas"]["Hash32"];
|
|
700
|
+
}>;
|
|
701
|
+
vaultAgents: (limit?: number) => AsyncGenerator<{
|
|
702
|
+
readonly agent: import("./generated/openapi.js").components["schemas"]["Address"];
|
|
703
|
+
readonly valid_until: import("./generated/openapi.js").components["schemas"]["UInt64String"];
|
|
704
|
+
readonly nonce: import("./generated/openapi.js").components["schemas"]["UInt64String"];
|
|
705
|
+
readonly copy_agent: boolean;
|
|
706
|
+
}, void, any>;
|
|
641
707
|
/** Render an RFQ's live quotes (entry or cash-out) from the stream; `[]` while the stream is down. */
|
|
642
708
|
watchQuotes: (rfqId: string, listener: (quotes: readonly Quote[], serverNowMs: number) => void) => () => void;
|
|
643
709
|
/** Issue a personal `ttk_` key; the signer must be the account wallet (embedded, or a wallet-only account's own). */
|
package/dist/totalis.js
CHANGED
|
@@ -7,7 +7,8 @@ import { spendableCash } from "./money.js";
|
|
|
7
7
|
import { ApiError, backoffDelay, envelope, NetworkError, randomBytes32, resendable, retryingFetch, sendOnce, singleFetch, sleep, unwrap, uuidv7 } from "./request.js";
|
|
8
8
|
import { toTotalisSigner } from "./signer.js";
|
|
9
9
|
import { TotalisStream } from "./stream.js";
|
|
10
|
-
import {
|
|
10
|
+
import { items } from "./pagination.js";
|
|
11
|
+
import { address, agentGrantTypedData, cashoutTransferTypedData, exitTypedData, receiveWithAuthorizationTypedData, sellBackTypedData, takerAcceptTypedData, payoutNonce, transferWithAuthorizationTypedData, } from "./typed-data.js";
|
|
11
12
|
/**
|
|
12
13
|
* What an entry's bridge may have moved to its wallet, by its state: nothing
|
|
13
14
|
* before it is sent, once discarded or refused. Only `LANDED` proves it
|
|
@@ -86,6 +87,11 @@ const minimumDeposit = 1000000n;
|
|
|
86
87
|
* best of them win instead of the fastest.
|
|
87
88
|
*/
|
|
88
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"]);
|
|
89
95
|
const timerSlackMs = 250;
|
|
90
96
|
function termsOf(quote) {
|
|
91
97
|
return quote.kind === "ENTRY" ? quote.entry : quote.kind === "SELL_BACK" ? quote.sell_back : quote.transfer;
|
|
@@ -314,7 +320,7 @@ function streamQuotes(stream, read) {
|
|
|
314
320
|
* stream; HTTP reads recover after a reconnect or answer a doorbell.
|
|
315
321
|
*/
|
|
316
322
|
export function createTotalis(options) {
|
|
317
|
-
const { environment, signer: signerInput, stream: streamOptions, streamBaseUrl, accessToken, ...clientOptions } = options;
|
|
323
|
+
const { environment, signer: signerInput, agent: agentInput, stream: streamOptions, streamBaseUrl, accessToken, ...clientOptions } = options;
|
|
318
324
|
const client = audienceClient(environment, { ...clientOptions, accessToken });
|
|
319
325
|
const keys = createApiKeyClient(environment, { ...clientOptions, accessToken });
|
|
320
326
|
const stream = new TotalisStream({
|
|
@@ -337,6 +343,22 @@ export function createTotalis(options) {
|
|
|
337
343
|
};
|
|
338
344
|
const signer = signerInput === undefined ? undefined : toTotalisSigner(signerInput);
|
|
339
345
|
const requireSigner = () => signer ?? Promise.reject(new Error("this flow needs a signer"));
|
|
346
|
+
const agent = agentInput === undefined ? undefined : toTotalisSigner(agentInput);
|
|
347
|
+
// The vault reverts a grant whose nonce does not exceed the agent's last, so
|
|
348
|
+
// two grants for one agent in one second (a grant, then its revoke) must not
|
|
349
|
+
// share one, and a default after an explicit nonce must exceed it too. The
|
|
350
|
+
// vault counts per agent, so the mark is per agent.
|
|
351
|
+
const lastGrantNonce = new Map();
|
|
352
|
+
const grantNonce = (agentAddress, explicit) => {
|
|
353
|
+
const last = lastGrantNonce.get(agentAddress) ?? 0n;
|
|
354
|
+
const clock = BigInt(now());
|
|
355
|
+
const nonce = explicit !== undefined ? BigInt(explicit) : clock > last ? clock : last + 1n;
|
|
356
|
+
if (nonce < 1n || nonce > 2n ** 64n - 1n)
|
|
357
|
+
throw new Error(`grant nonce ${nonce} is outside 1..2^64-1`);
|
|
358
|
+
if (nonce > last)
|
|
359
|
+
lastGrantNonce.set(agentAddress, nonce);
|
|
360
|
+
return String(nonce);
|
|
361
|
+
};
|
|
340
362
|
let deploymentRead;
|
|
341
363
|
const deployment = () => (deploymentRead ??= client.GET("/v1/deployment").then(unwrap).catch((error) => {
|
|
342
364
|
deploymentRead = undefined;
|
|
@@ -585,7 +607,7 @@ export function createTotalis(options) {
|
|
|
585
607
|
const signer = await requireSigner();
|
|
586
608
|
const account = await signer.address();
|
|
587
609
|
const chain = await deployment();
|
|
588
|
-
return { signer, account, chain, scope: { account, chainId: chain.chain_id, vault: address(chain.vault_address.toLowerCase()) } };
|
|
610
|
+
return { signer, vaultSigner: agent ?? signer, account, chain, scope: { account, chainId: chain.chain_id, vault: address(chain.vault_address.toLowerCase()) } };
|
|
589
611
|
}
|
|
590
612
|
/** A stored record signs nothing more, so without a signer the session's wallet (GET /v1/me) stands for the account. */
|
|
591
613
|
async function checkScope(signed) {
|
|
@@ -624,16 +646,51 @@ export function createTotalis(options) {
|
|
|
624
646
|
throw new EntryError("INSUFFICIENT_FUNDS", `${bridge - (routable > 0n ? routable : 0n)} USDC base units short for this stake`);
|
|
625
647
|
return { wallet: shortfall, bridge };
|
|
626
648
|
}
|
|
627
|
-
|
|
628
|
-
|
|
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) {
|
|
629
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
|
+
}
|
|
673
|
+
async function signEntryWith(request, funding) {
|
|
674
|
+
const { quote, cash, prepared } = request;
|
|
675
|
+
const { signer, vaultSigner, account, chain, scope } = await recordScope();
|
|
630
676
|
if (quote.entry.taker.toLowerCase() !== account)
|
|
631
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
|
+
}
|
|
632
689
|
const stake = BigInt(quote.entry.stake);
|
|
633
690
|
const core = request.core === undefined || request.core === null ? request.core : BigInt(request.core);
|
|
634
691
|
// Known cash is planned before any prompt, so a plan that fails asks the wallet for nothing.
|
|
635
692
|
let plan = funding === "plan" && cash !== undefined ? planEntry(stake, core, cash) : undefined;
|
|
636
|
-
const signing =
|
|
693
|
+
const signing = vaultSigner.signTypedData(takerAcceptTypedData(chain, quote.entry));
|
|
637
694
|
if (funding === "plan" && cash === undefined) {
|
|
638
695
|
// The quote signature names no amount, so the wallet signs it while the balances are read; a refusal stops the read and is the call's error.
|
|
639
696
|
const reading = new AbortController();
|
|
@@ -647,18 +704,11 @@ export function createTotalis(options) {
|
|
|
647
704
|
const walletAmount = plan?.wallet ?? 0n;
|
|
648
705
|
const [taker_signature, bridge, walletFunding] = await Promise.all([
|
|
649
706
|
signing,
|
|
650
|
-
|
|
651
|
-
...(request.hypercoreNonce === undefined ? {} : { nonce: request.hypercoreNonce }),
|
|
652
|
-
...(request.sourceDex === undefined ? {} : { sourceDex: request.sourceDex }),
|
|
653
|
-
})) : undefined,
|
|
707
|
+
signBridge(signer, chain, bridgeAmount, request),
|
|
654
708
|
walletAmount > 0n ? depositAuthorization(chain, account, walletAmount) : undefined,
|
|
655
709
|
]);
|
|
656
710
|
const fundingCommand = plan === undefined ? (funding === "none" || funding === "plan" ? undefined : funding) : walletFunding;
|
|
657
|
-
return
|
|
658
|
-
kind: "ENTRY", version: 2, acceptanceId: request.acceptanceId ?? uuidv7(), rfqId: quote.rfq_id, quoteId: quote.quote_id, ...scope, expiry: quote.entry.expiry,
|
|
659
|
-
command: { taker_signature, ...(fundingCommand === undefined ? {} : { funding: fundingCommand }) },
|
|
660
|
-
...(bridge === undefined ? {} : { bridge }),
|
|
661
|
-
};
|
|
711
|
+
return record(taker_signature, fundingCommand, bridge);
|
|
662
712
|
}
|
|
663
713
|
/**
|
|
664
714
|
* Sign a Combo entry for a quote without sending it. Funding is planned
|
|
@@ -674,7 +724,8 @@ export function createTotalis(options) {
|
|
|
674
724
|
* shows them in turn. `sourceDex: ""` bridges from perps instead of spot.
|
|
675
725
|
* Persist the record, then `sendAcceptance` it; after a restart,
|
|
676
726
|
* `recoverAcceptance` it. Pass `hypercoreNonce` if your app reserves
|
|
677
|
-
* Hyperliquid nonces itself.
|
|
727
|
+
* Hyperliquid nonces itself. With `prepared` (`prepareEntryFunding`), only
|
|
728
|
+
* the quote is signed, and its funding is the prepared one.
|
|
678
729
|
*/
|
|
679
730
|
function signEntry(request) {
|
|
680
731
|
return signEntryWith(request, "plan");
|
|
@@ -682,12 +733,12 @@ export function createTotalis(options) {
|
|
|
682
733
|
/** Sign a cash-out quote as the position's owner without sending it: the same SellBack or CashoutTransfer digest the maker signs. */
|
|
683
734
|
async function signCashout(request) {
|
|
684
735
|
const { quote } = request;
|
|
685
|
-
const {
|
|
736
|
+
const { vaultSigner, account, chain, scope } = await recordScope();
|
|
686
737
|
const seller = quote.kind === "SELL_BACK" ? quote.sell_back.owner : quote.transfer.seller;
|
|
687
738
|
if (seller.toLowerCase() !== account)
|
|
688
739
|
throw new Error("the quote sells another owner's position");
|
|
689
740
|
const typedData = quote.kind === "SELL_BACK" ? sellBackTypedData(chain, quote.sell_back) : cashoutTransferTypedData(chain, quote.transfer);
|
|
690
|
-
const taker_signature = await
|
|
741
|
+
const taker_signature = await vaultSigner.signTypedData(typedData);
|
|
691
742
|
return {
|
|
692
743
|
kind: "CASHOUT", acceptanceId: request.acceptanceId ?? uuidv7(), rfqId: quote.rfq_id, quoteId: quote.quote_id, ...scope,
|
|
693
744
|
positionId: termsOf(quote).position_id, command: { taker_signature },
|
|
@@ -756,9 +807,13 @@ export function createTotalis(options) {
|
|
|
756
807
|
* record from SDK 0.63 or older is only read. One still `PENDING_CONFIRM`
|
|
757
808
|
* is read again after the maker's deadline, or every 250 ms until 2 s past
|
|
758
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.
|
|
759
811
|
*/
|
|
760
812
|
async function recoverAcceptance(signed, options = {}) {
|
|
761
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
|
+
}
|
|
762
817
|
const clock = venueClock();
|
|
763
818
|
const outcome = await fenceAcceptance(signed, clock, undefined, options.wait ?? true, bridgeUnsent(signed));
|
|
764
819
|
return resultOf(signed, outcome, await moveBridge(signed, outcome, clock, options));
|
|
@@ -774,21 +829,30 @@ export function createTotalis(options) {
|
|
|
774
829
|
}
|
|
775
830
|
}
|
|
776
831
|
/**
|
|
777
|
-
* Open a combo position: create the RFQ,
|
|
778
|
-
*
|
|
779
|
-
*
|
|
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
|
|
780
837
|
* `operationId`; `FILLED` or `FAILED`, also with it, when a lost answer was
|
|
781
838
|
* read after the trade settled; `DECLINED` / `TIMED_OUT` / `CANCELLED`; or
|
|
782
839
|
* `NOT_ADMITTED` when a lost accept never arrived). The position opens
|
|
783
840
|
* under `positionId`, the quote's ID, once `waitForOperation(operationId)`
|
|
784
841
|
* resolves `COMMITTED`; pass its operation and the result's `record` to
|
|
785
842
|
* `entryFailure` to name a failure. Any vault shortfall is funded from the
|
|
786
|
-
* wallet, and from `core` by bridge, unless `funding` says otherwise.
|
|
787
|
-
*
|
|
788
|
-
*
|
|
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.
|
|
789
853
|
*/
|
|
790
854
|
async function openPosition(request) {
|
|
791
|
-
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;
|
|
792
856
|
const reading = funding === "wallet" && request.cash === undefined ? readSpendableCash(signal) : undefined;
|
|
793
857
|
reading?.catch(() => undefined);
|
|
794
858
|
const account = await (await requireSigner()).address();
|
|
@@ -797,20 +861,32 @@ export function createTotalis(options) {
|
|
|
797
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 })) };
|
|
798
862
|
const entry = { stake: wanted.stake, legs: wanted.legs };
|
|
799
863
|
const rfqId = await createRfq(intent === undefined ? entry : { ...entry, ...intent });
|
|
800
|
-
|
|
801
|
-
|
|
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 &&
|
|
802
868
|
quote.entry.taker.toLowerCase() === wanted.taker && quote.entry.stake === wanted.stake &&
|
|
803
|
-
JSON.stringify(quote.entry.legs.map(({ outcome_id, side }) => ({ outcome_id, side }))) === JSON.stringify(wanted.legs)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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;
|
|
807
881
|
if (!offered(selected))
|
|
808
882
|
throw new Error("the chosen quote is not for the requested position");
|
|
809
|
-
const signed = await signEntryWith({ quote: selected,
|
|
883
|
+
const signed = await signEntryWith({ quote: selected, prepared }, funding === "wallet" ? "plan" : funding);
|
|
810
884
|
await request.onSigned?.(signed);
|
|
811
885
|
return { selected, signed };
|
|
812
886
|
});
|
|
813
887
|
let { selected, signed } = await take();
|
|
888
|
+
floor ??= BigInt(selected.entry.payout);
|
|
889
|
+
const refreshEnds = refreshUntil ?? 0;
|
|
814
890
|
let record = signed;
|
|
815
891
|
const send = () => sendAcceptance(signed, {
|
|
816
892
|
...(request.transport === undefined ? {} : { transport: request.transport }),
|
|
@@ -820,18 +896,56 @@ export function createTotalis(options) {
|
|
|
820
896
|
await request.onSigned?.(next);
|
|
821
897
|
},
|
|
822
898
|
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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();
|
|
835
949
|
const { bridged: _bridged, ...settled } = result;
|
|
836
950
|
return {
|
|
837
951
|
...settled,
|
|
@@ -913,7 +1027,7 @@ export function createTotalis(options) {
|
|
|
913
1027
|
let vault_withdrawal;
|
|
914
1028
|
if (shortfall > 0n) {
|
|
915
1029
|
const exit = { amount: shortfall.toString(), expiry: String(issued + 600), salt: BigInt(randomBytes32()).toString() };
|
|
916
|
-
vault_withdrawal = { ...exit, signature: await signer.signTypedData(exitTypedData(chain, { account, to: account, ...exit })) };
|
|
1030
|
+
vault_withdrawal = { ...exit, signature: await (agent ?? signer).signTypedData(exitTypedData(chain, { account, to: account, ...exit })) };
|
|
917
1031
|
deadline = issued + 600;
|
|
918
1032
|
}
|
|
919
1033
|
// The venue requires the payout to outlive the vault leg by 300 s; a minute more absorbs clock skew.
|
|
@@ -1065,7 +1179,7 @@ export function createTotalis(options) {
|
|
|
1065
1179
|
const to = address(route.router_address.toLowerCase());
|
|
1066
1180
|
// A minute inside the venue's maximum life absorbs clock skew.
|
|
1067
1181
|
const exit = { amount: BigInt(request.amount).toString(), expiry: String(now() + Math.min(route.maximum_life_seconds - 60, 600)), salt: BigInt(randomBytes32()).toString() };
|
|
1068
|
-
const signature = await signer.signTypedData(exitTypedData(chain, { account, to, ...exit }));
|
|
1182
|
+
const signature = await (agent ?? signer).signTypedData(exitTypedData(chain, { account, to, ...exit }));
|
|
1069
1183
|
const body = envelope({ account, to, ...exit, signature, purpose: request.purpose ?? "ORDER", ...(request.activationFeeAccepted ? { activation_fee_accepted: true } : {}) });
|
|
1070
1184
|
return { exitId, account, body };
|
|
1071
1185
|
}
|
|
@@ -1190,6 +1304,36 @@ export function createTotalis(options) {
|
|
|
1190
1304
|
throw new Error(`core exit ${exitId} ended ${exit.status}, not credited on HyperCore`);
|
|
1191
1305
|
return exit;
|
|
1192
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Grant `agent`, a key one of the account's devices holds, as a vault agent
|
|
1309
|
+
* until `validUntil` (unix seconds, at most 180 days away): the account's
|
|
1310
|
+
* `signer` signs the AgentGrant and Totalis relays it to the vault, paying
|
|
1311
|
+
* the gas. Pass that key as `agent` to sign accepts, cash-outs and Exits
|
|
1312
|
+
* back to the account without a wallet prompt. The nonce defaults to the
|
|
1313
|
+
* venue's clock in seconds, and rises past this client's last grant, so a
|
|
1314
|
+
* later grant or revoke supersedes an earlier one. The
|
|
1315
|
+
* 202 is admission: follow `operation_id` with `waitForOperation`; the agent
|
|
1316
|
+
* signs once it lands. A resend of the same `idempotencyKey` returns the
|
|
1317
|
+
* same operation.
|
|
1318
|
+
*/
|
|
1319
|
+
async function grantVaultAgent(request) {
|
|
1320
|
+
const signer = await requireSigner();
|
|
1321
|
+
const account = address((await signer.address()).toLowerCase());
|
|
1322
|
+
const agentAddress = address(request.agent.toLowerCase());
|
|
1323
|
+
const grant = { account, agent: agentAddress, valid_until: String(request.validUntil), nonce: grantNonce(agentAddress, request.nonce) };
|
|
1324
|
+
const signature = await signer.signTypedData(agentGrantTypedData(await deployment(), grant));
|
|
1325
|
+
const key = request.idempotencyKey ?? uuidv7();
|
|
1326
|
+
return sendOnce(() => client.POST("/v1/me/vault-agents", { params: { header: idempotency(key) },
|
|
1327
|
+
body: { agent: grant.agent, valid_until: grant.valid_until, nonce: grant.nonce, signature } }));
|
|
1328
|
+
}
|
|
1329
|
+
/** The agent keeps signing until the revoke's operation lands; `waitForOperation` before treating it as gone. */
|
|
1330
|
+
function revokeVaultAgent(request) {
|
|
1331
|
+
return grantVaultAgent({ ...request, validUntil: 0 });
|
|
1332
|
+
}
|
|
1333
|
+
/** The account's live vault agents, every page; `copy_agent` marks a Combo copy agent's key. */
|
|
1334
|
+
function vaultAgents(limit = 200) {
|
|
1335
|
+
return items((query) => client.GET("/v1/me/vault-agents", { params: { query: { ...query, limit } } }).then(unwrap), (page) => page.agents);
|
|
1336
|
+
}
|
|
1193
1337
|
/**
|
|
1194
1338
|
* The transactions a wallet sends itself to deposit USDC from HyperEVM into
|
|
1195
1339
|
* its vault balance, paying its own gas: approve the vault for `amount`, then
|
|
@@ -1218,6 +1362,7 @@ export function createTotalis(options) {
|
|
|
1218
1362
|
spendableCash: readSpendableCash,
|
|
1219
1363
|
openPosition,
|
|
1220
1364
|
cashOut,
|
|
1365
|
+
prepareEntryFunding,
|
|
1221
1366
|
signEntry,
|
|
1222
1367
|
signCashout,
|
|
1223
1368
|
sendAcceptance,
|
|
@@ -1239,6 +1384,9 @@ export function createTotalis(options) {
|
|
|
1239
1384
|
sendReturnToCore,
|
|
1240
1385
|
returnToCore,
|
|
1241
1386
|
directDepositCalls,
|
|
1387
|
+
grantVaultAgent,
|
|
1388
|
+
revokeVaultAgent,
|
|
1389
|
+
vaultAgents,
|
|
1242
1390
|
/** Render an RFQ's live quotes (entry or cash-out) from the stream; `[]` while the stream is down. */
|
|
1243
1391
|
watchQuotes,
|
|
1244
1392
|
/** Issue a personal `ttk_` key; the signer must be the account wallet (embedded, or a wallet-only account's own). */
|
package/dist/typed-data.d.ts
CHANGED
|
@@ -127,15 +127,34 @@ export declare const totalisTypes: {
|
|
|
127
127
|
readonly name: "salt";
|
|
128
128
|
readonly type: "uint256";
|
|
129
129
|
}];
|
|
130
|
+
readonly AgentGrant: readonly [{
|
|
131
|
+
readonly name: "account";
|
|
132
|
+
readonly type: "address";
|
|
133
|
+
}, {
|
|
134
|
+
readonly name: "agent";
|
|
135
|
+
readonly type: "address";
|
|
136
|
+
}, {
|
|
137
|
+
readonly name: "validUntil";
|
|
138
|
+
readonly type: "uint64";
|
|
139
|
+
}, {
|
|
140
|
+
readonly name: "nonce";
|
|
141
|
+
readonly type: "uint64";
|
|
142
|
+
}];
|
|
130
143
|
};
|
|
131
144
|
export type TotalisPrimaryType = Exclude<keyof typeof totalisTypes, "Leg">;
|
|
132
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Who signs each struct: the maker's signer, the taker's signer, or either
|
|
147
|
+
* side of a cash-out. A vault agent the account granted may sign the taker's
|
|
148
|
+
* TakerAccept, SellBack and CashoutTransfer, and an Exit back to the account;
|
|
149
|
+
* only the account itself signs an AgentGrant.
|
|
150
|
+
*/
|
|
133
151
|
export declare const signedBy: {
|
|
134
152
|
readonly Quote: "maker";
|
|
135
153
|
readonly TakerAccept: "taker";
|
|
136
154
|
readonly SellBack: "maker_and_taker";
|
|
137
155
|
readonly CashoutTransfer: "maker_and_taker";
|
|
138
156
|
readonly Exit: "taker";
|
|
157
|
+
readonly AgentGrant: "taker";
|
|
139
158
|
};
|
|
140
159
|
/** The vault deployment a signature binds to: GET /v1/deployment. */
|
|
141
160
|
export type TotalisDeployment = Pick<Schemas["Deployment"], "chain_id" | "vault_address">;
|
|
@@ -201,6 +220,17 @@ export declare function exitTypedData(deployment: TotalisDeployment, exit: {
|
|
|
201
220
|
expiry: string;
|
|
202
221
|
salt: string;
|
|
203
222
|
}): TotalisTypedData<"Exit">;
|
|
223
|
+
/**
|
|
224
|
+
* The account's grant to a vault agent: a key it holds signs for the account
|
|
225
|
+
* until `valid_until` (unix seconds, at most 180 days away; 0 revokes). The
|
|
226
|
+
* nonce must exceed the agent's last grant. Relay it with POST /v1/me/vault-agents.
|
|
227
|
+
*/
|
|
228
|
+
export declare function agentGrantTypedData(deployment: TotalisDeployment, grant: {
|
|
229
|
+
account: Hex;
|
|
230
|
+
agent: Hex;
|
|
231
|
+
valid_until: string;
|
|
232
|
+
nonce: string;
|
|
233
|
+
}): TotalisTypedData<"AgentGrant">;
|
|
204
234
|
export declare const domainTypes: {
|
|
205
235
|
readonly EIP712Domain: readonly [{
|
|
206
236
|
readonly name: "name";
|
package/dist/typed-data.js
CHANGED
|
@@ -47,14 +47,26 @@ export const totalisTypes = {
|
|
|
47
47
|
{ name: "expiry", type: "uint64" },
|
|
48
48
|
{ name: "salt", type: "uint256" },
|
|
49
49
|
],
|
|
50
|
+
AgentGrant: [
|
|
51
|
+
{ name: "account", type: "address" },
|
|
52
|
+
{ name: "agent", type: "address" },
|
|
53
|
+
{ name: "validUntil", type: "uint64" },
|
|
54
|
+
{ name: "nonce", type: "uint64" },
|
|
55
|
+
],
|
|
50
56
|
};
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Who signs each struct: the maker's signer, the taker's signer, or either
|
|
59
|
+
* side of a cash-out. A vault agent the account granted may sign the taker's
|
|
60
|
+
* TakerAccept, SellBack and CashoutTransfer, and an Exit back to the account;
|
|
61
|
+
* only the account itself signs an AgentGrant.
|
|
62
|
+
*/
|
|
52
63
|
export const signedBy = {
|
|
53
64
|
Quote: "maker",
|
|
54
65
|
TakerAccept: "taker",
|
|
55
66
|
SellBack: "maker_and_taker",
|
|
56
67
|
CashoutTransfer: "maker_and_taker",
|
|
57
68
|
Exit: "taker",
|
|
69
|
+
AgentGrant: "taker",
|
|
58
70
|
};
|
|
59
71
|
/** A 20-byte hex address, or a TypeError. */
|
|
60
72
|
export function address(value) {
|
|
@@ -122,6 +134,17 @@ export function exitTypedData(deployment, exit) {
|
|
|
122
134
|
message: { account: exit.account, to: exit.to, amount: BigInt(exit.amount), expiry: BigInt(exit.expiry), salt: BigInt(exit.salt) },
|
|
123
135
|
};
|
|
124
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The account's grant to a vault agent: a key it holds signs for the account
|
|
139
|
+
* until `valid_until` (unix seconds, at most 180 days away; 0 revokes). The
|
|
140
|
+
* nonce must exceed the agent's last grant. Relay it with POST /v1/me/vault-agents.
|
|
141
|
+
*/
|
|
142
|
+
export function agentGrantTypedData(deployment, grant) {
|
|
143
|
+
return {
|
|
144
|
+
domain: domain(deployment), types: { AgentGrant: totalisTypes.AgentGrant }, primaryType: "AgentGrant",
|
|
145
|
+
message: { account: address(grant.account), agent: address(grant.agent), validUntil: BigInt(grant.valid_until), nonce: BigInt(grant.nonce) },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
125
148
|
// ---------------------------------------------------------------------------
|
|
126
149
|
// EIP-712 hashing, so makers can compute quote_digest and every signer can be
|
|
127
150
|
// checked against the contract's vectors without a wallet library.
|
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": [
|