hak-saucerswap-plugin 1.0.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.cjs +692 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +683 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContractFunctionParameters, ContractExecuteTransaction, AccountId, TokenId, ContractId } from '@hashgraph/sdk';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { AgentMode } from 'hedera-agent-kit';
|
|
5
|
+
|
|
6
|
+
// src/tools/swap.ts
|
|
7
|
+
|
|
8
|
+
// src/api/endpoints.ts
|
|
9
|
+
var SAUCER_ENDPOINTS = {
|
|
10
|
+
poolsV1: "/pools",
|
|
11
|
+
poolsV2: "/v2/pools",
|
|
12
|
+
swapQuote: "/v1/swap/quote",
|
|
13
|
+
tokens: "/tokens",
|
|
14
|
+
farms: "/farms",
|
|
15
|
+
stats: "/stats"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/api/client.ts
|
|
19
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
var isRetryableError = (error) => {
|
|
21
|
+
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const status = error.response?.status;
|
|
25
|
+
if (!status) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return status === 429 || status >= 500 && status < 600;
|
|
29
|
+
};
|
|
30
|
+
var SaucerSwapClient = class {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.tokensCache = null;
|
|
33
|
+
this.tokenIndex = null;
|
|
34
|
+
this.retries = options.retries ?? 2;
|
|
35
|
+
this.http = options.http ?? axios.create({
|
|
36
|
+
baseURL: options.baseUrl ?? "https://api.saucerswap.finance",
|
|
37
|
+
timeout: options.timeoutMs ?? 1e4
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async request(path, params) {
|
|
41
|
+
let lastError;
|
|
42
|
+
for (let attempt = 0; attempt <= this.retries; attempt += 1) {
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.http.get(path, { params });
|
|
45
|
+
return response.data;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const axiosError = error;
|
|
48
|
+
lastError = axiosError;
|
|
49
|
+
if (attempt < this.retries && isRetryableError(axiosError)) {
|
|
50
|
+
await sleep(200 * 2 ** attempt);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw axiosError;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastError;
|
|
57
|
+
}
|
|
58
|
+
async getTokens() {
|
|
59
|
+
const tokens = await this.request(SAUCER_ENDPOINTS.tokens);
|
|
60
|
+
this.cacheTokens(tokens);
|
|
61
|
+
return tokens;
|
|
62
|
+
}
|
|
63
|
+
async getTokenByIdOrSymbol(input) {
|
|
64
|
+
if (!this.tokenIndex) {
|
|
65
|
+
await this.getTokens();
|
|
66
|
+
}
|
|
67
|
+
return this.tokenIndex?.get(input.toLowerCase()) ?? null;
|
|
68
|
+
}
|
|
69
|
+
async resolveTokenId(input) {
|
|
70
|
+
if (/^\d+\.\d+\.\d+$/.test(input)) {
|
|
71
|
+
return input;
|
|
72
|
+
}
|
|
73
|
+
const token = await this.getTokenByIdOrSymbol(input);
|
|
74
|
+
return token?.id ?? input;
|
|
75
|
+
}
|
|
76
|
+
async getPools(version) {
|
|
77
|
+
const endpoint = version === "v1" ? SAUCER_ENDPOINTS.poolsV1 : SAUCER_ENDPOINTS.poolsV2;
|
|
78
|
+
return this.request(endpoint);
|
|
79
|
+
}
|
|
80
|
+
async getPoolByTokens(tokenA, tokenB, version) {
|
|
81
|
+
const pools = await this.getPools(version);
|
|
82
|
+
const match = pools.find((pool) => {
|
|
83
|
+
const direct = pool.tokenA.id === tokenA && pool.tokenB.id === tokenB;
|
|
84
|
+
const inverse = pool.tokenA.id === tokenB && pool.tokenB.id === tokenA;
|
|
85
|
+
return direct || inverse;
|
|
86
|
+
});
|
|
87
|
+
return match ?? null;
|
|
88
|
+
}
|
|
89
|
+
async getSwapQuote(params) {
|
|
90
|
+
const { fromToken, toToken, amount } = params;
|
|
91
|
+
return this.request(SAUCER_ENDPOINTS.swapQuote, {
|
|
92
|
+
tokenIn: fromToken,
|
|
93
|
+
tokenOut: toToken,
|
|
94
|
+
amount,
|
|
95
|
+
fromToken,
|
|
96
|
+
toToken
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async getFarms() {
|
|
100
|
+
return this.request(SAUCER_ENDPOINTS.farms);
|
|
101
|
+
}
|
|
102
|
+
async getStats() {
|
|
103
|
+
return this.request(SAUCER_ENDPOINTS.stats);
|
|
104
|
+
}
|
|
105
|
+
cacheTokens(tokens) {
|
|
106
|
+
this.tokensCache = tokens;
|
|
107
|
+
this.tokenIndex = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const token of tokens) {
|
|
109
|
+
this.tokenIndex.set(token.id.toLowerCase(), token);
|
|
110
|
+
this.tokenIndex.set(token.symbol.toLowerCase(), token);
|
|
111
|
+
this.tokenIndex.set(token.name.toLowerCase(), token);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var createSaucerSwapClient = (options) => {
|
|
116
|
+
return new SaucerSwapClient(options);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/config.ts
|
|
120
|
+
var DEFAULT_CONFIG = {
|
|
121
|
+
baseUrl: "https://api.saucerswap.finance",
|
|
122
|
+
timeoutMs: 1e4,
|
|
123
|
+
retries: 2,
|
|
124
|
+
routerContractId: void 0,
|
|
125
|
+
routerV2ContractId: void 0,
|
|
126
|
+
wrappedHbarTokenId: void 0,
|
|
127
|
+
tokenAliases: {},
|
|
128
|
+
defaultPoolVersion: "v2",
|
|
129
|
+
gasLimit: 2e6,
|
|
130
|
+
deadlineMinutes: 20
|
|
131
|
+
};
|
|
132
|
+
var toNumber = (value, fallback) => {
|
|
133
|
+
if (!value) {
|
|
134
|
+
return fallback;
|
|
135
|
+
}
|
|
136
|
+
const parsed = Number(value);
|
|
137
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
138
|
+
};
|
|
139
|
+
var readTokenAliases = (value) => {
|
|
140
|
+
if (!value) {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(value);
|
|
145
|
+
if (parsed && typeof parsed === "object") {
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
return {};
|
|
152
|
+
};
|
|
153
|
+
var readPoolVersion = (value) => {
|
|
154
|
+
if (value === "v1" || value === "v2") {
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
return void 0;
|
|
158
|
+
};
|
|
159
|
+
var readContextConfig = (context) => {
|
|
160
|
+
if (!context || typeof context !== "object") {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
const ctx = context;
|
|
164
|
+
return {
|
|
165
|
+
...ctx.pluginConfig?.saucerswap ?? {},
|
|
166
|
+
...ctx.config?.saucerswap ?? {}
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
var resolveSaucerSwapConfig = (context) => {
|
|
170
|
+
const ctxConfig = readContextConfig(context);
|
|
171
|
+
const envAliases = readTokenAliases(process.env.SAUCERSWAP_TOKEN_ALIASES);
|
|
172
|
+
const envDefaultVersion = readPoolVersion(process.env.SAUCERSWAP_DEFAULT_POOL_VERSION);
|
|
173
|
+
return {
|
|
174
|
+
...DEFAULT_CONFIG,
|
|
175
|
+
...ctxConfig,
|
|
176
|
+
baseUrl: ctxConfig.baseUrl ?? process.env.SAUCERSWAP_BASE_URL ?? DEFAULT_CONFIG.baseUrl,
|
|
177
|
+
timeoutMs: ctxConfig.timeoutMs ?? toNumber(process.env.SAUCERSWAP_TIMEOUT_MS, DEFAULT_CONFIG.timeoutMs),
|
|
178
|
+
retries: ctxConfig.retries ?? toNumber(process.env.SAUCERSWAP_RETRIES, DEFAULT_CONFIG.retries),
|
|
179
|
+
routerContractId: ctxConfig.routerContractId ?? process.env.SAUCERSWAP_ROUTER_CONTRACT_ID,
|
|
180
|
+
routerV2ContractId: ctxConfig.routerV2ContractId ?? process.env.SAUCERSWAP_ROUTER_V2_CONTRACT_ID,
|
|
181
|
+
wrappedHbarTokenId: ctxConfig.wrappedHbarTokenId ?? process.env.SAUCERSWAP_WRAPPED_HBAR_TOKEN_ID,
|
|
182
|
+
tokenAliases: {
|
|
183
|
+
...envAliases,
|
|
184
|
+
...ctxConfig.tokenAliases ?? {}
|
|
185
|
+
},
|
|
186
|
+
defaultPoolVersion: ctxConfig.defaultPoolVersion ?? envDefaultVersion ?? DEFAULT_CONFIG.defaultPoolVersion,
|
|
187
|
+
gasLimit: ctxConfig.gasLimit ?? toNumber(process.env.SAUCERSWAP_GAS_LIMIT, DEFAULT_CONFIG.gasLimit),
|
|
188
|
+
deadlineMinutes: ctxConfig.deadlineMinutes ?? toNumber(process.env.SAUCERSWAP_DEADLINE_MINUTES, DEFAULT_CONFIG.deadlineMinutes)
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/utils/amm.ts
|
|
193
|
+
var calculatePriceImpact = (inputAmount, outputAmount, poolReserveIn, poolReserveOut) => {
|
|
194
|
+
const inAmount = Number(inputAmount);
|
|
195
|
+
const outAmount = Number(outputAmount);
|
|
196
|
+
const reserveIn = Number(poolReserveIn);
|
|
197
|
+
const reserveOut = Number(poolReserveOut);
|
|
198
|
+
if (!Number.isFinite(inAmount) || !Number.isFinite(outAmount) || !Number.isFinite(reserveIn) || !Number.isFinite(reserveOut) || inAmount <= 0 || outAmount <= 0 || reserveIn <= 0 || reserveOut <= 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const expectedOutput = inAmount * reserveOut / (reserveIn + inAmount);
|
|
202
|
+
if (!Number.isFinite(expectedOutput) || expectedOutput <= 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return (expectedOutput - outAmount) / expectedOutput * 100;
|
|
206
|
+
};
|
|
207
|
+
var applySlippageToAmount = (amount, slippageTolerance) => {
|
|
208
|
+
const amountBig = BigInt(amount);
|
|
209
|
+
const bps = Math.max(0, Math.round(slippageTolerance * 100));
|
|
210
|
+
const numerator = BigInt(1e4 - bps);
|
|
211
|
+
return (amountBig * numerator / 10000n).toString();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/utils/quote.ts
|
|
215
|
+
var readAmount = (value) => {
|
|
216
|
+
if (typeof value === "string") {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
220
|
+
return value.toString();
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
var normalizeSwapQuote = (quote, fallbackAmountIn) => {
|
|
225
|
+
const amountIn = readAmount(quote.amountIn) ?? fallbackAmountIn;
|
|
226
|
+
const expectedOutput = readAmount(quote.expectedOutput) ?? readAmount(quote.amountOut) ?? readAmount(quote.amountOutMin) ?? "";
|
|
227
|
+
if (!expectedOutput) {
|
|
228
|
+
throw new Error("SaucerSwap quote did not include an output amount.");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
amountIn,
|
|
232
|
+
expectedOutput,
|
|
233
|
+
priceImpact: typeof quote.priceImpact === "number" ? quote.priceImpact : null,
|
|
234
|
+
route: Array.isArray(quote.route) ? quote.route : [],
|
|
235
|
+
raw: quote
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/utils/units.ts
|
|
240
|
+
var parseUnits = (amount, decimals) => {
|
|
241
|
+
if (!amount || typeof amount !== "string") {
|
|
242
|
+
throw new Error("Amount must be a non-empty string.");
|
|
243
|
+
}
|
|
244
|
+
if (decimals < 0) {
|
|
245
|
+
throw new Error("Decimals must be non-negative.");
|
|
246
|
+
}
|
|
247
|
+
const trimmed = amount.trim();
|
|
248
|
+
if (!/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
249
|
+
throw new Error(`Invalid amount format: ${amount}`);
|
|
250
|
+
}
|
|
251
|
+
const [whole, fraction = ""] = trimmed.split(".");
|
|
252
|
+
if (fraction.length > decimals) {
|
|
253
|
+
throw new Error(`Amount has more than ${decimals} decimal places.`);
|
|
254
|
+
}
|
|
255
|
+
const paddedFraction = fraction.padEnd(decimals, "0");
|
|
256
|
+
const combined = `${whole}${paddedFraction}`;
|
|
257
|
+
const normalized = combined.replace(/^0+/, "");
|
|
258
|
+
return normalized === "" ? "0" : normalized;
|
|
259
|
+
};
|
|
260
|
+
var normalizeTokenAlias = (token, config) => {
|
|
261
|
+
const tokenLower = token.toLowerCase();
|
|
262
|
+
for (const [alias, value] of Object.entries(config.tokenAliases)) {
|
|
263
|
+
if (alias.toLowerCase() === tokenLower) {
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (tokenLower === "hbar" && config.wrappedHbarTokenId) {
|
|
268
|
+
return config.wrappedHbarTokenId;
|
|
269
|
+
}
|
|
270
|
+
return token;
|
|
271
|
+
};
|
|
272
|
+
var requireTokenId = (token) => {
|
|
273
|
+
if (!/^\d+\.\d+\.\d+$/.test(token)) {
|
|
274
|
+
throw new Error(`Token ID required for on-chain actions: ${token}`);
|
|
275
|
+
}
|
|
276
|
+
return token;
|
|
277
|
+
};
|
|
278
|
+
var tokenIdToSolidityAddress = (tokenId) => {
|
|
279
|
+
return TokenId.fromString(tokenId).toSolidityAddress();
|
|
280
|
+
};
|
|
281
|
+
var accountIdToSolidityAddress = (accountId) => {
|
|
282
|
+
return AccountId.fromString(accountId).toSolidityAddress();
|
|
283
|
+
};
|
|
284
|
+
var contractIdFromString = (contractId) => {
|
|
285
|
+
return ContractId.fromString(contractId);
|
|
286
|
+
};
|
|
287
|
+
var finalizeTransaction = async (transaction, client, context, extra) => {
|
|
288
|
+
const mode = context.mode;
|
|
289
|
+
if (mode === AgentMode.RETURN_BYTES || mode === "returnBytes" || mode === "RETURN_BYTES") {
|
|
290
|
+
const txBytes = await transaction.toBytes();
|
|
291
|
+
return {
|
|
292
|
+
success: true,
|
|
293
|
+
transactionBytes: Buffer.from(txBytes).toString("base64"),
|
|
294
|
+
...extra
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const response = await transaction.execute(client);
|
|
298
|
+
const receipt = await response.getReceipt(client);
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
transactionId: response.transactionId?.toString(),
|
|
302
|
+
status: receipt.status.toString(),
|
|
303
|
+
...extra
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/tools/swap.ts
|
|
308
|
+
var swapInputSchema = z.object({
|
|
309
|
+
fromToken: z.string().describe("Token ID to swap from (e.g., 'HBAR' or '0.0.123456')"),
|
|
310
|
+
toToken: z.string().describe("Token ID to swap to"),
|
|
311
|
+
amount: z.string().describe("Amount to swap (decimal format)"),
|
|
312
|
+
slippageTolerance: z.number().optional().default(0.5).describe("Maximum slippage tolerance percentage"),
|
|
313
|
+
deadline: z.number().optional().describe("Transaction deadline in minutes from now or a unix timestamp")
|
|
314
|
+
});
|
|
315
|
+
var resolveDeadline = (deadlineInput, defaultMinutes) => {
|
|
316
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
317
|
+
if (!deadlineInput) {
|
|
318
|
+
return now + defaultMinutes * 60;
|
|
319
|
+
}
|
|
320
|
+
if (deadlineInput > now + 60) {
|
|
321
|
+
return Math.floor(deadlineInput);
|
|
322
|
+
}
|
|
323
|
+
return now + Math.round(deadlineInput) * 60;
|
|
324
|
+
};
|
|
325
|
+
var amountToSmallest = (amount, decimals) => {
|
|
326
|
+
return parseUnits(amount, decimals);
|
|
327
|
+
};
|
|
328
|
+
var expectedToSmallest = (amount, decimals) => {
|
|
329
|
+
return amount.includes(".") ? parseUnits(amount, decimals) : amount;
|
|
330
|
+
};
|
|
331
|
+
var swapTool = {
|
|
332
|
+
method: "saucerswap_swap_tokens",
|
|
333
|
+
name: "SaucerSwap Swap Tokens",
|
|
334
|
+
description: "Execute a token swap on SaucerSwap DEX.",
|
|
335
|
+
parameters: swapInputSchema,
|
|
336
|
+
execute: async (client, context, params) => {
|
|
337
|
+
const args = swapInputSchema.parse(params);
|
|
338
|
+
const config = resolveSaucerSwapConfig(context);
|
|
339
|
+
const operatorAccountId = client?.operatorAccountId?.toString();
|
|
340
|
+
const slippageTolerance = args.slippageTolerance ?? 0.5;
|
|
341
|
+
if (!operatorAccountId) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
error: "Hedera client with an operator account is required for swaps."
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const api = context.saucerswapClient ?? createSaucerSwapClient(config);
|
|
349
|
+
const fromTokenAlias = normalizeTokenAlias(args.fromToken, config);
|
|
350
|
+
const toTokenAlias = normalizeTokenAlias(args.toToken, config);
|
|
351
|
+
const fromTokenId = await api.resolveTokenId(fromTokenAlias);
|
|
352
|
+
const toTokenId = await api.resolveTokenId(toTokenAlias);
|
|
353
|
+
const fromTokenMeta = await api.getTokenByIdOrSymbol(fromTokenId);
|
|
354
|
+
const toTokenMeta = await api.getTokenByIdOrSymbol(toTokenId);
|
|
355
|
+
if (!fromTokenMeta || !toTokenMeta) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: "Unable to resolve token metadata from SaucerSwap API."
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const quote = normalizeSwapQuote(
|
|
362
|
+
await api.getSwapQuote({
|
|
363
|
+
fromToken: fromTokenId,
|
|
364
|
+
toToken: toTokenId,
|
|
365
|
+
amount: args.amount
|
|
366
|
+
}),
|
|
367
|
+
args.amount
|
|
368
|
+
);
|
|
369
|
+
const amountInSmallest = amountToSmallest(args.amount, fromTokenMeta.decimals);
|
|
370
|
+
const expectedOutSmallest = expectedToSmallest(
|
|
371
|
+
quote.expectedOutput,
|
|
372
|
+
toTokenMeta.decimals
|
|
373
|
+
);
|
|
374
|
+
const minOutSmallest = applySlippageToAmount(expectedOutSmallest, slippageTolerance);
|
|
375
|
+
const routerContractId = config.defaultPoolVersion === "v2" ? config.routerV2ContractId ?? config.routerContractId : config.routerContractId ?? config.routerV2ContractId;
|
|
376
|
+
if (!routerContractId) {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
error: "Missing SaucerSwap router contract ID configuration."
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const deadline = resolveDeadline(args.deadline, config.deadlineMinutes);
|
|
383
|
+
const toAddress = accountIdToSolidityAddress(operatorAccountId);
|
|
384
|
+
const path = [
|
|
385
|
+
tokenIdToSolidityAddress(requireTokenId(fromTokenId)),
|
|
386
|
+
tokenIdToSolidityAddress(requireTokenId(toTokenId))
|
|
387
|
+
];
|
|
388
|
+
const params2 = new ContractFunctionParameters().addUint256(amountInSmallest).addUint256(minOutSmallest).addAddressArray(path).addAddress(toAddress).addUint256(deadline);
|
|
389
|
+
const transaction = new ContractExecuteTransaction().setContractId(contractIdFromString(routerContractId)).setGas(config.gasLimit).setFunction("swapExactTokensForTokens", params2);
|
|
390
|
+
return await finalizeTransaction(transaction, client, context, {
|
|
391
|
+
estimatedOutput: quote.expectedOutput,
|
|
392
|
+
minOutput: minOutSmallest,
|
|
393
|
+
priceImpact: quote.priceImpact,
|
|
394
|
+
route: quote.route
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
var quoteInputSchema = z.object({
|
|
405
|
+
fromToken: z.string().describe("Token ID to swap from (e.g., 'HBAR' or '0.0.123456')"),
|
|
406
|
+
toToken: z.string().describe("Token ID to swap to"),
|
|
407
|
+
amount: z.string().describe("Amount to swap (decimal format)"),
|
|
408
|
+
slippageTolerance: z.number().optional().default(0.5).describe("Maximum slippage tolerance percentage")
|
|
409
|
+
});
|
|
410
|
+
var quoteTool = {
|
|
411
|
+
method: "saucerswap_get_swap_quote",
|
|
412
|
+
name: "SaucerSwap Get Swap Quote",
|
|
413
|
+
description: "Get a price quote for swapping tokens on SaucerSwap.",
|
|
414
|
+
parameters: quoteInputSchema,
|
|
415
|
+
execute: async (_client, context, params) => {
|
|
416
|
+
const args = quoteInputSchema.parse(params);
|
|
417
|
+
const config = resolveSaucerSwapConfig(context);
|
|
418
|
+
const client = context.saucerswapClient;
|
|
419
|
+
const api = client ?? createSaucerSwapClient(config);
|
|
420
|
+
const slippageTolerance = args.slippageTolerance ?? 0.5;
|
|
421
|
+
try {
|
|
422
|
+
const fromToken = normalizeTokenAlias(args.fromToken, config);
|
|
423
|
+
const toToken = normalizeTokenAlias(args.toToken, config);
|
|
424
|
+
const fromTokenId = await api.resolveTokenId(fromToken);
|
|
425
|
+
const toTokenId = await api.resolveTokenId(toToken);
|
|
426
|
+
const rawQuote = await api.getSwapQuote({
|
|
427
|
+
fromToken: fromTokenId,
|
|
428
|
+
toToken: toTokenId,
|
|
429
|
+
amount: args.amount
|
|
430
|
+
});
|
|
431
|
+
const normalized = normalizeSwapQuote(rawQuote, args.amount);
|
|
432
|
+
if (normalized.priceImpact === null) {
|
|
433
|
+
const pool = await api.getPoolByTokens(fromTokenId, toTokenId, config.defaultPoolVersion);
|
|
434
|
+
if (pool && rawQuote.amountIn && rawQuote.amountOut) {
|
|
435
|
+
const isAToB = pool.tokenA.id === fromTokenId;
|
|
436
|
+
const reserveIn = isAToB ? pool.tokenReserveA : pool.tokenReserveB;
|
|
437
|
+
const reserveOut = isAToB ? pool.tokenReserveB : pool.tokenReserveA;
|
|
438
|
+
normalized.priceImpact = calculatePriceImpact(
|
|
439
|
+
rawQuote.amountIn,
|
|
440
|
+
rawQuote.amountOut,
|
|
441
|
+
reserveIn,
|
|
442
|
+
reserveOut
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
let minOutput = null;
|
|
447
|
+
if (normalized.expectedOutput.includes(".")) {
|
|
448
|
+
const expectedNumber = Number(normalized.expectedOutput);
|
|
449
|
+
minOutput = Number.isFinite(expectedNumber) ? (expectedNumber * (1 - slippageTolerance / 100)).toString() : null;
|
|
450
|
+
} else {
|
|
451
|
+
minOutput = applySlippageToAmount(normalized.expectedOutput, slippageTolerance);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
success: true,
|
|
455
|
+
quote: normalized,
|
|
456
|
+
minOutput
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
return {
|
|
460
|
+
success: false,
|
|
461
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
var poolsInputSchema = z.object({
|
|
467
|
+
tokenA: z.string().optional().describe("First token ID or symbol"),
|
|
468
|
+
tokenB: z.string().optional().describe("Second token ID or symbol"),
|
|
469
|
+
version: z.enum(["v1", "v2"]).optional().describe("Pool version"),
|
|
470
|
+
limit: z.number().int().positive().optional().describe("Maximum number of pools to return")
|
|
471
|
+
});
|
|
472
|
+
var poolsTool = {
|
|
473
|
+
method: "saucerswap_get_pools",
|
|
474
|
+
name: "SaucerSwap Get Pools",
|
|
475
|
+
description: "Query SaucerSwap liquidity pools and reserves.",
|
|
476
|
+
parameters: poolsInputSchema,
|
|
477
|
+
execute: async (_client, context, params) => {
|
|
478
|
+
const args = poolsInputSchema.parse(params);
|
|
479
|
+
const config = resolveSaucerSwapConfig(context);
|
|
480
|
+
const api = context.saucerswapClient ?? createSaucerSwapClient(config);
|
|
481
|
+
try {
|
|
482
|
+
const version = args.version ?? config.defaultPoolVersion;
|
|
483
|
+
const pools = await api.getPools(version);
|
|
484
|
+
let filtered = pools;
|
|
485
|
+
if (args.tokenA && args.tokenB) {
|
|
486
|
+
const tokenA = normalizeTokenAlias(args.tokenA, config);
|
|
487
|
+
const tokenB = normalizeTokenAlias(args.tokenB, config);
|
|
488
|
+
const tokenAId = await api.resolveTokenId(tokenA);
|
|
489
|
+
const tokenBId = await api.resolveTokenId(tokenB);
|
|
490
|
+
filtered = pools.filter((pool) => {
|
|
491
|
+
const direct = pool.tokenA.id === tokenAId && pool.tokenB.id === tokenBId;
|
|
492
|
+
const inverse = pool.tokenA.id === tokenBId && pool.tokenB.id === tokenAId;
|
|
493
|
+
return direct || inverse;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (args.limit && args.limit > 0) {
|
|
497
|
+
filtered = filtered.slice(0, args.limit);
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
success: true,
|
|
501
|
+
version,
|
|
502
|
+
pools: filtered
|
|
503
|
+
};
|
|
504
|
+
} catch (error) {
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
var addLiquidityInputSchema = z.object({
|
|
513
|
+
tokenA: z.string().describe("First token ID"),
|
|
514
|
+
tokenB: z.string().describe("Second token ID"),
|
|
515
|
+
amountA: z.string().describe("Amount of tokenA to add"),
|
|
516
|
+
amountB: z.string().describe("Amount of tokenB to add"),
|
|
517
|
+
slippageTolerance: z.number().optional().default(0.5).describe("Maximum slippage tolerance percentage")
|
|
518
|
+
});
|
|
519
|
+
var removeLiquidityInputSchema = z.object({
|
|
520
|
+
tokenA: z.string().describe("First token ID"),
|
|
521
|
+
tokenB: z.string().describe("Second token ID"),
|
|
522
|
+
lpTokenAmount: z.string().describe("Amount of LP tokens to burn"),
|
|
523
|
+
minAmountA: z.string().describe("Minimum amount of tokenA to receive"),
|
|
524
|
+
minAmountB: z.string().describe("Minimum amount of tokenB to receive")
|
|
525
|
+
});
|
|
526
|
+
var resolveDeadline2 = (defaultMinutes) => {
|
|
527
|
+
return Math.floor(Date.now() / 1e3) + defaultMinutes * 60;
|
|
528
|
+
};
|
|
529
|
+
var resolveRouterContract = (config) => {
|
|
530
|
+
const routerContractId = config.defaultPoolVersion === "v2" ? config.routerV2ContractId ?? config.routerContractId : config.routerContractId ?? config.routerV2ContractId;
|
|
531
|
+
if (!routerContractId) {
|
|
532
|
+
throw new Error("Missing SaucerSwap router contract ID configuration.");
|
|
533
|
+
}
|
|
534
|
+
return routerContractId;
|
|
535
|
+
};
|
|
536
|
+
var addLiquidityTool = {
|
|
537
|
+
method: "saucerswap_add_liquidity",
|
|
538
|
+
name: "SaucerSwap Add Liquidity",
|
|
539
|
+
description: "Add liquidity to a SaucerSwap pool.",
|
|
540
|
+
parameters: addLiquidityInputSchema,
|
|
541
|
+
execute: async (client, context, params) => {
|
|
542
|
+
const args = addLiquidityInputSchema.parse(params);
|
|
543
|
+
const config = resolveSaucerSwapConfig(context);
|
|
544
|
+
const operatorAccountId = client?.operatorAccountId?.toString();
|
|
545
|
+
const slippageTolerance = args.slippageTolerance ?? 0.5;
|
|
546
|
+
if (!operatorAccountId) {
|
|
547
|
+
return {
|
|
548
|
+
success: false,
|
|
549
|
+
error: "Hedera client with an operator account is required for liquidity actions."
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const api = context.saucerswapClient ?? createSaucerSwapClient(config);
|
|
554
|
+
const tokenAInput = normalizeTokenAlias(args.tokenA, config);
|
|
555
|
+
const tokenBInput = normalizeTokenAlias(args.tokenB, config);
|
|
556
|
+
const tokenAId = await api.resolveTokenId(tokenAInput);
|
|
557
|
+
const tokenBId = await api.resolveTokenId(tokenBInput);
|
|
558
|
+
const tokenA = await api.getTokenByIdOrSymbol(tokenAId);
|
|
559
|
+
const tokenB = await api.getTokenByIdOrSymbol(tokenBId);
|
|
560
|
+
if (!tokenA || !tokenB) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: "Unable to resolve token metadata from SaucerSwap API."
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const amountADesired = parseUnits(args.amountA, tokenA.decimals);
|
|
567
|
+
const amountBDesired = parseUnits(args.amountB, tokenB.decimals);
|
|
568
|
+
const amountAMin = applySlippageToAmount(amountADesired, slippageTolerance);
|
|
569
|
+
const amountBMin = applySlippageToAmount(amountBDesired, slippageTolerance);
|
|
570
|
+
const routerContractId = resolveRouterContract(config);
|
|
571
|
+
const deadline = resolveDeadline2(config.deadlineMinutes);
|
|
572
|
+
const toAddress = accountIdToSolidityAddress(operatorAccountId);
|
|
573
|
+
const params2 = new ContractFunctionParameters().addAddress(tokenIdToSolidityAddress(requireTokenId(tokenAId))).addAddress(tokenIdToSolidityAddress(requireTokenId(tokenBId))).addUint256(amountADesired).addUint256(amountBDesired).addUint256(amountAMin).addUint256(amountBMin).addAddress(toAddress).addUint256(deadline);
|
|
574
|
+
const transaction = new ContractExecuteTransaction().setContractId(contractIdFromString(routerContractId)).setGas(config.gasLimit).setFunction("addLiquidity", params2);
|
|
575
|
+
return await finalizeTransaction(transaction, client, context, {
|
|
576
|
+
amountADesired,
|
|
577
|
+
amountBDesired,
|
|
578
|
+
amountAMin,
|
|
579
|
+
amountBMin
|
|
580
|
+
});
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var removeLiquidityTool = {
|
|
590
|
+
method: "saucerswap_remove_liquidity",
|
|
591
|
+
name: "SaucerSwap Remove Liquidity",
|
|
592
|
+
description: "Remove liquidity from a SaucerSwap pool.",
|
|
593
|
+
parameters: removeLiquidityInputSchema,
|
|
594
|
+
execute: async (client, context, params) => {
|
|
595
|
+
const args = removeLiquidityInputSchema.parse(params);
|
|
596
|
+
const config = resolveSaucerSwapConfig(context);
|
|
597
|
+
const operatorAccountId = client?.operatorAccountId?.toString();
|
|
598
|
+
if (!operatorAccountId) {
|
|
599
|
+
return {
|
|
600
|
+
success: false,
|
|
601
|
+
error: "Hedera client with an operator account is required for liquidity actions."
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const api = context.saucerswapClient ?? createSaucerSwapClient(config);
|
|
606
|
+
const tokenAInput = normalizeTokenAlias(args.tokenA, config);
|
|
607
|
+
const tokenBInput = normalizeTokenAlias(args.tokenB, config);
|
|
608
|
+
const tokenAId = await api.resolveTokenId(tokenAInput);
|
|
609
|
+
const tokenBId = await api.resolveTokenId(tokenBInput);
|
|
610
|
+
const pool = await api.getPoolByTokens(tokenAId, tokenBId, config.defaultPoolVersion);
|
|
611
|
+
if (!pool) {
|
|
612
|
+
return {
|
|
613
|
+
success: false,
|
|
614
|
+
error: "Unable to locate pool for the provided token pair."
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const tokenA = await api.getTokenByIdOrSymbol(tokenAId);
|
|
618
|
+
const tokenB = await api.getTokenByIdOrSymbol(tokenBId);
|
|
619
|
+
if (!tokenA || !tokenB) {
|
|
620
|
+
return {
|
|
621
|
+
success: false,
|
|
622
|
+
error: "Unable to resolve token metadata from SaucerSwap API."
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const lpAmount = parseUnits(args.lpTokenAmount, pool.lpToken.decimals);
|
|
626
|
+
const minAmountA = parseUnits(args.minAmountA, tokenA.decimals);
|
|
627
|
+
const minAmountB = parseUnits(args.minAmountB, tokenB.decimals);
|
|
628
|
+
const routerContractId = resolveRouterContract(config);
|
|
629
|
+
const deadline = resolveDeadline2(config.deadlineMinutes);
|
|
630
|
+
const toAddress = accountIdToSolidityAddress(operatorAccountId);
|
|
631
|
+
const params2 = new ContractFunctionParameters().addAddress(tokenIdToSolidityAddress(requireTokenId(tokenAId))).addAddress(tokenIdToSolidityAddress(requireTokenId(tokenBId))).addUint256(lpAmount).addUint256(minAmountA).addUint256(minAmountB).addAddress(toAddress).addUint256(deadline);
|
|
632
|
+
const transaction = new ContractExecuteTransaction().setContractId(contractIdFromString(routerContractId)).setGas(config.gasLimit).setFunction("removeLiquidity", params2);
|
|
633
|
+
return await finalizeTransaction(transaction, client, context, {
|
|
634
|
+
lpAmount,
|
|
635
|
+
minAmountA,
|
|
636
|
+
minAmountB
|
|
637
|
+
});
|
|
638
|
+
} catch (error) {
|
|
639
|
+
return {
|
|
640
|
+
success: false,
|
|
641
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var farmsInputSchema = z.object({
|
|
647
|
+
poolId: z.number().int().positive().optional().describe("Optional pool ID to filter farms")
|
|
648
|
+
});
|
|
649
|
+
var farmsTool = {
|
|
650
|
+
method: "saucerswap_get_farms",
|
|
651
|
+
name: "SaucerSwap Get Farms",
|
|
652
|
+
description: "Get active farming opportunities on SaucerSwap.",
|
|
653
|
+
parameters: farmsInputSchema,
|
|
654
|
+
execute: async (_client, context, params) => {
|
|
655
|
+
const args = farmsInputSchema.parse(params);
|
|
656
|
+
const config = resolveSaucerSwapConfig(context);
|
|
657
|
+
const api = context.saucerswapClient ?? createSaucerSwapClient(config);
|
|
658
|
+
try {
|
|
659
|
+
const farms = await api.getFarms();
|
|
660
|
+
const filtered = args.poolId ? farms.filter((farm) => farm.poolId === args.poolId) : farms;
|
|
661
|
+
return {
|
|
662
|
+
success: true,
|
|
663
|
+
farms: filtered
|
|
664
|
+
};
|
|
665
|
+
} catch (error) {
|
|
666
|
+
return {
|
|
667
|
+
success: false,
|
|
668
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// src/index.ts
|
|
675
|
+
var saucerswapPlugin = {
|
|
676
|
+
name: "saucerswap",
|
|
677
|
+
description: "Integration with SaucerSwap DEX for token swaps, liquidity provision, and yield farming",
|
|
678
|
+
tools: () => [swapTool, quoteTool, poolsTool, addLiquidityTool, removeLiquidityTool, farmsTool]
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
export { saucerswapPlugin as default, saucerswapPlugin };
|
|
682
|
+
//# sourceMappingURL=index.js.map
|
|
683
|
+
//# sourceMappingURL=index.js.map
|