@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 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, takes the best quote, signs the acceptance and funds any shortfall from the
70
- wallet. Only `COMMITTED` means the trade happened. A decided position settles and pays out on its own.
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 `OUTSIDE_WINDOW`; the market's `eligibility` and `combo` show when it clears. */
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 = "e742feade28ca115077a4275e4a6260646278e9adcf0839987b8566a2221c8f5";
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 = "e742feade28ca115077a4275e4a6260646278e9adcf0839987b8566a2221c8f5";
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 quotes arrive. */
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 { address, cashoutTransferTypedData, exitTypedData, receiveWithAuthorizationTypedData, sellBackTypedData, takerAcceptTypedData, payoutNonce, transferWithAuthorizationTypedData, } from "./typed-data.js";
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
- async function signEntryWith(request, funding) {
628
- const { quote, cash } = request;
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 = signer.signTypedData(takerAcceptTypedData(chain, quote.entry));
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
- bridgeAmount > 0n ? signSendAsset(signer, bridgeAction(hyperliquidNetwork(chain.chain_id), bridgeAmount, {
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 { signer, account, chain, scope } = await recordScope();
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 signer.signTypedData(typedData);
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, take a quote from those the stream
778
- * pushes, sign it (`signEntry`), hand the record to `onSigned` to persist,
779
- * then `sendAcceptance`. Returns the acceptance's status (`ACCEPTED` with
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. An accept refused with
787
- * `QUOTE_EXPIRED` or `QUOTE_REPLACED` takes and signs a fresh quote once
788
- * (`onSigned` gets the new record); a second refusal is thrown.
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
- // Only quotes for exactly this position are offered to the chooser, and the chosen one is checked again before signing.
801
- const matches = (quote) => quote.kind === "ENTRY" && quote.rfq_id === rfqId &&
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
- const take = (refused) => signedOrCancel(rfqId, async () => {
805
- const offered = (quote) => matches(quote) && quote.quote_id !== refused;
806
- const selected = await awaitOffer(rfqId, offered, choose, collectMs, quoteTimeoutMs, signal);
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, core: request.core, cash: request.cash ?? await reading, sourceDex: request.sourceDex }, funding === "wallet" ? "plan" : funding);
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
- let result;
824
- try {
825
- result = await send();
826
- }
827
- catch (error) {
828
- if (!(error instanceof ApiError) || error.code !== "QUOTE_EXPIRED" && error.code !== "QUOTE_REPLACED")
829
- throw error;
830
- // The quote went away before the accept (sendAcceptance discarded its bridge): sign a fresh one, once. No fresh quote in time is still that refusal.
831
- ({ selected, signed } = await take(signed.quoteId).catch((retried) => Promise.reject(retried instanceof Error && retried.name === "TimeoutError" ? error : retried)));
832
- record = signed;
833
- result = await send();
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). */
@@ -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
- /** Who signs each struct: the maker's signer, the taker's signer, or either side of a cash-out. */
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";
@@ -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
- /** Who signs each struct: the maker's signer, the taker's signer, or either side of a cash-out. */
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.75.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": [