@totalis/hip4-client 0.0.0-stage → 0.73.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +1903 -0
  2. package/LICENSE +202 -0
  3. package/README.md +143 -2
  4. package/dist/api-keys.d.ts +118 -0
  5. package/dist/api-keys.js +104 -0
  6. package/dist/audience-contract.d.ts +52 -0
  7. package/dist/audience-contract.js +1 -0
  8. package/dist/base.d.ts +41 -0
  9. package/dist/base.js +60 -0
  10. package/dist/client.d.ts +16 -0
  11. package/dist/client.js +15 -0
  12. package/dist/controller.d.ts +53 -0
  13. package/dist/controller.js +63 -0
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +3 -0
  16. package/dist/flow-errors.d.ts +62 -0
  17. package/dist/flow-errors.js +70 -0
  18. package/dist/generated/account-read.d.ts +2015 -0
  19. package/dist/generated/account-read.js +1 -0
  20. package/dist/generated/api-keys.d.ts +1303 -0
  21. package/dist/generated/api-keys.js +1 -0
  22. package/dist/generated/browse.d.ts +965 -0
  23. package/dist/generated/browse.js +1 -0
  24. package/dist/generated/client.d.ts +104 -0
  25. package/dist/generated/client.js +1 -0
  26. package/dist/generated/errors.d.ts +467 -0
  27. package/dist/generated/errors.js +1792 -0
  28. package/dist/generated/maker.d.ts +47 -0
  29. package/dist/generated/maker.js +1 -0
  30. package/dist/generated/openapi.d.ts +10867 -0
  31. package/dist/generated/openapi.js +1 -0
  32. package/dist/generated/operator.d.ts +7 -0
  33. package/dist/generated/operator.js +1 -0
  34. package/dist/generated/realtime.d.ts +1486 -0
  35. package/dist/generated/realtime.js +1 -0
  36. package/dist/generated/vault-abi.d.ts +49 -0
  37. package/dist/generated/vault-abi.js +70 -0
  38. package/dist/generated/version.d.ts +24 -0
  39. package/dist/generated/version.js +183 -0
  40. package/dist/hypercore.d.ts +111 -0
  41. package/dist/hypercore.js +132 -0
  42. package/dist/maker.d.ts +206 -0
  43. package/dist/maker.js +608 -0
  44. package/dist/money.d.ts +59 -0
  45. package/dist/money.js +66 -0
  46. package/dist/operator.d.ts +4 -0
  47. package/dist/operator.js +4 -0
  48. package/dist/pagination.d.ts +20 -0
  49. package/dist/pagination.js +27 -0
  50. package/dist/partner.d.ts +71 -0
  51. package/dist/partner.js +66 -0
  52. package/dist/realtime.d.ts +19 -0
  53. package/dist/realtime.js +10 -0
  54. package/dist/request.d.ts +115 -0
  55. package/dist/request.js +190 -0
  56. package/dist/signer.d.ts +51 -0
  57. package/dist/signer.js +90 -0
  58. package/dist/stream.d.ts +142 -0
  59. package/dist/stream.js +331 -0
  60. package/dist/totalis.d.ts +719 -0
  61. package/dist/totalis.js +1251 -0
  62. package/dist/typed-data.d.ts +330 -0
  63. package/dist/typed-data.js +340 -0
  64. package/dist/ui.d.ts +54 -0
  65. package/dist/ui.js +77 -0
  66. package/dist/wallet-session.d.ts +52 -0
  67. package/dist/wallet-session.js +63 -0
  68. package/examples/browser-wallet.html +69 -0
  69. package/examples/browser-wallet.mjs +57 -0
  70. package/examples/node-bot.mjs +57 -0
  71. package/examples/onboard.mjs +132 -0
  72. package/examples/partner-frontend.mjs +44 -0
  73. package/examples/partner-server.mjs +22 -0
  74. package/package.json +105 -4
