@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/README.md +14 -5
- package/dist/browser/index.js +4 -3
- package/dist/browser/index.js.map +1 -1
- package/dist/index.cjs +3282 -610
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2021 -471
- package/dist/index.d.ts +2021 -471
- package/dist/index.mjs +3213 -608
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -6
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
|
|
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
|
-
/**
|
|
60
|
-
|
|
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/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
return
|
|
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
|
-
/**
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
/**
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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") ??
|
|
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
|
|
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:
|
|
1109
|
-
unsubscribe: async () => this.unsubscribe(
|
|
1683
|
+
subscriptionId: "",
|
|
1684
|
+
unsubscribe: async () => this.unsubscribe(subscription)
|
|
1110
1685
|
};
|
|
1111
|
-
|
|
1112
|
-
serverId:
|
|
1686
|
+
const subscription = {
|
|
1687
|
+
serverId: "",
|
|
1113
1688
|
params,
|
|
1114
1689
|
callback,
|
|
1115
1690
|
handle,
|
|
1116
1691
|
active: true
|
|
1117
1692
|
};
|
|
1118
|
-
this.
|
|
1119
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
this.
|
|
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.
|
|
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
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1974
|
+
options;
|
|
1975
|
+
cachedSymbols;
|
|
1976
|
+
cachedAt = 0;
|
|
1977
|
+
refreshInFlight;
|
|
1978
|
+
http;
|
|
1343
1979
|
/**
|
|
1344
|
-
*
|
|
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,
|
|
1983
|
+
constructor(gateway, options = {}) {
|
|
1352
1984
|
this.gateway = gateway;
|
|
1353
|
-
this.
|
|
1354
|
-
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs
|
|
1985
|
+
this.options = options;
|
|
1986
|
+
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs, gateway.wire);
|
|
1355
1987
|
}
|
|
1356
|
-
http;
|
|
1357
1988
|
/**
|
|
1358
|
-
*
|
|
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
|
-
*
|
|
1363
|
-
* console.log(pools.length);
|
|
2009
|
+
* gSwap.symbols.invalidate();
|
|
1364
2010
|
* ```
|
|
1365
2011
|
*/
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
return response.data;
|
|
2012
|
+
invalidate() {
|
|
2013
|
+
this.cachedSymbols = void 0;
|
|
2014
|
+
this.cachedAt = 0;
|
|
1370
2015
|
}
|
|
1371
|
-
/**
|
|
1372
|
-
*
|
|
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
|
-
|
|
1409
|
-
|
|
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
|
|
1420
|
-
token1: await
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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:
|
|
1559
|
-
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:
|
|
1584
|
-
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
|
|
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
|
|
1690
|
-
|
|
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
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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:
|
|
1796
|
-
token1:
|
|
1797
|
-
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
3415
|
+
* const quoting = new Quoting(gateway, symbols); // a ChainGateway, or any { dexBackendBaseUrl, httpRequestor }
|
|
2004
3416
|
* ```
|
|
2005
3417
|
*/
|
|
2006
|
-
constructor(
|
|
2007
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
2041
|
-
tokenOut: await
|
|
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 ||
|
|
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
|
|
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/
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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
|
|
2210
|
-
|
|
2211
|
-
|
|
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
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
|
2232
|
-
return
|
|
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
|
-
/**
|
|
2238
|
-
|
|
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
|
|
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
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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('
|
|
3800
|
+
* const applied = await swaps.confirmSwap('galaswap-operation-2f1c…');
|
|
2290
3801
|
* ```
|
|
2291
3802
|
*/
|
|
2292
3803
|
async confirmSwap(uniqueKey, options) {
|
|
2293
|
-
|
|
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
|
-
|
|
2391
|
-
this.
|
|
2392
|
-
|
|
2393
|
-
this.
|
|
2394
|
-
this.
|
|
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
|
-
/**
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
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/
|
|
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
|
-
/**
|
|
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
|
-
|
|
2597
|
-
|
|
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
|
|
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
|
|
4475
|
+
return normalized;
|
|
2621
4476
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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
|
-
|
|
2632
|
-
|
|
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
|
-
|
|
4525
|
+
serialize(payload),
|
|
2642
4526
|
walletAddress,
|
|
2643
4527
|
methodName
|
|
2644
|
-
] : [
|
|
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
|
|
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
|
|
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
|
-
|
|
4574
|
+
const value = await provider.request({
|
|
2687
4575
|
method: "eth_signTypedData_v4",
|
|
2688
4576
|
params: [walletAddress, JSON.stringify(payload)]
|
|
2689
|
-
})
|
|
4577
|
+
});
|
|
4578
|
+
return {
|
|
2690
4579
|
...message,
|
|
4580
|
+
signature: getPersonalSignature(value),
|
|
2691
4581
|
types,
|
|
2692
|
-
domain
|
|
2693
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
*
|
|
2738
|
-
*
|
|
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
|
-
*
|
|
2741
|
-
*
|
|
2742
|
-
* `
|
|
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
|
|
2748
|
-
|
|
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
|
|
2759
|
-
* `
|
|
2760
|
-
*
|
|
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 =
|
|
2764
|
-
this.
|
|
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
|
-
|
|
2782
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|