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