@totalis/hip4-client 0.0.0-stage → 0.65.2

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 +1541 -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 +50 -0
  13. package/dist/controller.js +60 -0
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +3 -0
  16. package/dist/flow-errors.d.ts +61 -0
  17. package/dist/flow-errors.js +70 -0
  18. package/dist/generated/account-read.d.ts +2009 -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 +938 -0
  23. package/dist/generated/browse.js +1 -0
  24. package/dist/generated/client.d.ts +73 -0
  25. package/dist/generated/client.js +1 -0
  26. package/dist/generated/errors.d.ts +404 -0
  27. package/dist/generated/errors.js +1342 -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 +8044 -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 +1416 -0
  35. package/dist/generated/realtime.js +1 -0
  36. package/dist/generated/vault-abi.d.ts +39 -0
  37. package/dist/generated/vault-abi.js +56 -0
  38. package/dist/generated/version.d.ts +23 -0
  39. package/dist/generated/version.js +143 -0
  40. package/dist/hypercore.d.ts +111 -0
  41. package/dist/hypercore.js +132 -0
  42. package/dist/maker.d.ts +205 -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 +59 -0
  51. package/dist/partner.js +57 -0
  52. package/dist/realtime.d.ts +19 -0
  53. package/dist/realtime.js +10 -0
  54. package/dist/request.d.ts +112 -0
  55. package/dist/request.js +187 -0
  56. package/dist/signer.d.ts +51 -0
  57. package/dist/signer.js +90 -0
  58. package/dist/stream.d.ts +133 -0
  59. package/dist/stream.js +318 -0
  60. package/dist/totalis.d.ts +656 -0
  61. package/dist/totalis.js +1160 -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 +45 -0
  67. package/dist/wallet-session.js +61 -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 +104 -4
