saucer-swap-plugin 0.1.0 → 0.2.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/README.md CHANGED
@@ -15,6 +15,16 @@ A plugin for the Hedera Agent Kit that enables SaucerSwap V2 DeFi operations on
15
15
  npm install saucer-swap-plugin
16
16
  ```
17
17
 
18
+ ## Prerequisites
19
+
20
+ Before using this plugin, you need to set up the following environment variables:
21
+
22
+ - `SAUCERSWAP_API_KEY` - API key for SaucerSwap REST API access
23
+ - `ACCOUNT_ID` - Your Hedera account ID
24
+ - `PRIVATE_KEY` - Your Hedera account private key (ECDSA format)
25
+
26
+ See [SETUP.md](./SETUP.md) for detailed setup instructions.
27
+
18
28
  ## Usage
19
29
 
20
30
  ### With Hedera Agent Kit
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LedgerId } from "@hashgraph/sdk";
1
+ import { LedgerId } from "@hiero-ledger/sdk";
2
2
  export const saucerSwapConfig = {
3
3
  networks: {
4
4
  [LedgerId.MAINNET.toString()]: {
package/dist/errors.js CHANGED
@@ -47,3 +47,13 @@ export class ConfigurationError extends SaucerSwapError {
47
47
  Object.setPrototypeOf(this, ConfigurationError.prototype);
48
48
  }
49
49
  }
50
+ export class TokenNotAssociatedError extends SaucerSwapError {
51
+ constructor(accountId, tokenId, signerAccountId) {
52
+ const signerHint = signerAccountId
53
+ ? ` Cannot auto-associate because the recipient differs from the signer (${signerAccountId}). Have the recipient associate the token first.`
54
+ : ' Have the recipient associate the token first.';
55
+ super(`Recipient ${accountId} does not have token ${tokenId} associated.${signerHint}`, 'TOKEN_NOT_ASSOCIATED');
56
+ this.name = 'TokenNotAssociatedError';
57
+ Object.setPrototypeOf(this, TokenNotAssociatedError.prototype);
58
+ }
59
+ }
@@ -1,4 +1,4 @@
1
- import { AccountResolver } from "hedera-agent-kit";
1
+ import { AccountResolver } from "@hashgraph/hedera-agent-kit";
2
2
  import { getSwapQuoteV2Parameters, swapV2Parameters } from './saucer-swap.zod';
3
3
  import { ethers } from "ethers";
4
4
  import z from 'zod';
@@ -1,4 +1,4 @@
1
- import { LedgerId } from "@hashgraph/sdk";
1
+ import { LedgerId } from "@hiero-ledger/sdk";
2
2
  const SAUCERSWAP_REST_BASE_URLS = {
3
3
  MAINNET: "https://api.saucerswap.finance",
4
4
  TESTNET: "https://test-api.saucerswap.finance",
@@ -1,6 +1,6 @@
1
1
  // write a config service to get the config from the config file
2
2
  import { saucerSwapConfig } from "../config";
3
- import { ContractId, TokenId } from "@hashgraph/sdk";
3
+ import { ContractId, TokenId } from "@hiero-ledger/sdk";
4
4
  export class SaucerSwapV2ConfigService {
5
5
  saucerSwapConfig;
6
6
  ledgerId;
@@ -1,4 +1,4 @@
1
- import { PromptGenerator, getMirrornodeService } from "hedera-agent-kit";
1
+ import { BaseTool, PromptGenerator, getMirrornodeService, untypedQueryOutputParser, } from "@hashgraph/hedera-agent-kit";
2
2
  import { getSwapQuoteV2Parameters } from "../saucer-swap.zod";
3
3
  import { SaucerSwapV2QueryServiceImpl } from "../service/saucer-swap-v2-query-service-impl";
4
4
  import SaucerSwapV2ParameterNormaliser from "../saucer-swap-v2-parameter-normaliser";
@@ -9,36 +9,54 @@ const getSwapQuoteV2Prompt = (context = {}) => {
9
9
  const contextSnippet = PromptGenerator.getContextSnippet(context);
10
10
  const usageInstructions = PromptGenerator.getParameterUsageInstructions();
11
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
- `;
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
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());
25
+ const postProcess = (quote, tokenAmountInBaseUnit, params) => `Swapping ${tokenAmountInBaseUnit} token: ${params.tokenIn} to token: ${params.tokenOut} will result in ${quote} token: ${params.tokenOut}, the rate used is ${quote / tokenAmountInBaseUnit}`;
26
+ export const GET_SWAP_QUOTE_V2_TOOL = "get_swap_quote_v2_tool";
27
+ export class GetSwapQuoteV2Tool extends BaseTool {
28
+ method = GET_SWAP_QUOTE_V2_TOOL;
29
+ name = "Get Quote (SaucerSwap V2)";
30
+ description;
31
+ parameters;
32
+ outputParser = untypedQueryOutputParser;
33
+ constructor(context) {
34
+ super();
35
+ this.description = getSwapQuoteV2Prompt(context);
36
+ this.parameters = getSwapQuoteV2Parameters();
37
+ }
38
+ async normalizeParams(params, context, client) {
39
+ const config = new SaucerSwapV2ConfigService(client.ledgerId);
40
+ const api = new SaucerSwapApiServiceImpl(client.ledgerId, config.getSaucerSwapApiKey());
41
+ return await SaucerSwapV2ParameterNormaliser.normaliseGetSwapQuoteV2Params(params, context, api);
42
+ }
43
+ async coreAction(normalisedParams, context, client) {
44
+ const mirrorNode = getMirrornodeService(context.mirrornodeService, client.ledgerId);
45
+ const config = new SaucerSwapV2ConfigService(client.ledgerId);
46
+ const queryService = new SaucerSwapV2QueryServiceImpl(client.ledgerId, mirrorNode, config);
47
+ const quote = await queryService.getSwapQuote(normalisedParams.tokenIn, normalisedParams.tokenOut, normalisedParams.amountIn, normalisedParams.poolFeesInHexFormat.toLowerCase());
36
48
  return {
37
49
  raw: { quote },
38
50
  humanMessage: postProcess(quote, normalisedParams.amountIn, normalisedParams),
39
51
  };
40
52
  }
41
- catch (error) {
53
+ async shouldSecondaryAction(_coreActionResult, _context) {
54
+ return false;
55
+ }
56
+ async secondaryAction(_request, _client, _context) {
57
+ return null;
58
+ }
59
+ async handleError(error, _context) {
42
60
  const desc = 'Failed to get quote';
43
61
  let message;
44
62
  if (error instanceof SaucerSwapError) {
@@ -53,20 +71,6 @@ const getSwapQuoteV2 = async (client, context, params) => {
53
71
  console.error('[get_quote_tool]', message, error);
54
72
  return { raw: { error: message }, humanMessage: message };
55
73
  }
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
- });
74
+ }
75
+ const tool = (context) => new GetSwapQuoteV2Tool(context);
72
76
  export default tool;
@@ -1,40 +1,108 @@
1
1
  import SaucerSwapV2ParameterNormaliser from "../saucer-swap-v2-parameter-normaliser";
2
2
  import { swapV2Parameters } from "../saucer-swap.zod";
3
- import { getMirrornodeService, handleTransaction, HederaBuilder, PromptGenerator } from "hedera-agent-kit";
4
- import { Status } from "@hashgraph/sdk";
3
+ import { AccountResolver, AgentMode, BaseTool, getMirrornodeService, handleTransaction, HederaBuilder, HederaParameterNormaliser, PromptGenerator, transactionToolOutputParser, } from "@hashgraph/hedera-agent-kit";
4
+ import { Status } from "@hiero-ledger/sdk";
5
5
  import { SaucerSwapV2ConfigService } from "../service/saucer-swap-v2-config-service";
6
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)}
7
+ import { SaucerSwapError, TokenNotAssociatedError } from "../errors";
8
+ import { getHederaTokenAddress, getHederaTokenEVMAddress, getTokenDecimals, toBaseUnit } from "../utils";
9
+ import { isTokenAssociated } from "../utils/token-association";
10
+ import { ensureTokenAllowance } from "../utils/token-allowance";
11
+ import { PoolFinderService } from "../service/pool-finder-service";
12
+ const swapV2Prompt = (context = {}) => `
13
+ ${PromptGenerator.getContextSnippet(context)}
11
14
 
12
- This tool will swap tokens using the SaucerSwap V2 protocol.
15
+ This tool will swap tokens using the SaucerSwap V2 protocol. If the recipient
16
+ has not associated the output token, the tool will associate it first (only
17
+ works when the recipient equals the signing account; otherwise the call fails
18
+ with a clear error and the recipient must associate the token themselves).
19
+ When tokenIn is not native HBAR / WHBAR, the tool also grants an
20
+ AccountAllowance to the SwapRouter contract for amountIn before swapping.
13
21
 
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
- `;
22
+ Parameters:
23
+ - tokenIn (str, required): The input token address
24
+ - tokenOut (str, required): The output token address
25
+ - amountIn (number, required): The amount of input tokens to swap
26
+ - recipientAddress (str, required): The address to receive the output tokens
27
+ `;
28
+ const postProcess = (response) => `Swap successful.\nTransaction ID: ${response.transactionId}`;
29
+ const resolveSignerAccountId = (context, client) => {
30
+ if (context.mode === AgentMode.RETURN_BYTES) {
31
+ return context.accountId;
32
+ }
33
+ return client.operatorAccountId?.toString();
20
34
  };
21
- const postProcess = (response) => {
22
- return `
23
- Swap successful.
24
- Transaction ID: ${response.transactionId}
25
- `;
35
+ const ensureTokenAssociated = async (recipientAccountId, tokenOutHederaId, context, client, mirrorNode) => {
36
+ if (await isTokenAssociated(recipientAccountId, tokenOutHederaId, mirrorNode))
37
+ return;
38
+ const signer = resolveSignerAccountId(context, client);
39
+ if (!signer || signer !== recipientAccountId) {
40
+ throw new TokenNotAssociatedError(recipientAccountId, tokenOutHederaId, signer);
41
+ }
42
+ const associateParams = HederaParameterNormaliser.normaliseAssociateTokenParams({ accountId: recipientAccountId, tokenIds: [tokenOutHederaId] }, context, client);
43
+ const associateTx = HederaBuilder.associateToken(associateParams);
44
+ await handleTransaction(associateTx, client, context, () => `Associated token ${tokenOutHederaId} with account ${recipientAccountId}`);
26
45
  };
27
- const swapV2 = async (client, context, params) => {
28
- try {
46
+ export const SWAP_V2_TOOL = 'swap_v2_tool';
47
+ export class SwapV2Tool extends BaseTool {
48
+ method = SWAP_V2_TOOL;
49
+ name = 'Swap V2';
50
+ description;
51
+ parameters;
52
+ outputParser = transactionToolOutputParser;
53
+ constructor(context) {
54
+ super();
55
+ this.description = swapV2Prompt(context);
56
+ this.parameters = swapV2Parameters();
57
+ }
58
+ async normalizeParams(params, context, client) {
59
+ const mirrorNode = getMirrornodeService(context.mirrornodeService, client.ledgerId);
60
+ const config = new SaucerSwapV2ConfigService(client.ledgerId);
61
+ const api = new SaucerSwapApiServiceImpl(client.ledgerId, config.getSaucerSwapApiKey());
62
+ const recipientAccountId = AccountResolver.resolveAccount(params.recipientAddress, context, client);
63
+ const tokenInHederaId = getHederaTokenAddress(params.tokenIn);
64
+ const tokenOutHederaId = getHederaTokenAddress(params.tokenOut);
65
+ const tokenInEvm = getHederaTokenEVMAddress(params.tokenIn);
66
+ const wrappedHBarEvm = config.getWrappedHBarEvmAddress();
67
+ const isInputWrappedHBAR = tokenInEvm.toLowerCase() === wrappedHBarEvm.toLowerCase();
68
+ let amountInBase;
69
+ let spenderAccountId;
70
+ if (!isInputWrappedHBAR) {
71
+ const pool = await PoolFinderService.findPoolForTokens(tokenInHederaId, tokenOutHederaId, api);
72
+ const decimals = getTokenDecimals(pool, tokenInHederaId);
73
+ amountInBase = toBaseUnit(params.amountIn, decimals).toNumber();
74
+ spenderAccountId = config.getSwapRouterContractId().toString();
75
+ }
76
+ const swapParams = await SaucerSwapV2ParameterNormaliser.normaliseSwapV2Params(params, context, config, api, mirrorNode, client);
77
+ return {
78
+ swapParams,
79
+ prep: {
80
+ recipientAccountId,
81
+ tokenInHederaId,
82
+ tokenOutHederaId,
83
+ amountInBase,
84
+ spenderAccountId,
85
+ isInputWrappedHBAR,
86
+ },
87
+ };
88
+ }
89
+ async coreAction(normalisedParams, context, client) {
29
90
  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);
91
+ const { swapParams, prep } = normalisedParams;
92
+ await ensureTokenAssociated(prep.recipientAccountId, prep.tokenOutHederaId, context, client, mirrorNode);
93
+ if (!prep.isInputWrappedHBAR) {
94
+ const ownerAccountId = resolveSignerAccountId(context, client);
95
+ if (!ownerAccountId) {
96
+ throw new SaucerSwapError('Cannot resolve owner account for token allowance', 'OWNER_UNRESOLVED');
97
+ }
98
+ await ensureTokenAllowance(ownerAccountId, prep.spenderAccountId, prep.tokenInHederaId, prep.amountInBase, context, client, mirrorNode);
99
+ }
100
+ return HederaBuilder.executeTransaction(swapParams);
101
+ }
102
+ async secondaryAction(transaction, client, context) {
103
+ return await handleTransaction(transaction, client, context, postProcess);
36
104
  }
37
- catch (error) {
105
+ async handleError(error, _context) {
38
106
  const desc = 'Failed to swap tokens';
39
107
  let message;
40
108
  if (error instanceof SaucerSwapError) {
@@ -55,25 +123,11 @@ const swapV2 = async (client, context, params) => {
55
123
  transactionId: '',
56
124
  topicId: null,
57
125
  scheduleId: null,
58
- error: message
126
+ error: message,
59
127
  },
60
- humanMessage: message
128
+ humanMessage: message,
61
129
  };
62
130
  }
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
- });
131
+ }
132
+ const tool = (context) => new SwapV2Tool(context);
79
133
  export default tool;
@@ -0,0 +1,23 @@
1
+ import { HederaBuilder, HederaParameterNormaliser, handleTransaction, } from "@hashgraph/hedera-agent-kit";
2
+ export async function hasSufficientAllowance(ownerAccountId, spenderAccountId, tokenId, amount, mirrorNode) {
3
+ try {
4
+ const { allowances } = await mirrorNode.getTokenAllowances(ownerAccountId, spenderAccountId);
5
+ const existing = allowances.find(a => a.token_id === tokenId);
6
+ return !!existing && existing.amount >= amount;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export async function ensureTokenAllowance(ownerAccountId, spenderAccountId, tokenId, amount, context, client, mirrorNode) {
13
+ if (await hasSufficientAllowance(ownerAccountId, spenderAccountId, tokenId, amount, mirrorNode)) {
14
+ return;
15
+ }
16
+ const approveParams = await HederaParameterNormaliser.normaliseApproveTokenAllowance({
17
+ ownerAccountId,
18
+ spenderAccountId,
19
+ tokenApprovals: [{ tokenId, amount }],
20
+ }, context, client, mirrorNode);
21
+ const approveTx = HederaBuilder.approveTokenAllowance(approveParams);
22
+ await handleTransaction(approveTx, client, context, () => `Approved ${amount} of token ${tokenId} for spender ${spenderAccountId}`);
23
+ }
@@ -0,0 +1,4 @@
1
+ export async function isTokenAssociated(accountId, tokenId, mirrorNode) {
2
+ const balances = await mirrorNode.getAccountTokenBalances(accountId, tokenId);
3
+ return balances.tokens.some(t => t.token_id === tokenId);
4
+ }
package/dist/utils.js CHANGED
@@ -1,6 +1,6 @@
1
- import { TokenId } from "@hashgraph/sdk";
1
+ import { TokenId } from "@hiero-ledger/sdk";
2
2
  import BigNumber from 'bignumber.js';
3
- import { AccountResolver } from "hedera-agent-kit";
3
+ import { AccountResolver } from "@hashgraph/hedera-agent-kit";
4
4
  /**
5
5
  * Handles response formatting for both autonomous and manual modes
6
6
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saucer-swap-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "SaucerSwap plugin for Hedera Agent Kit",
6
6
  "main": "dist/index.js",
@@ -25,10 +25,13 @@
25
25
  "author": "",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "hedera-agent-kit": "^3.5.2",
29
28
  "tsup": "^8.5.0",
30
29
  "zod": "^3.0.0"
31
30
  },
31
+ "peerDependencies": {
32
+ "@hashgraph/hedera-agent-kit": "^4.0.0",
33
+ "@hiero-ledger/sdk": "^2.82.0"
34
+ },
32
35
  "devDependencies": {
33
36
  "@types/node": "^20.0.0",
34
37
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -41,5 +44,9 @@
41
44
  "files": [
42
45
  "dist/**/*",
43
46
  "README.md"
44
- ]
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/saucerswaplabs/hedera-agent-kit-saucer-swap-plugin"
51
+ }
45
52
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,19 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};