@gala-chain/gswap-sdk 1.1.1 → 1.2.0-rc.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.
package/dist/index.mjs CHANGED
@@ -1,8 +1,66 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import { Decimal } from "decimal.js";
3
3
  import debug from "debug";
4
- import { serialize, signatures } from "@gala-chain/api";
4
+ import { secp256k1 } from "@noble/curves/secp256k1";
5
+ import { keccak_256 } from "@noble/hashes/sha3";
6
+ import { bytesToHex } from "@noble/hashes/utils";
5
7
 
8
+ //#region src/utils/confirmation_schedule.ts
9
+ const DEFAULT_TIMEOUT_MS = 6e4;
10
+ const DEFAULT_POLL_INTERVAL_MS = 2e3;
11
+ const MIN_POLL_INTERVAL_MS = 500;
12
+ const MAX_BACKOFF_MS = 8e3;
13
+ /**
14
+ * Builds the schedule for one `confirm()` call. `abortError` creates the error thrown when
15
+ * `options.signal` aborts, so each confirmation kind keeps its own error type and details.
16
+ */
17
+ function createConfirmationSchedule(options, defaultInitialDelayMs, abortError) {
18
+ const timeoutMs = Math.max(0, (options === null || options === void 0 ? void 0 : options.timeoutMs) ?? DEFAULT_TIMEOUT_MS);
19
+ const pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, (options === null || options === void 0 ? void 0 : options.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS);
20
+ const initialDelayMs = Math.max(0, (options === null || options === void 0 ? void 0 : options.initialDelayMs) ?? defaultInitialDelayMs);
21
+ const signal = options === null || options === void 0 ? void 0 : options.signal;
22
+ const deadline = Date.now() + timeoutMs;
23
+ let firstRead = true;
24
+ let attempt = 0;
25
+ return { async waitForRead(retryAfterMs) {
26
+ throwIfAborted(signal, abortError);
27
+ if (firstRead) {
28
+ firstRead = false;
29
+ const remaining = Math.max(0, deadline - Date.now());
30
+ await delay(Math.min(initialDelayMs, remaining), signal, abortError);
31
+ return true;
32
+ }
33
+ const backoff = Math.min(MAX_BACKOFF_MS, pollIntervalMs * 2 ** Math.min(attempt, 5));
34
+ attempt += 1;
35
+ const waitMs = retryAfterMs === void 0 ? jitter(backoff) : Math.max(0, retryAfterMs);
36
+ if (Date.now() + waitMs > deadline) return false;
37
+ await delay(waitMs, signal, abortError);
38
+ return true;
39
+ } };
40
+ }
41
+ /** Throws `abortError(signal.reason)` when the signal has aborted. */
42
+ function throwIfAborted(signal, abortError) {
43
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) === true) throw abortError(signal.reason);
44
+ }
45
+ function delay(milliseconds, signal, abortError) {
46
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) === true) return Promise.reject(abortError(signal.reason));
47
+ return new Promise((resolve, reject) => {
48
+ const onAbort = () => {
49
+ clearTimeout(timer);
50
+ reject(abortError(signal === null || signal === void 0 ? void 0 : signal.reason));
51
+ };
52
+ const timer = setTimeout(() => {
53
+ signal === null || signal === void 0 || signal.removeEventListener("abort", onAbort);
54
+ resolve();
55
+ }, milliseconds);
56
+ signal === null || signal === void 0 || signal.addEventListener("abort", onAbort, { once: true });
57
+ });
58
+ }
59
+ function jitter(milliseconds) {
60
+ return Math.max(0, Math.round(milliseconds * (.9 + Math.random() * .2)));
61
+ }
62
+
63
+ //#endregion
6
64
  //#region src/classes/gswap_sdk_error.ts
7
65
  /** Error raised by the SDK for validation, gateway, chain, or transaction failures. */
8
66
  var GSwapSDKError = class GSwapSDKError extends Error {
@@ -10,6 +68,19 @@ var GSwapSDKError = class GSwapSDKError extends Error {
10
68
  details;
11
69
  chainMessage;
12
70
  retryAfterMs;
71
+ /** The HTTP status of the response that caused this error, when there was one. */
72
+ status;
73
+ /**
74
+ * The response body parsed as JSON, when the failing response had one; `undefined` for a body
75
+ * that was not JSON (see {@link bodyText}). Never part of {@link message}.
76
+ */
77
+ body;
78
+ /**
79
+ * The raw text of a failing response whose body was NOT JSON (a proxy's HTML 502, a WAF page,
80
+ * an empty body). It is kept here for diagnostics and is never copied into {@link message} or
81
+ * {@link userMessage}, so it cannot reach a user interface by accident.
82
+ */
83
+ bodyText;
13
84
  constructor(message, code, details, chainMessage, retryAfterMs) {
14
85
  super(message);
15
86
  this.code = code;
@@ -17,6 +88,32 @@ var GSwapSDKError = class GSwapSDKError extends Error {
17
88
  this.name = "GSwapSDKError";
18
89
  this.chainMessage = chainMessage;
19
90
  this.retryAfterMs = retryAfterMs;
91
+ const status = details === null || details === void 0 ? void 0 : details["status"];
92
+ this.status = typeof status === "number" ? status : void 0;
93
+ const body = details === null || details === void 0 ? void 0 : details["body"];
94
+ this.body = typeof body === "string" ? void 0 : body;
95
+ this.bodyText = typeof body === "string" ? body : void 0;
96
+ }
97
+ /**
98
+ * A message that is safe to show a user as is: the server's own message when it sent one
99
+ * (a gateway or chain rejection), a neutral line naming the status for an HTTP failure with no
100
+ * readable answer, and otherwise {@link message}. Apps that word these differently (a
101
+ * "network busy" line for a 502, say) branch on {@link code} and {@link status} instead.
102
+ */
103
+ get userMessage() {
104
+ if (this.chainMessage !== void 0 && this.chainMessage !== "") return this.chainMessage;
105
+ if (this.code === "HTTP_ERROR" && this.status !== void 0) return `The server answered with an unexpected response (HTTP ${this.status}).`;
106
+ return this.message;
107
+ }
108
+ /**
109
+ * True when the failure means "that market, pool or position does not exist" rather than
110
+ * "something went wrong": an HTTP 404, a pair with no pool at any fee tier
111
+ * (`NO_POOL_AVAILABLE`), or a token the DEX contract has no trading symbol for
112
+ * (`UNKNOWN_TOKEN`). A pool that exists but is too thin (`INSUFFICIENT_LIQUIDITY`) is NOT
113
+ * not-found.
114
+ */
115
+ get notFound() {
116
+ return this.status === 404 || this.code === "NO_POOL_AVAILABLE" || this.code === "UNKNOWN_TOKEN";
20
117
  }
21
118
  /** Create the standard missing-signer error. */
22
119
  static noSignerError() {
@@ -26,21 +123,38 @@ var GSwapSDKError = class GSwapSDKError extends Error {
26
123
  static noWalletAddressError() {
27
124
  return new GSwapSDKError("Cannot confirm this write: no walletAddress was provided to the GSwap constructor, so the resulting position cannot be looked up to confirm it landed on chain.", "NO_WALLET_ADDRESS");
28
125
  }
29
- /** Create the error used when no pool can quote the requested pair. */
30
- static noPoolAvailableError(tokenIn, tokenOut, fee) {
126
+ /**
127
+ * Create the error used when no pool can quote the requested pair. `source` is the backend
128
+ * answer that said so; its status, body and message are kept (the message as `chainMessage`,
129
+ * so {@link userMessage} is the server's own wording).
130
+ */
131
+ static noPoolAvailableError(tokenIn, tokenOut, fee, source) {
31
132
  return new GSwapSDKError("No pool is available for the requested pair.", "NO_POOL_AVAILABLE", {
32
133
  tokenIn,
33
134
  tokenOut,
34
- ...fee === void 0 ? {} : { fee }
35
- });
135
+ ...fee === void 0 ? {} : { fee },
136
+ ...sourceDetails(source)
137
+ }, source === null || source === void 0 ? void 0 : source.serverMessage);
36
138
  }
37
- /** Create the error used when a pool lacks liquidity for a quote. */
38
- static insufficientLiquidityError(tokenIn, tokenOut, fee) {
139
+ /** Create the error used when a pool lacks liquidity for a quote. `source` as for {@link noPoolAvailableError}. */
140
+ static insufficientLiquidityError(tokenIn, tokenOut, fee, source) {
39
141
  return new GSwapSDKError("The selected pool has insufficient liquidity for the requested quote.", "INSUFFICIENT_LIQUIDITY", {
40
142
  tokenIn,
41
143
  tokenOut,
42
- ...fee === void 0 ? {} : { fee }
43
- });
144
+ ...fee === void 0 ? {} : { fee },
145
+ ...sourceDetails(source)
146
+ }, source === null || source === void 0 ? void 0 : source.serverMessage);
147
+ }
148
+ /**
149
+ * The message the server put in a JSON answer (`message`, or `error.Message`), when this error
150
+ * came from an HTTP response that had one.
151
+ */
152
+ get serverMessage() {
153
+ var _this$details;
154
+ const direct = (_this$details = this.details) === null || _this$details === void 0 ? void 0 : _this$details["message"];
155
+ if (typeof direct === "string") return direct;
156
+ const body = this.body;
157
+ return getStringProperty(body, "message") ?? getStringProperty(body, "Message") ?? getStringProperty(getObjectProperty(body, "error"), "Message");
44
158
  }
45
159
  /** Convert a chain-gateway bounce into a typed SDK error. */
46
160
  static fromGatewayBounce(code, message, status, retryAfterMs) {
@@ -93,6 +207,15 @@ var GSwapSDKError = class GSwapSDKError extends Error {
93
207
  });
94
208
  }
95
209
  };
210
+ /** The HTTP facts of the error a classified error was derived from. */
211
+ function sourceDetails(source) {
212
+ if (source === void 0) return {};
213
+ return {
214
+ ...source.status === void 0 ? {} : { status: source.status },
215
+ ...source.body === void 0 ? {} : { body: source.body },
216
+ ...source.bodyText === void 0 ? {} : { body: source.bodyText }
217
+ };
218
+ }
96
219
  /** Narrow an object property without trusting a cast at a network boundary. */
97
220
  function getObjectProperty(value, property) {
98
221
  if (typeof value !== "object" || value === null || !(property in value)) return void 0;
@@ -114,6 +237,308 @@ function parseJson(text) {
114
237
  }
115
238
  }
116
239
 
240
+ //#endregion
241
+ //#region src/version.ts
242
+ /** Package version embedded by the bundler, with a source-test fallback. */
243
+ const SDK_VERSION = "1.2.0-rc.2";
244
+
245
+ //#endregion
246
+ //#region src/utils/transport.ts
247
+ /**
248
+ * Arms the abort timer for a request. `Infinity` means "never time out": `setTimeout` would turn
249
+ * it into 1 ms, so no timer is armed at all. Returns a function that disarms it.
250
+ */
251
+ function armTimeout(controller, timeoutMs) {
252
+ if (timeoutMs === Infinity) return () => void 0;
253
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
254
+ return () => clearTimeout(timer);
255
+ }
256
+ /**
257
+ * Validates a request timeout in milliseconds: a positive finite number, or `Infinity` for none.
258
+ *
259
+ * @throws GSwapSDKError VALIDATION_ERROR otherwise.
260
+ */
261
+ function validateTimeoutMs(timeoutMs, name) {
262
+ if (timeoutMs === Infinity || Number.isFinite(timeoutMs) && timeoutMs > 0) return timeoutMs;
263
+ throw new GSwapSDKError(`${name} must be a positive number of milliseconds, or Infinity for no timeout.`, "VALIDATION_ERROR", { [name]: timeoutMs });
264
+ }
265
+ const READABLE_QUERY_SAFE = /[A-Za-z0-9\-._~!'()*$|:,]/u;
266
+ function encodeReadable(text) {
267
+ let out = "";
268
+ for (const char of text) if (char === " ") out += "+";
269
+ else if (READABLE_QUERY_SAFE.test(char)) out += char;
270
+ else out += encodeScalar(char);
271
+ return out;
272
+ }
273
+ /** Percent-encodes one code point; a lone surrogate becomes U+FFFD, as `URLSearchParams` does. */
274
+ function encodeScalar(char) {
275
+ try {
276
+ return encodeURIComponent(char);
277
+ } catch {
278
+ return "%EF%BF%BD";
279
+ }
280
+ }
281
+ /** Serializes GET parameters, in insertion order, in the encoding the wire options select. */
282
+ function buildQueryString(params, encoding = "standard") {
283
+ if (encoding === "readable") return Object.entries(params).map(([key, value]) => `${encodeReadable(key)}=${encodeReadable(value)}`).join("&");
284
+ return new URLSearchParams(params).toString();
285
+ }
286
+ /**
287
+ * The request headers for ANY SDK request, GET or POST, from one place so the wire options reach
288
+ * every request alike. Order, which is the order the requestor sees them in: `User-Agent`
289
+ * (`wire.userAgent`, default `GalaChain-SDK/<version>`, `false` omits it), `parts.defaults`,
290
+ * `wire.headers`, `Content-Type` (JSON bodies only), `parts.fixed`. A later entry replaces an
291
+ * earlier one of the same name regardless of case, so a header is never sent twice.
292
+ */
293
+ function buildRequestHeaders(wire, parts = {}) {
294
+ const entries = /* @__PURE__ */ new Map();
295
+ const put = (headers, last = false) => {
296
+ for (const [name, value] of Object.entries(headers ?? {})) {
297
+ if (last) entries.delete(name.toLowerCase());
298
+ entries.set(name.toLowerCase(), [name, value]);
299
+ }
300
+ };
301
+ if ((wire === null || wire === void 0 ? void 0 : wire.userAgent) !== false) put({ "User-Agent": (wire === null || wire === void 0 ? void 0 : wire.userAgent) ?? `GalaChain-SDK/${"1.2.0-rc.2"}` });
302
+ put(parts.defaults);
303
+ put(wire === null || wire === void 0 ? void 0 : wire.headers);
304
+ if (parts.json === true && !entries.has("content-type")) put({ "Content-Type": "application/json" });
305
+ put(parts.fixed, true);
306
+ return Object.fromEntries(entries.values());
307
+ }
308
+ async function requestWithTimeout(requestor, url, options, timeoutMs) {
309
+ const controller = new AbortController();
310
+ const disarm = armTimeout(controller, timeoutMs);
311
+ const externalSignal = options.signal;
312
+ const abortFromCaller = () => controller.abort();
313
+ externalSignal === null || externalSignal === void 0 || externalSignal.addEventListener("abort", abortFromCaller, { once: true });
314
+ try {
315
+ return await requestor(url, {
316
+ ...options,
317
+ signal: controller.signal
318
+ });
319
+ } catch (error) {
320
+ if (controller.signal.aborted) throw new GSwapSDKError(`Request timed out after ${timeoutMs} ms.`, "REQUEST_TIMEOUT", {
321
+ url,
322
+ timeoutMs,
323
+ cause: error
324
+ });
325
+ throw error;
326
+ } finally {
327
+ disarm();
328
+ externalSignal === null || externalSignal === void 0 || externalSignal.removeEventListener("abort", abortFromCaller);
329
+ }
330
+ }
331
+ async function readResponseBody(response) {
332
+ const text = await response.text();
333
+ return parseJson(text) ?? text;
334
+ }
335
+ /** Read a case-insensitive response header from either supported header shape. */
336
+ function readHeader(response, name) {
337
+ var _Object$entries$find;
338
+ const headers = response.headers;
339
+ if (headers === void 0) return void 0;
340
+ if (hasHeaderGetter(headers)) {
341
+ const value = headers.get(name);
342
+ return value === null || value === "" ? void 0 : value;
343
+ }
344
+ const value = (_Object$entries$find = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())) === null || _Object$entries$find === void 0 ? void 0 : _Object$entries$find[1];
345
+ return value === void 0 || value === "" ? void 0 : value;
346
+ }
347
+ /** Read a Retry-After header (delta-seconds or an HTTP date) as non-negative milliseconds. */
348
+ function readRetryAfter(response) {
349
+ const raw = readHeader(response, "retry-after");
350
+ if (raw === void 0) return void 0;
351
+ const seconds = Number(raw);
352
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
353
+ const timestamp = Date.parse(raw);
354
+ return Number.isNaN(timestamp) ? void 0 : Math.max(0, timestamp - Date.now());
355
+ }
356
+ /** Read a non-negative integer block number header. */
357
+ function readBlockNumberHeader(response, headerName = "x-block-number") {
358
+ const raw = readHeader(response, headerName);
359
+ if (raw === void 0) return void 0;
360
+ const blockNumber = Number(raw);
361
+ return Number.isInteger(blockNumber) && blockNumber >= 0 ? blockNumber : void 0;
362
+ }
363
+ function hasHeaderGetter(headers) {
364
+ return "get" in headers && typeof headers.get === "function";
365
+ }
366
+
367
+ //#endregion
368
+ //#region src/classes/apply_results.ts
369
+ /** The apply-results key constraint of the swap backend (`apply-results.dto.ts`). */
370
+ const UNIQUE_KEY_PATTERN$1 = /^[A-Za-z0-9._:-]{1,128}$/u;
371
+ /**
372
+ * The AutoApplier waits at least 2 s before applying and the apply lands ~6-8 s after the
373
+ * acknowledgement, so a read at t=0 is a guaranteed 404.
374
+ */
375
+ const DEFAULT_INITIAL_DELAY_MS = 3e3;
376
+ const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
377
+ /**
378
+ * The outcome of a queued RequestTrade that could not be delivered as an {@link AppliedTrade}.
379
+ * `code` says why:
380
+ *
381
+ * - `CONFIRMATION_TIMEOUT`: no terminal result before the deadline. The trade may still apply;
382
+ * confirm the same `uniqueKey` again later and never resubmit.
383
+ * - `CONFIRMATION_ABORTED`: the caller's `signal` aborted. Nothing is known about the outcome.
384
+ * - `CONFIRMATION_FAILED`: the apply-results read itself failed (malformed key or body, a
385
+ * non-retryable HTTP status). The outcome is unknown.
386
+ * - anything else: the chain REJECTED the trade; `code` is the chain `ErrorKey` (or
387
+ * `CHAIN_ERROR`) and `chainMessage` is its message, which is also `message`.
388
+ *
389
+ * `details` always carries `uniqueKey` and, where known, `requestTransactionId`,
390
+ * `applyTransactionId`, `blockNumber`, `status`, `body`, `url` and the last transport `cause`.
391
+ */
392
+ var AppliedTradeError = class extends GSwapSDKError {
393
+ constructor(message, code, details = {}, chainMessage) {
394
+ super(message, code, details, chainMessage);
395
+ this.name = "AppliedTradeError";
396
+ }
397
+ };
398
+ /**
399
+ * Polls `GET /v1/chain/apply-results/{uniqueKey}?channel=asset&contract=dex-contract` until the
400
+ * queued RequestTrade reaches a terminal state.
401
+ *
402
+ * Pacing: wait `initialDelayMs` (default 3 s), then read; on 404, 429, 500, 502, 503, 504 or a
403
+ * transport error wait with jittered exponential backoff (`pollIntervalMs` doubling to 8 s,
404
+ * `Retry-After` honored as seconds or an HTTP date) until `timeoutMs`. Any other status is
405
+ * terminal. At least one read is always made.
406
+ *
407
+ * @returns The applied trade.
408
+ * @throws AppliedTradeError - see its documentation for every `code`.
409
+ * @example
410
+ * ```ts
411
+ * const applied = await confirmAppliedTrade(
412
+ * { dexBackendBaseUrl: 'https://dex-backend-prod1.defi.gala.com' },
413
+ * { uniqueKey, requestTransactionId: null },
414
+ * { initialDelayMs: 0, timeoutMs: 60_000, signal: controller.signal },
415
+ * );
416
+ * ```
417
+ */
418
+ async function confirmAppliedTrade(client, input, options) {
419
+ const { uniqueKey } = input;
420
+ if (!UNIQUE_KEY_PATTERN$1.test(uniqueKey)) throw new AppliedTradeError("uniqueKey must be 1-128 characters of A-Z, a-z, 0-9, \".\", \"_\", \":\" or \"-\".", "CONFIRMATION_FAILED", { uniqueKey });
421
+ const requestor = client.httpRequestor ?? fetch.bind(globalThis);
422
+ const requestTimeoutMs = client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
423
+ const signal = options === null || options === void 0 ? void 0 : options.signal;
424
+ const abortError = (cause) => new AppliedTradeError("Transaction confirmation was aborted.", "CONFIRMATION_ABORTED", {
425
+ uniqueKey,
426
+ ...cause === void 0 ? {} : { cause }
427
+ });
428
+ const url = `${client.dexBackendBaseUrl.replace(/\/$/u, "")}/v1/chain/apply-results/${encodeURIComponent(uniqueKey)}?channel=asset&contract=dex-contract`;
429
+ const schedule = createConfirmationSchedule(options, DEFAULT_INITIAL_DELAY_MS, abortError);
430
+ await schedule.waitForRead();
431
+ let lastError;
432
+ while (true) {
433
+ throwIfAborted(signal, abortError);
434
+ let response;
435
+ let body;
436
+ try {
437
+ response = await requestWithTimeout(requestor, url, {
438
+ method: "GET",
439
+ headers: buildRequestHeaders(client.wire),
440
+ ...signal === void 0 ? {} : { signal }
441
+ }, requestTimeoutMs);
442
+ body = await readResponseBody(response);
443
+ } catch (error) {
444
+ throwIfAborted(signal, abortError);
445
+ lastError = error;
446
+ if (!await schedule.waitForRead()) break;
447
+ continue;
448
+ }
449
+ throwIfAborted(signal, abortError);
450
+ if (response.status === 200) return readAppliedResult(input, response, body, url);
451
+ if (response.status === 404 || response.status === 429 || isTransient(response.status)) {
452
+ const retryAfter = response.status === 429 ? readRetryAfter(response) : void 0;
453
+ if (!await schedule.waitForRead(retryAfter)) break;
454
+ continue;
455
+ }
456
+ throw new AppliedTradeError(bodyMessage(body), "CONFIRMATION_FAILED", {
457
+ uniqueKey,
458
+ status: response.status,
459
+ body,
460
+ url
461
+ });
462
+ }
463
+ throw new AppliedTradeError("Transaction confirmation timed out; the trade may still apply. Confirm the same uniqueKey again later instead of resubmitting.", "CONFIRMATION_TIMEOUT", {
464
+ uniqueKey,
465
+ requestTransactionId: input.requestTransactionId,
466
+ url,
467
+ ...lastError === void 0 ? {} : { cause: lastError }
468
+ });
469
+ }
470
+ function readAppliedResult(input, response, body, url) {
471
+ const { uniqueKey } = input;
472
+ const record = asRecord$2(body);
473
+ const responseKey = (record === null || record === void 0 ? void 0 : record["uniqueKey"]) ?? (record === null || record === void 0 ? void 0 : record["UniqueKey"]);
474
+ if (typeof responseKey === "string" && responseKey !== uniqueKey) throw malformed$1(input, response, body, url, "Apply result key did not match the requested key.");
475
+ const status = record === null || record === void 0 ? void 0 : record["Status"];
476
+ const applyTransactionId = readHeader(response, "x-apply-transaction-id") ?? null;
477
+ const blockNumber = readBlockNumberHeader(response, "x-apply-block-number") ?? null;
478
+ if (status === 0) {
479
+ const errorKey = getStringProperty(record, "ErrorKey") ?? "CHAIN_ERROR";
480
+ const chainMessage = getStringProperty(record, "Message") ?? "The chain rejected this trade.";
481
+ throw new AppliedTradeError(chainMessage, errorKey, {
482
+ uniqueKey,
483
+ requestTransactionId: input.requestTransactionId,
484
+ applyTransactionId,
485
+ blockNumber,
486
+ status: response.status,
487
+ body,
488
+ url,
489
+ errorKey,
490
+ message: chainMessage
491
+ }, chainMessage);
492
+ }
493
+ if (status !== 1) throw malformed$1(input, response, body, url);
494
+ const result = parseTradeExecution(record === null || record === void 0 ? void 0 : record["Data"]);
495
+ if (result === void 0) throw malformed$1(input, response, body, url);
496
+ return {
497
+ uniqueKey,
498
+ transactionId: applyTransactionId,
499
+ blockNumber,
500
+ requestTransactionId: input.requestTransactionId,
501
+ result
502
+ };
503
+ }
504
+ function malformed$1(input, response, body, url, message = "Transaction confirmation returned a malformed body") {
505
+ return new AppliedTradeError(message, "CONFIRMATION_FAILED", {
506
+ uniqueKey: input.uniqueKey,
507
+ status: response.status,
508
+ body,
509
+ url
510
+ });
511
+ }
512
+ function parseTradeExecution(value) {
513
+ const record = asRecord$2(value);
514
+ if (record === void 0 || typeof record["token0"] !== "string" || typeof record["token1"] !== "string" || typeof record["fee"] !== "number") return;
515
+ const optionalFields = [
516
+ "sell0Qty",
517
+ "sell1Qty",
518
+ "buy0Qty",
519
+ "buy1Qty",
520
+ "tradingFees",
521
+ "protocolFees",
522
+ "totalFees"
523
+ ];
524
+ for (const field of optionalFields) if (field in record && typeof record[field] !== "string") return void 0;
525
+ return {
526
+ token0: record["token0"],
527
+ token1: record["token1"],
528
+ fee: record["fee"],
529
+ ...Object.fromEntries(optionalFields.flatMap((field) => typeof record[field] === "string" ? [[field, record[field]]] : []))
530
+ };
531
+ }
532
+ function isTransient(status) {
533
+ return status === 500 || status === 502 || status === 503 || status === 504;
534
+ }
535
+ function bodyMessage(body) {
536
+ return getStringProperty(body, "message") ?? getStringProperty(body, "Message") ?? getStringProperty(getObjectProperty(body, "error"), "message") ?? getStringProperty(getObjectProperty(body, "error"), "Message") ?? "Transaction confirmation failed";
537
+ }
538
+ function asRecord$2(value) {
539
+ return typeof value === "object" && value !== null ? value : void 0;
540
+ }
541
+
117
542
  //#endregion
118
543
  //#region src/types/fees.ts
119
544
  /**
@@ -206,6 +631,33 @@ function assertTickRange(lower, upper, fee) {
206
631
  function assertSpacing(spacing) {
207
632
  if (!Number.isInteger(spacing) || spacing <= 0) throw new RangeError("Tick spacing must be positive");
208
633
  }
634
+ /**
635
+ * Puts a `slot0` answer into the frame of the request. The backend always answers in the pool's
636
+ * canonical order and says so with `flippedFromRequest`; when the caller asked for the reverse
637
+ * order the price is inverted here (at 60 significant digits, so a very large or very small
638
+ * reciprocal keeps its precision) and the tick is re-derived from the inverted price with the
639
+ * contract's greatest-tick rule. Negating the tick instead is wrong for a price between ticks
640
+ * (and `-t - 1` is wrong for a price exactly on a tick: sqrtPrice 1 / tick 0 stays 1 / 0).
641
+ *
642
+ * @example
643
+ * ```ts
644
+ * const slot0 = await gSwap.pools.getSlot0('A', 'B', 500);
645
+ * const { sqrtPrice, tick } = frameSlot0(slot0);
646
+ * ```
647
+ */
648
+ function frameSlot0(slot0) {
649
+ if (slot0.flippedFromRequest !== true) return {
650
+ sqrtPrice: slot0.sqrtPrice,
651
+ tick: slot0.tick
652
+ };
653
+ const inverted = new DexDecimal(1).dividedBy(new DexDecimal(slot0.sqrtPrice));
654
+ if (!inverted.isFinite() || inverted.lessThanOrEqualTo(0)) throw new GSwapSDKError("Cannot invert a non-positive sqrt price.", "VALIDATION_ERROR", { sqrtPrice: slot0.sqrtPrice });
655
+ const sqrtPrice = inverted.toString();
656
+ return {
657
+ sqrtPrice,
658
+ tick: sqrtPriceToTick(sqrtPrice)
659
+ };
660
+ }
209
661
 
210
662
  //#endregion
211
663
  //#region src/utils/validation.ts
@@ -321,6 +773,36 @@ function validateWalletAddress(address) {
321
773
  value: address
322
774
  });
323
775
  }
776
+ /** Words the chain gateway refuses as a trading symbol. */
777
+ const RESERVED_TRADING_SYMBOLS = /* @__PURE__ */ new Set([
778
+ "Token",
779
+ "Unit",
780
+ "none"
781
+ ]);
782
+ const TRADING_SYMBOL_PATTERN = /^[A-Za-z0-9]+$/u;
783
+ const MAX_TRADING_SYMBOL_LENGTH = 32;
784
+ /**
785
+ * True when `symbol` is acceptable to propose into the trading-symbol registry: ASCII letters and
786
+ * digits only, 1 to 32 characters, and not a reserved word (`Token`, `Unit`, `none`) — the rules
787
+ * the chain gateway enforces on a CreatePool symbol.
788
+ */
789
+ function isProposableTradingSymbol(symbol) {
790
+ return TRADING_SYMBOL_PATTERN.test(symbol) && symbol.length <= MAX_TRADING_SYMBOL_LENGTH && !RESERVED_TRADING_SYMBOLS.has(symbol);
791
+ }
792
+ /**
793
+ * Asserts that a pair is in the contract's canonical order (`token0 < token1` by plain string
794
+ * comparison), which every write DTO requires. A pair a caller took from a stale quote or
795
+ * position can be reversed; signing it would sign something other than what was priced.
796
+ *
797
+ * @throws GSwapSDKError VALIDATION_ERROR (`details.type` = `NON_CANONICAL_PAIR`) otherwise.
798
+ */
799
+ function assertCanonicalPair(token0, token1) {
800
+ if (!(token0 < token1)) throw new GSwapSDKError(`The pair ${token0}/${token1} is not in canonical order: token0 must sort before token1.`, "VALIDATION_ERROR", {
801
+ type: "NON_CANONICAL_PAIR",
802
+ token0,
803
+ token1
804
+ });
805
+ }
324
806
 
325
807
  //#endregion
326
808
  //#region src/classes/assets.ts
@@ -381,62 +863,141 @@ function isRawUserAsset(value) {
381
863
  }
382
864
 
383
865
  //#endregion
