@gala-chain/gswap-sdk 1.1.0 → 1.2.0-rc.1
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 +2995 -600
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1806 -451
- package/dist/index.d.ts +1806 -451
- package/dist/index.mjs +2933 -598
- 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,280 @@ function parseJson(text) {
|
|
|
144
267
|
}
|
|
145
268
|
}
|
|
146
269
|
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/utils/transport.ts
|
|
272
|
+
/**
|
|
273
|
+
* Arms the abort timer for a request. `Infinity` means "never time out": `setTimeout` would turn
|
|
274
|
+
* it into 1 ms, so no timer is armed at all. Returns a function that disarms it.
|
|
275
|
+
*/
|
|
276
|
+
function armTimeout(controller, timeoutMs) {
|
|
277
|
+
if (timeoutMs === Infinity) return () => void 0;
|
|
278
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
279
|
+
return () => clearTimeout(timer);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Validates a request timeout in milliseconds: a positive finite number, or `Infinity` for none.
|
|
283
|
+
*
|
|
284
|
+
* @throws GSwapSDKError VALIDATION_ERROR otherwise.
|
|
285
|
+
*/
|
|
286
|
+
function validateTimeoutMs(timeoutMs, name) {
|
|
287
|
+
if (timeoutMs === Infinity || Number.isFinite(timeoutMs) && timeoutMs > 0) return timeoutMs;
|
|
288
|
+
throw new GSwapSDKError(`${name} must be a positive number of milliseconds, or Infinity for no timeout.`, "VALIDATION_ERROR", { [name]: timeoutMs });
|
|
289
|
+
}
|
|
290
|
+
const READABLE_QUERY_SAFE = /[A-Za-z0-9\-._~!'()*$|:,]/u;
|
|
291
|
+
function encodeReadable(text) {
|
|
292
|
+
let out = "";
|
|
293
|
+
for (const char of text) if (char === " ") out += "+";
|
|
294
|
+
else if (READABLE_QUERY_SAFE.test(char)) out += char;
|
|
295
|
+
else out += encodeScalar(char);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
/** Percent-encodes one code point; a lone surrogate becomes U+FFFD, as `URLSearchParams` does. */
|
|
299
|
+
function encodeScalar(char) {
|
|
300
|
+
try {
|
|
301
|
+
return encodeURIComponent(char);
|
|
302
|
+
} catch {
|
|
303
|
+
return "%EF%BF%BD";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Serializes GET parameters, in insertion order, in the encoding the wire options select. */
|
|
307
|
+
function buildQueryString(params, encoding = "standard") {
|
|
308
|
+
if (encoding === "readable") return Object.entries(params).map(([key, value]) => `${encodeReadable(key)}=${encodeReadable(value)}`).join("&");
|
|
309
|
+
return new URLSearchParams(params).toString();
|
|
310
|
+
}
|
|
311
|
+
async function requestWithTimeout(requestor, url, options, timeoutMs) {
|
|
312
|
+
const controller = new AbortController();
|
|
313
|
+
const disarm = armTimeout(controller, timeoutMs);
|
|
314
|
+
const externalSignal = options.signal;
|
|
315
|
+
const abortFromCaller = () => controller.abort();
|
|
316
|
+
externalSignal === null || externalSignal === void 0 || externalSignal.addEventListener("abort", abortFromCaller, { once: true });
|
|
317
|
+
try {
|
|
318
|
+
return await requestor(url, {
|
|
319
|
+
...options,
|
|
320
|
+
signal: controller.signal
|
|
321
|
+
});
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (controller.signal.aborted) throw new GSwapSDKError(`Request timed out after ${timeoutMs} ms.`, "REQUEST_TIMEOUT", {
|
|
324
|
+
url,
|
|
325
|
+
timeoutMs,
|
|
326
|
+
cause: error
|
|
327
|
+
});
|
|
328
|
+
throw error;
|
|
329
|
+
} finally {
|
|
330
|
+
disarm();
|
|
331
|
+
externalSignal === null || externalSignal === void 0 || externalSignal.removeEventListener("abort", abortFromCaller);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function readResponseBody(response) {
|
|
335
|
+
const text = await response.text();
|
|
336
|
+
return parseJson(text) ?? text;
|
|
337
|
+
}
|
|
338
|
+
/** Read a case-insensitive response header from either supported header shape. */
|
|
339
|
+
function readHeader(response, name) {
|
|
340
|
+
var _Object$entries$find;
|
|
341
|
+
const headers = response.headers;
|
|
342
|
+
if (headers === void 0) return void 0;
|
|
343
|
+
if (hasHeaderGetter(headers)) {
|
|
344
|
+
const value = headers.get(name);
|
|
345
|
+
return value === null || value === "" ? void 0 : value;
|
|
346
|
+
}
|
|
347
|
+
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];
|
|
348
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
349
|
+
}
|
|
350
|
+
/** Read a Retry-After header (delta-seconds or an HTTP date) as non-negative milliseconds. */
|
|
351
|
+
function readRetryAfter(response) {
|
|
352
|
+
const raw = readHeader(response, "retry-after");
|
|
353
|
+
if (raw === void 0) return void 0;
|
|
354
|
+
const seconds = Number(raw);
|
|
355
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
356
|
+
const timestamp = Date.parse(raw);
|
|
357
|
+
return Number.isNaN(timestamp) ? void 0 : Math.max(0, timestamp - Date.now());
|
|
358
|
+
}
|
|
359
|
+
/** Read a non-negative integer block number header. */
|
|
360
|
+
function readBlockNumberHeader(response, headerName = "x-block-number") {
|
|
361
|
+
const raw = readHeader(response, headerName);
|
|
362
|
+
if (raw === void 0) return void 0;
|
|
363
|
+
const blockNumber = Number(raw);
|
|
364
|
+
return Number.isInteger(blockNumber) && blockNumber >= 0 ? blockNumber : void 0;
|
|
365
|
+
}
|
|
366
|
+
function hasHeaderGetter(headers) {
|
|
367
|
+
return "get" in headers && typeof headers.get === "function";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/classes/apply_results.ts
|
|
372
|
+
/** The apply-results key constraint of the swap backend (`apply-results.dto.ts`). */
|
|
373
|
+
const UNIQUE_KEY_PATTERN$1 = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
374
|
+
/**
|
|
375
|
+
* The AutoApplier waits at least 2 s before applying and the apply lands ~6-8 s after the
|
|
376
|
+
* acknowledgement, so a read at t=0 is a guaranteed 404.
|
|
377
|
+
*/
|
|
378
|
+
const DEFAULT_INITIAL_DELAY_MS = 3e3;
|
|
379
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
380
|
+
/**
|
|
381
|
+
* The outcome of a queued RequestTrade that could not be delivered as an {@link AppliedTrade}.
|
|
382
|
+
* `code` says why:
|
|
383
|
+
*
|
|
384
|
+
* - `CONFIRMATION_TIMEOUT`: no terminal result before the deadline. The trade may still apply;
|
|
385
|
+
* confirm the same `uniqueKey` again later and never resubmit.
|
|
386
|
+
* - `CONFIRMATION_ABORTED`: the caller's `signal` aborted. Nothing is known about the outcome.
|
|
387
|
+
* - `CONFIRMATION_FAILED`: the apply-results read itself failed (malformed key or body, a
|
|
388
|
+
* non-retryable HTTP status). The outcome is unknown.
|
|
389
|
+
* - anything else: the chain REJECTED the trade; `code` is the chain `ErrorKey` (or
|
|
390
|
+
* `CHAIN_ERROR`) and `chainMessage` is its message, which is also `message`.
|
|
391
|
+
*
|
|
392
|
+
* `details` always carries `uniqueKey` and, where known, `requestTransactionId`,
|
|
393
|
+
* `applyTransactionId`, `blockNumber`, `status`, `body`, `url` and the last transport `cause`.
|
|
394
|
+
*/
|
|
395
|
+
var AppliedTradeError = class extends GSwapSDKError {
|
|
396
|
+
constructor(message, code, details = {}, chainMessage) {
|
|
397
|
+
super(message, code, details, chainMessage);
|
|
398
|
+
this.name = "AppliedTradeError";
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Polls `GET /v1/chain/apply-results/{uniqueKey}?channel=asset&contract=dex-contract` until the
|
|
403
|
+
* queued RequestTrade reaches a terminal state.
|
|
404
|
+
*
|
|
405
|
+
* Pacing: wait `initialDelayMs` (default 3 s), then read; on 404, 429, 500, 502, 503, 504 or a
|
|
406
|
+
* transport error wait with jittered exponential backoff (`pollIntervalMs` doubling to 8 s,
|
|
407
|
+
* `Retry-After` honored as seconds or an HTTP date) until `timeoutMs`. Any other status is
|
|
408
|
+
* terminal. At least one read is always made.
|
|
409
|
+
*
|
|
410
|
+
* @returns The applied trade.
|
|
411
|
+
* @throws AppliedTradeError - see its documentation for every `code`.
|
|
412
|
+
* @example
|
|
413
|
+
* ```ts
|
|
414
|
+
* const applied = await confirmAppliedTrade(
|
|
415
|
+
* { dexBackendBaseUrl: 'https://dex-backend-prod1.defi.gala.com' },
|
|
416
|
+
* { uniqueKey, requestTransactionId: null },
|
|
417
|
+
* { initialDelayMs: 0, timeoutMs: 60_000, signal: controller.signal },
|
|
418
|
+
* );
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
async function confirmAppliedTrade(client, input, options) {
|
|
422
|
+
const { uniqueKey } = input;
|
|
423
|
+
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 });
|
|
424
|
+
const requestor = client.httpRequestor ?? fetch.bind(globalThis);
|
|
425
|
+
const requestTimeoutMs = client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
426
|
+
const signal = options === null || options === void 0 ? void 0 : options.signal;
|
|
427
|
+
const abortError = (cause) => new AppliedTradeError("Transaction confirmation was aborted.", "CONFIRMATION_ABORTED", {
|
|
428
|
+
uniqueKey,
|
|
429
|
+
...cause === void 0 ? {} : { cause }
|
|
430
|
+
});
|
|
431
|
+
const url = `${client.dexBackendBaseUrl.replace(/\/$/u, "")}/v1/chain/apply-results/${encodeURIComponent(uniqueKey)}?channel=asset&contract=dex-contract`;
|
|
432
|
+
const schedule = createConfirmationSchedule(options, DEFAULT_INITIAL_DELAY_MS, abortError);
|
|
433
|
+
await schedule.waitForRead();
|
|
434
|
+
let lastError;
|
|
435
|
+
while (true) {
|
|
436
|
+
throwIfAborted(signal, abortError);
|
|
437
|
+
let response;
|
|
438
|
+
let body;
|
|
439
|
+
try {
|
|
440
|
+
response = await requestWithTimeout(requestor, url, {
|
|
441
|
+
method: "GET",
|
|
442
|
+
...signal === void 0 ? {} : { signal }
|
|
443
|
+
}, requestTimeoutMs);
|
|
444
|
+
body = await readResponseBody(response);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
throwIfAborted(signal, abortError);
|
|
447
|
+
lastError = error;
|
|
448
|
+
if (!await schedule.waitForRead()) break;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
throwIfAborted(signal, abortError);
|
|
452
|
+
if (response.status === 200) return readAppliedResult(input, response, body, url);
|
|
453
|
+
if (response.status === 404 || response.status === 429 || isTransient(response.status)) {
|
|
454
|
+
const retryAfter = response.status === 429 ? readRetryAfter(response) : void 0;
|
|
455
|
+
if (!await schedule.waitForRead(retryAfter)) break;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
throw new AppliedTradeError(bodyMessage(body), "CONFIRMATION_FAILED", {
|
|
459
|
+
uniqueKey,
|
|
460
|
+
status: response.status,
|
|
461
|
+
body,
|
|
462
|
+
url
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
throw new AppliedTradeError("Transaction confirmation timed out; the trade may still apply. Confirm the same uniqueKey again later instead of resubmitting.", "CONFIRMATION_TIMEOUT", {
|
|
466
|
+
uniqueKey,
|
|
467
|
+
requestTransactionId: input.requestTransactionId,
|
|
468
|
+
url,
|
|
469
|
+
...lastError === void 0 ? {} : { cause: lastError }
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function readAppliedResult(input, response, body, url) {
|
|
473
|
+
const { uniqueKey } = input;
|
|
474
|
+
const record = asRecord$2(body);
|
|
475
|
+
const responseKey = (record === null || record === void 0 ? void 0 : record["uniqueKey"]) ?? (record === null || record === void 0 ? void 0 : record["UniqueKey"]);
|
|
476
|
+
if (typeof responseKey === "string" && responseKey !== uniqueKey) throw malformed(input, response, body, url, "Apply result key did not match the requested key.");
|
|
477
|
+
const status = record === null || record === void 0 ? void 0 : record["Status"];
|
|
478
|
+
const applyTransactionId = readHeader(response, "x-apply-transaction-id") ?? null;
|
|
479
|
+
const blockNumber = readBlockNumberHeader(response, "x-apply-block-number") ?? null;
|
|
480
|
+
if (status === 0) {
|
|
481
|
+
const errorKey = getStringProperty(record, "ErrorKey") ?? "CHAIN_ERROR";
|
|
482
|
+
const chainMessage = getStringProperty(record, "Message") ?? "The chain rejected this trade.";
|
|
483
|
+
throw new AppliedTradeError(chainMessage, errorKey, {
|
|
484
|
+
uniqueKey,
|
|
485
|
+
requestTransactionId: input.requestTransactionId,
|
|
486
|
+
applyTransactionId,
|
|
487
|
+
blockNumber,
|
|
488
|
+
status: response.status,
|
|
489
|
+
body,
|
|
490
|
+
url,
|
|
491
|
+
errorKey,
|
|
492
|
+
message: chainMessage
|
|
493
|
+
}, chainMessage);
|
|
494
|
+
}
|
|
495
|
+
if (status !== 1) throw malformed(input, response, body, url);
|
|
496
|
+
const result = parseTradeExecution(record === null || record === void 0 ? void 0 : record["Data"]);
|
|
497
|
+
if (result === void 0) throw malformed(input, response, body, url);
|
|
498
|
+
return {
|
|
499
|
+
uniqueKey,
|
|
500
|
+
transactionId: applyTransactionId,
|
|
501
|
+
blockNumber,
|
|
502
|
+
requestTransactionId: input.requestTransactionId,
|
|
503
|
+
result
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function malformed(input, response, body, url, message = "Transaction confirmation returned a malformed body") {
|
|
507
|
+
return new AppliedTradeError(message, "CONFIRMATION_FAILED", {
|
|
508
|
+
uniqueKey: input.uniqueKey,
|
|
509
|
+
status: response.status,
|
|
510
|
+
body,
|
|
511
|
+
url
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function parseTradeExecution(value) {
|
|
515
|
+
const record = asRecord$2(value);
|
|
516
|
+
if (record === void 0 || typeof record["token0"] !== "string" || typeof record["token1"] !== "string" || typeof record["fee"] !== "number") return;
|
|
517
|
+
const optionalFields = [
|
|
518
|
+
"sell0Qty",
|
|
519
|
+
"sell1Qty",
|
|
520
|
+
"buy0Qty",
|
|
521
|
+
"buy1Qty",
|
|
522
|
+
"tradingFees",
|
|
523
|
+
"protocolFees",
|
|
524
|
+
"totalFees"
|
|
525
|
+
];
|
|
526
|
+
for (const field of optionalFields) if (field in record && typeof record[field] !== "string") return void 0;
|
|
527
|
+
return {
|
|
528
|
+
token0: record["token0"],
|
|
529
|
+
token1: record["token1"],
|
|
530
|
+
fee: record["fee"],
|
|
531
|
+
...Object.fromEntries(optionalFields.flatMap((field) => typeof record[field] === "string" ? [[field, record[field]]] : []))
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function isTransient(status) {
|
|
535
|
+
return status === 500 || status === 502 || status === 503 || status === 504;
|
|
536
|
+
}
|
|
537
|
+
function bodyMessage(body) {
|
|
538
|
+
return getStringProperty(body, "message") ?? getStringProperty(body, "Message") ?? getStringProperty(getObjectProperty(body, "error"), "message") ?? getStringProperty(getObjectProperty(body, "error"), "Message") ?? "Transaction confirmation failed";
|
|
539
|
+
}
|
|
540
|
+
function asRecord$2(value) {
|
|
541
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
147
544
|
//#endregion
|
|
148
545
|
//#region src/types/fees.ts
|
|
149
546
|
/**
|
|
@@ -236,6 +633,33 @@ function assertTickRange(lower, upper, fee) {
|
|
|
236
633
|
function assertSpacing(spacing) {
|
|
237
634
|
if (!Number.isInteger(spacing) || spacing <= 0) throw new RangeError("Tick spacing must be positive");
|
|
238
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Puts a `slot0` answer into the frame of the request. The backend always answers in the pool's
|
|
638
|
+
* canonical order and says so with `flippedFromRequest`; when the caller asked for the reverse
|
|
639
|
+
* order the price is inverted here (at 60 significant digits, so a very large or very small
|
|
640
|
+
* reciprocal keeps its precision) and the tick is re-derived from the inverted price with the
|
|
641
|
+
* contract's greatest-tick rule. Negating the tick instead is wrong for a price between ticks
|
|
642
|
+
* (and `-t - 1` is wrong for a price exactly on a tick: sqrtPrice 1 / tick 0 stays 1 / 0).
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```ts
|
|
646
|
+
* const slot0 = await gSwap.pools.getSlot0('A', 'B', 500);
|
|
647
|
+
* const { sqrtPrice, tick } = frameSlot0(slot0);
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
function frameSlot0(slot0) {
|
|
651
|
+
if (slot0.flippedFromRequest !== true) return {
|
|
652
|
+
sqrtPrice: slot0.sqrtPrice,
|
|
653
|
+
tick: slot0.tick
|
|
654
|
+
};
|
|
655
|
+
const inverted = new DexDecimal(1).dividedBy(new DexDecimal(slot0.sqrtPrice));
|
|
656
|
+
if (!inverted.isFinite() || inverted.lessThanOrEqualTo(0)) throw new GSwapSDKError("Cannot invert a non-positive sqrt price.", "VALIDATION_ERROR", { sqrtPrice: slot0.sqrtPrice });
|
|
657
|
+
const sqrtPrice = inverted.toString();
|
|
658
|
+
return {
|
|
659
|
+
sqrtPrice,
|
|
660
|
+
tick: sqrtPriceToTick(sqrtPrice)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
239
663
|
|
|
240
664
|
//#endregion
|
|
241
665
|
//#region src/utils/validation.ts
|
|
@@ -351,6 +775,36 @@ function validateWalletAddress(address) {
|
|
|
351
775
|
value: address
|
|
352
776
|
});
|
|
353
777
|
}
|
|
778
|
+
/** Words the chain gateway refuses as a trading symbol. */
|
|
779
|
+
const RESERVED_TRADING_SYMBOLS = /* @__PURE__ */ new Set([
|
|
780
|
+
"Token",
|
|
781
|
+
"Unit",
|
|
782
|
+
"none"
|
|
783
|
+
]);
|
|
784
|
+
const TRADING_SYMBOL_PATTERN = /^[A-Za-z0-9]+$/u;
|
|
785
|
+
const MAX_TRADING_SYMBOL_LENGTH = 32;
|
|
786
|
+
/**
|
|
787
|
+
* True when `symbol` is acceptable to propose into the trading-symbol registry: ASCII letters and
|
|
788
|
+
* digits only, 1 to 32 characters, and not a reserved word (`Token`, `Unit`, `none`) — the rules
|
|
789
|
+
* the chain gateway enforces on a CreatePool symbol.
|
|
790
|
+
*/
|
|
791
|
+
function isProposableTradingSymbol(symbol) {
|
|
792
|
+
return TRADING_SYMBOL_PATTERN.test(symbol) && symbol.length <= MAX_TRADING_SYMBOL_LENGTH && !RESERVED_TRADING_SYMBOLS.has(symbol);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Asserts that a pair is in the contract's canonical order (`token0 < token1` by plain string
|
|
796
|
+
* comparison), which every write DTO requires. A pair a caller took from a stale quote or
|
|
797
|
+
* position can be reversed; signing it would sign something other than what was priced.
|
|
798
|
+
*
|
|
799
|
+
* @throws GSwapSDKError VALIDATION_ERROR (`details.type` = `NON_CANONICAL_PAIR`) otherwise.
|
|
800
|
+
*/
|
|
801
|
+
function assertCanonicalPair(token0, token1) {
|
|
802
|
+
if (!(token0 < token1)) throw new GSwapSDKError(`The pair ${token0}/${token1} is not in canonical order: token0 must sort before token1.`, "VALIDATION_ERROR", {
|
|
803
|
+
type: "NON_CANONICAL_PAIR",
|
|
804
|
+
token0,
|
|
805
|
+
token1
|
|
806
|
+
});
|
|
807
|
+
}
|
|
354
808
|
|
|
355
809
|
//#endregion
|
|
356
810
|
//#region src/classes/assets.ts
|
|
@@ -411,62 +865,139 @@ function isRawUserAsset(value) {
|
|
|
411
865
|
}
|
|
412
866
|
|
|
413
867
|
//#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
|
-
}
|
|
437
|
-
}
|
|
438
|
-
async function readResponseBody(response) {
|
|
439
|
-
const text = await response.text();
|
|
440
|
-
return parseJson(text) ?? text;
|
|
868
|
+
//#region src/classes/dex_fees.ts
|
|
869
|
+
/** DEX methods the backend accepts a fee quote for (its allow-list enumerates the same six). */
|
|
870
|
+
const DEX_FEE_METHODS = [
|
|
871
|
+
"Swap",
|
|
872
|
+
"RequestTrade",
|
|
873
|
+
"AddLiquidity",
|
|
874
|
+
"RemoveLiquidity",
|
|
875
|
+
"CollectPositionFees",
|
|
876
|
+
"CreatePool"
|
|
877
|
+
];
|
|
878
|
+
/** The chain method name the signed DryRun wrapper is signed under. */
|
|
879
|
+
const DRY_RUN_METHOD = "DryRun";
|
|
880
|
+
/** `uniqueKey` prefix of the DryRun WRAPPER (distinct from the inner DTO's key). */
|
|
881
|
+
const DRY_RUN_UNIQUE_KEY_PREFIX = "galaconnect-operation-";
|
|
882
|
+
/** True when `method` is a DEX method a fee can be quoted for. */
|
|
883
|
+
function isDexFeeMethod(method) {
|
|
884
|
+
return typeof method === "string" && DEX_FEE_METHODS.includes(method);
|
|
441
885
|
}
|
|
442
|
-
/**
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
const headers = response.headers;
|
|
446
|
-
if (headers === void 0) return void 0;
|
|
447
|
-
if (hasHeaderGetter(headers)) {
|
|
448
|
-
const value = headers.get(name);
|
|
449
|
-
return value === null || value === "" ? void 0 : value;
|
|
450
|
-
}
|
|
451
|
-
const value = (_Object$entries$find = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())) === null || _Object$entries$find === void 0 ? void 0 : _Object$entries$find[1];
|
|
452
|
-
return value === void 0 || value === "" ? void 0 : value;
|
|
886
|
+
/** Current-contract typed DEX messages use trading symbols instead of TokenClassKey objects. */
|
|
887
|
+
function isCurrentContractDexMessage(message, method) {
|
|
888
|
+
return isDexFeeMethod(method) && typeof (message === null || message === void 0 ? void 0 : message["token0"]) === "string" && typeof message["token1"] === "string";
|
|
453
889
|
}
|
|
454
|
-
/**
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
890
|
+
/**
|
|
891
|
+
* Reduces an EIP-712 approval `message` to the DTO the chain actually receives: drops the signing
|
|
892
|
+
* envelope (`signature`, `prefix`, `types`, `domain`) and transport metadata (`trace`).
|
|
893
|
+
*/
|
|
894
|
+
function toChainDto(message) {
|
|
895
|
+
const chainDto = { ...message };
|
|
896
|
+
for (const field of [
|
|
897
|
+
"signature",
|
|
898
|
+
"prefix",
|
|
899
|
+
"types",
|
|
900
|
+
"domain",
|
|
901
|
+
"trace"
|
|
902
|
+
]) delete chainDto[field];
|
|
903
|
+
return chainDto;
|
|
460
904
|
}
|
|
461
|
-
/**
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
905
|
+
/**
|
|
906
|
+
* Signs the DryRun WRAPPER the gateway accepts for every contract:
|
|
907
|
+
* `{ callerPublicKey, method, dto: <inner DTO object>, uniqueKey, signature }`. The inner DTO
|
|
908
|
+
* travels unsigned and as an object; no `signerPublicKey` is sent because the signature already
|
|
909
|
+
* recovers it (the gateway rejects it as REDUNDANT_SIGNER_PUBLIC_KEY). The wrapper carries its own
|
|
910
|
+
* `uniqueKey`, distinct from the DTO's.
|
|
911
|
+
*/
|
|
912
|
+
function buildDryRunWrapper(signer, callerPublicKey, method, dto, uniqueKeyPrefix = DRY_RUN_UNIQUE_KEY_PREFIX) {
|
|
913
|
+
return signer.signObject(DRY_RUN_METHOD, {
|
|
914
|
+
callerPublicKey,
|
|
915
|
+
method,
|
|
916
|
+
dto,
|
|
917
|
+
uniqueKey: `${uniqueKeyPrefix}${globalThis.crypto.randomUUID()}`
|
|
918
|
+
});
|
|
467
919
|
}
|
|
468
|
-
|
|
469
|
-
|
|
920
|
+
/**
|
|
921
|
+
* Network-fee quote for GalaChain DEX methods, derived from a server-side DryRun.
|
|
922
|
+
*
|
|
923
|
+
* The client only SIGNS the DryRun wrapper (with whatever {@link GalaChainSigner} it is given) and
|
|
924
|
+
* the backend forwards it to the gateway, where the simulated writes carry the fee; the backend
|
|
925
|
+
* never holds a key. Intended for wallets: the DTO quoted is the typed message a dApp handed over.
|
|
926
|
+
*
|
|
927
|
+
* @example
|
|
928
|
+
* ```ts
|
|
929
|
+
* const fees = new DexFees({ dexBackendBaseUrl }, new PrivateKeySigner(sessionKey));
|
|
930
|
+
* const quote = await fees.quote(publicKey, 'RequestTrade', toChainDto(message), {
|
|
931
|
+
* contract: 'dex-contract',
|
|
932
|
+
* });
|
|
933
|
+
* const feeInGala = quote?.fees[0]?.feeInGala;
|
|
934
|
+
* ```
|
|
935
|
+
*/
|
|
936
|
+
var DexFees = class {
|
|
937
|
+
signer;
|
|
938
|
+
requestor;
|
|
939
|
+
baseUrl;
|
|
940
|
+
requestTimeoutMs;
|
|
941
|
+
constructor(client, signer) {
|
|
942
|
+
this.signer = signer;
|
|
943
|
+
this.requestor = client.httpRequestor ?? fetch.bind(globalThis);
|
|
944
|
+
this.baseUrl = client.dexBackendBaseUrl.replace(/\/$/u, "");
|
|
945
|
+
this.requestTimeoutMs = client.requestTimeoutMs ?? 3e4;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Quotes the network fee for `method` applied to `message`. The message is reduced with
|
|
949
|
+
* {@link toChainDto} first, so an EIP-712 approval message can be passed as is.
|
|
950
|
+
*
|
|
951
|
+
* Resolves `null` when the BACKEND could not be reached or answered unusably (offline, 4xx/5xx,
|
|
952
|
+
* malformed body): a transport outcome, not a fee. Deciding what to charge when the chain is
|
|
953
|
+
* silent is the backend's job (it serves `source: 'default'`), so the client never substitutes
|
|
954
|
+
* a fee of its own, with ONE exception that mirrors the wallet: a `dex-contract` quote answered
|
|
955
|
+
* 403 or 404 means the contract has no fee route yet and resolves `{ source: 'default', '0' }`.
|
|
956
|
+
* Never throws on transport, so a fee lookup cannot block or break a signing screen.
|
|
957
|
+
*
|
|
958
|
+
* @throws GSwapSDKError NO_SIGNER when constructed without a signer; any error the signer
|
|
959
|
+
* throws (for example a locked wallet) propagates unchanged.
|
|
960
|
+
*/
|
|
961
|
+
async quote(callerPublicKey, method, message, options = {}) {
|
|
962
|
+
if (this.signer === void 0) throw GSwapSDKError.noSignerError();
|
|
963
|
+
const channel = options.channel ?? "asset";
|
|
964
|
+
const contract = options.contract ?? "dexv3-contract";
|
|
965
|
+
const wrapper = await buildDryRunWrapper(this.signer, callerPublicKey, method, toChainDto(message));
|
|
966
|
+
const url = `${this.baseUrl}/wallet/connect/galachain/api/${encodeURIComponent(channel)}/${encodeURIComponent(contract)}/${encodeURIComponent(method)}/fee`;
|
|
967
|
+
let status;
|
|
968
|
+
let ok;
|
|
969
|
+
let body;
|
|
970
|
+
try {
|
|
971
|
+
const response = await requestWithTimeout(this.requestor, url, {
|
|
972
|
+
method: "POST",
|
|
973
|
+
headers: {
|
|
974
|
+
"Content-Type": "application/json",
|
|
975
|
+
Accept: "application/json"
|
|
976
|
+
},
|
|
977
|
+
body: JSON.stringify(wrapper)
|
|
978
|
+
}, this.requestTimeoutMs);
|
|
979
|
+
status = response.status;
|
|
980
|
+
ok = response.ok;
|
|
981
|
+
body = ok ? await readResponseBody(response) : void 0;
|
|
982
|
+
} catch {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
if (!ok) {
|
|
986
|
+
if (contract === "dex-contract" && (status === 403 || status === 404)) return {
|
|
987
|
+
source: "default",
|
|
988
|
+
fees: [{ feeInGala: "0" }]
|
|
989
|
+
};
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
return readQuote(body);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
function readQuote(body) {
|
|
996
|
+
var _fees$;
|
|
997
|
+
if (typeof body !== "object" || body === null) return null;
|
|
998
|
+
const fees = body.fees;
|
|
999
|
+
if (!Array.isArray(fees) || typeof ((_fees$ = fees[0]) === null || _fees$ === void 0 ? void 0 : _fees$.feeInGala) !== "string") return null;
|
|
1000
|
+
return body;
|
|
470
1001
|
}
|
|
471
1002
|
|
|
472
1003
|
//#endregion
|
|
@@ -480,6 +1011,12 @@ var SubmittedTransaction = class {
|
|
|
480
1011
|
mode;
|
|
481
1012
|
/** The synchronous gateway result, or `undefined` when `mode === 'queued'`. */
|
|
482
1013
|
result;
|
|
1014
|
+
/**
|
|
1015
|
+
* The gateway's whole JSON answer as it came (`{ status, message, error, data }`), for an app
|
|
1016
|
+
* that hands the backend's own envelope to its callers; `undefined` for a transaction built from
|
|
1017
|
+
* a uniqueKey alone.
|
|
1018
|
+
*/
|
|
1019
|
+
envelope;
|
|
483
1020
|
backendBaseUrl;
|
|
484
1021
|
requestor;
|
|
485
1022
|
requestTimeoutMs;
|
|
@@ -502,6 +1039,7 @@ var SubmittedTransaction = class {
|
|
|
502
1039
|
this.blockNumber = options.blockNumber ?? null;
|
|
503
1040
|
this.mode = options.mode;
|
|
504
1041
|
this.result = options.result;
|
|
1042
|
+
this.envelope = options.envelope;
|
|
505
1043
|
this.backendBaseUrl = options.dexBackendBaseUrl.replace(/\/$/u, "");
|
|
506
1044
|
this.requestor = options.httpRequestor;
|
|
507
1045
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
@@ -521,63 +1059,15 @@ var SubmittedTransaction = class {
|
|
|
521
1059
|
if (this.method === "RequestTrade") return this.confirmApplied(options);
|
|
522
1060
|
return this.confirmPosition(options);
|
|
523
1061
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
let response;
|
|
531
|
-
let body;
|
|
532
|
-
try {
|
|
533
|
-
response = await requestWithTimeout(this.requestor, url, { method: "GET" }, this.requestTimeoutMs);
|
|
534
|
-
body = await readResponseBody(response);
|
|
535
|
-
} catch (error) {
|
|
536
|
-
lastError = error;
|
|
537
|
-
if (!await schedule.waitForRead()) break;
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
if (response.status === 200) return this.readAppliedResult(response, body, url);
|
|
541
|
-
if (response.status === 404 || response.status === 429 || isTransient(response.status)) {
|
|
542
|
-
if (response.status === 429) {
|
|
543
|
-
const retryAfter = readRetryAfter(response);
|
|
544
|
-
if (retryAfter !== void 0) {
|
|
545
|
-
if (!await schedule.waitForRead(retryAfter)) break;
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
} else throw new GSwapSDKError(bodyMessage(body), "CONFIRMATION_FAILED", {
|
|
550
|
-
status: response.status,
|
|
551
|
-
body,
|
|
552
|
-
url
|
|
553
|
-
});
|
|
554
|
-
if (!await schedule.waitForRead()) break;
|
|
555
|
-
}
|
|
556
|
-
throw GSwapSDKError.confirmationTimeoutError(this.uniqueKey, lastError);
|
|
557
|
-
}
|
|
558
|
-
readAppliedResult(response, body, url) {
|
|
559
|
-
const record = asRecord$1(body);
|
|
560
|
-
const status = record === null || record === void 0 ? void 0 : record["Status"];
|
|
561
|
-
const applyTransactionId = readHeader(response, "x-apply-transaction-id") ?? null;
|
|
562
|
-
const blockNumber = readBlockNumberHeader(response, "x-apply-block-number") ?? null;
|
|
563
|
-
if (status === 0) throw GSwapSDKError.fromChainError(getStringProperty(record, "ErrorKey") ?? "CHAIN_ERROR", getStringProperty(record, "Message") ?? "The chain rejected this trade.", {
|
|
564
|
-
uniqueKey: this.uniqueKey,
|
|
565
|
-
applyTransactionId,
|
|
566
|
-
blockNumber,
|
|
567
|
-
status: response.status,
|
|
568
|
-
body,
|
|
569
|
-
url
|
|
570
|
-
});
|
|
571
|
-
if (status !== 1) throw malformedConfirmation(response, body, url);
|
|
572
|
-
const result = parseTradeExecution(record === null || record === void 0 ? void 0 : record["Data"]);
|
|
573
|
-
if (result === void 0) throw malformedConfirmation(response, body, url);
|
|
574
|
-
return {
|
|
1062
|
+
confirmApplied(options) {
|
|
1063
|
+
return confirmAppliedTrade({
|
|
1064
|
+
dexBackendBaseUrl: this.backendBaseUrl,
|
|
1065
|
+
httpRequestor: this.requestor,
|
|
1066
|
+
requestTimeoutMs: this.requestTimeoutMs
|
|
1067
|
+
}, {
|
|
575
1068
|
uniqueKey: this.uniqueKey,
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
requestTransactionId: this.transactionId,
|
|
579
|
-
result
|
|
580
|
-
};
|
|
1069
|
+
requestTransactionId: this.transactionId
|
|
1070
|
+
}, options);
|
|
581
1071
|
}
|
|
582
1072
|
/** Confirm a liquidity write against the position's observable effect.
|
|
583
1073
|
*
|
|
@@ -589,86 +1079,33 @@ var SubmittedTransaction = class {
|
|
|
589
1079
|
*/
|
|
590
1080
|
async confirmPosition(options) {
|
|
591
1081
|
if (this.positionConfirmation === void 0) throw GSwapSDKError.noWalletAddressError();
|
|
592
|
-
const
|
|
1082
|
+
const callerSignal = options === null || options === void 0 ? void 0 : options.signal;
|
|
1083
|
+
const abortError = (cause) => new GSwapSDKError("Transaction confirmation was aborted.", "CONFIRMATION_ABORTED", {
|
|
1084
|
+
uniqueKey: this.uniqueKey,
|
|
1085
|
+
...cause === void 0 ? {} : { cause }
|
|
1086
|
+
});
|
|
1087
|
+
const schedule = createConfirmationSchedule(options, 0, abortError);
|
|
593
1088
|
await schedule.waitForRead();
|
|
594
1089
|
while (true) {
|
|
595
1090
|
const controller = new AbortController();
|
|
596
|
-
const
|
|
1091
|
+
const disarm = armTimeout(controller, this.requestTimeoutMs);
|
|
1092
|
+
const forwardAbort = () => controller.abort();
|
|
1093
|
+
callerSignal === null || callerSignal === void 0 || callerSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
597
1094
|
try {
|
|
598
1095
|
const result = await this.positionConfirmation(controller.signal);
|
|
599
1096
|
if (result.confirmed) return result.position;
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
throwIfAborted(callerSignal, abortError);
|
|
1099
|
+
throw error;
|
|
600
1100
|
} finally {
|
|
601
|
-
|
|
1101
|
+
disarm();
|
|
1102
|
+
callerSignal === null || callerSignal === void 0 || callerSignal.removeEventListener("abort", forwardAbort);
|
|
602
1103
|
}
|
|
603
1104
|
if (!await schedule.waitForRead()) break;
|
|
604
1105
|
}
|
|
605
1106
|
throw GSwapSDKError.confirmationTimeoutError(this.uniqueKey);
|
|
606
1107
|
}
|
|
607
1108
|
};
|
|
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
1109
|
|
|
673
1110
|
//#endregion
|
|
674
1111
|
//#region src/classes/gateway.ts
|
|
@@ -679,38 +1116,28 @@ var ChainGateway = class {
|
|
|
679
1116
|
chainCallTimeoutMs;
|
|
680
1117
|
requestTimeoutMs;
|
|
681
1118
|
dexBackendBaseUrl;
|
|
1119
|
+
/** Wire format shared by the read services built on this gateway. */
|
|
1120
|
+
wire;
|
|
682
1121
|
/** Create a chain gateway client. */
|
|
683
1122
|
constructor(options) {
|
|
684
1123
|
this.dexBackendBaseUrl = trimTrailingSlash$1(options.dexBackendBaseUrl);
|
|
685
1124
|
this.requestor = options.httpRequestor ?? fetch.bind(globalThis);
|
|
686
1125
|
this.walletAddress = options.walletAddress;
|
|
687
|
-
this.chainCallTimeoutMs = options.chainCallTimeoutMs ?? 3e4;
|
|
1126
|
+
this.chainCallTimeoutMs = validateTimeoutMs(options.chainCallTimeoutMs ?? 3e4, "chainCallTimeoutMs");
|
|
688
1127
|
this.requestTimeoutMs = this.chainCallTimeoutMs;
|
|
1128
|
+
this.wire = options.wire ?? {};
|
|
689
1129
|
}
|
|
690
1130
|
async submit(method, signedBody, options) {
|
|
691
1131
|
if (method === "Trade") throw new GSwapSDKError("The synchronous Trade method was removed; swaps are queued through RequestTrade.", "METHOD_NOT_SUPPORTED", { method });
|
|
692
|
-
const url =
|
|
1132
|
+
const url = this.writeUrl(method);
|
|
693
1133
|
const headers = { "content-type": "application/json" };
|
|
694
1134
|
const walletAddress = (options === null || options === void 0 ? void 0 : options.walletAddress) ?? this.walletAddress;
|
|
695
1135
|
if (walletAddress !== void 0) headers["X-Wallet-Address"] = walletAddress;
|
|
696
|
-
|
|
697
|
-
try {
|
|
698
|
-
response = await requestWithTimeout(this.requestor, url, {
|
|
699
|
-
method: "POST",
|
|
700
|
-
headers,
|
|
701
|
-
body: JSON.stringify(signedBody)
|
|
702
|
-
}, this.chainCallTimeoutMs);
|
|
703
|
-
} catch (error) {
|
|
704
|
-
if (error instanceof GSwapSDKError && error.code === "REQUEST_TIMEOUT") throw new GSwapSDKError("The submission outcome is unknown because the gateway request timed out. Confirm before retrying.", "SUBMISSION_OUTCOME_UNKNOWN", {
|
|
705
|
-
uniqueKey: getStringProperty(signedBody, "uniqueKey") ?? "",
|
|
706
|
-
cause: error
|
|
707
|
-
});
|
|
708
|
-
throw error;
|
|
709
|
-
}
|
|
1136
|
+
const response = await this.post(url, headers, signedBody);
|
|
710
1137
|
const body = await readResponseBody(response);
|
|
711
1138
|
if (!response.ok) throw gatewayError(response, body, url);
|
|
712
|
-
const envelope = asRecord(body);
|
|
713
|
-
const data = asRecord((envelope === null || envelope === void 0 ? void 0 : envelope["data"]) ?? (envelope === null || envelope === void 0 ? void 0 : envelope["Data"]));
|
|
1139
|
+
const envelope = asRecord$1(body);
|
|
1140
|
+
const data = asRecord$1((envelope === null || envelope === void 0 ? void 0 : envelope["data"]) ?? (envelope === null || envelope === void 0 ? void 0 : envelope["Data"]));
|
|
714
1141
|
if (data === void 0) throw invalidGatewayResponse(response, body, url, "Expected a data object.");
|
|
715
1142
|
const transactionId = getStringProperty(data, "transactionId");
|
|
716
1143
|
const mode = getStringProperty(data, "mode");
|
|
@@ -725,7 +1152,7 @@ var ChainGateway = class {
|
|
|
725
1152
|
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
1153
|
} else if (mode === "queued" || !("result" in data)) throw invalidGatewayResponse(response, body, url, invalidModeMessage);
|
|
727
1154
|
if (mode === "sync") {
|
|
728
|
-
const embeddedResult = asRecord(data["result"]);
|
|
1155
|
+
const embeddedResult = asRecord$1(data["result"]);
|
|
729
1156
|
if (embeddedResult !== void 0) {
|
|
730
1157
|
const resultStatus = embeddedResult["Status"];
|
|
731
1158
|
if (resultStatus !== void 0 && resultStatus !== 1) throw GSwapSDKError.fromChainError(getStringProperty(embeddedResult, "ErrorKey") ?? "CHAIN_ERROR", getStringProperty(embeddedResult, "Message") ?? "The chain rejected this write.", {
|
|
@@ -735,22 +1162,75 @@ var ChainGateway = class {
|
|
|
735
1162
|
});
|
|
736
1163
|
}
|
|
737
1164
|
}
|
|
738
|
-
const uniqueKey = getStringProperty(signedBody, "uniqueKey") ?? "";
|
|
739
|
-
const headerTransactionId = readHeader(response, "x-transaction-id");
|
|
740
|
-
const resolvedTransactionId = transactionId === void 0 || transactionId === "" ? headerTransactionId : transactionId;
|
|
741
|
-
const blockNumber = bodyBlockNumber === null || bodyBlockNumber === void 0 ? readBlockNumberHeader(response) : isBlockNumber(bodyBlockNumber) ? bodyBlockNumber : void 0;
|
|
742
|
-
return new SubmittedTransaction({
|
|
743
|
-
method,
|
|
744
|
-
uniqueKey,
|
|
745
|
-
transactionId: resolvedTransactionId === void 0 || resolvedTransactionId === "" ? null : resolvedTransactionId,
|
|
746
|
-
blockNumber: blockNumber ?? null,
|
|
747
|
-
mode,
|
|
748
|
-
result: mode === "queued" ? void 0 : data["result"],
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1165
|
+
const uniqueKey = getStringProperty(signedBody, "uniqueKey") ?? "";
|
|
1166
|
+
const headerTransactionId = readHeader(response, "x-transaction-id");
|
|
1167
|
+
const resolvedTransactionId = transactionId === void 0 || transactionId === "" ? headerTransactionId : transactionId;
|
|
1168
|
+
const blockNumber = bodyBlockNumber === null || bodyBlockNumber === void 0 ? readBlockNumberHeader(response) : isBlockNumber(bodyBlockNumber) ? bodyBlockNumber : void 0;
|
|
1169
|
+
return new SubmittedTransaction({
|
|
1170
|
+
method,
|
|
1171
|
+
uniqueKey,
|
|
1172
|
+
transactionId: resolvedTransactionId === void 0 || resolvedTransactionId === "" ? null : resolvedTransactionId,
|
|
1173
|
+
blockNumber: blockNumber ?? null,
|
|
1174
|
+
mode,
|
|
1175
|
+
result: mode === "queued" ? void 0 : data["result"],
|
|
1176
|
+
envelope: body,
|
|
1177
|
+
dexBackendBaseUrl: this.dexBackendBaseUrl,
|
|
1178
|
+
httpRequestor: this.requestor,
|
|
1179
|
+
requestTimeoutMs: this.chainCallTimeoutMs,
|
|
1180
|
+
...(options === null || options === void 0 ? void 0 : options.positionConfirmation) === void 0 ? {} : { positionConfirmation: options.positionConfirmation }
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Submit a signed v2 DTO as a SHADOW (dark) write: the same route and body as {@link submit},
|
|
1185
|
+
* plus `X-Wallet-Address` and `X-Shadow: true`, so defi-backend marks the transaction private
|
|
1186
|
+
* before it reaches the contract. The wallet address is required (the backend resolves the
|
|
1187
|
+
* owner from it).
|
|
1188
|
+
*
|
|
1189
|
+
* Unlike `submit`, EVERY HTTP status resolves: the result is the raw `{ httpStatus, body }`,
|
|
1190
|
+
* because shadow failures are classified from the status together with the body's `code`
|
|
1191
|
+
* (`SHADOW_UNAVAILABLE`, `SHADOW_DISABLED`, `SHADOW_UNSUPPORTED_TYPE`, `SHADOW_RELAY_REJECTED`);
|
|
1192
|
+
* see `readShadowSubmitOutcome`. Only a transport failure rejects, and a timeout rejects
|
|
1193
|
+
* with `SUBMISSION_OUTCOME_UNKNOWN`.
|
|
1194
|
+
*
|
|
1195
|
+
* @example
|
|
1196
|
+
* ```ts
|
|
1197
|
+
* const result = await gateway.submitShadow('RequestTrade', signedDto, { walletAddress });
|
|
1198
|
+
* const outcome = readShadowSubmitOutcome('RequestTrade', signedDto, result);
|
|
1199
|
+
* ```
|
|
1200
|
+
*/
|
|
1201
|
+
async submitShadow(method, signedBody, options) {
|
|
1202
|
+
if (method === "Trade") throw new GSwapSDKError("The synchronous Trade method was removed; swaps are queued through RequestTrade.", "METHOD_NOT_SUPPORTED", { method });
|
|
1203
|
+
const walletAddress = (options === null || options === void 0 ? void 0 : options.walletAddress) ?? this.walletAddress;
|
|
1204
|
+
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");
|
|
1205
|
+
const headers = {
|
|
1206
|
+
"content-type": "application/json",
|
|
1207
|
+
"X-Wallet-Address": walletAddress,
|
|
1208
|
+
"X-Shadow": "true"
|
|
1209
|
+
};
|
|
1210
|
+
const response = await this.post(this.writeUrl(method), headers, signedBody);
|
|
1211
|
+
return {
|
|
1212
|
+
httpStatus: response.status,
|
|
1213
|
+
body: await readResponseBody(response)
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
writeUrl(method) {
|
|
1217
|
+
return `${this.dexBackendBaseUrl}/v1/chain/asset/dex-contract/${method}`;
|
|
1218
|
+
}
|
|
1219
|
+
/** POSTs a signed body; a timeout means the outcome is unknown, never "not submitted". */
|
|
1220
|
+
async post(url, headers, signedBody) {
|
|
1221
|
+
try {
|
|
1222
|
+
return await requestWithTimeout(this.requestor, url, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers,
|
|
1225
|
+
body: JSON.stringify(signedBody)
|
|
1226
|
+
}, this.chainCallTimeoutMs);
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
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", {
|
|
1229
|
+
uniqueKey: getStringProperty(signedBody, "uniqueKey") ?? "",
|
|
1230
|
+
cause: error
|
|
1231
|
+
});
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
754
1234
|
}
|
|
755
1235
|
/** Expose the requestor for sibling read services that share this gateway transport. */
|
|
756
1236
|
get httpRequestor() {
|
|
@@ -768,10 +1248,10 @@ function invalidGatewayResponse(response, body, url, message) {
|
|
|
768
1248
|
});
|
|
769
1249
|
}
|
|
770
1250
|
function gatewayError(response, body, url) {
|
|
771
|
-
const object = asRecord(body);
|
|
1251
|
+
const object = asRecord$1(body);
|
|
772
1252
|
const nested = getObjectProperty(body, "error");
|
|
773
1253
|
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") ??
|
|
1254
|
+
const message = getStringProperty(object, "message") ?? getStringProperty(nested, "message") ?? getStringProperty(nested, "Message") ?? `Gateway request failed with HTTP ${response.status}`;
|
|
775
1255
|
const retryAfterMs = response.status === 429 ? readRetryAfter(response) : void 0;
|
|
776
1256
|
if (code === "HTTP_ERROR") return new GSwapSDKError(message, code, {
|
|
777
1257
|
status: response.status,
|
|
@@ -780,7 +1260,7 @@ function gatewayError(response, body, url) {
|
|
|
780
1260
|
});
|
|
781
1261
|
return GSwapSDKError.fromGatewayBounce(code, message, response.status, retryAfterMs);
|
|
782
1262
|
}
|
|
783
|
-
function asRecord(value) {
|
|
1263
|
+
function asRecord$1(value) {
|
|
784
1264
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
785
1265
|
}
|
|
786
1266
|
function isBlockNumber(value) {
|
|
@@ -871,6 +1351,11 @@ var GdexStreamError = class extends Error {
|
|
|
871
1351
|
if (data !== void 0) this.data = data;
|
|
872
1352
|
}
|
|
873
1353
|
};
|
|
1354
|
+
function readSubscriptionId(value) {
|
|
1355
|
+
const id = typeof value === "object" && value !== null ? value.subscriptionId : void 0;
|
|
1356
|
+
if (typeof id !== "string") throw new GdexStreamError(-32603, "GDEX subscribe response is missing its subscriptionId");
|
|
1357
|
+
return id;
|
|
1358
|
+
}
|
|
874
1359
|
/**
|
|
875
1360
|
* Client for the gSwap realtime stream (gdex WebSocket Protocol v1): JSON-RPC 2.0 over one raw
|
|
876
1361
|
* WebSocket, with automatic reconnect and resubscribe.
|
|
@@ -887,6 +1372,7 @@ var GdexStreamError = class extends Error {
|
|
|
887
1372
|
var GdexStreamService = class {
|
|
888
1373
|
url;
|
|
889
1374
|
requestTimeoutMs;
|
|
1375
|
+
apiKey;
|
|
890
1376
|
webSocketConstructor;
|
|
891
1377
|
backoff;
|
|
892
1378
|
socket;
|
|
@@ -909,6 +1395,10 @@ var GdexStreamService = class {
|
|
|
909
1395
|
const implementation = config.webSocketImpl ?? globalThis.WebSocket;
|
|
910
1396
|
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
1397
|
this.webSocketConstructor = implementation;
|
|
1398
|
+
if (config.apiKey !== void 0) {
|
|
1399
|
+
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");
|
|
1400
|
+
this.apiKey = config.apiKey;
|
|
1401
|
+
}
|
|
912
1402
|
}
|
|
913
1403
|
/** Open the connection. Resolves at once when already connected. */
|
|
914
1404
|
connect() {
|
|
@@ -936,6 +1426,9 @@ var GdexStreamService = class {
|
|
|
936
1426
|
}
|
|
937
1427
|
/** Raw JSON-RPC escape hatch. */
|
|
938
1428
|
request(method, params) {
|
|
1429
|
+
return this.send(method, params);
|
|
1430
|
+
}
|
|
1431
|
+
send(method, params, onResult) {
|
|
939
1432
|
if (!this.isConnected() || !this.socket) return Promise.reject(/* @__PURE__ */ new Error("GDEX stream is not connected. Call connect() first."));
|
|
940
1433
|
const id = ++this.nextRequestId;
|
|
941
1434
|
const frame = {
|
|
@@ -952,7 +1445,8 @@ var GdexStreamService = class {
|
|
|
952
1445
|
this.pending.set(id, {
|
|
953
1446
|
resolve: (value) => resolve(value),
|
|
954
1447
|
reject,
|
|
955
|
-
timeout
|
|
1448
|
+
timeout,
|
|
1449
|
+
onResult
|
|
956
1450
|
});
|
|
957
1451
|
try {
|
|
958
1452
|
var _this$socket2;
|
|
@@ -995,7 +1489,11 @@ var GdexStreamService = class {
|
|
|
995
1489
|
subscribeSwaps(cb, opts = {}) {
|
|
996
1490
|
return this.subscribe("swap", cb, opts);
|
|
997
1491
|
}
|
|
998
|
-
/**
|
|
1492
|
+
/**
|
|
1493
|
+
* State changes of one pool (price, tick, reserves, liquidity). These events carry no acting wallet:
|
|
1494
|
+
* on v2 `creator` is whoever created the pool, not whoever just traded or added liquidity. For
|
|
1495
|
+
* "who did it", use {@link subscribeLiquidity} (`owner`) or {@link subscribeSwaps} (`sender`).
|
|
1496
|
+
*/
|
|
999
1497
|
subscribePool(selector, cb, opts = {}) {
|
|
1000
1498
|
return this.subscribe("pool", cb, {
|
|
1001
1499
|
...selector,
|
|
@@ -1033,7 +1531,7 @@ var GdexStreamService = class {
|
|
|
1033
1531
|
openSocket() {
|
|
1034
1532
|
return new Promise((resolve, reject) => {
|
|
1035
1533
|
let opened = false;
|
|
1036
|
-
const socket = new this.webSocketConstructor(this.url, GDEX_SUBPROTOCOL);
|
|
1534
|
+
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 } });
|
|
1037
1535
|
this.socket = socket;
|
|
1038
1536
|
socket.onopen = () => {
|
|
1039
1537
|
if (this.socket !== socket) return;
|
|
@@ -1078,7 +1576,16 @@ var GdexStreamService = class {
|
|
|
1078
1576
|
const code = typeof message.error.code === "number" ? message.error.code : -32603;
|
|
1079
1577
|
const errorMessage = typeof message.error.message === "string" ? message.error.message : "GDEX request failed";
|
|
1080
1578
|
pending.reject(new GdexStreamError(code, errorMessage, message.error.data));
|
|
1081
|
-
} else
|
|
1579
|
+
} else {
|
|
1580
|
+
try {
|
|
1581
|
+
var _pending$onResult;
|
|
1582
|
+
(_pending$onResult = pending.onResult) === null || _pending$onResult === void 0 || _pending$onResult.call(pending, message.result);
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
pending.resolve(message.result);
|
|
1588
|
+
}
|
|
1082
1589
|
return;
|
|
1083
1590
|
}
|
|
1084
1591
|
if (message.method !== "gdex_subscription" || !this.isRecord(message.params)) return;
|
|
@@ -1098,21 +1605,24 @@ var GdexStreamService = class {
|
|
|
1098
1605
|
channel,
|
|
1099
1606
|
...options
|
|
1100
1607
|
};
|
|
1101
|
-
const result = await this.request("gdex_subscribe", params);
|
|
1102
|
-
let active;
|
|
1103
1608
|
const handle = {
|
|
1104
|
-
subscriptionId:
|
|
1105
|
-
unsubscribe: async () => this.unsubscribe(
|
|
1609
|
+
subscriptionId: "",
|
|
1610
|
+
unsubscribe: async () => this.unsubscribe(subscription)
|
|
1106
1611
|
};
|
|
1107
|
-
|
|
1108
|
-
serverId:
|
|
1612
|
+
const subscription = {
|
|
1613
|
+
serverId: "",
|
|
1109
1614
|
params,
|
|
1110
1615
|
callback,
|
|
1111
1616
|
handle,
|
|
1112
1617
|
active: true
|
|
1113
1618
|
};
|
|
1114
|
-
this.
|
|
1115
|
-
|
|
1619
|
+
await this.send("gdex_subscribe", params, (value) => {
|
|
1620
|
+
const serverId = readSubscriptionId(value);
|
|
1621
|
+
subscription.serverId = serverId;
|
|
1622
|
+
handle.subscriptionId = serverId;
|
|
1623
|
+
this.activeSubscriptions.add(subscription);
|
|
1624
|
+
this.subscriptionsByServerId.set(serverId, subscription);
|
|
1625
|
+
});
|
|
1116
1626
|
return handle;
|
|
1117
1627
|
}
|
|
1118
1628
|
async unsubscribe(subscription) {
|
|
@@ -1155,19 +1665,19 @@ var GdexStreamService = class {
|
|
|
1155
1665
|
...subscription.params,
|
|
1156
1666
|
snapshot: true
|
|
1157
1667
|
};
|
|
1158
|
-
const result = await this.resubscribeWithRateLimitRetry(params)
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
this.
|
|
1668
|
+
const result = await this.resubscribeWithRateLimitRetry(params, (value) => {
|
|
1669
|
+
const serverId = readSubscriptionId(value);
|
|
1670
|
+
if (!subscription.active) return;
|
|
1671
|
+
subscription.serverId = serverId;
|
|
1672
|
+
subscription.handle.subscriptionId = serverId;
|
|
1673
|
+
this.subscriptionsByServerId.set(serverId, subscription);
|
|
1674
|
+
});
|
|
1675
|
+
if (!subscription.active) await this.request("gdex_unsubscribe", { subscriptionId: result.subscriptionId });
|
|
1166
1676
|
}
|
|
1167
1677
|
}
|
|
1168
|
-
async resubscribeWithRateLimitRetry(params) {
|
|
1678
|
+
async resubscribeWithRateLimitRetry(params, onResult) {
|
|
1169
1679
|
for (let attempt = 0;; attempt++) try {
|
|
1170
|
-
return await this.
|
|
1680
|
+
return await this.send("gdex_subscribe", params, onResult);
|
|
1171
1681
|
} catch (error) {
|
|
1172
1682
|
if (!(error instanceof GdexStreamError) || error.code !== RATE_LIMITED || attempt >= MAX_RESUBSCRIBE_RATE_RETRIES) throw error;
|
|
1173
1683
|
const retryAfterMs = this.readRetryAfterMs(error.data);
|
|
@@ -1211,28 +1721,35 @@ var GdexStreamService = class {
|
|
|
1211
1721
|
//#endregion
|
|
1212
1722
|
//#region src/version.ts
|
|
1213
1723
|
/** Package version embedded by the bundler, with a source-test fallback. */
|
|
1214
|
-
const SDK_VERSION = "1.
|
|
1724
|
+
const SDK_VERSION = "1.2.0-rc.1";
|
|
1215
1725
|
|
|
1216
1726
|
//#endregion
|
|
1217
1727
|
//#region src/classes/http_client.ts
|
|
1218
1728
|
var HttpClient = class {
|
|
1219
1729
|
httpRequestor;
|
|
1220
1730
|
timeoutMs;
|
|
1221
|
-
|
|
1222
|
-
|
|
1731
|
+
wire;
|
|
1732
|
+
/**
|
|
1733
|
+
* Create an HTTP client around fetch or an injected requestor. `wire` selects the query
|
|
1734
|
+
* encoding and the `User-Agent` (see {@link WireOptions}).
|
|
1735
|
+
*/
|
|
1736
|
+
constructor(httpRequestor = fetch, timeoutMs = 3e4, wire = {}) {
|
|
1223
1737
|
this.httpRequestor = httpRequestor;
|
|
1224
1738
|
this.timeoutMs = timeoutMs;
|
|
1739
|
+
this.wire = wire;
|
|
1225
1740
|
}
|
|
1226
1741
|
async sendRequest(method, baseUrl, basePath, endpoint, body, options) {
|
|
1227
1742
|
const url = `${baseUrl}${basePath}${endpoint}`;
|
|
1228
1743
|
debugLog(`Sending request to ${url} with body:`, body);
|
|
1744
|
+
const hasBody = Boolean(body);
|
|
1229
1745
|
const response = await requestWithTimeout(this.httpRequestor, url, {
|
|
1230
1746
|
method,
|
|
1231
1747
|
headers: {
|
|
1232
|
-
"Content-Type": "application/json",
|
|
1233
|
-
"User-Agent": `GalaChain-SDK/${
|
|
1748
|
+
...hasBody ? { "Content-Type": "application/json" } : {},
|
|
1749
|
+
...this.wire.userAgent === false ? {} : { "User-Agent": this.wire.userAgent ?? `GalaChain-SDK/${"1.2.0-rc.1"}` },
|
|
1750
|
+
...this.wire.headers
|
|
1234
1751
|
},
|
|
1235
|
-
...
|
|
1752
|
+
...hasBody ? { body: JSON.stringify(body) } : {},
|
|
1236
1753
|
...(options === null || options === void 0 ? void 0 : options.signal) === void 0 ? {} : { signal: options.signal }
|
|
1237
1754
|
}, this.timeoutMs);
|
|
1238
1755
|
if (!response.ok) throw await GSwapSDKError.fromErrorResponse(url, response);
|
|
@@ -1246,12 +1763,65 @@ var HttpClient = class {
|
|
|
1246
1763
|
}
|
|
1247
1764
|
/** Send a GET request with URL-encoded query parameters and decode its response. */
|
|
1248
1765
|
async sendGetRequest(baseUrl, basePath, endpoint, params, options) {
|
|
1249
|
-
const
|
|
1250
|
-
const endpointWithParams = searchParams ? `${endpoint}?${searchParams.toString()}` : endpoint;
|
|
1766
|
+
const endpointWithParams = params ? `${endpoint}?${buildQueryString(params, this.wire.queryEncoding)}` : endpoint;
|
|
1251
1767
|
return this.sendRequest("GET", baseUrl, basePath, endpointWithParams, void 0, options);
|
|
1252
1768
|
}
|
|
1253
1769
|
};
|
|
1254
1770
|
|
|
1771
|
+
//#endregion
|
|
1772
|
+
//#region src/utils/lenient.ts
|
|
1773
|
+
/** A usable row. */
|
|
1774
|
+
function entryOk(value) {
|
|
1775
|
+
return {
|
|
1776
|
+
ok: true,
|
|
1777
|
+
value
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
/** An unusable row, with the reason it is reported under. */
|
|
1781
|
+
function entryRejected(reason) {
|
|
1782
|
+
return {
|
|
1783
|
+
ok: false,
|
|
1784
|
+
reason
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Reads a list answer row by row: a row that cannot be used is skipped and reported, instead of
|
|
1789
|
+
* rejecting the whole list because of one bad row. An answer that is not a list at all is still
|
|
1790
|
+
* `INVALID_CHAIN_RESPONSE`: nothing in it can be trusted.
|
|
1791
|
+
*/
|
|
1792
|
+
function readEntries(source, data, parse, onInvalidEntry) {
|
|
1793
|
+
if (!Array.isArray(data)) throw new GSwapSDKError(`Backend returned an invalid ${source} list.`, "INVALID_CHAIN_RESPONSE", { data });
|
|
1794
|
+
const rows = [];
|
|
1795
|
+
data.forEach((entry, index) => {
|
|
1796
|
+
const parsed = parse(entry);
|
|
1797
|
+
if (parsed.ok) {
|
|
1798
|
+
rows.push(parsed.value);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
try {
|
|
1802
|
+
onInvalidEntry === null || onInvalidEntry === void 0 || onInvalidEntry({
|
|
1803
|
+
source,
|
|
1804
|
+
index,
|
|
1805
|
+
entry,
|
|
1806
|
+
reason: parsed.reason
|
|
1807
|
+
});
|
|
1808
|
+
} catch {}
|
|
1809
|
+
});
|
|
1810
|
+
return rows;
|
|
1811
|
+
}
|
|
1812
|
+
/** A plain JSON object (not null, not an array). */
|
|
1813
|
+
function asEntryRecord(value) {
|
|
1814
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1815
|
+
}
|
|
1816
|
+
/** A finite number, or a non-empty numeric string, as a number; otherwise `undefined`. */
|
|
1817
|
+
function finiteNumberOf(value) {
|
|
1818
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1819
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1820
|
+
const parsed = Number(value);
|
|
1821
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1255
1825
|
//#endregion
|
|
1256
1826
|
//#region src/utils/ordering.ts
|
|
1257
1827
|
/** Return two trading symbols in the contract's plain-string order. */
|
|
@@ -1330,39 +1900,250 @@ function parseTokenClassKey(tokenClassKey) {
|
|
|
1330
1900
|
};
|
|
1331
1901
|
}
|
|
1332
1902
|
|
|
1903
|
+
//#endregion
|
|
1904
|
+
//#region src/classes/symbols.ts
|
|
1905
|
+
const SYMBOL_CACHE_TTL_MS = 6e4;
|
|
1906
|
+
/** Symbol registry client for the current GalaChainDex contract. */
|
|
1907
|
+
var Symbols = class {
|
|
1908
|
+
gateway;
|
|
1909
|
+
options;
|
|
1910
|
+
cachedSymbols;
|
|
1911
|
+
cachedAt = 0;
|
|
1912
|
+
refreshInFlight;
|
|
1913
|
+
http;
|
|
1914
|
+
/**
|
|
1915
|
+
* Create a symbol service backed by the swap backend. `onInvalidEntry` is told about every
|
|
1916
|
+
* registry row that was skipped as unusable (see {@link InvalidEntryHandler}).
|
|
1917
|
+
*/
|
|
1918
|
+
constructor(gateway, options = {}) {
|
|
1919
|
+
this.gateway = gateway;
|
|
1920
|
+
this.options = options;
|
|
1921
|
+
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs, gateway.wire);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* List all registered GalaChainDex trading symbols, cached for 60 seconds. A registry row that
|
|
1925
|
+
* is not a usable symbol is skipped (and reported to `onInvalidEntry`) instead of failing the
|
|
1926
|
+
* whole list.
|
|
1927
|
+
*/
|
|
1928
|
+
async list() {
|
|
1929
|
+
if (this.cachedSymbols !== void 0 && Date.now() - this.cachedAt < SYMBOL_CACHE_TTL_MS) return this.cachedSymbols;
|
|
1930
|
+
if (this.refreshInFlight !== void 0) return this.refreshInFlight;
|
|
1931
|
+
this.refreshInFlight = this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/symbols").then((response) => readEntries("symbols", response.data, parseTradingSymbol, this.options.onInvalidEntry)).then((symbols) => {
|
|
1932
|
+
this.cachedSymbols = symbols;
|
|
1933
|
+
this.cachedAt = Date.now();
|
|
1934
|
+
return symbols;
|
|
1935
|
+
}).finally(() => {
|
|
1936
|
+
this.refreshInFlight = void 0;
|
|
1937
|
+
});
|
|
1938
|
+
return this.refreshInFlight;
|
|
1939
|
+
}
|
|
1940
|
+
/** Invalidate the cached symbol registry after a write changes registration state.
|
|
1941
|
+
*
|
|
1942
|
+
* @example
|
|
1943
|
+
* ```ts
|
|
1944
|
+
* gSwap.symbols.invalidate();
|
|
1945
|
+
* ```
|
|
1946
|
+
*/
|
|
1947
|
+
invalidate() {
|
|
1948
|
+
this.cachedSymbols = void 0;
|
|
1949
|
+
this.cachedAt = 0;
|
|
1950
|
+
}
|
|
1951
|
+
/** Force one registry refresh, bypassing the cache.
|
|
1952
|
+
*
|
|
1953
|
+
* @example
|
|
1954
|
+
* ```ts
|
|
1955
|
+
* const symbols = await gSwap.symbols.refresh();
|
|
1956
|
+
* ```
|
|
1957
|
+
*/
|
|
1958
|
+
async refresh() {
|
|
1959
|
+
this.invalidate();
|
|
1960
|
+
this.refreshInFlight = void 0;
|
|
1961
|
+
return this.list();
|
|
1962
|
+
}
|
|
1963
|
+
/** Resolve a symbol or token class key to its registered symbol metadata. */
|
|
1964
|
+
async resolve(ref) {
|
|
1965
|
+
let symbols = await this.list();
|
|
1966
|
+
const composite = typeof ref === "string" ? isCompositeKey(ref) ? compositeKeyOf(parseTokenClassKey(ref)) : void 0 : compositeKeyOf(ref);
|
|
1967
|
+
const resolved = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
|
|
1968
|
+
if (resolved === void 0) {
|
|
1969
|
+
symbols = await this.refresh();
|
|
1970
|
+
const refreshed = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
|
|
1971
|
+
if (refreshed === void 0) throw GSwapSDKError.unknownTokenError(ref);
|
|
1972
|
+
return refreshed;
|
|
1973
|
+
}
|
|
1974
|
+
return resolved;
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* The trading symbol to put in a token's first CreatePool.
|
|
1978
|
+
*
|
|
1979
|
+
* 1. The symbol ALREADY registered for exactly this class key, if any: proposing another one
|
|
1980
|
+
* fails the gateway's registry-conflict check.
|
|
1981
|
+
* 2. Otherwise `displaySymbol` (the token's curated symbol), when it is proposable
|
|
1982
|
+
* ({@link isProposableTradingSymbol}).
|
|
1983
|
+
* 3. Otherwise a `VALIDATION_ERROR`: a symbol is never derived from the key (BNB's collection is
|
|
1984
|
+
* `GBNB`, a launchpad token's is the reserved word `Token`).
|
|
1985
|
+
*
|
|
1986
|
+
* `registryFailure: 'ignore'` treats an unreachable registry as an empty one and falls through
|
|
1987
|
+
* to step 2 (the gateway still rejects a conflicting symbol); the default `'throw'` propagates
|
|
1988
|
+
* the failure.
|
|
1989
|
+
*
|
|
1990
|
+
* @example
|
|
1991
|
+
* ```ts
|
|
1992
|
+
* const symbol = await gSwap.symbols.proposeSymbol(
|
|
1993
|
+
* { collection: 'GALA_TEST1', category: 'Unit', type: 'none', additionalKey: 'none' },
|
|
1994
|
+
* 'GALATEST',
|
|
1995
|
+
* );
|
|
1996
|
+
* ```
|
|
1997
|
+
*/
|
|
1998
|
+
async proposeSymbol(classKey, displaySymbol, options = {}) {
|
|
1999
|
+
let registry = [];
|
|
2000
|
+
try {
|
|
2001
|
+
registry = await this.list();
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
if (options.registryFailure !== "ignore") throw error;
|
|
2004
|
+
}
|
|
2005
|
+
const wanted = compositeKeyOf(classKey);
|
|
2006
|
+
const registered = registry.find((entry) => entry.collection === classKey.collection && entry.category === classKey.category && entry.type === classKey.type && entry.additionalKey === classKey.additionalKey);
|
|
2007
|
+
if (registered !== void 0) return registered.symbol;
|
|
2008
|
+
if (isProposableTradingSymbol(displaySymbol)) return displaySymbol;
|
|
2009
|
+
throw new GSwapSDKError(`No valid trading symbol for token class ${wanted}: "${displaySymbol}" is not an acceptable symbol and none is registered`, "VALIDATION_ERROR", {
|
|
2010
|
+
classKey,
|
|
2011
|
+
displaySymbol
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
/** Resolve and put two token references in canonical trading-symbol order. */
|
|
2015
|
+
async orderPair(a, b) {
|
|
2016
|
+
const [resolvedA, resolvedB] = await Promise.all([this.resolve(a), this.resolve(b)]);
|
|
2017
|
+
const [token0Symbol, token1Symbol] = orderSymbols(resolvedA.symbol, resolvedB.symbol);
|
|
2018
|
+
return {
|
|
2019
|
+
token0: token0Symbol === resolvedA.symbol ? resolvedA : resolvedB,
|
|
2020
|
+
token1: token1Symbol === resolvedB.symbol ? resolvedB : resolvedA,
|
|
2021
|
+
flipped: token0Symbol !== resolvedA.symbol
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
/** Reads one registry row: all four class-key parts, a symbol and non-negative integer decimals. */
|
|
2026
|
+
function parseTradingSymbol(value) {
|
|
2027
|
+
const entry = asEntryRecord(value);
|
|
2028
|
+
if (entry === void 0) return entryRejected("the row is not an object");
|
|
2029
|
+
const { symbol, collection, category, type, additionalKey, decimals } = entry;
|
|
2030
|
+
if (typeof symbol !== "string") return entryRejected("symbol is not a string");
|
|
2031
|
+
if (symbol.length === 0) return entryRejected("symbol is empty");
|
|
2032
|
+
if (typeof collection !== "string") return entryRejected("collection is not a string");
|
|
2033
|
+
if (typeof category !== "string") return entryRejected("category is not a string");
|
|
2034
|
+
if (typeof type !== "string") return entryRejected("type is not a string");
|
|
2035
|
+
if (typeof additionalKey !== "string") return entryRejected("additionalKey is not a string");
|
|
2036
|
+
if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0) return entryRejected("decimals is not a non-negative integer");
|
|
2037
|
+
return entryOk({
|
|
2038
|
+
symbol,
|
|
2039
|
+
collection,
|
|
2040
|
+
category,
|
|
2041
|
+
type,
|
|
2042
|
+
additionalKey,
|
|
2043
|
+
decimals
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
function isCompositeKey(ref) {
|
|
2047
|
+
return ref.includes("|") || ref.includes("$");
|
|
2048
|
+
}
|
|
2049
|
+
/** A token reference as the caller wrote it: a string verbatim, a class key as its `$` composite. */
|
|
2050
|
+
function rawTokenOf(ref) {
|
|
2051
|
+
return typeof ref === "string" ? ref : compositeKeyOf(ref);
|
|
2052
|
+
}
|
|
2053
|
+
/** The trading symbol of a reference, or `SYMBOL_NOT_FOUND` when the resolver has none. */
|
|
2054
|
+
async function tradingSymbolOf(resolver, ref) {
|
|
2055
|
+
const resolved = await resolver.resolve(ref);
|
|
2056
|
+
if (resolved.symbol.length > 0) return resolved.symbol;
|
|
2057
|
+
throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token: ref });
|
|
2058
|
+
}
|
|
2059
|
+
/** What a read request names a token by: the reference itself in pass-through mode, else its symbol. */
|
|
2060
|
+
async function queryTokenOf(resolver, ref) {
|
|
2061
|
+
return resolver.passthrough === true ? rawTokenOf(ref) : tradingSymbolOf(resolver, ref);
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* The offline resolver for apps that already hold the backend's canonical trading symbols (from
|
|
2065
|
+
* `/quote`, `/add-liq-estimate`, a position row) and must not make a registry request of their own.
|
|
2066
|
+
*
|
|
2067
|
+
* - `resolve` accepts a bare trading symbol and returns it as is; a class key (object, `$` or `|`
|
|
2068
|
+
* form) is not a symbol and is refused as `UNKNOWN_TOKEN`. `createPool` relies on that refusal to
|
|
2069
|
+
* fall back to the symbols its caller supplies.
|
|
2070
|
+
* - `orderPair` orders two symbols by plain string comparison, as the contract does.
|
|
2071
|
+
* - Reads are pass-through (see {@link TokenResolver.passthrough}): the caller's references go on
|
|
2072
|
+
* the wire verbatim, composite keys included, and the backend orients the pair.
|
|
2073
|
+
*
|
|
2074
|
+
* @example
|
|
2075
|
+
* ```ts
|
|
2076
|
+
* const swaps = new Swaps(gateway, new CanonicalSymbols(), signer, walletAddress);
|
|
2077
|
+
* await swaps.swap('GALA', 'GUSDC', 3000, { exactIn: '10' });
|
|
2078
|
+
* ```
|
|
2079
|
+
*/
|
|
2080
|
+
var CanonicalSymbols = class {
|
|
2081
|
+
passthrough = true;
|
|
2082
|
+
/** Resolve a bare trading symbol to itself. */
|
|
2083
|
+
resolve(ref) {
|
|
2084
|
+
if (typeof ref === "string" && !isCompositeKey(ref)) return Promise.resolve({
|
|
2085
|
+
symbol: ref,
|
|
2086
|
+
collection: "",
|
|
2087
|
+
category: "",
|
|
2088
|
+
type: "",
|
|
2089
|
+
additionalKey: "",
|
|
2090
|
+
decimals: 0
|
|
2091
|
+
});
|
|
2092
|
+
return Promise.reject(GSwapSDKError.unknownTokenError(ref));
|
|
2093
|
+
}
|
|
2094
|
+
/** Put two symbols in canonical (string) order. */
|
|
2095
|
+
async orderPair(a, b) {
|
|
2096
|
+
const [resolvedA, resolvedB] = await Promise.all([this.resolve(a), this.resolve(b)]);
|
|
2097
|
+
const [first] = orderSymbols(resolvedA.symbol, resolvedB.symbol);
|
|
2098
|
+
const flipped = first !== resolvedA.symbol;
|
|
2099
|
+
return {
|
|
2100
|
+
token0: flipped ? resolvedB : resolvedA,
|
|
2101
|
+
token1: flipped ? resolvedA : resolvedB,
|
|
2102
|
+
flipped
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
/** There is no registry state to drop. */
|
|
2106
|
+
invalidate() {}
|
|
2107
|
+
};
|
|
2108
|
+
|
|
1333
2109
|
//#endregion
|
|
1334
2110
|
//#region src/classes/pools.ts
|
|
1335
2111
|
/** Read-only pool and current-price access for the current GalaChainDex contract. */
|
|
1336
2112
|
var Pools = class {
|
|
1337
2113
|
gateway;
|
|
1338
2114
|
symbols;
|
|
2115
|
+
options;
|
|
1339
2116
|
/**
|
|
1340
|
-
* Creates a pool service.
|
|
2117
|
+
* Creates a pool service. `options.onInvalidEntry` is told about every pool row that was
|
|
2118
|
+
* skipped as unusable (see {@link InvalidEntryHandler}).
|
|
1341
2119
|
*
|
|
1342
2120
|
* @example
|
|
1343
2121
|
* ```ts
|
|
1344
2122
|
* const pools = new Pools(gateway, symbols);
|
|
1345
2123
|
* ```
|
|
1346
2124
|
*/
|
|
1347
|
-
constructor(gateway, symbols) {
|
|
2125
|
+
constructor(gateway, symbols, options = {}) {
|
|
1348
2126
|
this.gateway = gateway;
|
|
1349
2127
|
this.symbols = symbols;
|
|
1350
|
-
this.
|
|
2128
|
+
this.options = options;
|
|
2129
|
+
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4, gateway.wire);
|
|
1351
2130
|
}
|
|
1352
2131
|
http;
|
|
1353
2132
|
/**
|
|
1354
|
-
* Fetches
|
|
2133
|
+
* Fetches current-contract pools from the swap backend, optionally searched, sorted and paged
|
|
2134
|
+
* by the backend. A row that is not a usable pool is skipped and reported to `onInvalidEntry`
|
|
2135
|
+
* instead of failing the whole list; an answer that is not a list at all is
|
|
2136
|
+
* `INVALID_CHAIN_RESPONSE`.
|
|
1355
2137
|
*
|
|
1356
2138
|
* @example
|
|
1357
2139
|
* ```ts
|
|
1358
|
-
* const
|
|
1359
|
-
* console.log(
|
|
2140
|
+
* const top = await service.getPools({ sortBy: 'tvl', sortOrder: 'desc', page: 1, limit: 8 });
|
|
2141
|
+
* console.log(top.length);
|
|
1360
2142
|
* ```
|
|
1361
2143
|
*/
|
|
1362
2144
|
async getPools(options) {
|
|
1363
|
-
const response = await this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/pools",
|
|
1364
|
-
|
|
1365
|
-
return response.data;
|
|
2145
|
+
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 });
|
|
2146
|
+
return readEntries("pools", response.data, parsePoolListInfo, (options === null || options === void 0 ? void 0 : options.onInvalidEntry) ?? this.options.onInvalidEntry);
|
|
1366
2147
|
}
|
|
1367
2148
|
/**
|
|
1368
2149
|
* Fetches one pool from the backend, preserving whether the request order was flipped.
|
|
@@ -1401,8 +2182,13 @@ var Pools = class {
|
|
|
1401
2182
|
*/
|
|
1402
2183
|
async getCompositePool(token0, token1, fee) {
|
|
1403
2184
|
validateFee(fee);
|
|
1404
|
-
|
|
1405
|
-
|
|
2185
|
+
if (this.symbols.passthrough === true) return this.getBackend("/composite-pool", {
|
|
2186
|
+
token0: rawTokenOf(token0),
|
|
2187
|
+
token1: rawTokenOf(token1),
|
|
2188
|
+
fee: String(fee)
|
|
2189
|
+
});
|
|
2190
|
+
const first = await tradingSymbolOf(this.symbols, token0);
|
|
2191
|
+
const second = await tradingSymbolOf(this.symbols, token1);
|
|
1406
2192
|
const ordered = readOrderedSymbols(orderSymbols(first, second));
|
|
1407
2193
|
return this.getBackend("/composite-pool", {
|
|
1408
2194
|
token0: ordered.token0,
|
|
@@ -1412,8 +2198,8 @@ var Pools = class {
|
|
|
1412
2198
|
}
|
|
1413
2199
|
async backendParams(token0, token1, fee) {
|
|
1414
2200
|
return {
|
|
1415
|
-
token0: await
|
|
1416
|
-
token1: await
|
|
2201
|
+
token0: await queryTokenOf(this.symbols, token0),
|
|
2202
|
+
token1: await queryTokenOf(this.symbols, token1),
|
|
1417
2203
|
fee: String(fee)
|
|
1418
2204
|
};
|
|
1419
2205
|
}
|
|
@@ -1425,53 +2211,449 @@ var Pools = class {
|
|
|
1425
2211
|
throw new GSwapSDKError("Pool request failed.", "HTTP_ERROR", { cause: error });
|
|
1426
2212
|
}
|
|
1427
2213
|
}
|
|
1428
|
-
};
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2214
|
+
};
|
|
2215
|
+
const SORT_KEYS = [
|
|
2216
|
+
"tvl",
|
|
2217
|
+
"volume1d",
|
|
2218
|
+
"volume30d"
|
|
2219
|
+
];
|
|
2220
|
+
const MAX_SEARCH_LENGTH = 64;
|
|
2221
|
+
const MAX_PAGE_SIZE = 1e3;
|
|
2222
|
+
/** The pool-list query, in the order the apps write it: search, sortBy, sortOrder, page, limit. */
|
|
2223
|
+
function poolListParams(options) {
|
|
2224
|
+
if (options === void 0) return void 0;
|
|
2225
|
+
const params = {};
|
|
2226
|
+
if (options.search !== void 0) {
|
|
2227
|
+
if (options.search.length > MAX_SEARCH_LENGTH) throw new GSwapSDKError(`search must be at most ${MAX_SEARCH_LENGTH} characters.`, "VALIDATION_ERROR", { search: options.search });
|
|
2228
|
+
params["search"] = options.search;
|
|
2229
|
+
}
|
|
2230
|
+
if (options.sortBy !== void 0) {
|
|
2231
|
+
if (!SORT_KEYS.includes(options.sortBy)) throw new GSwapSDKError(`sortBy must be one of ${SORT_KEYS.join(", ")}.`, "VALIDATION_ERROR", { sortBy: options.sortBy });
|
|
2232
|
+
params["sortBy"] = options.sortBy;
|
|
2233
|
+
}
|
|
2234
|
+
if (options.sortOrder !== void 0) {
|
|
2235
|
+
if (options.sortOrder !== "asc" && options.sortOrder !== "desc") throw new GSwapSDKError("sortOrder must be asc or desc.", "VALIDATION_ERROR", { sortOrder: options.sortOrder });
|
|
2236
|
+
params["sortOrder"] = options.sortOrder;
|
|
2237
|
+
}
|
|
2238
|
+
if (options.page !== void 0) {
|
|
2239
|
+
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 });
|
|
2240
|
+
params["page"] = String(options.page);
|
|
2241
|
+
}
|
|
2242
|
+
if (options.limit !== void 0) {
|
|
2243
|
+
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 });
|
|
2244
|
+
params["limit"] = String(options.limit);
|
|
2245
|
+
}
|
|
2246
|
+
return Object.keys(params).length === 0 ? void 0 : params;
|
|
2247
|
+
}
|
|
2248
|
+
const OPTIONAL_POOL_STRINGS = [
|
|
2249
|
+
"poolName",
|
|
2250
|
+
"token0CompositeKey",
|
|
2251
|
+
"token1CompositeKey",
|
|
2252
|
+
"token0Image",
|
|
2253
|
+
"token1Image",
|
|
2254
|
+
"creator",
|
|
2255
|
+
"price",
|
|
2256
|
+
"sqrtPrice",
|
|
2257
|
+
"token0Tvl",
|
|
2258
|
+
"token1Tvl"
|
|
2259
|
+
];
|
|
2260
|
+
const OPTIONAL_POOL_NUMBERS = [
|
|
2261
|
+
"tickSpacing",
|
|
2262
|
+
"protocolFees",
|
|
2263
|
+
"tradingFees",
|
|
2264
|
+
"tick",
|
|
2265
|
+
"volume1d",
|
|
2266
|
+
"volume7d",
|
|
2267
|
+
"volume30d",
|
|
2268
|
+
"trades1d",
|
|
2269
|
+
"fee24h",
|
|
2270
|
+
"token0Price",
|
|
2271
|
+
"token1Price"
|
|
2272
|
+
];
|
|
2273
|
+
/** Fields the apps accept as a number or a numeric string (the wallet does `Number(row.x)`). */
|
|
2274
|
+
const NUMERIC_POOL_FIELDS = [
|
|
2275
|
+
"fee",
|
|
2276
|
+
"tvlUsd",
|
|
2277
|
+
"apr1d"
|
|
2278
|
+
];
|
|
2279
|
+
/**
|
|
2280
|
+
* Reads one pool-list row the way the apps do: the identity (`token0`, `token1`, `poolRef`) and
|
|
2281
|
+
* `fee` must be usable, every other field is optional but, when present, must have its type.
|
|
2282
|
+
* `fee`, `tvlUsd` and `apr1d` may arrive as numeric strings and are returned as numbers. Fields
|
|
2283
|
+
* the SDK does not know are kept.
|
|
2284
|
+
*/
|
|
2285
|
+
function parsePoolListInfo(entry) {
|
|
2286
|
+
const row = asEntryRecord(entry);
|
|
2287
|
+
if (row === void 0) return entryRejected("the row is not an object");
|
|
2288
|
+
for (const key of [
|
|
2289
|
+
"token0",
|
|
2290
|
+
"token1",
|
|
2291
|
+
"poolRef"
|
|
2292
|
+
]) if (typeof row[key] !== "string") return entryRejected(`${key} is not a string`);
|
|
2293
|
+
if (row["contractVersion"] !== void 0 && row["contractVersion"] !== "v2") return entryRejected("contractVersion is not v2");
|
|
2294
|
+
const normalized = { ...row };
|
|
2295
|
+
for (const key of NUMERIC_POOL_FIELDS) {
|
|
2296
|
+
const raw = row[key];
|
|
2297
|
+
if (raw === void 0 && key !== "fee") continue;
|
|
2298
|
+
const value = finiteNumberOf(raw);
|
|
2299
|
+
if (value === void 0) return entryRejected(`${key} is not a number`);
|
|
2300
|
+
normalized[key] = value;
|
|
2301
|
+
}
|
|
2302
|
+
for (const key of OPTIONAL_POOL_STRINGS) if (row[key] !== void 0 && typeof row[key] !== "string") return entryRejected(`${key} is not a string`);
|
|
2303
|
+
for (const key of OPTIONAL_POOL_NUMBERS) {
|
|
2304
|
+
const value = row[key];
|
|
2305
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value))) return entryRejected(`${key} is not a number`);
|
|
2306
|
+
}
|
|
2307
|
+
return entryOk(normalized);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
//#endregion
|
|
2311
|
+
//#region src/utils/unique_key.ts
|
|
2312
|
+
/**
|
|
2313
|
+
* The `uniqueKey` prefix frontend_v2 and wallet_extension have always sent
|
|
2314
|
+
* (`galaswap-operation-<uuid>`). defi-backend does not depend on it: the chain-write verifier
|
|
2315
|
+
* only requires a non-empty key of at most 256 characters, and apply-results accepts
|
|
2316
|
+
* `[A-Za-z0-9._:-]{1,128}`; the only code that mints a prefixed key itself is the server-side v1
|
|
2317
|
+
* trade builder, which nothing reads back by prefix.
|
|
2318
|
+
*/
|
|
2319
|
+
const DEFAULT_UNIQUE_KEY_PREFIX = "galaswap-operation-";
|
|
2320
|
+
/** A canonical UUID is 36 characters; the apply-results key limit is 128. */
|
|
2321
|
+
const MAX_UNIQUE_KEY_PREFIX_LENGTH = 92;
|
|
2322
|
+
const UNIQUE_KEY_PREFIX_PATTERN = /^[A-Za-z0-9._:-]*$/u;
|
|
2323
|
+
/**
|
|
2324
|
+
* Validates a caller-supplied `uniqueKey` prefix. The whole key (`<prefix><uuid>`) must stay a
|
|
2325
|
+
* valid apply-results key, otherwise `confirm()` could never read the outcome of the write.
|
|
2326
|
+
*
|
|
2327
|
+
* @throws GSwapSDKError VALIDATION_ERROR when the prefix has characters outside `A-Za-z0-9._:-` or
|
|
2328
|
+
* is longer than 92 characters.
|
|
2329
|
+
*/
|
|
2330
|
+
function resolveUniqueKeyPrefix(prefix) {
|
|
2331
|
+
if (prefix === void 0) return DEFAULT_UNIQUE_KEY_PREFIX;
|
|
2332
|
+
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 });
|
|
2333
|
+
return prefix;
|
|
2334
|
+
}
|
|
2335
|
+
/** The swap backend's apply-results key constraint (`apply-results.dto.ts`). */
|
|
2336
|
+
const UNIQUE_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
2337
|
+
/**
|
|
2338
|
+
* Validates a caller-supplied write `uniqueKey`: 1 to 128 characters of `A-Za-z0-9._:-`, the
|
|
2339
|
+
* shape the apply-results route accepts, so the write's outcome can always be read back with
|
|
2340
|
+
* `confirm()`. A key is one attempt's identity: reuse it only to retry that same attempt.
|
|
2341
|
+
*
|
|
2342
|
+
* @throws GSwapSDKError VALIDATION_ERROR otherwise.
|
|
2343
|
+
*/
|
|
2344
|
+
function assertUniqueKey(uniqueKey) {
|
|
2345
|
+
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 });
|
|
2346
|
+
}
|
|
2347
|
+
/** A fresh per-attempt key: `<prefix><random UUID>`. */
|
|
2348
|
+
function createUniqueKey(prefix) {
|
|
2349
|
+
return `${prefix}${globalThis.crypto.randomUUID()}`;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
//#endregion
|
|
2353
|
+
//#region src/utils/dto_builders.ts
|
|
2354
|
+
const decimalString = (amount) => new bignumber_js.default(amount).toFixed();
|
|
2355
|
+
/**
|
|
2356
|
+
* Builds the RequestTrade DTO: `exactIn` sells that much of the input token (`sell0Qty` or
|
|
2357
|
+
* `sell1Qty`), `exactOut` buys that much of the output token (`buy1Qty` or `buy0Qty`), plus the
|
|
2358
|
+
* side's slippage bound and an optional `priceLimit`. Key order: `token0, token1, fee, uniqueKey,
|
|
2359
|
+
* <quantity>, amountOutMinimum | amountInMaximum, priceLimit`.
|
|
2360
|
+
*
|
|
2361
|
+
* @example
|
|
2362
|
+
* ```ts
|
|
2363
|
+
* const dto = buildRequestTradeDto({
|
|
2364
|
+
* token0: quote.token0Symbol, token1: quote.token1Symbol,
|
|
2365
|
+
* tokenInIsToken0: quote.tokenInIsToken0, fee: quote.fee,
|
|
2366
|
+
* uniqueKey: createUniqueKey('galaswap-operation-'),
|
|
2367
|
+
* exactIn: '100', amountOutMinimum: '14',
|
|
2368
|
+
* });
|
|
2369
|
+
* ```
|
|
2370
|
+
*/
|
|
2371
|
+
function buildRequestTradeDto(input) {
|
|
2372
|
+
validateFee(input.fee);
|
|
2373
|
+
assertCanonicalPair(input.token0, input.token1);
|
|
2374
|
+
assertUniqueKey(input.uniqueKey);
|
|
2375
|
+
const dto = {
|
|
2376
|
+
token0: input.token0,
|
|
2377
|
+
token1: input.token1,
|
|
2378
|
+
fee: input.fee,
|
|
2379
|
+
uniqueKey: input.uniqueKey
|
|
2380
|
+
};
|
|
2381
|
+
if ("exactIn" in input) {
|
|
2382
|
+
const exactIn = input.exactIn;
|
|
2383
|
+
if (exactIn === void 0) throw new GSwapSDKError("exactIn is required for an exact-input swap.", "VALIDATION_ERROR");
|
|
2384
|
+
validateNumericAmount(exactIn, "exactIn");
|
|
2385
|
+
dto[input.tokenInIsToken0 ? "sell0Qty" : "sell1Qty"] = decimalString(exactIn);
|
|
2386
|
+
if (input.amountOutMinimum !== void 0) {
|
|
2387
|
+
validateNumericAmount(input.amountOutMinimum, "amountOutMinimum", true);
|
|
2388
|
+
dto["amountOutMinimum"] = decimalString(input.amountOutMinimum);
|
|
2389
|
+
}
|
|
2390
|
+
} else {
|
|
2391
|
+
const exactOut = input.exactOut;
|
|
2392
|
+
if (exactOut === void 0) throw new GSwapSDKError("exactOut is required for an exact-output swap.", "VALIDATION_ERROR");
|
|
2393
|
+
validateNumericAmount(exactOut, "exactOut");
|
|
2394
|
+
dto[input.tokenInIsToken0 ? "buy1Qty" : "buy0Qty"] = decimalString(exactOut);
|
|
2395
|
+
if (input.amountInMaximum !== void 0) {
|
|
2396
|
+
validateNumericAmount(input.amountInMaximum, "amountInMaximum", true);
|
|
2397
|
+
dto["amountInMaximum"] = decimalString(input.amountInMaximum);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (input.priceLimit !== void 0) {
|
|
2401
|
+
validateNumericAmount(input.priceLimit, "priceLimit");
|
|
2402
|
+
dto["priceLimit"] = decimalString(input.priceLimit);
|
|
2403
|
+
}
|
|
2404
|
+
return dto;
|
|
2405
|
+
}
|
|
2406
|
+
function assertPoolPosition(input) {
|
|
2407
|
+
validateFee(input.fee);
|
|
2408
|
+
assertCanonicalPair(input.token0, input.token1);
|
|
2409
|
+
assertTickRange(input.tickLower, input.tickUpper, input.fee);
|
|
2410
|
+
assertUniqueKey(input.uniqueKey);
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Builds the AddLiquidity DTO: exactly one of `depositQuantityToken0` / `depositQuantityToken1`
|
|
2414
|
+
* (the other side is derived on chain). Key order: `token0, token1, fee, tickLower, tickUpper,
|
|
2415
|
+
* uniqueKey, <deposit>`.
|
|
2416
|
+
*/
|
|
2417
|
+
function buildAddLiquidityDto(input) {
|
|
2418
|
+
assertPoolPosition(input);
|
|
2419
|
+
const side = onlyOne(input.depositQuantityToken0, input.depositQuantityToken1, "depositQuantityToken0", "depositQuantityToken1");
|
|
2420
|
+
if (side === void 0) throw new GSwapSDKError("Provide exactly one of depositQuantityToken0 or depositQuantityToken1.", "VALIDATION_ERROR");
|
|
2421
|
+
validateNumericAmount(side.amount, side.name);
|
|
2422
|
+
return {
|
|
2423
|
+
token0: input.token0,
|
|
2424
|
+
token1: input.token1,
|
|
2425
|
+
fee: input.fee,
|
|
2426
|
+
tickLower: input.tickLower,
|
|
2427
|
+
tickUpper: input.tickUpper,
|
|
2428
|
+
uniqueKey: input.uniqueKey,
|
|
2429
|
+
[side.name]: decimalString(side.amount)
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Builds the RemoveLiquidity DTO: at most one of `withdrawalQuantityToken0` /
|
|
2434
|
+
* `withdrawalQuantityToken1`; omitting both closes the position and sweeps its fees.
|
|
2435
|
+
*/
|
|
2436
|
+
function buildRemoveLiquidityDto(input) {
|
|
2437
|
+
assertPoolPosition(input);
|
|
2438
|
+
const side = onlyOne(input.withdrawalQuantityToken0, input.withdrawalQuantityToken1, "withdrawalQuantityToken0", "withdrawalQuantityToken1", true);
|
|
2439
|
+
if (side !== void 0) validateNumericAmount(side.amount, side.name);
|
|
2440
|
+
return {
|
|
2441
|
+
token0: input.token0,
|
|
2442
|
+
token1: input.token1,
|
|
2443
|
+
fee: input.fee,
|
|
2444
|
+
tickLower: input.tickLower,
|
|
2445
|
+
tickUpper: input.tickUpper,
|
|
2446
|
+
uniqueKey: input.uniqueKey,
|
|
2447
|
+
...side === void 0 ? {} : { [side.name]: decimalString(side.amount) }
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
/** Builds the CollectPositionFees DTO (the contract sweeps all of a position's fees). */
|
|
2451
|
+
function buildCollectPositionFeesDto(input) {
|
|
2452
|
+
assertPoolPosition(input);
|
|
2453
|
+
return {
|
|
2454
|
+
token0: input.token0,
|
|
2455
|
+
token1: input.token1,
|
|
2456
|
+
fee: input.fee,
|
|
2457
|
+
tickLower: input.tickLower,
|
|
2458
|
+
tickUpper: input.tickUpper,
|
|
2459
|
+
uniqueKey: input.uniqueKey
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Builds the CreatePool DTO. Key order: `token0Key, token1Key, fee, startingPrice |
|
|
2464
|
+
* startingSqrtPrice, token0Symbol, token1Symbol, uniqueKey, name, isPrivate, privateAccess`.
|
|
2465
|
+
* Prices are those of the canonical pair (token1 per token0); a price quoted the other way round
|
|
2466
|
+
* is inverted by the caller before it gets here.
|
|
2467
|
+
*/
|
|
2468
|
+
function buildCreatePoolDto(input) {
|
|
2469
|
+
validateFee(input.fee);
|
|
2470
|
+
assertUniqueKey(input.uniqueKey);
|
|
2471
|
+
assertCanonicalPair(input.token0Symbol, input.token1Symbol);
|
|
2472
|
+
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 });
|
|
2473
|
+
for (const key of [input.token0Key, input.token1Key]) assertClassKey(key);
|
|
2474
|
+
const price = onlyOne(input.startingPrice, input.startingSqrtPrice, "startingPrice", "startingSqrtPrice");
|
|
2475
|
+
if (price === void 0) throw new GSwapSDKError("Provide exactly one of startingPrice or startingSqrtPrice.", "VALIDATION_ERROR");
|
|
2476
|
+
if (typeof price.amount === "number") throw new GSwapSDKError(`Invalid ${price.name}: use a decimal string or BigNumber, not a JavaScript number`, "VALIDATION_ERROR");
|
|
2477
|
+
const parsed = new bignumber_js.default(price.amount);
|
|
2478
|
+
if (!parsed.isFinite() || parsed.isLessThanOrEqualTo(0)) throw new GSwapSDKError("Starting price must be finite and positive.", "VALIDATION_ERROR");
|
|
2479
|
+
return {
|
|
2480
|
+
token0Key: input.token0Key,
|
|
2481
|
+
token1Key: input.token1Key,
|
|
2482
|
+
fee: input.fee,
|
|
2483
|
+
[price.name]: parsed.toFixed(),
|
|
2484
|
+
token0Symbol: input.token0Symbol,
|
|
2485
|
+
token1Symbol: input.token1Symbol,
|
|
2486
|
+
uniqueKey: input.uniqueKey,
|
|
2487
|
+
...input.name === void 0 || input.name === "" ? {} : { name: input.name },
|
|
2488
|
+
...input.isPrivate === void 0 ? {} : { isPrivate: input.isPrivate },
|
|
2489
|
+
...input.privateAccess === void 0 ? {} : { privateAccess: input.privateAccess }
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
function assertClassKey(key) {
|
|
2493
|
+
for (const part of [
|
|
2494
|
+
"collection",
|
|
2495
|
+
"category",
|
|
2496
|
+
"type",
|
|
2497
|
+
"additionalKey"
|
|
2498
|
+
]) if (typeof key[part] !== "string" || key[part].length === 0) throw new GSwapSDKError(`Token class key is missing ${part}.`, "VALIDATION_ERROR", { key });
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Of two mutually exclusive fields, the one that is set (`undefined` when neither is);
|
|
2502
|
+
* both set is a `VALIDATION_ERROR`, and so is neither unless `allowNeither`.
|
|
2503
|
+
*/
|
|
2504
|
+
function onlyOne(first, second, firstName, secondName, allowNeither = false) {
|
|
2505
|
+
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");
|
|
2506
|
+
if (first !== void 0) return {
|
|
2507
|
+
name: firstName,
|
|
2508
|
+
amount: first
|
|
2509
|
+
};
|
|
2510
|
+
if (second !== void 0) return {
|
|
2511
|
+
name: secondName,
|
|
2512
|
+
amount: second
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function invalidDto(method, field, expected) {
|
|
2516
|
+
return new GSwapSDKError(`${method} DTO field ${field} must be ${expected}.`, "VALIDATION_ERROR", {
|
|
2517
|
+
type: "INVALID_DTO",
|
|
2518
|
+
method,
|
|
2519
|
+
field
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Rebuilds a DTO from its own fields with the builder of its method: the canonical form of the
|
|
2524
|
+
* DTO, with the same validation. Key order and decimal text are the builder's.
|
|
2525
|
+
*/
|
|
2526
|
+
function rebuildV2Dto(method, dto) {
|
|
2527
|
+
const text = (field) => {
|
|
2528
|
+
const value = dto[field];
|
|
2529
|
+
if (typeof value !== "string") throw invalidDto(method, field, "a string");
|
|
2530
|
+
return value;
|
|
2531
|
+
};
|
|
2532
|
+
const integer = (field) => {
|
|
2533
|
+
const value = dto[field];
|
|
2534
|
+
if (typeof value !== "number") throw invalidDto(method, field, "a number");
|
|
2535
|
+
return value;
|
|
2536
|
+
};
|
|
2537
|
+
const optionalText = (field) => dto[field] === void 0 ? void 0 : text(field);
|
|
2538
|
+
const classKey = (field) => {
|
|
2539
|
+
const value = dto[field];
|
|
2540
|
+
if (typeof value !== "object" || value === null) throw invalidDto(method, field, "a class key");
|
|
2541
|
+
const key = value;
|
|
2542
|
+
const part = (name) => {
|
|
2543
|
+
if (typeof key[name] !== "string") throw invalidDto(method, `${field}.${name}`, "a string");
|
|
2544
|
+
return key[name];
|
|
2545
|
+
};
|
|
2546
|
+
return {
|
|
2547
|
+
collection: part("collection"),
|
|
2548
|
+
category: part("category"),
|
|
2549
|
+
type: part("type"),
|
|
2550
|
+
additionalKey: part("additionalKey")
|
|
2551
|
+
};
|
|
2552
|
+
};
|
|
2553
|
+
const base = () => ({
|
|
2554
|
+
token0: text("token0"),
|
|
2555
|
+
token1: text("token1"),
|
|
2556
|
+
fee: integer("fee")
|
|
2557
|
+
});
|
|
2558
|
+
const uniqueKey = text("uniqueKey");
|
|
2559
|
+
const position = () => ({
|
|
2560
|
+
...base(),
|
|
2561
|
+
uniqueKey,
|
|
2562
|
+
tickLower: integer("tickLower"),
|
|
2563
|
+
tickUpper: integer("tickUpper")
|
|
2564
|
+
});
|
|
2565
|
+
switch (method) {
|
|
2566
|
+
case "RequestTrade": {
|
|
2567
|
+
const qty = [
|
|
2568
|
+
"sell0Qty",
|
|
2569
|
+
"sell1Qty",
|
|
2570
|
+
"buy0Qty",
|
|
2571
|
+
"buy1Qty"
|
|
2572
|
+
].filter((f) => dto[f] !== void 0);
|
|
2573
|
+
const [field] = qty;
|
|
2574
|
+
if (qty.length !== 1 || field === void 0) throw invalidDto(method, "sell0Qty|sell1Qty|buy0Qty|buy1Qty", "present exactly once");
|
|
2575
|
+
const tokenInIsToken0 = field === "sell0Qty" || field === "buy1Qty";
|
|
2576
|
+
const shared = {
|
|
2577
|
+
...base(),
|
|
2578
|
+
uniqueKey,
|
|
2579
|
+
tokenInIsToken0
|
|
2580
|
+
};
|
|
2581
|
+
const priceLimit = optionalText("priceLimit");
|
|
2582
|
+
return field.startsWith("sell") ? buildRequestTradeDto({
|
|
2583
|
+
...shared,
|
|
2584
|
+
exactIn: text(field),
|
|
2585
|
+
amountOutMinimum: optionalText("amountOutMinimum"),
|
|
2586
|
+
priceLimit
|
|
2587
|
+
}) : buildRequestTradeDto({
|
|
2588
|
+
...shared,
|
|
2589
|
+
exactOut: text(field),
|
|
2590
|
+
amountInMaximum: optionalText("amountInMaximum"),
|
|
2591
|
+
priceLimit
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
case "AddLiquidity": {
|
|
2595
|
+
const token0Side = optionalText("depositQuantityToken0");
|
|
2596
|
+
const token1Side = optionalText("depositQuantityToken1");
|
|
2597
|
+
return buildAddLiquidityDto({
|
|
2598
|
+
...position(),
|
|
2599
|
+
...token0Side === void 0 ? { depositQuantityToken1: token1Side } : { depositQuantityToken0: token0Side }
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
case "RemoveLiquidity": return buildRemoveLiquidityDto({
|
|
2603
|
+
...position(),
|
|
2604
|
+
withdrawalQuantityToken0: optionalText("withdrawalQuantityToken0"),
|
|
2605
|
+
withdrawalQuantityToken1: optionalText("withdrawalQuantityToken1")
|
|
2606
|
+
});
|
|
2607
|
+
case "CollectPositionFees": return buildCollectPositionFeesDto(position());
|
|
2608
|
+
case "CreatePool": {
|
|
2609
|
+
const price = optionalText("startingPrice");
|
|
2610
|
+
const sqrtPrice = optionalText("startingSqrtPrice");
|
|
2611
|
+
const privateAccess = dto["privateAccess"];
|
|
2612
|
+
if (privateAccess !== void 0 && (!Array.isArray(privateAccess) || privateAccess.some((entry) => typeof entry !== "string"))) throw invalidDto(method, "privateAccess", "an array of strings");
|
|
2613
|
+
if (dto["isPrivate"] !== void 0 && typeof dto["isPrivate"] !== "boolean") throw invalidDto(method, "isPrivate", "a boolean");
|
|
2614
|
+
return buildCreatePoolDto({
|
|
2615
|
+
token0Key: classKey("token0Key"),
|
|
2616
|
+
token1Key: classKey("token1Key"),
|
|
2617
|
+
token0Symbol: text("token0Symbol"),
|
|
2618
|
+
token1Symbol: text("token1Symbol"),
|
|
2619
|
+
fee: integer("fee"),
|
|
2620
|
+
uniqueKey,
|
|
2621
|
+
name: optionalText("name"),
|
|
2622
|
+
isPrivate: dto["isPrivate"],
|
|
2623
|
+
privateAccess,
|
|
2624
|
+
...price === void 0 ? { startingSqrtPrice: sqrtPrice } : { startingPrice: price }
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
1433
2628
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
"
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
"token0Tvl",
|
|
1449
|
-
"token1Tvl"
|
|
1450
|
-
].every((key) => isOptionalString(row[key])) && [
|
|
1451
|
-
"tick",
|
|
1452
|
-
"tvlUsd",
|
|
1453
|
-
"volume1d",
|
|
1454
|
-
"volume7d",
|
|
1455
|
-
"volume30d",
|
|
1456
|
-
"trades1d",
|
|
1457
|
-
"fee24h",
|
|
1458
|
-
"apr1d",
|
|
1459
|
-
"token0Price",
|
|
1460
|
-
"token1Price"
|
|
1461
|
-
].every((key) => isOptionalNumber(row[key]));
|
|
1462
|
-
}
|
|
1463
|
-
function isFiniteNumber(value) {
|
|
1464
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
1465
|
-
}
|
|
1466
|
-
function isOptionalString(value) {
|
|
1467
|
-
return value === void 0 || typeof value === "string";
|
|
1468
|
-
}
|
|
1469
|
-
function isOptionalNumber(value) {
|
|
1470
|
-
return value === void 0 || isFiniteNumber(value);
|
|
2629
|
+
/**
|
|
2630
|
+
* Asserts that `dto` is EXACTLY what the builder of `method` produces for its own fields — the
|
|
2631
|
+
* same fields, canonical decimal text, builder key order, nothing extra (no `signature`). The
|
|
2632
|
+
* `submit*` methods use it so that what an app showed a user (a DTO it built) is byte for byte
|
|
2633
|
+
* what gets signed; a DTO edited by hand, or built elsewhere, is refused rather than signed.
|
|
2634
|
+
*
|
|
2635
|
+
* @throws GSwapSDKError VALIDATION_ERROR (`details.type` = `INVALID_DTO` or `DTO_NOT_CANONICAL`).
|
|
2636
|
+
*/
|
|
2637
|
+
function assertCanonicalV2Dto(method, dto) {
|
|
2638
|
+
const rebuilt = rebuildV2Dto(method, dto);
|
|
2639
|
+
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", {
|
|
2640
|
+
type: "DTO_NOT_CANONICAL",
|
|
2641
|
+
method
|
|
2642
|
+
});
|
|
1471
2643
|
}
|
|
1472
2644
|
|
|
1473
2645
|
//#endregion
|
|
1474
2646
|
//#region src/classes/positions.ts
|
|
2647
|
+
/** The pool position a liquidity DTO names, as its confirmation needs it. */
|
|
2648
|
+
function identityOf(dto) {
|
|
2649
|
+
return {
|
|
2650
|
+
token0: dto.token0,
|
|
2651
|
+
token1: dto.token1,
|
|
2652
|
+
fee: dto.fee,
|
|
2653
|
+
tickLower: dto.tickLower,
|
|
2654
|
+
tickUpper: dto.tickUpper
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
1475
2657
|
/**
|
|
1476
2658
|
* Manages current-contract concentrated-liquidity positions.
|
|
1477
2659
|
*
|
|
@@ -1487,26 +2669,32 @@ var Positions = class {
|
|
|
1487
2669
|
symbols;
|
|
1488
2670
|
signer;
|
|
1489
2671
|
walletAddress;
|
|
1490
|
-
constructor(gateway, symbols, signer, walletAddress) {
|
|
2672
|
+
constructor(gateway, symbols, signer, walletAddress, options = {}) {
|
|
1491
2673
|
this.gateway = gateway;
|
|
1492
2674
|
this.symbols = symbols;
|
|
1493
2675
|
this.signer = signer;
|
|
1494
2676
|
this.walletAddress = walletAddress;
|
|
1495
|
-
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4);
|
|
2677
|
+
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4, gateway.wire);
|
|
2678
|
+
this.uniqueKeyPrefix = resolveUniqueKeyPrefix(options.uniqueKeyPrefix);
|
|
2679
|
+
this.onInvalidEntry = options.onInvalidEntry;
|
|
1496
2680
|
}
|
|
1497
2681
|
http;
|
|
2682
|
+
uniqueKeyPrefix;
|
|
2683
|
+
onInvalidEntry;
|
|
1498
2684
|
/**
|
|
1499
2685
|
* Gets all current-contract positions owned by an address.
|
|
1500
2686
|
*
|
|
1501
2687
|
* @param owner - GalaChain owner identity.
|
|
1502
|
-
* @returns Enriched positions from the v2 trade backend.
|
|
2688
|
+
* @returns Enriched positions from the v2 trade backend, token images included. A row that is
|
|
2689
|
+
* not a usable position is skipped and reported to `onInvalidEntry`.
|
|
1503
2690
|
* @example
|
|
1504
2691
|
* ```typescript
|
|
1505
2692
|
* const positions = await gSwap.positions.getUserPositions('client|012345678901234567890123');
|
|
1506
2693
|
* ```
|
|
1507
2694
|
*/
|
|
1508
2695
|
async getUserPositions(owner) {
|
|
1509
|
-
|
|
2696
|
+
const response = await this.get("/positions", { user: owner });
|
|
2697
|
+
return readEntries("positions", response.data, parsePosition, this.onInvalidEntry);
|
|
1510
2698
|
}
|
|
1511
2699
|
/**
|
|
1512
2700
|
* Gets one position, normalizing a caller-supplied reversed pair to canonical symbol order.
|
|
@@ -1523,17 +2711,65 @@ var Positions = class {
|
|
|
1523
2711
|
* ```
|
|
1524
2712
|
*/
|
|
1525
2713
|
async getPosition(args, signal) {
|
|
2714
|
+
const response = await this.getNullable("/position", await this.positionParams(args), signal);
|
|
2715
|
+
return response === null ? null : readPositionBody(response.data);
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Reads one position and classifies the outcome instead of throwing: `found` with the position
|
|
2719
|
+
* (fees included), `not_found` (the backend's HTTP 404, which it sends only when the chain
|
|
2720
|
+
* itself has no such position) or `unknown` (any other failure, carried as `error`). Use it
|
|
2721
|
+
* to confirm a write: `unknown` is "no evidence either way", never a confirmed absence.
|
|
2722
|
+
*
|
|
2723
|
+
* Only the caller's own abort rethrows. With a pass-through resolver ({@link CanonicalSymbols})
|
|
2724
|
+
* the pair and ticks go to the backend exactly as given and the backend orients them.
|
|
2725
|
+
*
|
|
2726
|
+
* @example
|
|
2727
|
+
* ```typescript
|
|
2728
|
+
* const read = await gSwap.positions.readPosition({
|
|
2729
|
+
* token0: 'GALA', token1: 'GUSDC', owner: 'client|012345678901234567890123',
|
|
2730
|
+
* fee: 3000, tickLower: -19200, tickUpper: 12000,
|
|
2731
|
+
* });
|
|
2732
|
+
* if (read.kind === 'found') console.log(read.data.fees0);
|
|
2733
|
+
* ```
|
|
2734
|
+
*/
|
|
2735
|
+
async readPosition(args, signal) {
|
|
2736
|
+
try {
|
|
2737
|
+
const position = await this.getPosition(args, signal);
|
|
2738
|
+
return position === null ? { kind: "not_found" } : {
|
|
2739
|
+
kind: "found",
|
|
2740
|
+
data: position
|
|
2741
|
+
};
|
|
2742
|
+
} catch (error) {
|
|
2743
|
+
if ((signal === null || signal === void 0 ? void 0 : signal.aborted) === true) throw error;
|
|
2744
|
+
return {
|
|
2745
|
+
kind: "unknown",
|
|
2746
|
+
error: error instanceof GSwapSDKError ? error : new GSwapSDKError("Position request failed.", "HTTP_ERROR", { cause: error })
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* The `/position` query: canonical symbols and canonical ticks after a client-side flip, or,
|
|
2752
|
+
* for a pass-through resolver, the caller's references and ticks untouched.
|
|
2753
|
+
*/
|
|
2754
|
+
async positionParams(args) {
|
|
2755
|
+
if (this.symbols.passthrough === true) return {
|
|
2756
|
+
token0: rawTokenOf(args.token0),
|
|
2757
|
+
token1: rawTokenOf(args.token1),
|
|
2758
|
+
fee: `${args.fee}`,
|
|
2759
|
+
owner: args.owner,
|
|
2760
|
+
tickLower: `${args.tickLower}`,
|
|
2761
|
+
tickUpper: `${args.tickUpper}`
|
|
2762
|
+
};
|
|
1526
2763
|
const pair = await this.symbols.orderPair(args.token0, args.token1);
|
|
1527
2764
|
const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
|
|
1528
|
-
|
|
2765
|
+
return {
|
|
1529
2766
|
token0: pair.token0.symbol,
|
|
1530
2767
|
token1: pair.token1.symbol,
|
|
1531
2768
|
fee: `${args.fee}`,
|
|
1532
2769
|
owner: args.owner,
|
|
1533
2770
|
tickLower: `${ticks.tickLower}`,
|
|
1534
2771
|
tickUpper: `${ticks.tickUpper}`
|
|
1535
|
-
}
|
|
1536
|
-
return response === null ? null : this.mapPosition(response.data);
|
|
2772
|
+
};
|
|
1537
2773
|
}
|
|
1538
2774
|
/**
|
|
1539
2775
|
* Estimates the other token amount and resulting liquidity for a deposit.
|
|
@@ -1551,8 +2787,8 @@ var Positions = class {
|
|
|
1551
2787
|
async estimateAddLiquidity(args) {
|
|
1552
2788
|
validateNumericAmount(args.amount, "amount");
|
|
1553
2789
|
return (await this.get("/add-liq-estimate", {
|
|
1554
|
-
token0:
|
|
1555
|
-
token1:
|
|
2790
|
+
token0: rawTokenOf(args.token0),
|
|
2791
|
+
token1: rawTokenOf(args.token1),
|
|
1556
2792
|
fee: `${args.fee}`,
|
|
1557
2793
|
tickLower: `${args.tickLower}`,
|
|
1558
2794
|
tickUpper: `${args.tickUpper}`,
|
|
@@ -1576,8 +2812,8 @@ var Positions = class {
|
|
|
1576
2812
|
async estimateRemoveLiquidity(args) {
|
|
1577
2813
|
validateNumericAmount(args.liquidity, "liquidity");
|
|
1578
2814
|
return (await this.get("/remove-liq-estimate", {
|
|
1579
|
-
token0:
|
|
1580
|
-
token1:
|
|
2815
|
+
token0: rawTokenOf(args.token0),
|
|
2816
|
+
token1: rawTokenOf(args.token1),
|
|
1581
2817
|
fee: `${args.fee}`,
|
|
1582
2818
|
tickLower: `${args.tickLower}`,
|
|
1583
2819
|
tickUpper: `${args.tickUpper}`,
|
|
@@ -1599,30 +2835,54 @@ var Positions = class {
|
|
|
1599
2835
|
* ```
|
|
1600
2836
|
*/
|
|
1601
2837
|
async addLiquidityByTicks(args) {
|
|
2838
|
+
return this.submitAddLiquidity(await this.buildAddLiquidity(args));
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Builds the AddLiquidity DTO that {@link addLiquidityByTicks} would sign, without signing or
|
|
2842
|
+
* sending it: to show in an approval popup, or to sign elsewhere. Submit exactly this DTO with
|
|
2843
|
+
* {@link submitAddLiquidity}. The pure counterpart, with canonical inputs and no resolution,
|
|
2844
|
+
* is `buildAddLiquidityDto`.
|
|
2845
|
+
*
|
|
2846
|
+
* @example
|
|
2847
|
+
* ```typescript
|
|
2848
|
+
* const dto = await gSwap.positions.buildAddLiquidity({
|
|
2849
|
+
* token0: 'GALA', token1: 'GUSDC', fee: 3000,
|
|
2850
|
+
* tickLower: -19200, tickUpper: 12000, amount: '100', amountIsToken0: true,
|
|
2851
|
+
* });
|
|
2852
|
+
* ```
|
|
2853
|
+
*/
|
|
2854
|
+
async buildAddLiquidity(args) {
|
|
1602
2855
|
validateFee(args.fee);
|
|
1603
2856
|
const pair = await this.symbols.orderPair(args.token0, args.token1);
|
|
1604
2857
|
const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
|
|
1605
2858
|
assertTickRange(ticks.tickLower, ticks.tickUpper, args.fee);
|
|
1606
2859
|
validateNumericAmount(args.amount, "amount");
|
|
1607
|
-
const
|
|
1608
|
-
const dto = {
|
|
2860
|
+
const base = {
|
|
1609
2861
|
token0: pair.token0.symbol,
|
|
1610
2862
|
token1: pair.token1.symbol,
|
|
1611
2863
|
fee: args.fee,
|
|
1612
2864
|
tickLower: ticks.tickLower,
|
|
1613
2865
|
tickUpper: ticks.tickUpper,
|
|
1614
|
-
uniqueKey: this.uniqueKey
|
|
1615
|
-
...pair.flipped !== args.amountIsToken0 ? { depositQuantityToken0: amount } : { depositQuantityToken1: amount }
|
|
2866
|
+
uniqueKey: this.uniqueKeyFor(args.uniqueKey)
|
|
1616
2867
|
};
|
|
2868
|
+
return pair.flipped !== args.amountIsToken0 ? buildAddLiquidityDto({
|
|
2869
|
+
...base,
|
|
2870
|
+
depositQuantityToken0: args.amount
|
|
2871
|
+
}) : buildAddLiquidityDto({
|
|
2872
|
+
...base,
|
|
2873
|
+
depositQuantityToken1: args.amount
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Signs and submits a DTO built by {@link buildAddLiquidity} or `buildAddLiquidityDto`; it must
|
|
2878
|
+
* be exactly what the builder produces. The returned transaction confirms by proving the
|
|
2879
|
+
* position's liquidity grew.
|
|
2880
|
+
*/
|
|
2881
|
+
async submitAddLiquidity(dto) {
|
|
2882
|
+
assertCanonicalV2Dto("AddLiquidity", dto);
|
|
1617
2883
|
return this.submit("AddLiquidity", dto, {
|
|
1618
2884
|
kind: "add",
|
|
1619
|
-
identity:
|
|
1620
|
-
token0: pair.token0.symbol,
|
|
1621
|
-
token1: pair.token1.symbol,
|
|
1622
|
-
fee: args.fee,
|
|
1623
|
-
tickLower: ticks.tickLower,
|
|
1624
|
-
tickUpper: ticks.tickUpper
|
|
1625
|
-
}
|
|
2885
|
+
identity: identityOf(dto)
|
|
1626
2886
|
});
|
|
1627
2887
|
}
|
|
1628
2888
|
/**
|
|
@@ -1657,7 +2917,8 @@ var Positions = class {
|
|
|
1657
2917
|
tickLower,
|
|
1658
2918
|
tickUpper,
|
|
1659
2919
|
amount: args.amount,
|
|
1660
|
-
amountIsToken0: args.amountIsToken0
|
|
2920
|
+
amountIsToken0: args.amountIsToken0,
|
|
2921
|
+
uniqueKey: args.uniqueKey
|
|
1661
2922
|
});
|
|
1662
2923
|
}
|
|
1663
2924
|
/**
|
|
@@ -1675,6 +2936,14 @@ var Positions = class {
|
|
|
1675
2936
|
* ```
|
|
1676
2937
|
*/
|
|
1677
2938
|
async removeLiquidity(args) {
|
|
2939
|
+
return this.submitRemoveLiquidity(await this.buildRemoveLiquidity(args));
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Builds the RemoveLiquidity DTO that {@link removeLiquidity} would sign, without signing or
|
|
2943
|
+
* sending it. Submit exactly this DTO with {@link submitRemoveLiquidity}. The pure counterpart
|
|
2944
|
+
* is `buildRemoveLiquidityDto`.
|
|
2945
|
+
*/
|
|
2946
|
+
async buildRemoveLiquidity(args) {
|
|
1678
2947
|
validateFee(args.fee);
|
|
1679
2948
|
const pair = await this.symbols.orderPair(args.token0, args.token1);
|
|
1680
2949
|
const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
|
|
@@ -1682,26 +2951,29 @@ var Positions = class {
|
|
|
1682
2951
|
if (args.amount0 !== void 0 && args.amount1 !== void 0) throw new GSwapSDKError("Provide at most one of amount0 or amount1.", "VALIDATION_ERROR");
|
|
1683
2952
|
if (args.amount0 !== void 0) validateNumericAmount(args.amount0, "amount0");
|
|
1684
2953
|
if (args.amount1 !== void 0) validateNumericAmount(args.amount1, "amount1");
|
|
1685
|
-
const
|
|
1686
|
-
|
|
2954
|
+
const [canonical0, canonical1] = pair.flipped ? [args.amount1, args.amount0] : [args.amount0, args.amount1];
|
|
2955
|
+
return buildRemoveLiquidityDto({
|
|
1687
2956
|
token0: pair.token0.symbol,
|
|
1688
2957
|
token1: pair.token1.symbol,
|
|
1689
2958
|
fee: args.fee,
|
|
1690
2959
|
tickLower: ticks.tickLower,
|
|
1691
2960
|
tickUpper: ticks.tickUpper,
|
|
1692
|
-
uniqueKey: this.uniqueKey
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2961
|
+
uniqueKey: this.uniqueKeyFor(args.uniqueKey),
|
|
2962
|
+
withdrawalQuantityToken0: canonical0,
|
|
2963
|
+
withdrawalQuantityToken1: canonical1
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Signs and submits a DTO built by {@link buildRemoveLiquidity} or `buildRemoveLiquidityDto`;
|
|
2968
|
+
* it must be exactly what the builder produces. A DTO with no withdrawal quantity closes the
|
|
2969
|
+
* position, and the returned transaction then confirms by proving the position is gone.
|
|
2970
|
+
*/
|
|
2971
|
+
async submitRemoveLiquidity(dto) {
|
|
2972
|
+
assertCanonicalV2Dto("RemoveLiquidity", dto);
|
|
2973
|
+
const isFullClose = dto.withdrawalQuantityToken0 === void 0 && dto.withdrawalQuantityToken1 === void 0;
|
|
1696
2974
|
return this.submit("RemoveLiquidity", dto, {
|
|
1697
2975
|
kind: isFullClose ? "removeFull" : "removePartial",
|
|
1698
|
-
identity:
|
|
1699
|
-
token0: pair.token0.symbol,
|
|
1700
|
-
token1: pair.token1.symbol,
|
|
1701
|
-
fee: args.fee,
|
|
1702
|
-
tickLower: ticks.tickLower,
|
|
1703
|
-
tickUpper: ticks.tickUpper
|
|
1704
|
-
}
|
|
2976
|
+
identity: identityOf(dto)
|
|
1705
2977
|
});
|
|
1706
2978
|
}
|
|
1707
2979
|
/**
|
|
@@ -1718,27 +2990,36 @@ var Positions = class {
|
|
|
1718
2990
|
* ```
|
|
1719
2991
|
*/
|
|
1720
2992
|
async collectPositionFees(args) {
|
|
2993
|
+
return this.submitCollectPositionFees(await this.buildCollectPositionFees(args));
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Builds the CollectPositionFees DTO that {@link collectPositionFees} would sign, without
|
|
2997
|
+
* signing or sending it. Submit exactly this DTO with {@link submitCollectPositionFees}. The
|
|
2998
|
+
* pure counterpart is `buildCollectPositionFeesDto`.
|
|
2999
|
+
*/
|
|
3000
|
+
async buildCollectPositionFees(args) {
|
|
1721
3001
|
validateFee(args.fee);
|
|
1722
3002
|
const pair = await this.symbols.orderPair(args.token0, args.token1);
|
|
1723
3003
|
const ticks = this.canonicalTicks(args.tickLower, args.tickUpper, pair.flipped);
|
|
1724
3004
|
assertTickRange(ticks.tickLower, ticks.tickUpper, args.fee);
|
|
1725
|
-
|
|
3005
|
+
return buildCollectPositionFeesDto({
|
|
1726
3006
|
token0: pair.token0.symbol,
|
|
1727
3007
|
token1: pair.token1.symbol,
|
|
1728
3008
|
fee: args.fee,
|
|
1729
3009
|
tickLower: ticks.tickLower,
|
|
1730
3010
|
tickUpper: ticks.tickUpper,
|
|
1731
|
-
uniqueKey: this.uniqueKey
|
|
1732
|
-
};
|
|
3011
|
+
uniqueKey: this.uniqueKeyFor(args.uniqueKey)
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Signs and submits a DTO built by {@link buildCollectPositionFees} or
|
|
3016
|
+
* `buildCollectPositionFeesDto`; it must be exactly what the builder produces.
|
|
3017
|
+
*/
|
|
3018
|
+
async submitCollectPositionFees(dto) {
|
|
3019
|
+
assertCanonicalV2Dto("CollectPositionFees", dto);
|
|
1733
3020
|
return this.submit("CollectPositionFees", dto, {
|
|
1734
3021
|
kind: "collect",
|
|
1735
|
-
identity:
|
|
1736
|
-
token0: pair.token0.symbol,
|
|
1737
|
-
token1: pair.token1.symbol,
|
|
1738
|
-
fee: args.fee,
|
|
1739
|
-
tickLower: ticks.tickLower,
|
|
1740
|
-
tickUpper: ticks.tickUpper
|
|
1741
|
-
}
|
|
3022
|
+
identity: identityOf(dto)
|
|
1742
3023
|
});
|
|
1743
3024
|
}
|
|
1744
3025
|
/**
|
|
@@ -1763,7 +3044,15 @@ var Positions = class {
|
|
|
1763
3044
|
* ```
|
|
1764
3045
|
*/
|
|
1765
3046
|
async createPool(args) {
|
|
1766
|
-
|
|
3047
|
+
return this.submitCreatePool(await this.buildCreatePool(args));
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Builds the CreatePool DTO that {@link createPool} would sign, without signing or sending it:
|
|
3051
|
+
* symbols resolved or proposed, the pair put in canonical order and the price inverted when it
|
|
3052
|
+
* was reversed. Submit exactly this DTO with {@link submitCreatePool}. The pure counterpart,
|
|
3053
|
+
* with canonical inputs, is `buildCreatePoolDto`.
|
|
3054
|
+
*/
|
|
3055
|
+
async buildCreatePool(args) {
|
|
1767
3056
|
const hasPrice = args.startingPrice !== void 0;
|
|
1768
3057
|
if (hasPrice === (args.startingSqrtPrice !== void 0)) throw new GSwapSDKError("Provide exactly one of startingPrice or startingSqrtPrice.", "VALIDATION_ERROR");
|
|
1769
3058
|
validateFee(args.fee);
|
|
@@ -1775,22 +3064,38 @@ var Positions = class {
|
|
|
1775
3064
|
const sourcePrice = new bignumber_js.default(sourcePriceInput);
|
|
1776
3065
|
if (!sourcePrice.isFinite() || sourcePrice.isLessThanOrEqualTo(0)) throw new GSwapSDKError("Starting price must be finite and positive.", "VALIDATION_ERROR");
|
|
1777
3066
|
const canonicalPrice = flipped ? new bignumber_js.default(1).dividedBy(sourcePrice) : sourcePrice;
|
|
1778
|
-
const
|
|
3067
|
+
const shared = {
|
|
1779
3068
|
token0Key: token0.classKey,
|
|
1780
3069
|
token1Key: token1.classKey,
|
|
1781
3070
|
token0Symbol: token0.symbol,
|
|
1782
3071
|
token1Symbol: token1.symbol,
|
|
1783
3072
|
fee: args.fee,
|
|
1784
|
-
uniqueKey: this.uniqueKey
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
3073
|
+
uniqueKey: this.uniqueKeyFor(args.uniqueKey),
|
|
3074
|
+
name: args.name,
|
|
3075
|
+
isPrivate: args.isPrivate,
|
|
3076
|
+
privateAccess: args.privateAccess
|
|
1788
3077
|
};
|
|
3078
|
+
return hasPrice ? buildCreatePoolDto({
|
|
3079
|
+
...shared,
|
|
3080
|
+
startingPrice: canonicalPrice
|
|
3081
|
+
}) : buildCreatePoolDto({
|
|
3082
|
+
...shared,
|
|
3083
|
+
startingSqrtPrice: canonicalPrice
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Signs and submits a DTO built by {@link buildCreatePool} or `buildCreatePoolDto`; it must be
|
|
3088
|
+
* exactly what the builder produces. The returned transaction confirms by the pool existing, and
|
|
3089
|
+
* the cached symbol registry is dropped, since the pool may have registered new symbols.
|
|
3090
|
+
*/
|
|
3091
|
+
async submitCreatePool(dto) {
|
|
3092
|
+
var _this$symbols$invalid, _this$symbols;
|
|
3093
|
+
assertCanonicalV2Dto("CreatePool", dto);
|
|
1789
3094
|
const transaction = await this.submit("CreatePool", dto, {
|
|
1790
3095
|
kind: "createPool",
|
|
1791
|
-
token0:
|
|
1792
|
-
token1:
|
|
1793
|
-
fee:
|
|
3096
|
+
token0: dto.token0Symbol,
|
|
3097
|
+
token1: dto.token1Symbol,
|
|
3098
|
+
fee: dto.fee
|
|
1794
3099
|
});
|
|
1795
3100
|
(_this$symbols$invalid = (_this$symbols = this.symbols).invalidate) === null || _this$symbols$invalid === void 0 || _this$symbols$invalid.call(_this$symbols);
|
|
1796
3101
|
return transaction;
|
|
@@ -1896,30 +3201,6 @@ var Positions = class {
|
|
|
1896
3201
|
if (!(error instanceof GSwapSDKError) || error.details === void 0) return false;
|
|
1897
3202
|
return error.details["status"] === 404;
|
|
1898
3203
|
}
|
|
1899
|
-
mapPosition(position) {
|
|
1900
|
-
return {
|
|
1901
|
-
token0Symbol: position.token0Symbol,
|
|
1902
|
-
token1Symbol: position.token1Symbol,
|
|
1903
|
-
...position.token0CompositeKey === void 0 ? {} : { token0CompositeKey: position.token0CompositeKey },
|
|
1904
|
-
...position.token1CompositeKey === void 0 ? {} : { token1CompositeKey: position.token1CompositeKey },
|
|
1905
|
-
fee: position.fee,
|
|
1906
|
-
tickLower: position.tickLower,
|
|
1907
|
-
tickUpper: position.tickUpper,
|
|
1908
|
-
liquidity: position.liquidity,
|
|
1909
|
-
amount0: position.amount0,
|
|
1910
|
-
amount1: position.amount1,
|
|
1911
|
-
inRange: position.inRange,
|
|
1912
|
-
...position.currentTick === void 0 ? {} : { currentTick: position.currentTick },
|
|
1913
|
-
...position.sqrtPrice === void 0 ? {} : { sqrtPrice: position.sqrtPrice },
|
|
1914
|
-
pool: position.poolRef ?? position.pool ?? "",
|
|
1915
|
-
owner: position.owner,
|
|
1916
|
-
...position.fees0 === void 0 ? {} : { fees0: position.fees0 },
|
|
1917
|
-
...position.fees1 === void 0 ? {} : { fees1: position.fees1 }
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
requestToken(ref) {
|
|
1921
|
-
return typeof ref === "string" ? ref : compositeKeyOf(ref);
|
|
1922
|
-
}
|
|
1923
3204
|
canonicalTicks(tickLower, tickUpper, flipped) {
|
|
1924
3205
|
return flipped ? {
|
|
1925
3206
|
tickLower: -tickUpper,
|
|
@@ -1937,8 +3218,11 @@ var Positions = class {
|
|
|
1937
3218
|
rejectNumber(value, parameterName) {
|
|
1938
3219
|
if (typeof value === "number") throw new GSwapSDKError(`Invalid ${parameterName}: use a decimal string or BigNumber, not a JavaScript number`, "VALIDATION_ERROR");
|
|
1939
3220
|
}
|
|
1940
|
-
uniqueKey()
|
|
1941
|
-
|
|
3221
|
+
/** The caller's `uniqueKey` (validated), else a fresh one. */
|
|
3222
|
+
uniqueKeyFor(supplied) {
|
|
3223
|
+
if (supplied === void 0) return createUniqueKey(this.uniqueKeyPrefix);
|
|
3224
|
+
assertUniqueKey(supplied);
|
|
3225
|
+
return supplied;
|
|
1942
3226
|
}
|
|
1943
3227
|
async resolveCreateToken(ref, symbolOverride) {
|
|
1944
3228
|
try {
|
|
@@ -1950,6 +3234,7 @@ var Positions = class {
|
|
|
1950
3234
|
registeredSymbol: resolved.symbol
|
|
1951
3235
|
});
|
|
1952
3236
|
const suppliedClassKey = this.tryParseClassKey(ref);
|
|
3237
|
+
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 });
|
|
1953
3238
|
return {
|
|
1954
3239
|
symbol: resolved.symbol,
|
|
1955
3240
|
classKey: suppliedClassKey ?? parseTokenClassKey({
|
|
@@ -1968,7 +3253,7 @@ var Positions = class {
|
|
|
1968
3253
|
throw error;
|
|
1969
3254
|
}
|
|
1970
3255
|
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 });
|
|
1971
|
-
if (
|
|
3256
|
+
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 });
|
|
1972
3257
|
return {
|
|
1973
3258
|
symbol: symbolOverride,
|
|
1974
3259
|
classKey
|
|
@@ -1983,27 +3268,94 @@ var Positions = class {
|
|
|
1983
3268
|
}
|
|
1984
3269
|
}
|
|
1985
3270
|
};
|
|
3271
|
+
const REQUIRED_POSITION_STRINGS = [
|
|
3272
|
+
"token0Symbol",
|
|
3273
|
+
"token1Symbol",
|
|
3274
|
+
"owner",
|
|
3275
|
+
"liquidity",
|
|
3276
|
+
"amount0",
|
|
3277
|
+
"amount1"
|
|
3278
|
+
];
|
|
3279
|
+
const REQUIRED_POSITION_NUMBERS = [
|
|
3280
|
+
"fee",
|
|
3281
|
+
"tickLower",
|
|
3282
|
+
"tickUpper"
|
|
3283
|
+
];
|
|
3284
|
+
const OPTIONAL_POSITION_STRINGS = [
|
|
3285
|
+
"token0CompositeKey",
|
|
3286
|
+
"token1CompositeKey",
|
|
3287
|
+
"token0Image",
|
|
3288
|
+
"token1Image",
|
|
3289
|
+
"sqrtPrice",
|
|
3290
|
+
"fees0",
|
|
3291
|
+
"fees1"
|
|
3292
|
+
];
|
|
3293
|
+
/**
|
|
3294
|
+
* Reads one position row of `/positions` or `/position`: the pair, owner, range, liquidity and
|
|
3295
|
+
* amounts must be usable; the composite keys, token images, current tick and price, and the
|
|
3296
|
+
* escrowed fees are carried when present. `pool` is the pool reference (`poolRef` wins over
|
|
3297
|
+
* `pool`; `''` when the row names neither).
|
|
3298
|
+
*/
|
|
3299
|
+
function parsePosition(value) {
|
|
3300
|
+
const wire = asEntryRecord(value);
|
|
3301
|
+
if (wire === void 0) return entryRejected("the row is not an object");
|
|
3302
|
+
for (const key of REQUIRED_POSITION_STRINGS) if (typeof wire[key] !== "string") return entryRejected(`${key} is not a string`);
|
|
3303
|
+
for (const key of REQUIRED_POSITION_NUMBERS) {
|
|
3304
|
+
const number = wire[key];
|
|
3305
|
+
if (typeof number !== "number" || !Number.isFinite(number)) return entryRejected(`${key} is not a number`);
|
|
3306
|
+
}
|
|
3307
|
+
if (typeof wire["inRange"] !== "boolean") return entryRejected("inRange is not a boolean");
|
|
3308
|
+
for (const key of OPTIONAL_POSITION_STRINGS) if (wire[key] !== void 0 && typeof wire[key] !== "string") return entryRejected(`${key} is not a string`);
|
|
3309
|
+
const currentTick = wire["currentTick"];
|
|
3310
|
+
if (currentTick !== void 0 && (typeof currentTick !== "number" || !Number.isFinite(currentTick))) return entryRejected("currentTick is not a number");
|
|
3311
|
+
const pool = wire["poolRef"] ?? wire["pool"] ?? "";
|
|
3312
|
+
if (typeof pool !== "string") return entryRejected("pool is not a string");
|
|
3313
|
+
const optional = Object.fromEntries([...OPTIONAL_POSITION_STRINGS, "currentTick"].flatMap((key) => wire[key] === void 0 ? [] : [[key, wire[key]]]));
|
|
3314
|
+
return entryOk({
|
|
3315
|
+
token0Symbol: wire["token0Symbol"],
|
|
3316
|
+
token1Symbol: wire["token1Symbol"],
|
|
3317
|
+
fee: wire["fee"],
|
|
3318
|
+
tickLower: wire["tickLower"],
|
|
3319
|
+
tickUpper: wire["tickUpper"],
|
|
3320
|
+
liquidity: wire["liquidity"],
|
|
3321
|
+
amount0: wire["amount0"],
|
|
3322
|
+
amount1: wire["amount1"],
|
|
3323
|
+
inRange: wire["inRange"],
|
|
3324
|
+
pool,
|
|
3325
|
+
owner: wire["owner"],
|
|
3326
|
+
...optional
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
/** The position inside a `/position` answer, or `INVALID_CHAIN_RESPONSE` when it is not one. */
|
|
3330
|
+
function readPositionBody(data) {
|
|
3331
|
+
const parsed = parsePosition(data);
|
|
3332
|
+
if (parsed.ok) return parsed.value;
|
|
3333
|
+
throw new GSwapSDKError("Backend returned an invalid position.", "INVALID_CHAIN_RESPONSE", {
|
|
3334
|
+
data,
|
|
3335
|
+
reason: parsed.reason
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
1986
3338
|
|
|
1987
3339
|
//#endregion
|
|
1988
3340
|
//#region src/classes/quoting.ts
|
|
1989
3341
|
/** Read-only quote service backed by the v2 offline quote engine. */
|
|
1990
3342
|
var Quoting = class {
|
|
1991
|
-
|
|
1992
|
-
http;
|
|
3343
|
+
gateway;
|
|
1993
3344
|
symbols;
|
|
1994
3345
|
/**
|
|
1995
3346
|
* Creates a quote service.
|
|
1996
3347
|
*
|
|
1997
3348
|
* @example
|
|
1998
3349
|
* ```ts
|
|
1999
|
-
* const quoting = new Quoting(
|
|
3350
|
+
* const quoting = new Quoting(gateway, symbols); // a ChainGateway, or any { dexBackendBaseUrl, httpRequestor }
|
|
2000
3351
|
* ```
|
|
2001
3352
|
*/
|
|
2002
|
-
constructor(
|
|
2003
|
-
this.
|
|
2004
|
-
this.http = http;
|
|
3353
|
+
constructor(gateway, symbols) {
|
|
3354
|
+
this.gateway = gateway;
|
|
2005
3355
|
this.symbols = symbols;
|
|
3356
|
+
this.http = new HttpClient(gateway.httpRequestor, gateway.requestTimeoutMs ?? 3e4, gateway.wire);
|
|
2006
3357
|
}
|
|
3358
|
+
http;
|
|
2007
3359
|
/**
|
|
2008
3360
|
* Quotes the output for a positive exact-input amount.
|
|
2009
3361
|
*
|
|
@@ -2015,7 +3367,7 @@ var Quoting = class {
|
|
|
2015
3367
|
*/
|
|
2016
3368
|
async quoteExactInput(tokenIn, tokenOut, amountIn, fee) {
|
|
2017
3369
|
validateNumericAmount(amountIn, "amountIn");
|
|
2018
|
-
return this.quote(tokenIn, tokenOut, { amountIn: toDecimalString
|
|
3370
|
+
return this.quote(tokenIn, tokenOut, { amountIn: toDecimalString(amountIn) }, fee);
|
|
2019
3371
|
}
|
|
2020
3372
|
/**
|
|
2021
3373
|
* Quotes the input for a positive exact-output amount.
|
|
@@ -2028,18 +3380,18 @@ var Quoting = class {
|
|
|
2028
3380
|
*/
|
|
2029
3381
|
async quoteExactOutput(tokenIn, tokenOut, amountOut, fee) {
|
|
2030
3382
|
validateNumericAmount(amountOut, "amountOut");
|
|
2031
|
-
return this.quote(tokenIn, tokenOut, { amountOut: toDecimalString
|
|
3383
|
+
return this.quote(tokenIn, tokenOut, { amountOut: toDecimalString(amountOut) }, fee);
|
|
2032
3384
|
}
|
|
2033
3385
|
async quote(tokenIn, tokenOut, amount, fee) {
|
|
2034
3386
|
validateOptionalFee(fee);
|
|
2035
3387
|
const params = {
|
|
2036
|
-
tokenIn: await
|
|
2037
|
-
tokenOut: await
|
|
3388
|
+
tokenIn: await queryTokenOf(this.symbols, tokenIn),
|
|
3389
|
+
tokenOut: await queryTokenOf(this.symbols, tokenOut),
|
|
2038
3390
|
...amount
|
|
2039
3391
|
};
|
|
2040
3392
|
if (fee !== void 0) params["fee"] = String(fee);
|
|
2041
3393
|
try {
|
|
2042
|
-
return mapQuote((await this.http.sendGetRequest(this.dexBackendBaseUrl, "/v2/trade", "/quote", params)).data);
|
|
3394
|
+
return mapQuote((await this.http.sendGetRequest(this.gateway.dexBackendBaseUrl, "/v2/trade", "/quote", params)).data);
|
|
2043
3395
|
} catch (error) {
|
|
2044
3396
|
throw mapQuoteError(error, tokenIn, tokenOut, fee);
|
|
2045
3397
|
}
|
|
@@ -2101,12 +3453,19 @@ function optionalPositiveDecimal(value) {
|
|
|
2101
3453
|
return false;
|
|
2102
3454
|
}
|
|
2103
3455
|
}
|
|
3456
|
+
/**
|
|
3457
|
+
* The ways the backend says a pair has no market at all: a token the DEX contract never
|
|
3458
|
+
* registered ("has no trading symbol on the new DEX contract"), no pool at ANY fee tier ("No
|
|
3459
|
+
* pools found with sufficient liquidity"), or a missing pool ("Pool does not exist on the new
|
|
3460
|
+
* DEX contract"). A pool that exists but is too thin says "Insufficient liquidity: ..." instead.
|
|
3461
|
+
*/
|
|
3462
|
+
const NO_MARKET_MESSAGE = /no pool|no pools|pool does not exist|has no trading symbol on the new DEX contract/iu;
|
|
2104
3463
|
function mapQuoteError(error, tokenIn, tokenOut, fee) {
|
|
2105
3464
|
if (error instanceof GSwapSDKError) {
|
|
2106
3465
|
const status = readStatus(error.details);
|
|
2107
3466
|
const message = readMessage(error.details) ?? error.message;
|
|
2108
|
-
if (status === 404 ||
|
|
2109
|
-
if (status === 400 && /insufficient liquidity/iu.test(message)) return GSwapSDKError.insufficientLiquidityError(tokenIn, tokenOut, fee);
|
|
3467
|
+
if (status === 404 || NO_MARKET_MESSAGE.test(message)) return GSwapSDKError.noPoolAvailableError(tokenIn, tokenOut, fee, error);
|
|
3468
|
+
if (status === 400 && /insufficient liquidity/iu.test(message)) return GSwapSDKError.insufficientLiquidityError(tokenIn, tokenOut, fee, error);
|
|
2110
3469
|
return error;
|
|
2111
3470
|
}
|
|
2112
3471
|
return new GSwapSDKError("Quote request failed.", "HTTP_ERROR", {
|
|
@@ -2131,107 +3490,112 @@ function readMessage(details) {
|
|
|
2131
3490
|
function validateOptionalFee(fee) {
|
|
2132
3491
|
if (fee !== void 0 && !ALL_FEE_TIERS.some((tier) => Number(tier) === Number(fee))) throw new GSwapSDKError(`Invalid fee tier: ${fee}`, "VALIDATION_ERROR", { fee });
|
|
2133
3492
|
}
|
|
2134
|
-
function toDecimalString
|
|
3493
|
+
function toDecimalString(amount) {
|
|
2135
3494
|
return new bignumber_js.default(amount).toFixed();
|
|
2136
3495
|
}
|
|
2137
|
-
async function resolveSymbol$1(symbols, token) {
|
|
2138
|
-
const resolved = await symbols.resolve(token);
|
|
2139
|
-
if (resolved.symbol.length > 0) return resolved.symbol;
|
|
2140
|
-
throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token });
|
|
2141
|
-
}
|
|
2142
3496
|
|
|
2143
3497
|
//#endregion
|
|
2144
|
-
//#region src/classes/
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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
|
-
return this.list();
|
|
2193
|
-
}
|
|
2194
|
-
/** Resolve a symbol or token class key to its registered symbol metadata. */
|
|
2195
|
-
async resolve(ref) {
|
|
2196
|
-
let symbols = await this.list();
|
|
2197
|
-
const composite = typeof ref === "string" ? isCompositeKey(ref) ? compositeKeyOf(parseTokenClassKey(ref)) : void 0 : compositeKeyOf(ref);
|
|
2198
|
-
const resolved = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
|
|
2199
|
-
if (resolved === void 0) {
|
|
2200
|
-
symbols = await this.refresh();
|
|
2201
|
-
const refreshed = symbols.find((entry) => entry.symbol === ref || composite !== void 0 && compositeKeyOf(entry) === composite);
|
|
2202
|
-
if (refreshed === void 0) throw GSwapSDKError.unknownTokenError(ref);
|
|
2203
|
-
return refreshed;
|
|
3498
|
+
//#region src/classes/shadow_submit.ts
|
|
3499
|
+
/** Maps an HTTP status to a failure kind: 503 retryable, 400 disabled, anything else unknown. */
|
|
3500
|
+
function classifyShadowSubmitStatus(status) {
|
|
3501
|
+
if (status === 503) return "unavailable";
|
|
3502
|
+
if (status === 400) return "disabled";
|
|
3503
|
+
return "unknown";
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Prefers the body `code` (stable across status remaps) and falls back to the HTTP status.
|
|
3507
|
+
* `SHADOW_UNAVAILABLE` is the only retryable case; `SHADOW_RELAY_REJECTED` is a terminal gateway
|
|
3508
|
+
* rejection; the two other 400 codes and any unknown 400 map to "disabled".
|
|
3509
|
+
*/
|
|
3510
|
+
function classifyShadowSubmitError(status, code) {
|
|
3511
|
+
if (code === "SHADOW_UNAVAILABLE") return "unavailable";
|
|
3512
|
+
if (code === "SHADOW_RELAY_REJECTED") return "rejected";
|
|
3513
|
+
if (code === "SHADOW_DISABLED" || code === "SHADOW_UNSUPPORTED_TYPE") return "disabled";
|
|
3514
|
+
return classifyShadowSubmitStatus(status);
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Normalizes a shadow write's raw answer.
|
|
3518
|
+
*
|
|
3519
|
+
* A 2xx envelope with `error === false` is a success when it is a queued RequestTrade that
|
|
3520
|
+
* acknowledges the EXACT signed `uniqueKey`, or a synchronous (`mode: 'sync'`) liquidity write.
|
|
3521
|
+
* Everything else is classified as a failure from the status and the body's `code`.
|
|
3522
|
+
*
|
|
3523
|
+
* @throws GSwapSDKError INVALID_GATEWAY_RESPONSE when a RequestTrade success does not queue the
|
|
3524
|
+
* signed `uniqueKey` (a synchronous or mismatched answer must never be accepted as a swap).
|
|
3525
|
+
*/
|
|
3526
|
+
function readShadowSubmitOutcome(method, signedBody, result) {
|
|
3527
|
+
const { httpStatus, body } = result;
|
|
3528
|
+
const envelope = asRecord(body);
|
|
3529
|
+
if (httpStatus >= 200 && httpStatus < 300 && (envelope === null || envelope === void 0 ? void 0 : envelope["error"]) === false) {
|
|
3530
|
+
const data = asRecord(envelope["data"]);
|
|
3531
|
+
const rawTransactionId = typeof (data === null || data === void 0 ? void 0 : data["transactionId"]) === "string" ? data["transactionId"] : void 0;
|
|
3532
|
+
if (method === "RequestTrade") {
|
|
3533
|
+
const requestedKey = signedBody["uniqueKey"];
|
|
3534
|
+
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", {
|
|
3535
|
+
httpStatus,
|
|
3536
|
+
body
|
|
3537
|
+
});
|
|
3538
|
+
return {
|
|
3539
|
+
ok: true,
|
|
3540
|
+
bundleId: rawTransactionId ?? "",
|
|
3541
|
+
queued: {
|
|
3542
|
+
uniqueKey: requestedKey,
|
|
3543
|
+
requestTransactionId: rawTransactionId ?? null
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
2204
3546
|
}
|
|
2205
|
-
return
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
async orderPair(a, b) {
|
|
2209
|
-
const [resolvedA, resolvedB] = await Promise.all([this.resolve(a), this.resolve(b)]);
|
|
2210
|
-
const [token0Symbol, token1Symbol] = orderSymbols(resolvedA.symbol, resolvedB.symbol);
|
|
2211
|
-
return {
|
|
2212
|
-
token0: token0Symbol === resolvedA.symbol ? resolvedA : resolvedB,
|
|
2213
|
-
token1: token1Symbol === resolvedB.symbol ? resolvedB : resolvedA,
|
|
2214
|
-
flipped: token0Symbol !== resolvedA.symbol
|
|
3547
|
+
if ((data === null || data === void 0 ? void 0 : data["mode"]) === "sync") return {
|
|
3548
|
+
ok: true,
|
|
3549
|
+
bundleId: rawTransactionId ?? ""
|
|
2215
3550
|
};
|
|
2216
3551
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
3552
|
+
const kind = classifyShadowSubmitError(httpStatus, getStringProperty(body, "code"));
|
|
3553
|
+
if (kind === "rejected") return {
|
|
3554
|
+
ok: false,
|
|
3555
|
+
kind: "rejected",
|
|
3556
|
+
retryable: false,
|
|
3557
|
+
message: getStringProperty(body, "message") ?? ""
|
|
3558
|
+
};
|
|
3559
|
+
return kind === "unavailable" ? {
|
|
3560
|
+
ok: false,
|
|
3561
|
+
kind,
|
|
3562
|
+
retryable: true
|
|
3563
|
+
} : {
|
|
3564
|
+
ok: false,
|
|
3565
|
+
kind,
|
|
3566
|
+
retryable: false
|
|
3567
|
+
};
|
|
2226
3568
|
}
|
|
2227
|
-
function
|
|
2228
|
-
return
|
|
3569
|
+
function asRecord(value) {
|
|
3570
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
2229
3571
|
}
|
|
2230
3572
|
|
|
2231
3573
|
//#endregion
|
|
2232
3574
|
//#region src/classes/swaps.ts
|
|
2233
|
-
/**
|
|
2234
|
-
|
|
3575
|
+
/**
|
|
3576
|
+
* The route of a quote: its fee tier and pool, with `tokenIn` / `tokenOut` taken from
|
|
3577
|
+
* `tokenInIsToken0`, nothing re-derived. Use it to swap in exactly the pool a quote priced:
|
|
3578
|
+
*
|
|
3579
|
+
* @example
|
|
3580
|
+
* ```ts
|
|
3581
|
+
* const route = swapRouteFromQuote(quote);
|
|
3582
|
+
* await swaps.swap(route.tokenIn, route.tokenOut, route.fee, { exactIn: '100' }, { pool: route.pool });
|
|
3583
|
+
* ```
|
|
3584
|
+
*/
|
|
3585
|
+
function swapRouteFromQuote(quote) {
|
|
3586
|
+
validateFee(quote.fee);
|
|
3587
|
+
assertCanonicalPair(quote.token0Symbol, quote.token1Symbol);
|
|
3588
|
+
const [tokenIn, tokenOut] = quote.tokenInIsToken0 ? [quote.token0Symbol, quote.token1Symbol] : [quote.token1Symbol, quote.token0Symbol];
|
|
3589
|
+
return {
|
|
3590
|
+
tokenIn,
|
|
3591
|
+
tokenOut,
|
|
3592
|
+
fee: quote.fee,
|
|
3593
|
+
pool: {
|
|
3594
|
+
token0: quote.token0Symbol,
|
|
3595
|
+
token1: quote.token1Symbol
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
2235
3599
|
/** Executes queued current-contract RequestTrade operations through the Chain Gateway. */
|
|
2236
3600
|
var Swaps = class {
|
|
2237
3601
|
gateway;
|
|
@@ -2246,12 +3610,14 @@ var Swaps = class {
|
|
|
2246
3610
|
* const swaps = new Swaps(gateway, symbols, signer, 'client|alice');
|
|
2247
3611
|
* ```
|
|
2248
3612
|
*/
|
|
2249
|
-
constructor(gateway, symbols, signer, walletAddress) {
|
|
3613
|
+
constructor(gateway, symbols, signer, walletAddress, options = {}) {
|
|
2250
3614
|
this.gateway = gateway;
|
|
2251
3615
|
this.symbols = symbols;
|
|
2252
3616
|
this.signer = signer;
|
|
2253
3617
|
this.walletAddress = walletAddress;
|
|
3618
|
+
this.uniqueKeyPrefix = resolveUniqueKeyPrefix(options.uniqueKeyPrefix);
|
|
2254
3619
|
}
|
|
3620
|
+
uniqueKeyPrefix;
|
|
2255
3621
|
/**
|
|
2256
3622
|
* Signs and submits an exact-input or exact-output RequestTrade DTO. The returned transaction
|
|
2257
3623
|
* is QUEUED; call `await tx.confirm()` to obtain the executed trade or chain rejection.
|
|
@@ -2264,13 +3630,97 @@ var Swaps = class {
|
|
|
2264
3630
|
* });
|
|
2265
3631
|
* ```
|
|
2266
3632
|
*/
|
|
2267
|
-
async swap(tokenIn, tokenOut, fee, amount) {
|
|
3633
|
+
async swap(tokenIn, tokenOut, fee, amount, options) {
|
|
3634
|
+
return this.submitSwap(await this.buildSwap(tokenIn, tokenOut, fee, amount, options));
|
|
3635
|
+
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Signs and submits a SHADOW (dark) swap: the same RequestTrade as {@link swap}, relayed with
|
|
3638
|
+
* `X-Shadow: true` so defi-backend marks it private before it reaches the contract. Requires a
|
|
3639
|
+
* `walletAddress`. Resolves the normalized outcome instead of throwing on a gateway refusal:
|
|
3640
|
+
* `ok: false` carries `unavailable` (retryable), `disabled`, `rejected` (with the gateway's
|
|
3641
|
+
* message) or `unknown`. A success carries `queued`, to be confirmed with
|
|
3642
|
+
* `confirmSwap(queued.uniqueKey)`; never fall back to a public swap on failure.
|
|
3643
|
+
*
|
|
3644
|
+
* @example
|
|
3645
|
+
* ```ts
|
|
3646
|
+
* const outcome = await swaps.swapShadow('GALA', 'GUSDC', 3000, { exactIn: '100' });
|
|
3647
|
+
* if (outcome.ok && outcome.queued) await swaps.confirmSwap(outcome.queued.uniqueKey);
|
|
3648
|
+
* ```
|
|
3649
|
+
*/
|
|
3650
|
+
async swapShadow(tokenIn, tokenOut, fee, amount, options) {
|
|
3651
|
+
return this.submitSwapShadow(await this.buildSwap(tokenIn, tokenOut, fee, amount, options));
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Builds the RequestTrade DTO that {@link swap} would sign, without signing or sending it: to
|
|
3655
|
+
* show in an approval popup, to remember its `uniqueKey`, or to sign elsewhere. Submit exactly
|
|
3656
|
+
* this DTO with {@link submitSwap}. The pure counterpart, with canonical inputs and no
|
|
3657
|
+
* resolution, is `buildRequestTradeDto`.
|
|
3658
|
+
*
|
|
3659
|
+
* @example
|
|
3660
|
+
* ```ts
|
|
3661
|
+
* const dto = await swaps.buildSwap('GALA', 'GUSDC', 3000, { exactIn: '100' });
|
|
3662
|
+
* showApproval(dto);
|
|
3663
|
+
* const submitted = await swaps.submitSwap(dto);
|
|
3664
|
+
* ```
|
|
3665
|
+
*/
|
|
3666
|
+
async buildSwap(tokenIn, tokenOut, fee, amount, options) {
|
|
2268
3667
|
validateFee(fee);
|
|
2269
|
-
const
|
|
3668
|
+
const uniqueKey = (options === null || options === void 0 ? void 0 : options.uniqueKey) ?? createUniqueKey(this.uniqueKeyPrefix);
|
|
3669
|
+
assertUniqueKey(uniqueKey);
|
|
3670
|
+
const tokenInSymbol = await tradingSymbolOf(this.symbols, tokenIn);
|
|
3671
|
+
const tokenOutSymbol = await tradingSymbolOf(this.symbols, tokenOut);
|
|
3672
|
+
const ordered = this.orientPool(tokenInSymbol, tokenOutSymbol, options === null || options === void 0 ? void 0 : options.pool);
|
|
3673
|
+
return buildRequestTradeDto({
|
|
3674
|
+
token0: ordered.token0,
|
|
3675
|
+
token1: ordered.token1,
|
|
3676
|
+
fee,
|
|
3677
|
+
tokenInIsToken0: tokenInSymbol === ordered.token0,
|
|
3678
|
+
uniqueKey,
|
|
3679
|
+
...amount
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Signs and submits a DTO built by {@link buildSwap} or `buildRequestTradeDto`. The DTO must be
|
|
3684
|
+
* exactly what the builder produces (no signature, canonical decimals, builder key order), so
|
|
3685
|
+
* what an app showed is what gets signed.
|
|
3686
|
+
*
|
|
3687
|
+
* @example
|
|
3688
|
+
* ```ts
|
|
3689
|
+
* const submitted = await swaps.submitSwap(dto);
|
|
3690
|
+
* const applied = await submitted.confirm();
|
|
3691
|
+
* ```
|
|
3692
|
+
*/
|
|
3693
|
+
async submitSwap(dto) {
|
|
3694
|
+
const signed = await this.signRequestTrade(dto);
|
|
3695
|
+
return this.gateway.submit("RequestTrade", signed, this.submitOptions());
|
|
3696
|
+
}
|
|
3697
|
+
/** Signs and shadow-submits a DTO built by {@link buildSwap}; see {@link swapShadow}. */
|
|
3698
|
+
async submitSwapShadow(dto) {
|
|
3699
|
+
const signed = await this.signRequestTrade(dto);
|
|
3700
|
+
const result = await this.gateway.submitShadow("RequestTrade", signed, this.submitOptions());
|
|
3701
|
+
return readShadowSubmitOutcome("RequestTrade", signed, result);
|
|
3702
|
+
}
|
|
3703
|
+
submitOptions() {
|
|
3704
|
+
return this.walletAddress === void 0 ? {} : { walletAddress: this.walletAddress };
|
|
3705
|
+
}
|
|
3706
|
+
async signRequestTrade(dto) {
|
|
3707
|
+
assertCanonicalV2Dto("RequestTrade", dto);
|
|
2270
3708
|
if (this.signer === void 0) throw GSwapSDKError.noSignerError();
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
3709
|
+
return this.signer.signObject("RequestTrade", dto);
|
|
3710
|
+
}
|
|
3711
|
+
/** The pool's two symbols in contract order: the quote's own when pinned, else string order. */
|
|
3712
|
+
orientPool(tokenInSymbol, tokenOutSymbol, pool) {
|
|
3713
|
+
if (pool === void 0) return readOrderedSymbols(orderSymbols(tokenInSymbol, tokenOutSymbol));
|
|
3714
|
+
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", {
|
|
3715
|
+
type: "POOL_MISMATCH",
|
|
3716
|
+
tokenIn: tokenInSymbol,
|
|
3717
|
+
tokenOut: tokenOutSymbol,
|
|
3718
|
+
pool
|
|
3719
|
+
});
|
|
3720
|
+
return {
|
|
3721
|
+
token0: pool.token0,
|
|
3722
|
+
token1: pool.token1
|
|
3723
|
+
};
|
|
2274
3724
|
}
|
|
2275
3725
|
/**
|
|
2276
3726
|
* Confirm a swap by its durable `uniqueKey` without resubmitting it. This is the recovery
|
|
@@ -2282,11 +3732,11 @@ var Swaps = class {
|
|
|
2282
3732
|
*
|
|
2283
3733
|
* @example
|
|
2284
3734
|
* ```ts
|
|
2285
|
-
* const applied = await swaps.confirmSwap('
|
|
3735
|
+
* const applied = await swaps.confirmSwap('galaswap-operation-2f1c…');
|
|
2286
3736
|
* ```
|
|
2287
3737
|
*/
|
|
2288
3738
|
async confirmSwap(uniqueKey, options) {
|
|
2289
|
-
|
|
3739
|
+
assertUniqueKey(uniqueKey);
|
|
2290
3740
|
return new SubmittedTransaction({
|
|
2291
3741
|
method: "RequestTrade",
|
|
2292
3742
|
uniqueKey,
|
|
@@ -2301,47 +3751,7 @@ var Swaps = class {
|
|
|
2301
3751
|
...options
|
|
2302
3752
|
});
|
|
2303
3753
|
}
|
|
2304
|
-
async buildRequestTradeDto(tokenIn, tokenOut, fee, amount) {
|
|
2305
|
-
const tokenInSymbol = await resolveSymbol(this.symbols, tokenIn);
|
|
2306
|
-
const tokenOutSymbol = await resolveSymbol(this.symbols, tokenOut);
|
|
2307
|
-
const ordered = readOrderedSymbols(orderSymbols(tokenInSymbol, tokenOutSymbol));
|
|
2308
|
-
const tokenInIsToken0 = tokenInSymbol === ordered.token0;
|
|
2309
|
-
const dto = {
|
|
2310
|
-
token0: ordered.token0,
|
|
2311
|
-
token1: ordered.token1,
|
|
2312
|
-
fee,
|
|
2313
|
-
uniqueKey: `gswap-sdk-${globalThis.crypto.randomUUID()}`
|
|
2314
|
-
};
|
|
2315
|
-
if ("exactIn" in amount) {
|
|
2316
|
-
const exactIn = amount.exactIn;
|
|
2317
|
-
if (exactIn === void 0) throw new GSwapSDKError("exactIn is required for an exact-input swap.", "VALIDATION_ERROR");
|
|
2318
|
-
validateNumericAmount(exactIn, "exactIn");
|
|
2319
|
-
dto[tokenInIsToken0 ? "sell0Qty" : "sell1Qty"] = toDecimalString(exactIn);
|
|
2320
|
-
if (amount.amountOutMinimum !== void 0) {
|
|
2321
|
-
validateNumericAmount(amount.amountOutMinimum, "amountOutMinimum", true);
|
|
2322
|
-
dto["amountOutMinimum"] = toDecimalString(amount.amountOutMinimum);
|
|
2323
|
-
}
|
|
2324
|
-
} else {
|
|
2325
|
-
const exactOut = amount.exactOut;
|
|
2326
|
-
if (exactOut === void 0) throw new GSwapSDKError("exactOut is required for an exact-output swap.", "VALIDATION_ERROR");
|
|
2327
|
-
validateNumericAmount(exactOut, "exactOut");
|
|
2328
|
-
dto[tokenInIsToken0 ? "buy1Qty" : "buy0Qty"] = toDecimalString(exactOut);
|
|
2329
|
-
if (amount.amountInMaximum !== void 0) {
|
|
2330
|
-
validateNumericAmount(amount.amountInMaximum, "amountInMaximum", true);
|
|
2331
|
-
dto["amountInMaximum"] = toDecimalString(amount.amountInMaximum);
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
return dto;
|
|
2335
|
-
}
|
|
2336
3754
|
};
|
|
2337
|
-
function toDecimalString(amount) {
|
|
2338
|
-
return new bignumber_js.default(amount).toFixed();
|
|
2339
|
-
}
|
|
2340
|
-
async function resolveSymbol(symbols, token) {
|
|
2341
|
-
const resolved = await symbols.resolve(token);
|
|
2342
|
-
if (resolved.symbol.length > 0) return resolved.symbol;
|
|
2343
|
-
throw new GSwapSDKError("Token could not be resolved to a trading symbol.", "SYMBOL_NOT_FOUND", { token });
|
|
2344
|
-
}
|
|
2345
3755
|
|
|
2346
3756
|
//#endregion
|
|
2347
3757
|
//#region src/classes/gswap.ts
|
|
@@ -2358,8 +3768,11 @@ var GSwap = class {
|
|
|
2358
3768
|
positions;
|
|
2359
3769
|
swaps;
|
|
2360
3770
|
assets;
|
|
3771
|
+
/** Network-fee quotes (DryRun) signed with this client's signer. */
|
|
3772
|
+
fees;
|
|
2361
3773
|
dexBackendWsUrl;
|
|
2362
3774
|
webSocketImpl;
|
|
3775
|
+
streamApiKey;
|
|
2363
3776
|
streamClient;
|
|
2364
3777
|
/**
|
|
2365
3778
|
* Create an SDK client for prod or stage.
|
|
@@ -2376,21 +3789,30 @@ var GSwap = class {
|
|
|
2376
3789
|
this.chainCallTimeoutMs = (options === null || options === void 0 ? void 0 : options.chainCallTimeoutMs) ?? 3e4;
|
|
2377
3790
|
this.signer = options === null || options === void 0 ? void 0 : options.signer;
|
|
2378
3791
|
this.walletAddress = options === null || options === void 0 ? void 0 : options.walletAddress;
|
|
2379
|
-
const httpClient = new HttpClient(options === null || options === void 0 ? void 0 : options.httpRequestor, this.chainCallTimeoutMs);
|
|
3792
|
+
const httpClient = new HttpClient(options === null || options === void 0 ? void 0 : options.httpRequestor, this.chainCallTimeoutMs, options === null || options === void 0 ? void 0 : options.wire);
|
|
2380
3793
|
this.gateway = new ChainGateway({
|
|
2381
3794
|
dexBackendBaseUrl: this.dexBackendBaseUrl,
|
|
2382
3795
|
chainCallTimeoutMs: this.chainCallTimeoutMs,
|
|
3796
|
+
...(options === null || options === void 0 ? void 0 : options.wire) === void 0 ? {} : { wire: options.wire },
|
|
2383
3797
|
...(options === null || options === void 0 ? void 0 : options.httpRequestor) === void 0 ? {} : { httpRequestor: options.httpRequestor },
|
|
2384
3798
|
...this.walletAddress === void 0 ? {} : { walletAddress: this.walletAddress }
|
|
2385
3799
|
});
|
|
2386
|
-
|
|
2387
|
-
this.
|
|
2388
|
-
|
|
2389
|
-
this.
|
|
2390
|
-
this.
|
|
3800
|
+
const lenient = { onInvalidEntry: options === null || options === void 0 ? void 0 : options.onInvalidEntry };
|
|
3801
|
+
this.symbols = new Symbols(this.gateway, lenient);
|
|
3802
|
+
const resolver = (options === null || options === void 0 ? void 0 : options.symbolResolution) === "canonical" ? new CanonicalSymbols() : this.symbols;
|
|
3803
|
+
this.quoting = new Quoting(this.gateway, resolver);
|
|
3804
|
+
this.pools = new Pools(this.gateway, resolver, lenient);
|
|
3805
|
+
const keyOptions = {
|
|
3806
|
+
uniqueKeyPrefix: options === null || options === void 0 ? void 0 : options.uniqueKeyPrefix,
|
|
3807
|
+
...lenient
|
|
3808
|
+
};
|
|
3809
|
+
this.positions = new Positions(this.gateway, resolver, this.signer, this.walletAddress, keyOptions);
|
|
3810
|
+
this.swaps = new Swaps(this.gateway, resolver, this.signer, this.walletAddress, keyOptions);
|
|
2391
3811
|
this.assets = new Assets(this.dexBackendBaseUrl, httpClient);
|
|
3812
|
+
this.fees = new DexFees(this.gateway, this.signer);
|
|
2392
3813
|
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`);
|
|
2393
3814
|
this.webSocketImpl = options === null || options === void 0 ? void 0 : options.webSocketImpl;
|
|
3815
|
+
this.streamApiKey = options === null || options === void 0 ? void 0 : options.streamApiKey;
|
|
2394
3816
|
}
|
|
2395
3817
|
/**
|
|
2396
3818
|
* The realtime stream client for this environment (created on first use, not connected until you
|
|
@@ -2406,7 +3828,8 @@ var GSwap = class {
|
|
|
2406
3828
|
get stream() {
|
|
2407
3829
|
this.streamClient ??= new GdexStreamService({
|
|
2408
3830
|
url: this.dexBackendWsUrl,
|
|
2409
|
-
...this.webSocketImpl === void 0 ? {} : { webSocketImpl: this.webSocketImpl }
|
|
3831
|
+
...this.webSocketImpl === void 0 ? {} : { webSocketImpl: this.webSocketImpl },
|
|
3832
|
+
...this.streamApiKey === void 0 ? {} : { apiKey: this.streamApiKey }
|
|
2410
3833
|
});
|
|
2411
3834
|
return this.streamClient;
|
|
2412
3835
|
}
|
|
@@ -2415,9 +3838,149 @@ function trimTrailingSlash(value) {
|
|
|
2415
3838
|
return value.replace(/\/$/u, "");
|
|
2416
3839
|
}
|
|
2417
3840
|
|
|
3841
|
+
//#endregion
|
|
3842
|
+
//#region src/classes/pending_swaps.ts
|
|
3843
|
+
/** Key the wallet_extension has always used; its storage value is a plain array of entries. */
|
|
3844
|
+
const PENDING_SWAPS_STORAGE_KEY = "pendingSwaps.v1";
|
|
3845
|
+
/** Entries older than this are dropped the next time a wallet's swaps are listed. */
|
|
3846
|
+
const PENDING_SWAP_EXPIRY_MS = 6048e5;
|
|
3847
|
+
/**
|
|
3848
|
+
* Remembers queued swaps so their outcome can be confirmed after the UI, popup or service worker
|
|
3849
|
+
* that submitted them is gone (`confirmAppliedTrade` with the stored `uniqueKey`).
|
|
3850
|
+
*
|
|
3851
|
+
* Guarantees:
|
|
3852
|
+
* - Persistence is best effort and NEVER throws: a failing or missing storage must not turn an
|
|
3853
|
+
* accepted request into a client-side resubmission. `list` resolves `[]` when storage fails.
|
|
3854
|
+
* - Operations of one store instance are serialized, so concurrent `remember` / `forget` calls
|
|
3855
|
+
* cannot lose each other's write (read-modify-write over an async adapter). Two store instances
|
|
3856
|
+
* over the same storage (for example popup and service worker) are not coordinated.
|
|
3857
|
+
* - Entries that do not match the expected shape are ignored and dropped on the next write.
|
|
3858
|
+
* - Entries expire after seven days (an entry exactly seven days old is expired; frontend_v2
|
|
3859
|
+
* kept it for that one millisecond); `list` removes them from storage, which frontend_v2 never
|
|
3860
|
+
* did on read.
|
|
3861
|
+
* - New entries go to the end (or `insertAt: 'start'`); `list` returns the wallet's entries in
|
|
3862
|
+
* stored order (or `listOrder: 'newest-first'`).
|
|
3863
|
+
* - The stored shape is wallet_extension's by default; `storageKey`, `darkFlagField`, `insertAt`
|
|
3864
|
+
* and `listOrder` reproduce frontend_v2's. JSON key order inside an entry is not preserved.
|
|
3865
|
+
*
|
|
3866
|
+
* Passing no `storage` yields a store that remembers nothing.
|
|
3867
|
+
*
|
|
3868
|
+
* @example
|
|
3869
|
+
* ```ts
|
|
3870
|
+
* const store = new PendingSwapStore({
|
|
3871
|
+
* get: async (key) => (await chrome.storage.local.get(key))[key],
|
|
3872
|
+
* set: (key, value) => chrome.storage.local.set({ [key]: value }),
|
|
3873
|
+
* });
|
|
3874
|
+
* await store.remember({ uniqueKey, requestTransactionId, wallet, token0Symbol: 'GALA',
|
|
3875
|
+
* token1Symbol: 'GUSDC', isDarkSwap: false, createdAt: Date.now() });
|
|
3876
|
+
* for (const swap of await store.list(wallet)) await confirmAppliedTrade(client, swap);
|
|
3877
|
+
* ```
|
|
3878
|
+
*/
|
|
3879
|
+
var PendingSwapStore = class {
|
|
3880
|
+
storage;
|
|
3881
|
+
storageKey;
|
|
3882
|
+
darkFlagField;
|
|
3883
|
+
insertAt;
|
|
3884
|
+
listOrder;
|
|
3885
|
+
now;
|
|
3886
|
+
queue = Promise.resolve();
|
|
3887
|
+
constructor(storage, options = {}) {
|
|
3888
|
+
this.storage = storage;
|
|
3889
|
+
this.storageKey = options.storageKey ?? "pendingSwaps.v1";
|
|
3890
|
+
this.darkFlagField = options.darkFlagField ?? "isDarkSwap";
|
|
3891
|
+
this.insertAt = options.insertAt ?? "end";
|
|
3892
|
+
this.listOrder = options.listOrder ?? "stored";
|
|
3893
|
+
this.now = options.now ?? Date.now;
|
|
3894
|
+
}
|
|
3895
|
+
/** Saves (or replaces, by `uniqueKey`) a pending swap. */
|
|
3896
|
+
remember(swap) {
|
|
3897
|
+
return this.enqueue(async (storage) => {
|
|
3898
|
+
const others = (await this.read(storage)).filter((entry) => entry.uniqueKey !== swap.uniqueKey).map(this.toStored);
|
|
3899
|
+
const stored = this.toStored(swap);
|
|
3900
|
+
await storage.set(this.storageKey, this.insertAt === "end" ? [...others, stored] : [stored, ...others]);
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
/** Removes a pending swap once its outcome is known. */
|
|
3904
|
+
forget(uniqueKey) {
|
|
3905
|
+
return this.enqueue(async (storage) => {
|
|
3906
|
+
const current = await this.read(storage);
|
|
3907
|
+
await storage.set(this.storageKey, current.filter((entry) => entry.uniqueKey !== uniqueKey).map(this.toStored));
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
/** The wallet's unexpired pending swaps (see `listOrder`). Expired entries are pruned. */
|
|
3911
|
+
async list(wallet) {
|
|
3912
|
+
return await this.enqueue(async (storage) => {
|
|
3913
|
+
const current = await this.read(storage);
|
|
3914
|
+
const now = this.now();
|
|
3915
|
+
const fresh = current.filter((entry) => now - entry.createdAt < 6048e5);
|
|
3916
|
+
if (fresh.length !== current.length) await storage.set(this.storageKey, fresh.map(this.toStored));
|
|
3917
|
+
const mine = fresh.filter((entry) => entry.wallet === wallet);
|
|
3918
|
+
return this.listOrder === "newest-first" ? mine.sort((left, right) => right.createdAt - left.createdAt) : mine;
|
|
3919
|
+
}) ?? [];
|
|
3920
|
+
}
|
|
3921
|
+
enqueue(operation) {
|
|
3922
|
+
const storage = this.storage;
|
|
3923
|
+
if (storage === void 0) return Promise.resolve(void 0);
|
|
3924
|
+
const run = this.queue.then(async () => {
|
|
3925
|
+
try {
|
|
3926
|
+
return await operation(storage);
|
|
3927
|
+
} catch {
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
this.queue = run;
|
|
3932
|
+
return run;
|
|
3933
|
+
}
|
|
3934
|
+
async read(storage) {
|
|
3935
|
+
const value = await storage.get(this.storageKey);
|
|
3936
|
+
if (!Array.isArray(value)) return [];
|
|
3937
|
+
const entries = [];
|
|
3938
|
+
for (const item of value) {
|
|
3939
|
+
const entry = this.fromStored(item);
|
|
3940
|
+
if (entry !== void 0) entries.push(entry);
|
|
3941
|
+
}
|
|
3942
|
+
return entries;
|
|
3943
|
+
}
|
|
3944
|
+
toStored = (swap) => ({
|
|
3945
|
+
uniqueKey: swap.uniqueKey,
|
|
3946
|
+
requestTransactionId: swap.requestTransactionId,
|
|
3947
|
+
wallet: swap.wallet,
|
|
3948
|
+
token0Symbol: swap.token0Symbol,
|
|
3949
|
+
token1Symbol: swap.token1Symbol,
|
|
3950
|
+
[this.darkFlagField]: swap.isDarkSwap,
|
|
3951
|
+
createdAt: swap.createdAt
|
|
3952
|
+
});
|
|
3953
|
+
fromStored(value) {
|
|
3954
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
3955
|
+
const entry = value;
|
|
3956
|
+
const flag = entry[this.darkFlagField];
|
|
3957
|
+
const { uniqueKey, requestTransactionId, wallet, token0Symbol, token1Symbol, createdAt } = entry;
|
|
3958
|
+
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;
|
|
3959
|
+
return {
|
|
3960
|
+
uniqueKey,
|
|
3961
|
+
requestTransactionId,
|
|
3962
|
+
wallet,
|
|
3963
|
+
token0Symbol,
|
|
3964
|
+
token1Symbol,
|
|
3965
|
+
isDarkSwap: flag,
|
|
3966
|
+
createdAt
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
};
|
|
3970
|
+
|
|
2418
3971
|
//#endregion
|
|
2419
3972
|
//#region src/utils/eip712.ts
|
|
2420
3973
|
/**
|
|
3974
|
+
* Genuine EIP-712 typed-data signing for the four v2 dex-contract operations proven (2026-09-16,
|
|
3975
|
+
* live bisection against stage — see defi-backend's `chain-findings-artifact` memory /
|
|
3976
|
+
* https://claude.ai/artifact/3N9MevSjoQdU7gqnVB3dfU) to recover the `client|` identity correctly
|
|
3977
|
+
* via `eth_signTypedData_v4`, unlike `personal_sign` (which only ever resolves the bare `eth|`
|
|
3978
|
+
* form — see `BrowserWalletSigner`'s own doc comment). Mirrors frontend_v2's
|
|
3979
|
+
* `src/Services/eip712Types.ts` + `readWriteUtilsV2.ts`'s `EIP712_FIELD_COMPLETERS` exactly —
|
|
3980
|
+
* ported here rather than shared as a package because gswap-sdk has no frontend_v2 dependency
|
|
3981
|
+
* and should not gain one for four small functions.
|
|
3982
|
+
*/
|
|
3983
|
+
/**
|
|
2421
3984
|
* Derives the EIP-712 `types` for a DTO from its runtime shape.
|
|
2422
3985
|
*
|
|
2423
3986
|
* The generated `types` travel with the signed payload and are what BOTH sides hash, so they
|
|
@@ -2482,11 +4045,24 @@ function generateEIP712Types(typeName, params) {
|
|
|
2482
4045
|
});
|
|
2483
4046
|
return types;
|
|
2484
4047
|
}
|
|
2485
|
-
/**
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
4048
|
+
/**
|
|
4049
|
+
* Builds the EIP-712 domain from the wallet's LIVE `eth_chainId` result, exactly like frontend_v2
|
|
4050
|
+
* (DEFI-1895): a wallet hashes whatever domain it is handed, and MetaMask refuses a `chainId` that
|
|
4051
|
+
* differs from its active network, so a hardcoded `1` breaks every wallet that is not on Mainnet.
|
|
4052
|
+
* defi-backend recovers the signer from the `domain` the payload carries (live-proven for chain
|
|
4053
|
+
* ids 1, 56 and 11155111), so following the wallet is correct on every network.
|
|
4054
|
+
*
|
|
4055
|
+
* @param chainId - The raw `eth_chainId` result: a `0x` hex string (the JSON-RPC form) or an integer.
|
|
4056
|
+
* @throws GSwapSDKError (`INVALID_CHAIN_ID`) when the wallet answers with anything else.
|
|
4057
|
+
*/
|
|
4058
|
+
function eip712DomainFor(chainId) {
|
|
4059
|
+
const parsed = typeof chainId === "string" && /^0x[0-9a-fA-F]+$/u.test(chainId) ? Number(chainId) : typeof chainId === "number" ? chainId : NaN;
|
|
4060
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new GSwapSDKError("Wallet returned an invalid chain id.", "INVALID_CHAIN_ID", { chainId });
|
|
4061
|
+
return {
|
|
4062
|
+
name: "ethereum",
|
|
4063
|
+
chainId: parsed
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
2490
4066
|
/**
|
|
2491
4067
|
* The four v2 operations proven to recover the signer correctly via genuine EIP-712: `RequestTrade`
|
|
2492
4068
|
* never had the problem (one numeric field). `AddLiquidity`/`RemoveLiquidity`/
|
|
@@ -2583,15 +4159,216 @@ function buildEIP712Payload(methodName, dto) {
|
|
|
2583
4159
|
}
|
|
2584
4160
|
|
|
2585
4161
|
//#endregion
|
|
2586
|
-
//#region src/
|
|
4162
|
+
//#region src/utils/signing.ts
|
|
4163
|
+
/**
|
|
4164
|
+
* SDK-owned signing primitives.
|
|
4165
|
+
*
|
|
4166
|
+
* Everything the SDK needs to produce a GalaChain signature lives here, with no dependency on
|
|
4167
|
+
* `@gala-chain/api` (which drags `secp256k1` native bindings, `reflect-metadata`, `class-validator`
|
|
4168
|
+
* and its own `bignumber.js` major into every consumer). The only runtime dependencies are the
|
|
4169
|
+
* audited, pure-JS `@noble/curves` (secp256k1) and `@noble/hashes` (keccak256), which run
|
|
4170
|
+
* unchanged in browsers, MV3 service workers, Node and React Native/Hermes.
|
|
4171
|
+
*
|
|
4172
|
+
* Every function here is pinned byte-for-byte to the signatures produced by `@gala-chain/api`
|
|
4173
|
+
* (`serialize`, `signatures.getSignature`), `@gala-chain/connect`, the Gala Wallet extension and
|
|
4174
|
+
* frontend_v2 by the golden-vector suite in `tests/signing-golden.test.ts`.
|
|
4175
|
+
*/
|
|
4176
|
+
const textEncoder$1 = new TextEncoder();
|
|
4177
|
+
function toDeterministicNode(node) {
|
|
4178
|
+
if (node === null || node === void 0) return node;
|
|
4179
|
+
if (node instanceof RegExp) return node.toString();
|
|
4180
|
+
const candidate = node;
|
|
4181
|
+
return typeof candidate.toJSON === "function" ? Reflect.apply(candidate.toJSON, node, []) : node;
|
|
4182
|
+
}
|
|
4183
|
+
function deterministicNode(rawNode) {
|
|
4184
|
+
const node = toDeterministicNode(rawNode);
|
|
4185
|
+
if (node === void 0) return void 0;
|
|
4186
|
+
if (typeof node !== "object" || node === null) return JSON.stringify(node);
|
|
4187
|
+
if (Array.isArray(node)) return `[${node.map((item) => deterministicNode(item) ?? "null").join(",")}]`;
|
|
4188
|
+
const record = node;
|
|
4189
|
+
const members = [];
|
|
4190
|
+
for (const key of Object.keys(record).sort()) {
|
|
4191
|
+
const value = deterministicNode(record[key]);
|
|
4192
|
+
if (value === void 0) continue;
|
|
4193
|
+
members.push(`${JSON.stringify(key)}:${value}`);
|
|
4194
|
+
}
|
|
4195
|
+
return `{${members.join(",")}}`;
|
|
4196
|
+
}
|
|
4197
|
+
/**
|
|
4198
|
+
* GalaChain's deterministic JSON: object keys sorted (UTF-16 code-unit order) at every depth, no
|
|
4199
|
+
* whitespace, `undefined`/function members dropped, `toJSON()` honored. Byte-identical to
|
|
4200
|
+
* `serialize` from `@gala-chain/api` / `json-stringify-deterministic` for every JSON-shaped
|
|
4201
|
+
* value, which is all a DTO ever is.
|
|
4202
|
+
*
|
|
4203
|
+
* @throws TypeError for circular structures and BigInt values (as `JSON.stringify` does) and
|
|
4204
|
+
* when the value itself has no JSON representation (`undefined`, a function, a symbol).
|
|
4205
|
+
*/
|
|
4206
|
+
function serialize(value) {
|
|
4207
|
+
JSON.stringify(value);
|
|
4208
|
+
const result = deterministicNode(value);
|
|
4209
|
+
if (result === void 0) throw new TypeError("Cannot serialize a value with no JSON representation.");
|
|
4210
|
+
return result;
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* The exact string GalaChain hashes for a DTO: the (optional, string) `prefix` field PREPENDED to
|
|
4214
|
+
* the deterministic serialization of the DTO WITHOUT `signature`, `multisig`, `trace` and `prefix`.
|
|
4215
|
+
* Equivalent to `@gala-chain/api` 3.x `getPayloadToSign` for every non-EIP-712 DTO.
|
|
4216
|
+
*/
|
|
4217
|
+
function getPayloadToSign(dto) {
|
|
4218
|
+
const { prefix, ...rest } = dto;
|
|
4219
|
+
delete rest["signature"];
|
|
4220
|
+
delete rest["multisig"];
|
|
4221
|
+
delete rest["trace"];
|
|
4222
|
+
return (typeof prefix === "string" ? prefix : "") + serialize(rest);
|
|
4223
|
+
}
|
|
2587
4224
|
const ETHEREUM_SIGNED_MESSAGE = String.fromCharCode(25) + "Ethereum Signed Message:\n";
|
|
2588
|
-
/**
|
|
4225
|
+
/**
|
|
4226
|
+
* The EIP-191 header for `message`: `"\x19Ethereum Signed Message:\n"` + the message's UTF-8 BYTE
|
|
4227
|
+
* length. This is what a wallet's `personal_sign` prepends, and what the chain recomputes when it
|
|
4228
|
+
* recovers a personal-signed DTO.
|
|
4229
|
+
*/
|
|
4230
|
+
function calculatePersonalSignPrefix(message) {
|
|
4231
|
+
return ETHEREUM_SIGNED_MESSAGE + String(textEncoder$1.encode(message).length);
|
|
4232
|
+
}
|
|
4233
|
+
/**
|
|
4234
|
+
* The EIP-191 header for `serialize(dto)` (any `prefix` already on the DTO is ignored). This is
|
|
4235
|
+
* deliberately the same string the signers hand a wallet to sign, so the header's length can never
|
|
4236
|
+
* disagree with the signed message. Wallet-neutral: it is the value both a browser wallet's
|
|
4237
|
+
* `personal_sign` and Gala Wallet's `gala_signChainDto` (personal-sign scheme) prepend.
|
|
4238
|
+
*/
|
|
2589
4239
|
function personalSignPrefixFor(dto) {
|
|
2590
4240
|
const withoutPrefix = { ...dto };
|
|
2591
4241
|
delete withoutPrefix["prefix"];
|
|
2592
|
-
|
|
2593
|
-
|
|
4242
|
+
return calculatePersonalSignPrefix(serialize(withoutPrefix));
|
|
4243
|
+
}
|
|
4244
|
+
/**
|
|
4245
|
+
* The `@gala-chain/connect`-style prefix frontend_v2 puts in the body of an EIP-712 write: the
|
|
4246
|
+
* EIP-191 header whose number counts the prefix string ITSELF plus the body (the UTF-8 length of
|
|
4247
|
+
* `getPayloadToSign({ ...dto, prefix })`), found by fixpoint because adding the prefix changes the
|
|
4248
|
+
* length (a body of 99 bytes settles on 101).
|
|
4249
|
+
*
|
|
4250
|
+
* It is NOT a personal-sign prefix (a personal-sign write with this value recovers a phantom
|
|
4251
|
+
* signer; use `personalSignPrefixFor`). Under EIP-712 the field is an untyped extra that the
|
|
4252
|
+
* typed-data hash ignores and defi-backend recomputes, so it only exists to keep the submitted
|
|
4253
|
+
* body byte-identical to what frontend_v2 has always sent.
|
|
4254
|
+
*/
|
|
4255
|
+
function fixpointPrefixFor(dto) {
|
|
4256
|
+
const withoutPrefix = { ...dto };
|
|
4257
|
+
delete withoutPrefix["prefix"];
|
|
4258
|
+
let payloadLength = textEncoder$1.encode(getPayloadToSign(withoutPrefix)).length;
|
|
4259
|
+
let prefix = ETHEREUM_SIGNED_MESSAGE + String(payloadLength);
|
|
4260
|
+
let previousLength = -1;
|
|
4261
|
+
while (payloadLength !== previousLength) {
|
|
4262
|
+
previousLength = payloadLength;
|
|
4263
|
+
prefix = ETHEREUM_SIGNED_MESSAGE + String(payloadLength);
|
|
4264
|
+
payloadLength = textEncoder$1.encode(getPayloadToSign({
|
|
4265
|
+
...withoutPrefix,
|
|
4266
|
+
prefix
|
|
4267
|
+
})).length;
|
|
4268
|
+
}
|
|
4269
|
+
return prefix;
|
|
4270
|
+
}
|
|
4271
|
+
function invalidPrivateKey(reason) {
|
|
4272
|
+
return new GSwapSDKError(`Invalid private key: ${reason}`, "VALIDATION_ERROR", { type: "INVALID_PRIVATE_KEY" });
|
|
4273
|
+
}
|
|
4274
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
4275
|
+
function decodeBase64(input) {
|
|
4276
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(input) || input.length % 4 !== 0) return void 0;
|
|
4277
|
+
const unpadded = input.replace(/=+$/u, "");
|
|
4278
|
+
const bytes = [];
|
|
4279
|
+
let buffer = 0;
|
|
4280
|
+
let bits = 0;
|
|
4281
|
+
for (const char of unpadded) {
|
|
4282
|
+
buffer = buffer << 6 | BASE64_ALPHABET.indexOf(char);
|
|
4283
|
+
bits += 6;
|
|
4284
|
+
if (bits >= 8) {
|
|
4285
|
+
bits -= 8;
|
|
4286
|
+
bytes.push(buffer >> bits & 255);
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
return Uint8Array.from(bytes);
|
|
4290
|
+
}
|
|
4291
|
+
function decodeHex(input) {
|
|
4292
|
+
const bytes = new Uint8Array(input.length / 2);
|
|
4293
|
+
for (let index = 0; index < bytes.length; index += 1) bytes[index] = parseInt(input.slice(index * 2, index * 2 + 2), 16);
|
|
4294
|
+
return bytes;
|
|
4295
|
+
}
|
|
4296
|
+
/**
|
|
4297
|
+
* Normalizes a secp256k1 private key to a fresh 32-byte array (the caller may zero it). Also
|
|
4298
|
+
* accepts 32 raw bytes, which are validated and copied. For strings it accepts what `@gala-chain/api`'s
|
|
4299
|
+
* `normalizePrivateKey` accepted for a usable key: 64 hex characters (optionally `0x`-prefixed;
|
|
4300
|
+
* a 63-character hex key is left-padded with a zero) or a 44-character base64 string.
|
|
4301
|
+
*
|
|
4302
|
+
* @throws GSwapSDKError (`VALIDATION_ERROR`) for any other shape or an out-of-range scalar.
|
|
4303
|
+
*/
|
|
4304
|
+
function normalizePrivateKey(input) {
|
|
4305
|
+
if (typeof input !== "string") {
|
|
4306
|
+
if (!(input instanceof Uint8Array) || input.length !== 32) throw invalidPrivateKey("expected exactly 32 bytes.");
|
|
4307
|
+
if (!_noble_curves_secp256k1.secp256k1.utils.isValidSecretKey(input)) throw invalidPrivateKey("not a valid secp256k1 scalar.");
|
|
4308
|
+
return Uint8Array.from(input);
|
|
4309
|
+
}
|
|
4310
|
+
const withoutPrefix = input.startsWith("0x") ? input.slice(2) : input;
|
|
4311
|
+
let key;
|
|
4312
|
+
if (/^[0-9a-fA-F]{63,64}$/u.test(withoutPrefix)) key = decodeHex(withoutPrefix.length === 63 ? `0${withoutPrefix}` : withoutPrefix);
|
|
4313
|
+
else if (withoutPrefix.length === 44) key = decodeBase64(withoutPrefix);
|
|
4314
|
+
if ((key === null || key === void 0 ? void 0 : key.length) !== 32) throw invalidPrivateKey("expected 64 hex characters or a 44-character base64 string.");
|
|
4315
|
+
if (!_noble_curves_secp256k1.secp256k1.utils.isValidSecretKey(key)) throw invalidPrivateKey("not a valid secp256k1 scalar.");
|
|
4316
|
+
return key;
|
|
4317
|
+
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Signs a 32-byte keccak256 hash and returns GalaChain's recoverable signature: 64 hex `r` +
|
|
4320
|
+
* 64 hex `s` + `1b`/`1c` (`v`), lowercase, no `0x`. Deterministic (RFC 6979) and low-S, exactly
|
|
4321
|
+
* what `@gala-chain/api` and the Gala Wallet produce. A DER signature carries no recovery byte
|
|
4322
|
+
* and cannot be used: the chain RECOVERS the signer from the signature.
|
|
4323
|
+
*/
|
|
4324
|
+
function signHash(privateKey, hash) {
|
|
4325
|
+
if (hash.length !== 32) throw new GSwapSDKError(`secp256k1 can only sign a 32-byte hash (got ${hash.length}).`, "VALIDATION_ERROR");
|
|
4326
|
+
const signature = _noble_curves_secp256k1.secp256k1.sign(hash, privateKey, {
|
|
4327
|
+
prehash: false,
|
|
4328
|
+
lowS: true
|
|
4329
|
+
});
|
|
4330
|
+
return (0, _noble_hashes_utils.bytesToHex)(signature.toBytes("compact")) + (signature.recovery === 1 ? "1c" : "1b");
|
|
2594
4331
|
}
|
|
4332
|
+
/**
|
|
4333
|
+
* The lowercase `0x` Ethereum address (last 20 bytes of keccak256 of the uncompressed public key)
|
|
4334
|
+
* of a normalized private key. The Gala `eth|` identity of a key is this address.
|
|
4335
|
+
*/
|
|
4336
|
+
function ethAddressOfPrivateKey(privateKey) {
|
|
4337
|
+
const publicKey = _noble_curves_secp256k1.secp256k1.getPublicKey(privateKey, false);
|
|
4338
|
+
return `0x${(0, _noble_hashes_utils.bytesToHex)((0, _noble_hashes_sha3.keccak_256)(publicKey.slice(1)).slice(-20))}`;
|
|
4339
|
+
}
|
|
4340
|
+
/** keccak256 of a string's UTF-8 bytes. */
|
|
4341
|
+
function keccak256Utf8(data) {
|
|
4342
|
+
return (0, _noble_hashes_sha3.keccak_256)(textEncoder$1.encode(data));
|
|
4343
|
+
}
|
|
4344
|
+
/**
|
|
4345
|
+
* GalaChain NATIVE signature over an already-serialized DTO: keccak256 of the exact bytes, with no
|
|
4346
|
+
* prefix and no signature envelope. The caller owns serialization, so the bytes shown to a user
|
|
4347
|
+
* and the bytes signed can never diverge.
|
|
4348
|
+
*/
|
|
4349
|
+
function signSerializedNative(privateKey, serializedDto) {
|
|
4350
|
+
return signHash(privateKey, keccak256Utf8(serializedDto));
|
|
4351
|
+
}
|
|
4352
|
+
/**
|
|
4353
|
+
* GalaChain NATIVE signature for a DTO: keccak256 of `getPayloadToSign(dto)`, signed with
|
|
4354
|
+
* secp256k1. Byte-identical to `signatures.getSignature` from `@gala-chain/api`.
|
|
4355
|
+
*/
|
|
4356
|
+
function signNative(privateKey, dto) {
|
|
4357
|
+
return signSerializedNative(privateKey, getPayloadToSign(dto));
|
|
4358
|
+
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Personal-sign hash and signature: keccak256(`prefix` + `message`), both hashed VERBATIM with no
|
|
4361
|
+
* further EIP-191 wrapping. Passing `prefix = calculatePersonalSignPrefix(message)` reproduces a
|
|
4362
|
+
* wallet's `personal_sign` of `message`. Gala Wallet's `gala_signChainDto` (personal-sign
|
|
4363
|
+
* scheme) hashes `prefix` + the exact string it is handed, which can differ from the bare message
|
|
4364
|
+
* (see the `signatureOverPrefixlessBody` vectors); that quirk is why the two inputs stay separate.
|
|
4365
|
+
*/
|
|
4366
|
+
function signPersonalMessage(privateKey, prefix, message) {
|
|
4367
|
+
return signHash(privateKey, keccak256Utf8(prefix + message));
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
//#endregion
|
|
4371
|
+
//#region src/classes/signers.ts
|
|
2595
4372
|
function getWalletProvider() {
|
|
2596
4373
|
var _globalObject$window;
|
|
2597
4374
|
const wallet = (_globalObject$window = globalThis.window) === null || _globalObject$window === void 0 ? void 0 : _globalObject$window.gala;
|
|
@@ -2600,7 +4377,7 @@ function getWalletProvider() {
|
|
|
2600
4377
|
}
|
|
2601
4378
|
function getNativeSignature(value) {
|
|
2602
4379
|
if (typeof value !== "string") throw new GSwapSDKError("Wallet returned an invalid signature.", "INVALID_SIGNATURE");
|
|
2603
|
-
if (!/^[0-9a-fA-F]{130}$/u.test(value)) throw new GSwapSDKError("Wallet returned an invalid native signature.", "INVALID_SIGNATURE");
|
|
4380
|
+
if (!/^(?:0x)?[0-9a-fA-F]{130}$/u.test(value)) throw new GSwapSDKError("Wallet returned an invalid native signature.", "INVALID_SIGNATURE");
|
|
2604
4381
|
return value;
|
|
2605
4382
|
}
|
|
2606
4383
|
function getPersonalSignature(value) {
|
|
@@ -2613,19 +4390,48 @@ function normalizeEthereumAccount(address, allowGalaPrefix) {
|
|
|
2613
4390
|
type: "INVALID_WALLET_ADDRESS",
|
|
2614
4391
|
value: address
|
|
2615
4392
|
});
|
|
2616
|
-
return
|
|
4393
|
+
return normalized;
|
|
2617
4394
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
4395
|
+
/**
|
|
4396
|
+
* The account handed to Gala Wallet as the signer address. An Ethereum account is held to the
|
|
4397
|
+
* same strict check as ever (`0x` plus 40 hex digits; `eth|` plus 40 hex digits, with or without
|
|
4398
|
+
* `0x`, is normalized to it). A GalaChain id the wallet can also be asked to sign for is passed
|
|
4399
|
+
* through verbatim: `client|` plus 24 hex digits, or the bare 24 hex digits that frontend_v2 keeps
|
|
4400
|
+
* in `state.user.wallet` after a direct `client|` login. Nothing else is accepted.
|
|
4401
|
+
*/
|
|
4402
|
+
function normalizeGalaWalletAccount(address) {
|
|
4403
|
+
const eth = /^eth\|(?:0x)?([0-9a-fA-F]{40})$/u.exec(address);
|
|
4404
|
+
if ((eth === null || eth === void 0 ? void 0 : eth[1]) !== void 0) return `0x${eth[1]}`;
|
|
4405
|
+
if (/^0x[0-9a-fA-F]{40}$/u.test(address) || /^(?:client\|)?[0-9a-fA-F]{24}$/u.test(address)) return address;
|
|
4406
|
+
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", {
|
|
4407
|
+
type: "INVALID_WALLET_ADDRESS",
|
|
4408
|
+
value: address
|
|
4409
|
+
});
|
|
4410
|
+
}
|
|
4411
|
+
/** An identity Gala Wallet can act as: an account form above, any `eth|`/`client|` alias, verbatim. */
|
|
4412
|
+
function assertWalletIdentity(identity) {
|
|
4413
|
+
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", {
|
|
4414
|
+
type: "INVALID_WALLET_ADDRESS",
|
|
4415
|
+
value: identity
|
|
4416
|
+
});
|
|
2626
4417
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
4418
|
+
/** Provider-wide queue: `setAddress` + the signature request must not interleave across signers. */
|
|
4419
|
+
const walletIdentityQueue = /* @__PURE__ */ new WeakMap();
|
|
4420
|
+
/**
|
|
4421
|
+
* Runs `signWith` with `identity` selected on a Gala Wallet that supports `setAddress`. Gala
|
|
4422
|
+
* Wallet holds ONE current account, so two signatures for different identities in flight at once
|
|
4423
|
+
* would sign as whichever selection came last; the pair (select, sign) is therefore serialized
|
|
4424
|
+
* per provider. A failed selection fails the signature (nothing is requested).
|
|
4425
|
+
*/
|
|
4426
|
+
function withSelectedIdentity(wallet, identity, signWith) {
|
|
4427
|
+
const { setAddress } = wallet;
|
|
4428
|
+
if (identity === void 0 || typeof setAddress !== "function") return signWith();
|
|
4429
|
+
const run = (walletIdentityQueue.get(wallet) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
4430
|
+
await setAddress.call(wallet, identity);
|
|
4431
|
+
return signWith();
|
|
4432
|
+
});
|
|
4433
|
+
walletIdentityQueue.set(wallet, run);
|
|
4434
|
+
return run;
|
|
2629
4435
|
}
|
|
2630
4436
|
function personalSign(provider, dto, walletAddress, methodName) {
|
|
2631
4437
|
const prefix = personalSignPrefixFor(dto);
|
|
@@ -2634,10 +4440,10 @@ function personalSign(provider, dto, walletAddress, methodName) {
|
|
|
2634
4440
|
prefix
|
|
2635
4441
|
};
|
|
2636
4442
|
const params = methodName !== void 0 ? [
|
|
2637
|
-
|
|
4443
|
+
serialize(payload),
|
|
2638
4444
|
walletAddress,
|
|
2639
4445
|
methodName
|
|
2640
|
-
] : [
|
|
4446
|
+
] : [serialize(dto), walletAddress];
|
|
2641
4447
|
return provider.request({
|
|
2642
4448
|
method: methodName !== void 0 ? "gala_signChainDto" : "personal_sign",
|
|
2643
4449
|
params
|
|
@@ -2656,9 +4462,13 @@ function personalSign(provider, dto, walletAddress, methodName) {
|
|
|
2656
4462
|
* byte-for-byte equivalent (same recovered address from the same signature against both
|
|
2657
4463
|
* constructions) before this shipped, since gswap-sdk deliberately has no ethers dependency.
|
|
2658
4464
|
*/
|
|
2659
|
-
function signEIP712(provider, methodName, dto, walletAddress) {
|
|
4465
|
+
async function signEIP712(provider, methodName, dto, walletAddress) {
|
|
2660
4466
|
const { completedDto, types } = buildEIP712Payload(methodName, dto);
|
|
2661
|
-
const
|
|
4467
|
+
const domain = eip712DomainFor(await provider.request({
|
|
4468
|
+
method: "eth_chainId",
|
|
4469
|
+
params: []
|
|
4470
|
+
}));
|
|
4471
|
+
const prefix = fixpointPrefixFor(completedDto);
|
|
2662
4472
|
const message = {
|
|
2663
4473
|
...completedDto,
|
|
2664
4474
|
prefix
|
|
@@ -2671,7 +4481,7 @@ function signEIP712(provider, methodName, dto, walletAddress) {
|
|
|
2671
4481
|
type: "uint256"
|
|
2672
4482
|
}];
|
|
2673
4483
|
const payload = {
|
|
2674
|
-
domain
|
|
4484
|
+
domain,
|
|
2675
4485
|
types: {
|
|
2676
4486
|
...types,
|
|
2677
4487
|
EIP712Domain: eip712Domain
|
|
@@ -2679,15 +4489,16 @@ function signEIP712(provider, methodName, dto, walletAddress) {
|
|
|
2679
4489
|
primaryType: methodName,
|
|
2680
4490
|
message
|
|
2681
4491
|
};
|
|
2682
|
-
|
|
4492
|
+
const value = await provider.request({
|
|
2683
4493
|
method: "eth_signTypedData_v4",
|
|
2684
4494
|
params: [walletAddress, JSON.stringify(payload)]
|
|
2685
|
-
})
|
|
4495
|
+
});
|
|
4496
|
+
return {
|
|
2686
4497
|
...message,
|
|
4498
|
+
signature: getPersonalSignature(value),
|
|
2687
4499
|
types,
|
|
2688
|
-
domain
|
|
2689
|
-
|
|
2690
|
-
}));
|
|
4500
|
+
domain
|
|
4501
|
+
};
|
|
2691
4502
|
}
|
|
2692
4503
|
/**
|
|
2693
4504
|
* A GalaChain native signer backed by a secp256k1 private key.
|
|
@@ -2696,7 +4507,7 @@ function signEIP712(provider, methodName, dto, walletAddress) {
|
|
|
2696
4507
|
* or EIP-712 metadata. The private-key identity is recovered by the chain.
|
|
2697
4508
|
*/
|
|
2698
4509
|
var PrivateKeySigner = class {
|
|
2699
|
-
|
|
4510
|
+
privateKey;
|
|
2700
4511
|
/**
|
|
2701
4512
|
* Creates a native signer from a hex or base64 secp256k1 private key.
|
|
2702
4513
|
*
|
|
@@ -2707,7 +4518,7 @@ var PrivateKeySigner = class {
|
|
|
2707
4518
|
* ```
|
|
2708
4519
|
*/
|
|
2709
4520
|
constructor(privateKey) {
|
|
2710
|
-
this.
|
|
4521
|
+
this.privateKey = normalizePrivateKey(privateKey);
|
|
2711
4522
|
}
|
|
2712
4523
|
/**
|
|
2713
4524
|
* Signs the DTO with GalaChain native signing.
|
|
@@ -2722,7 +4533,7 @@ var PrivateKeySigner = class {
|
|
|
2722
4533
|
* ```
|
|
2723
4534
|
*/
|
|
2724
4535
|
signObject(_methodName, dto) {
|
|
2725
|
-
const signature =
|
|
4536
|
+
const signature = signNative(this.privateKey, dto);
|
|
2726
4537
|
return Promise.resolve({
|
|
2727
4538
|
...dto,
|
|
2728
4539
|
signature
|
|
@@ -2730,18 +4541,66 @@ var PrivateKeySigner = class {
|
|
|
2730
4541
|
}
|
|
2731
4542
|
};
|
|
2732
4543
|
/**
|
|
2733
|
-
*
|
|
2734
|
-
*
|
|
4544
|
+
* Creates the native-signing equivalent of {@link PrivateKeySigner} for apps that must not hold a
|
|
4545
|
+
* key in a long-lived object (an extension that reads its session key at sign time).
|
|
4546
|
+
*
|
|
4547
|
+
* `getPrivateKey` is called once per `signObject` call, never cached, never shared between calls
|
|
4548
|
+
* (concurrent signatures each fetch their own key), and the normalized key bytes are zeroed
|
|
4549
|
+
* before `signObject` settles, also on failure. A rejection or exception from `getPrivateKey` is
|
|
4550
|
+
* passed through unchanged, so the app's own "wallet is locked" error reaches its caller; a key
|
|
4551
|
+
* that is not a valid secp256k1 key string is a `VALIDATION_ERROR` (`INVALID_PRIVATE_KEY`).
|
|
4552
|
+
* Signatures are byte-identical to `new PrivateKeySigner(key).signObject(...)`.
|
|
4553
|
+
*
|
|
4554
|
+
* A JavaScript string cannot be wiped; the provider owns the lifetime of the string it returns.
|
|
4555
|
+
*
|
|
4556
|
+
* @example
|
|
4557
|
+
* ```typescript
|
|
4558
|
+
* const signer = createLazyPrivateKeySigner(
|
|
4559
|
+
* () => SecurityHelper.getSessionSecret(SecurityHelper.SESSION_PRIVATE_KEY_KEY),
|
|
4560
|
+
* { expectedAddress: account.address },
|
|
4561
|
+
* );
|
|
4562
|
+
* const gswap = new GSwap({ signer });
|
|
4563
|
+
* ```
|
|
4564
|
+
*/
|
|
4565
|
+
function createLazyPrivateKeySigner(getPrivateKey, options = {}) {
|
|
4566
|
+
const expected = options.expectedAddress;
|
|
4567
|
+
if (expected !== void 0) normalizeEthereumAccount(expected, true);
|
|
4568
|
+
const expectedLower = expected === null || expected === void 0 ? void 0 : expected.replace(/^eth\|/u, "").toLowerCase();
|
|
4569
|
+
return { async signObject(_methodName, dto) {
|
|
4570
|
+
const provided = await getPrivateKey();
|
|
4571
|
+
if (typeof provided !== "string") throw new GSwapSDKError("Invalid private key: the provider returned no key.", "VALIDATION_ERROR", { type: "INVALID_PRIVATE_KEY" });
|
|
4572
|
+
const key = normalizePrivateKey(provided);
|
|
4573
|
+
try {
|
|
4574
|
+
if (expectedLower !== void 0 && ethAddressOfPrivateKey(key) !== expectedLower) throw new GSwapSDKError("The signing key does not belong to the expected account.", "SIGNER_ADDRESS_MISMATCH");
|
|
4575
|
+
return {
|
|
4576
|
+
...dto,
|
|
4577
|
+
signature: signNative(key, dto)
|
|
4578
|
+
};
|
|
4579
|
+
} finally {
|
|
4580
|
+
key.fill(0);
|
|
4581
|
+
}
|
|
4582
|
+
} };
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* A Gala Wallet signer using native GalaChain signing, with an opt-in personal-sign mode.
|
|
4586
|
+
*
|
|
4587
|
+
* The selected Ethereum account is passed to Gala Wallet. There is NO automatic fallback: a
|
|
4588
|
+
* wallet that rejects `native` surfaces its error to the caller, because the only alternative,
|
|
4589
|
+
* `personal-sign`, silently recovers a phantom signer on every released Gala Wallet.
|
|
2735
4590
|
*
|
|
2736
|
-
*
|
|
2737
|
-
* `
|
|
2738
|
-
*
|
|
4591
|
+
* `scheme: 'personal-sign'` calls
|
|
4592
|
+
* `gala_signChainDto([serialize({ ...dto, prefix }), walletAddress, methodName])` and REQUIRES
|
|
4593
|
+
* a wallet build that contains wallet_extension commit 3ad7ae6 (strips the embedded `prefix`
|
|
4594
|
+
* field and hashes `prefix` + the prefix-less canonical body, the chain's scheme). Wallets
|
|
4595
|
+
* released before that fix hash the string with the `prefix` field still inside it and the
|
|
4596
|
+
* signature recovers to an address nobody holds, so only opt in once the fixed wallet is
|
|
4597
|
+
* released. See SIGNING-PARITY.md.
|
|
2739
4598
|
*/
|
|
2740
4599
|
var GalaWalletSigner = class {
|
|
2741
4600
|
/** The wallet address supplied to Gala Wallet for each signing request. */
|
|
2742
4601
|
walletAddress;
|
|
2743
|
-
/** The scheme
|
|
2744
|
-
|
|
4602
|
+
/** The signing scheme used for every request; it never changes after construction. */
|
|
4603
|
+
scheme;
|
|
2745
4604
|
/**
|
|
2746
4605
|
* Creates a Gala Wallet signer.
|
|
2747
4606
|
*
|
|
@@ -2751,14 +4610,23 @@ var GalaWalletSigner = class {
|
|
|
2751
4610
|
* const signed = await signer.signObject('RequestTrade', { token0: 'GALA', token1: 'GUSDC' });
|
|
2752
4611
|
* ```
|
|
2753
4612
|
*
|
|
2754
|
-
* @param walletAddress - The
|
|
2755
|
-
* `
|
|
2756
|
-
*
|
|
4613
|
+
* @param walletAddress - The account passed to Gala Wallet as the signer address. An Ethereum
|
|
4614
|
+
* account is `0x` plus 40 hex digits (Gala Wallet's `eth|...` form is normalized to it and held
|
|
4615
|
+
* to the same check); a GalaChain id is `client|` plus 24 hex digits, or the bare 24 hex digits
|
|
4616
|
+
* frontend_v2 keeps for a direct `client|` login, and is passed through unchanged.
|
|
4617
|
+
* @param options - Select native signing (default) or personal-sign explicitly.
|
|
4618
|
+
* `'personal-sign'` requires a Gala Wallet build with wallet_extension commit 3ad7ae6.
|
|
4619
|
+
* `selectAddress` is the identity (`client|...` / `eth|...`) Gala Wallet is switched to with
|
|
4620
|
+
* `window.gala.setAddress` before every signature, when the wallet offers it.
|
|
2757
4621
|
*/
|
|
2758
4622
|
constructor(walletAddress, options = {}) {
|
|
2759
|
-
this.walletAddress =
|
|
2760
|
-
this.
|
|
4623
|
+
this.walletAddress = normalizeGalaWalletAccount(walletAddress);
|
|
4624
|
+
this.scheme = options.scheme ?? "native";
|
|
4625
|
+
if (options.selectAddress !== void 0) assertWalletIdentity(options.selectAddress);
|
|
4626
|
+
this.selectAddress = options.selectAddress;
|
|
2761
4627
|
}
|
|
4628
|
+
/** The identity Gala Wallet is switched to before each signature, if one was given. */
|
|
4629
|
+
selectAddress;
|
|
2762
4630
|
/**
|
|
2763
4631
|
* Signs a DTO through `gala_signChainDto`.
|
|
2764
4632
|
*
|
|
@@ -2774,12 +4642,12 @@ var GalaWalletSigner = class {
|
|
|
2774
4642
|
*/
|
|
2775
4643
|
async signObject(methodName, dto) {
|
|
2776
4644
|
const wallet = getWalletProvider();
|
|
2777
|
-
|
|
2778
|
-
|
|
4645
|
+
return withSelectedIdentity(wallet, this.selectAddress, async () => {
|
|
4646
|
+
if (this.scheme === "personal-sign") return personalSign(wallet, dto, this.walletAddress, methodName);
|
|
2779
4647
|
const signature = await wallet.request({
|
|
2780
4648
|
method: "gala_signChainDto",
|
|
2781
4649
|
params: [
|
|
2782
|
-
|
|
4650
|
+
serialize(dto),
|
|
2783
4651
|
this.walletAddress,
|
|
2784
4652
|
methodName,
|
|
2785
4653
|
"native"
|
|
@@ -2789,11 +4657,7 @@ var GalaWalletSigner = class {
|
|
|
2789
4657
|
...dto,
|
|
2790
4658
|
signature: getNativeSignature(signature)
|
|
2791
4659
|
};
|
|
2792
|
-
}
|
|
2793
|
-
if (!isUnsupportedNativeSchemeError(error)) throw error;
|
|
2794
|
-
this.effectiveScheme = "personal-sign";
|
|
2795
|
-
return personalSign(wallet, dto, this.walletAddress, methodName);
|
|
2796
|
-
}
|
|
4660
|
+
});
|
|
2797
4661
|
}
|
|
2798
4662
|
};
|
|
2799
4663
|
/**
|
|
@@ -2854,11 +4718,490 @@ var BrowserWalletSigner = class {
|
|
|
2854
4718
|
}
|
|
2855
4719
|
};
|
|
2856
4720
|
|
|
4721
|
+
//#endregion
|
|
4722
|
+
//#region src/utils/chain_dto_signing.ts
|
|
4723
|
+
/**
|
|
4724
|
+
* String-key signing entry points for apps that hold a private key only briefly (the Gala Wallet
|
|
4725
|
+
* extension reads its session key at sign time and must not keep it in a long-lived object).
|
|
4726
|
+
*
|
|
4727
|
+
* These are thin, byte-exact ports of the functions the wallet shipped in
|
|
4728
|
+
* `wallet_extension/src/helpers/wallet/signing.ts` (including the fail-closed personal-sign
|
|
4729
|
+
* payload resolver from its commit 3ad7ae6), re-expressed over the SDK's own primitives. Every
|
|
4730
|
+
* function takes the key as a string (`0x`-optional hex, or base64) or as bytes, normalizes it per
|
|
4731
|
+
* call, and zeroes the normalized copy before returning, so nothing in this module retains a key.
|
|
4732
|
+
* A JavaScript string cannot be wiped; the caller owns the lifetime of the string it passes in.
|
|
4733
|
+
*/
|
|
4734
|
+
/**
|
|
4735
|
+
* Runs `action` with a private normalized copy of the key, zeroed afterwards (also when `action`
|
|
4736
|
+
* throws). Bytes given by the caller are copied, never modified: the caller owns them.
|
|
4737
|
+
*/
|
|
4738
|
+
function withPrivateKey(privateKey, action) {
|
|
4739
|
+
const key = normalizePrivateKey(privateKey);
|
|
4740
|
+
try {
|
|
4741
|
+
return action(key);
|
|
4742
|
+
} finally {
|
|
4743
|
+
key.fill(0);
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* GalaChain NATIVE signature over an already-serialized DTO: keccak256 of the exact bytes with no
|
|
4748
|
+
* prefix and no envelope (wallet `signChainDtoNative`). The caller owns serialization, so the
|
|
4749
|
+
* bytes a user reviewed and the bytes signed cannot diverge.
|
|
4750
|
+
*
|
|
4751
|
+
* @returns 130 lowercase hex characters, `r || s || v` with `v` = `1b`/`1c`, no `0x`.
|
|
4752
|
+
*/
|
|
4753
|
+
function signChainDtoNative(privateKey, serializedDto) {
|
|
4754
|
+
return withPrivateKey(privateKey, (key) => signSerializedNative(key, serializedDto));
|
|
4755
|
+
}
|
|
4756
|
+
/**
|
|
4757
|
+
* The personal-sign hash primitive (wallet `signChainDtoHash`): keccak256(`prefix` + `message`),
|
|
4758
|
+
* both verbatim, with no further EIP-191 wrap. `message` must be the DTO's serialization WITHOUT
|
|
4759
|
+
* its `prefix` field; callers holding a dApp-supplied string go through `signChainDtoPersonalSign`,
|
|
4760
|
+
* which derives both inputs from it.
|
|
4761
|
+
*/
|
|
4762
|
+
function signChainDtoHash(privateKey, prefix, message) {
|
|
4763
|
+
return withPrivateKey(privateKey, (key) => signPersonalMessage(key, prefix, message));
|
|
4764
|
+
}
|
|
4765
|
+
const PERSONAL_SIGN_HEADER = String.fromCharCode(25) + "Ethereum Signed Message:\n";
|
|
4766
|
+
const PERSONAL_SIGN_LENGTH_PATTERN = /^(?:0|[1-9][0-9]*)$/u;
|
|
4767
|
+
function isPersonalSignHeader(prefix) {
|
|
4768
|
+
return prefix.startsWith(PERSONAL_SIGN_HEADER) && PERSONAL_SIGN_LENGTH_PATTERN.test(prefix.slice(26));
|
|
4769
|
+
}
|
|
4770
|
+
/**
|
|
4771
|
+
* Top-level fields that would make the chain (`getPayloadToSign`) or defi-backend's personal-sign
|
|
4772
|
+
* verifier hash something other than `prefix + serialize(dto minus prefix)`. An unsigned DTO from
|
|
4773
|
+
* a dApp never legitimately carries any of them, so they fail closed. Order is part of the error
|
|
4774
|
+
* message the wallet shows its user.
|
|
4775
|
+
*/
|
|
4776
|
+
const PERSONAL_SIGN_FORBIDDEN_FIELDS = [
|
|
4777
|
+
"signature",
|
|
4778
|
+
"multisig",
|
|
4779
|
+
"trace",
|
|
4780
|
+
"types",
|
|
4781
|
+
"domain",
|
|
4782
|
+
"stringsInstructions"
|
|
4783
|
+
];
|
|
4784
|
+
function refusePersonalSign(message, type) {
|
|
4785
|
+
throw new GSwapSDKError(message, "INVALID_PERSONAL_SIGN_DTO", { type });
|
|
4786
|
+
}
|
|
4787
|
+
/**
|
|
4788
|
+
* Derives what to hash for a `gala_signChainDto` personal-sign request from the exact string the
|
|
4789
|
+
* dApp sent (and the approval card displayed): wallet `resolvePersonalSignChainDtoPayload`.
|
|
4790
|
+
*
|
|
4791
|
+
* The dApp submits `{ ...dto, prefix, signature }`, which the chain recovers from
|
|
4792
|
+
* `keccak256(prefix + serialize(dto WITHOUT prefix))`. So when the string embeds a `prefix`
|
|
4793
|
+
* field, that field is removed and the remainder re-serialized; the prefix used is the dApp's own
|
|
4794
|
+
* value byte for byte. A string with no `prefix` field is hashed verbatim with an empty prefix
|
|
4795
|
+
* (chain-equivalent to a native signature of that exact string).
|
|
4796
|
+
*
|
|
4797
|
+
* Fails closed (throws `GSwapSDKError` `INVALID_PERSONAL_SIGN_DTO`, message text identical to the
|
|
4798
|
+
* wallet's) on: malformed JSON; JSON that is not a plain object; a `prefix` that is not an EIP-191
|
|
4799
|
+
* "Ethereum Signed Message" header; any signing-envelope field (`signature`, `multisig`, `trace`,
|
|
4800
|
+
* `types`, `domain`, `stringsInstructions`); and a string that is not already the canonical
|
|
4801
|
+
* serialization of what it parses to (duplicate keys, lossy numbers, key-order tricks the card
|
|
4802
|
+
* would render differently). Stripping `prefix` is therefore the ONLY byte change between the
|
|
4803
|
+
* reviewed string and the signed message.
|
|
4804
|
+
*/
|
|
4805
|
+
function resolvePersonalSignChainDtoPayload(data) {
|
|
4806
|
+
let parsed;
|
|
4807
|
+
try {
|
|
4808
|
+
parsed = JSON.parse(data);
|
|
4809
|
+
} catch {
|
|
4810
|
+
return refusePersonalSign("Personal-sign DTO is not valid JSON", "NOT_JSON");
|
|
4811
|
+
}
|
|
4812
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return refusePersonalSign("Personal-sign DTO must be a JSON object", "NOT_OBJECT");
|
|
4813
|
+
const dto = parsed;
|
|
4814
|
+
if (!Object.prototype.hasOwnProperty.call(dto, "prefix")) return {
|
|
4815
|
+
prefix: "",
|
|
4816
|
+
message: data
|
|
4817
|
+
};
|
|
4818
|
+
const { prefix, ...withoutPrefix } = dto;
|
|
4819
|
+
if (typeof prefix !== "string" || !isPersonalSignHeader(prefix)) return refusePersonalSign("Personal-sign DTO prefix is not an EIP-191 \"Ethereum Signed Message\" header", "BAD_PREFIX");
|
|
4820
|
+
const forbidden = PERSONAL_SIGN_FORBIDDEN_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(withoutPrefix, field));
|
|
4821
|
+
if (forbidden.length > 0) return refusePersonalSign(`Personal-sign DTO carries signing-envelope field(s): ${forbidden.join(", ")}`, "ENVELOPE_FIELDS");
|
|
4822
|
+
if (serialize(dto) !== data) return refusePersonalSign("Personal-sign DTO is not in canonical serialized form", "NOT_CANONICAL");
|
|
4823
|
+
return {
|
|
4824
|
+
prefix,
|
|
4825
|
+
message: serialize(withoutPrefix)
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4829
|
+
* Signs a GalaChain personal-sign scheme DTO from the exact string a dApp sent through
|
|
4830
|
+
* `gala_signChainDto` (wallet `signChainDtoPersonalSign`). See
|
|
4831
|
+
* `resolvePersonalSignChainDtoPayload` for what is signed and what is refused; the signature
|
|
4832
|
+
* recovers, under the chain's `getPayloadToSign`, to this key for the DTO the dApp submits with
|
|
4833
|
+
* its `prefix`, and equals a `personal_sign` of the bare body when that prefix is the EIP-191
|
|
4834
|
+
* header of the bare body's byte length.
|
|
4835
|
+
*
|
|
4836
|
+
* @throws GSwapSDKError (`INVALID_PERSONAL_SIGN_DTO`) before any signing when the input is refused.
|
|
4837
|
+
*/
|
|
4838
|
+
function signChainDtoPersonalSign(privateKey, data) {
|
|
4839
|
+
const { prefix, message } = resolvePersonalSignChainDtoPayload(data);
|
|
4840
|
+
return signChainDtoHash(privateKey, prefix, message);
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Raw object signing (wallet `signObject`), used for transfers, bridge and fee-dry-run DTOs the
|
|
4844
|
+
* wallet signs itself. Removes ONLY `signature`, signs `keccak256(serialize(rest))` natively
|
|
4845
|
+
* (no `prefix` handling, and `trace` / `multisig` stay in the signed bytes, unlike
|
|
4846
|
+
* `signNative`), and returns `{ signature, ...rest }` with the signature as the FIRST key, exactly
|
|
4847
|
+
* like the wallet. Callers that must not sign extra fields strip them first.
|
|
4848
|
+
*/
|
|
4849
|
+
function signRawObject(privateKey, obj) {
|
|
4850
|
+
const rest = { ...obj };
|
|
4851
|
+
delete rest["signature"];
|
|
4852
|
+
return {
|
|
4853
|
+
signature: signChainDtoNative(privateKey, serialize(rest)),
|
|
4854
|
+
...rest
|
|
4855
|
+
};
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Drops the `EIP712Domain` entry from a dApp-supplied types map (a typed-data encoder derives the
|
|
4859
|
+
* domain type itself and rejects an explicit one). The order of the remaining types is preserved.
|
|
4860
|
+
*/
|
|
4861
|
+
function stripEip712DomainType(types) {
|
|
4862
|
+
const stripped = {};
|
|
4863
|
+
for (const typeName of Object.keys(types)) if (typeName !== "EIP712Domain") stripped[typeName] = types[typeName];
|
|
4864
|
+
return stripped;
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* `collection$category$type$additionalKey` for a token-class object (wallet `decodeTokenData`).
|
|
4868
|
+
* Lenient on purpose: any missing or non-string part becomes `''`, so a display layer never
|
|
4869
|
+
* throws on odd input.
|
|
4870
|
+
*/
|
|
4871
|
+
function tokenClassDollarKey(token) {
|
|
4872
|
+
const part = (field) => {
|
|
4873
|
+
const value = token === null || token === void 0 ? void 0 : token[field];
|
|
4874
|
+
return typeof value === "string" ? value : "";
|
|
4875
|
+
};
|
|
4876
|
+
return `${part("collection")}$${part("category")}$${part("type")}$${part("additionalKey")}`;
|
|
4877
|
+
}
|
|
4878
|
+
/**
|
|
4879
|
+
* A quantity in the form GalaChain's `IsBigNumber` validator accepts: FIXED notation, no leading
|
|
4880
|
+
* zeros, no trailing decimal zeros (`BigNumber#toFixed()`; never `toString()`, which turns
|
|
4881
|
+
* `0.00000001` into `1e-8`, and never a JS number in between).
|
|
4882
|
+
*
|
|
4883
|
+
* @throws GSwapSDKError (`VALIDATION_ERROR`) for a value that is not a finite number. The wallet's
|
|
4884
|
+
* `cleanNumber` returned the string `'NaN'` there; signing that text can only fail on chain.
|
|
4885
|
+
*/
|
|
4886
|
+
function canonicalDecimalString(value) {
|
|
4887
|
+
let parsed = new bignumber_js.default(NaN);
|
|
4888
|
+
try {
|
|
4889
|
+
parsed = new bignumber_js.default(value);
|
|
4890
|
+
} catch {}
|
|
4891
|
+
if (!parsed.isFinite()) throw new GSwapSDKError(`Invalid decimal quantity: ${String(value)}`, "VALIDATION_ERROR", { type: "INVALID_DECIMAL" });
|
|
4892
|
+
return parsed.toFixed();
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
//#endregion
|
|
4896
|
+
//#region src/utils/typed_data.ts
|
|
4897
|
+
/**
|
|
4898
|
+
* EIP-712 (`eth_signTypedData_v4`) hashing and signing for ARBITRARY dApp typed data, built on the
|
|
4899
|
+
* SDK's own primitives (`@noble/curves`, `@noble/hashes`) so a wallet can sign a dApp's typed-data
|
|
4900
|
+
* approval without ethers. It replaces the wallet's `new ethers.Wallet(key).signTypedData(...)`
|
|
4901
|
+
* and is pinned to it: the golden suite reproduces every fixture signature exactly and a seeded
|
|
4902
|
+
* differential fuzz compares the digest and signature with ethers for every supported type.
|
|
4903
|
+
*
|
|
4904
|
+
* Compared with ethers this encoder is deliberately STRICTER (it fails closed rather than coerce):
|
|
4905
|
+
* `bool` accepts only a boolean, numbers accept only safe integers / decimal or `0x` strings,
|
|
4906
|
+
* strings must be well-formed UTF-16, struct values must be objects whose fields are their own
|
|
4907
|
+
* properties, and every struct, field and type name must be a plain identifier. Anything ethers
|
|
4908
|
+
* accepts that is NOT in that set is refused here instead of being signed in a different shape
|
|
4909
|
+
* from what a user reviewed.
|
|
4910
|
+
*/
|
|
4911
|
+
const textEncoder = new TextEncoder();
|
|
4912
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
|
|
4913
|
+
const TYPE_SYNTAX = /^([A-Za-z_$][A-Za-z0-9_$]*)((?:\[(?:0|[1-9][0-9]*)?\])*)$/u;
|
|
4914
|
+
const BIGINT_DECIMAL = /^-?[0-9]+$/u;
|
|
4915
|
+
const BIGINT_HEX = /^-?0x[0-9a-fA-F]+$/u;
|
|
4916
|
+
const MAX_UINT256 = (1n << 256n) - 1n;
|
|
4917
|
+
/** The EIP-712 domain members and their types, in the order ethers (and the spec) hash them. */
|
|
4918
|
+
const DOMAIN_FIELDS = [
|
|
4919
|
+
{
|
|
4920
|
+
name: "name",
|
|
4921
|
+
type: "string"
|
|
4922
|
+
},
|
|
4923
|
+
{
|
|
4924
|
+
name: "version",
|
|
4925
|
+
type: "string"
|
|
4926
|
+
},
|
|
4927
|
+
{
|
|
4928
|
+
name: "chainId",
|
|
4929
|
+
type: "uint256"
|
|
4930
|
+
},
|
|
4931
|
+
{
|
|
4932
|
+
name: "verifyingContract",
|
|
4933
|
+
type: "address"
|
|
4934
|
+
},
|
|
4935
|
+
{
|
|
4936
|
+
name: "salt",
|
|
4937
|
+
type: "bytes32"
|
|
4938
|
+
}
|
|
4939
|
+
];
|
|
4940
|
+
function refuse(message, details) {
|
|
4941
|
+
throw new GSwapSDKError(`Invalid typed data: ${message}`, "INVALID_TYPED_DATA", details);
|
|
4942
|
+
}
|
|
4943
|
+
function concat(parts) {
|
|
4944
|
+
const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
4945
|
+
let offset = 0;
|
|
4946
|
+
for (const part of parts) {
|
|
4947
|
+
out.set(part, offset);
|
|
4948
|
+
offset += part.length;
|
|
4949
|
+
}
|
|
4950
|
+
return out;
|
|
4951
|
+
}
|
|
4952
|
+
function bigintToWord(value) {
|
|
4953
|
+
const word = /* @__PURE__ */ new Uint8Array(32);
|
|
4954
|
+
let rest = value < 0n ? value + (1n << 256n) : value;
|
|
4955
|
+
if (rest < 0n || rest > MAX_UINT256) return refuse("integer does not fit in 256 bits");
|
|
4956
|
+
for (let index = 31; index >= 0; index -= 1) {
|
|
4957
|
+
word[index] = Number(rest & 255n);
|
|
4958
|
+
rest >>= 8n;
|
|
4959
|
+
}
|
|
4960
|
+
return word;
|
|
4961
|
+
}
|
|
4962
|
+
function hexToBytes(hex) {
|
|
4963
|
+
const body = hex.slice(2);
|
|
4964
|
+
const out = new Uint8Array(body.length / 2);
|
|
4965
|
+
for (let index = 0; index < out.length; index += 1) out[index] = parseInt(body.slice(index * 2, index * 2 + 2), 16);
|
|
4966
|
+
return out;
|
|
4967
|
+
}
|
|
4968
|
+
function toBytes(value, what) {
|
|
4969
|
+
if (value instanceof Uint8Array) return value;
|
|
4970
|
+
if (typeof value === "string" && /^0x(?:[0-9a-fA-F]{2})*$/u.test(value)) return hexToBytes(value);
|
|
4971
|
+
return refuse(`${what} must be an even-length 0x hex string`);
|
|
4972
|
+
}
|
|
4973
|
+
function toBigInt(value, what) {
|
|
4974
|
+
if (typeof value === "bigint") return value;
|
|
4975
|
+
if (typeof value === "number") {
|
|
4976
|
+
if (!Number.isSafeInteger(value)) return refuse(`${what} must be a safe integer`);
|
|
4977
|
+
return BigInt(value);
|
|
4978
|
+
}
|
|
4979
|
+
if (typeof value === "string") {
|
|
4980
|
+
if (BIGINT_DECIMAL.test(value)) return BigInt(value);
|
|
4981
|
+
if (BIGINT_HEX.test(value)) return value.startsWith("-") ? -BigInt(value.slice(1)) : BigInt(value);
|
|
4982
|
+
}
|
|
4983
|
+
return refuse(`${what} must be an integer`);
|
|
4984
|
+
}
|
|
4985
|
+
function utf8(value, what) {
|
|
4986
|
+
if (typeof value !== "string") return refuse(`${what} must be a string`);
|
|
4987
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
4988
|
+
const unit = value.charCodeAt(index);
|
|
4989
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
4990
|
+
const next = value.charCodeAt(index + 1);
|
|
4991
|
+
if (!(next >= 56320 && next <= 57343)) return refuse(`${what} has an unpaired surrogate`);
|
|
4992
|
+
index += 1;
|
|
4993
|
+
} else if (unit >= 56320 && unit <= 57343) return refuse(`${what} has an unpaired surrogate`);
|
|
4994
|
+
}
|
|
4995
|
+
return textEncoder.encode(value);
|
|
4996
|
+
}
|
|
4997
|
+
/** EIP-55: a mixed-case address must carry the right checksum; all-lower / all-upper is accepted. */
|
|
4998
|
+
function addressBytes(value) {
|
|
4999
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/u.test(value)) return refuse("address must be 0x followed by 40 hex digits");
|
|
5000
|
+
const digits = value.slice(2);
|
|
5001
|
+
if (/[A-F]/u.test(digits) && /[a-f]/u.test(digits)) {
|
|
5002
|
+
const hash = (0, _noble_hashes_sha3.keccak_256)(textEncoder.encode(digits.toLowerCase()));
|
|
5003
|
+
for (let index = 0; index < 40; index += 1) {
|
|
5004
|
+
const nibble = hash[index >> 1] >> (index % 2 === 0 ? 4 : 0) & 15;
|
|
5005
|
+
const char = digits.charAt(index);
|
|
5006
|
+
if (char !== (nibble >= 8 ? char.toUpperCase() : char.toLowerCase())) return refuse("address has a bad checksum");
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
return hexToBytes(value);
|
|
5010
|
+
}
|
|
5011
|
+
/** The encoder for a non-struct, non-array type, or undefined when `base` is not one. */
|
|
5012
|
+
function leafEncoder(base) {
|
|
5013
|
+
if (base === "string") return (value) => (0, _noble_hashes_sha3.keccak_256)(utf8(value, "string"));
|
|
5014
|
+
if (base === "bytes") return (value) => (0, _noble_hashes_sha3.keccak_256)(toBytes(value, "bytes"));
|
|
5015
|
+
if (base === "bool") return (value) => {
|
|
5016
|
+
if (typeof value !== "boolean") return refuse("bool must be a boolean");
|
|
5017
|
+
return bigintToWord(value ? 1n : 0n);
|
|
5018
|
+
};
|
|
5019
|
+
if (base === "address") return (value) => concat([/* @__PURE__ */ new Uint8Array(12), addressBytes(value)]);
|
|
5020
|
+
const fixed = /^bytes([1-9][0-9]?)$/u.exec(base);
|
|
5021
|
+
if (fixed !== null) {
|
|
5022
|
+
const width = Number(fixed[1]);
|
|
5023
|
+
if (width > 32 || String(width) !== fixed[1]) return void 0;
|
|
5024
|
+
return (value) => {
|
|
5025
|
+
const bytes = toBytes(value, base);
|
|
5026
|
+
if (bytes.length !== width) return refuse(`${base} must be exactly ${width} bytes`);
|
|
5027
|
+
return concat([bytes, new Uint8Array(32 - width)]);
|
|
5028
|
+
};
|
|
5029
|
+
}
|
|
5030
|
+
const integer = /^(u?)int([1-9][0-9]*)$/u.exec(base);
|
|
5031
|
+
if (integer !== null) {
|
|
5032
|
+
const width = Number(integer[2]);
|
|
5033
|
+
if (width % 8 !== 0 || width > 256 || String(width) !== integer[2]) return void 0;
|
|
5034
|
+
const signed = integer[1] === "";
|
|
5035
|
+
const lower = signed ? -(1n << BigInt(width - 1)) : 0n;
|
|
5036
|
+
const upper = signed ? (1n << BigInt(width - 1)) - 1n : (1n << BigInt(width)) - 1n;
|
|
5037
|
+
return (value) => {
|
|
5038
|
+
const number = toBigInt(value, base);
|
|
5039
|
+
if (number < lower || number > upper) return refuse(`value out of bounds for ${base}`);
|
|
5040
|
+
return bigintToWord(number);
|
|
5041
|
+
};
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
function parseType(type) {
|
|
5045
|
+
const match = TYPE_SYNTAX.exec(type);
|
|
5046
|
+
if (match === null) return refuse(`unsupported type "${type}"`);
|
|
5047
|
+
const dimensions = [...(match[2] ?? "").matchAll(/\[([0-9]*)\]/gu)].map((dimension) => dimension[1] === "" ? -1 : Number(dimension[1]));
|
|
5048
|
+
return {
|
|
5049
|
+
base: match[1],
|
|
5050
|
+
dimensions
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
function parseFields(typeName, rawFields) {
|
|
5054
|
+
if (!Array.isArray(rawFields)) return refuse(`type ${typeName} must be a list of fields`);
|
|
5055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5056
|
+
return rawFields.map((raw) => {
|
|
5057
|
+
const field = raw;
|
|
5058
|
+
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`);
|
|
5059
|
+
if (!IDENTIFIER.test(field.name)) return refuse(`invalid field name "${field.name}"`);
|
|
5060
|
+
if (seen.has(field.name)) return refuse(`duplicate field "${field.name}" in ${typeName}`);
|
|
5061
|
+
seen.add(field.name);
|
|
5062
|
+
const parsed = parseType(field.type);
|
|
5063
|
+
const base = parsed.base === "uint" || parsed.base === "int" ? `${parsed.base}256` : parsed.base;
|
|
5064
|
+
return {
|
|
5065
|
+
name: field.name,
|
|
5066
|
+
type: base + field.type.slice(parsed.base.length)
|
|
5067
|
+
};
|
|
5068
|
+
});
|
|
5069
|
+
}
|
|
5070
|
+
/**
|
|
5071
|
+
* Builds the struct encoder for a set of type definitions: validates the type graph (unknown or
|
|
5072
|
+
* circular references, duplicate fields) and infers the primary type as the single type no other
|
|
5073
|
+
* type references, exactly like ethers' `TypedDataEncoder`.
|
|
5074
|
+
*/
|
|
5075
|
+
function createStructEncoder(rawTypes) {
|
|
5076
|
+
const names = Object.keys(rawTypes);
|
|
5077
|
+
const fields = /* @__PURE__ */ new Map();
|
|
5078
|
+
for (const name of names) {
|
|
5079
|
+
if (!IDENTIFIER.test(name)) return refuse(`invalid type name "${name}"`);
|
|
5080
|
+
fields.set(name, parseFields(name, rawTypes[name]));
|
|
5081
|
+
}
|
|
5082
|
+
const children = /* @__PURE__ */ new Map();
|
|
5083
|
+
const parents = /* @__PURE__ */ new Map();
|
|
5084
|
+
for (const name of names) {
|
|
5085
|
+
children.set(name, /* @__PURE__ */ new Set());
|
|
5086
|
+
parents.set(name, /* @__PURE__ */ new Set());
|
|
5087
|
+
}
|
|
5088
|
+
for (const [name, list] of fields) for (const field of list) {
|
|
5089
|
+
var _children$get, _parents$get;
|
|
5090
|
+
const { base } = parseType(field.type);
|
|
5091
|
+
if (base === name) return refuse(`circular type reference in ${name}`);
|
|
5092
|
+
if (leafEncoder(base) !== void 0) continue;
|
|
5093
|
+
if (!fields.has(base)) return refuse(`unknown type "${base}" in ${name}`);
|
|
5094
|
+
(_children$get = children.get(name)) === null || _children$get === void 0 || _children$get.add(base);
|
|
5095
|
+
(_parents$get = parents.get(base)) === null || _parents$get === void 0 || _parents$get.add(name);
|
|
5096
|
+
}
|
|
5097
|
+
const roots = names.filter((name) => {
|
|
5098
|
+
var _parents$get2;
|
|
5099
|
+
return ((_parents$get2 = parents.get(name)) === null || _parents$get2 === void 0 ? void 0 : _parents$get2.size) === 0;
|
|
5100
|
+
});
|
|
5101
|
+
if (roots.length === 0) return refuse("missing primary type");
|
|
5102
|
+
if (roots.length > 1) return refuse("ambiguous primary types or unused types", { roots });
|
|
5103
|
+
const primaryType = roots[0];
|
|
5104
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
5105
|
+
const checked = /* @__PURE__ */ new Set();
|
|
5106
|
+
const checkCircular = (name) => {
|
|
5107
|
+
if (visiting.has(name)) return refuse(`circular type reference at ${name}`);
|
|
5108
|
+
if (checked.has(name)) return;
|
|
5109
|
+
visiting.add(name);
|
|
5110
|
+
for (const child of children.get(name) ?? []) checkCircular(child);
|
|
5111
|
+
visiting.delete(name);
|
|
5112
|
+
checked.add(name);
|
|
5113
|
+
};
|
|
5114
|
+
checkCircular(primaryType);
|
|
5115
|
+
const dependencies = (name, found) => {
|
|
5116
|
+
for (const child of children.get(name) ?? []) if (!found.has(child)) {
|
|
5117
|
+
found.add(child);
|
|
5118
|
+
dependencies(child, found);
|
|
5119
|
+
}
|
|
5120
|
+
return found;
|
|
5121
|
+
};
|
|
5122
|
+
const encodeOne = (name) => `${name}(${(fields.get(name) ?? []).map((field) => `${field.type} ${field.name}`).join(",")})`;
|
|
5123
|
+
const typeHashes = /* @__PURE__ */ new Map();
|
|
5124
|
+
for (const name of names) {
|
|
5125
|
+
const sorted = [...dependencies(name, /* @__PURE__ */ new Set())].filter((dep) => dep !== name).sort();
|
|
5126
|
+
typeHashes.set(name, (0, _noble_hashes_sha3.keccak_256)(textEncoder.encode([name, ...sorted].map(encodeOne).join(""))));
|
|
5127
|
+
}
|
|
5128
|
+
const encodeValue = (parsed, value, depth) => {
|
|
5129
|
+
const dimension = parsed.dimensions[parsed.dimensions.length - 1 - depth];
|
|
5130
|
+
if (dimension !== void 0) {
|
|
5131
|
+
if (!Array.isArray(value)) return refuse(`${parsed.base} array value must be an array`);
|
|
5132
|
+
if (dimension !== -1 && dimension !== value.length) return refuse("array length mismatch");
|
|
5133
|
+
return (0, _noble_hashes_sha3.keccak_256)(concat(value.map((item) => encodeValue(parsed, item, depth + 1))));
|
|
5134
|
+
}
|
|
5135
|
+
const leaf = leafEncoder(parsed.base);
|
|
5136
|
+
if (leaf !== void 0) return leaf(value);
|
|
5137
|
+
return hashStruct(parsed.base, value);
|
|
5138
|
+
};
|
|
5139
|
+
function hashStruct(typeName, value) {
|
|
5140
|
+
const list = fields.get(typeName);
|
|
5141
|
+
const typeHash = typeHashes.get(typeName);
|
|
5142
|
+
if (list === void 0 || typeHash === void 0) return refuse(`unknown type "${typeName}"`);
|
|
5143
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return refuse(`${typeName} value must be an object`);
|
|
5144
|
+
const record = value;
|
|
5145
|
+
const words = list.map((field) => {
|
|
5146
|
+
if (!Object.prototype.hasOwnProperty.call(record, field.name)) return refuse(`${typeName} value is missing field "${field.name}"`);
|
|
5147
|
+
return encodeValue(parseType(field.type), record[field.name], 0);
|
|
5148
|
+
});
|
|
5149
|
+
return (0, _noble_hashes_sha3.keccak_256)(concat([typeHash, ...words]));
|
|
5150
|
+
}
|
|
5151
|
+
return {
|
|
5152
|
+
primaryType,
|
|
5153
|
+
hashStruct
|
|
5154
|
+
};
|
|
5155
|
+
}
|
|
5156
|
+
function hashDomain(domain) {
|
|
5157
|
+
if (domain === null || typeof domain !== "object" || Array.isArray(domain)) return refuse("domain must be an object");
|
|
5158
|
+
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)}`);
|
|
5159
|
+
return createStructEncoder({ EIP712Domain: DOMAIN_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(domain, field.name)) }).hashStruct("EIP712Domain", domain);
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* The EIP-712 digest `keccak256(0x1901 || domainSeparator || hashStruct(primaryType, message))`
|
|
5163
|
+
* that a wallet signs for `eth_signTypedData_v4`. The primary type is inferred, as ethers does.
|
|
5164
|
+
*
|
|
5165
|
+
* @throws GSwapSDKError (`INVALID_TYPED_DATA`) for any type graph or value it cannot encode
|
|
5166
|
+
* exactly (see the module note on strictness).
|
|
5167
|
+
*/
|
|
5168
|
+
function hashTypedData(payload) {
|
|
5169
|
+
const domainHash = hashDomain(payload.domain);
|
|
5170
|
+
const encoder = createStructEncoder(payload.types);
|
|
5171
|
+
const structHash = encoder.hashStruct(encoder.primaryType, payload.message);
|
|
5172
|
+
return (0, _noble_hashes_sha3.keccak_256)(concat([
|
|
5173
|
+
Uint8Array.of(25, 1),
|
|
5174
|
+
domainHash,
|
|
5175
|
+
structHash
|
|
5176
|
+
]));
|
|
5177
|
+
}
|
|
5178
|
+
/**
|
|
5179
|
+
* Signs a dApp's EIP-712 typed-data approval (the wallet's `signTypedMessage` path), replacing
|
|
5180
|
+
* `new ethers.Wallet(key).signTypedData(domain, types, message)`. `types` must already have
|
|
5181
|
+
* `EIP712Domain` removed (`stripEip712DomainType`).
|
|
5182
|
+
*
|
|
5183
|
+
* @returns `0x` + r + s + v with `v` = `0x1b`/`0x1c` (27/28): ethers' serialized signature form,
|
|
5184
|
+
* recoverable with `verifyTypedData` / `eth_signTypedData_v4` verifiers. Rejects (never throws
|
|
5185
|
+
* synchronously) on invalid input.
|
|
5186
|
+
*/
|
|
5187
|
+
async function signTypedDataPayload(privateKey, payload) {
|
|
5188
|
+
const digest = hashTypedData(payload);
|
|
5189
|
+
return withPrivateKey(privateKey, (key) => `0x${signHash(key, digest)}`);
|
|
5190
|
+
}
|
|
5191
|
+
|
|
2857
5192
|
//#endregion
|
|
2858
5193
|
exports.ALL_FEE_TIERS = ALL_FEE_TIERS;
|
|
5194
|
+
exports.AppliedTradeError = AppliedTradeError;
|
|
2859
5195
|
exports.Assets = Assets;
|
|
2860
5196
|
exports.BrowserWalletSigner = BrowserWalletSigner;
|
|
5197
|
+
exports.CanonicalSymbols = CanonicalSymbols;
|
|
2861
5198
|
exports.ChainGateway = ChainGateway;
|
|
5199
|
+
exports.DEFAULT_UNIQUE_KEY_PREFIX = DEFAULT_UNIQUE_KEY_PREFIX;
|
|
5200
|
+
exports.DEX_FEE_METHODS = DEX_FEE_METHODS;
|
|
5201
|
+
exports.DRY_RUN_METHOD = DRY_RUN_METHOD;
|
|
5202
|
+
exports.DRY_RUN_UNIQUE_KEY_PREFIX = DRY_RUN_UNIQUE_KEY_PREFIX;
|
|
5203
|
+
exports.DexFees = DexFees;
|
|
5204
|
+
exports.EIP712_CAPABLE_METHODS = EIP712_CAPABLE_METHODS;
|
|
2862
5205
|
exports.FEE_TIER = FEE_TIER;
|
|
2863
5206
|
exports.GC_MAX_TICK = GC_MAX_TICK;
|
|
2864
5207
|
exports.GC_MIN_TICK = GC_MIN_TICK;
|
|
@@ -2868,6 +5211,9 @@ exports.GSwapSDKError = GSwapSDKError;
|
|
|
2868
5211
|
exports.GalaWalletSigner = GalaWalletSigner;
|
|
2869
5212
|
exports.GdexStreamError = GdexStreamError;
|
|
2870
5213
|
exports.GdexStreamService = GdexStreamService;
|
|
5214
|
+
exports.PENDING_SWAPS_STORAGE_KEY = PENDING_SWAPS_STORAGE_KEY;
|
|
5215
|
+
exports.PENDING_SWAP_EXPIRY_MS = PENDING_SWAP_EXPIRY_MS;
|
|
5216
|
+
exports.PendingSwapStore = PendingSwapStore;
|
|
2871
5217
|
exports.Pools = Pools;
|
|
2872
5218
|
exports.Positions = Positions;
|
|
2873
5219
|
exports.PrivateKeySigner = PrivateKeySigner;
|
|
@@ -2878,18 +5224,66 @@ exports.Symbols = Symbols;
|
|
|
2878
5224
|
exports.TICK_SPACING_BY_FEE = TICK_SPACING_BY_FEE;
|
|
2879
5225
|
exports.alignTickDown = alignTickDown;
|
|
2880
5226
|
exports.alignTickUp = alignTickUp;
|
|
5227
|
+
exports.assertCanonicalPair = assertCanonicalPair;
|
|
5228
|
+
exports.assertCanonicalV2Dto = assertCanonicalV2Dto;
|
|
2881
5229
|
exports.assertTickRange = assertTickRange;
|
|
5230
|
+
exports.assertUniqueKey = assertUniqueKey;
|
|
5231
|
+
exports.buildAddLiquidityDto = buildAddLiquidityDto;
|
|
5232
|
+
exports.buildCollectPositionFeesDto = buildCollectPositionFeesDto;
|
|
5233
|
+
exports.buildCreatePoolDto = buildCreatePoolDto;
|
|
5234
|
+
exports.buildDryRunWrapper = buildDryRunWrapper;
|
|
5235
|
+
exports.buildEIP712Payload = buildEIP712Payload;
|
|
5236
|
+
exports.buildRemoveLiquidityDto = buildRemoveLiquidityDto;
|
|
5237
|
+
exports.buildRequestTradeDto = buildRequestTradeDto;
|
|
5238
|
+
exports.calculatePersonalSignPrefix = calculatePersonalSignPrefix;
|
|
5239
|
+
exports.canonicalDecimalString = canonicalDecimalString;
|
|
5240
|
+
exports.classifyShadowSubmitError = classifyShadowSubmitError;
|
|
5241
|
+
exports.classifyShadowSubmitStatus = classifyShadowSubmitStatus;
|
|
2882
5242
|
exports.compositeKeyOf = compositeKeyOf;
|
|
5243
|
+
exports.confirmAppliedTrade = confirmAppliedTrade;
|
|
5244
|
+
exports.createLazyPrivateKeySigner = createLazyPrivateKeySigner;
|
|
5245
|
+
exports.createUniqueKey = createUniqueKey;
|
|
5246
|
+
exports.eip712DomainFor = eip712DomainFor;
|
|
5247
|
+
exports.ethAddressOfPrivateKey = ethAddressOfPrivateKey;
|
|
5248
|
+
exports.fixpointPrefixFor = fixpointPrefixFor;
|
|
5249
|
+
exports.frameSlot0 = frameSlot0;
|
|
5250
|
+
exports.generateEIP712Types = generateEIP712Types;
|
|
2883
5251
|
exports.getObjectProperty = getObjectProperty;
|
|
5252
|
+
exports.getPayloadToSign = getPayloadToSign;
|
|
2884
5253
|
exports.getStringProperty = getStringProperty;
|
|
5254
|
+
exports.hashTypedData = hashTypedData;
|
|
5255
|
+
exports.isCurrentContractDexMessage = isCurrentContractDexMessage;
|
|
5256
|
+
exports.isDexFeeMethod = isDexFeeMethod;
|
|
2885
5257
|
exports.isGdexLiquidityEvent = isGdexLiquidityEvent;
|
|
5258
|
+
exports.isProposableTradingSymbol = isProposableTradingSymbol;
|
|
5259
|
+
exports.normalizePrivateKey = normalizePrivateKey;
|
|
2886
5260
|
exports.orderSymbols = orderSymbols;
|
|
2887
5261
|
exports.parseJson = parseJson;
|
|
2888
5262
|
exports.parseTokenClassKey = parseTokenClassKey;
|
|
5263
|
+
exports.personalSignPrefixFor = personalSignPrefixFor;
|
|
5264
|
+
exports.queryTokenOf = queryTokenOf;
|
|
5265
|
+
exports.rawTokenOf = rawTokenOf;
|
|
2889
5266
|
exports.readOrderedSymbols = readOrderedSymbols;
|
|
5267
|
+
exports.readShadowSubmitOutcome = readShadowSubmitOutcome;
|
|
5268
|
+
exports.resolvePersonalSignChainDtoPayload = resolvePersonalSignChainDtoPayload;
|
|
5269
|
+
exports.resolveUniqueKeyPrefix = resolveUniqueKeyPrefix;
|
|
5270
|
+
exports.serialize = serialize;
|
|
5271
|
+
exports.signChainDtoHash = signChainDtoHash;
|
|
5272
|
+
exports.signChainDtoNative = signChainDtoNative;
|
|
5273
|
+
exports.signChainDtoPersonalSign = signChainDtoPersonalSign;
|
|
5274
|
+
exports.signNative = signNative;
|
|
5275
|
+
exports.signPersonalMessage = signPersonalMessage;
|
|
5276
|
+
exports.signRawObject = signRawObject;
|
|
5277
|
+
exports.signSerializedNative = signSerializedNative;
|
|
5278
|
+
exports.signTypedDataPayload = signTypedDataPayload;
|
|
2890
5279
|
exports.sqrtPriceToTick = sqrtPriceToTick;
|
|
5280
|
+
exports.stripEip712DomainType = stripEip712DomainType;
|
|
5281
|
+
exports.swapRouteFromQuote = swapRouteFromQuote;
|
|
2891
5282
|
exports.tickFromPrice = tickFromPrice;
|
|
2892
5283
|
exports.tickToSqrtPrice = tickToSqrtPrice;
|
|
5284
|
+
exports.toChainDto = toChainDto;
|
|
5285
|
+
exports.tokenClassDollarKey = tokenClassDollarKey;
|
|
5286
|
+
exports.tradingSymbolOf = tradingSymbolOf;
|
|
2893
5287
|
exports.validateFee = validateFee;
|
|
2894
5288
|
exports.validateNumericAmount = validateNumericAmount;
|
|
2895
5289
|
exports.validatePriceValues = validatePriceValues;
|
|
@@ -2897,4 +5291,5 @@ exports.validateTickRange = validateTickRange;
|
|
|
2897
5291
|
exports.validateTickSpacing = validateTickSpacing;
|
|
2898
5292
|
exports.validateTokenDecimals = validateTokenDecimals;
|
|
2899
5293
|
exports.validateWalletAddress = validateWalletAddress;
|
|
5294
|
+
exports.withPrivateKey = withPrivateKey;
|
|
2900
5295
|
//# sourceMappingURL=index.cjs.map
|