package/dist/money.js ADDED
@@ -0,0 +1,66 @@
1
+ const bps = 10000n;
2
+ /**
3
+ * Spendable Totalis cash from `GET /v1/me/balances`, in USDC base units, as
4
+ * the SDK plans entries and withdrawals with it. `null` means unknown (the
5
+ * component is not `CURRENT`), never 0: show it as unknown and read again.
6
+ * - `vault`: free vault cash (`VAULT_FREE`).
7
+ * - `wallet`: wallet USDC (`WALLET_USDC`) less `pending.wallet_reserved`
8
+ * (open withdrawals, stake fundings and wallet deposits not yet committed),
9
+ * floored at 0.
10
+ * - `total`: the venue's spendable `amount`; it subtracts withdrawals and
11
+ * stake fundings but not a pending wallet deposit, which moves into the
12
+ * vault, so it can exceed `vault + wallet` while a deposit is pending.
13
+ */
14
+ export function spendableCash(balance) {
15
+ const component = (kind) => {
16
+ const entry = balance.components.find((candidate) => candidate.kind === kind);
17
+ return entry === undefined || entry.state !== "CURRENT" || entry.amount === null ? null : BigInt(entry.amount);
18
+ };
19
+ const walletUsdc = component("WALLET_USDC");
20
+ // As the venue computes it: reservations beyond the wallet leave nothing available, never less.
21
+ const wallet = walletUsdc === null ? null : walletUsdc - BigInt(balance.pending.wallet_reserved);
22
+ return {
23
+ vault: component("VAULT_FREE"),
24
+ wallet: wallet === null ? null : wallet > 0n ? wallet : 0n,
25
+ total: balance.state === "CURRENT" && balance.amount !== null ? BigInt(balance.amount) : null,
26
+ };
27
+ }
28
+ /**
29
+ * What the owner receives from a gross close amount (a settlement payout or a
30
+ * sell-back price), as the vault pays it: the profit fee is
31
+ * `floor((amount - netStake) * feeBps / 10000)` on the part above the
32
+ * position's net stake, and nothing at or below it. `feeBps` and `netStake`
33
+ * are the position's own (`fee_bps`, `net_stake`), fixed when it opened.
34
+ */
35
+ export function netOfProfitFee(amount, netStake, feeBps) {
36
+ const gross = BigInt(amount);
37
+ const profit = gross - BigInt(netStake);
38
+ return profit > 0n ? gross - profit * BigInt(feeBps) / bps : gross;
39
+ }
40
+ /** A quote's net stake: the stake less the entry fee, `floor(stake * taker_fee_bps / 10000)`. */
41
+ export function netStake(terms) {
42
+ const stake = BigInt(terms.stake);
43
+ return stake - stake * BigInt(terms.taker_fee_bps) / bps;
44
+ }
45
+ /**
46
+ * What the taker receives if every leg wins, net of the profit fee: the
47
+ * figure to show next to an entry quote, whose `payout` is gross. A fractional
48
+ * or partial result pays less; `netOfProfitFee` applies to whatever the
49
+ * position settles at. Example: stake 3 USDC at 20 bps entry fee and payout
50
+ * 9.18 USDC at 100 bps profit fee nets 9.118140 USDC.
51
+ */
52
+ export function netPayout(quote) {
53
+ const terms = "entry" in quote ? quote.entry : quote;
54
+ return netOfProfitFee(terms.payout, netStake(terms), terms.fee_bps);
55
+ }
56
+ /**
57
+ * What the owner receives from a cash-out quote. A `TRANSFER` pays its price
58
+ * in full; a `SELL_BACK` withholds the position's profit fee on the price
59
+ * above its net stake. `position` is the position read (`GET
60
+ * /v1/me/positions/{position_id}`); `null` when its `net_stake` is unknown.
61
+ */
62
+ export function netCashout(bid, position) {
63
+ if (bid.kind === "TRANSFER")
64
+ return BigInt(bid.transfer.price);
65
+ return position.net_stake === null ? null : netOfProfitFee(bid.sell_back.price, position.net_stake, position.fee_bps);
66
+ }
@@ -0,0 +1,4 @@
1
+ import { type ClientOptions } from "openapi-fetch";
2
+ import type { paths } from "./generated/operator.js";
3
+ export type { components, operations, paths } from "./generated/operator.js";
4
+ export declare function createOperatorClient(privateOrigin: string, options?: Omit<ClientOptions, "baseUrl">): import("openapi-fetch").Client<paths, `${string}/${string}`>;
@@ -0,0 +1,4 @@
1
+ import createOpenApiClient from "openapi-fetch";
2
+ export function createOperatorClient(privateOrigin, options = {}) {
3
+ return createOpenApiClient({ ...options, baseUrl: privateOrigin });
4
+ }
@@ -0,0 +1,20 @@
1
+ /** A list response in the TT-1771 shape: items plus top-level `next_cursor` and `has_more`. */
2
+ export type Page = {
3
+ readonly next_cursor: string | null;
4
+ readonly has_more: boolean;
5
+ };
6
+ /** Spread into the query of a list read; the first page omits `cursor`. */
7
+ export type PageQuery = {
8
+ cursor?: string;
9
+ };
10
+ /**
11
+ * Read a list page by page: the first request omits `cursor`, each later one
12
+ * sends the previous `next_cursor` unchanged, until `has_more` is false. A page
13
+ * may be empty while `has_more` is true.
14
+ *
15
+ * ```ts
16
+ * for await (const page of pages((query) => client.GET("/v1/me/positions", { params: { query: { ...query, limit: 100 } } }).then(unwrap))) …
17
+ * ```
18
+ */
19
+ export declare function pages<P extends Page>(read: (query: PageQuery) => Promise<P>): AsyncGenerator<P, void>;
20
+ export declare function items<P extends Page, Item>(read: (query: PageQuery) => Promise<P>, select: (page: P) => readonly Item[]): AsyncGenerator<Item, void>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Read a list page by page: the first request omits `cursor`, each later one
3
+ * sends the previous `next_cursor` unchanged, until `has_more` is false. A page
4
+ * may be empty while `has_more` is true.
5
+ *
6
+ * ```ts
7
+ * for await (const page of pages((query) => client.GET("/v1/me/positions", { params: { query: { ...query, limit: 100 } } }).then(unwrap))) …
8
+ * ```
9
+ */
10
+ export async function* pages(read) {
11
+ const seen = new Set();
12
+ let query = {};
13
+ for (;;) {
14
+ const page = await read(query);
15
+ yield page;
16
+ if (!page.has_more)
17
+ return;
18
+ if (page.next_cursor === null || seen.has(page.next_cursor))
19
+ throw new Error("list says has_more without a new next_cursor");
20
+ seen.add(page.next_cursor);
21
+ query = { cursor: page.next_cursor };
22
+ }
23
+ }
24
+ export async function* items(read, select) {
25
+ for await (const page of pages(read))
26
+ yield* select(page);
27
+ }
@@ -0,0 +1,71 @@
1
+ import { type ApiClientOptions, type PublicEnvironment } from "./base.js";
2
+ import type { components } from "./generated/client.js";
3
+ import { type SignerInput } from "./signer.js";
4
+ export type WalletAdmission = components["schemas"]["WalletAdmission"];
5
+ /** Your partner's fees, terms, balance and payouts (TT-2420). Amounts are atomic USDC strings. */
6
+ export type PartnerReport = components["schemas"]["PartnerReport"];
7
+ /** A wallet's signed consent to one partner opening its account (TT-1688). */
8
+ export type AdmissionConsent = {
9
+ readonly expires_at: string;
10
+ readonly signature: string;
11
+ };
12
+ /**
13
+ * The exact message a wallet signs (personal_sign) to let `partner`, your
14
+ * partner slug, open its Totalis account. The venue rebuilds the same text from
15
+ * your server key and refuses a consent for another partner, wallet,
16
+ * environment or chain, or one expired or more than a day ahead.
17
+ */
18
+ export declare function admissionConsentMessage(terms: {
19
+ readonly partner: string;
20
+ readonly wallet: string;
21
+ readonly environment: PublicEnvironment;
22
+ readonly chainId: string;
23
+ readonly expiresAt: string;
24
+ }): string;
25
+ /**
26
+ * Have the wallet consent to your partner opening its account, in your
27
+ * frontend before sign-up. Send the result to your server for `admitWallet`.
28
+ * `chainId` is `GET /v1/deployment` `chain_id`. The consent lasts an hour by
29
+ * default (at most a day).
30
+ */
31
+ export declare function signAdmissionConsent(request: {
32
+ readonly signer: SignerInput;
33
+ readonly partner: string;
34
+ readonly environment: PublicEnvironment;
35
+ readonly chainId: string;
36
+ readonly lifetimeSeconds?: number;
37
+ }): Promise<{
38
+ readonly address: string;
39
+ readonly consent: AdmissionConsent;
40
+ }>;
41
+ export type PartnerServerOptions = Omit<ApiClientOptions, "accessToken" | "partnerKey"> & {
42
+ readonly environment: PublicEnvironment;
43
+ /** The partner's secret server key (`tpk_v1_…`). Keep it on your server; never ship it to a browser. */
44
+ readonly serverKey: string;
45
+ };
46
+ /**
47
+ * The partner's server calls (TT-1678). During the invite-only launch a new
48
+ * wallet gets an account only through a partner: admit it here with the
49
+ * wallet's consent (`signAdmissionConsent` in your frontend), and its next
50
+ * wallet sign-in creates the account, attributed to you. `report` reads your
51
+ * fees, terms and payouts (TT-2420). Refuses to run in a browser, where the
52
+ * server key would be exposed.
53
+ */
54
+ export declare function createPartnerServer(options: PartnerServerOptions): {
55
+ /**
56
+ * Admit a consenting wallet. Admitting it again returns the first admission;
57
+ * another partner's wallet is STATE_CONFLICT, and a consent that does not
58
+ * verify is INVALID_SIGNATURE.
59
+ */
60
+ admitWallet(address: string, consent: AdmissionConsent, idempotencyKey?: string): Promise<WalletAdmission>;
61
+ /**
62
+ * Your report: fees per UTC day for `[from, to)` (YYYY-MM-DD, at most 366
63
+ * days; omit both for the 30 days through today), with your terms and,
64
+ * all time, what you earned, were paid and are owed, and every payout.
65
+ */
66
+ report(range?: {
67
+ readonly from: string;
68
+ readonly to: string;
69
+ }): Promise<PartnerReport>;
70
+ };
71
+ export type PartnerServer = ReturnType<typeof createPartnerServer>;
@@ -0,0 +1,66 @@
1
+ import { audienceClient } from "./base.js";
2
+ import { unwrap, uuidv7 } from "./request.js";
3
+ import { toTotalisSigner } from "./signer.js";
4
+ /** The key environment a consent names, from the SDK's environment. */
5
+ const keyEnvironment = { production: "production", staging: "staging", local: "development" };
6
+ /**
7
+ * The exact message a wallet signs (personal_sign) to let `partner`, your
8
+ * partner slug, open its Totalis account. The venue rebuilds the same text from
9
+ * your server key and refuses a consent for another partner, wallet,
10
+ * environment or chain, or one expired or more than a day ahead.
11
+ */
12
+ export function admissionConsentMessage(terms) {
13
+ return `Allow ${terms.partner} to open a Totalis account for this wallet.\nThis does not authorize a financial action.\n\n`
14
+ + `Wallet: ${terms.wallet.toLowerCase()}\nPartner: ${terms.partner}\nEnvironment: ${keyEnvironment[terms.environment]}\nChain ID: ${terms.chainId}\nExpires At: ${terms.expiresAt}`;
15
+ }
16
+ /**
17
+ * Have the wallet consent to your partner opening its account, in your
18
+ * frontend before sign-up. Send the result to your server for `admitWallet`.
19
+ * `chainId` is `GET /v1/deployment` `chain_id`. The consent lasts an hour by
20
+ * default (at most a day).
21
+ */
22
+ export async function signAdmissionConsent(request) {
23
+ const signer = toTotalisSigner(request.signer);
24
+ const address = (await signer.address()).toLowerCase();
25
+ // The venue verifies the expiry exactly as sent; whole seconds keep it short.
26
+ const expiresAt = new Date(Math.floor(Date.now() / 1000) * 1000 + (request.lifetimeSeconds ?? 3600) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
27
+ const message = admissionConsentMessage({ partner: request.partner, wallet: address, environment: request.environment, chainId: request.chainId, expiresAt });
28
+ return { address, consent: { expires_at: expiresAt, signature: await signer.signMessage(message) } };
29
+ }
30
+ /**
31
+ * The partner's server calls (TT-1678). During the invite-only launch a new
32
+ * wallet gets an account only through a partner: admit it here with the
33
+ * wallet's consent (`signAdmissionConsent` in your frontend), and its next
34
+ * wallet sign-in creates the account, attributed to you. `report` reads your
35
+ * fees, terms and payouts (TT-2420). Refuses to run in a browser, where the
36
+ * server key would be exposed.
37
+ */
38
+ export function createPartnerServer(options) {
39
+ if (typeof globalThis.document !== "undefined") {
40
+ throw new Error("createPartnerServer runs on your server; a browser would expose the tpk_ server key");
41
+ }
42
+ const { environment, serverKey, ...clientOptions } = options;
43
+ if (!serverKey.startsWith("tpk_v1_"))
44
+ throw new Error("serverKey is not a tpk_v1 partner server key");
45
+ const client = audienceClient(environment, { ...clientOptions, accessToken: serverKey });
46
+ return {
47
+ /**
48
+ * Admit a consenting wallet. Admitting it again returns the first admission;
49
+ * another partner's wallet is STATE_CONFLICT, and a consent that does not
50
+ * verify is INVALID_SIGNATURE.
51
+ */
52
+ async admitWallet(address, consent, idempotencyKey = uuidv7()) {
53
+ return unwrap(await client.POST("/v1/partner/wallet-admissions", {
54
+ params: { header: { "Idempotency-Key": idempotencyKey } }, body: { address, consent },
55
+ }));
56
+ },
57
+ /**
58
+ * Your report: fees per UTC day for `[from, to)` (YYYY-MM-DD, at most 366
59
+ * days; omit both for the 30 days through today), with your terms and,
60
+ * all time, what you earned, were paid and are owed, and every payout.
61
+ */
62
+ async report(range) {
63
+ return unwrap(await client.GET("/v1/partner/report", { params: { query: range ?? {} } }));
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,19 @@
1
+ export { publicStreamServers, streamServerUrl } from "./base.js";
2
+ import type { PublicEnvironment } from "./base.js";
3
+ export type { components } from "./generated/realtime.js";
4
+ export type { CloseReason as RealtimeCloseReason } from "./generated/errors.js";
5
+ import type { components } from "./generated/realtime.js";
6
+ export { marketStreamProtocol, marketStreamUrl, realtimeCredentialProtocol, realtimeProtocol, realtimeUrl, StreamClosedError, TotalisStream, type RealtimeCursor, type StreamEvent, type StreamEventFrame, type StreamEventOf, type StreamEventType, type StreamFrame, type StreamOptions, type StreamSnapshot, type StreamStatus, type StreamSubscription, type WebSocketFactory, } from "./stream.js";
7
+ export type RealtimeSubscribe = components["schemas"]["Subscribe"];
8
+ export type RealtimeSubscriptionResponse = components["schemas"]["SubscriptionResponse"];
9
+ export type RealtimeSnapshot = components["schemas"]["SnapshotFrame"];
10
+ export type RealtimeEvent = components["schemas"]["EventFrame"];
11
+ export type RealtimeReset = components["schemas"]["ResetFrame"];
12
+ export type RealtimeDrain = components["schemas"]["DrainFrame"];
13
+ export type RealtimeMarketRefresh = components["schemas"]["MarketRefresh"];
14
+ export type RealtimePublication = components["schemas"]["PublicationFrame"];
15
+ export type RealtimeReadInvalidation = components["schemas"]["ReadInvalidatedFrame"];
16
+ export type RealtimeFrame = RealtimeReadInvalidation | RealtimePublication | RealtimeSubscriptionResponse | RealtimeSnapshot | RealtimeEvent | RealtimeReset | RealtimeDrain | RealtimeMarketRefresh;
17
+ type WebSocketConstructor = new (url: string, protocols?: string | string[]) => WebSocket;
18
+ /** A raw socket with the reviewed subprotocols. `TotalisStream` adds the subscribe frame, replay and reconnect. */
19
+ export declare function createRealtimeSocket(environment: PublicEnvironment, accessToken: string, Socket?: WebSocketConstructor): WebSocket;
@@ -0,0 +1,10 @@
1
+ export { publicStreamServers, streamServerUrl } from "./base.js";
2
+ import { realtimeCredentialProtocol, realtimeProtocol, realtimeUrl } from "./stream.js";
3
+ export { marketStreamProtocol, marketStreamUrl, realtimeCredentialProtocol, realtimeProtocol, realtimeUrl, StreamClosedError, TotalisStream, } from "./stream.js";
4
+ /** A raw socket with the reviewed subprotocols. `TotalisStream` adds the subscribe frame, replay and reconnect. */
5
+ export function createRealtimeSocket(environment, accessToken, Socket = WebSocket) {
6
+ return new Socket(realtimeUrl(environment), [
7
+ realtimeProtocol,
8
+ realtimeCredentialProtocol(accessToken),
9
+ ]);
10
+ }
@@ -0,0 +1,115 @@
1
+ import { type ErrorCode, type Retry } from "./generated/errors.js";
2
+ /** A lowercase RFC 9562 UUIDv7: the venue requires it for request IDs and idempotency keys. */
3
+ export declare function uuidv7(now?: number): string;
4
+ /** 32 random bytes as 0x-hex, for salts and nonces. */
5
+ export declare function randomBytes32(): `0x${string}`;
6
+ /**
7
+ * The envelope core exits and maker onboarding still carry. Every other
8
+ * command is sent bare. Build it once and resend it unchanged.
9
+ */
10
+ export declare function envelope<Command>(command: Command, requestId?: string): {
11
+ schema_version: 1;
12
+ request_id: string;
13
+ sent_at: string;
14
+ command: Command;
15
+ };
16
+ type FieldViolation = {
17
+ readonly code: string;
18
+ readonly field: string;
19
+ };
20
+ /**
21
+ * A registered error from the venue (TT-1770). `retry` says what to do:
22
+ * NEVER stop; BACKOFF resend the same request (the SDK already did, until its
23
+ * attempts ran out); REFRESH re-read state, then decide on a new request.
24
+ */
25
+ export declare class ApiError extends Error {
26
+ readonly status: number;
27
+ readonly code: ErrorCode | `HTTP_${number}`;
28
+ readonly retry: Retry;
29
+ readonly requestId?: string | undefined;
30
+ readonly fieldViolations: readonly FieldViolation[];
31
+ readonly retryAfterMs?: number | undefined;
32
+ readonly name = "ApiError";
33
+ /** For a flow command answered BACKOFF after every resend, or whose resend was refused after an earlier send's answer was lost and the outcome could not be read: send the identical request again later. */
34
+ resend?: () => Promise<unknown>;
35
+ constructor(status: number, code: ErrorCode | `HTTP_${number}`, retry: Retry, message: string, requestId?: string | undefined, fieldViolations?: readonly FieldViolation[], retryAfterMs?: number | undefined);
36
+ static from(response: Response, body: unknown): ApiError;
37
+ }
38
+ /**
39
+ * The request may or may not have reached the venue. Flows attach `resend()`,
40
+ * which sends the identical request (same bytes, same idempotency key) and
41
+ * returns its result: the venue replays a command it already accepted.
42
+ */
43
+ export declare class NetworkError extends Error {
44
+ readonly request: {
45
+ readonly method: string;
46
+ readonly url: string;
47
+ };
48
+ readonly name = "NetworkError";
49
+ resend?: () => Promise<unknown>;
50
+ constructor(request: {
51
+ readonly method: string;
52
+ readonly url: string;
53
+ }, options: {
54
+ cause: unknown;
55
+ });
56
+ }
57
+ /** Retry-After in milliseconds: delta-seconds or an HTTP date. */
58
+ export declare function retryAfterMs(response: Response, now?: number): number | undefined;
59
+ /** Exponential backoff with equal jitter: half the capped delay, plus up to half again at random. */
60
+ export declare function backoffDelay(retry: number, baseDelayMs: number, maxDelayMs: number): number;
61
+ export type RetryPolicy = {
62
+ /** Total sends, including the first. Default 5. */
63
+ readonly attempts?: number;
64
+ readonly baseDelayMs?: number;
65
+ readonly maxDelayMs?: number;
66
+ /** The longest Retry-After waited out before a resend. Default 30 s; a longer one is returned as is, so the caller's error carries `retryAfterMs`. */
67
+ readonly maxRetryAfterMs?: number;
68
+ readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
69
+ };
70
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
71
+ /**
72
+ * Wrap fetch so each request is built once and resent byte for byte, with the
73
+ * same Idempotency-Key, after a network failure or a BACKOFF error (honouring
74
+ * a Retry-After up to `maxRetryAfterMs`; a longer one is not waited out). A
75
+ * network failure is only retried when resending is safe: a read, a DELETE,
76
+ * or a command that carries an Idempotency-Key. REFRESH and NEVER responses
77
+ * are returned untouched for the caller to surface.
78
+ */
79
+ export type Fetch = (request: Request) => Promise<Response>;
80
+ export declare function retryingFetch(fetchImpl?: Fetch, policy?: RetryPolicy): Fetch;
81
+ /**
82
+ * One send with no resends, for callers that own their retries. A request
83
+ * lost before the response, or a body cut off while it is read, throws
84
+ * NetworkError; the body is read here so the caller parses it from memory.
85
+ */
86
+ export declare function singleFetch(fetchImpl?: Fetch): Fetch;
87
+ /**
88
+ * openapi-fetch's response typing maps over plain arrays only, so the
89
+ * generated readonly arrays come back as array-like objects. Restore them.
90
+ */
91
+ export type Repaired<T> = T extends string | number | boolean | bigint | null | undefined ? T : T extends {
92
+ readonly [index: number]: infer Item;
93
+ readonly length: number;
94
+ } ? readonly Repaired<Item>[] : T extends object ? {
95
+ readonly [K in keyof T]: Repaired<T[K]>;
96
+ } : T;
97
+ /** A failure whose command may still be resent unchanged: lost responses, or BACKOFF past the resend budget. */
98
+ export declare function resendable(error: unknown): error is NetworkError | ApiError;
99
+ /**
100
+ * Send a command built once. If every response is lost, or BACKOFF outlasts
101
+ * the resends, the error carries `resend()` for the identical request, so the
102
+ * caller can finish it instead of signing a second command.
103
+ */
104
+ export declare function sendOnce<Data>(send: () => Promise<{
105
+ data?: Data;
106
+ error?: unknown;
107
+ response: Response;
108
+ }>): Promise<Repaired<Data>>;
109
+ /** The data of an openapi-fetch result, or its registered error thrown as ApiError. */
110
+ export declare function unwrap<Data>(result: {
111
+ data?: Data;
112
+ error?: unknown;
113
+ response: Response;
114
+ }): Repaired<Data>;
115
+ export {};
@@ -0,0 +1,190 @@
1
+ import { errorRegistry } from "./generated/errors.js";
2
+ /** A lowercase RFC 9562 UUIDv7: the venue requires it for request IDs and idempotency keys. */
3
+ export function uuidv7(now = Date.now()) {
4
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
5
+ for (let index = 0; index < 6; index++)
6
+ bytes[index] = Math.floor(now / 2 ** (8 * (5 - index))) & 0xff;
7
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
8
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
9
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
10
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
11
+ }
12
+ /** 32 random bytes as 0x-hex, for salts and nonces. */
13
+ export function randomBytes32() {
14
+ return `0x${Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
15
+ }
16
+ /**
17
+ * The envelope core exits and maker onboarding still carry. Every other
18
+ * command is sent bare. Build it once and resend it unchanged.
19
+ */
20
+ export function envelope(command, requestId = uuidv7()) {
21
+ return { schema_version: 1, request_id: requestId, sent_at: new Date().toISOString(), command };
22
+ }
23
+ /**
24
+ * A registered error from the venue (TT-1770). `retry` says what to do:
25
+ * NEVER stop; BACKOFF resend the same request (the SDK already did, until its
26
+ * attempts ran out); REFRESH re-read state, then decide on a new request.
27
+ */
28
+ export class ApiError extends Error {
29
+ status;
30
+ code;
31
+ retry;
32
+ requestId;
33
+ fieldViolations;
34
+ retryAfterMs;
35
+ name = "ApiError";
36
+ /** For a flow command answered BACKOFF after every resend, or whose resend was refused after an earlier send's answer was lost and the outcome could not be read: send the identical request again later. */
37
+ resend;
38
+ constructor(status, code, retry, message, requestId, fieldViolations = [], retryAfterMs) {
39
+ super(`${code}: ${message}`);
40
+ this.status = status;
41
+ this.code = code;
42
+ this.retry = retry;
43
+ this.requestId = requestId;
44
+ this.fieldViolations = fieldViolations;
45
+ this.retryAfterMs = retryAfterMs;
46
+ }
47
+ static from(response, body) {
48
+ const detail = body?.error;
49
+ const code = detail?.code ?? `HTTP_${response.status}`;
50
+ const retry = detail?.retry ?? (code in errorRegistry ? errorRegistry[code].retry : undefined) ?? (transient(response.status) ? "BACKOFF" : "NEVER");
51
+ return new ApiError(response.status, code, retry, detail?.message ?? response.statusText, detail?.request_id, detail?.field_violations, retryAfterMs(response));
52
+ }
53
+ }
54
+ /**
55
+ * The request may or may not have reached the venue. Flows attach `resend()`,
56
+ * which sends the identical request (same bytes, same idempotency key) and
57
+ * returns its result: the venue replays a command it already accepted.
58
+ */
59
+ export class NetworkError extends Error {
60
+ request;
61
+ name = "NetworkError";
62
+ resend;
63
+ constructor(request, options) {
64
+ super(`${request.method} ${request.url} failed without a response`, options);
65
+ this.request = request;
66
+ }
67
+ }
68
+ const transient = (status) => status === 429 || status === 502 || status === 503 || status === 504;
69
+ /** Retry-After in milliseconds: delta-seconds or an HTTP date. */
70
+ export function retryAfterMs(response, now = Date.now()) {
71
+ const header = response.headers.get("Retry-After")?.trim();
72
+ if (!header)
73
+ return undefined;
74
+ if (/^[0-9]+$/.test(header))
75
+ return Number(header) * 1000;
76
+ const date = Date.parse(header);
77
+ return Number.isNaN(date) ? undefined : Math.max(0, date - now);
78
+ }
79
+ /** Exponential backoff with equal jitter: half the capped delay, plus up to half again at random. */
80
+ export function backoffDelay(retry, baseDelayMs, maxDelayMs) {
81
+ return Math.min(maxDelayMs, baseDelayMs * 2 ** retry) * (0.5 + Math.random() / 2);
82
+ }
83
+ export function sleep(ms, signal) {
84
+ return new Promise((resolve, reject) => {
85
+ if (signal?.aborted)
86
+ return reject(signal.reason);
87
+ const timer = setTimeout(() => {
88
+ signal?.removeEventListener("abort", abort);
89
+ resolve();
90
+ }, ms);
91
+ const abort = () => {
92
+ clearTimeout(timer);
93
+ reject(signal?.reason);
94
+ };
95
+ signal?.addEventListener("abort", abort, { once: true });
96
+ });
97
+ }
98
+ async function wantsBackoff(response) {
99
+ if (response.ok)
100
+ return false;
101
+ const body = await response.clone().json().catch(() => undefined);
102
+ const retry = body?.error?.retry;
103
+ return retry === undefined ? transient(response.status) : retry === "BACKOFF";
104
+ }
105
+ export function retryingFetch(fetchImpl = (request) => globalThis.fetch(request), policy = {}) {
106
+ const { attempts = 5, baseDelayMs = 250, maxDelayMs = 10_000, maxRetryAfterMs = 30_000, sleep: wait = sleep } = policy;
107
+ return async (original) => {
108
+ const body = original.body === null ? null : new Uint8Array(await original.arrayBuffer());
109
+ const replayable = ["GET", "HEAD", "DELETE"].includes(original.method) || original.headers.has("Idempotency-Key");
110
+ const build = () => new Request(original.url, {
111
+ method: original.method, headers: original.headers, body, signal: original.signal,
112
+ credentials: original.credentials, cache: original.cache, redirect: original.redirect,
113
+ mode: original.mode, integrity: original.integrity, referrer: original.referrer,
114
+ referrerPolicy: original.referrerPolicy, keepalive: original.keepalive,
115
+ });
116
+ for (let attempt = 1;; attempt++) {
117
+ const backoff = backoffDelay(attempt - 1, baseDelayMs, maxDelayMs);
118
+ let response;
119
+ try {
120
+ response = await fetchImpl(build());
121
+ }
122
+ catch (cause) {
123
+ if (original.signal.aborted)
124
+ throw cause;
125
+ if (!replayable || attempt >= attempts)
126
+ throw new NetworkError(original, { cause });
127
+ await wait(backoff, original.signal);
128
+ continue;
129
+ }
130
+ if (attempt >= attempts || !(await wantsBackoff(response)))
131
+ return response;
132
+ const after = retryAfterMs(response);
133
+ if (after !== undefined && after > maxRetryAfterMs)
134
+ return response;
135
+ await response.body?.cancel();
136
+ await wait(after ?? backoff, original.signal);
137
+ }
138
+ };
139
+ }
140
+ /**
141
+ * One send with no resends, for callers that own their retries. A request
142
+ * lost before the response, or a body cut off while it is read, throws
143
+ * NetworkError; the body is read here so the caller parses it from memory.
144
+ */
145
+ export function singleFetch(fetchImpl = (request) => globalThis.fetch(request)) {
146
+ return async (request) => {
147
+ let response;
148
+ try {
149
+ response = await fetchImpl(request);
150
+ }
151
+ catch (cause) {
152
+ if (request.signal.aborted)
153
+ throw cause;
154
+ throw new NetworkError(request, { cause });
155
+ }
156
+ try {
157
+ return new Response(response.body === null ? null : await response.arrayBuffer(), response);
158
+ }
159
+ catch (cause) {
160
+ if (request.signal.aborted)
161
+ throw cause;
162
+ throw new NetworkError(request, { cause });
163
+ }
164
+ };
165
+ }
166
+ /** A failure whose command may still be resent unchanged: lost responses, or BACKOFF past the resend budget. */
167
+ export function resendable(error) {
168
+ return error instanceof NetworkError || (error instanceof ApiError && error.retry === "BACKOFF");
169
+ }
170
+ /**
171
+ * Send a command built once. If every response is lost, or BACKOFF outlasts
172
+ * the resends, the error carries `resend()` for the identical request, so the
173
+ * caller can finish it instead of signing a second command.
174
+ */
175
+ export async function sendOnce(send) {
176
+ try {
177
+ return unwrap(await send());
178
+ }
179
+ catch (error) {
180
+ if (resendable(error))
181
+ error.resend = () => sendOnce(send);
182
+ throw error;
183
+ }
184
+ }
185
+ /** The data of an openapi-fetch result, or its registered error thrown as ApiError. */
186
+ export function unwrap(result) {
187
+ if (result.error !== undefined || !result.response.ok)
188
+ throw ApiError.from(result.response, result.error);
189
+ return result.data;
190
+ }
@@ -0,0 +1,51 @@
1
+ import { type Hex, type TypedData } from "./typed-data.js";
2
+ /**
3
+ * The one signer shape the SDK uses. It never holds a key: every call goes to
4
+ * the wallet it adapts. Takers sign with their account's `signer`, makers with
5
+ * their quote signer, and API key issuance with the account's `owner`.
6
+ */
7
+ export type TotalisSigner = {
8
+ address(): Promise<Hex>;
9
+ signTypedData(typedData: TypedData): Promise<Hex>;
10
+ /** EIP-191 personal_sign of a UTF-8 message. */
11
+ signMessage(message: string): Promise<Hex>;
12
+ };
13
+ /** A viem account (`privateKeyToAccount`, `toAccount`, a JSON-RPC account). */
14
+ export type ViemAccountLike = {
15
+ readonly address: string;
16
+ signTypedData(parameters: TypedData): Promise<string>;
17
+ signMessage(parameters: {
18
+ message: string;
19
+ }): Promise<string>;
20
+ };
21
+ /** A viem wallet client. Its hoisted `account` is used, else its first address. */
22
+ export type ViemWalletClientLike = {
23
+ readonly account?: {
24
+ readonly address: string;
25
+ } | undefined;
26
+ getAddresses(): Promise<readonly string[]>;
27
+ signTypedData(parameters: never): Promise<string>;
28
+ signMessage(parameters: never): Promise<string>;
29
+ };
30
+ /** An EIP-1193 provider (`window.ethereum`, a WalletConnect or embedded-wallet provider). */
31
+ export type Eip1193ProviderLike = {
32
+ request(arguments_: never): Promise<unknown>;
33
+ };
34
+ type EthersTypes = Record<string, readonly {
35
+ name: string;
36
+ type: string;
37
+ }[]>;
38
+ /** An ethers v6 `Signer` (v5's `_signTypedData` also works). */
39
+ export type EthersSignerLike = {
40
+ getAddress(): Promise<string>;
41
+ signMessage(message: string): Promise<string>;
42
+ signTypedData?(domain: TypedData["domain"], types: EthersTypes, value: Record<string, unknown>): Promise<string>;
43
+ _signTypedData?(domain: TypedData["domain"], types: EthersTypes, value: Record<string, unknown>): Promise<string>;
44
+ };
45
+ export type SignerInput = TotalisSigner | ViemAccountLike | ViemWalletClientLike | Eip1193ProviderLike | EthersSignerLike;
46
+ /**
47
+ * Adapt a viem account or wallet client, an EIP-1193 provider, or an ethers
48
+ * signer. Signatures come back canonical (lowercase, v in {27, 28}).
49
+ */
50
+ export declare function toTotalisSigner(input: SignerInput): TotalisSigner;
51
+ export {};