@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.
- package/CHANGELOG.md +1903 -0
- package/LICENSE +202 -0
- package/README.md +143 -2
- package/dist/api-keys.d.ts +118 -0
- package/dist/api-keys.js +104 -0
- package/dist/audience-contract.d.ts +52 -0
- package/dist/audience-contract.js +1 -0
- package/dist/base.d.ts +41 -0
- package/dist/base.js +60 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +15 -0
- package/dist/controller.d.ts +53 -0
- package/dist/controller.js +63 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +3 -0
- package/dist/flow-errors.d.ts +62 -0
- package/dist/flow-errors.js +70 -0
- package/dist/generated/account-read.d.ts +2015 -0
- package/dist/generated/account-read.js +1 -0
- package/dist/generated/api-keys.d.ts +1303 -0
- package/dist/generated/api-keys.js +1 -0
- package/dist/generated/browse.d.ts +965 -0
- package/dist/generated/browse.js +1 -0
- package/dist/generated/client.d.ts +104 -0
- package/dist/generated/client.js +1 -0
- package/dist/generated/errors.d.ts +467 -0
- package/dist/generated/errors.js +1792 -0
- package/dist/generated/maker.d.ts +47 -0
- package/dist/generated/maker.js +1 -0
- package/dist/generated/openapi.d.ts +10867 -0
- package/dist/generated/openapi.js +1 -0
- package/dist/generated/operator.d.ts +7 -0
- package/dist/generated/operator.js +1 -0
- package/dist/generated/realtime.d.ts +1486 -0
- package/dist/generated/realtime.js +1 -0
- package/dist/generated/vault-abi.d.ts +49 -0
- package/dist/generated/vault-abi.js +70 -0
- package/dist/generated/version.d.ts +24 -0
- package/dist/generated/version.js +183 -0
- package/dist/hypercore.d.ts +111 -0
- package/dist/hypercore.js +132 -0
- package/dist/maker.d.ts +206 -0
- package/dist/maker.js +608 -0
- package/dist/money.d.ts +59 -0
- package/dist/money.js +66 -0
- package/dist/operator.d.ts +4 -0
- package/dist/operator.js +4 -0
- package/dist/pagination.d.ts +20 -0
- package/dist/pagination.js +27 -0
- package/dist/partner.d.ts +71 -0
- package/dist/partner.js +66 -0
- package/dist/realtime.d.ts +19 -0
- package/dist/realtime.js +10 -0
- package/dist/request.d.ts +115 -0
- package/dist/request.js +190 -0
- package/dist/signer.d.ts +51 -0
- package/dist/signer.js +90 -0
- package/dist/stream.d.ts +142 -0
- package/dist/stream.js +331 -0
- package/dist/totalis.d.ts +719 -0
- package/dist/totalis.js +1251 -0
- package/dist/typed-data.d.ts +330 -0
- package/dist/typed-data.js +340 -0
- package/dist/ui.d.ts +54 -0
- package/dist/ui.js +77 -0
- package/dist/wallet-session.d.ts +52 -0
- package/dist/wallet-session.js +63 -0
- package/examples/browser-wallet.html +69 -0
- package/examples/browser-wallet.mjs +57 -0
- package/examples/node-bot.mjs +57 -0
- package/examples/onboard.mjs +132 -0
- package/examples/partner-frontend.mjs +44 -0
- package/examples/partner-server.mjs +22 -0
- package/package.json +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}`>;
|
package/dist/operator.js
ADDED
|
@@ -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>;
|
package/dist/partner.js
ADDED
|
@@ -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;
|
package/dist/realtime.js
ADDED
|
@@ -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 {};
|
package/dist/request.js
ADDED
|
@@ -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
|
+
}
|
package/dist/signer.d.ts
ADDED
|
@@ -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 {};
|