package/dist/ui.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { components } from "./generated/realtime.js";
2
+ import { type Quote, type Totalis } from "./totalis.js";
3
+ type Invalidation = components["schemas"]["Invalidation"];
4
+ /** An account read a `readInvalidated` frame names: refetch the queries built on it. */
5
+ export type AccountResource = Invalidation["resources"][number];
6
+ export type UiAdapterOptions = {
7
+ /**
8
+ * Refetch these account reads, for example `queryClient.invalidateQueries`.
9
+ * Called for each newer `readInvalidated` revision (a session's account
10
+ * stream gets its own ACTIVITY and POSITIONS; other resources and
11
+ * scoped keys need `subscription: { account_reads: true }`), and with every
12
+ * resource each time the stream goes live, because invalidations sent while
13
+ * it was down are lost.
14
+ */
15
+ readonly onInvalidate?: (resources: readonly AccountResource[]) => void;
16
+ /**
17
+ * Refetch the market catalog (`/v1/markets`, `/v1/discovery`): a
18
+ * `marketRefresh` named a generation newer than the one passed to
19
+ * `catalogLoaded`. Called once per generation; every connection announces
20
+ * the current one, so a reconnect alone never refetches.
21
+ */
22
+ readonly onCatalogStale?: (generation: string) => void;
23
+ };
24
+ /** What `watchQuotes` renders: the live quotes, the best one still acceptable, and the venue clock they were judged at. */
25
+ export type QuoteView = {
26
+ readonly quotes: readonly Quote[];
27
+ readonly best: Quote | undefined;
28
+ readonly serverNowMs: number;
29
+ };
30
+ /**
31
+ * A small framework-agnostic bridge from the account stream to a UI: quotes
32
+ * from the subscription, the venue clock, account invalidations for a query
33
+ * cache and generation-aware catalog refresh. It polls nothing and holds no
34
+ * framework dependency; wire the callbacks to your store.
35
+ */
36
+ export declare function createUiAdapter(totalis: Pick<Totalis, "stream" | "watchQuotes">, options?: UiAdapterOptions): {
37
+ /** The venue clock in ms. Read it on every render tick; a timestamp kept from a callback freezes countdowns. */
38
+ serverNow: () => number;
39
+ /** Seconds the quote can still be accepted: an entry until its `accept_before`, a cash-out bid until its expiry. */
40
+ secondsLeft: (quote: Quote) => number;
41
+ /**
42
+ * Render an RFQ's live quotes from the stream (`totalis.watchQuotes`) with
43
+ * the best one: highest payout before its `accept_before` (`bestQuote`),
44
+ * or highest price with at least `minLifeSeconds` left (`bestBid`,
45
+ * default 25). `quotes` is `[]` while the stream is not live. Returns the unsubscribe.
46
+ */
47
+ watchQuotes(rfqId: string, listener: (view: QuoteView) => void, minLifeSeconds?: number): () => void;
48
+ /** Record the `catalog_generation` of the catalog you just loaded; `onCatalogStale` then fires only for a newer one. */
49
+ catalogLoaded(generation: string | number | bigint): void;
50
+ /** Detach the listeners; the stream stays open. */
51
+ close(): void;
52
+ };
53
+ export type UiAdapter = ReturnType<typeof createUiAdapter>;
54
+ export {};
package/dist/ui.js ADDED
@@ -0,0 +1,77 @@
1
+ import { acceptBefore, bestBid, bestQuote } from "./totalis.js";
2
+ const everyResource = ["BALANCES", "CAPITAL", "ACTIVITY", "POSITIONS", "PERFORMANCE", "TELEMETRY", "QUOTES", "ACCESS"];
3
+ const expiryOf = (quote) => (quote.kind === "ENTRY" ? quote.entry : quote.kind === "SELL_BACK" ? quote.sell_back : quote.transfer).expiry;
4
+ /**
5
+ * A small framework-agnostic bridge from the account stream to a UI: quotes
6
+ * from the subscription, the venue clock, account invalidations for a query
7
+ * cache and generation-aware catalog refresh. It polls nothing and holds no
8
+ * framework dependency; wire the callbacks to your store.
9
+ */
10
+ export function createUiAdapter(totalis, options = {}) {
11
+ const { stream } = totalis;
12
+ const revisions = new Map();
13
+ let catalog;
14
+ let staleCatalog;
15
+ const offs = [
16
+ stream.on("status", (status) => {
17
+ if (status !== "live")
18
+ return;
19
+ revisions.clear();
20
+ options.onInvalidate?.(everyResource);
21
+ }),
22
+ stream.on("frame", (frame) => {
23
+ if (frame.channel === "readInvalidated") {
24
+ const invalidation = frame.data;
25
+ const scope = JSON.stringify(invalidation.scope);
26
+ const revision = BigInt(invalidation.revision);
27
+ if (revision <= (revisions.get(scope) ?? -1n))
28
+ return;
29
+ revisions.set(scope, revision);
30
+ options.onInvalidate?.(invalidation.resources);
31
+ }
32
+ else if (frame.channel === "marketRefresh") {
33
+ // Every connection announces the current generation; only a newer one than held (or already announced) is news.
34
+ const generation = BigInt(frame.data.catalog_generation);
35
+ if (generation <= (catalog ?? 0n) || generation <= (staleCatalog ?? 0n))
36
+ return;
37
+ staleCatalog = generation;
38
+ options.onCatalogStale?.(generation.toString());
39
+ }
40
+ }),
41
+ ];
42
+ // Attached to a stream that is already live: no status event will come, and invalidations before now were missed.
43
+ if (stream.status === "live")
44
+ options.onInvalidate?.(everyResource);
45
+ stream.start();
46
+ return {
47
+ /** The venue clock in ms. Read it on every render tick; a timestamp kept from a callback freezes countdowns. */
48
+ serverNow: () => stream.serverNow(),
49
+ /** Seconds the quote can still be accepted: an entry until its `accept_before`, a cash-out bid until its expiry. */
50
+ secondsLeft: (quote) => Math.max(0, Math.floor(((quote.kind === "ENTRY" ? acceptBefore(quote) : Number(expiryOf(quote)) * 1000) - stream.serverNow()) / 1000)),
51
+ /**
52
+ * Render an RFQ's live quotes from the stream (`totalis.watchQuotes`) with
53
+ * the best one: highest payout before its `accept_before` (`bestQuote`),
54
+ * or highest price with at least `minLifeSeconds` left (`bestBid`,
55
+ * default 25). `quotes` is `[]` while the stream is not live. Returns the unsubscribe.
56
+ */
57
+ watchQuotes(rfqId, listener, minLifeSeconds) {
58
+ return totalis.watchQuotes(rfqId, (quotes, serverNowMs) => {
59
+ const entries = quotes.filter((quote) => quote.kind === "ENTRY");
60
+ const bids = quotes.filter((quote) => quote.kind !== "ENTRY");
61
+ const best = entries.length > 0 ? bestQuote()(entries, serverNowMs) : bestBid(minLifeSeconds)(bids, serverNowMs);
62
+ listener({ quotes, best, serverNowMs });
63
+ });
64
+ },
65
+ /** Record the `catalog_generation` of the catalog you just loaded; `onCatalogStale` then fires only for a newer one. */
66
+ catalogLoaded(generation) {
67
+ const loaded = BigInt(generation);
68
+ if (catalog === undefined || loaded > catalog)
69
+ catalog = loaded;
70
+ },
71
+ /** Detach the listeners; the stream stays open. */
72
+ close() {
73
+ for (const off of offs)
74
+ off();
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,45 @@
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
+ type Schemas = components["schemas"];
5
+ export type WalletChallenge = Schemas["WalletChallenge"];
6
+ /** A bearer session for the wallet's account: at most 30 days, ending after 7 idle days or at sign-out. */
7
+ export type WalletSession = Schemas["WalletSession"];
8
+ export type WalletSignInOptions = Omit<ApiClientOptions, "accessToken"> & {
9
+ readonly environment: PublicEnvironment;
10
+ /** The external wallet that signs in. It is the account (TT-1780); its key never leaves it. */
11
+ readonly signer: SignerInput;
12
+ /**
13
+ * The page's origin, defaulting to `location.origin` in a browser. Outside a
14
+ * browser it is sent as the Origin header: a maker passes the Totalis app's
15
+ * origin (`https://hip4.totalis.trade`, or `https://hip4-staging.totalis.trade`
16
+ * on staging), and a partner its registered origin. Send the same origin as
17
+ * the Origin header with every request on the session's token.
18
+ */
19
+ readonly origin?: string;
20
+ };
21
+ /**
22
+ * Check a sign-in challenge before the wallet signs it: the exact Totalis
23
+ * session message, for this origin, wallet and request, and not expired.
24
+ * A sign-in message authorizes no financial action, but a wallet should never
25
+ * sign one it did not ask for.
26
+ */
27
+ export declare function checkSignInChallenge(challenge: WalletChallenge, expected: {
28
+ readonly address: string;
29
+ readonly origin: string;
30
+ readonly requestId: string;
31
+ }, now?: number): void;
32
+ /**
33
+ * Sign an external wallet in (TT-1676): ask for a challenge, have the wallet
34
+ * personal_sign it, and return the session. Use `access_token` as the
35
+ * `accessToken` of `createTotalis` with the same wallet as its signer. A wallet
36
+ * with no account yet needs a partner to admit it first (ACCESS_REQUIRED).
37
+ */
38
+ export declare function signInWithWallet(options: WalletSignInOptions): Promise<WalletSession>;
39
+ /** End the wallet session now; any later request with its token is UNAUTHENTICATED. */
40
+ export declare function signOutWallet(options: Omit<ApiClientOptions, "accessToken"> & {
41
+ readonly environment: PublicEnvironment;
42
+ readonly accessToken: string;
43
+ readonly origin?: string;
44
+ }): Promise<void>;
45
+ export {};
@@ -0,0 +1,61 @@
1
+ import { audienceClient } from "./base.js";
2
+ import { ApiError, unwrap, uuidv7 } from "./request.js";
3
+ import { toTotalisSigner } from "./signer.js";
4
+ const sessionStatement = "Create a Totalis venue transport session. This does not authorize a financial action.";
5
+ const inBrowser = () => typeof globalThis.document !== "undefined";
6
+ /**
7
+ * Check a sign-in challenge before the wallet signs it: the exact Totalis
8
+ * session message, for this origin, wallet and request, and not expired.
9
+ * A sign-in message authorizes no financial action, but a wallet should never
10
+ * sign one it did not ask for.
11
+ */
12
+ export function checkSignInChallenge(challenge, expected, now = Date.now()) {
13
+ const lines = challenge.message.split("\n");
14
+ const field = (name) => lines.find((line) => line.startsWith(`${name}: `))?.slice(name.length + 2);
15
+ const expiration = field("Expiration Time");
16
+ const problems = [
17
+ // The venue names the origin's authority as sent, keeping an explicit default port.
18
+ lines[0] !== `${expected.origin.slice(expected.origin.indexOf("//") + 2)} wants you to sign in with your Ethereum account:` && "message is for another site",
19
+ lines[1]?.toLowerCase() !== expected.address.toLowerCase() && "message is for another wallet",
20
+ lines[3] !== sessionStatement && "message is not a Totalis sign-in",
21
+ field("URI") !== `${expected.origin}/v1/auth/wallet-sessions` && "message is for another URI",
22
+ field("Request ID") !== expected.requestId && "message is for another request",
23
+ (expiration === undefined || !(Date.parse(expiration) > now)) && "challenge expired",
24
+ ].filter(Boolean);
25
+ if (problems.length > 0)
26
+ throw new Error(`refusing to sign the sign-in message: ${problems.join("; ")}`);
27
+ }
28
+ /**
29
+ * Sign an external wallet in (TT-1676): ask for a challenge, have the wallet
30
+ * personal_sign it, and return the session. Use `access_token` as the
31
+ * `accessToken` of `createTotalis` with the same wallet as its signer. A wallet
32
+ * with no account yet needs a partner to admit it first (ACCESS_REQUIRED).
33
+ */
34
+ export async function signInWithWallet(options) {
35
+ const { environment, signer: input, origin = globalThis.location?.origin, ...clientOptions } = options;
36
+ if (origin === undefined || origin === "")
37
+ throw new Error("signInWithWallet needs an origin outside a browser");
38
+ const signer = toTotalisSigner(input);
39
+ const address = await signer.address();
40
+ const client = audienceClient(environment, clientOptions);
41
+ // A browser sets Origin itself and refuses a script's value.
42
+ const originHeader = inBrowser() ? {} : { Origin: origin };
43
+ const requestId = uuidv7();
44
+ const challenge = unwrap(await client.POST("/v1/auth/wallet-challenges", {
45
+ params: { header: { "Idempotency-Key": requestId } }, body: { address, origin }, headers: originHeader,
46
+ }));
47
+ checkSignInChallenge(challenge, { address, origin, requestId });
48
+ const signature = await signer.signMessage(challenge.message);
49
+ return unwrap(await client.POST("/v1/auth/wallet-sessions", {
50
+ params: { header: { "Idempotency-Key": uuidv7() } }, body: { challenge_id: challenge.challenge_id, signature }, headers: originHeader,
51
+ }));
52
+ }
53
+ /** End the wallet session now; any later request with its token is UNAUTHENTICATED. */
54
+ export async function signOutWallet(options) {
55
+ const { environment, origin = globalThis.location?.origin, ...clientOptions } = options;
56
+ const client = audienceClient(environment, clientOptions);
57
+ const headers = inBrowser() || origin === undefined ? {} : { Origin: origin };
58
+ const { error, response } = await client.DELETE("/v1/auth/wallet-sessions/current", { headers });
59
+ if (error !== undefined || !response.ok)
60
+ throw ApiError.from(response, error);
61
+ }
@@ -0,0 +1,69 @@
1
+ <!doctype html>
2
+ <!--
3
+ A browser-wallet taker on @totalis/hip4-client, with no bundler.
4
+ From clients/typescript: `npm run build`, serve this directory
5
+ (for example `python3 -m http.server 8000`), then open
6
+ http://localhost:8000/examples/browser-wallet.html with an injected wallet.
7
+ The import map below needs that checkout's dist/ and node_modules/, so this
8
+ page does not run from an installed copy; an app with a bundler imports
9
+ "@totalis/hip4-client" and browser-wallet.mjs instead.
10
+ -->
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <title>Totalis SDK: browser wallet</title>
15
+ <script type="importmap">
16
+ {
17
+ "imports": {
18
+ "@totalis/hip4-client": "../dist/client.js",
19
+ "openapi-fetch": "../node_modules/openapi-fetch/dist/index.mjs",
20
+ "@noble/hashes/sha3": "../node_modules/@noble/hashes/esm/sha3.js",
21
+ "@noble/hashes/crypto": "../node_modules/@noble/hashes/esm/crypto.js"
22
+ }
23
+ }
24
+ </script>
25
+ <style>
26
+ body { font: 14px system-ui, sans-serif; max-width: 42rem; margin: 2rem auto; }
27
+ label { display: block; margin: .5rem 0; }
28
+ input, textarea { width: 100%; font: inherit; }
29
+ pre { background: #f4f4f4; padding: 1rem; white-space: pre-wrap; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <h1>Totalis SDK: browser wallet</h1>
34
+ <label>API origin <input id="baseUrl" value="https://hip4-api-staging.totalis.trade"></label>
35
+ <label>Session token (the app's access token) <input id="token" type="password"></label>
36
+ <label>Legs <textarea id="legs">[{"outcome_id": "1", "side": "YES"}]</textarea></label>
37
+ <label>Amount (USDC base units) <input id="amount" value="5000000"></label>
38
+ <label>Position ID (for a cash-out) <input id="position"></label>
39
+ <button id="bet">Place bet</button>
40
+ <button id="cashout">Cash out</button>
41
+ <pre id="out">Connect an injected wallet (window.ethereum) to begin.</pre>
42
+ <script type="module">
43
+ import { cashOut, connect, openPosition } from "./browser-wallet.mjs";
44
+
45
+ const $ = (id) => document.getElementById(id);
46
+ const show = (value) => { $("out").textContent = typeof value === "string" ? value : JSON.stringify(value, null, 2); };
47
+ const session = () => {
48
+ if (!window.ethereum) throw new Error("no injected wallet");
49
+ return connect({ provider: window.ethereum, accessToken: $("token").value, baseUrl: $("baseUrl").value });
50
+ };
51
+ const run = (action) => async () => {
52
+ const totalis = session();
53
+ totalis.stream.on("status", (status) => show(`stream ${status}`));
54
+ try {
55
+ show(await action(totalis));
56
+ } catch (error) {
57
+ show(`${error.name}: ${error.message}`);
58
+ } finally {
59
+ totalis.close();
60
+ }
61
+ };
62
+ $("bet").onclick = run((totalis) => openPosition(totalis, {
63
+ legs: JSON.parse($("legs").value), stake: $("amount").value,
64
+ onQuotes: (quotes) => show(`${quotes.length} quote(s)`),
65
+ }));
66
+ $("cashout").onclick = run((totalis) => cashOut(totalis, { positionId: $("position").value }));
67
+ </script>
68
+ </body>
69
+ </html>
@@ -0,0 +1,57 @@
1
+ // Taker flows for any browser wallet (an EIP-1193 provider such as
2
+ // window.ethereum or an embedded wallet's provider). browser-wallet.html wires
3
+ // these to buttons; the local test harness runs the same functions in Node.
4
+ import { createTotalis, entryFailure } from "@totalis/hip4-client";
5
+
6
+ /**
7
+ * @typedef {object} ConnectOptions
8
+ * @property {import("@totalis/hip4-client/signer").Eip1193ProviderLike} provider The wallet.
9
+ * @property {import("@totalis/hip4-client").AccessToken} accessToken The app session, or a scoped ttk_ key.
10
+ * @property {import("@totalis/hip4-client").PublicEnvironment} [environment]
11
+ * @property {string} [baseUrl]
12
+ * @property {Record<string, string>} [headers] Extra headers outside a browser, such as a session's Origin.
13
+ * @property {import("@totalis/hip4-client/realtime").WebSocketFactory} [webSocket]
14
+ */
15
+
16
+ /** @param {ConnectOptions} options */
17
+ export function connect({ provider, accessToken, environment = "production", baseUrl, headers, webSocket }) {
18
+ return createTotalis({
19
+ environment, accessToken, signer: provider,
20
+ ...(baseUrl ? { baseUrl } : {}),
21
+ ...(headers ? { headers } : {}),
22
+ ...(webSocket ? { stream: { webSocket } } : {}),
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Open a combo position and return the accept result with its operation's
28
+ * current status. The quotes come
29
+ * from the account stream; `onQuotes` sees each version of them. Any vault
30
+ * shortfall is funded from the wallet in the same accept.
31
+ */
32
+ export async function openPosition(totalis, { legs, stake, onQuotes }) {
33
+ /** @type {import("@totalis/hip4-client").Chooser<import("@totalis/hip4-client").EntryQuote>} */
34
+ const choose = (quotes) => {
35
+ onQuotes?.(quotes);
36
+ return [...quotes]
37
+ .sort((a, b) => (BigInt(b.entry.payout) > BigInt(a.entry.payout) ? 1 : -1))[0];
38
+ };
39
+ const result = await totalis.openPosition({ legs, stake: BigInt(stake), choose });
40
+ if (result.status !== "ACCEPTED") return result;
41
+ // The operation's chain outcome arrives on the stream; read its current status once.
42
+ // Only COMMITTED opened the position; a failed one's `failure` names the message to show.
43
+ const { data } = await totalis.client.GET("/v1/me/operations/{operation_id}", { params: { path: { operation_id: result.operationId } } });
44
+ const operation = data?.operation;
45
+ return { ...result, operation, failure: operation === undefined ? undefined : entryFailure(result.record, operation) };
46
+ }
47
+
48
+ /**
49
+ * Cash out a position at the best quote. The wallet signs the quote's SellBack
50
+ * or CashoutTransfer before the accept; ACCEPTED means the sale is queued as
51
+ * `operationId`. Any other status (DECLINED, TIMED_OUT, CANCELLED, NOT_ADMITTED)
52
+ * sold nothing.
53
+ */
54
+ export async function cashOut(totalis, { positionId }) {
55
+ const result = await totalis.cashOut({ positionId });
56
+ return { rfqId: result.rfqId, acceptanceId: result.acceptanceId, status: result.status, operationId: result.operationId };
57
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // A Node maker bot on @totalis/hip4-client: quote every open entry RFQ,
3
+ // bid to buy back its own positions on cash-out RFQs when given a `bid` price,
4
+ // and confirm what takers accept while `willing` still holds.
5
+ // Entry and cash-out quotes are published on the same route. Pricing here is a placeholder; a real
6
+ // maker prices from its own market data and, in `willing`, re-checks its price
7
+ // and free capacity before it confirms.
8
+ //
9
+ // TOTALIS_ENVIRONMENT=staging TOTALIS_MAKER_ID=1 TOTALIS_MAKER_KEY=ttk_v1_stg_... \
10
+ // TOTALIS_WALLET_KEY=0x... node examples/node-bot.mjs
11
+ import { pathToFileURL } from "node:url";
12
+ import { createMaker } from "@totalis/hip4-client/maker";
13
+ import { privateKeyToAccount } from "viem/accounts";
14
+
15
+ /** Start quoting and confirming. Returns the maker; `close()` it to stop. */
16
+ export function startBot({ environment = "production", baseUrl, apiKey, makerId, signer, payout = (rfq) => (BigInt(rfq.stake) * 18n) / 10n, bid, willing = () => true, log = console.log }) {
17
+ const maker = createMaker({ environment, ...(baseUrl === undefined ? {} : { baseUrl }), apiKey, makerId, signer });
18
+ maker.stream.on("status", (status) => log(`stream ${status}`));
19
+ // Returning the quote promise lets onRfq offer the RFQ again if publishing fails.
20
+ maker.onRfq(async (rfq) => {
21
+ if (rfq.status !== "OPEN") return;
22
+ if (rfq.kind === "ENTRY") {
23
+ const { quoteId } = await maker.quote(rfq, { payout: payout(rfq) });
24
+ log(`quoted ${rfq.rfq_id} ${quoteId}`);
25
+ } else if (rfq.kind === "CASHOUT" && bid !== undefined && rfq.maker_id === makerId) {
26
+ // A sell-back of a position this maker wrote, paid from the maker vault. Another
27
+ // maker's position would be a transfer, paid from this wallet's own account
28
+ // balance, which this bot does not fund.
29
+ const { quoteId } = await maker.bid(rfq, { price: bid(rfq) });
30
+ log(`bid ${rfq.rfq_id} ${quoteId}`);
31
+ }
32
+ }, (error) => log(`rfq handling failed: ${error.message}`));
33
+ // Acceptances arrive as QUOTE_ACCEPTED (and in the snapshot, paged past its
34
+ // cap); confirming a cash-out executes the owner-signed sale, answering ACCEPTED.
35
+ maker.onAcceptance(async (acceptance) => {
36
+ // Price or capital may have moved since the quote: decline rather than fill.
37
+ if (!(await willing(acceptance))) {
38
+ log(`declining ${acceptance.quote_id} on ${acceptance.rfq_id}`);
39
+ return "DECLINE";
40
+ }
41
+ log(`confirming ${acceptance.quote_id} on ${acceptance.rfq_id}`);
42
+ return "CONFIRM";
43
+ }, (error, acceptance) => log(`confirm ${acceptance?.quote_id ?? "read"} failed: ${error.message}`));
44
+ return maker;
45
+ }
46
+
47
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
48
+ const required = (name) => process.env[name] ?? (() => { throw new Error(`${name} is required`); })();
49
+ const maker = startBot({
50
+ environment: process.env.TOTALIS_ENVIRONMENT ?? "staging",
51
+ baseUrl: process.env.TOTALIS_BASE_URL,
52
+ apiKey: required("TOTALIS_MAKER_KEY"),
53
+ makerId: required("TOTALIS_MAKER_ID"),
54
+ signer: privateKeyToAccount(required("TOTALIS_WALLET_KEY")),
55
+ });
56
+ process.once("SIGINT", () => maker.close());
57
+ }
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // Headless maker onboarding on @totalis/hip4-client, from the exported
3
+ // key of your Totalis account's wallet: sign in, open and admit the maker,
4
+ // issue the quoting bot's key, fund the maker vault, and start quoting. Run it
5
+ // again to resume: each step skips what is already done.
6
+ //
7
+ // Before it, once in a browser: sign up at hip4.totalis.trade with your invite
8
+ // code, then in Account choose Export private key. Fund that wallet on HyperEVM
9
+ // with HYPE for gas (makers are never sponsored) and the USDC you will deposit.
10
+ //
11
+ // TOTALIS_ENVIRONMENT=production TOTALIS_WALLET_KEY=0x... TOTALIS_DEPOSIT=1000000000 \
12
+ // node examples/onboard.mjs
13
+ //
14
+ // TOTALIS_DEPOSIT is the first deposit, in atomic USDC: it is made only while
15
+ // the maker vault is empty on chain, so a rerun never deposits twice. Add more
16
+ // later with controllerTransactions' approveUsdc and deposit.
17
+ //
18
+ // The quoting key's secret is printed once. Store it as TOTALIS_MAKER_KEY: a
19
+ // rerun with it set quotes with it instead of issuing another, and from then on
20
+ // examples/node-bot.mjs runs the bot on its own.
21
+ import { pathToFileURL } from "node:url";
22
+ import { createPublicClient, createWalletClient, defineChain, http, parseAbi } from "viem";
23
+ import { privateKeyToAccount } from "viem/accounts";
24
+ import { createClient, unwrap } from "@totalis/hip4-client";
25
+ import { createApiKeyClient, issueApiKey, listApiKeys, QUOTING_BOT_SCOPES } from "@totalis/hip4-client/api-keys";
26
+ import { controllerTransactions, openMakerVaultTransaction, waitForMaker } from "@totalis/hip4-client/maker";
27
+ import { toTotalisSigner } from "@totalis/hip4-client/signer";
28
+ import { signInWithWallet } from "@totalis/hip4-client/wallet-session";
29
+ import { startBot } from "./node-bot.mjs";
30
+
31
+ /** The Totalis app's origin: a maker's wallet session signs in from it, and every request on the session sends it as Origin. */
32
+ export const appOrigins = { production: "https://hip4.totalis.trade", staging: "https://hip4-staging.totalis.trade" };
33
+ /** Public HyperEVM RPCs by chain ID; pass your own `rpcUrl` for anything heavier than onboarding. */
34
+ const publicRpcs = { "999": "https://rpc.hyperliquid.xyz/evm", "998": "https://rpc.hyperliquid-testnet.xyz/evm" };
35
+
36
+ const vaultReads = parseAbi([
37
+ "function makerOf(address) view returns (uint64)",
38
+ "function makers(uint64) view returns (address controller, address quoteSigner, uint128 vault, uint128 stakeEscrow, uint128 payableOwed, uint128 grossOpen, uint128 discount, uint128 settledTurnover, uint64 lastReduceAt)",
39
+ ]);
40
+
41
+ /**
42
+ * The wallet as onboarding uses it: send the SDK's zero-value vault calls and
43
+ * wait for each to succeed, sign one for a self-funded route, and read from the
44
+ * vault the maker this wallet opened (0 if none) and a maker's vault lane.
45
+ */
46
+ export function hyperEvmWallet({ account, chainId, vault, rpcUrl = publicRpcs[chainId] }) {
47
+ const chain = defineChain({ id: Number(chainId), name: "HyperEVM", nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } } });
48
+ const reader = createPublicClient({ chain, transport: http(rpcUrl) });
49
+ const writer = createWalletClient({ account, chain, transport: http(rpcUrl) });
50
+ const request = (call) => {
51
+ if (call.chain_id !== String(chain.id)) throw new Error(`the call is for chain ${call.chain_id}, not ${chain.id}`);
52
+ return { to: call.to, data: call.data, value: BigInt(call.value) };
53
+ };
54
+ return {
55
+ async send(call) {
56
+ const hash = await writer.sendTransaction(request(call));
57
+ const receipt = await reader.waitForTransactionReceipt({ hash });
58
+ if (receipt.status !== "success") throw new Error(`transaction ${hash} reverted`);
59
+ return hash;
60
+ },
61
+ sign: async (call) => writer.signTransaction(await writer.prepareTransactionRequest(request(call))),
62
+ makerOf: () => reader.readContract({ address: vault, abi: vaultReads, functionName: "makerOf", args: [account.address] }),
63
+ makerVault: async (makerId) => (await reader.readContract({ address: vault, abi: vaultReads, functionName: "makers", args: [BigInt(makerId)] }))[2],
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Onboard the wallet's maker and return what the bot needs. `deposit` is the
69
+ * first deposit in atomic USDC (6 decimals), made only while the maker vault is
70
+ * empty on chain. Without `makerKey`, a "Quoting bot" key is issued and its
71
+ * secret returned once.
72
+ */
73
+ export async function onboard({ environment = "production", baseUrl, origin = appOrigins[environment], walletKey, rpcUrl, deposit = 0n, makerKey, fetch, wallet: walletOverride, log = console.log }) {
74
+ const signer = privateKeyToAccount(walletKey);
75
+ const options = { ...(baseUrl === undefined ? {} : { baseUrl }), ...(fetch === undefined ? {} : { fetch }) };
76
+
77
+ // 1. Sign in with the wallet. The session lasts at most 30 days and ends after 7 idle days: run this again to sign in again.
78
+ const session = await signInWithWallet({ environment, ...options, signer, origin });
79
+ const onSession = { ...options, accessToken: session.access_token, headers: { Origin: origin } };
80
+ const client = createClient(environment, onSession);
81
+ const deployment = unwrap(await client.GET("/v1/deployment"));
82
+ const wallet = walletOverride ?? hyperEvmWallet({ account: signer, chainId: deployment.chain_id, vault: deployment.vault_address, rpcUrl });
83
+
84
+ // 2. Open the maker vault: one transaction from this wallet, which becomes its controller and quote signer. A wallet opens one maker.
85
+ if (BigInt(await wallet.makerOf()) === 0n) {
86
+ log("opening the maker vault");
87
+ await wallet.send(openMakerVaultTransaction(deployment, signer.address));
88
+ }
89
+
90
+ // 3. Admit it once Totalis has indexed it: no signature, and safe to repeat.
91
+ const readiness = await waitForMaker(client);
92
+ if (readiness.state !== "READY") throw new Error(`the maker is ${readiness.state}, not READY: email founders@totalis.trade`);
93
+ const { makerId } = readiness;
94
+ log(`maker ${makerId} is ready`);
95
+
96
+ // 4. The quoting bot's key: maker scopes that cannot move vault money, approved by this wallet's signature.
97
+ const keys = createApiKeyClient(environment, onSession);
98
+ let secret = makerKey;
99
+ if (secret === undefined) {
100
+ const active = (await listApiKeys(keys, { maker: makerId })).filter((key) => key.status === "ACTIVE");
101
+ if (active.length > 0) log(`maker ${makerId} already has ${active.length} active key(s); issuing another`);
102
+ const issued = await issueApiKey(keys, toTotalisSigner(signer), { maker: makerId, label: "Quoting bot", scopes: [...QUOTING_BOT_SCOPES], expires_in_days: 90 });
103
+ secret = issued.secret;
104
+ log(`issued key ${issued.key.id}; store its secret as TOTALIS_MAKER_KEY, it is shown only now:\n${secret}`);
105
+ }
106
+
107
+ // 5. Fund the maker vault: approve USDC, then deposit it, once. Withdrawing later is requestWithdraw, then executeWithdraw after the delay.
108
+ if (deposit > 0n && BigInt(await wallet.makerVault(makerId)) > 0n) log(`maker ${makerId}'s vault is already funded; depositing nothing`);
109
+ else if (deposit > 0n) {
110
+ const vault = controllerTransactions(deployment, makerId, signer.address);
111
+ log(`depositing ${deposit} atomic USDC`);
112
+ await wallet.send(vault.approveUsdc(deposit));
113
+ await wallet.send(vault.deposit(deposit));
114
+ }
115
+ return { makerId, makerKey: secret, signer, wallet };
116
+ }
117
+
118
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
119
+ const required = (name) => process.env[name] ?? (() => { throw new Error(`${name} is required`); })();
120
+ const environment = process.env.TOTALIS_ENVIRONMENT ?? "production";
121
+ const { makerId, makerKey, signer } = await onboard({
122
+ environment,
123
+ baseUrl: process.env.TOTALIS_BASE_URL,
124
+ walletKey: required("TOTALIS_WALLET_KEY"),
125
+ rpcUrl: process.env.HYPEREVM_RPC_URL,
126
+ deposit: BigInt(process.env.TOTALIS_DEPOSIT ?? "0"),
127
+ makerKey: process.env.TOTALIS_MAKER_KEY,
128
+ });
129
+ // 6. Quote and confirm. The maker wallet signs every confirm; nothing fills without it.
130
+ const maker = startBot({ environment, baseUrl: process.env.TOTALIS_BASE_URL, apiKey: makerKey, makerId, signer });
131
+ process.once("SIGINT", () => maker.close());
132
+ }
@@ -0,0 +1,44 @@
1
+ // A partner's frontend (TT-1685): an external wallet signs in and trades as its
2
+ // own account. The wallet is any EIP-1193 provider: an extension's
3
+ // window.ethereum, or your own embedded wallet's provider. Totalis never holds
4
+ // its key; every bet, cash-out and withdrawal is signed by the wallet itself.
5
+ // A new wallet first consents to your partner opening its account, and your
6
+ // server admits it with that consent (partner-server.mjs).
7
+ import { createClient, createTotalis } from "@totalis/hip4-client";
8
+ import { signAdmissionConsent } from "@totalis/hip4-client/partner";
9
+ import { signInWithWallet, signOutWallet } from "@totalis/hip4-client/wallet-session";
10
+
11
+ /**
12
+ * A new user's consent, for your server's admitWallet: the wallet signs that
13
+ * your partner (`slug`) may open its account. Once, before its first sign-in.
14
+ */
15
+ export async function consentToPartner({ provider, slug, environment = "production" }) {
16
+ // The deployment is public: read it with no credential.
17
+ const { data: deployment } = await createClient(environment).GET("/v1/deployment");
18
+ return signAdmissionConsent({ signer: provider, partner: slug, environment, chainId: deployment.chain_id });
19
+ }
20
+
21
+ /**
22
+ * @param {object} options
23
+ * @param {import("@totalis/hip4-client/signer").Eip1193ProviderLike} options.provider The wallet.
24
+ * @param {string} options.partnerKey Your publishable `tpb_v1_…` key, honoured from your registered origins.
25
+ * @param {import("@totalis/hip4-client").PublicEnvironment} [options.environment]
26
+ */
27
+ export async function connectPartner({ provider, partnerKey, environment = "production" }) {
28
+ // The wallet signs a sign-in message for this page's origin; it authorizes no financial action.
29
+ const session = await signInWithWallet({ environment, signer: provider, partnerKey });
30
+ const totalis = createTotalis({ environment, accessToken: session.access_token, signer: provider, partnerKey });
31
+ return {
32
+ session,
33
+ totalis,
34
+ async signOut() {
35
+ totalis.close();
36
+ await signOutWallet({ environment, accessToken: session.access_token, partnerKey });
37
+ },
38
+ };
39
+ }
40
+
41
+ // For example, with an extension wallet:
42
+ //
43
+ // const { totalis } = await connectPartner({ provider: window.ethereum, partnerKey: "tpb_v1_prd_…" });
44
+ // await totalis.openPosition({ legs: [{ outcome_id: "13160", side: "YES" }], stake: 5_000_000n });
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // A partner's server (TT-1685): admit a wallet so its next sign-in creates its
3
+ // Totalis account, attributed to you. During the invite-only launch this is the
4
+ // only way a new wallet gets an account. The wallet's consent comes from your
5
+ // frontend (partner-frontend.mjs consentToPartner). Run it where your tpk_
6
+ // server key lives; never in a browser.
7
+ //
8
+ // TOTALIS_ENVIRONMENT=staging TOTALIS_PARTNER_KEY=tpk_v1_stg_... node examples/partner-server.mjs '{"address":"0x…","consent":{…}}'
9
+ import { pathToFileURL } from "node:url";
10
+ import { createPartnerServer } from "@totalis/hip4-client/partner";
11
+
12
+ /** Admit `address` with its consent; admitting it again returns the first admission. */
13
+ export async function admit({ environment = "production", baseUrl, serverKey, address, consent }) {
14
+ const partner = createPartnerServer({ environment, serverKey, ...(baseUrl === undefined ? {} : { baseUrl }) });
15
+ return partner.admitWallet(address, consent);
16
+ }
17
+
18
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
19
+ const serverKey = process.env.TOTALIS_PARTNER_KEY ?? (() => { throw new Error("TOTALIS_PARTNER_KEY is required"); })();
20
+ const { address, consent } = JSON.parse(process.argv[2] ?? (() => { throw new Error("usage: partner-server.mjs '{\"address\":…,\"consent\":…}'"); })());
21
+ console.log(await admit({ environment: process.env.TOTALIS_ENVIRONMENT, serverKey, address, consent }));
22
+ }