384
- //#region src/utils/transport.ts
385
- async function requestWithTimeout(requestor, url, options, timeoutMs) {
386
- const controller = new AbortController();
387
- const timer = setTimeout(() => controller.abort(), timeoutMs);
388
- const externalSignal = options.signal;
389
- const abortFromCaller = () => controller.abort();
390
- externalSignal === null || externalSignal === void 0 || externalSignal.addEventListener("abort", abortFromCaller, { once: true });
391
- try {
392
- return await requestor(url, {
393
- ...options,
394
- signal: controller.signal
395
- });
396
- } catch (error) {
397
- if (controller.signal.aborted) throw new GSwapSDKError(`Request timed out after ${timeoutMs} ms.`, "REQUEST_TIMEOUT", {
398
- url,
399
- timeoutMs,
400
- cause: error
401
- });
402
- throw error;
403
- } finally {
404
- clearTimeout(timer);
405
- externalSignal === null || externalSignal === void 0 || externalSignal.removeEventListener("abort", abortFromCaller);
406
- }
866
+ //#region src/classes/dex_fees.ts
867
+ /** DEX methods the backend accepts a fee quote for (its allow-list enumerates the same six). */
868
+ const DEX_FEE_METHODS = [
869
+ "Swap",
870
+ "RequestTrade",
871
+ "AddLiquidity",
872
+ "RemoveLiquidity",
873
+ "CollectPositionFees",
874
+ "CreatePool"
875
+ ];
876
+ /** The chain method name the signed DryRun wrapper is signed under. */
877
+ const DRY_RUN_METHOD = "DryRun";
878
+ /** `uniqueKey` prefix of the DryRun WRAPPER (distinct from the inner DTO's key). */
879
+ const DRY_RUN_UNIQUE_KEY_PREFIX = "galaconnect-operation-";
880
+ /** True when `method` is a DEX method a fee can be quoted for. */
881
+ function isDexFeeMethod(method) {
882
+ return typeof method === "string" && DEX_FEE_METHODS.includes(method);
407
883
  }
408
- async function readResponseBody(response) {
409
- const text = await response.text();
410
- return parseJson(text) ?? text;
884
+ /** Current-contract typed DEX messages use trading symbols instead of TokenClassKey objects. */
885
+ function isCurrentContractDexMessage(message, method) {
886
+ return isDexFeeMethod(method) && typeof (message === null || message === void 0 ? void 0 : message["token0"]) === "string" && typeof message["token1"] === "string";
411
887
  }
412
- /** Read a case-insensitive response header from either supported header shape. */
413
- function readHeader(response, name) {
414
- var _Object$entries$find;
415
- const headers = response.headers;
416
- if (headers === void 0) return void 0;
417
- if (hasHeaderGetter(headers)) {
418
- const value = headers.get(name);
419
- return value === null || value === "" ? void 0 : value;
420
- }
421
- const value = (_Object$entries$find = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())) === null || _Object$entries$find === void 0 ? void 0 : _Object$entries$find[1];
422
- return value === void 0 || value === "" ? void 0 : value;
423
- }
424
- /** Read a non-negative Retry-After header as milliseconds. */
425
- function readRetryAfter(response) {
426
- const raw = readHeader(response, "retry-after");
427
- if (raw === void 0) return void 0;
428
- const seconds = Number(raw);
429
- return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
888
+ /**
889
+ * Reduces an EIP-712 approval `message` to the DTO the chain actually receives: drops the signing
890
+ * envelope (`signature`, `prefix`, `types`, `domain`) and transport metadata (`trace`).
891
+ */
892
+ function toChainDto(message) {
893
+ const chainDto = { ...message };
894
+ for (const field of [
895
+ "signature",
896
+ "prefix",
897
+ "types",
898
+ "domain",
899
+ "trace"
900
+ ]) delete chainDto[field];
901
+ return chainDto;
430
902
  }
431
- /** Read a non-negative integer block number header. */
432
- function readBlockNumberHeader(response, headerName = "x-block-number") {
433
- const raw = readHeader(response, headerName);
434
- if (raw === void 0) return void 0;
435
- const blockNumber = Number(raw);
436
- return Number.isInteger(blockNumber) && blockNumber >= 0 ? blockNumber : void 0;
903
+ /**
904
+ * Signs the DryRun WRAPPER the gateway accepts for every contract:
905
+ * `{ callerPublicKey, method, dto: <inner DTO object>, uniqueKey, signature }`. The inner DTO
906
+ * travels unsigned and as an object; no `signerPublicKey` is sent because the signature already
907
+ * recovers it (the gateway rejects it as REDUNDANT_SIGNER_PUBLIC_KEY). The wrapper carries its own
908
+ * `uniqueKey`, distinct from the DTO's.
909
+ */
910
+ function buildDryRunWrapper(signer, callerPublicKey, method, dto, uniqueKeyPrefix = DRY_RUN_UNIQUE_KEY_PREFIX) {
911
+ return signer.signObject(DRY_RUN_METHOD, {
912
+ callerPublicKey,
913
+ method,
914
+ dto,
915
+ uniqueKey: `${uniqueKeyPrefix}${globalThis.crypto.randomUUID()}`
916
+ });
437
917
  }
438
- function hasHeaderGetter(headers) {
439
- return "get" in headers && typeof headers.get === "function";
918
+ /**
919
+ * Network-fee quote for GalaChain DEX methods, derived from a server-side DryRun.
920
+ *
921
+ * The client only SIGNS the DryRun wrapper (with whatever {@link GalaChainSigner} it is given) and
922
+ * the backend forwards it to the gateway, where the simulated writes carry the fee; the backend
923
+ * never holds a key. Intended for wallets: the DTO quoted is the typed message a dApp handed over.
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * const fees = new DexFees({ dexBackendBaseUrl }, new PrivateKeySigner(sessionKey));
928
+ * const quote = await fees.quote(publicKey, 'RequestTrade', toChainDto(message), {
929
+ * contract: 'dex-contract',
930
+ * });
931
+ * const feeInGala = quote?.fees[0]?.feeInGala;
932
+ * ```
933
+ */
934
+ var DexFees = class {
935
+ signer;
936
+ requestor;
937
+ baseUrl;
938
+ requestTimeoutMs;
939
+ wire;
940
+ constructor(client, signer) {
941
+ this.signer = signer;
942
+ this.requestor = client.httpRequestor ?? fetch.bind(globalThis);
943
+ this.baseUrl = client.dexBackendBaseUrl.replace(/\/$/u, "");
944
+ this.requestTimeoutMs = client.requestTimeoutMs ?? 3e4;
945
+ this.wire = client.wire;
946
+ }
947
+ /**
948
+ * Quotes the network fee for `method` applied to `message`. The message is reduced with
949
+ * {@link toChainDto} first, so an EIP-712 approval message can be passed as is.
950
+ *
951
+ * Resolves `null` when the BACKEND could not be reached or answered unusably (offline, 4xx/5xx,
952
+ * malformed body): a transport outcome, not a fee. Deciding what to charge when the chain is
953
+ * silent is the backend's job (it serves `source: 'default'`), so the client never substitutes
954
+ * a fee of its own, with ONE exception that mirrors the wallet: a `dex-contract` quote answered
955
+ * 403 or 404 means the contract has no fee route yet and resolves `{ source: 'default', '0' }`.
956
+ * Never throws on transport, so a fee lookup cannot block or break a signing screen.
957
+ *
958
+ * @throws GSwapSDKError NO_SIGNER when constructed without a signer; any error the signer
959
+ * throws (for example a locked wallet) propagates unchanged.
960
+ */
961
+ async quote(callerPublicKey, method, message, options = {}) {
962
+ if (this.signer === void 0) throw GSwapSDKError.noSignerError();
963
+ const channel = options.channel ?? "asset";
964
+ const contract = options.contract ?? "dexv3-contract";
965
+ const wrapper = await buildDryRunWrapper(this.signer, callerPublicKey, method, toChainDto(message));
966
+ const url = `${this.baseUrl}/wallet/connect/galachain/api/${encodeURIComponent(channel)}/${encodeURIComponent(contract)}/${encodeURIComponent(method)}/fee`;
967
+ let status;
968
+ let ok;
969
+ let body;
970
+ try {
971
+ const response = await requestWithTimeout(this.requestor, url, {
972
+ method: "POST",
973
+ headers: buildRequestHeaders(this.wire, {
974
+ json: true,
975
+ defaults: { Accept: "application/json" }
976
+ }),
977
+ body: JSON.stringify(wrapper)
978
+ }, this.requestTimeoutMs);
979
+ status = response.status;
980
+ ok = response.ok;
981
+ body = ok ? await readResponseBody(response) : void 0;
982
+ } catch {
983
+ return null;
984
+ }
985
+ if (!ok) {
986
+ if (contract === "dex-contract" && (status === 403 || status === 404)) return {
987
+ source: "default",
988
+ fees: [{ feeInGala: "0" }]
989
+ };
990
+ return null;
991
+ }
992
+ return readQuote(body);
993
+ }
994
+ };
995
+ function readQuote(body) {
996
+ var _fees$;
997
+ if (typeof body !== "object" || body === null) return null;
998
+ const fees = body.fees;
999
+ if (!Array.isArray(fees) || typeof ((_fees$ = fees[0]) === null || _fees$ === void 0 ? void 0 : _fees$.feeInGala) !== "string") return null;
1000
+ return body;
440
1001
  }
441
1002
 
442
1003
  //#endregion
@@ -447,12 +1008,27 @@ var SubmittedTransaction = class {
447
1008
  uniqueKey;
448
1009
  transactionId;
449
1010
  blockNumber;
1011
+ /** The gateway's `data.mode`: `'sync'` (executed, `result` is the chain result) or `'queued'`. */
450
1012
  mode;
1013
+ /**
1014
+ * What the gateway's answer established: `'executed'` for `mode: 'sync'`, `'queued'` for
1015
+ * `mode: 'queued'` (accepted, NOT executed: `await confirm()` for the outcome). A refused answer
1016
+ * never becomes a transaction, so there is no `'rejected'` here; see {@link classifySubmitEnvelope}
1017
+ * for classifying a raw envelope.
1018
+ */
1019
+ status;
451
1020
  /** The synchronous gateway result, or `undefined` when `mode === 'queued'`. */
452
1021
  result;
1022
+ /**
1023
+ * The gateway's whole JSON answer as it came (`{ status, message, error, data }`), for an app
1024
+ * that hands the backend's own envelope to its callers; `undefined` for a transaction built from
1025
+ * a uniqueKey alone.
1026
+ */
1027
+ envelope;
453
1028
  backendBaseUrl;
454
1029
  requestor;
455
1030
  requestTimeoutMs;
1031
+ wire;
456
1032
  positionConfirmation;
457
1033
  /** Construct a submitted transaction from a successful gateway response.
458
1034
  *
@@ -471,10 +1047,13 @@ var SubmittedTransaction = class {
471
1047
  this.transactionId = options.transactionId;
472
1048
  this.blockNumber = options.blockNumber ?? null;
473
1049
  this.mode = options.mode;
1050
+ this.status = options.mode === "queued" ? "queued" : "executed";
474
1051
  this.result = options.result;
1052
+ this.envelope = options.envelope;
475
1053
  this.backendBaseUrl = options.dexBackendBaseUrl.replace(/\/$/u, "");
476
1054
  this.requestor = options.httpRequestor;
477
1055
  this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
1056
+ this.wire = options.wire;
478
1057
  this.positionConfirmation = options.positionConfirmation;
479
1058
  }
480
1059
  /** Confirm a queued RequestTrade or prove the affected position for a liquidity write.
@@ -491,63 +1070,16 @@ var SubmittedTransaction = class {
491
1070
  if (this.method === "RequestTrade") return this.confirmApplied(options);
492
1071
  return this.confirmPosition(options);
493
1072
  }
494
- async confirmApplied(options) {
495
- const schedule = createConfirmationSchedule(options, 3e3);
496
- await schedule.waitForRead();
497
- const url = `${this.backendBaseUrl}/v1/chain/apply-results/${encodeURIComponent(this.uniqueKey)}?channel=asset&contract=dex-contract`;
498
- let lastError;
499
- while (true) {
500
- let response;
501
- let body;
502
- try {
503
- response = await requestWithTimeout(this.requestor, url, { method: "GET" }, this.requestTimeoutMs);
504
- body = await readResponseBody(response);
505
- } catch (error) {
506
- lastError = error;
507
- if (!await schedule.waitForRead()) break;
508
- continue;
509
- }
510
- if (response.status === 200) return this.readAppliedResult(response, body, url);
511
- if (response.status === 404 || response.status === 429 || isTransient(response.status)) {
512
- if (response.status === 429) {
513
- const retryAfter = readRetryAfter(response);
514
- if (retryAfter !== void 0) {
515
- if (!await schedule.waitForRead(retryAfter)) break;
516
- continue;
517
- }
518
- }
519
- } else throw new GSwapSDKError(bodyMessage(body), "CONFIRMATION_FAILED", {
520
- status: response.status,
521
- body,
522
- url
523
- });
524
- if (!await schedule.waitForRead()) break;
525
- }
526
- throw GSwapSDKError.confirmationTimeoutError(this.uniqueKey, lastError);
527
- }
528
- readAppliedResult(response, body, url) {
529
- const record = asRecord$1(body);
530
- const status = record === null || record === void 0 ? void 0 : record["Status"];
531
- const applyTransactionId = readHeader(response, "x-apply-transaction-id") ?? null;
532
- const blockNumber = readBlockNumberHeader(response, "x-apply-block-number") ?? null;
533
- if (status === 0) throw GSwapSDKError.fromChainError(getStringProperty(record, "ErrorKey") ?? "CHAIN_ERROR", getStringProperty(record, "Message") ?? "The chain rejected this trade.", {
534
- uniqueKey: this.uniqueKey,
535
- applyTransactionId,
536
- blockNumber,
537
- status: response.status,
538
- body,
539
- url
540
- });
541
- if (status !== 1) throw malformedConfirmation(response, body, url);
542
- const result = parseTradeExecution(record === null || record === void 0 ? void 0 : record["Data"]);
543
- if (result === void 0) throw malformedConfirmation(response, body, url);
544
- return {
1073
+ confirmApplied(options) {
1074
+ return confirmAppliedTrade({
1075
+ dexBackendBaseUrl: this.backendBaseUrl,
1076
+ httpRequestor: this.requestor,
1077
+ requestTimeoutMs: this.requestTimeoutMs,
1078
+ wire: this.wire
1079
+ }, {
545
1080
  uniqueKey: this.uniqueKey,
546
- transactionId: applyTransactionId,
547
- blockNumber,
548
- requestTransactionId: this.transactionId,
549
- result
550
- };
1081
+ requestTransactionId: this.transactionId
1082
+ }, options);
551
1083
  }
552
1084
  /** Confirm a liquidity write against the position's observable effect.
553
1085
  *
@@ -559,86 +1091,33 @@ var SubmittedTransaction = class {
559
1091
  */
560
1092
  async confirmPosition(options) {
561
1093
  if (this.positionConfirmation === void 0) throw GSwapSDKError.noWalletAddressError();
562
- const schedule = createConfirmationSchedule(options, 0);
1094
+ const callerSignal = options === null || options === void 0 ? void 0 : options.signal;
1095
+ const abortError = (cause) => new GSwapSDKError("Transaction confirmation was aborted.", "CONFIRMATION_ABORTED", {
1096
+ uniqueKey: this.uniqueKey,
1097
+ ...cause === void 0 ? {} : { cause }
1098
+ });
1099
+ const schedule = createConfirmationSchedule(options, 0, abortError);
563
1100
  await schedule.waitForRead();
564
1101
  while (true) {
565
1102
  const controller = new AbortController();
566
- const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
1103
+ const disarm = armTimeout(controller, this.requestTimeoutMs);
1104
+ const forwardAbort = () => controller.abort();
1105
+ callerSignal === null || callerSignal === void 0 || callerSignal.addEventListener("abort", forwardAbort, { once: true });
567
1106
  try {
568
1107
  const result = await this.positionConfirmation(controller.signal);
569
1108
  if (result.confirmed) return result.position;
1109
+ } catch (error) {
1110
+ throwIfAborted(callerSignal, abortError);
1111
+ throw error;
570
1112
  } finally {
571
- clearTimeout(timer);
1113
+ disarm();
1114
+ callerSignal === null || callerSignal === void 0 || callerSignal.removeEventListener("abort", forwardAbort);
572
1115
  }
573
1116
  if (!await schedule.waitForRead()) break;
574
1117
  }
575
1118
  throw GSwapSDKError.confirmationTimeoutError(this.uniqueKey);
576
1119
  }
577
1120
  };
578
- function createConfirmationSchedule(options, defaultInitialDelayMs) {
579
- const timeoutMs = Math.max(0, (options === null || options === void 0 ? void 0 : options.timeoutMs) ?? 6e4);
580
- const pollIntervalMs = Math.max(500, (options === null || options === void 0 ? void 0 : options.pollIntervalMs) ?? 2e3);
581
- const initialDelayMs = Math.max(0, (options === null || options === void 0 ? void 0 : options.initialDelayMs) ?? defaultInitialDelayMs);
582
- const deadline = Date.now() + timeoutMs;
583
- let firstRead = true;
584
- let attempt = 0;
585
- return { async waitForRead(retryAfterMs) {
586
- if (firstRead) {
587
- firstRead = false;
588
- const remaining = Math.max(0, deadline - Date.now());
589
- await delay(Math.min(initialDelayMs, remaining));
590
- return true;
591
- }
592
- const backoff = Math.min(8e3, pollIntervalMs * 2 ** Math.min(attempt, 5));
593
- attempt += 1;
594
- const waitMs = retryAfterMs === void 0 ? jitter(backoff) : retryAfterMs;
595
- if (Date.now() + waitMs > deadline) return false;
596
- await delay(waitMs);
597
- return true;
598
- } };
599
- }
600
- function parseTradeExecution(value) {
601
- const record = asRecord$1(value);
602
- if (record === void 0 || typeof record["token0"] !== "string" || typeof record["token1"] !== "string" || typeof record["fee"] !== "number") return;
603
- const optionalFields = [
604
- "sell0Qty",
605
- "sell1Qty",
606
- "buy0Qty",
607
- "buy1Qty",
608
- "tradingFees",
609
- "protocolFees",
610
- "totalFees"
611
- ];
612
- for (const field of optionalFields) if (field in record && typeof record[field] !== "string") return void 0;
613
- return {
614
- token0: record["token0"],
615
- token1: record["token1"],
616
- fee: record["fee"],
617
- ...Object.fromEntries(optionalFields.flatMap((field) => typeof record[field] === "string" ? [[field, record[field]]] : []))
618
- };
619
- }
620
- function malformedConfirmation(response, body, url) {
621
- return new GSwapSDKError("Transaction confirmation returned a malformed body", "CONFIRMATION_FAILED", {
622
- status: response.status,
623
- body,
624
- url
625
- });
626
- }
627
- function isTransient(status) {
628
- return status === 500 || status === 502 || status === 503 || status === 504;
629
- }
630
- function bodyMessage(body) {
631
- return getStringProperty(body, "message") ?? getStringProperty(body, "Message") ?? getStringProperty(getObjectProperty(body, "error"), "message") ?? getStringProperty(getObjectProperty(body, "error"), "Message") ?? (typeof body === "string" ? body : "Transaction confirmation failed");
632
- }
633
- function asRecord$1(value) {
634
- return typeof value === "object" && value !== null ? value : void 0;
635
- }
636
- function delay(milliseconds) {
637
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
638
- }
639
- function jitter(milliseconds) {
640
- return Math.max(0, Math.round(milliseconds * (.9 + Math.random() * .2)));
641
- }
642
1121
 
643
1122
  //#endregion
644
1123
  //#region src/classes/gateway.ts
@@ -649,38 +1128,36 @@ var ChainGateway = class {
649
1128
  chainCallTimeoutMs;
650
1129
  requestTimeoutMs;
651
1130
  dexBackendBaseUrl;
1131
+ /** Wire format shared by the read services built on this gateway. */
1132
+ wire;
652
1133
  /** Create a chain gateway client. */
653
1134
  constructor(options) {
654
1135
  this.dexBackendBaseUrl = trimTrailingSlash$1(options.dexBackendBaseUrl);
655
1136
  this.requestor = options.httpRequestor ?? fetch.bind(globalThis);
656
1137
  this.walletAddress = options.walletAddress;
657
- this.chainCallTimeoutMs = options.chainCallTimeoutMs ?? 3e4;
1138
+ this.chainCallTimeoutMs = validateTimeoutMs(options.chainCallTimeoutMs ?? 3e4, "chainCallTimeoutMs");
658
1139
  this.requestTimeoutMs = this.chainCallTimeoutMs;
1140
+ this.wire = options.wire ?? {};
659
1141
  }
660
1142
  async submit(method, signedBody, options) {
661
1143
  if (method === "Trade") throw new GSwapSDKError("The synchronous Trade method was removed; swaps are queued through RequestTrade.", "METHOD_NOT_SUPPORTED", { method });
662
- const url = `${this.dexBackendBaseUrl}/v1/chain/asset/dex-contract/${method}`;
663
- const headers = { "content-type": "application/json" };
1144
+ const url = this.writeUrl(method);
664
1145
  const walletAddress = (options === null || options === void 0 ? void 0 : options.walletAddress) ?? this.walletAddress;
665
- if (walletAddress !== void 0) headers["X-Wallet-Address"] = walletAddress;
666
- let response;
667
- try {
668
- response = await requestWithTimeout(this.requestor, url, {
669
- method: "POST",
670
- headers,
671
- body: JSON.stringify(signedBody)
672
- }, this.chainCallTimeoutMs);
673
- } catch (error) {
674
- if (error instanceof GSwapSDKError && error.code === "REQUEST_TIMEOUT") throw new GSwapSDKError("The submission outcome is unknown because the gateway request timed out. Confirm before retrying.", "SUBMISSION_OUTCOME_UNKNOWN", {
675
- uniqueKey: getStringProperty(signedBody, "uniqueKey") ?? "",
676
- cause: error
677
- });
678
- throw error;
679
- }
1146
+ const headers = buildRequestHeaders(this.wire, {
1147
+ json: true,
1148
+ fixed: walletAddress === void 0 ? {} : { "X-Wallet-Address": walletAddress }
1149
+ });
1150
+ const response = await this.post(url, headers, signedBody);
680
1151
  const body = await readResponseBody(response);
681
1152
  if (!response.ok) throw gatewayError(response, body, url);
682
- const envelope = asRecord(body);
683
- const data = asRecord((envelope === null || envelope === void 0 ? void 0 : envelope["data"]) ?? (envelope === null || envelope === void 0 ? void 0 : envelope["Data"]));
1153
+ const envelope = asRecord$1(body);
1154
+ if ((envelope === null || envelope === void 0 ? void 0 : envelope["error"]) === true) throw new GSwapSDKError(getStringProperty(envelope, "message") || "The gateway reported an error for this write.", "INVALID_GATEWAY_RESPONSE", {
1155
+ status: response.status,
1156
+ body,
1157
+ url,
1158
+ type: "GATEWAY_REPORTED_ERROR"
1159
+ });
1160
+ const data = asRecord$1((envelope === null || envelope === void 0 ? void 0 : envelope["data"]) ?? (envelope === null || envelope === void 0 ? void 0 : envelope["Data"]));
684
1161
  if (data === void 0) throw invalidGatewayResponse(response, body, url, "Expected a data object.");
685
1162
  const transactionId = getStringProperty(data, "transactionId");
686
1163
  const mode = getStringProperty(data, "mode");
@@ -690,12 +1167,10 @@ var ChainGateway = class {
690
1167
  if (mode !== "sync" && mode !== "queued") throw invalidGatewayResponse(response, body, url, invalidModeMessage);
691
1168
  if ("transactionId" in data && typeof rawTransactionId !== "string" || bodyBlockNumber !== void 0 && bodyBlockNumber !== null && !isBlockNumber(bodyBlockNumber)) throw invalidGatewayResponse(response, body, url, invalidModeMessage);
692
1169
  if (method === "RequestTrade" && mode === "sync") throw invalidGatewayResponse(response, body, url, "The gateway executed RequestTrade synchronously, which this SDK refuses; RequestTrade must be queued.");
693
- if (method === "RequestTrade" && mode === "queued") {
694
- const signedUniqueKey = getStringProperty(signedBody, "uniqueKey");
695
- if ("uniqueKey" in data && (typeof data["uniqueKey"] !== "string" || data["uniqueKey"] !== signedUniqueKey)) throw invalidGatewayResponse(response, body, url, "Gateway response data.uniqueKey must match the signed RequestTrade uniqueKey.");
696
- } else if (mode === "queued" || !("result" in data)) throw invalidGatewayResponse(response, body, url, invalidModeMessage);
1170
+ if (method === "RequestTrade" && mode === "queued") assertQueuedUnderSignedKey(response, body, url, data, signedBody);
1171
+ else if (mode === "queued" || !("result" in data)) throw invalidGatewayResponse(response, body, url, invalidModeMessage);
697
1172
  if (mode === "sync") {
698
- const embeddedResult = asRecord(data["result"]);
1173
+ const embeddedResult = asRecord$1(data["result"]);
699
1174
  if (embeddedResult !== void 0) {
700
1175
  const resultStatus = embeddedResult["Status"];
701
1176
  if (resultStatus !== void 0 && resultStatus !== 1) throw GSwapSDKError.fromChainError(getStringProperty(embeddedResult, "ErrorKey") ?? "CHAIN_ERROR", getStringProperty(embeddedResult, "Message") ?? "The chain rejected this write.", {
@@ -716,12 +1191,68 @@ var ChainGateway = class {
716
1191
  blockNumber: blockNumber ?? null,
717
1192
  mode,
718
1193
  result: mode === "queued" ? void 0 : data["result"],
1194
+ envelope: body,
719
1195
  dexBackendBaseUrl: this.dexBackendBaseUrl,
720
1196
  httpRequestor: this.requestor,
721
1197
  requestTimeoutMs: this.chainCallTimeoutMs,
1198
+ wire: this.wire,
722
1199
  ...(options === null || options === void 0 ? void 0 : options.positionConfirmation) === void 0 ? {} : { positionConfirmation: options.positionConfirmation }
723
1200
  });
724
1201
  }
1202
+ /**
1203
+ * Submit a signed v2 DTO as a SHADOW (dark) write: the same route and body as {@link submit},
1204
+ * plus `X-Wallet-Address` and `X-Shadow: true`, so defi-backend marks the transaction private
1205
+ * before it reaches the contract. The wallet address is required (the backend resolves the
1206
+ * owner from it).
1207
+ *
1208
+ * Unlike `submit`, EVERY HTTP status resolves: the result is the raw `{ httpStatus, body }`,
1209
+ * because shadow failures are classified from the status together with the body's `code`
1210
+ * (`SHADOW_UNAVAILABLE`, `SHADOW_DISABLED`, `SHADOW_UNSUPPORTED_TYPE`, `SHADOW_RELAY_REJECTED`);
1211
+ * see `readShadowSubmitOutcome`. Only a transport failure rejects, and a timeout rejects
1212
+ * with `SUBMISSION_OUTCOME_UNKNOWN`.
1213
+ *
1214
+ * @example
1215
+ * ```ts
1216
+ * const result = await gateway.submitShadow('RequestTrade', signedDto, { walletAddress });
1217
+ * const outcome = readShadowSubmitOutcome('RequestTrade', signedDto, result);
1218
+ * ```
1219
+ */
1220
+ async submitShadow(method, signedBody, options) {
1221
+ if (method === "Trade") throw new GSwapSDKError("The synchronous Trade method was removed; swaps are queued through RequestTrade.", "METHOD_NOT_SUPPORTED", { method });
1222
+ const walletAddress = (options === null || options === void 0 ? void 0 : options.walletAddress) ?? this.walletAddress;
1223
+ if (walletAddress === void 0) throw new GSwapSDKError("A shadow submission needs a walletAddress (the X-Wallet-Address header): pass one to the GSwap constructor or to this call.", "NO_WALLET_ADDRESS");
1224
+ const headers = buildRequestHeaders(this.wire, {
1225
+ json: true,
1226
+ fixed: {
1227
+ "X-Wallet-Address": walletAddress,
1228
+ "X-Shadow": "true"
1229
+ }
1230
+ });
1231
+ const response = await this.post(this.writeUrl(method), headers, signedBody);
1232
+ return {
1233
+ httpStatus: response.status,
1234
+ body: await readResponseBody(response)
1235
+ };
1236
+ }
1237
+ writeUrl(method) {
1238
+ return `${this.dexBackendBaseUrl}/v1/chain/asset/dex-contract/${method}`;
1239
+ }
1240
+ /** POSTs a signed body; a timeout means the outcome is unknown, never "not submitted". */
1241
+ async post(url, headers, signedBody) {
1242
+ try {
1243
+ return await requestWithTimeout(this.requestor, url, {
1244
+ method: "POST",
1245
+ headers,
1246
+ body: JSON.stringify(signedBody)
1247
+ }, this.chainCallTimeoutMs);
1248
+ } catch (error) {
1249
+ if (error instanceof GSwapSDKError && error.code === "REQUEST_TIMEOUT") throw new GSwapSDKError("The submission outcome is unknown because the gateway request timed out. Confirm before retrying.", "SUBMISSION_OUTCOME_UNKNOWN", {
1250
+ uniqueKey: getStringProperty(signedBody, "uniqueKey") ?? "",
1251
+ cause: error
1252
+ });
1253
+ throw error;
1254
+ }
1255
+ }
725
1256
  /** Expose the requestor for sibling read services that share this gateway transport. */
726
1257
  get httpRequestor() {
727
1258
  return this.requestor;
@@ -737,11 +1268,34 @@ function invalidGatewayResponse(response, body, url, message) {
737
1268
  url
738
1269
  });
739
1270
  }
1271
+ /**
1272
+ * A queued RequestTrade acknowledgement is a swap only if it names the exact key that was signed:
1273
+ * the key is the only handle `confirm()` and every recovery path have on the trade, so an
1274
+ * acknowledgement without it (or with another) proves nothing about THIS request. Fails closed:
1275
+ * an absent echo is as unacceptable as a wrong one. `INVALID_GATEWAY_RESPONSE`, `details.type` is
1276
+ * `MISSING_UNIQUE_KEY` or `UNIQUE_KEY_MISMATCH`, `details.uniqueKey` the signed key.
1277
+ *
1278
+ * Only RequestTrade is checked: a synchronous write is proven by its own executed chain result,
1279
+ * and the apps never required an echo for it.
1280
+ */
1281
+ function assertQueuedUnderSignedKey(response, body, url, data, signedBody) {
1282
+ const signedUniqueKey = getStringProperty(signedBody, "uniqueKey") ?? "";
1283
+ const echoed = data["uniqueKey"];
1284
+ if (typeof echoed === "string" && echoed !== "" && echoed === signedUniqueKey) return;
1285
+ const missing = echoed === void 0 || echoed === null || echoed === "";
1286
+ throw new GSwapSDKError(missing ? "The gateway acknowledged RequestTrade without echoing its uniqueKey, so it cannot be tied to the signed request." : "Gateway response data.uniqueKey must match the signed RequestTrade uniqueKey.", "INVALID_GATEWAY_RESPONSE", {
1287
+ status: response.status,
1288
+ body,
1289
+ url,
1290
+ type: missing ? "MISSING_UNIQUE_KEY" : "UNIQUE_KEY_MISMATCH",
1291
+ uniqueKey: signedUniqueKey
1292
+ });
1293
+ }
740
1294
  function gatewayError(response, body, url) {
741
- const object = asRecord(body);
1295
+ const object = asRecord$1(body);
742
1296
  const nested = getObjectProperty(body, "error");
743
1297
  const code = getStringProperty(object, "code") ?? getStringProperty(nested, "code") ?? getStringProperty(nested, "ErrorKey") ?? "HTTP_ERROR";
744
- const message = getStringProperty(object, "message") ?? getStringProperty(nested, "message") ?? getStringProperty(nested, "Message") ?? (typeof body === "string" ? body : `Gateway request failed with HTTP ${response.status}`);
1298
+ const message = getStringProperty(object, "message") ?? getStringProperty(nested, "message") ?? getStringProperty(nested, "Message") ?? `Gateway request failed with HTTP ${response.status}`;
745
1299
  const retryAfterMs = response.status === 429 ? readRetryAfter(response) : void 0;
746
1300
  if (code === "HTTP_ERROR") return new GSwapSDKError(message, code, {
747
1301
  status: response.status,
@@ -750,7 +1304,7 @@ function gatewayError(response, body, url) {
750
1304
  });
751
1305
  return GSwapSDKError.fromGatewayBounce(code, message, response.status, retryAfterMs);
752
1306
  }
753
- function asRecord(value) {
1307
+ function asRecord$1(value) {
754
1308
  return typeof value === "object" && value !== null ? value : void 0;
755
1309
  }
756
1310
  function isBlockNumber(value) {
@@ -841,6 +1395,11 @@ var GdexStreamError = class extends Error {
841
1395
  if (data !== void 0) this.data = data;
842
1396
  }
843
1397
  };
1398
+ function readSubscriptionId(value) {
1399
+ const id = typeof value === "object" && value !== null ? value.subscriptionId : void 0;
1400
+ if (typeof id !== "string") throw new GdexStreamError(-32603, "GDEX subscribe response is missing its subscriptionId");
1401
+ return id;
1402
+ }
844
1403
  /**
845
1404
  * Client for the gSwap realtime stream (gdex WebSocket Protocol v1): JSON-RPC 2.0 over one raw
846
1405
  * WebSocket, with automatic reconnect and resubscribe.
@@ -857,6 +1416,7 @@ var GdexStreamError = class extends Error {
857
1416
  var GdexStreamService = class {
858
1417
  url;
859
1418
  requestTimeoutMs;
1419
+ apiKey;
860
1420
  webSocketConstructor;
861
1421
  backoff;
862
1422
  socket;
@@ -879,6 +1439,10 @@ var GdexStreamService = class {
879
1439
  const implementation = config.webSocketImpl ?? globalThis.WebSocket;
880
1440
  if (typeof implementation !== "function") throw new Error("GdexStreamService requires a standards-compatible WebSocket constructor. Node.js versions before 22 must provide one with config.webSocketImpl (for example, from the \"ws\" package).");
881
1441
  this.webSocketConstructor = implementation;
1442
+ if (config.apiKey !== void 0) {
1443
+ if (config.apiKey === "" || config.webSocketImpl === void 0) throw new GSwapSDKError("GdexStreamConfig.apiKey needs a non-empty key and an explicit webSocketImpl that accepts upgrade headers as its third argument (the \"ws\" package, or React Native). A browser WebSocket cannot send x-api-key, so the key would be silently dropped.", "VALIDATION_ERROR");
1444
+ this.apiKey = config.apiKey;
1445
+ }
882
1446
  }
883
1447
  /** Open the connection. Resolves at once when already connected. */
884
1448
  connect() {
@@ -906,6 +1470,9 @@ var GdexStreamService = class {
906
1470
  }
907
1471
  /** Raw JSON-RPC escape hatch. */
908
1472
  request(method, params) {
1473
+ return this.send(method, params);
1474
+ }
1475
+ send(method, params, onResult) {
909
1476
  if (!this.isConnected() || !this.socket) return Promise.reject(/* @__PURE__ */ new Error("GDEX stream is not connected. Call connect() first."));
910
1477
  const id = ++this.nextRequestId;
911
1478
  const frame = {
@@ -922,7 +1489,8 @@ var GdexStreamService = class {
922
1489
  this.pending.set(id, {
923
1490
  resolve: (value) => resolve(value),
924
1491
  reject,
925
- timeout
1492
+ timeout,
1493
+ onResult
926
1494
  });
927
1495
  try {
928
1496
  var _this$socket2;
@@ -1007,7 +1575,7 @@ var GdexStreamService = class {
1007
1575
  openSocket() {
1008
1576
  return new Promise((resolve, reject) => {
1009
1577
  let opened = false;
1010
- const socket = new this.webSocketConstructor(this.url, GDEX_SUBPROTOCOL);
1578
+ const socket = this.apiKey === void 0 ? new this.webSocketConstructor(this.url, GDEX_SUBPROTOCOL) : new this.webSocketConstructor(this.url, GDEX_SUBPROTOCOL, { headers: { "x-api-key": this.apiKey } });
1011
1579
  this.socket = socket;
1012
1580
  socket.onopen = () => {
1013
1581
  if (this.socket !== socket) return;
@@ -1052,7 +1620,16 @@ var GdexStreamService = class {
1052
1620
  const code = typeof message.error.code === "number" ? message.error.code : -32603;
1053
1621
  const errorMessage = typeof message.error.message === "string" ? message.error.message : "GDEX request failed";
1054
1622
  pending.reject(new GdexStreamError(code, errorMessage, message.error.data));
1055
- } else pending.resolve(message.result);
1623
+ } else {
1624
+ try {
1625
+ var _pending$onResult;
1626
+ (_pending$onResult = pending.onResult) === null || _pending$onResult === void 0 || _pending$onResult.call(pending, message.result);
1627
+ } catch (error) {
1628
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
1629
+ return;
1630
+ }
1631
+ pending.resolve(message.result);
1632
+ }
1056
1633
  return;
1057
1634
  }
1058
1635
  if (message.method !== "gdex_subscription" || !this.isRecord(message.params)) return;
@@ -1072,21 +1649,24 @@ var GdexStreamService = class {
1072
1649
  channel,
1073
1650
  ...options
1074
1651
  };
1075
- const result = await this.request("gdex_subscribe", params);
1076
- let active;
1077
1652
  const handle = {
1078
- subscriptionId: result.subscriptionId,
1079
- unsubscribe: async () => this.unsubscribe(active)
1653
+ subscriptionId: "",
1654
+ unsubscribe: async () => this.unsubscribe(subscription)
1080
1655
  };
1081
- active = {
1082
- serverId: result.subscriptionId,
1656
+ const subscription = {
1657
+ serverId: "",
1083
1658
  params,
1084
1659
  callback,
1085
1660
  handle,
1086
1661
  active: true
1087
1662
  };
1088
- this.activeSubscriptions.add(active);
1089
- this.subscriptionsByServerId.set(active.serverId, active);
1663
+ await this.send("gdex_subscribe", params, (value) => {
1664
+ const serverId = readSubscriptionId(value);
1665
+ subscription.serverId = serverId;
1666
+ handle.subscriptionId = serverId;
1667
+ this.activeSubscriptions.add(subscription);
1668
+ this.subscriptionsByServerId.set(serverId, subscription);
1669
+ });
1090
1670
  return handle;
1091
1671
  }
1092
1672
  async unsubscribe(subscription) {
@@ -1129,19 +1709,19 @@ var GdexStreamService = class {
1129
1709
  ...subscription.params,
1130
1710
  snapshot: true
1131
1711
  };
1132
- const result = await this.resubscribeWithRateLimitRetry(params);
1133
- if (!subscription.active) {
1134
- await this.request("gdex_unsubscribe", { subscriptionId: result.subscriptionId });
1135
- continue;
1136
- }
1137
- subscription.serverId = result.subscriptionId;
1138
- subscription.handle.subscriptionId = result.subscriptionId;
1139
- this.subscriptionsByServerId.set(result.subscriptionId, subscription);
1712
+ const result = await this.resubscribeWithRateLimitRetry(params, (value) => {
1713
+ const serverId = readSubscriptionId(value);
1714
+ if (!subscription.active) return;
1715
+ subscription.serverId = serverId;
1716
+ subscription.handle.subscriptionId = serverId;
1717
+ this.subscriptionsByServerId.set(serverId, subscription);
1718
+ });
1719
+ if (!subscription.active) await this.request("gdex_unsubscribe", { subscriptionId: result.subscriptionId });
1140
1720
  }
1141
1721
  }
1142
- async resubscribeWithRateLimitRetry(params) {
1722
+ async resubscribeWithRateLimitRetry(params, onResult) {
1143
1723
  for (let attempt = 0;; attempt++) try {
1144
- return await this.request("gdex_subscribe", params);
1724
+ return await this.send("gdex_subscribe", params, onResult);
1145
1725
  } catch (error) {
1146
1726
  if (!(error instanceof GdexStreamError) || error.code !== RATE_LIMITED || attempt >= MAX_RESUBSCRIBE_RATE_RETRIES) throw error;
1147
1727
  const retryAfterMs = this.readRetryAfterMs(error.data);
@@ -1182,31 +1762,29 @@ var GdexStreamService = class {
1182
1762
  }
1183
1763
  };
1184
1764
 
1185
- //#endregion
1186
- //#region src/version.ts
1187
- /** Package version embedded by the bundler, with a source-test fallback. */
1188
- const SDK_VERSION = "1.1.1";
1189
-
1190
1765
  //#endregion
1191
1766
  //#region src/classes/http_client.ts
1192
1767
  var HttpClient = class {
1193
1768
  httpRequestor;
1194
1769
  timeoutMs;
1195
- /** Create an HTTP client around fetch or an injected requestor. */
1196
- constructor(httpRequestor = fetch, timeoutMs = 3e4) {
1770
+ wire;
1771
+ /**
1772
+ * Create an HTTP client around fetch or an injected requestor. `wire` selects the query
1773
+ * encoding and the `User-Agent` (see {@link WireOptions}).
1774
+ */
1775
+ constructor(httpRequestor = fetch, timeoutMs = 3e4, wire = {}) {
1197
1776
  this.httpRequestor = httpRequestor;
1198
1777
  this.timeoutMs = timeoutMs;
1778
+ this.wire = wire;
1199
1779
  }
1200
1780
  async sendRequest(method, baseUrl, basePath, endpoint, body, options) {
1201
1781
  const url = `${baseUrl}${basePath}${endpoint}`;
1202
1782
  debugLog(`Sending request to ${url} with body:`, body);
1783
+ const hasBody = Boolean(body);
1203
1784
  const response = await requestWithTimeout(this.httpRequestor, url, {
1204
1785
  method,
1205
- headers: {
1206
- "Content-Type": "application/json",
1207
- "User-Agent": `GalaChain-SDK/${SDK_VERSION}`
1208
- },
1209
- ...body ? { body: JSON.stringify(body) } : {},
1786
+ headers: buildRequestHeaders(this.wire, { json: hasBody }),
1787
+ ...hasBody ? { body: JSON.stringify(body) } : {},
1210
1788
  ...(options === null || options === void 0 ? void 0 : options.signal) === void 0 ? {} : { signal: options.signal }
1211
1789
  }, this.timeoutMs);
1212
1790
  if (!response.ok) throw await GSwapSDKError.fromErrorResponse(url, response);
@@ -1220,12 +1798,65 @@ var HttpClient = class {
1220
1798
  }
1221
1799
  /** Send a GET request with URL-encoded query parameters and decode its response. */
1222
1800
  async sendGetRequest(baseUrl, basePath, endpoint, params, options) {
1223
- const searchParams = params ? new URLSearchParams(params) : void 0;
1224
- const endpointWithParams = searchParams ? `${endpoint}?${searchParams.toString()}` : endpoint;
1801
+ const endpointWithParams = params ? `${endpoint}?${buildQueryString(params, this.wire.queryEncoding)}` : endpoint;
1225
1802
  return this.sendRequest("GET", baseUrl, basePath, endpointWithParams, void 0, options);
1226
1803
  }
1227
1804
  };
1228
1805
 
1806
+ //#endregion
1807
+ //#region src/utils/lenient.ts
1808
+ /** A usable row. */
1809
+ function entryOk(value) {
1810
+ return {
1811
+ ok: true,
1812
+ value
1813
+ };
1814
+ }
1815
+ /** An unusable row, with the reason it is reported under. */
1816
+ function entryRejected(reason) {
1817
+ return {
1818
+ ok: false,
1819
+ reason
1820
+ };
1821
+ }
1822
+ /**
1823
+ * Reads a list answer row by row: a row that cannot be used is skipped and reported, instead of
1824
+ * rejecting the whole list because of one bad row. An answer that is not a list at all is still
1825
+ * `INVALID_CHAIN_RESPONSE`: nothing in it can be trusted.
1826
+ */
1827
+ function readEntries(source, data, parse, onInvalidEntry) {
1828
+ if (!Array.isArray(data)) throw new GSwapSDKError(`Backend returned an invalid ${source} list.`, "INVALID_CHAIN_RESPONSE", { data });
1829
+ const rows = [];
1830
+ data.forEach((entry, index) => {
1831
+ const parsed = parse(entry);
1832
+ if (parsed.ok) {
1833
+ rows.push(parsed.value);
1834
+ return;
1835
+ }
1836
+ try {
1837
+ onInvalidEntry === null || onInvalidEntry === void 0 || onInvalidEntry({
1838
+ source,
1839
+ index,
1840
+ entry,
1841
+ reason: parsed.reason
1842
+ });
1843
+ } catch {}
1844
+ });
1845
+ return rows;
1846
+ }
1847
+ /** A plain JSON object (not null, not an array). */
1848
+ function asEntryRecord(value) {
1849
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1850
+ }
1851
+ /** A finite number, or a non-empty numeric string, as a number; otherwise `undefined`. */
1852
+ function finiteNumberOf(value) {
1853
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1854
+ if (typeof value === "string" && value.trim() !== "") {
1855
+ const parsed = Number(value);
1856
+ return Number.isFinite(parsed) ? parsed : void 0;
1857
+ }
1858
+ }
1859
+
1229
1860
  //#endregion
1230
1861
  //#region src/utils/ordering.ts
1231
1862
  /** Return two trading symbols in the contract's plain-string order. */
@@ -1305,41 +1936,252 @@ function parseTokenClassKey(tokenClassKey) {
1305
1936
  }
1306
1937
 
1307
1938
  //#endregion
1308
- //#region src/classes/pools.ts
1309
- /** Read-only pool and current-price access for the current GalaChainDex contract. */
1310
- var Pools = class {
1939
+ //#region src/classes/symbols.ts
1940
+ const SYMBOL_CACHE_TTL_MS = 6e4;
1941
+ /** Symbol registry client for the current GalaChainDex contract. */
1942
+ var Symbols = class {
1311
1943
  gateway;
1312
- symbols;
1944
+ options;
1945
+ cachedSymbols;
1946
+ cachedAt = 0;
1947
+ refreshInFlight;
1948
+ http;
1313
1949
  /**
1314
- * Creates a pool service.
1315
- *
1316
- * @example
1317
- * ```ts
1318
- * const pools = new Pools(gateway, symbols);
1319
- * ```
1950
+ * Create a symbol service backed by the swap backend. `onInvalidEntry` is told about every
1951
+ * registry row that was skipped as unusable (see {@link InvalidEntryHandler}).
1320
1952
  */
1321
- constructor(gateway, symbols) {
1953
+ constructor(gateway, options = {}) {
1322
1954
  this.gateway = gateway;
1323
- this.symbols = symbols;
1324
- this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4);
1955
+ this.options = options;
1956
+ this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs, gateway.wire);
1325
1957
  }
1326
- http;
1327
1958
  /**
1328
- * Fetches every current-contract pool from the swap backend.
1959
+ * List all registered GalaChainDex trading symbols, cached for 60 seconds. A registry row that
1960
+ * is not a usable symbol is skipped (and reported to `onInvalidEntry`) instead of failing the
1961
+ * whole list.
1962
+ */
1963
+ async list() {
1964
+ if (this.cachedSymbols !== void 0 && Date.now() - this.cachedAt < SYMBOL_CACHE_TTL_MS) return this.cachedSymbols;
1965
+ if (this.refreshInFlight !== void 0) return this.refreshInFlight;
1966
+ this.refreshInFlight = this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/symbols").then((response) => readEntries("symbols", response.data, parseTradingSymbol, this.options.onInvalidEntry)).then((symbols) => {
1967
+ this.cachedSymbols = symbols;
1968
+ this.cachedAt = Date.now();
1969
+ return symbols;
1970
+ }).finally(() => {
1971
+ this.refreshInFlight = void 0;
1972
+ });
1973
+ return this.refreshInFlight;
1974
+ }
1975
+ /** Invalidate the cached symbol registry after a write changes registration state.
1329
1976
  *
1330
1977
  * @example
1331
1978
  * ```ts
1332
- * const pools = await service.getPools();
1333
- * console.log(pools.length);
1979
+ * gSwap.symbols.invalidate();
1334
1980
  * ```
1335
1981
  */
1336
- async getPools(options) {
1337
- const response = await this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/pools", void 0, (options === null || options === void 0 ? void 0 : options.signal) === void 0 ? void 0 : { signal: options.signal });
1338
- if (!Array.isArray(response.data) || !response.data.every(isPoolListInfo)) throw new GSwapSDKError("Backend returned an invalid pool list.", "INVALID_CHAIN_RESPONSE", { data: response.data });
1339
- return response.data;
1982
+ invalidate() {
1983
+ this.cachedSymbols = void 0;
1984
+ this.cachedAt = 0;
1340
1985
  }
1341
- /**
1342
- * Fetches one pool from the backend, preserving whether the request order was flipped.
1986
+ /** Force one registry refresh, bypassing the cache.
1987
+ *
1988
+ * @example
1989
+ * ```ts
1990
+ * const symbols = await gSwap.symbols.refresh();
1991
+ * ```
1992
+ */
1993
+ async refresh() {
1994
+ this.invalidate();
1995
+ this.refreshInFlight = void 0;
1996
+ return this.list();
1997
+ }
1998
+ /** Resolve a symbol or token class key to its registered symbol metadata. */
1999
+ async resolve(ref) {
2000
+ let symbols = await this.list();
2001
+ const composite = typeof ref === "string" ? isCompositeKey(ref) ? compositeKeyOf(parseTokenClassKey(ref)) : void 0 : compositeKeyOf(ref);
2002
+ const resolved = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
2003
+ if (resolved === void 0) {
2004
+ symbols = await this.refresh();
2005
+ const refreshed = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
2006
+ if (refreshed === void 0) throw GSwapSDKError.unknownTokenError(ref);
2007
+ return refreshed;
2008
+ }
2009
+ return resolved;
2010
+ }
2011
+ /**
2012
+ * The trading symbol to put in a token's first CreatePool.
2013
+ *
2014
+ * 1. The symbol ALREADY registered for exactly this class key, if any: proposing another one
2015
+ * fails the gateway's registry-conflict check.
2016
+ * 2. Otherwise `displaySymbol` (the token's curated symbol), when it is proposable
2017
+ * ({@link isProposableTradingSymbol}).
2018
+ * 3. Otherwise a `VALIDATION_ERROR`: a symbol is never derived from the key (BNB's collection is
2019
+ * `GBNB`, a launchpad token's is the reserved word `Token`).
2020
+ *
2021
+ * `registryFailure: 'ignore'` treats an unreachable registry as an empty one and falls through
2022
+ * to step 2 (the gateway still rejects a conflicting symbol); the default `'throw'` propagates
2023
+ * the failure.
2024
+ *
2025
+ * @example
2026
+ * ```ts
2027
+ * const symbol = await gSwap.symbols.proposeSymbol(
2028
+ * { collection: 'GALA_TEST1', category: 'Unit', type: 'none', additionalKey: 'none' },
2029
+ * 'GALATEST',
2030
+ * );
2031
+ * ```
2032
+ */
2033
+ async proposeSymbol(classKey, displaySymbol, options = {}) {
2034
+ let registry = [];
2035
+ try {
2036
+ registry = await this.list();
2037
+ } catch (error) {
2038
+ if (options.registryFailure !== "ignore") throw error;
2039
+ }
2040
+ const wanted = compositeKeyOf(classKey);
2041
+ const registered = registry.find((entry) => entry.collection === classKey.collection && entry.category === classKey.category && entry.type === classKey.type && entry.additionalKey === classKey.additionalKey);
2042
+ if (registered !== void 0) return registered.symbol;
2043
+ if (isProposableTradingSymbol(displaySymbol)) return displaySymbol;
2044
+ throw new GSwapSDKError(`No valid trading symbol for token class ${wanted}: "${displaySymbol}" is not an acceptable symbol and none is registered`, "VALIDATION_ERROR", {
2045
+ classKey,
2046
+ displaySymbol
2047
+ });
2048
+ }
2049
+ /** Resolve and put two token references in canonical trading-symbol order. */
2050
+ async orderPair(a, b) {
2051
+ const [resolvedA, resolvedB] = await Promise.all([this.resolve(a), this.resolve(b)]);
2052
+ const [token0Symbol, token1Symbol] = orderSymbols(resolvedA.symbol, resolvedB.symbol);
2053
+ return {
2054
+ token0: token0Symbol === resolvedA.symbol ? resolvedA : resolvedB,
2055
+ token1: token1Symbol === resolvedB.symbol ? resolvedB : resolvedA,
2056
+ flipped: token0Symbol !== resolvedA.symbol
2057
+ };
2058
+ }
2059
+ };
2060
+ /** Reads one registry row: all four class-key parts, a symbol and non-negative integer decimals. */
2061
+ function parseTradingSymbol(value) {
2062
+ const entry = asEntryRecord(value);
2063
+ if (entry === void 0) return entryRejected("the row is not an object");
2064
+ const { symbol, collection, category, type, additionalKey, decimals } = entry;
2065
+ if (typeof symbol !== "string") return entryRejected("symbol is not a string");
2066
+ if (symbol.length === 0) return entryRejected("symbol is empty");
2067
+ if (typeof collection !== "string") return entryRejected("collection is not a string");
2068
+ if (typeof category !== "string") return entryRejected("category is not a string");
2069
+ if (typeof type !== "string") return entryRejected("type is not a string");
2070
+ if (typeof additionalKey !== "string") return entryRejected("additionalKey is not a string");
2071
+ if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0) return entryRejected("decimals is not a non-negative integer");
2072
+ return entryOk({
2073
+ symbol,
2074
+ collection,
2075
+ category,
2076
+ type,
2077
+ additionalKey,
2078
+ decimals
2079
+ });
2080
+ }
2081
+ function isCompositeKey(ref) {
2082
+ return ref.includes("|") || ref.includes("$");
2083
+ }
2084
+ /** A token reference as the caller wrote it: a string verbatim, a class key as its `$` composite. */
2085
+ function rawTokenOf(ref) {
2086
+ return typeof ref === "string" ? ref : compositeKeyOf(ref);
2087
+ }
2088
+ /** The trading symbol of a reference, or `SYMBOL_NOT_FOUND` when the resolver has none. */
2089
+ async function tradingSymbolOf(resolver, ref) {
2090
+ const resolved = await resolver.resolve(ref);
2091
+ if (resolved.symbol.length > 0) return resolved.symbol;
2092
+ throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token: ref });
2093
+ }
2094
+ /** What a read request names a token by: the reference itself in pass-through mode, else its symbol. */
2095
+ async function queryTokenOf(resolver, ref) {
2096
+ return resolver.passthrough === true ? rawTokenOf(ref) : tradingSymbolOf(resolver, ref);
2097
+ }
2098
+ /**
2099
+ * The offline resolver for apps that already hold the backend's canonical trading symbols (from
2100
+ * `/quote`, `/add-liq-estimate`, a position row) and must not make a registry request of their own.
2101
+ *
2102
+ * - `resolve` accepts a bare trading symbol and returns it as is; a class key (object, `$` or `|`
2103
+ * form) is not a symbol and is refused as `UNKNOWN_TOKEN`. `createPool` relies on that refusal to
2104
+ * fall back to the symbols its caller supplies.
2105
+ * - `orderPair` orders two symbols by plain string comparison, as the contract does.
2106
+ * - Reads are pass-through (see {@link TokenResolver.passthrough}): the caller's references go on
2107
+ * the wire verbatim, composite keys included, and the backend orients the pair.
2108
+ *
2109
+ * @example
2110
+ * ```ts
2111
+ * const swaps = new Swaps(gateway, new CanonicalSymbols(), signer, walletAddress);
2112
+ * await swaps.swap('GALA', 'GUSDC', 3000, { exactIn: '10' });
2113
+ * ```
2114
+ */
2115
+ var CanonicalSymbols = class {
2116
+ passthrough = true;
2117
+ /** Resolve a bare trading symbol to itself. */
2118
+ resolve(ref) {
2119
+ if (typeof ref === "string" && !isCompositeKey(ref)) return Promise.resolve({
2120
+ symbol: ref,
2121
+ collection: "",
2122
+ category: "",
2123
+ type: "",
2124
+ additionalKey: "",
2125
+ decimals: 0
2126
+ });
2127
+ return Promise.reject(GSwapSDKError.unknownTokenError(ref));
2128
+ }
2129
+ /** Put two symbols in canonical (string) order. */
2130
+ async orderPair(a, b) {
2131
+ const [resolvedA, resolvedB] = await Promise.all([this.resolve(a), this.resolve(b)]);
2132
+ const [first] = orderSymbols(resolvedA.symbol, resolvedB.symbol);
2133
+ const flipped = first !== resolvedA.symbol;
2134
+ return {
2135
+ token0: flipped ? resolvedB : resolvedA,
2136
+ token1: flipped ? resolvedA : resolvedB,
2137
+ flipped
2138
+ };
2139
+ }
2140
+ /** There is no registry state to drop. */
2141
+ invalidate() {}
2142
+ };
2143
+
2144
+ //#endregion
2145
+ //#region src/classes/pools.ts
2146
+ /** Read-only pool and current-price access for the current GalaChainDex contract. */
2147
+ var Pools = class {
2148
+ gateway;
2149
+ symbols;
2150
+ options;
2151
+ /**
2152
+ * Creates a pool service. `options.onInvalidEntry` is told about every pool row that was
2153
+ * skipped as unusable (see {@link InvalidEntryHandler}).
2154
+ *
2155
+ * @example
2156
+ * ```ts
2157
+ * const pools = new Pools(gateway, symbols);
2158
+ * ```
2159
+ */
2160
+ constructor(gateway, symbols, options = {}) {
2161
+ this.gateway = gateway;
2162
+ this.symbols = symbols;
2163
+ this.options = options;
2164
+ this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4, gateway.wire);
2165
+ }
2166
+ http;
2167
+ /**
2168
+ * Fetches current-contract pools from the swap backend, optionally searched, sorted and paged
2169
+ * by the backend. A row that is not a usable pool is skipped and reported to `onInvalidEntry`
2170
+ * instead of failing the whole list; an answer that is not a list at all is
2171
+ * `INVALID_CHAIN_RESPONSE`.
2172
+ *
2173
+ * @example
2174
+ * ```ts
2175
+ * const top = await service.getPools({ sortBy: 'tvl', sortOrder: 'desc', page: 1, limit: 8 });
2176
+ * console.log(top.length);
2177
+ * ```
2178
+ */
2179
+ async getPools(options) {
2180
+ const response = await this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/pools", poolListParams(options), (options === null || options === void 0 ? void 0 : options.signal) === void 0 ? void 0 : { signal: options.signal });
2181
+ return readEntries("pools", response.data, parsePoolListInfo, (options === null || options === void 0 ? void 0 : options.onInvalidEntry) ?? this.options.onInvalidEntry);
2182
+ }
2183
+ /**
2184
+ * Fetches one pool from the backend, preserving whether the request order was flipped.
1343
2185
  *
1344
2186
  * @example
1345
2187
  * ```ts
@@ -1375,8 +2217,13 @@ var Pools = class {
1375
2217
  */
1376
2218
  async getCompositePool(token0, token1, fee) {
1377
2219
  validateFee(fee);
1378
- const first = await resolveSymbol$2(this.symbols, token0);
1379
- const second = await resolveSymbol$2(this.symbols, token1);
2220
+ if (this.symbols.passthrough === true) return this.getBackend("/composite-pool", {
2221
+ token0: rawTokenOf(token0),
2222
+ token1: rawTokenOf(token1),
2223
+ fee: String(fee)
2224
+ });
2225
+ const first = await tradingSymbolOf(this.symbols, token0);
2226
+ const second = await tradingSymbolOf(this.symbols, token1);
1380
2227
  const ordered = readOrderedSymbols(orderSymbols(first, second));
1381
2228
  return this.getBackend("/composite-pool", {
1382
2229
  token0: ordered.token0,
@@ -1386,8 +2233,8 @@ var Pools = class {
1386
2233
  }
1387
2234
  async backendParams(token0, token1, fee) {
1388
2235
  return {
1389
- token0: await resolveSymbol$2(this.symbols, token0),
1390
- token1: await resolveSymbol$2(this.symbols, token1),
2236
+ token0: await queryTokenOf(this.symbols, token0),
2237
+ token1: await queryTokenOf(this.symbols, token1),
1391
2238
  fee: String(fee)
1392
2239
  };
1393
2240
  }
@@ -1400,52 +2247,448 @@ var Pools = class {
1400
2247
  }
1401
2248
  }
1402
2249
  };
1403
- async function resolveSymbol$2(symbols, token) {
1404
- const resolved = await symbols.resolve(token);
1405
- if (resolved.symbol.length > 0) return resolved.symbol;
1406
- throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token });
2250
+ const SORT_KEYS = [
2251
+ "tvl",
2252
+ "volume1d",
2253
+ "volume30d"
2254
+ ];
2255
+ const MAX_SEARCH_LENGTH = 64;
2256
+ const MAX_PAGE_SIZE = 1e3;
2257
+ /** The pool-list query, in the order the apps write it: search, sortBy, sortOrder, page, limit. */
2258
+ function poolListParams(options) {
2259
+ if (options === void 0) return void 0;
2260
+ const params = {};
2261
+ if (options.search !== void 0) {
2262
+ if (options.search.length > MAX_SEARCH_LENGTH) throw new GSwapSDKError(`search must be at most ${MAX_SEARCH_LENGTH} characters.`, "VALIDATION_ERROR", { search: options.search });
2263
+ params["search"] = options.search;
2264
+ }
2265
+ if (options.sortBy !== void 0) {
2266
+ if (!SORT_KEYS.includes(options.sortBy)) throw new GSwapSDKError(`sortBy must be one of ${SORT_KEYS.join(", ")}.`, "VALIDATION_ERROR", { sortBy: options.sortBy });
2267
+ params["sortBy"] = options.sortBy;
2268
+ }
2269
+ if (options.sortOrder !== void 0) {
2270
+ if (options.sortOrder !== "asc" && options.sortOrder !== "desc") throw new GSwapSDKError("sortOrder must be asc or desc.", "VALIDATION_ERROR", { sortOrder: options.sortOrder });
2271
+ params["sortOrder"] = options.sortOrder;
2272
+ }
2273
+ if (options.page !== void 0) {
2274
+ if (!Number.isInteger(options.page) || options.page < 1) throw new GSwapSDKError("page must be an integer of at least 1.", "VALIDATION_ERROR", { page: options.page });
2275
+ params["page"] = String(options.page);
2276
+ }
2277
+ if (options.limit !== void 0) {
2278
+ if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > MAX_PAGE_SIZE) throw new GSwapSDKError(`limit must be an integer from 1 to ${MAX_PAGE_SIZE}.`, "VALIDATION_ERROR", { limit: options.limit });
2279
+ params["limit"] = String(options.limit);
2280
+ }
2281
+ return Object.keys(params).length === 0 ? void 0 : params;
1407
2282
  }
1408
- function isPoolListInfo(value) {
1409
- if (typeof value !== "object" || value === null) return false;
1410
- const row = value;
1411
- return row["contractVersion"] === "v2" && typeof row["token0"] === "string" && typeof row["token1"] === "string" && typeof row["poolName"] === "string" && typeof row["poolRef"] === "string" && [
1412
- "fee",
1413
- "tickSpacing",
1414
- "protocolFees",
1415
- "tradingFees"
1416
- ].every((key) => isFiniteNumber(row[key])) && [
1417
- "token0CompositeKey",
1418
- "token1CompositeKey",
1419
- "creator",
1420
- "price",
1421
- "sqrtPrice",
1422
- "token0Tvl",
1423
- "token1Tvl"
1424
- ].every((key) => isOptionalString(row[key])) && [
1425
- "tick",
1426
- "tvlUsd",
1427
- "volume1d",
1428
- "volume7d",
1429
- "volume30d",
1430
- "trades1d",
1431
- "fee24h",
1432
- "apr1d",
1433
- "token0Price",
1434
- "token1Price"
1435
- ].every((key) => isOptionalNumber(row[key]));
1436
- }
1437
- function isFiniteNumber(value) {
1438
- return typeof value === "number" && Number.isFinite(value);
1439
- }
1440
- function isOptionalString(value) {
1441
- return value === void 0 || typeof value === "string";
1442
- }
1443
- function isOptionalNumber(value) {
1444
- return value === void 0 || isFiniteNumber(value);
2283
+ const OPTIONAL_POOL_STRINGS = [
2284
+ "poolName",
2285
+ "token0CompositeKey",
2286
+ "token1CompositeKey",
2287
+ "token0Image",
2288
+ "token1Image",
2289
+ "creator",
2290
+ "price",
2291
+ "sqrtPrice",
2292
+ "token0Tvl",
2293
+ "token1Tvl"
2294
+ ];
2295
+ const OPTIONAL_POOL_NUMBERS = [
2296
+ "tickSpacing",
2297
+ "protocolFees",
2298
+ "tradingFees",
2299
+ "tick",
2300
+ "volume1d",
2301
+ "volume7d",
2302
+ "volume30d",
2303
+ "trades1d",
2304
+ "fee24h",
2305
+ "token0Price",
2306
+ "token1Price"
2307
+ ];
2308
+ /** Fields the apps accept as a number or a numeric string (the wallet does `Number(row.x)`). */
2309
+ const NUMERIC_POOL_FIELDS = [
2310
+ "fee",
2311
+ "tvlUsd",
2312
+ "apr1d"
2313
+ ];
2314
+ /**
2315
+ * Reads one pool-list row the way the apps do: the identity (`token0`, `token1`, `poolRef`) and
2316
+ * `fee` must be usable, every other field is optional but, when present, must have its type.
2317
+ * `fee`, `tvlUsd` and `apr1d` may arrive as numeric strings and are returned as numbers. Fields
2318
+ * the SDK does not know are kept.
2319
+ */
2320
+ function parsePoolListInfo(entry) {
2321
+ const row = asEntryRecord(entry);
2322
+ if (row === void 0) return entryRejected("the row is not an object");
2323
+ for (const key of [
2324
+ "token0",
2325
+ "token1",
2326
+ "poolRef"
2327
+ ]) if (typeof row[key] !== "string") return entryRejected(`${key} is not a string`);
2328
+ if (row["contractVersion"] !== void 0 && row["contractVersion"] !== "v2") return entryRejected("contractVersion is not v2");
2329
+ const normalized = { ...row };
2330
+ for (const key of NUMERIC_POOL_FIELDS) {
2331
+ const raw = row[key];
2332
+ if (raw === void 0 && key !== "fee") continue;
2333
+ const value = finiteNumberOf(raw);
2334
+ if (value === void 0) return entryRejected(`${key} is not a number`);
2335
+ normalized[key] = value;
2336
+ }
2337
+ for (const key of OPTIONAL_POOL_STRINGS) if (row[key] !== void 0 && typeof row[key] !== "string") return entryRejected(`${key} is not a string`);
2338
+ for (const key of OPTIONAL_POOL_NUMBERS) {
2339
+ const value = row[key];
2340
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value))) return entryRejected(`${key} is not a number`);
2341
+ }
2342
+ return entryOk(normalized);
2343
+ }
2344
+
2345
+ //#endregion
2346
+ //#region src/utils/unique_key.ts
2347
+ /**
2348
+ * The `uniqueKey` prefix frontend_v2 and wallet_extension have always sent
2349
+ * (`galaswap-operation-<uuid>`). defi-backend does not depend on it: the chain-write verifier
2350
+ * only requires a non-empty key of at most 256 characters, and apply-results accepts
2351
+ * `[A-Za-z0-9._:-]{1,128}`; the only code that mints a prefixed key itself is the server-side v1
2352
+ * trade builder, which nothing reads back by prefix.
2353
+ */
2354
+ const DEFAULT_UNIQUE_KEY_PREFIX = "galaswap-operation-";
2355
+ /** A canonical UUID is 36 characters; the apply-results key limit is 128. */
2356
+ const MAX_UNIQUE_KEY_PREFIX_LENGTH = 92;
2357
+ const UNIQUE_KEY_PREFIX_PATTERN = /^[A-Za-z0-9._:-]*$/u;
2358
+ /**
2359
+ * Validates a caller-supplied `uniqueKey` prefix. The whole key (`<prefix><uuid>`) must stay a
2360
+ * valid apply-results key, otherwise `confirm()` could never read the outcome of the write.
2361
+ *
2362
+ * @throws GSwapSDKError VALIDATION_ERROR when the prefix has characters outside `A-Za-z0-9._:-` or
2363
+ * is longer than 92 characters.
2364
+ */
2365
+ function resolveUniqueKeyPrefix(prefix) {
2366
+ if (prefix === void 0) return DEFAULT_UNIQUE_KEY_PREFIX;
2367
+ if (!UNIQUE_KEY_PREFIX_PATTERN.test(prefix) || prefix.length > MAX_UNIQUE_KEY_PREFIX_LENGTH) throw new GSwapSDKError(`uniqueKeyPrefix must be at most ${MAX_UNIQUE_KEY_PREFIX_LENGTH} characters of A-Z, a-z, 0-9, ".", "_", ":" or "-".`, "VALIDATION_ERROR", { uniqueKeyPrefix: prefix });
2368
+ return prefix;
2369
+ }
2370
+ /** The swap backend's apply-results key constraint (`apply-results.dto.ts`). */
2371
+ const UNIQUE_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
2372
+ /**
2373
+ * Validates a caller-supplied write `uniqueKey`: 1 to 128 characters of `A-Za-z0-9._:-`, the
2374
+ * shape the apply-results route accepts, so the write's outcome can always be read back with
2375
+ * `confirm()`. A key is one attempt's identity: reuse it only to retry that same attempt.
2376
+ *
2377
+ * @throws GSwapSDKError VALIDATION_ERROR otherwise.
2378
+ */
2379
+ function assertUniqueKey(uniqueKey) {
2380
+ if (typeof uniqueKey !== "string" || !UNIQUE_KEY_PATTERN.test(uniqueKey)) throw new GSwapSDKError("uniqueKey must be 1-128 characters of A-Z, a-z, 0-9, \".\", \"_\", \":\" or \"-\".", "VALIDATION_ERROR", { uniqueKey });
2381
+ }
2382
+ /** A fresh per-attempt key: `<prefix><random UUID>`. */
2383
+ function createUniqueKey(prefix) {
2384
+ return `${prefix}${globalThis.crypto.randomUUID()}`;
2385
+ }
2386
+
2387
+ //#endregion
2388
+ //#region src/utils/dto_builders.ts
2389
+ const decimalString = (amount) => new BigNumber(amount).toFixed();
2390
+ /**
2391
+ * Builds the RequestTrade DTO: `exactIn` sells that much of the input token (`sell0Qty` or
2392
+ * `sell1Qty`), `exactOut` buys that much of the output token (`buy1Qty` or `buy0Qty`), plus the
2393
+ * side's slippage bound and an optional `priceLimit`. Key order: `token0, token1, fee, uniqueKey,
2394
+ * <quantity>, amountOutMinimum | amountInMaximum, priceLimit`.
2395
+ *
2396
+ * @example
2397
+ * ```ts
2398
+ * const dto = buildRequestTradeDto({
2399
+ * token0: quote.token0Symbol, token1: quote.token1Symbol,
2400
+ * tokenInIsToken0: quote.tokenInIsToken0, fee: quote.fee,
2401
+ * uniqueKey: createUniqueKey('galaswap-operation-'),
2402
+ * exactIn: '100', amountOutMinimum: '14',
2403
+ * });
2404
+ * ```
2405
+ */
2406
+ function buildRequestTradeDto(input) {
2407
+ validateFee(input.fee);
2408
+ assertCanonicalPair(input.token0, input.token1);
2409
+ assertUniqueKey(input.uniqueKey);
2410
+ const dto = {
2411
+ token0: input.token0,
2412
+ token1: input.token1,
2413
+ fee: input.fee,
2414
+ uniqueKey: input.uniqueKey
2415
+ };
2416
+ if ("exactIn" in input) {
2417
+ const exactIn = input.exactIn;
2418
+ if (exactIn === void 0) throw new GSwapSDKError("exactIn is required for an exact-input swap.", "VALIDATION_ERROR");
2419
+ validateNumericAmount(exactIn, "exactIn");
2420
+ dto[input.tokenInIsToken0 ? "sell0Qty" : "sell1Qty"] = decimalString(exactIn);
2421
+ if (input.amountOutMinimum !== void 0) {
2422
+ validateNumericAmount(input.amountOutMinimum, "amountOutMinimum", true);
2423
+ dto["amountOutMinimum"] = decimalString(input.amountOutMinimum);
2424
+ }
2425
+ } else {
2426
+ const exactOut = input.exactOut;
2427
+ if (exactOut === void 0) throw new GSwapSDKError("exactOut is required for an exact-output swap.", "VALIDATION_ERROR");
2428
+ validateNumericAmount(exactOut, "exactOut");
2429
+ dto[input.tokenInIsToken0 ? "buy1Qty" : "buy0Qty"] = decimalString(exactOut);
2430
+ if (input.amountInMaximum !== void 0) {
2431
+ validateNumericAmount(input.amountInMaximum, "amountInMaximum", true);
2432
+ dto["amountInMaximum"] = decimalString(input.amountInMaximum);
2433
+ }
2434
+ }
2435
+ if (input.priceLimit !== void 0) {
2436
+ validateNumericAmount(input.priceLimit, "priceLimit");
2437
+ dto["priceLimit"] = decimalString(input.priceLimit);
2438
+ }
2439
+ return dto;
2440
+ }
2441
+ function assertPoolPosition(input) {
2442
+ validateFee(input.fee);
2443
+ assertCanonicalPair(input.token0, input.token1);
2444
+ assertTickRange(input.tickLower, input.tickUpper, input.fee);
2445
+ assertUniqueKey(input.uniqueKey);
2446
+ }
2447
+ /**
2448
+ * Builds the AddLiquidity DTO: exactly one of `depositQuantityToken0` / `depositQuantityToken1`
2449
+ * (the other side is derived on chain). Key order: `token0, token1, fee, tickLower, tickUpper,
2450
+ * uniqueKey, <deposit>`.
2451
+ */
2452
+ function buildAddLiquidityDto(input) {
2453
+ assertPoolPosition(input);
2454
+ const side = onlyOne(input.depositQuantityToken0, input.depositQuantityToken1, "depositQuantityToken0", "depositQuantityToken1");
2455
+ if (side === void 0) throw new GSwapSDKError("Provide exactly one of depositQuantityToken0 or depositQuantityToken1.", "VALIDATION_ERROR");
2456
+ validateNumericAmount(side.amount, side.name);
2457
+ return {
2458
+ token0: input.token0,
2459
+ token1: input.token1,
2460
+ fee: input.fee,
2461
+ tickLower: input.tickLower,
2462
+ tickUpper: input.tickUpper,
2463
+ uniqueKey: input.uniqueKey,
2464
+ [side.name]: decimalString(side.amount)
2465
+ };
2466
+ }
2467
+ /**
2468
+ * Builds the RemoveLiquidity DTO: at most one of `withdrawalQuantityToken0` /
2469
+ * `withdrawalQuantityToken1`; omitting both closes the position and sweeps its fees.
2470
+ */
2471
+ function buildRemoveLiquidityDto(input) {
2472
+ assertPoolPosition(input);
2473
+ const side = onlyOne(input.withdrawalQuantityToken0, input.withdrawalQuantityToken1, "withdrawalQuantityToken0", "withdrawalQuantityToken1", true);
2474
+ if (side !== void 0) validateNumericAmount(side.amount, side.name);
2475
+ return {
2476
+ token0: input.token0,
2477
+ token1: input.token1,
2478
+ fee: input.fee,
2479
+ tickLower: input.tickLower,
2480
+ tickUpper: input.tickUpper,
2481
+ uniqueKey: input.uniqueKey,
2482
+ ...side === void 0 ? {} : { [side.name]: decimalString(side.amount) }
2483
+ };
2484
+ }
2485
+ /** Builds the CollectPositionFees DTO (the contract sweeps all of a position's fees). */
2486
+ function buildCollectPositionFeesDto(input) {
2487
+ assertPoolPosition(input);
2488
+ return {
2489
+ token0: input.token0,
2490
+ token1: input.token1,
2491
+ fee: input.fee,
2492
+ tickLower: input.tickLower,
2493
+ tickUpper: input.tickUpper,
2494
+ uniqueKey: input.uniqueKey
2495
+ };
2496
+ }
2497
+ /**
2498
+ * Builds the CreatePool DTO. Key order: `token0Key, token1Key, fee, startingPrice |
2499
+ * startingSqrtPrice, token0Symbol, token1Symbol, uniqueKey, name, isPrivate, privateAccess`.
2500
+ * Prices are those of the canonical pair (token1 per token0); a price quoted the other way round
2501
+ * is inverted by the caller before it gets here.
2502
+ */
2503
+ function buildCreatePoolDto(input) {
2504
+ validateFee(input.fee);
2505
+ assertUniqueKey(input.uniqueKey);
2506
+ assertCanonicalPair(input.token0Symbol, input.token1Symbol);
2507
+ for (const symbol of [input.token0Symbol, input.token1Symbol]) if (!isProposableTradingSymbol(symbol)) throw new GSwapSDKError("Invalid trading symbol: must be 1-32 ASCII letters and digits and not a reserved word (Token, Unit, none).", "VALIDATION_ERROR", { symbol });
2508
+ for (const key of [input.token0Key, input.token1Key]) assertClassKey(key);
2509
+ const price = onlyOne(input.startingPrice, input.startingSqrtPrice, "startingPrice", "startingSqrtPrice");
2510
+ if (price === void 0) throw new GSwapSDKError("Provide exactly one of startingPrice or startingSqrtPrice.", "VALIDATION_ERROR");
2511
+ if (typeof price.amount === "number") throw new GSwapSDKError(`Invalid ${price.name}: use a decimal string or BigNumber, not a JavaScript number`, "VALIDATION_ERROR");
2512
+ const parsed = new BigNumber(price.amount);
2513
+ if (!parsed.isFinite() || parsed.isLessThanOrEqualTo(0)) throw new GSwapSDKError("Starting price must be finite and positive.", "VALIDATION_ERROR");
2514
+ return {
2515
+ token0Key: input.token0Key,
2516
+ token1Key: input.token1Key,
2517
+ fee: input.fee,
2518
+ [price.name]: parsed.toFixed(),
2519
+ token0Symbol: input.token0Symbol,
2520
+ token1Symbol: input.token1Symbol,
2521
+ uniqueKey: input.uniqueKey,
2522
+ ...input.name === void 0 || input.name === "" ? {} : { name: input.name },
2523
+ ...input.isPrivate === void 0 ? {} : { isPrivate: input.isPrivate },
2524
+ ...input.privateAccess === void 0 ? {} : { privateAccess: input.privateAccess }
2525
+ };
2526
+ }
2527
+ function assertClassKey(key) {
2528
+ for (const part of [
2529
+ "collection",
2530
+ "category",
2531
+ "type",
2532
+ "additionalKey"
2533
+ ]) if (typeof key[part] !== "string" || key[part].length === 0) throw new GSwapSDKError(`Token class key is missing ${part}.`, "VALIDATION_ERROR", { key });
2534
+ }
2535
+ /**
2536
+ * Of two mutually exclusive fields, the one that is set (`undefined` when neither is);
2537
+ * both set is a `VALIDATION_ERROR`, and so is neither unless `allowNeither`.
2538
+ */
2539
+ function onlyOne(first, second, firstName, secondName, allowNeither = false) {
2540
+ if (first !== void 0 && second !== void 0) throw new GSwapSDKError(allowNeither ? `Provide at most one of ${firstName} or ${secondName}.` : `Provide exactly one of ${firstName} or ${secondName}.`, "VALIDATION_ERROR");
2541
+ if (first !== void 0) return {
2542
+ name: firstName,
2543
+ amount: first
2544
+ };
2545
+ if (second !== void 0) return {
2546
+ name: secondName,
2547
+ amount: second
2548
+ };
2549
+ }
2550
+ function invalidDto(method, field, expected) {
2551
+ return new GSwapSDKError(`${method} DTO field ${field} must be ${expected}.`, "VALIDATION_ERROR", {
2552
+ type: "INVALID_DTO",
2553
+ method,
2554
+ field
2555
+ });
2556
+ }
2557
+ /**
2558
+ * Rebuilds a DTO from its own fields with the builder of its method: the canonical form of the
2559
+ * DTO, with the same validation. Key order and decimal text are the builder's.
2560
+ */
2561
+ function rebuildV2Dto(method, dto) {
2562
+ const text = (field) => {
2563
+ const value = dto[field];
2564
+ if (typeof value !== "string") throw invalidDto(method, field, "a string");
2565
+ return value;
2566
+ };
2567
+ const integer = (field) => {
2568
+ const value = dto[field];
2569
+ if (typeof value !== "number") throw invalidDto(method, field, "a number");
2570
+ return value;
2571
+ };
2572
+ const optionalText = (field) => dto[field] === void 0 ? void 0 : text(field);
2573
+ const classKey = (field) => {
2574
+ const value = dto[field];
2575
+ if (typeof value !== "object" || value === null) throw invalidDto(method, field, "a class key");
2576
+ const key = value;
2577
+ const part = (name) => {
2578
+ if (typeof key[name] !== "string") throw invalidDto(method, `${field}.${name}`, "a string");
2579
+ return key[name];
2580
+ };
2581
+ return {
2582
+ collection: part("collection"),
2583
+ category: part("category"),
2584
+ type: part("type"),
2585
+ additionalKey: part("additionalKey")
2586
+ };
2587
+ };
2588
+ const base = () => ({
2589
+ token0: text("token0"),
2590
+ token1: text("token1"),
2591
+ fee: integer("fee")
2592
+ });
2593
+ const uniqueKey = text("uniqueKey");
2594
+ const position = () => ({
2595
+ ...base(),
2596
+ uniqueKey,
2597
+ tickLower: integer("tickLower"),
2598
+ tickUpper: integer("tickUpper")
2599
+ });
2600
+ switch (method) {
2601
+ case "RequestTrade": {
2602
+ const qty = [
2603
+ "sell0Qty",
2604
+ "sell1Qty",
2605
+ "buy0Qty",
2606
+ "buy1Qty"
2607
+ ].filter((f) => dto[f] !== void 0);
2608
+ const [field] = qty;
2609
+ if (qty.length !== 1 || field === void 0) throw invalidDto(method, "sell0Qty|sell1Qty|buy0Qty|buy1Qty", "present exactly once");
2610
+ const tokenInIsToken0 = field === "sell0Qty" || field === "buy1Qty";
2611
+ const shared = {
2612
+ ...base(),
2613
+ uniqueKey,
2614
+ tokenInIsToken0
2615
+ };
2616
+ const priceLimit = optionalText("priceLimit");
2617
+ return field.startsWith("sell") ? buildRequestTradeDto({
2618
+ ...shared,
2619
+ exactIn: text(field),
2620
+ amountOutMinimum: optionalText("amountOutMinimum"),
2621
+ priceLimit
2622
+ }) : buildRequestTradeDto({
2623
+ ...shared,
2624
+ exactOut: text(field),
2625
+ amountInMaximum: optionalText("amountInMaximum"),
2626
+ priceLimit
2627
+ });
2628
+ }
2629
+ case "AddLiquidity": {
2630
+ const token0Side = optionalText("depositQuantityToken0");
2631
+ const token1Side = optionalText("depositQuantityToken1");
2632
+ return buildAddLiquidityDto({
2633
+ ...position(),
2634
+ ...token0Side === void 0 ? { depositQuantityToken1: token1Side } : { depositQuantityToken0: token0Side }
2635
+ });
2636
+ }
2637
+ case "RemoveLiquidity": return buildRemoveLiquidityDto({
2638
+ ...position(),
2639
+ withdrawalQuantityToken0: optionalText("withdrawalQuantityToken0"),
2640
+ withdrawalQuantityToken1: optionalText("withdrawalQuantityToken1")
2641
+ });
2642
+ case "CollectPositionFees": return buildCollectPositionFeesDto(position());
2643
+ case "CreatePool": {
2644
+ const price = optionalText("startingPrice");
2645
+ const sqrtPrice = optionalText("startingSqrtPrice");
2646
+ const privateAccess = dto["privateAccess"];
2647
+ if (privateAccess !== void 0 && (!Array.isArray(privateAccess) || privateAccess.some((entry) => typeof entry !== "string"))) throw invalidDto(method, "privateAccess", "an array of strings");
2648
+ if (dto["isPrivate"] !== void 0 && typeof dto["isPrivate"] !== "boolean") throw invalidDto(method, "isPrivate", "a boolean");
2649
+ return buildCreatePoolDto({
2650
+ token0Key: classKey("token0Key"),
2651
+ token1Key: classKey("token1Key"),
2652
+ token0Symbol: text("token0Symbol"),
2653
+ token1Symbol: text("token1Symbol"),
2654
+ fee: integer("fee"),
2655
+ uniqueKey,
2656
+ name: optionalText("name"),
2657
+ isPrivate: dto["isPrivate"],
2658
+ privateAccess,
2659
+ ...price === void 0 ? { startingSqrtPrice: sqrtPrice } : { startingPrice: price }
2660
+ });
2661
+ }
2662
+ }
2663
+ }
2664
+ /**
2665
+ * Asserts that `dto` is EXACTLY what the builder of `method` produces for its own fields — the
2666
+ * same fields, canonical decimal text, builder key order, nothing extra (no `signature`). The
2667
+ * `submit*` methods use it so that what an app showed a user (a DTO it built) is byte for byte
2668
+ * what gets signed; a DTO edited by hand, or built elsewhere, is refused rather than signed.
2669
+ *
2670
+ * @throws GSwapSDKError VALIDATION_ERROR (`details.type` = `INVALID_DTO` or `DTO_NOT_CANONICAL`).
2671
+ */
2672
+ function assertCanonicalV2Dto(method, dto) {
2673
+ const rebuilt = rebuildV2Dto(method, dto);
2674
+ if (JSON.stringify(rebuilt) !== JSON.stringify(dto)) throw new GSwapSDKError(`The ${method} DTO is not in the form the SDK builders produce; build it with the matching build function and submit that.`, "VALIDATION_ERROR", {
2675
+ type: "DTO_NOT_CANONICAL",
2676
+ method
2677
+ });
1445
2678
  }
1446
2679
 
1447
2680
  //#endregion
1448
2681
  //#region src/classes/positions.ts
2682
+ /** The pool position a liquidity DTO names, as its confirmation needs it. */
2683
+ function identityOf(dto) {
2684
+ return {
2685
+ token0: dto.token0,
2686
+ token1: dto.token1,
2687
+ fee: dto.fee,
2688
+ tickLower: dto.tickLower,
2689
+ tickUpper: dto.tickUpper
2690
+ };
2691
+ }
1449
2692
  /**
1450
2693
  * Manages current-contract concentrated-liquidity positions.
1451
2694
  *
@@ -1461,26 +2704,32 @@ var Positions = class {
1461
2704
  symbols;
1462
2705
  signer;
1463
2706
  walletAddress;
1464
- constructor(gateway, symbols, signer, walletAddress) {
2707
+ constructor(gateway, symbols, signer, walletAddress, options = {}) {
1465
2708
  this.gateway = gateway;
1466
2709
  this.symbols = symbols;
1467
2710
  this.signer = signer;
1468
2711
  this.walletAddress = walletAddress;
1469
- this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4);
2712
+ this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4, gateway.wire);
2713
+ this.uniqueKeyPrefix = resolveUniqueKeyPrefix(options.uniqueKeyPrefix);
2714
+ this.onInvalidEntry = options.onInvalidEntry;
1470
2715
  }
1471
2716
  http;
2717
+ uniqueKeyPrefix;
2718
+ onInvalidEntry;
1472
2719
  /**
1473
2720
  * Gets all current-contract positions owned by an address.
1474
2721
  *
1475
2722
  * @param owner - GalaChain owner identity.
1476
- * @returns Enriched positions from the v2 trade backend.
2723
+ * @returns Enriched positions from the v2 trade backend, token images included. A row that is
2724
+ * not a usable position is skipped and reported to `onInvalidEntry`.
1477
2725
  * @example
1478
2726
  * ```typescript
1479
2727
  * const positions = await gSwap.positions.getUserPositions('client|012345678901234567890123');
1480
2728
  * ```
1481
2729
  */
1482
2730
  async getUserPositions(owner) {
1483
- return (await this.get("/positions", { user: owner })).data.map((position) => this.mapPosition(position));
2731
+ const response = await this.get("/positions", { user: owner });
2732
+ return readEntries("positions", response.data, parsePosition, this.onInvalidEntry);
1484
2733
  }
1485
2734
  /**
1486
2735
  * Gets one position, normalizing a caller-supplied reversed pair to canonical symbol order.
@@ -1497,17 +2746,65 @@ var Positions = class {
1497
2746
  * ```
1498
2747
  */
1499
2748
  async getPosition(args, signal) {
2749
+ const response = await this.getNullable("/position", await this.positionParams(args), signal);
2750
+ return response === null ? null : readPositionBody(response.data);
2751
+ }
2752
+ /**
2753
+ * Reads one position and classifies the outcome instead of throwing: `found` with the position
2754
+ * (fees included), `not_found` (the backend's HTTP 404, which it sends only when the chain
2755
+ * itself has no such position) or `unknown` (any other failure, carried as `error`). Use it
2756
+ * to confirm a write: `unknown` is "no evidence either way", never a confirmed absence.
2757
+ *
2758
+ * Only the caller's own abort rethrows. With a pass-through resolver ({@link CanonicalSymbols})
2759
+ * the pair and ticks go to the backend exactly as given and the backend orients them.
2760
+ *
2761
+ * @example
2762
+ * ```typescript
2763
+ * const read = await gSwap.positions.readPosition({
2764
+ * token0: 'GALA', token1: 'GUSDC', owner: 'client|012345678901234567890123',
2765
+ * fee: 3000, tickLower: -19200, tickUpper: 12000,
2766
+ * });
2767
+ * if (read.kind === 'found') console.log(read.data.fees0);
2768
+ * ```
2769
+ */
2770
+ async readPosition(args, signal) {
2771
+ try {
2772
+ const position = await this.getPosition(args, signal);
2773
+ return position === null ? { kind: "not_found" } : {
2774
+ kind: "found",
2775
+ data: position
2776
+ };
2777
+ } catch (error) {
2778
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) === true) throw error;
2779
+ return {
2780
+ kind: "unknown",
2781
+ error: error instanceof GSwapSDKError ? error : new GSwapSDKError("Position request failed.", "HTTP_ERROR", { cause: error })
2782
+ };
2783
+ }
2784
+ }
2785
+ /**
2786
+ * The `/position` query: canonical symbols and canonical ticks after a client-side flip, or,
2787
+ * for a pass-through resolver, the caller's references and ticks untouched.
2788
+ */
2789
+ async positionParams(args) {
2790
+ if (this.symbols.passthrough === true) return {
2791
+ token0: rawTokenOf(args.token0),
2792
+ token1: rawTokenOf(args.token1),
2793
+ fee: `${args.fee}`,
2794
+ owner: args.owner,
2795
+ tickLower: `${args.tickLower}`,
2796
+ tickUpper: `${args.tickUpper}`
2797
+ };
1500
2798
  const pair = await this.symbols.orderPair(args.token0, args.token1);
1501
2799
  const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
1502
- const response = await this.getNullable("/position", {
2800
+ return {
1503
2801
  token0: pair.token0.symbol,
1504
2802
  token1: pair.token1.symbol,
1505
2803
  fee: `${args.fee}`,
1506
2804
  owner: args.owner,
1507
2805
  tickLower: `${ticks.tickLower}`,
1508
2806
  tickUpper: `${ticks.tickUpper}`
1509
- }, signal);
1510
- return response === null ? null : this.mapPosition(response.data);
2807
+ };
1511
2808
  }
1512
2809
  /**
1513
2810
  * Estimates the other token amount and resulting liquidity for a deposit.
@@ -1525,8 +2822,8 @@ var Positions = class {
1525
2822
  async estimateAddLiquidity(args) {
1526
2823
  validateNumericAmount(args.amount, "amount");
1527
2824
  return (await this.get("/add-liq-estimate", {
1528
- token0: this.requestToken(args.token0),
1529
- token1: this.requestToken(args.token1),
2825
+ token0: rawTokenOf(args.token0),
2826
+ token1: rawTokenOf(args.token1),
1530
2827
  fee: `${args.fee}`,
1531
2828
  tickLower: `${args.tickLower}`,
1532
2829
  tickUpper: `${args.tickUpper}`,
@@ -1550,8 +2847,8 @@ var Positions = class {
1550
2847
  async estimateRemoveLiquidity(args) {
1551
2848
  validateNumericAmount(args.liquidity, "liquidity");
1552
2849
  return (await this.get("/remove-liq-estimate", {
1553
- token0: this.requestToken(args.token0),
1554
- token1: this.requestToken(args.token1),
2850
+ token0: rawTokenOf(args.token0),
2851
+ token1: rawTokenOf(args.token1),
1555
2852
  fee: `${args.fee}`,
1556
2853
  tickLower: `${args.tickLower}`,
1557
2854
  tickUpper: `${args.tickUpper}`,
@@ -1573,30 +2870,54 @@ var Positions = class {
1573
2870
  * ```
1574
2871
  */
1575
2872
  async addLiquidityByTicks(args) {
2873
+ return this.submitAddLiquidity(await this.buildAddLiquidity(args));
2874
+ }
2875
+ /**
2876
+ * Builds the AddLiquidity DTO that {@link addLiquidityByTicks} would sign, without signing or
2877
+ * sending it: to show in an approval popup, or to sign elsewhere. Submit exactly this DTO with
2878
+ * {@link submitAddLiquidity}. The pure counterpart, with canonical inputs and no resolution,
2879
+ * is `buildAddLiquidityDto`.
2880
+ *
2881
+ * @example
2882
+ * ```typescript
2883
+ * const dto = await gSwap.positions.buildAddLiquidity({
2884
+ * token0: 'GALA', token1: 'GUSDC', fee: 3000,
2885
+ * tickLower: -19200, tickUpper: 12000, amount: '100', amountIsToken0: true,
2886
+ * });
2887
+ * ```
2888
+ */
2889
+ async buildAddLiquidity(args) {
1576
2890
  validateFee(args.fee);
1577
2891
  const pair = await this.symbols.orderPair(args.token0, args.token1);
1578
2892
  const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
1579
2893
  assertTickRange(ticks.tickLower, ticks.tickUpper, args.fee);
1580
2894
  validateNumericAmount(args.amount, "amount");
1581
- const amount = new BigNumber(args.amount).toFixed();
1582
- const dto = {
2895
+ const base = {
1583
2896
  token0: pair.token0.symbol,
1584
2897
  token1: pair.token1.symbol,
1585
2898
  fee: args.fee,
1586
2899
  tickLower: ticks.tickLower,
1587
2900
  tickUpper: ticks.tickUpper,
1588
- uniqueKey: this.uniqueKey(),
1589
- ...pair.flipped !== args.amountIsToken0 ? { depositQuantityToken0: amount } : { depositQuantityToken1: amount }
2901
+ uniqueKey: this.uniqueKeyFor(args.uniqueKey)
1590
2902
  };
2903
+ return pair.flipped !== args.amountIsToken0 ? buildAddLiquidityDto({
2904
+ ...base,
2905
+ depositQuantityToken0: args.amount
2906
+ }) : buildAddLiquidityDto({
2907
+ ...base,
2908
+ depositQuantityToken1: args.amount
2909
+ });
2910
+ }
2911
+ /**
2912
+ * Signs and submits a DTO built by {@link buildAddLiquidity} or `buildAddLiquidityDto`; it must
2913
+ * be exactly what the builder produces. The returned transaction confirms by proving the
2914
+ * position's liquidity grew.
2915
+ */
2916
+ async submitAddLiquidity(dto) {
2917
+ assertCanonicalV2Dto("AddLiquidity", dto);
1591
2918
  return this.submit("AddLiquidity", dto, {
1592
2919
  kind: "add",
1593
- identity: {
1594
- token0: pair.token0.symbol,
1595
- token1: pair.token1.symbol,
1596
- fee: args.fee,
1597
- tickLower: ticks.tickLower,
1598
- tickUpper: ticks.tickUpper
1599
- }
2920
+ identity: identityOf(dto)
1600
2921
  });
1601
2922
  }
1602
2923
  /**
@@ -1631,7 +2952,8 @@ var Positions = class {
1631
2952
  tickLower,
1632
2953
  tickUpper,
1633
2954
  amount: args.amount,
1634
- amountIsToken0: args.amountIsToken0
2955
+ amountIsToken0: args.amountIsToken0,
2956
+ uniqueKey: args.uniqueKey
1635
2957
  });
1636
2958
  }
1637
2959
  /**
@@ -1649,6 +2971,14 @@ var Positions = class {
1649
2971
  * ```
1650
2972
  */
1651
2973
  async removeLiquidity(args) {
2974
+ return this.submitRemoveLiquidity(await this.buildRemoveLiquidity(args));
2975
+ }
2976
+ /**
2977
+ * Builds the RemoveLiquidity DTO that {@link removeLiquidity} would sign, without signing or
2978
+ * sending it. Submit exactly this DTO with {@link submitRemoveLiquidity}. The pure counterpart
2979
+ * is `buildRemoveLiquidityDto`.
2980
+ */
2981
+ async buildRemoveLiquidity(args) {
1652
2982
  validateFee(args.fee);
1653
2983
  const pair = await this.symbols.orderPair(args.token0, args.token1);
1654
2984
  const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
@@ -1656,26 +2986,29 @@ var Positions = class {
1656
2986
  if (args.amount0 !== void 0 && args.amount1 !== void 0) throw new GSwapSDKError("Provide at most one of amount0 or amount1.", "VALIDATION_ERROR");
1657
2987
  if (args.amount0 !== void 0) validateNumericAmount(args.amount0, "amount0");
1658
2988
  if (args.amount1 !== void 0) validateNumericAmount(args.amount1, "amount1");
1659
- const withdrawal = args.amount0 !== void 0 ? pair.flipped ? { withdrawalQuantityToken1: new BigNumber(args.amount0).toFixed() } : { withdrawalQuantityToken0: new BigNumber(args.amount0).toFixed() } : args.amount1 !== void 0 ? pair.flipped ? { withdrawalQuantityToken0: new BigNumber(args.amount1).toFixed() } : { withdrawalQuantityToken1: new BigNumber(args.amount1).toFixed() } : {};
1660
- const dto = {
2989
+ const [canonical0, canonical1] = pair.flipped ? [args.amount1, args.amount0] : [args.amount0, args.amount1];
2990
+ return buildRemoveLiquidityDto({
1661
2991
  token0: pair.token0.symbol,
1662
2992
  token1: pair.token1.symbol,
1663
2993
  fee: args.fee,
1664
2994
  tickLower: ticks.tickLower,
1665
2995
  tickUpper: ticks.tickUpper,
1666
- uniqueKey: this.uniqueKey(),
1667
- ...withdrawal
1668
- };
1669
- const isFullClose = args.amount0 === void 0 && args.amount1 === void 0;
2996
+ uniqueKey: this.uniqueKeyFor(args.uniqueKey),
2997
+ withdrawalQuantityToken0: canonical0,
2998
+ withdrawalQuantityToken1: canonical1
2999
+ });
3000
+ }
3001
+ /**
3002
+ * Signs and submits a DTO built by {@link buildRemoveLiquidity} or `buildRemoveLiquidityDto`;
3003
+ * it must be exactly what the builder produces. A DTO with no withdrawal quantity closes the
3004
+ * position, and the returned transaction then confirms by proving the position is gone.
3005
+ */
3006
+ async submitRemoveLiquidity(dto) {
3007
+ assertCanonicalV2Dto("RemoveLiquidity", dto);
3008
+ const isFullClose = dto.withdrawalQuantityToken0 === void 0 && dto.withdrawalQuantityToken1 === void 0;
1670
3009
  return this.submit("RemoveLiquidity", dto, {
1671
3010
  kind: isFullClose ? "removeFull" : "removePartial",
1672
- identity: {
1673
- token0: pair.token0.symbol,
1674
- token1: pair.token1.symbol,
1675
- fee: args.fee,
1676
- tickLower: ticks.tickLower,
1677
- tickUpper: ticks.tickUpper
1678
- }
3011
+ identity: identityOf(dto)
1679
3012
  });
1680
3013
  }
1681
3014
  /**
@@ -1692,27 +3025,36 @@ var Positions = class {
1692
3025
  * ```
1693
3026
  */
1694
3027
  async collectPositionFees(args) {
3028
+ return this.submitCollectPositionFees(await this.buildCollectPositionFees(args));
3029
+ }
3030
+ /**
3031
+ * Builds the CollectPositionFees DTO that {@link collectPositionFees} would sign, without
3032
+ * signing or sending it. Submit exactly this DTO with {@link submitCollectPositionFees}. The
3033
+ * pure counterpart is `buildCollectPositionFeesDto`.
3034
+ */
3035
+ async buildCollectPositionFees(args) {
1695
3036
  validateFee(args.fee);
1696
3037
  const pair = await this.symbols.orderPair(args.token0, args.token1);
1697
3038
  const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
1698
3039
  assertTickRange(ticks.tickLower, ticks.tickUpper, args.fee);
1699
- const dto = {
3040
+ return buildCollectPositionFeesDto({
1700
3041
  token0: pair.token0.symbol,
1701
3042
  token1: pair.token1.symbol,
1702
3043
  fee: args.fee,
1703
3044
  tickLower: ticks.tickLower,
1704
3045
  tickUpper: ticks.tickUpper,
1705
- uniqueKey: this.uniqueKey()
1706
- };
3046
+ uniqueKey: this.uniqueKeyFor(args.uniqueKey)
3047
+ });
3048
+ }
3049
+ /**
3050
+ * Signs and submits a DTO built by {@link buildCollectPositionFees} or
3051
+ * `buildCollectPositionFeesDto`; it must be exactly what the builder produces.
3052
+ */
3053
+ async submitCollectPositionFees(dto) {
3054
+ assertCanonicalV2Dto("CollectPositionFees", dto);
1707
3055
  return this.submit("CollectPositionFees", dto, {
1708
3056
  kind: "collect",
1709
- identity: {
1710
- token0: pair.token0.symbol,
1711
- token1: pair.token1.symbol,
1712
- fee: args.fee,
1713
- tickLower: ticks.tickLower,
1714
- tickUpper: ticks.tickUpper
1715
- }
3057
+ identity: identityOf(dto)
1716
3058
  });
1717
3059
  }
1718
3060
  /**
@@ -1737,7 +3079,15 @@ var Positions = class {
1737
3079
  * ```
1738
3080
  */
1739
3081
  async createPool(args) {
1740
- var _this$symbols$invalid, _this$symbols;
3082
+ return this.submitCreatePool(await this.buildCreatePool(args));
3083
+ }
3084
+ /**
3085
+ * Builds the CreatePool DTO that {@link createPool} would sign, without signing or sending it:
3086
+ * symbols resolved or proposed, the pair put in canonical order and the price inverted when it
3087
+ * was reversed. Submit exactly this DTO with {@link submitCreatePool}. The pure counterpart,
3088
+ * with canonical inputs, is `buildCreatePoolDto`.
3089
+ */
3090
+ async buildCreatePool(args) {
1741
3091
  const hasPrice = args.startingPrice !== void 0;
1742
3092
  if (hasPrice === (args.startingSqrtPrice !== void 0)) throw new GSwapSDKError("Provide exactly one of startingPrice or startingSqrtPrice.", "VALIDATION_ERROR");
1743
3093
  validateFee(args.fee);
@@ -1749,22 +3099,38 @@ var Positions = class {
1749
3099
  const sourcePrice = new BigNumber(sourcePriceInput);
1750
3100
  if (!sourcePrice.isFinite() || sourcePrice.isLessThanOrEqualTo(0)) throw new GSwapSDKError("Starting price must be finite and positive.", "VALIDATION_ERROR");
1751
3101
  const canonicalPrice = flipped ? new BigNumber(1).dividedBy(sourcePrice) : sourcePrice;
1752
- const dto = {
3102
+ const shared = {
1753
3103
  token0Key: token0.classKey,
1754
3104
  token1Key: token1.classKey,
1755
3105
  token0Symbol: token0.symbol,
1756
3106
  token1Symbol: token1.symbol,
1757
3107
  fee: args.fee,
1758
- uniqueKey: this.uniqueKey(),
1759
- ...hasPrice ? { startingPrice: canonicalPrice.toFixed() } : { startingSqrtPrice: canonicalPrice.toFixed() },
1760
- ...args.isPrivate === void 0 ? {} : { isPrivate: args.isPrivate },
1761
- ...args.privateAccess === void 0 ? {} : { privateAccess: args.privateAccess }
3108
+ uniqueKey: this.uniqueKeyFor(args.uniqueKey),
3109
+ name: args.name,
3110
+ isPrivate: args.isPrivate,
3111
+ privateAccess: args.privateAccess
1762
3112
  };
3113
+ return hasPrice ? buildCreatePoolDto({
3114
+ ...shared,
3115
+ startingPrice: canonicalPrice
3116
+ }) : buildCreatePoolDto({
3117
+ ...shared,
3118
+ startingSqrtPrice: canonicalPrice
3119
+ });
3120
+ }
3121
+ /**
3122
+ * Signs and submits a DTO built by {@link buildCreatePool} or `buildCreatePoolDto`; it must be
3123
+ * exactly what the builder produces. The returned transaction confirms by the pool existing, and
3124
+ * the cached symbol registry is dropped, since the pool may have registered new symbols.
3125
+ */
3126
+ async submitCreatePool(dto) {
3127
+ var _this$symbols$invalid, _this$symbols;
3128
+ assertCanonicalV2Dto("CreatePool", dto);
1763
3129
  const transaction = await this.submit("CreatePool", dto, {
1764
3130
  kind: "createPool",
1765
- token0: token0.symbol,
1766
- token1: token1.symbol,
1767
- fee: args.fee
3131
+ token0: dto.token0Symbol,
3132
+ token1: dto.token1Symbol,
3133
+ fee: dto.fee
1768
3134
  });
1769
3135
  (_this$symbols$invalid = (_this$symbols = this.symbols).invalidate) === null || _this$symbols$invalid === void 0 || _this$symbols$invalid.call(_this$symbols);
1770
3136
  return transaction;
@@ -1870,30 +3236,6 @@ var Positions = class {
1870
3236
  if (!(error instanceof GSwapSDKError) || error.details === void 0) return false;
1871
3237
  return error.details["status"] === 404;
1872
3238
  }
1873
- mapPosition(position) {
1874
- return {
1875
- token0Symbol: position.token0Symbol,
1876
- token1Symbol: position.token1Symbol,
1877
- ...position.token0CompositeKey === void 0 ? {} : { token0CompositeKey: position.token0CompositeKey },
1878
- ...position.token1CompositeKey === void 0 ? {} : { token1CompositeKey: position.token1CompositeKey },
1879
- fee: position.fee,
1880
- tickLower: position.tickLower,
1881
- tickUpper: position.tickUpper,
1882
- liquidity: position.liquidity,
1883
- amount0: position.amount0,
1884
- amount1: position.amount1,
1885
- inRange: position.inRange,
1886
- ...position.currentTick === void 0 ? {} : { currentTick: position.currentTick },
1887
- ...position.sqrtPrice === void 0 ? {} : { sqrtPrice: position.sqrtPrice },
1888
- pool: position.poolRef ?? position.pool ?? "",
1889
- owner: position.owner,
1890
- ...position.fees0 === void 0 ? {} : { fees0: position.fees0 },
1891
- ...position.fees1 === void 0 ? {} : { fees1: position.fees1 }
1892
- };
1893
- }
1894
- requestToken(ref) {
1895
- return typeof ref === "string" ? ref : compositeKeyOf(ref);
1896
- }
1897
3239
  canonicalTicks(tickLower, tickUpper, flipped) {
1898
3240
  return flipped ? {
1899
3241
  tickLower: -tickUpper,
@@ -1911,8 +3253,11 @@ var Positions = class {
1911
3253
  rejectNumber(value, parameterName) {
1912
3254
  if (typeof value === "number") throw new GSwapSDKError(`Invalid ${parameterName}: use a decimal string or BigNumber, not a JavaScript number`, "VALIDATION_ERROR");
1913
3255
  }
1914
- uniqueKey() {
1915
- return `gswap-sdk-${globalThis.crypto.randomUUID()}`;
3256
+ /** The caller's `uniqueKey` (validated), else a fresh one. */
3257
+ uniqueKeyFor(supplied) {
3258
+ if (supplied === void 0) return createUniqueKey(this.uniqueKeyPrefix);
3259
+ assertUniqueKey(supplied);
3260
+ return supplied;
1916
3261
  }
1917
3262
  async resolveCreateToken(ref, symbolOverride) {
1918
3263
  try {
@@ -1924,6 +3269,7 @@ var Positions = class {
1924
3269
  registeredSymbol: resolved.symbol
1925
3270
  });
1926
3271
  const suppliedClassKey = this.tryParseClassKey(ref);
3272
+ if (suppliedClassKey === void 0 && resolved.collection === "") throw new GSwapSDKError("createPool needs each token as a class key; a bare trading symbol does not carry one.", "VALIDATION_ERROR", { token: ref });
1927
3273
  return {
1928
3274
  symbol: resolved.symbol,
1929
3275
  classKey: suppliedClassKey ?? parseTokenClassKey({
@@ -1942,7 +3288,7 @@ var Positions = class {
1942
3288
  throw error;
1943
3289
  }
1944
3290
  if (symbolOverride === void 0 || symbolOverride === "") throw new GSwapSDKError("Token is not registered in the trading-symbol registry yet; pass token0Symbol/token1Symbol (the token class symbol) to propose one. A symbol cannot be derived from a class key.", "VALIDATION_ERROR", { token: ref });
1945
- if (!/^[A-Za-z0-9]+$/u.test(symbolOverride)) throw new GSwapSDKError("Invalid trading symbol: must be ASCII letters and digits only.", "VALIDATION_ERROR", { symbol: symbolOverride });
3291
+ if (!isProposableTradingSymbol(symbolOverride)) throw new GSwapSDKError("Invalid trading symbol: must be 1-32 ASCII letters and digits and not a reserved word (Token, Unit, none).", "VALIDATION_ERROR", { symbol: symbolOverride });
1946
3292
  return {
1947
3293
  symbol: symbolOverride,
1948
3294
  classKey
@@ -1957,27 +3303,94 @@ var Positions = class {
1957
3303
  }
1958
3304
  }
1959
3305
  };
3306
+ const REQUIRED_POSITION_STRINGS = [
3307
+ "token0Symbol",
3308
+ "token1Symbol",
3309
+ "owner",
3310
+ "liquidity",
3311
+ "amount0",
3312
+ "amount1"
3313
+ ];
3314
+ const REQUIRED_POSITION_NUMBERS = [
3315
+ "fee",
3316
+ "tickLower",
3317
+ "tickUpper"
3318
+ ];
3319
+ const OPTIONAL_POSITION_STRINGS = [
3320
+ "token0CompositeKey",
3321
+ "token1CompositeKey",
3322
+ "token0Image",
3323
+ "token1Image",
3324
+ "sqrtPrice",
3325
+ "fees0",
3326
+ "fees1"
3327
+ ];
3328
+ /**
3329
+ * Reads one position row of `/positions` or `/position`: the pair, owner, range, liquidity and
3330
+ * amounts must be usable; the composite keys, token images, current tick and price, and the
3331
+ * escrowed fees are carried when present. `pool` is the pool reference (`poolRef` wins over
3332
+ * `pool`; `''` when the row names neither).
3333
+ */
3334
+ function parsePosition(value) {
3335
+ const wire = asEntryRecord(value);
3336
+ if (wire === void 0) return entryRejected("the row is not an object");
3337
+ for (const key of REQUIRED_POSITION_STRINGS) if (typeof wire[key] !== "string") return entryRejected(`${key} is not a string`);
3338
+ for (const key of REQUIRED_POSITION_NUMBERS) {
3339
+ const number = wire[key];
3340
+ if (typeof number !== "number" || !Number.isFinite(number)) return entryRejected(`${key} is not a number`);
3341
+ }
3342
+ if (typeof wire["inRange"] !== "boolean") return entryRejected("inRange is not a boolean");
3343
+ for (const key of OPTIONAL_POSITION_STRINGS) if (wire[key] !== void 0 && typeof wire[key] !== "string") return entryRejected(`${key} is not a string`);
3344
+ const currentTick = wire["currentTick"];
3345
+ if (currentTick !== void 0 && (typeof currentTick !== "number" || !Number.isFinite(currentTick))) return entryRejected("currentTick is not a number");
3346
+ const pool = wire["poolRef"] ?? wire["pool"] ?? "";
3347
+ if (typeof pool !== "string") return entryRejected("pool is not a string");
3348
+ const optional = Object.fromEntries([...OPTIONAL_POSITION_STRINGS, "currentTick"].flatMap((key) => wire[key] === void 0 ? [] : [[key, wire[key]]]));
3349
+ return entryOk({
3350
+ token0Symbol: wire["token0Symbol"],
3351
+ token1Symbol: wire["token1Symbol"],
3352
+ fee: wire["fee"],
3353
+ tickLower: wire["tickLower"],
3354
+ tickUpper: wire["tickUpper"],
3355
+ liquidity: wire["liquidity"],
3356
+ amount0: wire["amount0"],
3357
+ amount1: wire["amount1"],
3358
+ inRange: wire["inRange"],
3359
+ pool,
3360
+ owner: wire["owner"],
3361
+ ...optional
3362
+ });
3363
+ }
3364
+ /** The position inside a `/position` answer, or `INVALID_CHAIN_RESPONSE` when it is not one. */
3365
+ function readPositionBody(data) {
3366
+ const parsed = parsePosition(data);
3367
+ if (parsed.ok) return parsed.value;
3368
+ throw new GSwapSDKError("Backend returned an invalid position.", "INVALID_CHAIN_RESPONSE", {
3369
+ data,
3370
+ reason: parsed.reason
3371
+ });
3372
+ }
1960
3373
 
1961
3374
  //#endregion
1962
3375
  //#region src/classes/quoting.ts
1963
3376
  /** Read-only quote service backed by the v2 offline quote engine. */
1964
3377
  var Quoting = class {
1965
- dexBackendBaseUrl;
1966
- http;
3378
+ gateway;
1967
3379
  symbols;
1968
3380
  /**
1969
3381
  * Creates a quote service.
1970
3382
  *
1971
3383
  * @example
1972
3384
  * ```ts
1973
- * const quoting = new Quoting('https://swap-backend.stage.defi.ovh.gala.com', http, symbols);
3385
+ * const quoting = new Quoting(gateway, symbols); // a ChainGateway, or any { dexBackendBaseUrl, httpRequestor }
1974
3386
  * ```
1975
3387
  */
1976
- constructor(dexBackendBaseUrl, http, symbols) {
1977
- this.dexBackendBaseUrl = dexBackendBaseUrl;
1978
- this.http = http;
3388
+ constructor(gateway, symbols) {
3389
+ this.gateway = gateway;
1979
3390
  this.symbols = symbols;
3391
+ this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4, gateway.wire);
1980
3392
  }
3393
+ http;
1981
3394
  /**
1982
3395
  * Quotes the output for a positive exact-input amount.
1983
3396
  *
@@ -1989,7 +3402,7 @@ var Quoting = class {
1989
3402
  */
1990
3403
  async quoteExactInput(tokenIn, tokenOut, amountIn, fee) {
1991
3404
  validateNumericAmount(amountIn, "amountIn");
1992
- return this.quote(tokenIn, tokenOut, { amountIn: toDecimalString$1(amountIn) }, fee);
3405
+ return this.quote(tokenIn, tokenOut, { amountIn: toDecimalString(amountIn) }, fee);
1993
3406
  }
1994
3407
  /**
1995
3408
  * Quotes the input for a positive exact-output amount.
@@ -2002,18 +3415,18 @@ var Quoting = class {
2002
3415
  */
2003
3416
  async quoteExactOutput(tokenIn, tokenOut, amountOut, fee) {
2004
3417
  validateNumericAmount(amountOut, "amountOut");
2005
- return this.quote(tokenIn, tokenOut, { amountOut: toDecimalString$1(amountOut) }, fee);
3418
+ return this.quote(tokenIn, tokenOut, { amountOut: toDecimalString(amountOut) }, fee);
2006
3419
  }
2007
3420
  async quote(tokenIn, tokenOut, amount, fee) {
2008
3421
  validateOptionalFee(fee);
2009
3422
  const params = {
2010
- tokenIn: await resolveSymbol$1(this.symbols, tokenIn),
2011
- tokenOut: await resolveSymbol$1(this.symbols, tokenOut),
3423
+ tokenIn: await queryTokenOf(this.symbols, tokenIn),
3424
+ tokenOut: await queryTokenOf(this.symbols, tokenOut),
2012
3425
  ...amount
2013
3426
  };
2014
3427
  if (fee !== void 0) params["fee"] = String(fee);
2015
3428
  try {
2016
- return mapQuote((await this.http.sendGetRequest(this.dexBackendBaseUrl, "/v2/trade", "/quote", params)).data);
3429
+ return mapQuote((await this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/quote", params)).data);
2017
3430
  } catch (error) {
2018
3431
  throw mapQuoteError(error, tokenIn, tokenOut, fee);
2019
3432
  }
@@ -2075,12 +3488,19 @@ function optionalPositiveDecimal(value) {
2075
3488
  return false;
2076
3489
  }
2077
3490
  }
3491
+ /**
3492
+ * The ways the backend says a pair has no market at all: a token the DEX contract never
3493
+ * registered ("has no trading symbol on the new DEX contract"), no pool at ANY fee tier ("No
3494
+ * pools found with sufficient liquidity"), or a missing pool ("Pool does not exist on the new
3495
+ * DEX contract"). A pool that exists but is too thin says "Insufficient liquidity: ..." instead.
3496
+ */
3497
+ const NO_MARKET_MESSAGE = /no pool|no pools|pool does not exist|has no trading symbol on the new DEX contract/iu;
2078
3498
  function mapQuoteError(error, tokenIn, tokenOut, fee) {
2079
3499
  if (error instanceof GSwapSDKError) {
2080
3500
  const status = readStatus(error.details);
2081
3501
  const message = readMessage(error.details) ?? error.message;
2082
- if (status === 404 || /no pool|no pools|pool does not exist/iu.test(message)) return GSwapSDKError.noPoolAvailableError(tokenIn, tokenOut, fee);
2083
- if (status === 400 && /insufficient liquidity/iu.test(message)) return GSwapSDKError.insufficientLiquidityError(tokenIn, tokenOut, fee);
3502
+ if (status === 404 || NO_MARKET_MESSAGE.test(message)) return GSwapSDKError.noPoolAvailableError(tokenIn, tokenOut, fee, error);
3503
+ if (status === 400 && /insufficient liquidity/iu.test(message)) return GSwapSDKError.insufficientLiquidityError(tokenIn, tokenOut, fee, error);
2084
3504
  return error;
2085
3505
  }
2086
3506
  return new GSwapSDKError("Quote request failed.", "HTTP_ERROR", {
@@ -2105,107 +3525,112 @@ function readMessage(details) {
2105
3525
  function validateOptionalFee(fee) {
2106
3526
  if (fee !== void 0 && !ALL_FEE_TIERS.some((tier) => Number(tier) === Number(fee))) throw new GSwapSDKError(`Invalid fee tier: ${fee}`, "VALIDATION_ERROR", { fee });
2107
3527
  }
2108
- function toDecimalString$1(amount) {
3528
+ function toDecimalString(amount) {
2109
3529
  return new BigNumber(amount).toFixed();
2110
3530
  }
2111
- async function resolveSymbol$1(symbols, token) {
2112
- const resolved = await symbols.resolve(token);
2113
- if (resolved.symbol.length > 0) return resolved.symbol;
2114
- throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token });
2115
- }
2116
3531
 
2117
3532
  //#endregion
2118
- //#region src/classes/symbols.ts
2119
- const SYMBOL_CACHE_TTL_MS = 6e4;
2120
- /** Symbol registry client for the current GalaChainDex contract. */
2121
- var Symbols = class {
2122
- gateway;
2123
- cachedSymbols;
2124
- cachedAt = 0;
2125
- refreshInFlight;
2126
- http;
2127
- /** Create a symbol service backed by the swap backend. */
2128
- constructor(gateway) {
2129
- this.gateway = gateway;
2130
- this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs);
2131
- }
2132
- /** List all registered GalaChainDex trading symbols, cached for 60 seconds. */
2133
- async list() {
2134
- if (this.cachedSymbols !== void 0 && Date.now() - this.cachedAt < SYMBOL_CACHE_TTL_MS) return this.cachedSymbols;
2135
- if (this.refreshInFlight !== void 0) return this.refreshInFlight;
2136
- this.refreshInFlight = this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/symbols").then((response) => parseSymbols(response.data)).then((symbols) => {
2137
- this.cachedSymbols = symbols;
2138
- this.cachedAt = Date.now();
2139
- return symbols;
2140
- }).finally(() => {
2141
- this.refreshInFlight = void 0;
2142
- });
2143
- return this.refreshInFlight;
2144
- }
2145
- /** Invalidate the cached symbol registry after a write changes registration state.
2146
- *
2147
- * @example
2148
- * ```ts
2149
- * gSwap.symbols.invalidate();
2150
- * ```
2151
- */
2152
- invalidate() {
2153
- this.cachedSymbols = void 0;
2154
- this.cachedAt = 0;
2155
- }
2156
- /** Force one registry refresh, bypassing the cache.
2157
- *
2158
- * @example
2159
- * ```ts
2160
- * const symbols = await gSwap.symbols.refresh();
2161
- * ```
2162
- */
2163
- async refresh() {
2164
- this.invalidate();
2165
- this.refreshInFlight = void 0;
2166
- return this.list();
2167
- }
2168
- /** Resolve a symbol or token class key to its registered symbol metadata. */
2169
- async resolve(ref) {
2170
- let symbols = await this.list();
2171
- const composite = typeof ref === "string" ? isCompositeKey(ref) ? compositeKeyOf(parseTokenClassKey(ref)) : void 0 : compositeKeyOf(ref);
2172
- const resolved = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
2173
- if (resolved === void 0) {
2174
- symbols = await this.refresh();
2175
- const refreshed = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
2176
- if (refreshed === void 0) throw GSwapSDKError.unknownTokenError(ref);
2177
- return refreshed;
3533
+ //#region src/classes/shadow_submit.ts
3534
+ /** Maps an HTTP status to a failure kind: 503 retryable, 400 disabled, anything else unknown. */
3535
+ function classifyShadowSubmitStatus(status) {
3536
+ if (status === 503) return "unavailable";
3537
+ if (status === 400) return "disabled";
3538
+ return "unknown";
3539
+ }
3540
+ /**
3541
+ * Prefers the body `code` (stable across status remaps) and falls back to the HTTP status.
3542
+ * `SHADOW_UNAVAILABLE` is the only retryable case; `SHADOW_RELAY_REJECTED` is a terminal gateway
3543
+ * rejection; the two other 400 codes and any unknown 400 map to "disabled".
3544
+ */
3545
+ function classifyShadowSubmitError(status, code) {
3546
+ if (code === "SHADOW_UNAVAILABLE") return "unavailable";
3547
+ if (code === "SHADOW_RELAY_REJECTED") return "rejected";
3548
+ if (code === "SHADOW_DISABLED" || code === "SHADOW_UNSUPPORTED_TYPE") return "disabled";
3549
+ return classifyShadowSubmitStatus(status);
3550
+ }
3551
+ /**
3552
+ * Normalizes a shadow write's raw answer.
3553
+ *
3554
+ * A 2xx envelope with `error === false` is a success when it is a queued RequestTrade that
3555
+ * acknowledges the EXACT signed `uniqueKey`, or a synchronous (`mode: 'sync'`) liquidity write.
3556
+ * Everything else is classified as a failure from the status and the body's `code`.
3557
+ *
3558
+ * @throws GSwapSDKError INVALID_GATEWAY_RESPONSE when a RequestTrade success does not queue the
3559
+ * signed `uniqueKey` (a synchronous or mismatched answer must never be accepted as a swap).
3560
+ */
3561
+ function readShadowSubmitOutcome(method, signedBody, result) {
3562
+ const { httpStatus, body } = result;
3563
+ const envelope = asRecord(body);
3564
+ if (httpStatus >= 200 && httpStatus < 300 && (envelope === null || envelope === void 0 ? void 0 : envelope["error"]) === false) {
3565
+ const data = asRecord(envelope["data"]);
3566
+ const rawTransactionId = typeof (data === null || data === void 0 ? void 0 : data["transactionId"]) === "string" ? data["transactionId"] : void 0;
3567
+ if (method === "RequestTrade") {
3568
+ const requestedKey = signedBody["uniqueKey"];
3569
+ if ((data === null || data === void 0 ? void 0 : data["mode"]) !== "queued" || typeof requestedKey !== "string" || data["uniqueKey"] !== requestedKey) throw new GSwapSDKError(`The swap backend did not queue the request ${typeof requestedKey === "string" ? requestedKey : ""}.`, "INVALID_GATEWAY_RESPONSE", {
3570
+ httpStatus,
3571
+ body
3572
+ });
3573
+ return {
3574
+ ok: true,
3575
+ bundleId: rawTransactionId ?? "",
3576
+ queued: {
3577
+ uniqueKey: requestedKey,
3578
+ requestTransactionId: rawTransactionId ?? null
3579
+ }
3580
+ };
2178
3581
  }
2179
- return resolved;
2180
- }
2181
- /** Resolve and put two token references in canonical trading-symbol order. */
2182
- async orderPair(a, b) {
2183
- const [resolvedA, resolvedB] = await Promise.all([this.resolve(a), this.resolve(b)]);
2184
- const [token0Symbol, token1Symbol] = orderSymbols(resolvedA.symbol, resolvedB.symbol);
2185
- return {
2186
- token0: token0Symbol === resolvedA.symbol ? resolvedA : resolvedB,
2187
- token1: token1Symbol === resolvedB.symbol ? resolvedB : resolvedA,
2188
- flipped: token0Symbol !== resolvedA.symbol
3582
+ if ((data === null || data === void 0 ? void 0 : data["mode"]) === "sync") return {
3583
+ ok: true,
3584
+ bundleId: rawTransactionId ?? ""
2189
3585
  };
2190
3586
  }
2191
- };
2192
- function parseSymbols(value) {
2193
- if (!Array.isArray(value) || !value.every(isTradingSymbol)) throw new GSwapSDKError("Backend returned an invalid symbol list.", "INVALID_CHAIN_RESPONSE", { data: value });
2194
- return value;
2195
- }
2196
- function isTradingSymbol(value) {
2197
- if (typeof value !== "object" || value === null) return false;
2198
- const entry = value;
2199
- return typeof entry["symbol"] === "string" && typeof entry["collection"] === "string" && typeof entry["category"] === "string" && typeof entry["type"] === "string" && typeof entry["additionalKey"] === "string" && typeof entry["decimals"] === "number" && Number.isInteger(entry["decimals"]) && entry["decimals"] >= 0;
3587
+ const kind = classifyShadowSubmitError(httpStatus, getStringProperty(body, "code"));
3588
+ if (kind === "rejected") return {
3589
+ ok: false,
3590
+ kind: "rejected",
3591
+ retryable: false,
3592
+ message: getStringProperty(body, "message") ?? ""
3593
+ };
3594
+ return kind === "unavailable" ? {
3595
+ ok: false,
3596
+ kind,
3597
+ retryable: true
3598
+ } : {
3599
+ ok: false,
3600
+ kind,
3601
+ retryable: false
3602
+ };
2200
3603
  }
2201
- function isCompositeKey(ref) {
2202
- return ref.includes("|") || ref.includes("$");
3604
+ function asRecord(value) {
3605
+ return typeof value === "object" && value !== null ? value : void 0;
2203
3606
  }
2204
3607
 
2205
3608
  //#endregion
2206
3609
  //#region src/classes/swaps.ts
2207
- /** The swap backend's apply-results key constraint (`apply-results.dto.ts`). */
2208
- const UNIQUE_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
3610
+ /**
3611
+ * The route of a quote: its fee tier and pool, with `tokenIn` / `tokenOut` taken from
3612
+ * `tokenInIsToken0`, nothing re-derived. Use it to swap in exactly the pool a quote priced:
3613
+ *
3614
+ * @example
3615
+ * ```ts
3616
+ * const route = swapRouteFromQuote(quote);
3617
+ * await swaps.swap(route.tokenIn, route.tokenOut, route.fee, { exactIn: '100' }, { pool: route.pool });
3618
+ * ```
3619
+ */
3620
+ function swapRouteFromQuote(quote) {
3621
+ validateFee(quote.fee);
3622
+ assertCanonicalPair(quote.token0Symbol, quote.token1Symbol);
3623
+ const [tokenIn, tokenOut] = quote.tokenInIsToken0 ? [quote.token0Symbol, quote.token1Symbol] : [quote.token1Symbol, quote.token0Symbol];
3624
+ return {
3625
+ tokenIn,
3626
+ tokenOut,
3627
+ fee: quote.fee,
3628
+ pool: {
3629
+ token0: quote.token0Symbol,
3630
+ token1: quote.token1Symbol
3631
+ }
3632
+ };
3633
+ }
2209
3634
  /** Executes queued current-contract RequestTrade operations through the Chain Gateway. */
2210
3635
  var Swaps = class {
2211
3636
  gateway;
@@ -2220,12 +3645,14 @@ var Swaps = class {
2220
3645
  * const swaps = new Swaps(gateway, symbols, signer, 'client|alice');
2221
3646
  * ```
2222
3647
  */
2223
- constructor(gateway, symbols, signer, walletAddress) {
3648
+ constructor(gateway, symbols, signer, walletAddress, options = {}) {
2224
3649
  this.gateway = gateway;
2225
3650
  this.symbols = symbols;
2226
3651
  this.signer = signer;
2227
3652
  this.walletAddress = walletAddress;
3653
+ this.uniqueKeyPrefix = resolveUniqueKeyPrefix(options.uniqueKeyPrefix);
2228
3654
  }
3655
+ uniqueKeyPrefix;
2229
3656
  /**
2230
3657
  * Signs and submits an exact-input or exact-output RequestTrade DTO. The returned transaction
2231
3658
  * is QUEUED; call `await tx.confirm()` to obtain the executed trade or chain rejection.
@@ -2238,13 +3665,97 @@ var Swaps = class {
2238
3665
  * });
2239
3666
  * ```
2240
3667
  */
2241
- async swap(tokenIn, tokenOut, fee, amount) {
3668
+ async swap(tokenIn, tokenOut, fee, amount, options) {
3669
+ return this.submitSwap(await this.buildSwap(tokenIn, tokenOut, fee, amount, options));
3670
+ }
3671
+ /**
3672
+ * Signs and submits a SHADOW (dark) swap: the same RequestTrade as {@link swap}, relayed with
3673
+ * `X-Shadow: true` so defi-backend marks it private before it reaches the contract. Requires a
3674
+ * `walletAddress`. Resolves the normalized outcome instead of throwing on a gateway refusal:
3675
+ * `ok: false` carries `unavailable` (retryable), `disabled`, `rejected` (with the gateway's
3676
+ * message) or `unknown`. A success carries `queued`, to be confirmed with
3677
+ * `confirmSwap(queued.uniqueKey)`; never fall back to a public swap on failure.
3678
+ *
3679
+ * @example
3680
+ * ```ts
3681
+ * const outcome = await swaps.swapShadow('GALA', 'GUSDC', 3000, { exactIn: '100' });
3682
+ * if (outcome.ok && outcome.queued) await swaps.confirmSwap(outcome.queued.uniqueKey);
3683
+ * ```
3684
+ */
3685
+ async swapShadow(tokenIn, tokenOut, fee, amount, options) {
3686
+ return this.submitSwapShadow(await this.buildSwap(tokenIn, tokenOut, fee, amount, options));
3687
+ }
3688
+ /**
3689
+ * Builds the RequestTrade DTO that {@link swap} would sign, without signing or sending it: to
3690
+ * show in an approval popup, to remember its `uniqueKey`, or to sign elsewhere. Submit exactly
3691
+ * this DTO with {@link submitSwap}. The pure counterpart, with canonical inputs and no
3692
+ * resolution, is `buildRequestTradeDto`.
3693
+ *
3694
+ * @example
3695
+ * ```ts
3696
+ * const dto = await swaps.buildSwap('GALA', 'GUSDC', 3000, { exactIn: '100' });
3697
+ * showApproval(dto);
3698
+ * const submitted = await swaps.submitSwap(dto);
3699
+ * ```
3700
+ */
3701
+ async buildSwap(tokenIn, tokenOut, fee, amount, options) {
2242
3702
  validateFee(fee);
2243
- const dto = await this.buildRequestTradeDto(tokenIn, tokenOut, fee, amount);
3703
+ const uniqueKey = (options === null || options === void 0 ? void 0 : options.uniqueKey) ?? createUniqueKey(this.uniqueKeyPrefix);
3704
+ assertUniqueKey(uniqueKey);
3705
+ const tokenInSymbol = await tradingSymbolOf(this.symbols, tokenIn);
3706
+ const tokenOutSymbol = await tradingSymbolOf(this.symbols, tokenOut);
3707
+ const ordered = this.orientPool(tokenInSymbol, tokenOutSymbol, options === null || options === void 0 ? void 0 : options.pool);
3708
+ return buildRequestTradeDto({
3709
+ token0: ordered.token0,
3710
+ token1: ordered.token1,
3711
+ fee,
3712
+ tokenInIsToken0: tokenInSymbol === ordered.token0,
3713
+ uniqueKey,
3714
+ ...amount
3715
+ });
3716
+ }
3717
+ /**
3718
+ * Signs and submits a DTO built by {@link buildSwap} or `buildRequestTradeDto`. The DTO must be
3719
+ * exactly what the builder produces (no signature, canonical decimals, builder key order), so
3720
+ * what an app showed is what gets signed.
3721
+ *
3722
+ * @example
3723
+ * ```ts
3724
+ * const submitted = await swaps.submitSwap(dto);
3725
+ * const applied = await submitted.confirm();
3726
+ * ```
3727
+ */
3728
+ async submitSwap(dto) {
3729
+ const signed = await this.signRequestTrade(dto);
3730
+ return this.gateway.submit("RequestTrade", signed, this.submitOptions());
3731
+ }
3732
+ /** Signs and shadow-submits a DTO built by {@link buildSwap}; see {@link swapShadow}. */
3733
+ async submitSwapShadow(dto) {
3734
+ const signed = await this.signRequestTrade(dto);
3735
+ const result = await this.gateway.submitShadow("RequestTrade", signed, this.submitOptions());
3736
+ return readShadowSubmitOutcome("RequestTrade", signed, result);
3737
+ }
3738
+ submitOptions() {
3739
+ return this.walletAddress === void 0 ? {} : { walletAddress: this.walletAddress };
3740
+ }
3741
+ async signRequestTrade(dto) {
3742
+ assertCanonicalV2Dto("RequestTrade", dto);
2244
3743
  if (this.signer === void 0) throw GSwapSDKError.noSignerError();
2245
- const signedDto = await this.signer.signObject("RequestTrade", dto);
2246
- const submitOptions = this.walletAddress === void 0 ? {} : { walletAddress: this.walletAddress };
2247
- return this.gateway.submit("RequestTrade", signedDto, submitOptions);
3744
+ return this.signer.signObject("RequestTrade", dto);
3745
+ }
3746
+ /** The pool's two symbols in contract order: the quote's own when pinned, else string order. */
3747
+ orientPool(tokenInSymbol, tokenOutSymbol, pool) {
3748
+ if (pool === void 0) return readOrderedSymbols(orderSymbols(tokenInSymbol, tokenOutSymbol));
3749
+ if (!(tokenInSymbol === pool.token0 && tokenOutSymbol === pool.token1 || tokenInSymbol === pool.token1 && tokenOutSymbol === pool.token0)) throw new GSwapSDKError("tokenIn and tokenOut must be the two tokens of the pinned pool.", "VALIDATION_ERROR", {
3750
+ type: "POOL_MISMATCH",
3751
+ tokenIn: tokenInSymbol,
3752
+ tokenOut: tokenOutSymbol,
3753
+ pool
3754
+ });
3755
+ return {
3756
+ token0: pool.token0,
3757
+ token1: pool.token1
3758
+ };
2248
3759
  }
2249
3760
  /**
2250
3761
  * Confirm a swap by its durable `uniqueKey` without resubmitting it. This is the recovery
@@ -2256,11 +3767,11 @@ var Swaps = class {
2256
3767
  *
2257
3768
  * @example
2258
3769
  * ```ts
2259
- * const applied = await swaps.confirmSwap('gswap-sdk-2f1c…');
3770
+ * const applied = await swaps.confirmSwap('galaswap-operation-2f1c…');
2260
3771
  * ```
2261
3772
  */
2262
3773
  async confirmSwap(uniqueKey, options) {
2263
- if (!UNIQUE_KEY_PATTERN.test(uniqueKey)) throw new GSwapSDKError("uniqueKey must be 1-128 characters of A-Z, a-z, 0-9, \".\", \"_\", \":\" or \"-\".", "VALIDATION_ERROR", { uniqueKey });
3774
+ assertUniqueKey(uniqueKey);
2264
3775
  return new SubmittedTransaction({
2265
3776
  method: "RequestTrade",
2266
3777
  uniqueKey,
@@ -2269,53 +3780,14 @@ var Swaps = class {
2269
3780
  result: void 0,
2270
3781
  dexBackendBaseUrl: this.gateway.dexBackendBaseUrl,
2271
3782
  httpRequestor: this.gateway.httpRequestor,
2272
- requestTimeoutMs: this.gateway.requestTimeoutMs
3783
+ requestTimeoutMs: this.gateway.requestTimeoutMs,
3784
+ wire: this.gateway.wire
2273
3785
  }).confirm({
2274
3786
  initialDelayMs: 0,
2275
3787
  ...options
2276
3788
  });
2277
3789
  }
2278
- async buildRequestTradeDto(tokenIn, tokenOut, fee, amount) {
2279
- const tokenInSymbol = await resolveSymbol(this.symbols, tokenIn);
2280
- const tokenOutSymbol = await resolveSymbol(this.symbols, tokenOut);
2281
- const ordered = readOrderedSymbols(orderSymbols(tokenInSymbol, tokenOutSymbol));
2282
- const tokenInIsToken0 = tokenInSymbol === ordered.token0;
2283
- const dto = {
2284
- token0: ordered.token0,
2285
- token1: ordered.token1,
2286
- fee,
2287
- uniqueKey: `gswap-sdk-${globalThis.crypto.randomUUID()}`
2288
- };
2289
- if ("exactIn" in amount) {
2290
- const exactIn = amount.exactIn;
2291
- if (exactIn === void 0) throw new GSwapSDKError("exactIn is required for an exact-input swap.", "VALIDATION_ERROR");
2292
- validateNumericAmount(exactIn, "exactIn");
2293
- dto[tokenInIsToken0 ? "sell0Qty" : "sell1Qty"] = toDecimalString(exactIn);
2294
- if (amount.amountOutMinimum !== void 0) {
2295
- validateNumericAmount(amount.amountOutMinimum, "amountOutMinimum", true);
2296
- dto["amountOutMinimum"] = toDecimalString(amount.amountOutMinimum);
2297
- }
2298
- } else {
2299
- const exactOut = amount.exactOut;
2300
- if (exactOut === void 0) throw new GSwapSDKError("exactOut is required for an exact-output swap.", "VALIDATION_ERROR");
2301
- validateNumericAmount(exactOut, "exactOut");
2302
- dto[tokenInIsToken0 ? "buy1Qty" : "buy0Qty"] = toDecimalString(exactOut);
2303
- if (amount.amountInMaximum !== void 0) {
2304
- validateNumericAmount(amount.amountInMaximum, "amountInMaximum", true);
2305
- dto["amountInMaximum"] = toDecimalString(amount.amountInMaximum);
2306
- }
2307
- }
2308
- return dto;
2309
- }
2310
3790
  };
2311
- function toDecimalString(amount) {
2312
- return new BigNumber(amount).toFixed();
2313
- }
2314
- async function resolveSymbol(symbols, token) {
2315
- const resolved = await symbols.resolve(token);
2316
- if (resolved.symbol.length > 0) return resolved.symbol;
2317
- throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token });
2318
- }
2319
3791
 
2320
3792
  //#endregion
2321
3793
  //#region src/classes/gswap.ts
@@ -2332,8 +3804,11 @@ var GSwap = class {
2332
3804
  positions;
2333
3805
  swaps;
2334
3806
  assets;
3807
+ /** Network-fee quotes (DryRun) signed with this client's signer. */
3808
+ fees;
2335
3809
  dexBackendWsUrl;
2336
3810
  webSocketImpl;
3811
+ streamApiKey;
2337
3812
  streamClient;
2338
3813
  /**
2339
3814
  * Create an SDK client for prod or stage.
@@ -2350,21 +3825,30 @@ var GSwap = class {
2350
3825
  this.chainCallTimeoutMs = (options === null || options === void 0 ? void 0 : options.chainCallTimeoutMs) ?? 3e4;
2351
3826
  this.signer = options === null || options === void 0 ? void 0 : options.signer;
2352
3827
  this.walletAddress = options === null || options === void 0 ? void 0 : options.walletAddress;
2353
- const httpClient = new HttpClient(options === null || options === void 0 ? void 0 : options.httpRequestor, this.chainCallTimeoutMs);
3828
+ const httpClient = new HttpClient(options === null || options === void 0 ? void 0 : options.httpRequestor, this.chainCallTimeoutMs, options === null || options === void 0 ? void 0 : options.wire);
2354
3829
  this.gateway = new ChainGateway({
2355
3830
  dexBackendBaseUrl: this.dexBackendBaseUrl,
2356
3831
  chainCallTimeoutMs: this.chainCallTimeoutMs,
3832
+ ...(options === null || options === void 0 ? void 0 : options.wire) === void 0 ? {} : { wire: options.wire },
2357
3833
  ...(options === null || options === void 0 ? void 0 : options.httpRequestor) === void 0 ? {} : { httpRequestor: options.httpRequestor },
2358
3834
  ...this.walletAddress === void 0 ? {} : { walletAddress: this.walletAddress }
2359
3835
  });
2360
- this.symbols = new Symbols(this.gateway);
2361
- this.quoting = new Quoting(this.dexBackendBaseUrl, httpClient, this.symbols);
2362
- this.pools = new Pools(this.gateway, this.symbols);
2363
- this.positions = new Positions(this.gateway, this.symbols, this.signer, this.walletAddress);
2364
- this.swaps = new Swaps(this.gateway, this.symbols, this.signer, this.walletAddress);
3836
+ const lenient = { onInvalidEntry: options === null || options === void 0 ? void 0 : options.onInvalidEntry };
3837
+ this.symbols = new Symbols(this.gateway, lenient);
3838
+ const resolver = (options === null || options === void 0 ? void 0 : options.symbolResolution) === "canonical" ? new CanonicalSymbols() : this.symbols;
3839
+ this.quoting = new Quoting(this.gateway, resolver);
3840
+ this.pools = new Pools(this.gateway, resolver, lenient);
3841
+ const keyOptions = {
3842
+ uniqueKeyPrefix: options === null || options === void 0 ? void 0 : options.uniqueKeyPrefix,
3843
+ ...lenient
3844
+ };
3845
+ this.positions = new Positions(this.gateway, resolver, this.signer, this.walletAddress, keyOptions);
3846
+ this.swaps = new Swaps(this.gateway, resolver, this.signer, this.walletAddress, keyOptions);
2365
3847
  this.assets = new Assets(this.dexBackendBaseUrl, httpClient);
3848
+ this.fees = new DexFees(this.gateway, this.signer);
2366
3849
  this.dexBackendWsUrl = (options === null || options === void 0 ? void 0 : options.dexBackendWsUrl) ?? ((options === null || options === void 0 ? void 0 : options.dexBackendBaseUrl) === void 0 ? environment.dexBackendWsUrl : `${this.dexBackendBaseUrl.replace(/^http/u, "ws")}/ws`);
2367
3850
  this.webSocketImpl = options === null || options === void 0 ? void 0 : options.webSocketImpl;
3851
+ this.streamApiKey = options === null || options === void 0 ? void 0 : options.streamApiKey;
2368
3852
  }
2369
3853
  /**
2370
3854
  * The realtime stream client for this environment (created on first use, not connected until you
@@ -2380,7 +3864,8 @@ var GSwap = class {
2380
3864
  get stream() {
2381
3865
  this.streamClient ??= new GdexStreamService({
2382
3866
  url: this.dexBackendWsUrl,
2383
- ...this.webSocketImpl === void 0 ? {} : { webSocketImpl: this.webSocketImpl }
3867
+ ...this.webSocketImpl === void 0 ? {} : { webSocketImpl: this.webSocketImpl },
3868
+ ...this.streamApiKey === void 0 ? {} : { apiKey: this.streamApiKey }
2384
3869
  });
2385
3870
  return this.streamClient;
2386
3871
  }
@@ -2389,9 +3874,149 @@ function trimTrailingSlash(value) {
2389
3874
  return value.replace(/\/$/u, "");
2390
3875
  }
2391
3876
 
3877
+ //#endregion
3878
+ //#region src/classes/pending_swaps.ts
3879
+ /** Key the wallet_extension has always used; its storage value is a plain array of entries. */
3880
+ const PENDING_SWAPS_STORAGE_KEY = "pendingSwaps.v1";
3881
+ /** Entries older than this are dropped the next time a wallet's swaps are listed. */
3882
+ const PENDING_SWAP_EXPIRY_MS = 6048e5;
3883
+ /**
3884
+ * Remembers queued swaps so their outcome can be confirmed after the UI, popup or service worker
3885
+ * that submitted them is gone (`confirmAppliedTrade` with the stored `uniqueKey`).
3886
+ *
3887
+ * Guarantees:
3888
+ * - Persistence is best effort and NEVER throws: a failing or missing storage must not turn an
3889
+ * accepted request into a client-side resubmission. `list` resolves `[]` when storage fails.
3890
+ * - Operations of one store instance are serialized, so concurrent `remember` / `forget` calls
3891
+ * cannot lose each other's write (read-modify-write over an async adapter). Two store instances
3892
+ * over the same storage (for example popup and service worker) are not coordinated.
3893
+ * - Entries that do not match the expected shape are ignored and dropped on the next write.
3894
+ * - Entries expire after seven days (an entry exactly seven days old is expired; frontend_v2
3895
+ * kept it for that one millisecond); `list` removes them from storage, which frontend_v2 never
3896
+ * did on read.
3897
+ * - New entries go to the end (or `insertAt: 'start'`); `list` returns the wallet's entries in
3898
+ * stored order (or `listOrder: 'newest-first'`).
3899
+ * - The stored shape is wallet_extension's by default; `storageKey`, `darkFlagField`, `insertAt`
3900
+ * and `listOrder` reproduce frontend_v2's. JSON key order inside an entry is not preserved.
3901
+ *
3902
+ * Passing no `storage` yields a store that remembers nothing.
3903
+ *
3904
+ * @example
3905
+ * ```ts
3906
+ * const store = new PendingSwapStore({
3907
+ * get: async (key) => (await chrome.storage.local.get(key))[key],
3908
+ * set: (key, value) => chrome.storage.local.set({ [key]: value }),
3909
+ * });
3910
+ * await store.remember({ uniqueKey, requestTransactionId, wallet, token0Symbol: 'GALA',
3911
+ * token1Symbol: 'GUSDC', isDarkSwap: false, createdAt: Date.now() });
3912
+ * for (const swap of await store.list(wallet)) await confirmAppliedTrade(client, swap);
3913
+ * ```
3914
+ */
3915
+ var PendingSwapStore = class {
3916
+ storage;
3917
+ storageKey;
3918
+ darkFlagField;
3919
+ insertAt;
3920
+ listOrder;
3921
+ now;
3922
+ queue = Promise.resolve();
3923
+ constructor(storage, options = {}) {
3924
+ this.storage = storage;
3925
+ this.storageKey = options.storageKey ?? "pendingSwaps.v1";
3926
+ this.darkFlagField = options.darkFlagField ?? "isDarkSwap";
3927
+ this.insertAt = options.insertAt ?? "end";
3928
+ this.listOrder = options.listOrder ?? "stored";
3929
+ this.now = options.now ?? Date.now;
3930
+ }
3931
+ /** Saves (or replaces, by `uniqueKey`) a pending swap. */
3932
+ remember(swap) {
3933
+ return this.enqueue(async (storage) => {
3934
+ const others = (await this.read(storage)).filter((entry) => entry.uniqueKey !== swap.uniqueKey).map(this.toStored);
3935
+ const stored = this.toStored(swap);
3936
+ await storage.set(this.storageKey, this.insertAt === "end" ? [...others, stored] : [stored, ...others]);
3937
+ });
3938
+ }
3939
+ /** Removes a pending swap once its outcome is known. */
3940
+ forget(uniqueKey) {
3941
+ return this.enqueue(async (storage) => {
3942
+ const current = await this.read(storage);
3943
+ await storage.set(this.storageKey, current.filter((entry) => entry.uniqueKey !== uniqueKey).map(this.toStored));
3944
+ });
3945
+ }
3946
+ /** The wallet's unexpired pending swaps (see `listOrder`). Expired entries are pruned. */
3947
+ async list(wallet) {
3948
+ return await this.enqueue(async (storage) => {
3949
+ const current = await this.read(storage);
3950
+ const now = this.now();
3951
+ const fresh = current.filter((entry) => now - entry.createdAt < 6048e5);
3952
+ if (fresh.length !== current.length) await storage.set(this.storageKey, fresh.map(this.toStored));
3953
+ const mine = fresh.filter((entry) => entry.wallet === wallet);
3954
+ return this.listOrder === "newest-first" ? mine.sort((left, right) => right.createdAt - left.createdAt) : mine;
3955
+ }) ?? [];
3956
+ }
3957
+ enqueue(operation) {
3958
+ const storage = this.storage;
3959
+ if (storage === void 0) return Promise.resolve(void 0);
3960
+ const run = this.queue.then(async () => {
3961
+ try {
3962
+ return await operation(storage);
3963
+ } catch {
3964
+ return;
3965
+ }
3966
+ });
3967
+ this.queue = run;
3968
+ return run;
3969
+ }
3970
+ async read(storage) {
3971
+ const value = await storage.get(this.storageKey);
3972
+ if (!Array.isArray(value)) return [];
3973
+ const entries = [];
3974
+ for (const item of value) {
3975
+ const entry = this.fromStored(item);
3976
+ if (entry !== void 0) entries.push(entry);
3977
+ }
3978
+ return entries;
3979
+ }
3980
+ toStored = (swap) => ({
3981
+ uniqueKey: swap.uniqueKey,
3982
+ requestTransactionId: swap.requestTransactionId,
3983
+ wallet: swap.wallet,
3984
+ token0Symbol: swap.token0Symbol,
3985
+ token1Symbol: swap.token1Symbol,
3986
+ [this.darkFlagField]: swap.isDarkSwap,
3987
+ createdAt: swap.createdAt
3988
+ });
3989
+ fromStored(value) {
3990
+ if (value === null || typeof value !== "object") return void 0;
3991
+ const entry = value;
3992
+ const flag = entry[this.darkFlagField];
3993
+ const { uniqueKey, requestTransactionId, wallet, token0Symbol, token1Symbol, createdAt } = entry;
3994
+ if (typeof uniqueKey !== "string" || !(typeof requestTransactionId === "string" || requestTransactionId === null) || typeof wallet !== "string" || typeof token0Symbol !== "string" || typeof token1Symbol !== "string" || typeof flag !== "boolean" || typeof createdAt !== "number" || !Number.isFinite(createdAt)) return;
3995
+ return {
3996
+ uniqueKey,
3997
+ requestTransactionId,
3998
+ wallet,
3999
+ token0Symbol,
4000
+ token1Symbol,
4001
+ isDarkSwap: flag,
4002
+ createdAt
4003
+ };
4004
+ }
4005
+ };
4006
+
2392
4007
  //#endregion
2393
4008
  //#region src/utils/eip712.ts
2394
4009
  /**
4010
+ * Genuine EIP-712 typed-data signing for the four v2 dex-contract operations proven (2026-09-16,
4011
+ * live bisection against stage — see defi-backend's `chain-findings-artifact` memory /
4012
+ * https://claude.ai/artifact/3N9MevSjoQdU7gqnVB3dfU) to recover the `client|` identity correctly
4013
+ * via `eth_signTypedData_v4`, unlike `personal_sign` (which only ever resolves the bare `eth|`
4014
+ * form — see `BrowserWalletSigner`'s own doc comment). Mirrors frontend_v2's
4015
+ * `src/Services/eip712Types.ts` + `readWriteUtilsV2.ts`'s `EIP712_FIELD_COMPLETERS` exactly —
4016
+ * ported here rather than shared as a package because gswap-sdk has no frontend_v2 dependency
4017
+ * and should not gain one for four small functions.
4018
+ */
4019
+ /**
2395
4020
  * Derives the EIP-712 `types` for a DTO from its runtime shape.
2396
4021
  *
2397
4022
  * The generated `types` travel with the signed payload and are what BOTH sides hash, so they
@@ -2456,11 +4081,24 @@ function generateEIP712Types(typeName, params) {
2456
4081
  });
2457
4082
  return types;
2458
4083
  }
2459
- /** Matches every genuine-EIP-712 probe that has actually recovered `client|` on stage. */
2460
- const EIP712_DOMAIN = {
2461
- name: "ethereum",
2462
- chainId: 1
2463
- };
4084
+ /**
4085
+ * Builds the EIP-712 domain from the wallet's LIVE `eth_chainId` result, exactly like frontend_v2
4086
+ * (DEFI-1895): a wallet hashes whatever domain it is handed, and MetaMask refuses a `chainId` that
4087
+ * differs from its active network, so a hardcoded `1` breaks every wallet that is not on Mainnet.
4088
+ * defi-backend recovers the signer from the `domain` the payload carries (live-proven for chain
4089
+ * ids 1, 56 and 11155111), so following the wallet is correct on every network.
4090
+ *
4091
+ * @param chainId - The raw `eth_chainId` result: a `0x` hex string (the JSON-RPC form) or an integer.
4092
+ * @throws GSwapSDKError (`INVALID_CHAIN_ID`) when the wallet answers with anything else.
4093
+ */
4094
+ function eip712DomainFor(chainId) {
4095
+ const parsed = typeof chainId === "string" && /^0x[0-9a-fA-F]+$/u.test(chainId) ? Number(chainId) : typeof chainId === "number" ? chainId : NaN;
4096
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new GSwapSDKError("Wallet returned an invalid chain id.", "INVALID_CHAIN_ID", { chainId });
4097
+ return {
4098
+ name: "ethereum",
4099
+ chainId: parsed
4100
+ };
4101
+ }
2464
4102
  /**
2465
4103
  * The four v2 operations proven to recover the signer correctly via genuine EIP-712: `RequestTrade`
2466
4104
  * never had the problem (one numeric field). `AddLiquidity`/`RemoveLiquidity`/
@@ -2557,15 +4195,216 @@ function buildEIP712Payload(methodName, dto) {
2557
4195
  }
2558
4196
 
2559
4197
  //#endregion
2560
- //#region src/classes/signers.ts
4198
+ //#region src/utils/signing.ts
4199
+ /**
4200
+ * SDK-owned signing primitives.
4201
+ *
4202
+ * Everything the SDK needs to produce a GalaChain signature lives here, with no dependency on
4203
+ * `@gala-chain/api` (which drags `secp256k1` native bindings, `reflect-metadata`, `class-validator`
4204
+ * and its own `bignumber.js` major into every consumer). The only runtime dependencies are the
4205
+ * audited, pure-JS `@noble/curves` (secp256k1) and `@noble/hashes` (keccak256), which run
4206
+ * unchanged in browsers, MV3 service workers, Node and React Native/Hermes.
4207
+ *
4208
+ * Every function here is pinned byte-for-byte to the signatures produced by `@gala-chain/api`
4209
+ * (`serialize`, `signatures.getSignature`), `@gala-chain/connect`, the Gala Wallet extension and
4210
+ * frontend_v2 by the golden-vector suite in `tests/signing-golden.test.ts`.
4211
+ */
4212
+ const textEncoder$1 = new TextEncoder();
4213
+ function toDeterministicNode(node) {
4214
+ if (node === null || node === void 0) return node;
4215
+ if (node instanceof RegExp) return node.toString();
4216
+ const candidate = node;
4217
+ return typeof candidate.toJSON === "function" ? Reflect.apply(candidate.toJSON, node, []) : node;
4218
+ }
4219
+ function deterministicNode(rawNode) {
4220
+ const node = toDeterministicNode(rawNode);
4221
+ if (node === void 0) return void 0;
4222
+ if (typeof node !== "object" || node === null) return JSON.stringify(node);
4223
+ if (Array.isArray(node)) return `[${node.map((item) => deterministicNode(item) ?? "null").join(",")}]`;
4224
+ const record = node;
4225
+ const members = [];
4226
+ for (const key of Object.keys(record).sort()) {
4227
+ const value = deterministicNode(record[key]);
4228
+ if (value === void 0) continue;
4229
+ members.push(`${JSON.stringify(key)}:${value}`);
4230
+ }
4231
+ return `{${members.join(",")}}`;
4232
+ }
4233
+ /**
4234
+ * GalaChain's deterministic JSON: object keys sorted (UTF-16 code-unit order) at every depth, no
4235
+ * whitespace, `undefined`/function members dropped, `toJSON()` honored. Byte-identical to
4236
+ * `serialize` from `@gala-chain/api` / `json-stringify-deterministic` for every JSON-shaped
4237
+ * value, which is all a DTO ever is.
4238
+ *
4239
+ * @throws TypeError for circular structures and BigInt values (as `JSON.stringify` does) and
4240
+ * when the value itself has no JSON representation (`undefined`, a function, a symbol).
4241
+ */
4242
+ function serialize(value) {
4243
+ JSON.stringify(value);
4244
+ const result = deterministicNode(value);
4245
+ if (result === void 0) throw new TypeError("Cannot serialize a value with no JSON representation.");
4246
+ return result;
4247
+ }
4248
+ /**
4249
+ * The exact string GalaChain hashes for a DTO: the (optional, string) `prefix` field PREPENDED to
4250
+ * the deterministic serialization of the DTO WITHOUT `signature`, `multisig`, `trace` and `prefix`.
4251
+ * Equivalent to `@gala-chain/api` 3.x `getPayloadToSign` for every non-EIP-712 DTO.
4252
+ */
4253
+ function getPayloadToSign(dto) {
4254
+ const { prefix, ...rest } = dto;
4255
+ delete rest["signature"];
4256
+ delete rest["multisig"];
4257
+ delete rest["trace"];
4258
+ return (typeof prefix === "string" ? prefix : "") + serialize(rest);
4259
+ }
2561
4260
  const ETHEREUM_SIGNED_MESSAGE = String.fromCharCode(25) + "Ethereum Signed Message:\n";
2562
- /** The EIP-191 header for `dto`'s canonical JSON (UTF-8 byte length; any `prefix` on it is ignored). */
4261
+ /**
4262
+ * The EIP-191 header for `message`: `"\x19Ethereum Signed Message:\n"` + the message's UTF-8 BYTE
4263
+ * length. This is what a wallet's `personal_sign` prepends, and what the chain recomputes when it
4264
+ * recovers a personal-signed DTO.
4265
+ */
4266
+ function calculatePersonalSignPrefix(message) {
4267
+ return ETHEREUM_SIGNED_MESSAGE + String(textEncoder$1.encode(message).length);
4268
+ }
4269
+ /**
4270
+ * The EIP-191 header for `serialize(dto)` (any `prefix` already on the DTO is ignored). This is
4271
+ * deliberately the same string the signers hand a wallet to sign, so the header's length can never
4272
+ * disagree with the signed message. Wallet-neutral: it is the value both a browser wallet's
4273
+ * `personal_sign` and Gala Wallet's `gala_signChainDto` (personal-sign scheme) prepend.
4274
+ */
2563
4275
  function personalSignPrefixFor(dto) {
2564
4276
  const withoutPrefix = { ...dto };
2565
4277
  delete withoutPrefix["prefix"];
2566
- const message = serialize(withoutPrefix);
2567
- return ETHEREUM_SIGNED_MESSAGE + String(new TextEncoder().encode(message).length);
4278
+ return calculatePersonalSignPrefix(serialize(withoutPrefix));
4279
+ }
4280
+ /**
4281
+ * The `@gala-chain/connect`-style prefix frontend_v2 puts in the body of an EIP-712 write: the
4282
+ * EIP-191 header whose number counts the prefix string ITSELF plus the body (the UTF-8 length of
4283
+ * `getPayloadToSign({ ...dto, prefix })`), found by fixpoint because adding the prefix changes the
4284
+ * length (a body of 99 bytes settles on 101).
4285
+ *
4286
+ * It is NOT a personal-sign prefix (a personal-sign write with this value recovers a phantom
4287
+ * signer; use `personalSignPrefixFor`). Under EIP-712 the field is an untyped extra that the
4288
+ * typed-data hash ignores and defi-backend recomputes, so it only exists to keep the submitted
4289
+ * body byte-identical to what frontend_v2 has always sent.
4290
+ */
4291
+ function fixpointPrefixFor(dto) {
4292
+ const withoutPrefix = { ...dto };
4293
+ delete withoutPrefix["prefix"];
4294
+ let payloadLength = textEncoder$1.encode(getPayloadToSign(withoutPrefix)).length;
4295
+ let prefix = ETHEREUM_SIGNED_MESSAGE + String(payloadLength);
4296
+ let previousLength = -1;
4297
+ while (payloadLength !== previousLength) {
4298
+ previousLength = payloadLength;
4299
+ prefix = ETHEREUM_SIGNED_MESSAGE + String(payloadLength);
4300
+ payloadLength = textEncoder$1.encode(getPayloadToSign({
4301
+ ...withoutPrefix,
4302
+ prefix
4303
+ })).length;
4304
+ }
4305
+ return prefix;
4306
+ }
4307
+ function invalidPrivateKey(reason) {
4308
+ return new GSwapSDKError(`Invalid private key: ${reason}`, "VALIDATION_ERROR", { type: "INVALID_PRIVATE_KEY" });
4309
+ }
4310
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4311
+ function decodeBase64(input) {
4312
+ if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(input) || input.length % 4 !== 0) return void 0;
4313
+ const unpadded = input.replace(/=+$/u, "");
4314
+ const bytes = [];
4315
+ let buffer = 0;
4316
+ let bits = 0;
4317
+ for (const char of unpadded) {
4318
+ buffer = buffer << 6 | BASE64_ALPHABET.indexOf(char);
4319
+ bits += 6;
4320
+ if (bits >= 8) {
4321
+ bits -= 8;
4322
+ bytes.push(buffer >> bits & 255);
4323
+ }
4324
+ }
4325
+ return Uint8Array.from(bytes);
2568
4326
  }
4327
+ function decodeHex(input) {
4328
+ const bytes = new Uint8Array(input.length / 2);
4329
+ for (let index = 0; index < bytes.length; index += 1) bytes[index] = parseInt(input.slice(index * 2, index * 2 + 2), 16);
4330
+ return bytes;
4331
+ }
4332
+ /**
4333
+ * Normalizes a secp256k1 private key to a fresh 32-byte array (the caller may zero it). Also
4334
+ * accepts 32 raw bytes, which are validated and copied. For strings it accepts what `@gala-chain/api`'s
4335
+ * `normalizePrivateKey` accepted for a usable key: 64 hex characters (optionally `0x`-prefixed;
4336
+ * a 63-character hex key is left-padded with a zero) or a 44-character base64 string.
4337
+ *
4338
+ * @throws GSwapSDKError (`VALIDATION_ERROR`) for any other shape or an out-of-range scalar.
4339
+ */
4340
+ function normalizePrivateKey(input) {
4341
+ if (typeof input !== "string") {
4342
+ if (!(input instanceof Uint8Array) || input.length !== 32) throw invalidPrivateKey("expected exactly 32 bytes.");
4343
+ if (!secp256k1.utils.isValidSecretKey(input)) throw invalidPrivateKey("not a valid secp256k1 scalar.");
4344
+ return Uint8Array.from(input);
4345
+ }
4346
+ const withoutPrefix = input.startsWith("0x") ? input.slice(2) : input;
4347
+ let key;
4348
+ if (/^[0-9a-fA-F]{63,64}$/u.test(withoutPrefix)) key = decodeHex(withoutPrefix.length === 63 ? `0${withoutPrefix}` : withoutPrefix);
4349
+ else if (withoutPrefix.length === 44) key = decodeBase64(withoutPrefix);
4350
+ if ((key === null || key === void 0 ? void 0 : key.length) !== 32) throw invalidPrivateKey("expected 64 hex characters or a 44-character base64 string.");
4351
+ if (!secp256k1.utils.isValidSecretKey(key)) throw invalidPrivateKey("not a valid secp256k1 scalar.");
4352
+ return key;
4353
+ }
4354
+ /**
4355
+ * Signs a 32-byte keccak256 hash and returns GalaChain's recoverable signature: 64 hex `r` +
4356
+ * 64 hex `s` + `1b`/`1c` (`v`), lowercase, no `0x`. Deterministic (RFC 6979) and low-S, exactly
4357
+ * what `@gala-chain/api` and the Gala Wallet produce. A DER signature carries no recovery byte
4358
+ * and cannot be used: the chain RECOVERS the signer from the signature.
4359
+ */
4360
+ function signHash(privateKey, hash) {
4361
+ if (hash.length !== 32) throw new GSwapSDKError(`secp256k1 can only sign a 32-byte hash (got ${hash.length}).`, "VALIDATION_ERROR");
4362
+ const signature = secp256k1.sign(hash, privateKey, {
4363
+ prehash: false,
4364
+ lowS: true
4365
+ });
4366
+ return bytesToHex(signature.toBytes("compact")) + (signature.recovery === 1 ? "1c" : "1b");
4367
+ }
4368
+ /**
4369
+ * The lowercase `0x` Ethereum address (last 20 bytes of keccak256 of the uncompressed public key)
4370
+ * of a normalized private key. The Gala `eth|` identity of a key is this address.
4371
+ */
4372
+ function ethAddressOfPrivateKey(privateKey) {
4373
+ const publicKey = secp256k1.getPublicKey(privateKey, false);
4374
+ return `0x${bytesToHex(keccak_256(publicKey.slice(1)).slice(-20))}`;
4375
+ }
4376
+ /** keccak256 of a string's UTF-8 bytes. */
4377
+ function keccak256Utf8(data) {
4378
+ return keccak_256(textEncoder$1.encode(data));
4379
+ }
4380
+ /**
4381
+ * GalaChain NATIVE signature over an already-serialized DTO: keccak256 of the exact bytes, with no
4382
+ * prefix and no signature envelope. The caller owns serialization, so the bytes shown to a user
4383
+ * and the bytes signed can never diverge.
4384
+ */
4385
+ function signSerializedNative(privateKey, serializedDto) {
4386
+ return signHash(privateKey, keccak256Utf8(serializedDto));
4387
+ }
4388
+ /**
4389
+ * GalaChain NATIVE signature for a DTO: keccak256 of `getPayloadToSign(dto)`, signed with
4390
+ * secp256k1. Byte-identical to `signatures.getSignature` from `@gala-chain/api`.
4391
+ */
4392
+ function signNative(privateKey, dto) {
4393
+ return signSerializedNative(privateKey, getPayloadToSign(dto));
4394
+ }
4395
+ /**
4396
+ * Personal-sign hash and signature: keccak256(`prefix` + `message`), both hashed VERBATIM with no
4397
+ * further EIP-191 wrapping. Passing `prefix = calculatePersonalSignPrefix(message)` reproduces a
4398
+ * wallet's `personal_sign` of `message`. Gala Wallet's `gala_signChainDto` (personal-sign
4399
+ * scheme) hashes `prefix` + the exact string it is handed, which can differ from the bare message
4400
+ * (see the `signatureOverPrefixlessBody` vectors); that quirk is why the two inputs stay separate.
4401
+ */
4402
+ function signPersonalMessage(privateKey, prefix, message) {
4403
+ return signHash(privateKey, keccak256Utf8(prefix + message));
4404
+ }
4405
+
4406
+ //#endregion
4407
+ //#region src/classes/signers.ts
2569
4408
  function getWalletProvider() {
2570
4409
  var _globalObject$window;
2571
4410
  const wallet = (_globalObject$window = globalThis.window) === null || _globalObject$window === void 0 ? void 0 : _globalObject$window.gala;
@@ -2574,32 +4413,77 @@ function getWalletProvider() {
2574
4413
  }
2575
4414
  function getNativeSignature(value) {
2576
4415
  if (typeof value !== "string") throw new GSwapSDKError("Wallet returned an invalid signature.", "INVALID_SIGNATURE");
2577
- if (!/^[0-9a-fA-F]{130}$/u.test(value)) throw new GSwapSDKError("Wallet returned an invalid native signature.", "INVALID_SIGNATURE");
4416
+ if (!/^(?:0x)?[0-9a-fA-F]{130}$/u.test(value)) throw new GSwapSDKError("Wallet returned an invalid native signature.", "INVALID_SIGNATURE");
2578
4417
  return value;
2579
4418
  }
2580
4419
  function getPersonalSignature(value) {
2581
4420
  if (typeof value !== "string" || !/^(?:0x)?[0-9a-fA-F]{130}$/u.test(value)) throw new GSwapSDKError("Wallet returned an invalid personal-sign signature.", "INVALID_SIGNATURE");
2582
4421
  return value;
2583
4422
  }
4423
+ /**
4424
+ * The `0x`-prefixed Ethereum address a Gala account is signed by, or `undefined` when the account
4425
+ * names no key (a `client|` alias or a bare GalaChain id is not derivable from a key). Accepts
4426
+ * `0x` plus 40 hex digits and `eth|` plus 40 hex digits, with or without `0x`; the caller's casing
4427
+ * is kept. For {@link LazyPrivateKeySignerOptions.expectedAddress}, which should only be set for
4428
+ * an account whose key can be checked.
4429
+ *
4430
+ * @example
4431
+ * ```ts
4432
+ * createLazyPrivateKeySigner(getKey, { expectedAddress: ethereumAddressOfAccount(account) });
4433
+ * ```
4434
+ */
4435
+ function ethereumAddressOfAccount(account) {
4436
+ const match = /^(?:eth\|(?:0x)?|0x)([0-9a-fA-F]{40})$/u.exec(account);
4437
+ return (match === null || match === void 0 ? void 0 : match[1]) === void 0 ? void 0 : `0x${match[1]}`;
4438
+ }
2584
4439
  function normalizeEthereumAccount(address, allowGalaPrefix) {
2585
- const normalized = allowGalaPrefix && address.startsWith("eth|") ? address.slice(4) : address;
4440
+ const normalized = allowGalaPrefix ? ethereumAddressOfAccount(address) ?? address : address;
2586
4441
  if (!/^0x[0-9a-fA-F]{40}$/u.test(normalized)) throw new GSwapSDKError("Wallet address must be the selected Ethereum account in 0x-prefixed form.", "VALIDATION_ERROR", {
2587
4442
  type: "INVALID_WALLET_ADDRESS",
2588
4443
  value: address
2589
4444
  });
2590
- return `0x${normalized.slice(2).toLowerCase()}`;
4445
+ return normalized;
2591
4446
  }
2592
- function getErrorMessage(error) {
2593
- if (error instanceof Error) return error.message;
2594
- if (typeof error === "string") return error;
2595
- if (typeof error === "object" && error !== null && "message" in error) {
2596
- const message = error.message;
2597
- return typeof message === "string" ? message : "";
2598
- }
2599
- return "";
4447
+ /**
4448
+ * The account handed to Gala Wallet as the signer address. An Ethereum account is held to the
4449
+ * same strict check as ever (`0x` plus 40 hex digits; `eth|` plus 40 hex digits, with or without
4450
+ * `0x`, is normalized to it). A GalaChain id the wallet can also be asked to sign for is passed
4451
+ * through verbatim: `client|` plus 24 hex digits, or the bare 24 hex digits that frontend_v2 keeps
4452
+ * in `state.user.wallet` after a direct `client|` login. Nothing else is accepted.
4453
+ */
4454
+ function normalizeGalaWalletAccount(address) {
4455
+ const eth = /^eth\|(?:0x)?([0-9a-fA-F]{40})$/u.exec(address);
4456
+ if ((eth === null || eth === void 0 ? void 0 : eth[1]) !== void 0) return `0x${eth[1]}`;
4457
+ if (/^0x[0-9a-fA-F]{40}$/u.test(address) || /^(?:client\|)?[0-9a-fA-F]{24}$/u.test(address)) return address;
4458
+ throw new GSwapSDKError("Wallet address must be the selected Ethereum account (0x plus 40 hex digits, or eth|...) or a GalaChain id (client|... or 24 hex digits).", "VALIDATION_ERROR", {
4459
+ type: "INVALID_WALLET_ADDRESS",
4460
+ value: address
4461
+ });
4462
+ }
4463
+ /** An identity Gala Wallet can act as: an account form above, any `eth|`/`client|` alias, verbatim. */
4464
+ function assertWalletIdentity(identity) {
4465
+ if (!/^(?:eth\|(?:0x)?[0-9a-fA-F]{40}|0x[0-9a-fA-F]{40}|(?:client\|)?[0-9a-fA-F]{24})$/u.test(identity)) throw new GSwapSDKError("selectAddress must be an eth| or client| identity, or an account in one of the accepted forms.", "VALIDATION_ERROR", {
4466
+ type: "INVALID_WALLET_ADDRESS",
4467
+ value: identity
4468
+ });
2600
4469
  }
2601
- function isUnsupportedNativeSchemeError(error) {
2602
- return /unsupported|unknown scheme/i.test(getErrorMessage(error));
4470
+ /** Provider-wide queue: `setAddress` + the signature request must not interleave across signers. */
4471
+ const walletIdentityQueue = /* @__PURE__ */ new WeakMap();
4472
+ /**
4473
+ * Runs `signWith` with `identity` selected on a Gala Wallet that supports `setAddress`. Gala
4474
+ * Wallet holds ONE current account, so two signatures for different identities in flight at once
4475
+ * would sign as whichever selection came last; the pair (select, sign) is therefore serialized
4476
+ * per provider. A failed selection fails the signature (nothing is requested).
4477
+ */
4478
+ function withSelectedIdentity(wallet, identity, signWith) {
4479
+ const { setAddress } = wallet;
4480
+ if (identity === void 0 || typeof setAddress !== "function") return signWith();
4481
+ const run = (walletIdentityQueue.get(wallet) ?? Promise.resolve()).catch(() => void 0).then(async () => {
4482
+ await setAddress.call(wallet, identity);
4483
+ return signWith();
4484
+ });
4485
+ walletIdentityQueue.set(wallet, run);
4486
+ return run;
2603
4487
  }
2604
4488
  function personalSign(provider, dto, walletAddress, methodName) {
2605
4489
  const prefix = personalSignPrefixFor(dto);
@@ -2630,9 +4514,13 @@ function personalSign(provider, dto, walletAddress, methodName) {
2630
4514
  * byte-for-byte equivalent (same recovered address from the same signature against both
2631
4515
  * constructions) before this shipped, since gswap-sdk deliberately has no ethers dependency.
2632
4516
  */
2633
- function signEIP712(provider, methodName, dto, walletAddress) {
4517
+ async function signEIP712(provider, methodName, dto, walletAddress) {
2634
4518
  const { completedDto, types } = buildEIP712Payload(methodName, dto);
2635
- const prefix = personalSignPrefixFor(completedDto);
4519
+ const domain = eip712DomainFor(await provider.request({
4520
+ method: "eth_chainId",
4521
+ params: []
4522
+ }));
4523
+ const prefix = fixpointPrefixFor(completedDto);
2636
4524
  const message = {
2637
4525
  ...completedDto,
2638
4526
  prefix
@@ -2645,7 +4533,7 @@ function signEIP712(provider, methodName, dto, walletAddress) {
2645
4533
  type: "uint256"
2646
4534
  }];
2647
4535
  const payload = {
2648
- domain: EIP712_DOMAIN,
4536
+ domain,
2649
4537
  types: {
2650
4538
  ...types,
2651
4539
  EIP712Domain: eip712Domain
@@ -2653,15 +4541,16 @@ function signEIP712(provider, methodName, dto, walletAddress) {
2653
4541
  primaryType: methodName,
2654
4542
  message
2655
4543
  };
2656
- return provider.request({
4544
+ const value = await provider.request({
2657
4545
  method: "eth_signTypedData_v4",
2658
4546
  params: [walletAddress, JSON.stringify(payload)]
2659
- }).then((value) => ({
4547
+ });
4548
+ return {
2660
4549
  ...message,
4550
+ signature: getPersonalSignature(value),
2661
4551
  types,
2662
- domain: EIP712_DOMAIN,
2663
- signature: getPersonalSignature(value)
2664
- }));
4552
+ domain
4553
+ };
2665
4554
  }
2666
4555
  /**
2667
4556
  * A GalaChain native signer backed by a secp256k1 private key.
@@ -2670,7 +4559,7 @@ function signEIP712(provider, methodName, dto, walletAddress) {
2670
4559
  * or EIP-712 metadata. The private-key identity is recovered by the chain.
2671
4560
  */
2672
4561
  var PrivateKeySigner = class {
2673
- keyBuffer;
4562
+ privateKey;
2674
4563
  /**
2675
4564
  * Creates a native signer from a hex or base64 secp256k1 private key.
2676
4565
  *
@@ -2681,7 +4570,7 @@ var PrivateKeySigner = class {
2681
4570
  * ```
2682
4571
  */
2683
4572
  constructor(privateKey) {
2684
- this.keyBuffer = signatures.normalizePrivateKey(privateKey);
4573
+ this.privateKey = normalizePrivateKey(privateKey);
2685
4574
  }
2686
4575
  /**
2687
4576
  * Signs the DTO with GalaChain native signing.
@@ -2696,7 +4585,7 @@ var PrivateKeySigner = class {
2696
4585
  * ```
2697
4586
  */
2698
4587
  signObject(_methodName, dto) {
2699
- const signature = signatures.getSignature(dto, this.keyBuffer);
4588
+ const signature = signNative(this.privateKey, dto);
2700
4589
  return Promise.resolve({
2701
4590
  ...dto,
2702
4591
  signature
@@ -2704,18 +4593,65 @@ var PrivateKeySigner = class {
2704
4593
  }
2705
4594
  };
2706
4595
  /**
2707
- * A Gala Wallet signer using native GalaChain signing, with an opt-in personal-sign mode and
2708
- * automatic fallback for older wallets.
4596
+ * Creates the native-signing equivalent of {@link PrivateKeySigner} for apps that must not hold a
4597
+ * key in a long-lived object (an extension that reads its session key at sign time).
2709
4598
  *
2710
- * The selected Ethereum account is passed to Gala Wallet. Personal-sign resolves the bare
2711
- * `eth|` identity; the legacy EIP-712 `eth_signTypedData` path is not used. The fallback calls
2712
- * `gala_signChainDto([serialize({ ...dto, prefix }), walletAddress, methodName])`.
4599
+ * `getPrivateKey` is called once per `signObject` call, never cached, never shared between calls
4600
+ * (concurrent signatures each fetch their own key), and the normalized key bytes are zeroed
4601
+ * before `signObject` settles, also on failure. A rejection or exception from `getPrivateKey` is
4602
+ * passed through unchanged, so the app's own "wallet is locked" error reaches its caller; a key
4603
+ * that is not a valid secp256k1 key string is a `VALIDATION_ERROR` (`INVALID_PRIVATE_KEY`).
4604
+ * Signatures are byte-identical to `new PrivateKeySigner(key).signObject(...)`.
4605
+ *
4606
+ * A JavaScript string cannot be wiped; the provider owns the lifetime of the string it returns.
4607
+ *
4608
+ * @example
4609
+ * ```typescript
4610
+ * const signer = createLazyPrivateKeySigner(
4611
+ * () => SecurityHelper.getSessionSecret(SecurityHelper.SESSION_PRIVATE_KEY_KEY),
4612
+ * { expectedAddress: account.address },
4613
+ * );
4614
+ * const gswap = new GSwap({ signer });
4615
+ * ```
4616
+ */
4617
+ function createLazyPrivateKeySigner(getPrivateKey, options = {}) {
4618
+ const expected = options.expectedAddress;
4619
+ const expectedLower = expected === void 0 ? void 0 : normalizeEthereumAccount(expected, true).toLowerCase();
4620
+ return { async signObject(_methodName, dto) {
4621
+ const provided = await getPrivateKey();
4622
+ if (typeof provided !== "string") throw new GSwapSDKError("Invalid private key: the provider returned no key.", "VALIDATION_ERROR", { type: "INVALID_PRIVATE_KEY" });
4623
+ const key = normalizePrivateKey(provided);
4624
+ try {
4625
+ if (expectedLower !== void 0 && ethAddressOfPrivateKey(key) !== expectedLower) throw new GSwapSDKError("The signing key does not belong to the expected account.", "SIGNER_ADDRESS_MISMATCH");
4626
+ return {
4627
+ ...dto,
4628
+ signature: signNative(key, dto)
4629
+ };
4630
+ } finally {
4631
+ key.fill(0);
4632
+ }
4633
+ } };
4634
+ }
4635
+ /**
4636
+ * A Gala Wallet signer using native GalaChain signing, with an opt-in personal-sign mode.
4637
+ *
4638
+ * The selected Ethereum account is passed to Gala Wallet. There is NO automatic fallback: a
4639
+ * wallet that rejects `native` surfaces its error to the caller, because the only alternative,
4640
+ * `personal-sign`, silently recovers a phantom signer on every released Gala Wallet.
4641
+ *
4642
+ * `scheme: 'personal-sign'` calls
4643
+ * `gala_signChainDto([serialize({ ...dto, prefix }), walletAddress, methodName])` and REQUIRES
4644
+ * a wallet build that contains wallet_extension commit 3ad7ae6 (strips the embedded `prefix`
4645
+ * field and hashes `prefix` + the prefix-less canonical body, the chain's scheme). Wallets
4646
+ * released before that fix hash the string with the `prefix` field still inside it and the
4647
+ * signature recovers to an address nobody holds, so only opt in once the fixed wallet is
4648
+ * released. See SIGNING-PARITY.md.
2713
4649
  */
2714
4650
  var GalaWalletSigner = class {
2715
4651
  /** The wallet address supplied to Gala Wallet for each signing request. */
2716
4652
  walletAddress;
2717
- /** The scheme currently used, including any automatic native-to-personal fallback. */
2718
- effectiveScheme;
4653
+ /** The signing scheme used for every request; it never changes after construction. */
4654
+ scheme;
2719
4655
  /**
2720
4656
  * Creates a Gala Wallet signer.
2721
4657
  *
@@ -2725,14 +4661,23 @@ var GalaWalletSigner = class {
2725
4661
  * const signed = await signer.signObject('RequestTrade', { token0: 'GALA', token1: 'GUSDC' });
2726
4662
  * ```
2727
4663
  *
2728
- * @param walletAddress - The selected Ethereum account (`0x` plus 40 hex digits); Gala Wallet's
2729
- * `eth|0x...` form is also accepted and normalized.
2730
- * @param options - Select native signing or personal-sign explicitly.
4664
+ * @param walletAddress - The account passed to Gala Wallet as the signer address. An Ethereum
4665
+ * account is `0x` plus 40 hex digits (Gala Wallet's `eth|...` form is normalized to it and held
4666
+ * to the same check); a GalaChain id is `client|` plus 24 hex digits, or the bare 24 hex digits
4667
+ * frontend_v2 keeps for a direct `client|` login, and is passed through unchanged.
4668
+ * @param options - Select native signing (default) or personal-sign explicitly.
4669
+ * `'personal-sign'` requires a Gala Wallet build with wallet_extension commit 3ad7ae6.
4670
+ * `selectAddress` is the identity (`client|...` / `eth|...`) Gala Wallet is switched to with
4671
+ * `window.gala.setAddress` before every signature, when the wallet offers it.
2731
4672
  */
2732
4673
  constructor(walletAddress, options = {}) {
2733
- this.walletAddress = normalizeEthereumAccount(walletAddress, true);
2734
- this.effectiveScheme = options.scheme ?? "native";
4674
+ this.walletAddress = normalizeGalaWalletAccount(walletAddress);
4675
+ this.scheme = options.scheme ?? "native";
4676
+ if (options.selectAddress !== void 0) assertWalletIdentity(options.selectAddress);
4677
+ this.selectAddress = options.selectAddress;
2735
4678
  }
4679
+ /** The identity Gala Wallet is switched to before each signature, if one was given. */
4680
+ selectAddress;
2736
4681
  /**
2737
4682
  * Signs a DTO through `gala_signChainDto`.
2738
4683
  *
@@ -2748,8 +4693,8 @@ var GalaWalletSigner = class {
2748
4693
  */
2749
4694
  async signObject(methodName, dto) {
2750
4695
  const wallet = getWalletProvider();
2751
- if (this.effectiveScheme === "personal-sign") return personalSign(wallet, dto, this.walletAddress, methodName);
2752
- try {
4696
+ return withSelectedIdentity(wallet, this.selectAddress, async () => {
4697
+ if (this.scheme === "personal-sign") return personalSign(wallet, dto, this.walletAddress, methodName);
2753
4698
  const signature = await wallet.request({
2754
4699
  method: "gala_signChainDto",
2755
4700
  params: [
@@ -2763,11 +4708,7 @@ var GalaWalletSigner = class {
2763
4708
  ...dto,
2764
4709
  signature: getNativeSignature(signature)
2765
4710
  };
2766
- } catch (error) {
2767
- if (!isUnsupportedNativeSchemeError(error)) throw error;
2768
- this.effectiveScheme = "personal-sign";
2769
- return personalSign(wallet, dto, this.walletAddress, methodName);
2770
- }
4711
+ });
2771
4712
  }
2772
4713
  };
2773
4714
  /**
@@ -2829,5 +4770,669 @@ var BrowserWalletSigner = class {
2829
4770
  };
2830
4771
 
2831
4772
  //#endregion
2832
- export { ALL_FEE_TIERS, Assets, BrowserWalletSigner, ChainGateway, FEE_TIER, GC_MAX_TICK, GC_MIN_TICK, GSWAP_ENVIRONMENTS, GSwap, GSwapSDKError, GalaWalletSigner, GdexStreamError, GdexStreamService, Pools, Positions, PrivateKeySigner, Quoting, SubmittedTransaction, Swaps, Symbols, TICK_SPACING_BY_FEE, alignTickDown, alignTickUp, assertTickRange, compositeKeyOf, getObjectProperty, getStringProperty, isGdexLiquidityEvent, orderSymbols, parseJson, parseTokenClassKey, readOrderedSymbols, sqrtPriceToTick, tickFromPrice, tickToSqrtPrice, validateFee, validateNumericAmount, validatePriceValues, validateTickRange, validateTickSpacing, validateTokenDecimals, validateWalletAddress };
4773
+ //#region src/utils/chain_dto_signing.ts
4774
+ /**
4775
+ * String-key signing entry points for apps that hold a private key only briefly (the Gala Wallet
4776
+ * extension reads its session key at sign time and must not keep it in a long-lived object).
4777
+ *
4778
+ * These are thin, byte-exact ports of the functions the wallet shipped in
4779
+ * `wallet_extension/src/helpers/wallet/signing.ts` (including the fail-closed personal-sign
4780
+ * payload resolver from its commit 3ad7ae6), re-expressed over the SDK's own primitives. Every
4781
+ * function takes the key as a string (`0x`-optional hex, or base64) or as bytes, normalizes it per
4782
+ * call, and zeroes the normalized copy before returning, so nothing in this module retains a key.
4783
+ * A JavaScript string cannot be wiped; the caller owns the lifetime of the string it passes in.
4784
+ */
4785
+ /**
4786
+ * Runs `action` with a private normalized copy of the key, zeroed afterwards (also when `action`
4787
+ * throws). Bytes given by the caller are copied, never modified: the caller owns them.
4788
+ */
4789
+ function withPrivateKey(privateKey, action) {
4790
+ const key = normalizePrivateKey(privateKey);
4791
+ try {
4792
+ return action(key);
4793
+ } finally {
4794
+ key.fill(0);
4795
+ }
4796
+ }
4797
+ /**
4798
+ * GalaChain NATIVE signature over an already-serialized DTO: keccak256 of the exact bytes with no
4799
+ * prefix and no envelope (wallet `signChainDtoNative`). The caller owns serialization, so the
4800
+ * bytes a user reviewed and the bytes signed cannot diverge.
4801
+ *
4802
+ * @returns 130 lowercase hex characters, `r || s || v` with `v` = `1b`/`1c`, no `0x`.
4803
+ */
4804
+ function signChainDtoNative(privateKey, serializedDto) {
4805
+ return withPrivateKey(privateKey, (key) => signSerializedNative(key, serializedDto));
4806
+ }
4807
+ /**
4808
+ * The personal-sign hash primitive (wallet `signChainDtoHash`): keccak256(`prefix` + `message`),
4809
+ * both verbatim, with no further EIP-191 wrap. `message` must be the DTO's serialization WITHOUT
4810
+ * its `prefix` field; callers holding a dApp-supplied string go through `signChainDtoPersonalSign`,
4811
+ * which derives both inputs from it.
4812
+ */
4813
+ function signChainDtoHash(privateKey, prefix, message) {
4814
+ return withPrivateKey(privateKey, (key) => signPersonalMessage(key, prefix, message));
4815
+ }
4816
+ const PERSONAL_SIGN_HEADER = String.fromCharCode(25) + "Ethereum Signed Message:\n";
4817
+ const PERSONAL_SIGN_LENGTH_PATTERN = /^(?:0|[1-9][0-9]*)$/u;
4818
+ function isPersonalSignHeader(prefix) {
4819
+ return prefix.startsWith(PERSONAL_SIGN_HEADER) && PERSONAL_SIGN_LENGTH_PATTERN.test(prefix.slice(26));
4820
+ }
4821
+ /**
4822
+ * Top-level fields that would make the chain (`getPayloadToSign`) or defi-backend's personal-sign
4823
+ * verifier hash something other than `prefix + serialize(dto minus prefix)`. An unsigned DTO from
4824
+ * a dApp never legitimately carries any of them, so they fail closed. Order is part of the error
4825
+ * message the wallet shows its user.
4826
+ */
4827
+ const PERSONAL_SIGN_FORBIDDEN_FIELDS = [
4828
+ "signature",
4829
+ "multisig",
4830
+ "trace",
4831
+ "types",
4832
+ "domain",
4833
+ "stringsInstructions"
4834
+ ];
4835
+ function refusePersonalSign(message, type) {
4836
+ throw new GSwapSDKError(message, "INVALID_PERSONAL_SIGN_DTO", { type });
4837
+ }
4838
+ /**
4839
+ * Derives what to hash for a `gala_signChainDto` personal-sign request from the exact string the
4840
+ * dApp sent (and the approval card displayed): wallet `resolvePersonalSignChainDtoPayload`.
4841
+ *
4842
+ * The dApp submits `{ ...dto, prefix, signature }`, which the chain recovers from
4843
+ * `keccak256(prefix + serialize(dto WITHOUT prefix))`. So when the string embeds a `prefix`
4844
+ * field, that field is removed and the remainder re-serialized; the prefix used is the dApp's own
4845
+ * value byte for byte. A string with no `prefix` field is hashed verbatim with an empty prefix
4846
+ * (chain-equivalent to a native signature of that exact string).
4847
+ *
4848
+ * Fails closed (throws `GSwapSDKError` `INVALID_PERSONAL_SIGN_DTO`, message text identical to the
4849
+ * wallet's) on: malformed JSON; JSON that is not a plain object; a `prefix` that is not an EIP-191
4850
+ * "Ethereum Signed Message" header; any signing-envelope field (`signature`, `multisig`, `trace`,
4851
+ * `types`, `domain`, `stringsInstructions`); and a string that is not already the canonical
4852
+ * serialization of what it parses to (duplicate keys, lossy numbers, key-order tricks the card
4853
+ * would render differently). Stripping `prefix` is therefore the ONLY byte change between the
4854
+ * reviewed string and the signed message.
4855
+ */
4856
+ function resolvePersonalSignChainDtoPayload(data) {
4857
+ let parsed;
4858
+ try {
4859
+ parsed = JSON.parse(data);
4860
+ } catch {
4861
+ return refusePersonalSign("Personal-sign DTO is not valid JSON", "NOT_JSON");
4862
+ }
4863
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return refusePersonalSign("Personal-sign DTO must be a JSON object", "NOT_OBJECT");
4864
+ const dto = parsed;
4865
+ if (!Object.prototype.hasOwnProperty.call(dto, "prefix")) return {
4866
+ prefix: "",
4867
+ message: data
4868
+ };
4869
+ const { prefix, ...withoutPrefix } = dto;
4870
+ if (typeof prefix !== "string" || !isPersonalSignHeader(prefix)) return refusePersonalSign("Personal-sign DTO prefix is not an EIP-191 \"Ethereum Signed Message\" header", "BAD_PREFIX");
4871
+ const forbidden = PERSONAL_SIGN_FORBIDDEN_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(withoutPrefix, field));
4872
+ if (forbidden.length > 0) return refusePersonalSign(`Personal-sign DTO carries signing-envelope field(s): ${forbidden.join(", ")}`, "ENVELOPE_FIELDS");
4873
+ if (serialize(dto) !== data) return refusePersonalSign("Personal-sign DTO is not in canonical serialized form", "NOT_CANONICAL");
4874
+ return {
4875
+ prefix,
4876
+ message: serialize(withoutPrefix)
4877
+ };
4878
+ }
4879
+ /**
4880
+ * Signs a GalaChain personal-sign scheme DTO from the exact string a dApp sent through
4881
+ * `gala_signChainDto` (wallet `signChainDtoPersonalSign`). See
4882
+ * `resolvePersonalSignChainDtoPayload` for what is signed and what is refused; the signature
4883
+ * recovers, under the chain's `getPayloadToSign`, to this key for the DTO the dApp submits with
4884
+ * its `prefix`, and equals a `personal_sign` of the bare body when that prefix is the EIP-191
4885
+ * header of the bare body's byte length.
4886
+ *
4887
+ * @throws GSwapSDKError (`INVALID_PERSONAL_SIGN_DTO`) before any signing when the input is refused.
4888
+ */
4889
+ function signChainDtoPersonalSign(privateKey, data) {
4890
+ const { prefix, message } = resolvePersonalSignChainDtoPayload(data);
4891
+ return signChainDtoHash(privateKey, prefix, message);
4892
+ }
4893
+ /**
4894
+ * Raw object signing (wallet `signObject`), used for transfers, bridge and fee-dry-run DTOs the
4895
+ * wallet signs itself. Removes ONLY `signature`, signs `keccak256(serialize(rest))` natively
4896
+ * (no `prefix` handling, and `trace` / `multisig` stay in the signed bytes, unlike
4897
+ * `signNative`), and returns `{ signature, ...rest }` with the signature as the FIRST key, exactly
4898
+ * like the wallet. Callers that must not sign extra fields strip them first.
4899
+ */
4900
+ function signRawObject(privateKey, obj) {
4901
+ const rest = { ...obj };
4902
+ delete rest["signature"];
4903
+ return {
4904
+ signature: signChainDtoNative(privateKey, serialize(rest)),
4905
+ ...rest
4906
+ };
4907
+ }
4908
+ /**
4909
+ * Drops the `EIP712Domain` entry from a dApp-supplied types map (a typed-data encoder derives the
4910
+ * domain type itself and rejects an explicit one). The order of the remaining types is preserved.
4911
+ */
4912
+ function stripEip712DomainType(types) {
4913
+ const stripped = {};
4914
+ for (const typeName of Object.keys(types)) if (typeName !== "EIP712Domain") stripped[typeName] = types[typeName];
4915
+ return stripped;
4916
+ }
4917
+ /**
4918
+ * `collection$category$type$additionalKey` for a token-class object (wallet `decodeTokenData`).
4919
+ * Lenient on purpose: any missing or non-string part becomes `''`, so a display layer never
4920
+ * throws on odd input.
4921
+ */
4922
+ function tokenClassDollarKey(token) {
4923
+ const part = (field) => {
4924
+ const value = token === null || token === void 0 ? void 0 : token[field];
4925
+ return typeof value === "string" ? value : "";
4926
+ };
4927
+ return `${part("collection")}$${part("category")}$${part("type")}$${part("additionalKey")}`;
4928
+ }
4929
+ /**
4930
+ * A quantity in the form GalaChain's `IsBigNumber` validator accepts: FIXED notation, no leading
4931
+ * zeros, no trailing decimal zeros (`BigNumber#toFixed()`; never `toString()`, which turns
4932
+ * `0.00000001` into `1e-8`, and never a JS number in between).
4933
+ *
4934
+ * @throws GSwapSDKError (`VALIDATION_ERROR`) for a value that is not a finite number. The wallet's
4935
+ * `cleanNumber` returned the string `'NaN'` there; signing that text can only fail on chain.
4936
+ */
4937
+ function canonicalDecimalString(value) {
4938
+ let parsed = new BigNumber(NaN);
4939
+ try {
4940
+ parsed = new BigNumber(value);
4941
+ } catch {}
4942
+ if (!parsed.isFinite()) throw new GSwapSDKError(`Invalid decimal quantity: ${String(value)}`, "VALIDATION_ERROR", { type: "INVALID_DECIMAL" });
4943
+ return parsed.toFixed();
4944
+ }
4945
+
4946
+ //#endregion
4947
+ //#region src/utils/typed_data.ts
4948
+ /**
4949
+ * EIP-712 (`eth_signTypedData_v4`) hashing and signing for ARBITRARY dApp typed data, built on the
4950
+ * SDK's own primitives (`@noble/curves`, `@noble/hashes`) so a wallet can sign a dApp's typed-data
4951
+ * approval without ethers. It replaces the wallet's `new ethers.Wallet(key).signTypedData(...)`
4952
+ * and is pinned to it: the golden suite reproduces every fixture signature exactly and a seeded
4953
+ * differential fuzz compares the digest and signature with ethers for every supported type.
4954
+ *
4955
+ * Compared with ethers this encoder is deliberately STRICTER (it fails closed rather than coerce):
4956
+ * `bool` accepts only a boolean, numbers accept only safe integers / decimal or `0x` strings,
4957
+ * strings must be well-formed UTF-16, struct values must be objects whose fields are their own
4958
+ * properties, and every struct, field and type name must be a plain identifier. Anything ethers
4959
+ * accepts that is NOT in that set is refused here instead of being signed in a different shape
4960
+ * from what a user reviewed.
4961
+ */
4962
+ const textEncoder = new TextEncoder();
4963
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
4964
+ const TYPE_SYNTAX = /^([A-Za-z_$][A-Za-z0-9_$]*)((?:\[(?:0|[1-9][0-9]*)?\])*)$/u;
4965
+ const BIGINT_DECIMAL = /^-?[0-9]+$/u;
4966
+ const BIGINT_HEX = /^-?0x[0-9a-fA-F]+$/u;
4967
+ const MAX_UINT256 = (1n << 256n) - 1n;
4968
+ /** The EIP-712 domain members and their types, in the order ethers (and the spec) hash them. */
4969
+ const DOMAIN_FIELDS = [
4970
+ {
4971
+ name: "name",
4972
+ type: "string"
4973
+ },
4974
+ {
4975
+ name: "version",
4976
+ type: "string"
4977
+ },
4978
+ {
4979
+ name: "chainId",
4980
+ type: "uint256"
4981
+ },
4982
+ {
4983
+ name: "verifyingContract",
4984
+ type: "address"
4985
+ },
4986
+ {
4987
+ name: "salt",
4988
+ type: "bytes32"
4989
+ }
4990
+ ];
4991
+ function refuse(message, details) {
4992
+ throw new GSwapSDKError(`Invalid typed data: ${message}`, "INVALID_TYPED_DATA", details);
4993
+ }
4994
+ function concat(parts) {
4995
+ const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
4996
+ let offset = 0;
4997
+ for (const part of parts) {
4998
+ out.set(part, offset);
4999
+ offset += part.length;
5000
+ }
5001
+ return out;
5002
+ }
5003
+ function bigintToWord(value) {
5004
+ const word = /* @__PURE__ */ new Uint8Array(32);
5005
+ let rest = value < 0n ? value + (1n << 256n) : value;
5006
+ if (rest < 0n || rest > MAX_UINT256) return refuse("integer does not fit in 256 bits");
5007
+ for (let index = 31; index >= 0; index -= 1) {
5008
+ word[index] = Number(rest & 255n);
5009
+ rest >>= 8n;
5010
+ }
5011
+ return word;
5012
+ }
5013
+ function hexToBytes(hex) {
5014
+ const body = hex.slice(2);
5015
+ const out = new Uint8Array(body.length / 2);
5016
+ for (let index = 0; index < out.length; index += 1) out[index] = parseInt(body.slice(index * 2, index * 2 + 2), 16);
5017
+ return out;
5018
+ }
5019
+ function toBytes(value, what) {
5020
+ if (value instanceof Uint8Array) return value;
5021
+ if (typeof value === "string" && /^0x(?:[0-9a-fA-F]{2})*$/u.test(value)) return hexToBytes(value);
5022
+ return refuse(`${what} must be an even-length 0x hex string`);
5023
+ }
5024
+ function toBigInt(value, what) {
5025
+ if (typeof value === "bigint") return value;
5026
+ if (typeof value === "number") {
5027
+ if (!Number.isSafeInteger(value)) return refuse(`${what} must be a safe integer`);
5028
+ return BigInt(value);
5029
+ }
5030
+ if (typeof value === "string") {
5031
+ if (BIGINT_DECIMAL.test(value)) return BigInt(value);
5032
+ if (BIGINT_HEX.test(value)) return value.startsWith("-") ? -BigInt(value.slice(1)) : BigInt(value);
5033
+ }
5034
+ return refuse(`${what} must be an integer`);
5035
+ }
5036
+ function utf8(value, what) {
5037
+ if (typeof value !== "string") return refuse(`${what} must be a string`);
5038
+ for (let index = 0; index < value.length; index += 1) {
5039
+ const unit = value.charCodeAt(index);
5040
+ if (unit >= 55296 && unit <= 56319) {
5041
+ const next = value.charCodeAt(index + 1);
5042
+ if (!(next >= 56320 && next <= 57343)) return refuse(`${what} has an unpaired surrogate`);
5043
+ index += 1;
5044
+ } else if (unit >= 56320 && unit <= 57343) return refuse(`${what} has an unpaired surrogate`);
5045
+ }
5046
+ return textEncoder.encode(value);
5047
+ }
5048
+ /** EIP-55: a mixed-case address must carry the right checksum; all-lower / all-upper is accepted. */
5049
+ function addressBytes(value) {
5050
+ if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/u.test(value)) return refuse("address must be 0x followed by 40 hex digits");
5051
+ const digits = value.slice(2);
5052
+ if (/[A-F]/u.test(digits) && /[a-f]/u.test(digits)) {
5053
+ const hash = keccak_256(textEncoder.encode(digits.toLowerCase()));
5054
+ for (let index = 0; index < 40; index += 1) {
5055
+ const nibble = hash[index >> 1] >> (index % 2 === 0 ? 4 : 0) & 15;
5056
+ const char = digits.charAt(index);
5057
+ if (char !== (nibble >= 8 ? char.toUpperCase() : char.toLowerCase())) return refuse("address has a bad checksum");
5058
+ }
5059
+ }
5060
+ return hexToBytes(value);
5061
+ }
5062
+ /** The encoder for a non-struct, non-array type, or undefined when `base` is not one. */
5063
+ function leafEncoder(base) {
5064
+ if (base === "string") return (value) => keccak_256(utf8(value, "string"));
5065
+ if (base === "bytes") return (value) => keccak_256(toBytes(value, "bytes"));
5066
+ if (base === "bool") return (value) => {
5067
+ if (typeof value !== "boolean") return refuse("bool must be a boolean");
5068
+ return bigintToWord(value ? 1n : 0n);
5069
+ };
5070
+ if (base === "address") return (value) => concat([/* @__PURE__ */ new Uint8Array(12), addressBytes(value)]);
5071
+ const fixed = /^bytes([1-9][0-9]?)$/u.exec(base);
5072
+ if (fixed !== null) {
5073
+ const width = Number(fixed[1]);
5074
+ if (width > 32 || String(width) !== fixed[1]) return void 0;
5075
+ return (value) => {
5076
+ const bytes = toBytes(value, base);
5077
+ if (bytes.length !== width) return refuse(`${base} must be exactly ${width} bytes`);
5078
+ return concat([bytes, new Uint8Array(32 - width)]);
5079
+ };
5080
+ }
5081
+ const integer = /^(u?)int([1-9][0-9]*)$/u.exec(base);
5082
+ if (integer !== null) {
5083
+ const width = Number(integer[2]);
5084
+ if (width % 8 !== 0 || width > 256 || String(width) !== integer[2]) return void 0;
5085
+ const signed = integer[1] === "";
5086
+ const lower = signed ? -(1n << BigInt(width - 1)) : 0n;
5087
+ const upper = signed ? (1n << BigInt(width - 1)) - 1n : (1n << BigInt(width)) - 1n;
5088
+ return (value) => {
5089
+ const number = toBigInt(value, base);
5090
+ if (number < lower || number > upper) return refuse(`value out of bounds for ${base}`);
5091
+ return bigintToWord(number);
5092
+ };
5093
+ }
5094
+ }
5095
+ function parseType(type) {
5096
+ const match = TYPE_SYNTAX.exec(type);
5097
+ if (match === null) return refuse(`unsupported type "${type}"`);
5098
+ const dimensions = [...(match[2] ?? "").matchAll(/\[([0-9]*)\]/gu)].map((dimension) => dimension[1] === "" ? -1 : Number(dimension[1]));
5099
+ return {
5100
+ base: match[1],
5101
+ dimensions
5102
+ };
5103
+ }
5104
+ function parseFields(typeName, rawFields) {
5105
+ if (!Array.isArray(rawFields)) return refuse(`type ${typeName} must be a list of fields`);
5106
+ const seen = /* @__PURE__ */ new Set();
5107
+ return rawFields.map((raw) => {
5108
+ const field = raw;
5109
+ if (typeof (field === null || field === void 0 ? void 0 : field.name) !== "string" || typeof field.type !== "string") return refuse(`type ${typeName} has a field without a string name and type`);
5110
+ if (!IDENTIFIER.test(field.name)) return refuse(`invalid field name "${field.name}"`);
5111
+ if (seen.has(field.name)) return refuse(`duplicate field "${field.name}" in ${typeName}`);
5112
+ seen.add(field.name);
5113
+ const parsed = parseType(field.type);
5114
+ const base = parsed.base === "uint" || parsed.base === "int" ? `${parsed.base}256` : parsed.base;
5115
+ return {
5116
+ name: field.name,
5117
+ type: base + field.type.slice(parsed.base.length)
5118
+ };
5119
+ });
5120
+ }
5121
+ /**
5122
+ * Builds the struct encoder for a set of type definitions: validates the type graph (unknown or
5123
+ * circular references, duplicate fields) and infers the primary type as the single type no other
5124
+ * type references, exactly like ethers' `TypedDataEncoder`.
5125
+ */
5126
+ function createStructEncoder(rawTypes) {
5127
+ const names = Object.keys(rawTypes);
5128
+ const fields = /* @__PURE__ */ new Map();
5129
+ for (const name of names) {
5130
+ if (!IDENTIFIER.test(name)) return refuse(`invalid type name "${name}"`);
5131
+ fields.set(name, parseFields(name, rawTypes[name]));
5132
+ }
5133
+ const children = /* @__PURE__ */ new Map();
5134
+ const parents = /* @__PURE__ */ new Map();
5135
+ for (const name of names) {
5136
+ children.set(name, /* @__PURE__ */ new Set());
5137
+ parents.set(name, /* @__PURE__ */ new Set());
5138
+ }
5139
+ for (const [name, list] of fields) for (const field of list) {
5140
+ var _children$get, _parents$get;
5141
+ const { base } = parseType(field.type);
5142
+ if (base === name) return refuse(`circular type reference in ${name}`);
5143
+ if (leafEncoder(base) !== void 0) continue;
5144
+ if (!fields.has(base)) return refuse(`unknown type "${base}" in ${name}`);
5145
+ (_children$get = children.get(name)) === null || _children$get === void 0 || _children$get.add(base);
5146
+ (_parents$get = parents.get(base)) === null || _parents$get === void 0 || _parents$get.add(name);
5147
+ }
5148
+ const roots = names.filter((name) => {
5149
+ var _parents$get2;
5150
+ return ((_parents$get2 = parents.get(name)) === null || _parents$get2 === void 0 ? void 0 : _parents$get2.size) === 0;
5151
+ });
5152
+ if (roots.length === 0) return refuse("missing primary type");
5153
+ if (roots.length > 1) return refuse("ambiguous primary types or unused types", { roots });
5154
+ const primaryType = roots[0];
5155
+ const visiting = /* @__PURE__ */ new Set();
5156
+ const checked = /* @__PURE__ */ new Set();
5157
+ const checkCircular = (name) => {
5158
+ if (visiting.has(name)) return refuse(`circular type reference at ${name}`);
5159
+ if (checked.has(name)) return;
5160
+ visiting.add(name);
5161
+ for (const child of children.get(name) ?? []) checkCircular(child);
5162
+ visiting.delete(name);
5163
+ checked.add(name);
5164
+ };
5165
+ checkCircular(primaryType);
5166
+ const dependencies = (name, found) => {
5167
+ for (const child of children.get(name) ?? []) if (!found.has(child)) {
5168
+ found.add(child);
5169
+ dependencies(child, found);
5170
+ }
5171
+ return found;
5172
+ };
5173
+ const encodeOne = (name) => `${name}(${(fields.get(name) ?? []).map((field) => `${field.type} ${field.name}`).join(",")})`;
5174
+ const typeHashes = /* @__PURE__ */ new Map();
5175
+ for (const name of names) {
5176
+ const sorted = [...dependencies(name, /* @__PURE__ */ new Set())].filter((dep) => dep !== name).sort();
5177
+ typeHashes.set(name, keccak_256(textEncoder.encode([name, ...sorted].map(encodeOne).join(""))));
5178
+ }
5179
+ const encodeValue = (parsed, value, depth) => {
5180
+ const dimension = parsed.dimensions[parsed.dimensions.length - 1 - depth];
5181
+ if (dimension !== void 0) {
5182
+ if (!Array.isArray(value)) return refuse(`${parsed.base} array value must be an array`);
5183
+ if (dimension !== -1 && dimension !== value.length) return refuse("array length mismatch");
5184
+ return keccak_256(concat(value.map((item) => encodeValue(parsed, item, depth + 1))));
5185
+ }
5186
+ const leaf = leafEncoder(parsed.base);
5187
+ if (leaf !== void 0) return leaf(value);
5188
+ return hashStruct(parsed.base, value);
5189
+ };
5190
+ function hashStruct(typeName, value) {
5191
+ const list = fields.get(typeName);
5192
+ const typeHash = typeHashes.get(typeName);
5193
+ if (list === void 0 || typeHash === void 0) return refuse(`unknown type "${typeName}"`);
5194
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return refuse(`${typeName} value must be an object`);
5195
+ const record = value;
5196
+ const words = list.map((field) => {
5197
+ if (!Object.prototype.hasOwnProperty.call(record, field.name)) return refuse(`${typeName} value is missing field "${field.name}"`);
5198
+ return encodeValue(parseType(field.type), record[field.name], 0);
5199
+ });
5200
+ return keccak_256(concat([typeHash, ...words]));
5201
+ }
5202
+ return {
5203
+ primaryType,
5204
+ hashStruct
5205
+ };
5206
+ }
5207
+ function hashDomain(domain) {
5208
+ if (domain === null || typeof domain !== "object" || Array.isArray(domain)) return refuse("domain must be an object");
5209
+ for (const key of Object.keys(domain)) if (!DOMAIN_FIELDS.some((field) => field.name === key)) return refuse(`invalid typed-data domain key ${JSON.stringify(key)}`);
5210
+ return createStructEncoder({ EIP712Domain: DOMAIN_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(domain, field.name)) }).hashStruct("EIP712Domain", domain);
5211
+ }
5212
+ /**
5213
+ * The EIP-712 digest `keccak256(0x1901 || domainSeparator || hashStruct(primaryType, message))`
5214
+ * that a wallet signs for `eth_signTypedData_v4`. The primary type is inferred, as ethers does.
5215
+ *
5216
+ * @throws GSwapSDKError (`INVALID_TYPED_DATA`) for any type graph or value it cannot encode
5217
+ * exactly (see the module note on strictness).
5218
+ */
5219
+ function hashTypedData(payload) {
5220
+ const domainHash = hashDomain(payload.domain);
5221
+ const encoder = createStructEncoder(payload.types);
5222
+ const structHash = encoder.hashStruct(encoder.primaryType, payload.message);
5223
+ return keccak_256(concat([
5224
+ Uint8Array.of(25, 1),
5225
+ domainHash,
5226
+ structHash
5227
+ ]));
5228
+ }
5229
+ /**
5230
+ * Signs a dApp's EIP-712 typed-data approval (the wallet's `signTypedMessage` path), replacing
5231
+ * `new ethers.Wallet(key).signTypedData(domain, types, message)`. `types` must already have
5232
+ * `EIP712Domain` removed (`stripEip712DomainType`).
5233
+ *
5234
+ * @returns `0x` + r + s + v with `v` = `0x1b`/`0x1c` (27/28): ethers' serialized signature form,
5235
+ * recoverable with `verifyTypedData` / `eth_signTypedData_v4` verifiers. Rejects (never throws
5236
+ * synchronously) on invalid input.
5237
+ */
5238
+ async function signTypedDataPayload(privateKey, payload) {
5239
+ const digest = hashTypedData(payload);
5240
+ return withPrivateKey(privateKey, (key) => `0x${signHash(key, digest)}`);
5241
+ }
5242
+
5243
+ //#endregion
5244
+ //#region src/utils/axios_requestor.ts
5245
+ function isRecord$1(value) {
5246
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5247
+ }
5248
+ /**
5249
+ * An axios interceptor may resolve something that is not a response at all (for example a string
5250
+ * for ECONNREFUSED). That is a transport failure, not an HTTP answer.
5251
+ */
5252
+ function isAxiosLikeResponse(value) {
5253
+ return isRecord$1(value) && typeof value["status"] === "number";
5254
+ }
5255
+ function readAxiosHeader(headers, name) {
5256
+ if (!isRecord$1(headers)) return null;
5257
+ const getter = headers["get"];
5258
+ if (typeof getter === "function") {
5259
+ const value = getter.call(headers, name);
5260
+ return typeof value === "string" ? value : null;
5261
+ }
5262
+ const match = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase());
5263
+ return typeof (match === null || match === void 0 ? void 0 : match[1]) === "string" ? match[1] : null;
5264
+ }
5265
+ function toHttpResponse(answer, url) {
5266
+ if (!isAxiosLikeResponse(answer)) throw new GSwapSDKError("The request failed before the server answered.", "TRANSPORT_ERROR", { url });
5267
+ const { status, data, headers } = answer;
5268
+ return {
5269
+ ok: status >= 200 && status < 300,
5270
+ status,
5271
+ json: () => Promise.resolve(data),
5272
+ text: () => Promise.resolve(typeof data === "string" ? data : JSON.stringify(data)),
5273
+ headers: { get: (name) => readAxiosHeader(headers, name) }
5274
+ };
5275
+ }
5276
+ function headersFor(init, omit) {
5277
+ const omitted = new Set(omit.map((name) => name.toLowerCase()));
5278
+ const raw = init === null || init === void 0 ? void 0 : init.headers;
5279
+ const entries = raw === void 0 ? [] : raw instanceof Headers ? [...raw.entries()] : Array.isArray(raw) ? raw.map(([name, value]) => [String(name), String(value)]) : Object.entries(raw).map(([name, value]) => [name, String(value)]);
5280
+ return Object.fromEntries(entries.filter(([name]) => !omitted.has(name.toLowerCase())));
5281
+ }
5282
+ /**
5283
+ * An SDK {@link HttpRequestor} over an axios instance, so the SDK's requests travel through the
5284
+ * app's own base URL, interceptors and error handling.
5285
+ *
5286
+ * A POST's string body is parsed back to the object the SDK serialized it from (axios serializes
5287
+ * again, and an interceptor may read the object); a GET goes through `instance.get`. The answer
5288
+ * is an HTTP response whose `json()`/`text()` read `response.data`. An answer that is not an
5289
+ * axios response is a `TRANSPORT_ERROR` rejection.
5290
+ *
5291
+ * Pair it with `ChainGateway`'s `dexBackendBaseUrl: ''` when the instance owns the base URL.
5292
+ *
5293
+ * @example
5294
+ * ```ts
5295
+ * const submitGateway = new ChainGateway({
5296
+ * dexBackendBaseUrl: '',
5297
+ * httpRequestor: httpRequestorFromAxios(chainGatewayInstance, { rejectOnHttpError: true }),
5298
+ * });
5299
+ * ```
5300
+ */
5301
+ function httpRequestorFromAxios(instance, options) {
5302
+ const omit = options.omitHeaders ?? ["content-type"];
5303
+ return async (uri, init) => {
5304
+ const signal = (init === null || init === void 0 ? void 0 : init.signal) ?? void 0;
5305
+ const config = {
5306
+ headers: headersFor(init, omit),
5307
+ ...signal ? { signal } : {},
5308
+ ...options.rejectOnHttpError ? {} : { validateStatus: () => true }
5309
+ };
5310
+ if (((init === null || init === void 0 ? void 0 : init.method) ?? "GET").toUpperCase() === "POST") {
5311
+ const body = typeof (init === null || init === void 0 ? void 0 : init.body) === "string" ? JSON.parse(init.body) : void 0;
5312
+ return toHttpResponse(await instance.post(uri, body, config), uri);
5313
+ }
5314
+ return toHttpResponse(await instance.get(uri, config), uri);
5315
+ };
5316
+ }
5317
+
5318
+ //#endregion
5319
+ //#region src/utils/ethers_provider.ts
5320
+ function isRecord(value) {
5321
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5322
+ }
5323
+ function isTypedDataTypes(value) {
5324
+ return isRecord(value) && Object.values(value).every((fields) => Array.isArray(fields) && fields.every((field) => isRecord(field) && typeof field["name"] === "string" && typeof field["type"] === "string"));
5325
+ }
5326
+ function malformed(method) {
5327
+ return new GSwapSDKError(`Wallet ${method} request is malformed.`, "MALFORMED_WALLET_REQUEST", { method });
5328
+ }
5329
+ /** Splits `eth_signTypedData_v4` params (`[account, JSON payload]`) into what ethers' signer takes. */
5330
+ function readTypedDataRequest(params) {
5331
+ const raw = params === null || params === void 0 ? void 0 : params[1];
5332
+ let payload;
5333
+ try {
5334
+ payload = typeof raw === "string" ? JSON.parse(raw) : raw;
5335
+ } catch {
5336
+ throw malformed("eth_signTypedData_v4");
5337
+ }
5338
+ if (!isRecord(payload)) throw malformed("eth_signTypedData_v4");
5339
+ const { domain, types, message } = payload;
5340
+ if (!isRecord(domain) || !isTypedDataTypes(types) || !isRecord(message)) throw malformed("eth_signTypedData_v4");
5341
+ return {
5342
+ domain,
5343
+ types: stripEip712DomainType(types),
5344
+ message
5345
+ };
5346
+ }
5347
+ /**
5348
+ * An EIP-1193 provider over an ethers v6 provider, for {@link BrowserWalletSigner}: apps whose
5349
+ * wallet connection is an ethers `BrowserProvider` sign through the very ethers calls they always
5350
+ * made, with no hand-written shim.
5351
+ *
5352
+ * - `eth_chainId` is `provider.send('eth_chainId', [])`: the wallet's LIVE chain. Never
5353
+ * `provider.getNetwork()`, which ethers caches and does not invalidate on `chainChanged`, so a
5354
+ * user who switches networks after connecting would sign against a stale chainId.
5355
+ * - `eth_signTypedData_v4` parses the `[account, JSON]` params and calls
5356
+ * `signer.signTypedData(domain, types, message)` with `EIP712Domain` removed from `types`.
5357
+ * - `personal_sign` is `options.personalSign(message)` when given, else `signer.signMessage`.
5358
+ * - Anything else is `UNSUPPORTED_WALLET_METHOD`; a malformed request is
5359
+ * `MALFORMED_WALLET_REQUEST`; a missing provider is `WALLET_UNAVAILABLE`.
5360
+ *
5361
+ * `provider` may be `null`/`undefined` (nothing connected): a request that needs it then fails
5362
+ * with `WALLET_UNAVAILABLE` rather than at construction, so a signer can be built before the
5363
+ * connection state is known.
5364
+ *
5365
+ * @example
5366
+ * ```ts
5367
+ * const signer = new BrowserWalletSigner(eip1193ProviderFromEthers(browserProvider), account, {
5368
+ * scheme: 'eip712',
5369
+ * });
5370
+ * ```
5371
+ */
5372
+ function eip1193ProviderFromEthers(provider, options = {}) {
5373
+ const connected = () => {
5374
+ if (provider === null || provider === void 0) throw new GSwapSDKError(options.unavailableMessage ?? "No wallet is connected.", "WALLET_UNAVAILABLE");
5375
+ return provider;
5376
+ };
5377
+ return { request: async ({ method, params }) => {
5378
+ switch (method) {
5379
+ case "eth_chainId": return connected().send("eth_chainId", []);
5380
+ case "eth_signTypedData_v4": {
5381
+ const wallet = connected();
5382
+ const { domain, types, message } = readTypedDataRequest(params);
5383
+ return (await wallet.getSigner()).signTypedData(domain, types, message);
5384
+ }
5385
+ case "personal_sign": {
5386
+ const message = params === null || params === void 0 ? void 0 : params[0];
5387
+ if (typeof message !== "string") throw malformed("personal_sign");
5388
+ if (options.personalSign !== void 0) return options.personalSign(message);
5389
+ return (await connected().getSigner()).signMessage(message);
5390
+ }
5391
+ default: throw new GSwapSDKError(`Unsupported wallet method ${method}.`, "UNSUPPORTED_WALLET_METHOD", { method });
5392
+ }
5393
+ } };
5394
+ }
5395
+
5396
+ //#endregion
5397
+ //#region src/utils/submit_state.ts
5398
+ /** The `mode` of `envelope.data`, or `undefined` when there is no such field. */
5399
+ function modeOf(envelope) {
5400
+ const data = envelope["data"];
5401
+ return typeof data === "object" && data !== null ? data["mode"] : void 0;
5402
+ }
5403
+ function recordOf(value) {
5404
+ return typeof value === "object" && value !== null ? value : void 0;
5405
+ }
5406
+ /** True when the envelope is a definite failure: absent, or `error === true`. */
5407
+ function isSubmitRejected(envelope) {
5408
+ var _recordOf;
5409
+ if (!envelope) return true;
5410
+ return ((_recordOf = recordOf(envelope)) === null || _recordOf === void 0 ? void 0 : _recordOf["error"]) === true;
5411
+ }
5412
+ /**
5413
+ * True when the envelope carries an executed result: `error === false` and `data.mode === 'sync'`.
5414
+ * A queued RequestTrade acknowledgement is deliberately NOT executed: its outcome is not known
5415
+ * until apply-results resolves.
5416
+ */
5417
+ function isSubmitExecuted(envelope) {
5418
+ const record = recordOf(envelope);
5419
+ return (record === null || record === void 0 ? void 0 : record["error"]) === false && modeOf(record) === "sync";
5420
+ }
5421
+ /** True when the envelope acknowledges a `uniqueKey` as queued (`data.mode === 'queued'`). */
5422
+ function isSubmitQueued(envelope) {
5423
+ var _recordOf2;
5424
+ const record = recordOf(envelope);
5425
+ if ((record === null || record === void 0 ? void 0 : record["error"]) !== false || modeOf(record) !== "queued") return false;
5426
+ return typeof ((_recordOf2 = recordOf(record["data"])) === null || _recordOf2 === void 0 ? void 0 : _recordOf2["uniqueKey"]) === "string";
5427
+ }
5428
+ /** Classifies a raw gateway envelope; see {@link SubmitClassification}. */
5429
+ function classifySubmitEnvelope(envelope) {
5430
+ if (isSubmitRejected(envelope)) return "rejected";
5431
+ if (isSubmitExecuted(envelope)) return "executed";
5432
+ if (isSubmitQueued(envelope)) return "queued";
5433
+ return "unconfirmed";
5434
+ }
5435
+
5436
+ //#endregion
5437
+ export { ALL_FEE_TIERS, AppliedTradeError, Assets, BrowserWalletSigner, CanonicalSymbols, ChainGateway, DEFAULT_UNIQUE_KEY_PREFIX, DEX_FEE_METHODS, DRY_RUN_METHOD, DRY_RUN_UNIQUE_KEY_PREFIX, DexFees, EIP712_CAPABLE_METHODS, FEE_TIER, GC_MAX_TICK, GC_MIN_TICK, GSWAP_ENVIRONMENTS, GSwap, GSwapSDKError, GalaWalletSigner, GdexStreamError, GdexStreamService, PENDING_SWAPS_STORAGE_KEY, PENDING_SWAP_EXPIRY_MS, PendingSwapStore, Pools, Positions, PrivateKeySigner, Quoting, SubmittedTransaction, Swaps, Symbols, TICK_SPACING_BY_FEE, alignTickDown, alignTickUp, assertCanonicalPair, assertCanonicalV2Dto, assertTickRange, assertUniqueKey, buildAddLiquidityDto, buildCollectPositionFeesDto, buildCreatePoolDto, buildDryRunWrapper, buildEIP712Payload, buildRemoveLiquidityDto, buildRequestTradeDto, calculatePersonalSignPrefix, canonicalDecimalString, classifyShadowSubmitError, classifyShadowSubmitStatus, classifySubmitEnvelope, compositeKeyOf, confirmAppliedTrade, createLazyPrivateKeySigner, createUniqueKey, eip1193ProviderFromEthers, eip712DomainFor, ethAddressOfPrivateKey, ethereumAddressOfAccount, fixpointPrefixFor, frameSlot0, generateEIP712Types, getObjectProperty, getPayloadToSign, getStringProperty, hashTypedData, httpRequestorFromAxios, isCurrentContractDexMessage, isDexFeeMethod, isGdexLiquidityEvent, isProposableTradingSymbol, isSubmitExecuted, isSubmitQueued, isSubmitRejected, normalizePrivateKey, orderSymbols, parseJson, parseTokenClassKey, personalSignPrefixFor, queryTokenOf, rawTokenOf, readOrderedSymbols, readShadowSubmitOutcome, resolvePersonalSignChainDtoPayload, resolveUniqueKeyPrefix, serialize, signChainDtoHash, signChainDtoNative, signChainDtoPersonalSign, signNative, signPersonalMessage, signRawObject, signSerializedNative, signTypedDataPayload, sqrtPriceToTick, stripEip712DomainType, swapRouteFromQuote, tickFromPrice, tickToSqrtPrice, toChainDto, tokenClassDollarKey, tradingSymbolOf, validateFee, validateNumericAmount, validatePriceValues, validateTickRange, validateTickSpacing, validateTokenDecimals, validateWalletAddress, withPrivateKey };
2833
5438
  //# sourceMappingURL=index.mjs.map