hak-saucerswap-plugin 1.0.0 → 2.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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
+ import { BaseTool, handleTransaction } from '@hashgraph/hedera-agent-kit';
1
2
  import { z } from 'zod';
2
- import { ContractFunctionParameters, ContractExecuteTransaction, AccountId, TokenId, ContractId } from '@hashgraph/sdk';
3
3
  import axios from 'axios';
4
- import { AgentMode } from 'hedera-agent-kit';
4
+ import { ContractFunctionParameters, ContractExecuteTransaction, AccountId, TokenId, ContractId } from '@hiero-ledger/sdk';
5
5
 
6
- // src/tools/swap.ts
6
+ // src/tools/farms.ts
7
7
 
8
8
  // src/api/endpoints.ts
9
9
  var SAUCER_ENDPOINTS = {
@@ -34,7 +34,8 @@ var SaucerSwapClient = class {
34
34
  this.retries = options.retries ?? 2;
35
35
  this.http = options.http ?? axios.create({
36
36
  baseURL: options.baseUrl ?? "https://api.saucerswap.finance",
37
- timeout: options.timeoutMs ?? 1e4
37
+ timeout: options.timeoutMs ?? 1e4,
38
+ headers: options.apiKey ? { "x-api-key": options.apiKey } : void 0
38
39
  });
39
40
  }
40
41
  async request(path, params) {
@@ -176,6 +177,7 @@ var resolveSaucerSwapConfig = (context) => {
176
177
  baseUrl: ctxConfig.baseUrl ?? process.env.SAUCERSWAP_BASE_URL ?? DEFAULT_CONFIG.baseUrl,
177
178
  timeoutMs: ctxConfig.timeoutMs ?? toNumber(process.env.SAUCERSWAP_TIMEOUT_MS, DEFAULT_CONFIG.timeoutMs),
178
179
  retries: ctxConfig.retries ?? toNumber(process.env.SAUCERSWAP_RETRIES, DEFAULT_CONFIG.retries),
180
+ apiKey: ctxConfig.apiKey ?? process.env.SAUCERSWAP_API_KEY,
179
181
  routerContractId: ctxConfig.routerContractId ?? process.env.SAUCERSWAP_ROUTER_CONTRACT_ID,
180
182
  routerV2ContractId: ctxConfig.routerV2ContractId ?? process.env.SAUCERSWAP_ROUTER_V2_CONTRACT_ID,
181
183
  wrappedHbarTokenId: ctxConfig.wrappedHbarTokenId ?? process.env.SAUCERSWAP_WRAPPED_HBAR_TOKEN_ID,
@@ -189,6 +191,47 @@ var resolveSaucerSwapConfig = (context) => {
189
191
  };
190
192
  };
191
193
 
194
+ // src/tools/farms.ts
195
+ var farmsInputSchema = z.object({
196
+ poolId: z.number().int().positive().optional().describe("Optional pool ID to filter farms")
197
+ });
198
+ var FarmsTool = class extends BaseTool {
199
+ constructor() {
200
+ super(...arguments);
201
+ this.method = "saucerswap_get_farms";
202
+ this.name = "SaucerSwap Get Farms";
203
+ this.description = "Get active farming opportunities on SaucerSwap.";
204
+ this.parameters = farmsInputSchema;
205
+ }
206
+ async normalizeParams(params, _context, _client) {
207
+ return farmsInputSchema.parse(params);
208
+ }
209
+ async coreAction(args, context, _client) {
210
+ const config = resolveSaucerSwapConfig(context);
211
+ const api = context.saucerswapClient ?? createSaucerSwapClient(config);
212
+ try {
213
+ const farms = await api.getFarms();
214
+ const filtered = args.poolId ? farms.filter((farm) => farm.poolId === args.poolId) : farms;
215
+ return {
216
+ success: true,
217
+ farms: filtered
218
+ };
219
+ } catch (error) {
220
+ return {
221
+ success: false,
222
+ error: error instanceof Error ? error.message : "Unknown error"
223
+ };
224
+ }
225
+ }
226
+ async shouldSecondaryAction(_coreActionResult, _context) {
227
+ return false;
228
+ }
229
+ async secondaryAction(_request, _client, _context) {
230
+ return null;
231
+ }
232
+ };
233
+ var farmsTool = new FarmsTool();
234
+
192
235
  // src/utils/amm.ts
193
236
  var calculatePriceImpact = (inputAmount, outputAmount, poolReserveIn, poolReserveOut) => {
194
237
  const inAmount = Number(inputAmount);
@@ -210,53 +253,6 @@ var applySlippageToAmount = (amount, slippageTolerance) => {
210
253
  const numerator = BigInt(1e4 - bps);
211
254
  return (amountBig * numerator / 10000n).toString();
212
255
  };
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
256
  var normalizeTokenAlias = (token, config) => {
261
257
  const tokenLower = token.toLowerCase();
262
258
  for (const [alias, value] of Object.entries(config.tokenAliases)) {
@@ -284,115 +280,106 @@ var accountIdToSolidityAddress = (accountId) => {
284
280
  var contractIdFromString = (contractId) => {
285
281
  return ContractId.fromString(contractId);
286
282
  };
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
283
 
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;
284
+ // src/utils/units.ts
285
+ var parseUnits = (amount, decimals) => {
286
+ if (!amount || typeof amount !== "string") {
287
+ throw new Error("Amount must be a non-empty string.");
319
288
  }
320
- if (deadlineInput > now + 60) {
321
- return Math.floor(deadlineInput);
289
+ if (decimals < 0) {
290
+ throw new Error("Decimals must be non-negative.");
322
291
  }
323
- return now + Math.round(deadlineInput) * 60;
292
+ const trimmed = amount.trim();
293
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) {
294
+ throw new Error(`Invalid amount format: ${amount}`);
295
+ }
296
+ const [whole, fraction = ""] = trimmed.split(".");
297
+ if (fraction.length > decimals) {
298
+ throw new Error(`Amount has more than ${decimals} decimal places.`);
299
+ }
300
+ const paddedFraction = fraction.padEnd(decimals, "0");
301
+ const combined = `${whole}${paddedFraction}`;
302
+ const normalized = combined.replace(/^0+/, "");
303
+ return normalized === "" ? "0" : normalized;
324
304
  };
325
- var amountToSmallest = (amount, decimals) => {
326
- return parseUnits(amount, decimals);
305
+
306
+ // src/tools/liquidity.ts
307
+ var addLiquidityInputSchema = z.object({
308
+ tokenA: z.string().describe("First token ID"),
309
+ tokenB: z.string().describe("Second token ID"),
310
+ amountA: z.string().describe("Amount of tokenA to add"),
311
+ amountB: z.string().describe("Amount of tokenB to add"),
312
+ slippageTolerance: z.number().optional().default(0.5).describe("Maximum slippage tolerance percentage")
313
+ });
314
+ var removeLiquidityInputSchema = z.object({
315
+ tokenA: z.string().describe("First token ID"),
316
+ tokenB: z.string().describe("Second token ID"),
317
+ lpTokenAmount: z.string().describe("Amount of LP tokens to burn"),
318
+ minAmountA: z.string().describe("Minimum amount of tokenA to receive"),
319
+ minAmountB: z.string().describe("Minimum amount of tokenB to receive")
320
+ });
321
+ var isLiquidityCorePayload = (value) => typeof value === "object" && value !== null && "transaction" in value;
322
+ var addLiquidityPostProcess = (response) => `SaucerSwap add liquidity submitted. Status: ${response.status}. Transaction ID: ${response.transactionId}`;
323
+ var removeLiquidityPostProcess = (response) => `SaucerSwap remove liquidity submitted. Status: ${response.status}. Transaction ID: ${response.transactionId}`;
324
+ var resolveDeadline = (defaultMinutes) => {
325
+ return Math.floor(Date.now() / 1e3) + defaultMinutes * 60;
327
326
  };
328
- var expectedToSmallest = (amount, decimals) => {
329
- return amount.includes(".") ? parseUnits(amount, decimals) : amount;
327
+ var resolveRouterContract = (config) => {
328
+ const routerContractId = config.defaultPoolVersion === "v2" ? config.routerV2ContractId ?? config.routerContractId : config.routerContractId ?? config.routerV2ContractId;
329
+ if (!routerContractId) {
330
+ throw new Error("Missing SaucerSwap router contract ID configuration.");
331
+ }
332
+ return routerContractId;
330
333
  };
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);
334
+ var AddLiquidityTool = class extends BaseTool {
335
+ constructor() {
336
+ super(...arguments);
337
+ this.method = "saucerswap_add_liquidity";
338
+ this.name = "SaucerSwap Add Liquidity";
339
+ this.description = "Add liquidity to a SaucerSwap pool.";
340
+ this.parameters = addLiquidityInputSchema;
341
+ }
342
+ async normalizeParams(params, _context, _client) {
343
+ return addLiquidityInputSchema.parse(params);
344
+ }
345
+ async coreAction(args, context, client) {
338
346
  const config = resolveSaucerSwapConfig(context);
339
347
  const operatorAccountId = client?.operatorAccountId?.toString();
340
348
  const slippageTolerance = args.slippageTolerance ?? 0.5;
341
349
  if (!operatorAccountId) {
342
350
  return {
343
351
  success: false,
344
- error: "Hedera client with an operator account is required for swaps."
352
+ error: "Hedera client with an operator account is required for liquidity actions."
345
353
  };
346
354
  }
347
355
  try {
348
356
  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) {
357
+ const tokenAInput = normalizeTokenAlias(args.tokenA, config);
358
+ const tokenBInput = normalizeTokenAlias(args.tokenB, config);
359
+ const tokenAId = await api.resolveTokenId(tokenAInput);
360
+ const tokenBId = await api.resolveTokenId(tokenBInput);
361
+ const tokenA = await api.getTokenByIdOrSymbol(tokenAId);
362
+ const tokenB = await api.getTokenByIdOrSymbol(tokenBId);
363
+ if (!tokenA || !tokenB) {
356
364
  return {
357
365
  success: false,
358
366
  error: "Unable to resolve token metadata from SaucerSwap API."
359
367
  };
360
368
  }
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);
369
+ const amountADesired = parseUnits(args.amountA, tokenA.decimals);
370
+ const amountBDesired = parseUnits(args.amountB, tokenB.decimals);
371
+ const amountAMin = applySlippageToAmount(amountADesired, slippageTolerance);
372
+ const amountBMin = applySlippageToAmount(amountBDesired, slippageTolerance);
373
+ const routerContractId = resolveRouterContract(config);
374
+ const deadline = resolveDeadline(config.deadlineMinutes);
383
375
  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
- });
376
+ const params = new ContractFunctionParameters().addAddress(tokenIdToSolidityAddress(requireTokenId(tokenAId))).addAddress(tokenIdToSolidityAddress(requireTokenId(tokenBId))).addUint256(amountADesired).addUint256(amountBDesired).addUint256(amountAMin).addUint256(amountBMin).addAddress(toAddress).addUint256(deadline);
377
+ const transaction = new ContractExecuteTransaction().setContractId(contractIdFromString(routerContractId)).setGas(config.gasLimit).setFunction("addLiquidity", params);
378
+ const payload = {
379
+ transaction,
380
+ extras: { amountADesired, amountBDesired, amountAMin, amountBMin }
381
+ };
382
+ return payload;
396
383
  } catch (error) {
397
384
  return {
398
385
  success: false,
@@ -400,61 +387,73 @@ var swapTool = {
400
387
  };
401
388
  }
402
389
  }
390
+ async shouldSecondaryAction(coreActionResult, _context) {
391
+ return isLiquidityCorePayload(coreActionResult);
392
+ }
393
+ async secondaryAction(payload, client, context) {
394
+ const result = await handleTransaction(
395
+ payload.transaction,
396
+ client,
397
+ context,
398
+ addLiquidityPostProcess
399
+ );
400
+ return { ...result, ...payload.extras };
401
+ }
403
402
  };
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);
403
+ var RemoveLiquidityTool = class extends BaseTool {
404
+ constructor() {
405
+ super(...arguments);
406
+ this.method = "saucerswap_remove_liquidity";
407
+ this.name = "SaucerSwap Remove Liquidity";
408
+ this.description = "Remove liquidity from a SaucerSwap pool.";
409
+ this.parameters = removeLiquidityInputSchema;
410
+ }
411
+ async normalizeParams(params, _context, _client) {
412
+ return removeLiquidityInputSchema.parse(params);
413
+ }
414
+ async coreAction(args, context, client) {
417
415
  const config = resolveSaucerSwapConfig(context);
418
- const client = context.saucerswapClient;
419
- const api = client ?? createSaucerSwapClient(config);
420
- const slippageTolerance = args.slippageTolerance ?? 0.5;
416
+ const operatorAccountId = client?.operatorAccountId?.toString();
417
+ if (!operatorAccountId) {
418
+ return {
419
+ success: false,
420
+ error: "Hedera client with an operator account is required for liquidity actions."
421
+ };
422
+ }
421
423
  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
- }
424
+ const api = context.saucerswapClient ?? createSaucerSwapClient(config);
425
+ const tokenAInput = normalizeTokenAlias(args.tokenA, config);
426
+ const tokenBInput = normalizeTokenAlias(args.tokenB, config);
427
+ const tokenAId = await api.resolveTokenId(tokenAInput);
428
+ const tokenBId = await api.resolveTokenId(tokenBInput);
429
+ const pool = await api.getPoolByTokens(tokenAId, tokenBId, config.defaultPoolVersion);
430
+ if (!pool) {
431
+ return {
432
+ success: false,
433
+ error: "Unable to locate pool for the provided token pair."
434
+ };
445
435
  }
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);
436
+ const tokenA = await api.getTokenByIdOrSymbol(tokenAId);
437
+ const tokenB = await api.getTokenByIdOrSymbol(tokenBId);
438
+ if (!tokenA || !tokenB) {
439
+ return {
440
+ success: false,
441
+ error: "Unable to resolve token metadata from SaucerSwap API."
442
+ };
452
443
  }
453
- return {
454
- success: true,
455
- quote: normalized,
456
- minOutput
444
+ const lpAmount = parseUnits(args.lpTokenAmount, pool.lpToken.decimals);
445
+ const minAmountA = parseUnits(args.minAmountA, tokenA.decimals);
446
+ const minAmountB = parseUnits(args.minAmountB, tokenB.decimals);
447
+ const routerContractId = resolveRouterContract(config);
448
+ const deadline = resolveDeadline(config.deadlineMinutes);
449
+ const toAddress = accountIdToSolidityAddress(operatorAccountId);
450
+ const params = new ContractFunctionParameters().addAddress(tokenIdToSolidityAddress(requireTokenId(tokenAId))).addAddress(tokenIdToSolidityAddress(requireTokenId(tokenBId))).addUint256(lpAmount).addUint256(minAmountA).addUint256(minAmountB).addAddress(toAddress).addUint256(deadline);
451
+ const transaction = new ContractExecuteTransaction().setContractId(contractIdFromString(routerContractId)).setGas(config.gasLimit).setFunction("removeLiquidity", params);
452
+ const payload = {
453
+ transaction,
454
+ extras: { lpAmount, minAmountA, minAmountB }
457
455
  };
456
+ return payload;
458
457
  } catch (error) {
459
458
  return {
460
459
  success: false,
@@ -462,20 +461,39 @@ var quoteTool = {
462
461
  };
463
462
  }
464
463
  }
464
+ async shouldSecondaryAction(coreActionResult, _context) {
465
+ return isLiquidityCorePayload(coreActionResult);
466
+ }
467
+ async secondaryAction(payload, client, context) {
468
+ const result = await handleTransaction(
469
+ payload.transaction,
470
+ client,
471
+ context,
472
+ removeLiquidityPostProcess
473
+ );
474
+ return { ...result, ...payload.extras };
475
+ }
465
476
  };
477
+ var addLiquidityTool = new AddLiquidityTool();
478
+ var removeLiquidityTool = new RemoveLiquidityTool();
466
479
  var poolsInputSchema = z.object({
467
480
  tokenA: z.string().optional().describe("First token ID or symbol"),
468
481
  tokenB: z.string().optional().describe("Second token ID or symbol"),
469
482
  version: z.enum(["v1", "v2"]).optional().describe("Pool version"),
470
483
  limit: z.number().int().positive().optional().describe("Maximum number of pools to return")
471
484
  });
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);
485
+ var PoolsTool = class extends BaseTool {
486
+ constructor() {
487
+ super(...arguments);
488
+ this.method = "saucerswap_get_pools";
489
+ this.name = "SaucerSwap Get Pools";
490
+ this.description = "Query SaucerSwap liquidity pools and reserves.";
491
+ this.parameters = poolsInputSchema;
492
+ }
493
+ async normalizeParams(params, _context, _client) {
494
+ return poolsInputSchema.parse(params);
495
+ }
496
+ async coreAction(args, context, _client) {
479
497
  const config = resolveSaucerSwapConfig(context);
480
498
  const api = context.saucerswapClient ?? createSaucerSwapClient(config);
481
499
  try {
@@ -508,76 +526,100 @@ var poolsTool = {
508
526
  };
509
527
  }
510
528
  }
529
+ async shouldSecondaryAction(_coreActionResult, _context) {
530
+ return false;
531
+ }
532
+ async secondaryAction(_request, _client, _context) {
533
+ return null;
534
+ }
511
535
  };
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;
536
+ var poolsTool = new PoolsTool();
537
+
538
+ // src/utils/quote.ts
539
+ var readAmount = (value) => {
540
+ if (typeof value === "string") {
541
+ return value;
542
+ }
543
+ if (typeof value === "number" && Number.isFinite(value)) {
544
+ return value.toString();
545
+ }
546
+ return null;
528
547
  };
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.");
548
+ var normalizeSwapQuote = (quote, fallbackAmountIn) => {
549
+ const amountIn = readAmount(quote.amountIn) ?? fallbackAmountIn;
550
+ const expectedOutput = readAmount(quote.expectedOutput) ?? readAmount(quote.amountOut) ?? readAmount(quote.amountOutMin) ?? "";
551
+ if (!expectedOutput) {
552
+ throw new Error("SaucerSwap quote did not include an output amount.");
533
553
  }
534
- return routerContractId;
554
+ return {
555
+ amountIn,
556
+ expectedOutput,
557
+ priceImpact: typeof quote.priceImpact === "number" ? quote.priceImpact : null,
558
+ route: Array.isArray(quote.route) ? quote.route : [],
559
+ raw: quote
560
+ };
535
561
  };
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);
562
+
563
+ // src/tools/quote.ts
564
+ var quoteInputSchema = z.object({
565
+ fromToken: z.string().describe("Token ID to swap from (e.g., 'HBAR' or '0.0.123456')"),
566
+ toToken: z.string().describe("Token ID to swap to"),
567
+ amount: z.string().describe("Amount to swap (decimal format)"),
568
+ slippageTolerance: z.number().optional().default(0.5).describe("Maximum slippage tolerance percentage")
569
+ });
570
+ var QuoteTool = class extends BaseTool {
571
+ constructor() {
572
+ super(...arguments);
573
+ this.method = "saucerswap_get_swap_quote";
574
+ this.name = "SaucerSwap Get Swap Quote";
575
+ this.description = "Get a price quote for swapping tokens on SaucerSwap.";
576
+ this.parameters = quoteInputSchema;
577
+ }
578
+ async normalizeParams(params, _context, _client) {
579
+ return quoteInputSchema.parse(params);
580
+ }
581
+ async coreAction(args, context, _client) {
543
582
  const config = resolveSaucerSwapConfig(context);
544
- const operatorAccountId = client?.operatorAccountId?.toString();
583
+ const cachedApi = context.saucerswapClient;
584
+ const api = cachedApi ?? createSaucerSwapClient(config);
545
585
  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
586
  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
587
+ const fromToken = normalizeTokenAlias(args.fromToken, config);
588
+ const toToken = normalizeTokenAlias(args.toToken, config);
589
+ const fromTokenId = await api.resolveTokenId(fromToken);
590
+ const toTokenId = await api.resolveTokenId(toToken);
591
+ const rawQuote = await api.getSwapQuote({
592
+ fromToken: fromTokenId,
593
+ toToken: toTokenId,
594
+ amount: args.amount
580
595
  });
596
+ const normalized = normalizeSwapQuote(rawQuote, args.amount);
597
+ if (normalized.priceImpact === null) {
598
+ const pool = await api.getPoolByTokens(fromTokenId, toTokenId, config.defaultPoolVersion);
599
+ if (pool && rawQuote.amountIn && rawQuote.amountOut) {
600
+ const isAToB = pool.tokenA.id === fromTokenId;
601
+ const reserveIn = isAToB ? pool.tokenReserveA : pool.tokenReserveB;
602
+ const reserveOut = isAToB ? pool.tokenReserveB : pool.tokenReserveA;
603
+ normalized.priceImpact = calculatePriceImpact(
604
+ rawQuote.amountIn,
605
+ rawQuote.amountOut,
606
+ reserveIn,
607
+ reserveOut
608
+ );
609
+ }
610
+ }
611
+ let minOutput = null;
612
+ if (normalized.expectedOutput.includes(".")) {
613
+ const expectedNumber = Number(normalized.expectedOutput);
614
+ minOutput = Number.isFinite(expectedNumber) ? (expectedNumber * (1 - slippageTolerance / 100)).toString() : null;
615
+ } else {
616
+ minOutput = applySlippageToAmount(normalized.expectedOutput, slippageTolerance);
617
+ }
618
+ return {
619
+ success: true,
620
+ quote: normalized,
621
+ minOutput
622
+ };
581
623
  } catch (error) {
582
624
  return {
583
625
  success: false,
@@ -585,83 +627,110 @@ var addLiquidityTool = {
585
627
  };
586
628
  }
587
629
  }
630
+ async shouldSecondaryAction(_coreActionResult, _context) {
631
+ return false;
632
+ }
633
+ async secondaryAction(_request, _client, _context) {
634
+ return null;
635
+ }
636
+ };
637
+ var quoteTool = new QuoteTool();
638
+ var swapInputSchema = z.object({
639
+ fromToken: z.string().describe("Token ID to swap from (e.g., 'HBAR' or '0.0.123456')"),
640
+ toToken: z.string().describe("Token ID to swap to"),
641
+ amount: z.string().describe("Amount to swap (decimal format)"),
642
+ slippageTolerance: z.number().optional().default(0.5).describe("Maximum slippage tolerance percentage"),
643
+ deadline: z.number().optional().describe("Transaction deadline in minutes from now or a unix timestamp")
644
+ });
645
+ var isSwapCorePayload = (value) => typeof value === "object" && value !== null && "transaction" in value;
646
+ var swapPostProcess = (response) => `SaucerSwap swap submitted. Status: ${response.status}. Transaction ID: ${response.transactionId}`;
647
+ var resolveDeadline2 = (deadlineInput, defaultMinutes) => {
648
+ const now = Math.floor(Date.now() / 1e3);
649
+ if (!deadlineInput) {
650
+ return now + defaultMinutes * 60;
651
+ }
652
+ if (deadlineInput > now + 60) {
653
+ return Math.floor(deadlineInput);
654
+ }
655
+ return now + Math.round(deadlineInput) * 60;
588
656
  };
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);
657
+ var amountToSmallest = (amount, decimals) => {
658
+ return parseUnits(amount, decimals);
659
+ };
660
+ var expectedToSmallest = (amount, decimals) => {
661
+ return amount.includes(".") ? parseUnits(amount, decimals) : amount;
662
+ };
663
+ var SwapTool = class extends BaseTool {
664
+ constructor() {
665
+ super(...arguments);
666
+ this.method = "saucerswap_swap_tokens";
667
+ this.name = "SaucerSwap Swap Tokens";
668
+ this.description = "Execute a token swap on SaucerSwap DEX.";
669
+ this.parameters = swapInputSchema;
670
+ }
671
+ async normalizeParams(params, _context, _client) {
672
+ return swapInputSchema.parse(params);
673
+ }
674
+ async coreAction(args, context, client) {
596
675
  const config = resolveSaucerSwapConfig(context);
597
676
  const operatorAccountId = client?.operatorAccountId?.toString();
677
+ const slippageTolerance = args.slippageTolerance ?? 0.5;
598
678
  if (!operatorAccountId) {
599
679
  return {
600
680
  success: false,
601
- error: "Hedera client with an operator account is required for liquidity actions."
681
+ error: "Hedera client with an operator account is required for swaps."
602
682
  };
603
683
  }
604
684
  try {
605
685
  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) {
686
+ const fromTokenAlias = normalizeTokenAlias(args.fromToken, config);
687
+ const toTokenAlias = normalizeTokenAlias(args.toToken, config);
688
+ const fromTokenId = await api.resolveTokenId(fromTokenAlias);
689
+ const toTokenId = await api.resolveTokenId(toTokenAlias);
690
+ const fromTokenMeta = await api.getTokenByIdOrSymbol(fromTokenId);
691
+ const toTokenMeta = await api.getTokenByIdOrSymbol(toTokenId);
692
+ if (!fromTokenMeta || !toTokenMeta) {
612
693
  return {
613
694
  success: false,
614
- error: "Unable to locate pool for the provided token pair."
695
+ error: "Unable to resolve token metadata from SaucerSwap API."
615
696
  };
616
697
  }
617
- const tokenA = await api.getTokenByIdOrSymbol(tokenAId);
618
- const tokenB = await api.getTokenByIdOrSymbol(tokenBId);
619
- if (!tokenA || !tokenB) {
698
+ const quote = normalizeSwapQuote(
699
+ await api.getSwapQuote({
700
+ fromToken: fromTokenId,
701
+ toToken: toTokenId,
702
+ amount: args.amount
703
+ }),
704
+ args.amount
705
+ );
706
+ const amountInSmallest = amountToSmallest(args.amount, fromTokenMeta.decimals);
707
+ const expectedOutSmallest = expectedToSmallest(quote.expectedOutput, toTokenMeta.decimals);
708
+ const minOutSmallest = applySlippageToAmount(expectedOutSmallest, slippageTolerance);
709
+ const routerContractId = config.defaultPoolVersion === "v2" ? config.routerV2ContractId ?? config.routerContractId : config.routerContractId ?? config.routerV2ContractId;
710
+ if (!routerContractId) {
620
711
  return {
621
712
  success: false,
622
- error: "Unable to resolve token metadata from SaucerSwap API."
713
+ error: "Missing SaucerSwap router contract ID configuration."
623
714
  };
624
715
  }
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);
716
+ const deadline = resolveDeadline2(args.deadline, config.deadlineMinutes);
630
717
  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
718
+ const path = [
719
+ tokenIdToSolidityAddress(requireTokenId(fromTokenId)),
720
+ tokenIdToSolidityAddress(requireTokenId(toTokenId))
721
+ ];
722
+ const params = new ContractFunctionParameters().addUint256(amountInSmallest).addUint256(minOutSmallest).addAddressArray(path).addAddress(toAddress).addUint256(deadline);
723
+ const transaction = new ContractExecuteTransaction().setContractId(contractIdFromString(routerContractId)).setGas(config.gasLimit).setFunction("swapExactTokensForTokens", params);
724
+ const payload = {
725
+ transaction,
726
+ extras: {
727
+ estimatedOutput: quote.expectedOutput,
728
+ minOutput: minOutSmallest,
729
+ priceImpact: quote.priceImpact,
730
+ route: quote.route
731
+ }
664
732
  };
733
+ return payload;
665
734
  } catch (error) {
666
735
  return {
667
736
  success: false,
@@ -669,15 +738,36 @@ var farmsTool = {
669
738
  };
670
739
  }
671
740
  }
741
+ async shouldSecondaryAction(coreActionResult, _context) {
742
+ return isSwapCorePayload(coreActionResult);
743
+ }
744
+ async secondaryAction(payload, client, context) {
745
+ const result = await handleTransaction(
746
+ payload.transaction,
747
+ client,
748
+ context,
749
+ swapPostProcess
750
+ );
751
+ return { ...result, ...payload.extras };
752
+ }
672
753
  };
754
+ var swapTool = new SwapTool();
673
755
 
674
756
  // src/index.ts
757
+ var saucerswapPluginToolNames = {
758
+ SAUCERSWAP_GET_SWAP_QUOTE_TOOL: "saucerswap_get_swap_quote",
759
+ SAUCERSWAP_SWAP_TOKENS_TOOL: "saucerswap_swap_tokens",
760
+ SAUCERSWAP_GET_POOLS_TOOL: "saucerswap_get_pools",
761
+ SAUCERSWAP_ADD_LIQUIDITY_TOOL: "saucerswap_add_liquidity",
762
+ SAUCERSWAP_REMOVE_LIQUIDITY_TOOL: "saucerswap_remove_liquidity",
763
+ SAUCERSWAP_GET_FARMS_TOOL: "saucerswap_get_farms"
764
+ };
675
765
  var saucerswapPlugin = {
676
766
  name: "saucerswap",
677
767
  description: "Integration with SaucerSwap DEX for token swaps, liquidity provision, and yield farming",
678
768
  tools: () => [swapTool, quoteTool, poolsTool, addLiquidityTool, removeLiquidityTool, farmsTool]
679
769
  };
680
770
 
681
- export { saucerswapPlugin as default, saucerswapPlugin };
771
+ export { saucerswapPlugin as default, saucerswapPlugin, saucerswapPluginToolNames };
682
772
  //# sourceMappingURL=index.js.map
683
773
  //# sourceMappingURL=index.js.map