saucer-swap-plugin 0.1.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.
@@ -0,0 +1,102 @@
1
+ import { AccountResolver } from "hedera-agent-kit";
2
+ import { getSwapQuoteV2Parameters, swapV2Parameters } from './saucer-swap.zod';
3
+ import { ethers } from "ethers";
4
+ import z from 'zod';
5
+ import { getHederaTokenAddress, getHederaTokenEVMAddress, getTokenDecimals, toBaseUnit } from "./utils";
6
+ import SwapRouterAbi from './abi/SwapRouter.json' assert { type: 'json' };
7
+ import { buildEncodedPath } from "./utils/swap-path";
8
+ import { SAUCER_SWAP_CONFIG } from "./constants";
9
+ import { PoolFinderService } from "./service/pool-finder-service";
10
+ import { InvalidAmountError } from "./errors";
11
+ export default class SaucerSwapV2ParameterNormaliser {
12
+ static parseParamsWithSchema(params, schema, context = {}) {
13
+ let parsedParams;
14
+ try {
15
+ parsedParams = schema(context).parse(params);
16
+ }
17
+ catch (e) {
18
+ if (e instanceof z.ZodError) {
19
+ const issues = this.formatZodIssues(e);
20
+ throw new Error(`Invalid parameters: ${issues}`);
21
+ }
22
+ throw e;
23
+ }
24
+ return parsedParams;
25
+ }
26
+ static formatZodIssues(error) {
27
+ return error.errors.map(err => `Field "${err.path.join('.')}" - ${err.message}`).join('; ');
28
+ }
29
+ static async normaliseGetSwapQuoteV2Params(params, context, saucerSwapApiService) {
30
+ const parsedParams = this.parseParamsWithSchema(params, getSwapQuoteV2Parameters, context);
31
+ // Validate amount
32
+ if (parsedParams.amountIn <= 0) {
33
+ throw new InvalidAmountError(parsedParams.amountIn);
34
+ }
35
+ const tokenInEVM = getHederaTokenEVMAddress(parsedParams.tokenIn);
36
+ const inputTokenHedera = getHederaTokenAddress(parsedParams.tokenIn);
37
+ const tokenOutEVM = getHederaTokenEVMAddress(parsedParams.tokenOut);
38
+ const outputTokenHedera = getHederaTokenAddress(parsedParams.tokenOut);
39
+ // Use PoolFinderService to find the pool
40
+ const pool = await PoolFinderService.findPoolForTokens(inputTokenHedera, outputTokenHedera, saucerSwapApiService);
41
+ const poolFees = pool.fee;
42
+ const poolFeesInHexFormat = `0x${poolFees?.toString(16).padStart(6, '0')}`;
43
+ const decimals = getTokenDecimals(pool, inputTokenHedera);
44
+ const amountIn = toBaseUnit(parsedParams.amountIn, decimals).toNumber();
45
+ return {
46
+ tokenIn: tokenInEVM,
47
+ tokenOut: tokenOutEVM,
48
+ amountIn,
49
+ poolFeesInHexFormat: poolFeesInHexFormat
50
+ };
51
+ }
52
+ static async normaliseSwapV2Params(params, context, saucerSwapV2ConfigService, saucerSwapApiService, mirrorNode, client) {
53
+ const parsedParams = this.parseParamsWithSchema(params, swapV2Parameters, context);
54
+ // Validate amount is positive
55
+ if (parsedParams.amountIn <= 0) {
56
+ throw new InvalidAmountError(parsedParams.amountIn);
57
+ }
58
+ const inputTokenEVM = getHederaTokenEVMAddress(parsedParams.tokenIn);
59
+ const inputTokenHedera = getHederaTokenAddress(parsedParams.tokenIn);
60
+ const outputTokenEVM = getHederaTokenEVMAddress(parsedParams.tokenOut);
61
+ const outputTokenHedera = getHederaTokenAddress(parsedParams.tokenOut);
62
+ const recipient = AccountResolver.resolveAccount(parsedParams.recipientAddress, context, client);
63
+ const recipientAddress = await AccountResolver.getHederaEVMAddress(recipient, mirrorNode);
64
+ // Use PoolFinderService to find the pool
65
+ const pool = await PoolFinderService.findPoolForTokens(inputTokenHedera, outputTokenHedera, saucerSwapApiService);
66
+ const poolFees = pool.fee;
67
+ const decimals = getTokenDecimals(pool, inputTokenHedera);
68
+ const amountIn = toBaseUnit(parsedParams.amountIn, decimals).toNumber();
69
+ const poolFeesInHexFormat = `0x${poolFees?.toString(16).padStart(6, '0')}`;
70
+ const routeDataWithFee = buildEncodedPath(inputTokenEVM, poolFeesInHexFormat.toLowerCase(), outputTokenEVM);
71
+ const abiSwapRouterInterface = new ethers.Interface(SwapRouterAbi);
72
+ const swapRouterContractId = saucerSwapV2ConfigService.getSwapRouterContractId();
73
+ const wrappedHBarEvmAddress = saucerSwapV2ConfigService.getWrappedHBarEvmAddress();
74
+ //ExactInputParams
75
+ const exactInputParams = {
76
+ path: routeDataWithFee, //'0x...'
77
+ recipient: recipientAddress, //'0x...' - user's recipient address
78
+ deadline: Math.floor(Date.now() / 1000) + SAUCER_SWAP_CONFIG.DEFAULT_DEADLINE_SECONDS, // Unix seconds from now
79
+ amountIn: amountIn, //in Tinybar
80
+ amountOutMinimum: 0 //in token's smallest unit
81
+ };
82
+ //encode each function individually
83
+ const swapEncoded = abiSwapRouterInterface.encodeFunctionData('exactInput', [exactInputParams]);
84
+ const refundHBAREncoded = abiSwapRouterInterface.encodeFunctionData('refundETH');
85
+ //multi-call parameter: bytes[]
86
+ const multiCallParam = [swapEncoded, refundHBAREncoded];
87
+ //get encoded data for the multicall involving both functions
88
+ const encodedData = abiSwapRouterInterface.encodeFunctionData('multicall', [multiCallParam]);
89
+ //get encoded data as Uint8Array
90
+ const functionParameters = ethers.getBytes(encodedData);
91
+ let response = {
92
+ contractId: swapRouterContractId.toString(),
93
+ functionParameters,
94
+ gas: SAUCER_SWAP_CONFIG.SWAP_GAS_LIMIT,
95
+ payableAmount: undefined,
96
+ };
97
+ if (inputTokenEVM === wrappedHBarEvmAddress) {
98
+ response.payableAmount = amountIn;
99
+ }
100
+ return response;
101
+ }
102
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ export const getSwapQuoteV2Parameters = () => z.object({
3
+ tokenIn: z.string().describe("Input token address"),
4
+ tokenOut: z.string().describe("Output token address"),
5
+ amountIn: z.number().describe("Amount of input tokens to swap"),
6
+ });
7
+ export const getSwapQuoteV2ParametersNormalised = () => z.object({
8
+ tokenIn: z.string().describe("Input token address"),
9
+ tokenOut: z.string().describe("Output token address"),
10
+ amountIn: z.number().describe("Amount of input tokens to swap"),
11
+ poolFeesInHexFormat: z.string().describe("Pool fees in hex format"),
12
+ });
13
+ export const swapV2Parameters = () => z.object({
14
+ tokenIn: z.string().describe("Input token address"),
15
+ tokenOut: z.string().describe("Output token address"),
16
+ amountIn: z.number().describe("Amount of input tokens to swap"),
17
+ recipientAddress: z.string().optional().describe("Recipient address"),
18
+ });
@@ -0,0 +1,24 @@
1
+ import { PoolNotFoundError } from "../errors";
2
+ /**
3
+ * Service for finding pools by token pairs
4
+ */
5
+ export class PoolFinderService {
6
+ /**
7
+ * Finds a pool for the given token pair
8
+ *
9
+ * @param tokenA - Hedera token address (e.g., "0.0.123456")
10
+ * @param tokenB - Hedera token address (e.g., "0.0.789012")
11
+ * @param apiService - SaucerSwap API service instance
12
+ * @returns The pool matching the token pair
13
+ * @throws {PoolNotFoundError} If no pool exists for the token pair
14
+ */
15
+ static async findPoolForTokens(tokenA, tokenB, apiService) {
16
+ const pools = await apiService.getAllPoolsCompact();
17
+ const pool = pools.find(p => (p.tokenA.id === tokenA && p.tokenB.id === tokenB) ||
18
+ (p.tokenA.id === tokenB && p.tokenB.id === tokenA));
19
+ if (!pool) {
20
+ throw new PoolNotFoundError(tokenA, tokenB);
21
+ }
22
+ return pool;
23
+ }
24
+ }
@@ -0,0 +1,52 @@
1
+ import { LedgerId } from "@hashgraph/sdk";
2
+ const SAUCERSWAP_REST_BASE_URLS = {
3
+ MAINNET: "https://api.saucerswap.finance",
4
+ TESTNET: "https://test-api.saucerswap.finance",
5
+ };
6
+ /**
7
+ * Service to access SaucerSwap REST API V2 pools endpoint:
8
+ * GET /v2/pools – "Get compact data for all SaucerSwap V2 pools".
9
+ *
10
+ * Docs: https://docs.saucerswap.finance/v/developer/rest-api/pools-v2/pools
11
+ */
12
+ export class SaucerSwapApiServiceImpl {
13
+ baseUrl;
14
+ apiKey;
15
+ constructor(ledgerId, apiKey) {
16
+ const network = this.mapLedgerToNetwork(ledgerId);
17
+ this.baseUrl = SAUCERSWAP_REST_BASE_URLS[network];
18
+ // The docs provide a demo key that is globally rate limited and not for production use:
19
+ // default: 875e1017-87b8-4b12-8301-6aa1f1aa073b
20
+ // See: https://docs.saucerswap.finance/v/developer/rest-api/pools-v1/pools
21
+ this.apiKey = apiKey;
22
+ }
23
+ /**
24
+ * Calls SaucerSwap V2 "Get compact data for all SaucerSwap V2 pools" endpoint.
25
+ * GET {baseUrl}/v2/pools
26
+ */
27
+ async getAllPoolsCompact() {
28
+ const url = `${this.baseUrl}/v2/pools`;
29
+ const response = await fetch(url, {
30
+ method: "GET",
31
+ headers: {
32
+ "x-api-key": this.apiKey,
33
+ },
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`SaucerSwap REST "GET /pools" failed with status ${response.status}`);
37
+ }
38
+ const pools = (await response.json());
39
+ return pools;
40
+ }
41
+ mapLedgerToNetwork(ledgerId) {
42
+ switch (ledgerId.toString()) {
43
+ case LedgerId.MAINNET.toString():
44
+ return "MAINNET";
45
+ case LedgerId.TESTNET.toString():
46
+ return "TESTNET";
47
+ default:
48
+ // Fallback to MAINNET base URL if an unsupported ledger is used.
49
+ return "MAINNET";
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,36 @@
1
+ // write a config service to get the config from the config file
2
+ import { saucerSwapConfig } from "../config";
3
+ import { ContractId, TokenId } from "@hashgraph/sdk";
4
+ export class SaucerSwapV2ConfigService {
5
+ saucerSwapConfig;
6
+ ledgerId;
7
+ constructor(ledgerId) {
8
+ this.saucerSwapConfig = saucerSwapConfig;
9
+ this.ledgerId = ledgerId;
10
+ }
11
+ //get router address
12
+ getRouterAddress() {
13
+ return this.saucerSwapConfig.networks[this.ledgerId.toString()]?.router ?? '';
14
+ }
15
+ getSwapRouterContractId() {
16
+ return ContractId.fromEvmAddress(0, 0, this.saucerSwapConfig.networks[this.ledgerId.toString()]?.router ?? '');
17
+ }
18
+ getWrappedHBARTokenId() {
19
+ return TokenId.fromEvmAddress(0, 0, this.saucerSwapConfig.networks[this.ledgerId.toString()]?.wrappedHBAR ?? '');
20
+ }
21
+ getWrappedHBarEvmAddress() {
22
+ return this.saucerSwapConfig.networks[this.ledgerId.toString()]?.wrappedHBAR ?? '';
23
+ }
24
+ getSaucerSwapApiKey() {
25
+ //get the api key from environment variable
26
+ const apiKey = process.env.SAUCERSWAP_API_KEY;
27
+ if (!apiKey) {
28
+ throw new Error('SAUCERSWAP_API_KEY is not set');
29
+ }
30
+ return apiKey;
31
+ }
32
+ //get quoter address
33
+ getQuoterAddress() {
34
+ return this.saucerSwapConfig.networks[this.ledgerId.toString()]?.quoter ?? '';
35
+ }
36
+ }
@@ -0,0 +1,96 @@
1
+ import { ethers } from 'ethers';
2
+ import QuoterV2Abi from '../abi/QuoterV2.json' assert { type: 'json' };
3
+ import ERC20Abi from '../abi/ERC20.json' assert { type: 'json' };
4
+ import { SAUCER_SWAP_CONFIG } from '../constants';
5
+ import { MirrorNodeError, InvalidAmountError } from '../errors';
6
+ export class SaucerSwapV2QueryServiceImpl {
7
+ abiQuoterInterface;
8
+ abiERC20Interface;
9
+ quoterEvmAddress;
10
+ mirrorNodeService;
11
+ saucerSwapV2ConfigService;
12
+ constructor(ledgerId, mirrorNodeService, saucerSwapV2ConfigService) {
13
+ this.mirrorNodeService = mirrorNodeService;
14
+ this.saucerSwapV2ConfigService = saucerSwapV2ConfigService;
15
+ this.abiQuoterInterface = new ethers.Interface(QuoterV2Abi);
16
+ this.abiERC20Interface = new ethers.Interface(ERC20Abi);
17
+ this.quoterEvmAddress = this.saucerSwapV2ConfigService.getQuoterAddress();
18
+ }
19
+ /**
20
+ * Gets a swap quote for the specified token pair
21
+ ken - The EVM address const poolFeesInHexFormat = this.saucerSwapV2ConfigService.getPoolFeesInHexFormat(inputToken, outputToken);
22
+ of the input token
23
+ * @param outputToken - The EVM address of the output token
24
+ * @param amountIn - The amount of input tokens in base units
25
+ * @param poolFeesInHexFormat - The pool fee in hex format (e.g., "0x001e" for 30 bps)
26
+ * @returns The amount of output tokens that would be received
27
+ * @throws {MirrorNodeError} If the mirror node call fails
28
+ */
29
+ async getSwapQuote(inputToken, outputToken, amountIn, poolFeesInHexFormat) {
30
+ // Validate inputs
31
+ if (amountIn <= 0) {
32
+ throw new InvalidAmountError(amountIn);
33
+ }
34
+ const encodedData = this.abiQuoterInterface.encodeFunctionData(this.abiQuoterInterface.getFunction('quoteExactInputSingle'), [{
35
+ tokenIn: inputToken,
36
+ tokenOut: outputToken,
37
+ amountIn: amountIn,
38
+ fee: poolFeesInHexFormat,
39
+ sqrtPriceLimitX96: 0
40
+ }]);
41
+ const url = `${this.mirrorNodeService.getBaseUrl()}/contracts/call`;
42
+ const body = {
43
+ data: encodedData,
44
+ from: SAUCER_SWAP_CONFIG.MIRROR_NODE_FROM_ADDRESS,
45
+ to: this.quoterEvmAddress,
46
+ block: "latest",
47
+ estimate: false,
48
+ gas: SAUCER_SWAP_CONFIG.QUOTE_GAS_LIMIT,
49
+ gasPrice: SAUCER_SWAP_CONFIG.DEFAULT_GAS_PRICE,
50
+ value: 0,
51
+ };
52
+ const response = await fetch(url, {
53
+ method: 'POST',
54
+ headers: { 'content-type': 'application/json' },
55
+ body: JSON.stringify(body),
56
+ });
57
+ if (!response.ok) {
58
+ throw new MirrorNodeError(`Call failed with status ${response.status}`, response.status);
59
+ }
60
+ const json = await response.json();
61
+ const decoded = this.abiQuoterInterface.decodeFunctionResult('quoteExactInputSingle', json.result);
62
+ return Number(decoded.amountOut);
63
+ }
64
+ /**
65
+ * Gets the decimals for a token
66
+ *
67
+ * @param tokenEvmAddress - The EVM address of the token
68
+ * @returns The number of decimals for the token
69
+ * @throws {MirrorNodeError} If the mirror node call fails
70
+ */
71
+ async getDecimals(tokenEvmAddress) {
72
+ const encodedData = this.abiERC20Interface.encodeFunctionData(this.abiERC20Interface.getFunction('decimals'), []);
73
+ const url = `${this.mirrorNodeService.getBaseUrl()}/contracts/call`;
74
+ const body = {
75
+ data: encodedData,
76
+ from: SAUCER_SWAP_CONFIG.MIRROR_NODE_FROM_ADDRESS,
77
+ to: tokenEvmAddress,
78
+ block: "latest",
79
+ estimate: false,
80
+ gas: SAUCER_SWAP_CONFIG.DECIMALS_GAS_LIMIT,
81
+ gasPrice: SAUCER_SWAP_CONFIG.DEFAULT_GAS_PRICE,
82
+ value: 0,
83
+ };
84
+ const response = await fetch(url, {
85
+ method: 'POST',
86
+ headers: { 'content-type': 'application/json' },
87
+ body: JSON.stringify(body),
88
+ });
89
+ if (!response.ok) {
90
+ throw new MirrorNodeError(`Call failed with status ${response.status}`, response.status);
91
+ }
92
+ const json = await response.json();
93
+ const decoded = this.abiERC20Interface.decodeFunctionResult('decimals', json.result);
94
+ return decoded[0];
95
+ }
96
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ export function hexToUint8Array(hex) {
2
+ const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
3
+ if (cleanHex.length % 2 !== 0) {
4
+ throw new Error('Invalid hex string length');
5
+ }
6
+ const array = new Uint8Array(cleanHex.length / 2);
7
+ for (let i = 0; i < array.length; i++) {
8
+ array[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
9
+ }
10
+ return array;
11
+ }
12
+ export function buildEncodedPath(inputToken, hexFee, outputToken) {
13
+ const clean = (s) => s.startsWith('0x') ? s.slice(2) : s;
14
+ const pathParts = [];
15
+ pathParts.push(clean(inputToken));
16
+ pathParts.push(clean(hexFee));
17
+ pathParts.push(clean(outputToken));
18
+ return hexToUint8Array(pathParts.join(''));
19
+ }
@@ -0,0 +1,72 @@
1
+ import { PromptGenerator, getMirrornodeService } from "hedera-agent-kit";
2
+ import { getSwapQuoteV2Parameters } from "../saucer-swap.zod";
3
+ import { SaucerSwapV2QueryServiceImpl } from "../service/saucer-swap-v2-query-service-impl";
4
+ import SaucerSwapV2ParameterNormaliser from "../saucer-swap-v2-parameter-normaliser";
5
+ import { SaucerSwapV2ConfigService } from "../service/saucer-swap-v2-config-service";
6
+ import { SaucerSwapApiServiceImpl } from "../service/saucer-swap-rest-pools-service";
7
+ import { SaucerSwapError } from "../errors";
8
+ const getSwapQuoteV2Prompt = (context = {}) => {
9
+ const contextSnippet = PromptGenerator.getContextSnippet(context);
10
+ const usageInstructions = PromptGenerator.getParameterUsageInstructions();
11
+ return `
12
+ ${contextSnippet}
13
+
14
+ This tool will get a quote for swapping from tokenIn to tokenOut. Provide either optional.amountIn (exact-in) or optional.amountOut (exact-out).
15
+
16
+ Parameters:
17
+ - tokenIn (str, required): The input token address.
18
+ - tokenOut (str, required): The output token address.
19
+ - amountIn (number, required): The amount of input tokens to swap.
20
+ ${usageInstructions}
21
+
22
+ Example: "Get quote for swapping 1000000000000000000 amountIn from 0x1234567890abcdef1234567890abcdef12345678 tokenIn to 0xabcdef1234567890abcdef1234567890abcdef12345678 tokenOut"
23
+ `;
24
+ };
25
+ const postProcess = (quote, tokenAmountInBaseUnit, params) => {
26
+ return `Swapping ${tokenAmountInBaseUnit} token: ${params.tokenIn} to token: ${params.tokenOut} will result in ${quote} token: ${params.tokenOut}, the rate used is ${quote / tokenAmountInBaseUnit}`;
27
+ };
28
+ const getSwapQuoteV2 = async (client, context, params) => {
29
+ const mirrorNode = getMirrornodeService(context.mirrornodeService, client.ledgerId);
30
+ const saucerSwapV2ConfigService = new SaucerSwapV2ConfigService(client.ledgerId);
31
+ const saucerSwapV2QueryService = new SaucerSwapV2QueryServiceImpl(client.ledgerId, mirrorNode, saucerSwapV2ConfigService);
32
+ const saucerSwapApiService = new SaucerSwapApiServiceImpl(client.ledgerId, saucerSwapV2ConfigService.getSaucerSwapApiKey());
33
+ try {
34
+ const normalisedParams = await SaucerSwapV2ParameterNormaliser.normaliseGetSwapQuoteV2Params(params, context, saucerSwapApiService);
35
+ const quote = await saucerSwapV2QueryService.getSwapQuote(normalisedParams.tokenIn, normalisedParams.tokenOut, normalisedParams.amountIn, normalisedParams.poolFeesInHexFormat.toLowerCase());
36
+ return {
37
+ raw: { quote },
38
+ humanMessage: postProcess(quote, normalisedParams.amountIn, normalisedParams),
39
+ };
40
+ }
41
+ catch (error) {
42
+ const desc = 'Failed to get quote';
43
+ let message;
44
+ if (error instanceof SaucerSwapError) {
45
+ message = `${desc}: ${error.message} (code: ${error.code})`;
46
+ }
47
+ else if (error instanceof Error) {
48
+ message = `${desc}: ${error.message}`;
49
+ }
50
+ else {
51
+ message = `${desc}: Unknown error occurred`;
52
+ }
53
+ console.error('[get_quote_tool]', message, error);
54
+ return { raw: { error: message }, humanMessage: message };
55
+ }
56
+ };
57
+ export const GET_SWAP_QUOTE_V2_TOOL = "get_swap_quote_v2_tool";
58
+ const tool = (context) => ({
59
+ method: GET_SWAP_QUOTE_V2_TOOL,
60
+ name: "Get Quote (SaucerSwap V2)",
61
+ description: getSwapQuoteV2Prompt(context),
62
+ parameters: getSwapQuoteV2Parameters(),
63
+ execute: getSwapQuoteV2,
64
+ outputParser: (rawOutput) => {
65
+ const json = JSON.parse(rawOutput);
66
+ return {
67
+ raw: json,
68
+ humanMessage: json.quote,
69
+ };
70
+ },
71
+ });
72
+ export default tool;
@@ -0,0 +1,79 @@
1
+ import SaucerSwapV2ParameterNormaliser from "../saucer-swap-v2-parameter-normaliser";
2
+ import { swapV2Parameters } from "../saucer-swap.zod";
3
+ import { getMirrornodeService, handleTransaction, HederaBuilder, PromptGenerator } from "hedera-agent-kit";
4
+ import { Status } from "@hashgraph/sdk";
5
+ import { SaucerSwapV2ConfigService } from "../service/saucer-swap-v2-config-service";
6
+ import { SaucerSwapApiServiceImpl } from "../service/saucer-swap-rest-pools-service";
7
+ import { SaucerSwapError } from "../errors";
8
+ const swapV2Prompt = (context = {}) => {
9
+ return `
10
+ ${PromptGenerator.getContextSnippet(context)}
11
+
12
+ This tool will swap tokens using the SaucerSwap V2 protocol.
13
+
14
+ Parameters:
15
+ - tokenIn (str, required): The input token address
16
+ - tokenOut (str, required): The output token address
17
+ - amountIn (number, required): The amount of input tokens to swap
18
+ - recipientAddress (str, required): The address to receive the output tokens
19
+ `;
20
+ };
21
+ const postProcess = (response) => {
22
+ return `
23
+ Swap successful.
24
+ Transaction ID: ${response.transactionId}
25
+ `;
26
+ };
27
+ const swapV2 = async (client, context, params) => {
28
+ try {
29
+ const mirrorNode = getMirrornodeService(context.mirrornodeService, client.ledgerId);
30
+ const saucerSwapV2ConfigService = new SaucerSwapV2ConfigService(client.ledgerId);
31
+ const saucerSwapApiService = new SaucerSwapApiServiceImpl(client.ledgerId, saucerSwapV2ConfigService.getSaucerSwapApiKey());
32
+ const normalisedParams = await SaucerSwapV2ParameterNormaliser.normaliseSwapV2Params(params, context, saucerSwapV2ConfigService, saucerSwapApiService, mirrorNode, client);
33
+ const modifiedParams = { ...normalisedParams, gas: normalisedParams.gas };
34
+ const tx = HederaBuilder.executeTransaction(modifiedParams);
35
+ return handleTransaction(tx, client, context, postProcess);
36
+ }
37
+ catch (error) {
38
+ const desc = 'Failed to swap tokens';
39
+ let message;
40
+ if (error instanceof SaucerSwapError) {
41
+ message = `${desc}: ${error.message} (code: ${error.code})`;
42
+ }
43
+ else if (error instanceof Error) {
44
+ message = `${desc}: ${error.message}`;
45
+ }
46
+ else {
47
+ message = `${desc}: Unknown error occurred`;
48
+ }
49
+ console.error('[swap_v2_tool]', message, error);
50
+ return {
51
+ raw: {
52
+ status: Status.InvalidTransaction.toString(),
53
+ accountId: null,
54
+ tokenId: null,
55
+ transactionId: '',
56
+ topicId: null,
57
+ scheduleId: null,
58
+ error: message
59
+ },
60
+ humanMessage: message
61
+ };
62
+ }
63
+ };
64
+ export const SWAP_V2_TOOL = 'swap_v2_tool';
65
+ const tool = (context) => ({
66
+ method: SWAP_V2_TOOL,
67
+ name: 'Swap V2',
68
+ description: swapV2Prompt(context),
69
+ parameters: swapV2Parameters(),
70
+ execute: swapV2,
71
+ outputParser: (rawOutput) => {
72
+ const json = JSON.parse(rawOutput);
73
+ return {
74
+ raw: json,
75
+ humanMessage: json.transactionId,
76
+ };
77
+ },
78
+ });
79
+ export default tool;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ export function hexToUint8Array(hex) {
2
+ const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
3
+ if (cleanHex.length % 2 !== 0) {
4
+ throw new Error('Invalid hex string length');
5
+ }
6
+ const array = new Uint8Array(cleanHex.length / 2);
7
+ for (let i = 0; i < array.length; i++) {
8
+ array[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
9
+ }
10
+ return array;
11
+ }
12
+ export function buildEncodedPath(inputToken, hexFee, outputToken) {
13
+ const clean = (s) => s.toLowerCase().replace(/^0x/, '');
14
+ const inHex = clean(inputToken);
15
+ const feeHex = clean(hexFee).padStart(6, '0'); // ensure 3 bytes
16
+ const outHex = clean(outputToken);
17
+ if (inHex.length !== 40)
18
+ throw new Error(`tokenIn must be 20 bytes (40 hex), got ${inHex.length}`);
19
+ if (feeHex.length !== 6)
20
+ throw new Error(`fee must be 3 bytes (6 hex), got ${feeHex.length}`);
21
+ if (outHex.length !== 40)
22
+ throw new Error(`tokenOut must be 20 bytes (40 hex), got ${outHex.length}`);
23
+ const joined = inHex + feeHex + outHex; // total 86 hex chars (43 bytes)
24
+ return hexToUint8Array(joined);
25
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,98 @@
1
+ import { TokenId } from "@hashgraph/sdk";
2
+ import BigNumber from 'bignumber.js';
3
+ import { AccountResolver } from "hedera-agent-kit";
4
+ /**
5
+ * Handles response formatting for both autonomous and manual modes
6
+ */
7
+ export const handleResponse = (data, message) => {
8
+ return {
9
+ success: true,
10
+ data,
11
+ message,
12
+ };
13
+ };
14
+ /**
15
+ * Converts decimal amount to tiny units (wei)
16
+ */
17
+ export const toTiny = (amount) => {
18
+ const amountBigInt = typeof amount === "string" ? BigInt(amount) : BigInt(amount);
19
+ return amountBigInt.toString();
20
+ };
21
+ /**
22
+ * Validates token addresses
23
+ */
24
+ export const isValidAddress = (address) => {
25
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
26
+ };
27
+ /**
28
+ * Normalizes token addresses to lowercase
29
+ */
30
+ export const normalizeAddress = (address) => {
31
+ if (!isValidAddress(address)) {
32
+ throw new Error(`Invalid address: ${address}`);
33
+ }
34
+ return address.toLowerCase();
35
+ };
36
+ /**
37
+ * Sorts token addresses for consistent pair ordering
38
+ */
39
+ export const sortTokens = (tokenA, tokenB) => {
40
+ const normalizedA = normalizeAddress(tokenA);
41
+ const normalizedB = normalizeAddress(tokenB);
42
+ if (normalizedA === normalizedB) {
43
+ throw new Error("Cannot create pool with same token");
44
+ }
45
+ return normalizedA < normalizedB ? [normalizedA, normalizedB] : [normalizedB, normalizedA];
46
+ };
47
+ /**
48
+ * Calculates slippage bounds
49
+ */
50
+ export const calculateSlippageBounds = (amount, slippageBps) => {
51
+ const slippageMultiplier = BigInt(10000 - slippageBps);
52
+ const min = (amount * slippageMultiplier) / 10000n;
53
+ const maxMultiplier = BigInt(10000 + slippageBps);
54
+ const max = (amount * maxMultiplier) / 10000n;
55
+ return { min, max };
56
+ };
57
+ /**
58
+ * Formats amount with decimals
59
+ */
60
+ export const formatAmount = (amount, decimals) => {
61
+ const divisor = BigInt(10 ** decimals);
62
+ const whole = amount / divisor;
63
+ const fractional = amount % divisor;
64
+ if (fractional === 0n) {
65
+ return whole.toString();
66
+ }
67
+ const fractionalStr = fractional.toString().padStart(decimals, '0');
68
+ const trimmed = fractionalStr.replace(/0+$/, '');
69
+ if (trimmed === '') {
70
+ return whole.toString();
71
+ }
72
+ return `${whole}.${trimmed}`;
73
+ };
74
+ export const getHederaTokenEVMAddress = (address) => {
75
+ if (!AccountResolver.isHederaAddress(address)) {
76
+ return address;
77
+ }
78
+ const token = TokenId.fromString(address);
79
+ return '0x' + token.toEvmAddress();
80
+ };
81
+ export const getHederaTokenAddress = (address) => {
82
+ if (AccountResolver.isHederaAddress(address)) {
83
+ return address;
84
+ }
85
+ const token = TokenId.fromEvmAddress(0, 0, address);
86
+ return token.toString();
87
+ };
88
+ export function toBaseUnit(amount, decimals) {
89
+ const amountBN = new BigNumber(amount);
90
+ const multiplier = new BigNumber(10).pow(decimals);
91
+ return amountBN.multipliedBy(multiplier).integerValue(BigNumber.ROUND_FLOOR);
92
+ }
93
+ export const getTokenDecimals = (pool, hederaTokenAddress) => {
94
+ if (pool.tokenA.id === hederaTokenAddress) {
95
+ return pool.tokenA.decimals;
96
+ }
97
+ return pool.tokenB.decimals;
98
+ };