genius-intents 0.28.0 → 0.29.1-develop.0
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/CHANGELOG.md +54 -0
- package/dist/genius-intents.d.ts.map +1 -1
- package/dist/genius-intents.js +3 -0
- package/dist/genius-intents.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/dex/evm-direct-pool/evm-direct-pool.service.d.ts.map +1 -1
- package/dist/lib/dex/evm-direct-pool/evm-direct-pool.service.js +23 -2
- package/dist/lib/dex/evm-direct-pool/evm-direct-pool.service.js.map +1 -1
- package/dist/lib/dex/evm-direct-pool/evm-direct-pool.types.d.ts +3 -0
- package/dist/lib/dex/evm-direct-pool/evm-direct-pool.types.d.ts.map +1 -1
- package/dist/protocols/liquid-mesh/index.d.ts +4 -0
- package/dist/protocols/liquid-mesh/index.d.ts.map +1 -0
- package/dist/protocols/liquid-mesh/index.js +9 -0
- package/dist/protocols/liquid-mesh/index.js.map +1 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.service.d.ts +232 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.service.d.ts.map +1 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.service.js +706 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.service.js.map +1 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.types.d.ts +289 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.types.d.ts.map +1 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.types.js +21 -0
- package/dist/protocols/liquid-mesh/liquid-mesh.types.js.map +1 -0
- package/dist/types/enums.d.ts +2 -1
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/enums.js +1 -0
- package/dist/types/enums.js.map +1 -1
- package/dist/types/price-params.d.ts +2 -0
- package/dist/types/price-params.d.ts.map +1 -1
- package/dist/types/price-response.d.ts +2 -1
- package/dist/types/price-response.d.ts.map +1 -1
- package/dist/types/quote-response.d.ts +2 -1
- package/dist/types/quote-response.d.ts.map +1 -1
- package/package.json +7 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LiquidMeshService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const bs58_1 = __importDefault(require("bs58"));
|
|
9
|
+
const jose_1 = require("jose");
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const enums_1 = require("../../types/enums");
|
|
12
|
+
const address_1 = require("../../utils/address");
|
|
13
|
+
const logger_1 = require("../../utils/logger");
|
|
14
|
+
const constants_1 = require("../../utils/constants");
|
|
15
|
+
const is_native_1 = require("../../utils/is-native");
|
|
16
|
+
const throw_error_1 = require("../../utils/throw-error");
|
|
17
|
+
const create_error_message_1 = require("../../utils/create-error-message");
|
|
18
|
+
let logger;
|
|
19
|
+
/**
|
|
20
|
+
* The `LiquidMeshService` class implements the IIntentProtocol interface for token swaps
|
|
21
|
+
* using the Liquid Mesh aggregator. It provides functionality for fetching price quotes
|
|
22
|
+
* and generating transaction data for token swaps on various EVM-compatible blockchains.
|
|
23
|
+
*
|
|
24
|
+
* @implements {IIntentProtocol}
|
|
25
|
+
*/
|
|
26
|
+
class LiquidMeshService {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
/**
|
|
29
|
+
* The protocol identifier for Liquid Mesh.
|
|
30
|
+
*/
|
|
31
|
+
this.protocol = enums_1.ProtocolEnum.LIQUID_MESH;
|
|
32
|
+
/**
|
|
33
|
+
* The list of blockchain networks supported by the Liquid Mesh service.
|
|
34
|
+
*/
|
|
35
|
+
this.chains = [enums_1.ChainIdEnum.BSC, enums_1.ChainIdEnum.BASE, enums_1.ChainIdEnum.SOLANA];
|
|
36
|
+
this.networkIds = {
|
|
37
|
+
[enums_1.ChainIdEnum.BSC]: 'bsc',
|
|
38
|
+
[enums_1.ChainIdEnum.BASE]: 'base',
|
|
39
|
+
[enums_1.ChainIdEnum.SOLANA]: 'sol',
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Indicates that the service operates only on a single blockchain.
|
|
43
|
+
*/
|
|
44
|
+
this.singleChain = true;
|
|
45
|
+
/**
|
|
46
|
+
* Indicates that the service does not support cross-chain operations.
|
|
47
|
+
*/
|
|
48
|
+
this.multiChain = false;
|
|
49
|
+
/**
|
|
50
|
+
* The endpoint for quote requests.
|
|
51
|
+
*/
|
|
52
|
+
this.quoteEndpoint = '/v1/quote';
|
|
53
|
+
/**
|
|
54
|
+
* Cache for approve addresses per network to avoid repeated API calls.
|
|
55
|
+
*/
|
|
56
|
+
this._approvalContract = '0x8157a9d65807521FBB8db8f37EEEcEfDD247E9B1';
|
|
57
|
+
if (config?.debug) {
|
|
58
|
+
logger_1.LoggerFactory.configure(logger_1.LoggerFactory.createConsoleLogger({ level: logger_1.LogLevelEnum.DEBUG }));
|
|
59
|
+
}
|
|
60
|
+
// Use custom logger if provided
|
|
61
|
+
else if (config?.logger) {
|
|
62
|
+
logger_1.LoggerFactory.configure(config.logger);
|
|
63
|
+
}
|
|
64
|
+
logger = logger_1.LoggerFactory.getLogger();
|
|
65
|
+
// Initialize proxy configuration first (takes precedence over direct API access)
|
|
66
|
+
if (config?.liquidMeshProxyUrl) {
|
|
67
|
+
this._proxyUrl = config.liquidMeshProxyUrl;
|
|
68
|
+
this._proxyApiKey = config.liquidMeshProxyApiKey;
|
|
69
|
+
logger.debug('Liquid Mesh configured to use proxy', {
|
|
70
|
+
proxyUrl: this._proxyUrl,
|
|
71
|
+
hasProxyApiKey: !!this._proxyApiKey,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Only initialize direct API credentials if not using proxy
|
|
76
|
+
this._apiKey = config?.liquidMeshApiKey;
|
|
77
|
+
// Initialize Ed25519 private key if credentials are provided
|
|
78
|
+
if (config?.liquidMeshPrivateKeyBase64Seed && config?.liquidMeshPublicKeyBase64) {
|
|
79
|
+
this._privateKeyPromise = this._initializePrivateKey(config.liquidMeshPrivateKeyBase64Seed, config.liquidMeshPublicKeyBase64);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Converts a standard Base64 string to Base64URL encoding.
|
|
85
|
+
* Base64URL replaces '+' with '-', '/' with '_', and removes '=' padding.
|
|
86
|
+
*
|
|
87
|
+
* @param base64 - The standard Base64 encoded string.
|
|
88
|
+
* @returns The Base64URL encoded string.
|
|
89
|
+
*/
|
|
90
|
+
_toBase64Url(base64) {
|
|
91
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Initializes the Ed25519 private key from base64-encoded seed and public key.
|
|
95
|
+
* Automatically converts standard Base64 to Base64URL if needed.
|
|
96
|
+
*
|
|
97
|
+
* @param privateKeySeed - The Ed25519 private key seed (Base64 or Base64URL).
|
|
98
|
+
* @param publicKey - The Ed25519 public key (Base64 or Base64URL).
|
|
99
|
+
*/
|
|
100
|
+
async _initializePrivateKey(privateKeySeed, publicKey) {
|
|
101
|
+
try {
|
|
102
|
+
// Convert to Base64URL if standard Base64 is provided
|
|
103
|
+
const d = this._toBase64Url(privateKeySeed);
|
|
104
|
+
const x = this._toBase64Url(publicKey);
|
|
105
|
+
logger.debug('Initializing Ed25519 private key for Liquid Mesh', {
|
|
106
|
+
dLength: d.length,
|
|
107
|
+
xLength: x.length,
|
|
108
|
+
});
|
|
109
|
+
this._privateKey = await (0, jose_1.importJWK)({
|
|
110
|
+
kty: 'OKP',
|
|
111
|
+
crv: 'Ed25519',
|
|
112
|
+
d,
|
|
113
|
+
x,
|
|
114
|
+
}, 'EdDSA');
|
|
115
|
+
logger.debug('Successfully initialized Ed25519 private key for Liquid Mesh');
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error('Failed to initialize Ed25519 private key', error, {
|
|
119
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
120
|
+
});
|
|
121
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, `Failed to initialize Liquid Mesh authentication credentials: ${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generates a signed JWT for Liquid Mesh API authentication.
|
|
126
|
+
*
|
|
127
|
+
* @param method - The HTTP method (GET, POST, etc.).
|
|
128
|
+
* @param path - The canonical request path (e.g., /v1/bsc/quote?amount=100).
|
|
129
|
+
* @param body - The request body (empty string for GET requests).
|
|
130
|
+
* @returns A promise that resolves to the signed JWT token.
|
|
131
|
+
*/
|
|
132
|
+
async _generateAuthToken(method, path, body = '') {
|
|
133
|
+
if (!this._apiKey || !this._privateKey) {
|
|
134
|
+
// Wait for private key initialization if in progress
|
|
135
|
+
if (this._privateKeyPromise) {
|
|
136
|
+
await this._privateKeyPromise;
|
|
137
|
+
}
|
|
138
|
+
if (!this._apiKey || !this._privateKey) {
|
|
139
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, 'Liquid Mesh API credentials not configured. Please provide liquidMeshApiKey, liquidMeshPrivateKeyBase64Seed, and liquidMeshPublicKeyBase64 in config.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const timestamp = Date.now();
|
|
143
|
+
const preimage = `${timestamp}${method.toUpperCase()}${path}${body}`;
|
|
144
|
+
const message = (0, crypto_1.createHash)('sha256').update(preimage).digest('hex');
|
|
145
|
+
logger.debug('Generating Liquid Mesh auth token', {
|
|
146
|
+
method: method.toUpperCase(),
|
|
147
|
+
path,
|
|
148
|
+
timestamp,
|
|
149
|
+
});
|
|
150
|
+
const token = await new jose_1.SignJWT({
|
|
151
|
+
tim: timestamp,
|
|
152
|
+
message,
|
|
153
|
+
iss: this._apiKey,
|
|
154
|
+
})
|
|
155
|
+
.setProtectedHeader({ typ: 'JWT', alg: 'EdDSA' })
|
|
156
|
+
.setIssuedAt()
|
|
157
|
+
.setExpirationTime('2s')
|
|
158
|
+
.sign(this._privateKey);
|
|
159
|
+
return token;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Generates the authentication headers for Liquid Mesh API requests.
|
|
163
|
+
*
|
|
164
|
+
* @param method - The HTTP method.
|
|
165
|
+
* @param path - The canonical request path.
|
|
166
|
+
* @param body - The request body.
|
|
167
|
+
* @returns A promise that resolves to the headers object.
|
|
168
|
+
*/
|
|
169
|
+
async _getAuthHeaders(method, path, body = '') {
|
|
170
|
+
const token = await this._generateAuthToken(method, path, body);
|
|
171
|
+
return {
|
|
172
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
173
|
+
'LM-API-KEY': this._apiKey,
|
|
174
|
+
Authorization: `Bearer ${token}`,
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Checks if the service is configured to use a proxy.
|
|
181
|
+
*
|
|
182
|
+
* @returns True if using proxy, false for direct API access.
|
|
183
|
+
*/
|
|
184
|
+
_isUsingProxy() {
|
|
185
|
+
return !!this._proxyUrl;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Builds the request URL, routing through proxy if configured.
|
|
189
|
+
* When proxy is enabled, the original target URL is passed as a query parameter.
|
|
190
|
+
*
|
|
191
|
+
* @param targetUrl - The original Liquid Mesh API URL.
|
|
192
|
+
* @returns The URL to use for the request (proxy URL or original URL).
|
|
193
|
+
*/
|
|
194
|
+
_buildRequestUrl(targetUrl) {
|
|
195
|
+
if (!this._proxyUrl) {
|
|
196
|
+
return targetUrl;
|
|
197
|
+
}
|
|
198
|
+
const proxyUrl = new URL(this._proxyUrl);
|
|
199
|
+
proxyUrl.searchParams.set('targetUrl', targetUrl);
|
|
200
|
+
if (this._proxyApiKey) {
|
|
201
|
+
proxyUrl.searchParams.set('api_key', this._proxyApiKey);
|
|
202
|
+
}
|
|
203
|
+
logger.debug('Routing request through proxy', {
|
|
204
|
+
proxyUrl: this._proxyUrl,
|
|
205
|
+
targetUrl,
|
|
206
|
+
});
|
|
207
|
+
return proxyUrl.toString();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Gets the appropriate headers for the request.
|
|
211
|
+
* When using proxy, no auth headers are needed (proxy handles authentication).
|
|
212
|
+
* For direct API access, generates JWT auth headers.
|
|
213
|
+
*
|
|
214
|
+
* @param method - The HTTP method.
|
|
215
|
+
* @param path - The canonical request path.
|
|
216
|
+
* @param body - The request body.
|
|
217
|
+
* @returns A promise that resolves to the headers object.
|
|
218
|
+
*/
|
|
219
|
+
async _getRequestHeaders(method, path, body = '') {
|
|
220
|
+
// When using proxy, the proxy handles authentication
|
|
221
|
+
if (this._isUsingProxy()) {
|
|
222
|
+
return {
|
|
223
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
224
|
+
'Content-Type': 'application/json',
|
|
225
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// For direct API access, generate auth headers
|
|
229
|
+
return this._getAuthHeaders(method, path, body);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Checks if the provided configuration object is valid.
|
|
233
|
+
*
|
|
234
|
+
* @typeParam T - The expected shape of the configuration object.
|
|
235
|
+
* @param config - The configuration object to validate.
|
|
236
|
+
* @returns `true` if the configuration is valid, otherwise `false`.
|
|
237
|
+
*/
|
|
238
|
+
isCorrectConfig(config) {
|
|
239
|
+
// Liquid Mesh doesn't require specific config keys
|
|
240
|
+
// Accept any config object
|
|
241
|
+
return config !== null && typeof config === 'object';
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Checks if the given network is Solana.
|
|
245
|
+
*
|
|
246
|
+
* @param networkId - The chain ID to check.
|
|
247
|
+
* @returns True if the network is Solana.
|
|
248
|
+
*/
|
|
249
|
+
_isSolanaNetwork(networkId) {
|
|
250
|
+
return networkId === enums_1.ChainIdEnum.SOLANA;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Gets the native token address for the given network.
|
|
254
|
+
*
|
|
255
|
+
* @param networkId - The chain ID.
|
|
256
|
+
* @returns The native token address for the network.
|
|
257
|
+
*/
|
|
258
|
+
_getNativeAddress(networkId) {
|
|
259
|
+
return this._isSolanaNetwork(networkId) ? constants_1.SOL_NATIVE_ADDRESS : constants_1.NATIVE_ADDRESS;
|
|
260
|
+
}
|
|
261
|
+
_wrapSolanaMessageAsTransaction(base64Message) {
|
|
262
|
+
// Decode base64 to get raw bytes
|
|
263
|
+
const bytes = Buffer.from(base64Message, 'base64');
|
|
264
|
+
if (bytes.length === 0) {
|
|
265
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, 'Empty message bytes from Liquid Mesh');
|
|
266
|
+
}
|
|
267
|
+
// Detect if this is already a complete transaction or just raw message bytes
|
|
268
|
+
// A complete transaction starts with: [sigCount] [sig1...] [sig2...] [message...]
|
|
269
|
+
// A raw message starts with either: [0x80 + version] [header...] (versioned) or [numRequiredSigs] [header...] (legacy)
|
|
270
|
+
const firstByte = bytes[0];
|
|
271
|
+
// Check if this looks like a complete transaction by seeing if we can find
|
|
272
|
+
// a valid message at the expected offset
|
|
273
|
+
const potentialSigCount = firstByte;
|
|
274
|
+
const potentialMessageStart = 1 + potentialSigCount * 64;
|
|
275
|
+
// If potentialMessageStart is within bounds, check if there's a valid message there
|
|
276
|
+
if (potentialSigCount <= 10 && potentialMessageStart < bytes.length) {
|
|
277
|
+
const potentialMessageFirstByte = bytes[potentialMessageStart];
|
|
278
|
+
// Check if byte at message start looks like a valid message header
|
|
279
|
+
const isVersionedAtOffset = (potentialMessageFirstByte & 0x80) !== 0;
|
|
280
|
+
const isLegacyAtOffset = potentialMessageFirstByte > 0 && potentialMessageFirstByte <= 127;
|
|
281
|
+
if (isVersionedAtOffset || isLegacyAtOffset) {
|
|
282
|
+
// This appears to be a complete transaction already
|
|
283
|
+
// Verify by checking if the message parses correctly
|
|
284
|
+
const messageBytes = bytes.slice(potentialMessageStart);
|
|
285
|
+
let messageHeaderOffset = 0;
|
|
286
|
+
if (isVersionedAtOffset) {
|
|
287
|
+
messageHeaderOffset = 1; // Skip version prefix
|
|
288
|
+
}
|
|
289
|
+
const numRequiredSigsResponse = messageBytes[messageHeaderOffset];
|
|
290
|
+
const numRequiredSigs = numRequiredSigsResponse ?? 0;
|
|
291
|
+
// Final sanity check: signature count should match or be >= required sigs
|
|
292
|
+
if (potentialSigCount >= numRequiredSigs || potentialSigCount === 0) {
|
|
293
|
+
logger.debug('Data appears to be a complete transaction, encoding as-is', {
|
|
294
|
+
signatureCount: potentialSigCount,
|
|
295
|
+
messageStart: potentialMessageStart,
|
|
296
|
+
firstMessageByte: `0x${potentialMessageFirstByte.toString(16)}`,
|
|
297
|
+
numRequiredSignatures: numRequiredSigs,
|
|
298
|
+
totalLength: bytes.length,
|
|
299
|
+
});
|
|
300
|
+
// If sigCount is 0 but signatures are required, we need to add empty slots
|
|
301
|
+
if (potentialSigCount === 0 && numRequiredSigs > 0) {
|
|
302
|
+
// Extract the message and wrap it properly
|
|
303
|
+
return this._wrapRawMessage(messageBytes, numRequiredSigs);
|
|
304
|
+
}
|
|
305
|
+
// Already a complete transaction with proper signature slots
|
|
306
|
+
return bs58_1.default.encode(bytes);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// If we get here, treat the bytes as raw message bytes that need wrapping
|
|
311
|
+
return this._wrapRawMessage(bytes);
|
|
312
|
+
}
|
|
313
|
+
_wrapRawMessage(messageBytes, numRequiredSignaturesOverride) {
|
|
314
|
+
// Parse the message to get the number of required signatures
|
|
315
|
+
let numRequiredSignatures;
|
|
316
|
+
let messageHeaderOffset = 0;
|
|
317
|
+
const firstByte = messageBytes[0];
|
|
318
|
+
// Check if this is a versioned message (high bit set means version prefix)
|
|
319
|
+
if ((firstByte & 0x80) !== 0) {
|
|
320
|
+
// Versioned message - first byte is version prefix, header starts at byte 1
|
|
321
|
+
messageHeaderOffset = 1;
|
|
322
|
+
numRequiredSignatures = messageBytes[messageHeaderOffset] ?? 0;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// Legacy message - first byte is numRequiredSignatures (part of header)
|
|
326
|
+
numRequiredSignatures = firstByte;
|
|
327
|
+
}
|
|
328
|
+
// Allow override (used when we detected a complete txn with 0 sig slots)
|
|
329
|
+
if (numRequiredSignaturesOverride !== undefined) {
|
|
330
|
+
numRequiredSignatures = numRequiredSignaturesOverride;
|
|
331
|
+
}
|
|
332
|
+
logger.debug('Wrapping raw Solana message as transaction', {
|
|
333
|
+
messageLength: messageBytes.length,
|
|
334
|
+
firstByte: `0x${firstByte.toString(16)}`,
|
|
335
|
+
isVersioned: (firstByte & 0x80) !== 0,
|
|
336
|
+
numRequiredSignatures,
|
|
337
|
+
});
|
|
338
|
+
// Create the full transaction buffer:
|
|
339
|
+
// 1 byte for signature count + (numRequiredSignatures * 64 bytes for empty signatures) + message
|
|
340
|
+
const signatureSlotSize = 64;
|
|
341
|
+
const fullTransactionLength = 1 + numRequiredSignatures * signatureSlotSize + messageBytes.length;
|
|
342
|
+
const fullTransaction = Buffer.alloc(fullTransactionLength);
|
|
343
|
+
// Write signature count
|
|
344
|
+
fullTransaction[0] = numRequiredSignatures;
|
|
345
|
+
// Signature slots are already zeroed by Buffer.alloc
|
|
346
|
+
// Write message bytes after the signature slots
|
|
347
|
+
const messageOffset = 1 + numRequiredSignatures * signatureSlotSize;
|
|
348
|
+
messageBytes.copy(fullTransaction, messageOffset);
|
|
349
|
+
return bs58_1.default.encode(fullTransaction);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Fetches a price quote for a token swap from the Liquid Mesh API.
|
|
353
|
+
*
|
|
354
|
+
* @param {IntentPriceParams} params - The parameters required for the price quote.
|
|
355
|
+
*
|
|
356
|
+
* @returns {Promise<Omit<PriceResponse, 'protocolResponse'> & { protocolResponse: LiquidMeshPriceResponse }>}
|
|
357
|
+
* A promise that resolves to a `PriceResponse` object containing:
|
|
358
|
+
* - The amount of output tokens expected from the swap.
|
|
359
|
+
* - Gas estimation for the transaction.
|
|
360
|
+
* - The raw response from the Liquid Mesh API.
|
|
361
|
+
*
|
|
362
|
+
* @throws {SdkError} If the parameters are invalid or unsupported.
|
|
363
|
+
* @throws {SdkError} If the API returns an invalid response.
|
|
364
|
+
* @throws {SdkError} If there's an error fetching the price.
|
|
365
|
+
*/
|
|
366
|
+
async fetchPrice(params) {
|
|
367
|
+
this.validatePriceParams(params);
|
|
368
|
+
const requestParams = this.priceParamsToRequestParams(params);
|
|
369
|
+
logger.debug('Generated Liquid Mesh price request params', requestParams);
|
|
370
|
+
try {
|
|
371
|
+
const networkId = this.networkIds[params.networkIn];
|
|
372
|
+
if (!networkId) {
|
|
373
|
+
logger.error(`Unsupported network ID for Liquid Mesh: ${params.networkIn}`);
|
|
374
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, `Unsupported network ID for Liquid Mesh: ${params.networkIn}`);
|
|
375
|
+
}
|
|
376
|
+
const baseUrl = `https://api.liquidmesh.io/v1/${networkId}/quote`;
|
|
377
|
+
const url = new URL(baseUrl);
|
|
378
|
+
// Add query parameters
|
|
379
|
+
Object.entries(requestParams).forEach(([key, value]) => {
|
|
380
|
+
if (value !== undefined && value !== null) {
|
|
381
|
+
url.searchParams.append(key, value.toString());
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Build the canonical path with query string for JWT signing (used for direct API access)
|
|
385
|
+
const canonicalPath = `/v1/${params.networkIn}/quote?${url.searchParams.toString()}`;
|
|
386
|
+
const headers = await this._getRequestHeaders('GET', canonicalPath, '');
|
|
387
|
+
const requestUrl = this._buildRequestUrl(url.toString());
|
|
388
|
+
logger.debug(`Making request to Liquid Mesh API: ${requestUrl}`);
|
|
389
|
+
const response = await axios_1.default.get(requestUrl, { headers });
|
|
390
|
+
const liquidMeshResponse = response.data;
|
|
391
|
+
if (!liquidMeshResponse || liquidMeshResponse.code !== 0 || !liquidMeshResponse.data) {
|
|
392
|
+
logger.error('Invalid response received from Liquid Mesh API', undefined, {
|
|
393
|
+
liquidMeshResponse,
|
|
394
|
+
});
|
|
395
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.PRICE_NOT_FOUND, liquidMeshResponse?.msg || 'Invalid response received from Liquid Mesh API');
|
|
396
|
+
}
|
|
397
|
+
const quoteData = liquidMeshResponse.data;
|
|
398
|
+
logger.debug('Successfully received price info from Liquid Mesh', {
|
|
399
|
+
amountOut: quoteData.outputAmount,
|
|
400
|
+
gasEstimate: quoteData.estimatedGas,
|
|
401
|
+
});
|
|
402
|
+
const amountOut = quoteData.outputAmount;
|
|
403
|
+
if (!amountOut) {
|
|
404
|
+
logger.error('No output amount received from Liquid Mesh', undefined, {
|
|
405
|
+
liquidMeshResponse,
|
|
406
|
+
});
|
|
407
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.PRICE_NOT_FOUND, 'No output amount received from Liquid Mesh');
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
protocol: this.protocol,
|
|
411
|
+
networkIn: params.networkIn,
|
|
412
|
+
networkOut: params.networkOut,
|
|
413
|
+
tokenIn: params.tokenIn,
|
|
414
|
+
tokenOut: params.tokenOut,
|
|
415
|
+
amountIn: params.amountIn,
|
|
416
|
+
amountOut,
|
|
417
|
+
estimatedGas: quoteData.estimatedGas?.toString(),
|
|
418
|
+
protocolResponse: {
|
|
419
|
+
quoteData,
|
|
420
|
+
},
|
|
421
|
+
slippage: params.slippage,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const formattedError = (0, create_error_message_1.createErrorMessage)(error, this.protocol);
|
|
426
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.PRICE_NOT_FOUND, formattedError);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Fetches a swap quote from the Liquid Mesh API and builds the transaction data
|
|
431
|
+
* needed to execute the swap.
|
|
432
|
+
*
|
|
433
|
+
* Note: Liquid Mesh uses a single endpoint for both price and quote. The quote
|
|
434
|
+
* endpoint returns route information that can be used to construct the swap transaction.
|
|
435
|
+
*
|
|
436
|
+
* @param {IntentQuoteParams} params - The parameters required for the swap quote.
|
|
437
|
+
*
|
|
438
|
+
* @returns {Promise<QuoteResponse & { protocolResponse: LiquidMeshPriceResponse }>}
|
|
439
|
+
* A promise that resolves to a `QuoteResponse` object containing:
|
|
440
|
+
* - The expected amount of output tokens.
|
|
441
|
+
* - The transaction data needed to execute the swap.
|
|
442
|
+
* - Gas estimates for the transaction.
|
|
443
|
+
*
|
|
444
|
+
* @throws {SdkError} If the parameters are invalid or unsupported.
|
|
445
|
+
* @throws {SdkError} If the API returns an invalid response.
|
|
446
|
+
* @throws {SdkError} If there's an error fetching the quote.
|
|
447
|
+
*/
|
|
448
|
+
async fetchQuote(params) {
|
|
449
|
+
logger.info(`Fetching swap quote for address: ${params.from}`);
|
|
450
|
+
const nativeAddress = this._getNativeAddress(params.networkIn);
|
|
451
|
+
params.tokenIn = (0, is_native_1.isNative)(params.tokenIn) ? nativeAddress : params.tokenIn;
|
|
452
|
+
params.tokenOut = (0, is_native_1.isNative)(params.tokenOut) ? nativeAddress : params.tokenOut;
|
|
453
|
+
this.validatePriceParams(params);
|
|
454
|
+
const { networkIn } = params;
|
|
455
|
+
let { priceResponse } = params;
|
|
456
|
+
if (!priceResponse || !this.isLiquidMeshPriceResponse(priceResponse.protocolResponse)) {
|
|
457
|
+
logger.info('No price response received, fetching price...');
|
|
458
|
+
priceResponse = await this.fetchPrice(params);
|
|
459
|
+
}
|
|
460
|
+
if (!this.isLiquidMeshPriceResponse(priceResponse.protocolResponse)) {
|
|
461
|
+
logger.error('Invalid price response received', undefined, {
|
|
462
|
+
priceResponse,
|
|
463
|
+
});
|
|
464
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.PRICE_NOT_FOUND, 'Invalid price response received');
|
|
465
|
+
}
|
|
466
|
+
const quoteData = priceResponse.protocolResponse.quoteData;
|
|
467
|
+
console.log(`QUOTE_DATA`, quoteData);
|
|
468
|
+
// Route to appropriate handler based on network type
|
|
469
|
+
if (this._isSolanaNetwork(networkIn)) {
|
|
470
|
+
return this._fetchSolanaQuote(params, quoteData, priceResponse);
|
|
471
|
+
}
|
|
472
|
+
return this._fetchEvmQuote(params, quoteData, priceResponse);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Fetches a swap quote for EVM chains and builds the transaction data.
|
|
476
|
+
*
|
|
477
|
+
* @private
|
|
478
|
+
*/
|
|
479
|
+
async _fetchEvmQuote(params, quoteData, priceResponse) {
|
|
480
|
+
const { from, receiver, tokenIn, amountIn, networkIn, networkOut } = params;
|
|
481
|
+
try {
|
|
482
|
+
logger.debug('Processing Liquid Mesh EVM quote response');
|
|
483
|
+
if (!quoteData || !quoteData.outputAmount) {
|
|
484
|
+
logger.error('No output amount received from Liquid Mesh', undefined, {
|
|
485
|
+
quoteData,
|
|
486
|
+
});
|
|
487
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.QUOTE_NOT_FOUND, 'No output amount received from Liquid Mesh');
|
|
488
|
+
}
|
|
489
|
+
// Call the swap endpoint to get transaction calldata
|
|
490
|
+
// Convert slippage from percentage to basis points (1% = 100 bps)
|
|
491
|
+
const slippageBps = Math.round(params.slippage * 100);
|
|
492
|
+
const swapRequestBody = {
|
|
493
|
+
userAddress: (0, address_1.formatAddress)(from),
|
|
494
|
+
slippageBps,
|
|
495
|
+
swapInfo: quoteData,
|
|
496
|
+
disableSimulate: true,
|
|
497
|
+
};
|
|
498
|
+
const networkId = this.networkIds[networkIn];
|
|
499
|
+
if (!networkId) {
|
|
500
|
+
logger.error(`Unsupported network ID for Liquid Mesh: ${networkIn}`);
|
|
501
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, `Unsupported network ID for Liquid Mesh: ${networkIn}`);
|
|
502
|
+
}
|
|
503
|
+
const swapUrl = `https://api.liquidmesh.io/v1/${networkId}/swap`;
|
|
504
|
+
const swapPath = `/v1/${networkId}/swap`;
|
|
505
|
+
const swapBodyString = JSON.stringify(swapRequestBody);
|
|
506
|
+
const swapHeaders = await this._getRequestHeaders('POST', swapPath, swapBodyString);
|
|
507
|
+
const swapRequestUrl = this._buildRequestUrl(swapUrl);
|
|
508
|
+
logger.debug(`Making request to Liquid Mesh swap API: ${swapRequestUrl}`, swapRequestBody);
|
|
509
|
+
const swapResponse = await axios_1.default.post(swapRequestUrl, swapRequestBody, {
|
|
510
|
+
headers: swapHeaders,
|
|
511
|
+
});
|
|
512
|
+
if (!swapResponse.data || swapResponse.data.code !== 0 || !swapResponse.data.data) {
|
|
513
|
+
logger.error('Invalid response received from Liquid Mesh swap API', undefined, {
|
|
514
|
+
swapResponse: swapResponse.data,
|
|
515
|
+
});
|
|
516
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.QUOTE_NOT_FOUND, swapResponse.data?.msg || 'Invalid response received from Liquid Mesh swap API');
|
|
517
|
+
}
|
|
518
|
+
const swapData = swapResponse.data.data;
|
|
519
|
+
const callMsg = swapData.callMsg;
|
|
520
|
+
const gasEstimate = swapData.estimatedGas?.toString() || quoteData.estimatedGas?.toString() || '0';
|
|
521
|
+
const gasLimit = Math.floor(Number(gasEstimate) * 1.1).toString(); // 10% buffer
|
|
522
|
+
const evmExecutionPayload = {
|
|
523
|
+
transactionData: {
|
|
524
|
+
data: callMsg.data,
|
|
525
|
+
to: callMsg.to,
|
|
526
|
+
value: callMsg.value || ((0, is_native_1.isNative)(tokenIn) ? amountIn : '0'),
|
|
527
|
+
gasEstimate,
|
|
528
|
+
gasLimit,
|
|
529
|
+
},
|
|
530
|
+
approval: {
|
|
531
|
+
token: tokenIn,
|
|
532
|
+
amount: amountIn,
|
|
533
|
+
spender: this._approvalContract,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const quote = {
|
|
537
|
+
protocol: this.protocol,
|
|
538
|
+
tokenIn: tokenIn,
|
|
539
|
+
tokenOut: quoteData.outputToken,
|
|
540
|
+
amountIn: amountIn,
|
|
541
|
+
amountOut: quoteData.outputAmount,
|
|
542
|
+
from,
|
|
543
|
+
receiver: receiver || from,
|
|
544
|
+
evmExecutionPayload,
|
|
545
|
+
slippage: priceResponse.slippage,
|
|
546
|
+
networkIn,
|
|
547
|
+
networkOut,
|
|
548
|
+
protocolResponse: {
|
|
549
|
+
quoteData,
|
|
550
|
+
swapData,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
return quote;
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
const formattedError = (0, create_error_message_1.createErrorMessage)(error, this.protocol);
|
|
557
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.QUOTE_NOT_FOUND, formattedError);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Fetches a swap quote for Solana and builds the transaction data.
|
|
562
|
+
*
|
|
563
|
+
* @private
|
|
564
|
+
*/
|
|
565
|
+
async _fetchSolanaQuote(params, quoteData, priceResponse) {
|
|
566
|
+
const { from, receiver, tokenIn, amountIn, networkIn, networkOut } = params;
|
|
567
|
+
try {
|
|
568
|
+
logger.debug('Processing Liquid Mesh Solana quote response');
|
|
569
|
+
if (!quoteData || !quoteData.outputAmount) {
|
|
570
|
+
logger.error('No output amount received from Liquid Mesh', undefined, {
|
|
571
|
+
quoteData,
|
|
572
|
+
});
|
|
573
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.QUOTE_NOT_FOUND, 'No output amount received from Liquid Mesh');
|
|
574
|
+
}
|
|
575
|
+
const overrideParams = params?.overrideParamsLiquidMesh;
|
|
576
|
+
const computeUnitLimit = overrideParams?.liquidMeshComputeUnitLimit;
|
|
577
|
+
const prioritizationFeeLamports = overrideParams?.liquidMeshPrioritizationFeeLamports;
|
|
578
|
+
const jitoAccount = overrideParams?.liquidMeshJitoAccount;
|
|
579
|
+
const jitoTips = overrideParams?.liquidMeshJitoTips;
|
|
580
|
+
// Call the swap endpoint to get serialized transaction
|
|
581
|
+
// Convert slippage from percentage to basis points (1% = 100 bps)
|
|
582
|
+
const slippageBps = Math.round(params.slippage * 100);
|
|
583
|
+
const swapRequestBody = {
|
|
584
|
+
userAddress: from, // Solana address in base58
|
|
585
|
+
slippageBps,
|
|
586
|
+
swapInfo: quoteData,
|
|
587
|
+
disableSimulate: true,
|
|
588
|
+
...(computeUnitLimit && { computeUnitLimit }),
|
|
589
|
+
...(prioritizationFeeLamports && { prioritizationFeeLamports }),
|
|
590
|
+
...(jitoAccount && { jitoAccount }),
|
|
591
|
+
...(jitoTips && { jitoTips }),
|
|
592
|
+
};
|
|
593
|
+
const networkId = this.networkIds[networkIn];
|
|
594
|
+
if (!networkId) {
|
|
595
|
+
logger.error(`Unsupported network ID for Liquid Mesh: ${networkIn}`);
|
|
596
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, `Unsupported network ID for Liquid Mesh: ${networkIn}`);
|
|
597
|
+
}
|
|
598
|
+
const swapUrl = `https://api.liquidmesh.io/v1/${networkId}/swap`;
|
|
599
|
+
const swapPath = `/v1/${networkId}/swap`;
|
|
600
|
+
const swapBodyString = JSON.stringify(swapRequestBody);
|
|
601
|
+
const swapHeaders = await this._getRequestHeaders('POST', swapPath, swapBodyString);
|
|
602
|
+
const swapRequestUrl = this._buildRequestUrl(swapUrl);
|
|
603
|
+
logger.debug(`Making request to Liquid Mesh Solana swap API: ${swapRequestUrl}`, swapRequestBody);
|
|
604
|
+
const swapResponse = await axios_1.default.post(swapRequestUrl, swapRequestBody, {
|
|
605
|
+
headers: swapHeaders,
|
|
606
|
+
});
|
|
607
|
+
if (!swapResponse.data || swapResponse.data.code !== 0 || !swapResponse.data.data) {
|
|
608
|
+
logger.error('Invalid response received from Liquid Mesh Solana swap API', undefined, {
|
|
609
|
+
swapResponse: swapResponse.data,
|
|
610
|
+
});
|
|
611
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.QUOTE_NOT_FOUND, swapResponse.data?.msg || 'Invalid response received from Liquid Mesh Solana swap API');
|
|
612
|
+
}
|
|
613
|
+
const swapData = swapResponse.data.data;
|
|
614
|
+
// Liquid Mesh returns a base64-encoded raw message (not a full serialized transaction)
|
|
615
|
+
// We need to wrap it with the proper signature structure before converting to base58
|
|
616
|
+
const base58Transaction = this._wrapSolanaMessageAsTransaction(swapData.swapTransaction);
|
|
617
|
+
const svmExecutionPayload = [base58Transaction];
|
|
618
|
+
const quote = {
|
|
619
|
+
protocol: this.protocol,
|
|
620
|
+
tokenIn: tokenIn,
|
|
621
|
+
tokenOut: quoteData.outputToken,
|
|
622
|
+
amountIn: amountIn,
|
|
623
|
+
amountOut: quoteData.outputAmount,
|
|
624
|
+
from,
|
|
625
|
+
receiver: receiver || from,
|
|
626
|
+
svmExecutionPayload,
|
|
627
|
+
slippage: priceResponse.slippage,
|
|
628
|
+
networkIn,
|
|
629
|
+
networkOut,
|
|
630
|
+
protocolResponse: {
|
|
631
|
+
quoteData,
|
|
632
|
+
swapData,
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
return quote;
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
const formattedError = (0, create_error_message_1.createErrorMessage)(error, this.protocol);
|
|
639
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.QUOTE_NOT_FOUND, formattedError);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Transforms the price parameters to the format expected by the Liquid Mesh API.
|
|
644
|
+
*
|
|
645
|
+
* @param {IntentPriceParams} params - The original price parameters.
|
|
646
|
+
*
|
|
647
|
+
* @returns {LiquidMeshPriceRequestParams} The transformed parameters ready for the Liquid Mesh API.
|
|
648
|
+
*/
|
|
649
|
+
priceParamsToRequestParams(params) {
|
|
650
|
+
const { tokenIn, tokenOut, amountIn, networkIn, from } = params;
|
|
651
|
+
logger.debug('Converting price params to Liquid Mesh request params', {
|
|
652
|
+
params,
|
|
653
|
+
});
|
|
654
|
+
const nativeAddress = this._getNativeAddress(networkIn);
|
|
655
|
+
const isSolana = this._isSolanaNetwork(networkIn);
|
|
656
|
+
const requestParams = {
|
|
657
|
+
chainId: networkIn.toString(),
|
|
658
|
+
inputToken: (0, is_native_1.isNative)(tokenIn) ? nativeAddress : tokenIn,
|
|
659
|
+
outputToken: (0, is_native_1.isNative)(tokenOut) ? nativeAddress : tokenOut,
|
|
660
|
+
amount: amountIn,
|
|
661
|
+
// For Solana, don't use formatAddress as it expects EVM checksummed addresses
|
|
662
|
+
userAddress: isSolana ? from : (0, address_1.formatAddress)(from),
|
|
663
|
+
};
|
|
664
|
+
logger.debug('Generated Liquid Mesh request params', requestParams);
|
|
665
|
+
return requestParams;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Validates the parameters for a price quote request.
|
|
669
|
+
*
|
|
670
|
+
* @param {IntentPriceParams} params - The parameters to validate.
|
|
671
|
+
*
|
|
672
|
+
* @throws {SdkError} If any of the parameters are invalid or unsupported.
|
|
673
|
+
*/
|
|
674
|
+
validatePriceParams(params) {
|
|
675
|
+
const { networkIn, networkOut } = params;
|
|
676
|
+
logger.debug('Validating price params');
|
|
677
|
+
if (!this.multiChain && networkIn !== networkOut) {
|
|
678
|
+
logger.error('Multi-chain swaps not supported');
|
|
679
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, 'Multi-chain swaps not supported');
|
|
680
|
+
}
|
|
681
|
+
if (!this.singleChain && networkIn === networkOut) {
|
|
682
|
+
logger.error('Single-chain swaps not supported');
|
|
683
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, 'Single-chain swaps not supported');
|
|
684
|
+
}
|
|
685
|
+
if (!this.chains.includes(networkIn)) {
|
|
686
|
+
logger.error(`Network ${networkIn} not supported`);
|
|
687
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, `Network ${networkIn} not supported`);
|
|
688
|
+
}
|
|
689
|
+
if (!this.chains.includes(networkOut)) {
|
|
690
|
+
logger.error(`Network ${networkOut} not supported`);
|
|
691
|
+
throw (0, throw_error_1.sdkError)(enums_1.SdkErrorEnum.INVALID_PARAMS, `Network ${networkOut} not supported`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Type guard to check if a response is a valid Liquid Mesh price response.
|
|
696
|
+
*
|
|
697
|
+
* @param {RawProtocolPriceResponse} response - The response to check.
|
|
698
|
+
*
|
|
699
|
+
* @returns {boolean} True if the response is a valid Liquid Mesh price response.
|
|
700
|
+
*/
|
|
701
|
+
isLiquidMeshPriceResponse(response) {
|
|
702
|
+
return 'quoteData' in response;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
exports.LiquidMeshService = LiquidMeshService;
|
|
706
|
+
//# sourceMappingURL=liquid-mesh.service.js.map
|