@coinbase/agentkit 0.0.0-nightly-20251116210417 → 0.0.0-nightly-20251117210447

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
@@ -505,6 +505,23 @@ const agent = createReactAgent({
505
505
  </table>
506
506
  </details>
507
507
  <details>
508
+ <summary><strong>Sushi</strong></summary>
509
+ <table width="100%">
510
+ <tr>
511
+ <td width="200"><code>find-token</code></td>
512
+ <td width="768">Searches the Sushi Data API for up to 10 matching tokens by symbol or address.</td>
513
+ </tr>
514
+ <tr>
515
+ <td width="200"><code>quote</code></td>
516
+ <td width="768">Fetches an off-chain swap quote between ERC20 or native assets using the Sushi Swap API.</td>
517
+ </tr>
518
+ <tr>
519
+ <td width="200"><code>swap</code></td>
520
+ <td width="768">Executes a Sushi-routed swap after validating balances and approvals, returning the transaction hash.</td>
521
+ </tr>
522
+ </table>
523
+ </details>
524
+ <details>
508
525
  <summary><strong>Twitter</strong></summary>
509
526
  <table width="100%">
510
527
  <tr>
@@ -22,6 +22,7 @@ export * from "./morpho";
22
22
  export * from "./opensea";
23
23
  export * from "./spl";
24
24
  export * from "./superfluid";
25
+ export * from "./sushi";
25
26
  export * from "./truemarkets";
26
27
  export * from "./twitter";
27
28
  export * from "./wallet";
@@ -38,6 +38,7 @@ __exportStar(require("./morpho"), exports);
38
38
  __exportStar(require("./opensea"), exports);
39
39
  __exportStar(require("./spl"), exports);
40
40
  __exportStar(require("./superfluid"), exports);
41
+ __exportStar(require("./sushi"), exports);
41
42
  __exportStar(require("./truemarkets"), exports);
42
43
  __exportStar(require("./twitter"), exports);
43
44
  __exportStar(require("./wallet"), exports);
@@ -0,0 +1,35 @@
1
+ export declare const routeProcessor9Abi_Route: readonly [{
2
+ readonly name: "Route";
3
+ readonly type: "event";
4
+ readonly inputs: readonly [{
5
+ readonly type: "address";
6
+ readonly name: "from";
7
+ readonly indexed: true;
8
+ }, {
9
+ readonly type: "address";
10
+ readonly name: "to";
11
+ }, {
12
+ readonly type: "address";
13
+ readonly name: "tokenIn";
14
+ readonly indexed: true;
15
+ }, {
16
+ readonly type: "address";
17
+ readonly name: "tokenOut";
18
+ }, {
19
+ readonly type: "uint256";
20
+ readonly name: "amountIn";
21
+ }, {
22
+ readonly type: "uint256";
23
+ readonly name: "amountOut";
24
+ }, {
25
+ readonly type: "int256";
26
+ readonly name: "slippage";
27
+ }, {
28
+ readonly type: "uint32";
29
+ readonly name: "referralCode";
30
+ readonly indexed: true;
31
+ }, {
32
+ readonly type: "bytes32";
33
+ readonly name: "diagnosticsFirst32";
34
+ }];
35
+ }];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routeProcessor9Abi_Route = void 0;
4
+ const viem_1 = require("viem");
5
+ exports.routeProcessor9Abi_Route = (0, viem_1.parseAbi)([
6
+ "event Route(address indexed from, address to, address indexed tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, int256 slippage, uint32 indexed referralCode, bytes32 diagnosticsFirst32)",
7
+ ]);
@@ -0,0 +1,4 @@
1
+ export * from "./sushiDataSchemas";
2
+ export * from "./sushiDataActionProvider";
3
+ export * from "./sushiRouterSchemas";
4
+ export * from "./sushiRouterActionProvider";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./sushiDataSchemas"), exports);
18
+ __exportStar(require("./sushiDataActionProvider"), exports);
19
+ __exportStar(require("./sushiRouterSchemas"), exports);
20
+ __exportStar(require("./sushiRouterActionProvider"), exports);
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { EvmWalletProvider } from "../../wallet-providers";
3
+ import { ActionProvider } from "../actionProvider";
4
+ import { Network } from "../../network";
5
+ import { FindTokenSchema } from "./sushiDataSchemas";
6
+ /**
7
+ * SushiDataActionProvider is an action provider for Sushi.
8
+ *
9
+ * This provider is used for any action that uses the Sushi Data API.
10
+ */
11
+ export declare class SushiDataActionProvider extends ActionProvider<EvmWalletProvider> {
12
+ /**
13
+ * Constructor for the SushiDataActionProvider class.
14
+ */
15
+ constructor();
16
+ /**
17
+ * Swaps a specified amount of a from token to a to token for the wallet.
18
+ *
19
+ * @param walletProvider - The wallet provider to swap the tokens from.
20
+ * @param args - The input arguments for the action.
21
+ * @returns A message containing the swap details.
22
+ */
23
+ findToken(walletProvider: EvmWalletProvider, args: z.infer<typeof FindTokenSchema>): Promise<string>;
24
+ /**
25
+ * Custom action providers are supported on all networks
26
+ *
27
+ * @param network - The network to checkpointSaver
28
+ * @returns True if the network is supported, false otherwise
29
+ */
30
+ supportsNetwork(network: Network): boolean;
31
+ }
32
+ export declare const sushiDataActionProvider: () => SushiDataActionProvider;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.sushiDataActionProvider = exports.SushiDataActionProvider = void 0;
13
+ const zod_1 = require("zod");
14
+ const wallet_providers_1 = require("../../wallet-providers");
15
+ const actionDecorator_1 = require("../actionDecorator");
16
+ const actionProvider_1 = require("../actionProvider");
17
+ const sushiDataSchemas_1 = require("./sushiDataSchemas");
18
+ const evm_1 = require("sushi/evm");
19
+ const viem_1 = require("viem");
20
+ /**
21
+ * SushiDataActionProvider is an action provider for Sushi.
22
+ *
23
+ * This provider is used for any action that uses the Sushi Data API.
24
+ */
25
+ class SushiDataActionProvider extends actionProvider_1.ActionProvider {
26
+ /**
27
+ * Constructor for the SushiDataActionProvider class.
28
+ */
29
+ constructor() {
30
+ super("sushi-data", []);
31
+ }
32
+ /**
33
+ * Swaps a specified amount of a from token to a to token for the wallet.
34
+ *
35
+ * @param walletProvider - The wallet provider to swap the tokens from.
36
+ * @param args - The input arguments for the action.
37
+ * @returns A message containing the swap details.
38
+ */
39
+ async findToken(walletProvider, args) {
40
+ try {
41
+ const chainId = Number((await walletProvider.getNetwork()).chainId);
42
+ if (!(0, evm_1.isEvmChainId)(chainId)) {
43
+ return `Unsupported chainId: ${chainId}`;
44
+ }
45
+ const request = await fetch(`${evm_1.SUSHI_DATA_API_HOST}/graphql`, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ Accept: "application/json",
50
+ },
51
+ body: `{"query":"query { tokenList(chainId: ${chainId}, first: 10, search: \\"${args.search}\\") { address symbol name decimals } }"}`,
52
+ });
53
+ const response = await request.json();
54
+ const schema = zod_1.z.object({
55
+ data: zod_1.z.object({
56
+ tokenList: zod_1.z.array(zod_1.z.object({
57
+ address: zod_1.z.string().refine(val => (0, viem_1.isAddress)(val, { strict: false })),
58
+ symbol: zod_1.z.string(),
59
+ name: zod_1.z.string(),
60
+ decimals: zod_1.z.number(),
61
+ })),
62
+ }),
63
+ });
64
+ const result = schema.safeParse(response);
65
+ if (!result.success) {
66
+ return `Error parsing response: ${result.error.message}`;
67
+ }
68
+ const tokens = result.data.data.tokenList;
69
+ const chain = (0, evm_1.getEvmChainById)(chainId);
70
+ let message = `Found ${tokens.length} tokens on ${chain.shortName}:`;
71
+ tokens.forEach(token => {
72
+ message += `\n- ${token.symbol} (${token.name}) - ${token.address}`;
73
+ });
74
+ return message;
75
+ }
76
+ catch (error) {
77
+ return `Error finding tokens: ${error}`;
78
+ }
79
+ }
80
+ /**
81
+ * Custom action providers are supported on all networks
82
+ *
83
+ * @param network - The network to checkpointSaver
84
+ * @returns True if the network is supported, false otherwise
85
+ */
86
+ supportsNetwork(network) {
87
+ if (network.protocolFamily !== "evm" || !network.chainId) {
88
+ return false;
89
+ }
90
+ return (0, evm_1.isEvmChainId)(Number(network.chainId));
91
+ }
92
+ }
93
+ exports.SushiDataActionProvider = SushiDataActionProvider;
94
+ __decorate([
95
+ (0, actionDecorator_1.CreateAction)({
96
+ name: "find-token",
97
+ description: `This tool finds tokens (erc20) using the Sushi Data API
98
+ It takes the following inputs:
99
+ - Search: Either the token symbol (either full or partial) or token address to search for
100
+
101
+ Important notes:
102
+ - Only returns the first 10 tokens found
103
+ - Always use the full token symbol for better results
104
+ - Always use this tool to verify the token address if you are not sure about the address
105
+ `,
106
+ schema: sushiDataSchemas_1.FindTokenSchema,
107
+ }),
108
+ __metadata("design:type", Function),
109
+ __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
110
+ __metadata("design:returntype", Promise)
111
+ ], SushiDataActionProvider.prototype, "findToken", null);
112
+ const sushiDataActionProvider = () => new SushiDataActionProvider();
113
+ exports.sushiDataActionProvider = sushiDataActionProvider;
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Input schema for find token action
4
+ */
5
+ export declare const FindTokenSchema: z.ZodObject<{
6
+ search: z.ZodString;
7
+ }, "strip", z.ZodTypeAny, {
8
+ search: string;
9
+ }, {
10
+ search: string;
11
+ }>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FindTokenSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * Input schema for find token action
7
+ */
8
+ exports.FindTokenSchema = zod_1.z
9
+ .object({
10
+ search: zod_1.z
11
+ .string()
12
+ .min(2)
13
+ .describe("Either the (partial or complete) symbol OR the address of the token to find"),
14
+ })
15
+ .strip()
16
+ .describe("Instructions for finding a token");
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { EvmWalletProvider } from "../../wallet-providers";
3
+ import { ActionProvider } from "../actionProvider";
4
+ import { SushiQuoteSchema, SushiSwapSchema } from "./sushiRouterSchemas";
5
+ import { Network } from "../../network";
6
+ /**
7
+ * SushiRouterActionProvider is an action provider for Sushi.
8
+ *
9
+ * This provider is used for any action that uses the Sushi Router API.
10
+ */
11
+ export declare class SushiRouterActionProvider extends ActionProvider<EvmWalletProvider> {
12
+ /**
13
+ * Constructor for the SushiRouterActionProvider class.
14
+ */
15
+ constructor();
16
+ /**
17
+ * Swaps a specified amount of a from token to a to token for the wallet.
18
+ *
19
+ * @param walletProvider - The wallet provider to swap the tokens from.
20
+ * @param args - The input arguments for the action.
21
+ * @returns A message containing the swap details.
22
+ */
23
+ swap(walletProvider: EvmWalletProvider, args: z.infer<typeof SushiSwapSchema>): Promise<string>;
24
+ /**
25
+ * Gets a quote for a specified amount of a from token to a to token
26
+ *
27
+ * @param walletProvider - The wallet provider to swap the tokens from.
28
+ * @param args - The input arguments for the action.
29
+ * @returns A message containing the quote details.
30
+ */
31
+ quote(walletProvider: EvmWalletProvider, args: z.infer<typeof SushiQuoteSchema>): Promise<string>;
32
+ /**
33
+ * Custom action providers are supported on all networks
34
+ *
35
+ * @param network - The network to checkpointSaver
36
+ * @returns True if the network is supported, false otherwise
37
+ */
38
+ supportsNetwork(network: Network): boolean;
39
+ }
40
+ export declare const sushiRouterActionProvider: () => SushiRouterActionProvider;
@@ -0,0 +1,386 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.sushiRouterActionProvider = exports.SushiRouterActionProvider = void 0;
13
+ const zod_1 = require("zod");
14
+ const wallet_providers_1 = require("../../wallet-providers");
15
+ const actionDecorator_1 = require("../actionDecorator");
16
+ const actionProvider_1 = require("../actionProvider");
17
+ const sushiRouterSchemas_1 = require("./sushiRouterSchemas");
18
+ const evm_1 = require("sushi/evm");
19
+ const viem_1 = require("viem");
20
+ const constants_1 = require("./constants");
21
+ /**
22
+ * SushiRouterActionProvider is an action provider for Sushi.
23
+ *
24
+ * This provider is used for any action that uses the Sushi Router API.
25
+ */
26
+ class SushiRouterActionProvider extends actionProvider_1.ActionProvider {
27
+ /**
28
+ * Constructor for the SushiRouterActionProvider class.
29
+ */
30
+ constructor() {
31
+ super("sushi-router", []);
32
+ }
33
+ /**
34
+ * Swaps a specified amount of a from token to a to token for the wallet.
35
+ *
36
+ * @param walletProvider - The wallet provider to swap the tokens from.
37
+ * @param args - The input arguments for the action.
38
+ * @returns A message containing the swap details.
39
+ */
40
+ async swap(walletProvider, args) {
41
+ try {
42
+ const chainId = Number((await walletProvider.getNetwork()).chainId);
43
+ // Compatible chainId is expected since it should be pre-checked in supportsNetwork
44
+ if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
45
+ return `Unsupported chainId: ${chainId}`;
46
+ }
47
+ const chain = (0, evm_1.getEvmChainById)(chainId);
48
+ const decimalsIn = await fetchDecimals({ walletProvider, token: args.fromAssetAddress });
49
+ if (!decimalsIn.success) {
50
+ return decimalsIn.message;
51
+ }
52
+ const amountIn = (0, viem_1.parseUnits)(args.amount, decimalsIn.decimals);
53
+ // First fetch to see if the swap is even possible
54
+ const firstSwap = await handleGetSwap({
55
+ amount: amountIn,
56
+ chainId,
57
+ tokenIn: args.fromAssetAddress,
58
+ tokenOut: args.toAssetAddress,
59
+ maxSlippage: args.maxSlippage,
60
+ recipient: walletProvider.getAddress(),
61
+ });
62
+ if (firstSwap.swap.status !== evm_1.RouteStatus.Success) {
63
+ return firstSwap.message;
64
+ }
65
+ // Check if the wallet has enough balance to perform the swap
66
+ const balance = await handleBalance({
67
+ walletProvider,
68
+ token: firstSwap.swap.tokenFrom,
69
+ minAmount: amountIn,
70
+ });
71
+ if (!balance.success) {
72
+ return balance.message;
73
+ }
74
+ const approval = await handleApproval({
75
+ walletProvider,
76
+ token: args.fromAssetAddress,
77
+ to: firstSwap.swap.tx.to,
78
+ amount: amountIn,
79
+ });
80
+ if (!approval.success) {
81
+ return approval.message;
82
+ }
83
+ // Refetch in case the route changed during approval
84
+ const secondSwap = await handleGetSwap({
85
+ amount: amountIn,
86
+ chainId,
87
+ tokenIn: args.fromAssetAddress,
88
+ tokenOut: args.toAssetAddress,
89
+ maxSlippage: args.maxSlippage,
90
+ recipient: walletProvider.getAddress(),
91
+ });
92
+ if (secondSwap.swap.status !== evm_1.RouteStatus.Success) {
93
+ return secondSwap.message;
94
+ }
95
+ const swapHash = await walletProvider.sendTransaction({
96
+ from: secondSwap.swap.tx.from,
97
+ to: secondSwap.swap.tx.to,
98
+ data: secondSwap.swap.tx.data,
99
+ value: BigInt(secondSwap.swap.tx.value || 0),
100
+ });
101
+ const swapReceipt = await walletProvider.waitForTransactionReceipt(swapHash);
102
+ if (swapReceipt.status === "reverted" || swapReceipt.status === "failed") {
103
+ return `Swap failed: Transaction Reverted.\n - Transaction hash: ${swapHash}\n - Transaction link: ${chain.getTransactionUrl(swapHash)}`;
104
+ }
105
+ // Find the Route event log, which includes the actual amountOut
106
+ const [routeLog] = swapReceipt.logs
107
+ .filter(log => (0, viem_1.encodeEventTopics)({
108
+ abi: constants_1.routeProcessor9Abi_Route,
109
+ eventName: "Route",
110
+ })[0] === log.topics[0])
111
+ .map(log => (0, viem_1.decodeEventLog)({
112
+ abi: constants_1.routeProcessor9Abi_Route,
113
+ data: log.data,
114
+ topics: log.topics,
115
+ }));
116
+ return `Swapped ${(0, viem_1.formatUnits)(routeLog.args.amountIn, secondSwap.swap.tokenFrom.decimals)} of ${secondSwap.swap.tokenFrom.symbol} (${args.fromAssetAddress}) for ${(0, viem_1.formatUnits)(routeLog.args.amountOut, secondSwap.swap.tokenTo.decimals)} of ${secondSwap.swap.tokenTo.symbol} (${args.toAssetAddress}) on ${chain.shortName}
117
+ - Transaction hash: ${swapHash}
118
+ - Transaction link: ${chain.getTransactionUrl(swapHash)}`;
119
+ }
120
+ catch (error) {
121
+ return `Error swapping tokens: ${error}`;
122
+ }
123
+ }
124
+ /**
125
+ * Gets a quote for a specified amount of a from token to a to token
126
+ *
127
+ * @param walletProvider - The wallet provider to swap the tokens from.
128
+ * @param args - The input arguments for the action.
129
+ * @returns A message containing the quote details.
130
+ */
131
+ async quote(walletProvider, args) {
132
+ try {
133
+ const chainId = Number((await walletProvider.getNetwork()).chainId);
134
+ // Compatible chainId is expected since it should be pre-checked in supportsNetwork
135
+ if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
136
+ return `Unsupported chainId: ${chainId}`;
137
+ }
138
+ const decimalsIn = await fetchDecimals({ walletProvider, token: args.fromAssetAddress });
139
+ if (!decimalsIn.success) {
140
+ return decimalsIn.message;
141
+ }
142
+ const amountIn = (0, viem_1.parseUnits)(args.amount, decimalsIn.decimals);
143
+ const swap = await handleGetSwap({
144
+ amount: amountIn,
145
+ chainId,
146
+ tokenIn: args.fromAssetAddress,
147
+ tokenOut: args.toAssetAddress,
148
+ maxSlippage: 0.0005, // 0.05%
149
+ recipient: walletProvider.getAddress(),
150
+ });
151
+ return swap.message;
152
+ }
153
+ catch (error) {
154
+ return `Error quoting for tokens: ${error}`;
155
+ }
156
+ }
157
+ /**
158
+ * Custom action providers are supported on all networks
159
+ *
160
+ * @param network - The network to checkpointSaver
161
+ * @returns True if the network is supported, false otherwise
162
+ */
163
+ supportsNetwork(network) {
164
+ if (network.protocolFamily !== "evm" || !network.chainId) {
165
+ return false;
166
+ }
167
+ return (0, evm_1.isSwapApiSupportedChainId)(Number(network.chainId));
168
+ }
169
+ }
170
+ exports.SushiRouterActionProvider = SushiRouterActionProvider;
171
+ __decorate([
172
+ (0, actionDecorator_1.CreateAction)({
173
+ name: "swap",
174
+ description: `This tool will swap a specified amount of a 'from token' (erc20) to a 'to token' (erc20) for the wallet.
175
+ It takes the following inputs:
176
+ - The human-readable amount of the 'from token' to swap
177
+ - The from token address to trade
178
+ - The token address to receive from the swap
179
+ - The maximum slippage allowed for the swap, where 0 is 0% and 1 is 100%, the default is 0.005 (0.05%)
180
+
181
+ Important notes:
182
+ - The native asset (ie 'eth' on 'ethereum-mainnet') is represented by ${evm_1.nativeAddress} (not the wrapped native asset!)
183
+ - Fetch a quote first before the swap action. Stop, ask the user if they want to proceed. If the user answers affirmatively, then swap
184
+ - If you are not absolutely sure about token addresses, either use an action to fetch the token address or ask the user
185
+ `,
186
+ schema: sushiRouterSchemas_1.SushiSwapSchema,
187
+ }),
188
+ __metadata("design:type", Function),
189
+ __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
190
+ __metadata("design:returntype", Promise)
191
+ ], SushiRouterActionProvider.prototype, "swap", null);
192
+ __decorate([
193
+ (0, actionDecorator_1.CreateAction)({
194
+ name: "quote",
195
+ description: `This tool will fetch a quote for a specified amount of a 'from token' (erc20 or native ETH) to a 'to token' (erc20 or native ETH).
196
+ It takes the following inputs:
197
+ - The human-readable amount of the 'from token' to fetch a quote for
198
+ - The from token address to fetch a quote for
199
+ - The token address to receive from the quoted swap
200
+
201
+ Important notes:
202
+ - The native asset (ie 'eth' on 'ethereum-mainnet') is represented by ${evm_1.nativeAddress} (not the wrapped native asset!)
203
+ - This action does not require any on-chain transactions or gas
204
+ - If you are not 100% certain about token addresses, use an action to fetch the token address first or ask the user
205
+ - NEVER assume that tokens have the same address on across networks (ie the address of 'usdc' on 'ethereum-mainnet' is different from 'usdc' on 'base-mainnet')
206
+ `,
207
+ schema: sushiRouterSchemas_1.SushiQuoteSchema,
208
+ }),
209
+ __metadata("design:type", Function),
210
+ __metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
211
+ __metadata("design:returntype", Promise)
212
+ ], SushiRouterActionProvider.prototype, "quote", null);
213
+ /**
214
+ * Fetches the number of decimals for the token
215
+ *
216
+ * @param root0 - The input arguments for the action
217
+ * @param root0.walletProvider - The wallet provider to fetch the decimals from
218
+ * @param root0.token - The token address to fetch the decimals for
219
+ *
220
+ * @returns The number of decimals for the token
221
+ */
222
+ async function fetchDecimals({ walletProvider, token, }) {
223
+ const chainId = Number((await walletProvider.getNetwork()).chainId);
224
+ if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
225
+ return {
226
+ success: false,
227
+ message: `Unsupported chainId: ${chainId}`,
228
+ };
229
+ }
230
+ if (token === evm_1.nativeAddress) {
231
+ return { success: true, decimals: evm_1.EvmNative.fromChainId(chainId).decimals };
232
+ }
233
+ const decimals = (await walletProvider.readContract({
234
+ address: token,
235
+ abi: viem_1.erc20Abi,
236
+ functionName: "decimals",
237
+ }));
238
+ return { success: true, decimals };
239
+ }
240
+ /**
241
+ * Checks if the wallet has enough balance to perform the swap
242
+ *
243
+ * @param root0 - The input arguments for the action
244
+ * @param root0.walletProvider - The wallet provider to fetch the balance from
245
+ * @param root0.token - The token address to fetch the balance for
246
+ * @param root0.token.address - The token address to fetch the balance for
247
+ * @param root0.token.symbol - The token symbol to fetch the balance for
248
+ * @param root0.token.decimals - The token decimals to fetch the balance for
249
+ * @param root0.minAmount - The minimum amount to check for
250
+ *
251
+ * @returns The balance of the wallet
252
+ */
253
+ async function handleBalance({ walletProvider, token, minAmount, }) {
254
+ let balance;
255
+ if (token.address.toLowerCase() === evm_1.nativeAddress) {
256
+ balance = await walletProvider.getBalance();
257
+ }
258
+ else {
259
+ balance = (await walletProvider.readContract({
260
+ address: token.address,
261
+ abi: viem_1.erc20Abi,
262
+ functionName: "balanceOf",
263
+ args: [walletProvider.getAddress()],
264
+ }));
265
+ }
266
+ if (balance < minAmount) {
267
+ return {
268
+ success: false,
269
+ message: `Swap failed: Insufficient balance for ${token.symbol} (${token.address})
270
+ - Balance: ${(0, viem_1.formatUnits)(balance, token.decimals)}
271
+ - Required: ${(0, viem_1.formatUnits)(minAmount, token.decimals)}`,
272
+ };
273
+ }
274
+ return {
275
+ success: true,
276
+ };
277
+ }
278
+ /**
279
+ *
280
+ * Wraps the getSwap function, providing messages for possible states
281
+ *
282
+ * @param root0 - The input arguments for the action
283
+ * @param root0.amount - The amount to swap
284
+ * @param root0.chainId - The chainId to swap on
285
+ * @param root0.tokenIn - The input token address
286
+ * @param root0.tokenOut - The output token address
287
+ * @param root0.maxSlippage - The maximum slippage allowed
288
+ * @param root0.recipient - The recipient of the swap
289
+ *
290
+ * @returns The result of the swap and a message
291
+ */
292
+ async function handleGetSwap({ amount, chainId, tokenIn, tokenOut, maxSlippage, recipient, }) {
293
+ if (!(0, evm_1.isRedSnwapperChainId)(chainId)) {
294
+ return {
295
+ swap: { status: evm_1.RouteStatus.NoWay },
296
+ message: `Unsupported chainId: ${chainId}`,
297
+ };
298
+ }
299
+ const swap = await (0, evm_1.getSwap)({
300
+ amount,
301
+ chainId,
302
+ tokenIn,
303
+ tokenOut,
304
+ maxSlippage,
305
+ sender: recipient,
306
+ recipient,
307
+ });
308
+ const chain = (0, evm_1.getEvmChainById)(chainId);
309
+ if (swap.status === evm_1.RouteStatus.NoWay) {
310
+ return {
311
+ swap,
312
+ message: `No route found to swap ${amount} of ${tokenIn} for ${tokenOut} on ${chain.shortName}`,
313
+ };
314
+ }
315
+ if (swap.status === evm_1.RouteStatus.Partial) {
316
+ return {
317
+ swap,
318
+ message: `Found a partial quote for ${swap.tokenFrom.symbol} -> ${swap.tokenTo.symbol}. Swapping the full amount is not possible.
319
+ - AmountIn: ${(0, viem_1.formatUnits)(BigInt(swap.amountIn), swap.tokenFrom.decimals)}
320
+ - AmountOut: ${(0, viem_1.formatUnits)(BigInt(swap.assumedAmountOut), swap.tokenTo.decimals)}`,
321
+ };
322
+ }
323
+ return {
324
+ swap,
325
+ message: `Found a quote for ${swap.tokenFrom.symbol} (${swap.tokenFrom.address}) -> ${swap.tokenTo.symbol} (${swap.tokenTo.address})
326
+ - AmountIn: ${(0, viem_1.formatUnits)(BigInt(swap.amountIn), swap.tokenFrom.decimals)} ${swap.tokenFrom.symbol}
327
+ - AmountOut: ${(0, viem_1.formatUnits)(BigInt(swap.assumedAmountOut), swap.tokenTo.decimals)} ${swap.tokenTo.symbol}`,
328
+ };
329
+ }
330
+ /**
331
+ *
332
+ * Handles the approval for the token
333
+ *
334
+ * @param root0 - The input arguments for the action
335
+ * @param root0.walletProvider - The wallet provider to handle the approval with
336
+ * @param root0.token - The token address to approve
337
+ * @param root0.to - The address to approve the token to
338
+ * @param root0.amount - The amount to approve
339
+ *
340
+ * @returns Either success: true on success or success: false with a message containing the reason for failure
341
+ */
342
+ async function handleApproval({ walletProvider, token, to, amount, }) {
343
+ // No need to approve if the token is the native token
344
+ if (token.toLowerCase() === evm_1.nativeAddress) {
345
+ return { success: true };
346
+ }
347
+ // Check if the wallet already has enough allowance
348
+ const allowance = (await walletProvider.readContract({
349
+ address: token,
350
+ abi: viem_1.erc20Abi,
351
+ functionName: "allowance",
352
+ args: [walletProvider.getAddress(), to],
353
+ }));
354
+ if (allowance >= amount) {
355
+ return { success: true };
356
+ }
357
+ // Exact approval
358
+ const approvalHash = await walletProvider.sendTransaction({
359
+ to: token,
360
+ data: (0, viem_1.encodeFunctionData)({
361
+ abi: viem_1.erc20Abi,
362
+ functionName: "approve",
363
+ args: [to, BigInt(amount)],
364
+ }),
365
+ });
366
+ const approvalReceipt = await walletProvider.waitForTransactionReceipt(approvalHash);
367
+ const chainId = Number((await walletProvider.getNetwork()).chainId);
368
+ if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
369
+ return {
370
+ success: false,
371
+ message: `Unsupported chainId: ${chainId}`,
372
+ };
373
+ }
374
+ const chain = (0, evm_1.getEvmChainById)(chainId);
375
+ if (approvalReceipt.status === "reverted") {
376
+ return {
377
+ success: false,
378
+ message: `Swap failed: Approval Reverted.
379
+ - Transaction hash: ${approvalHash}
380
+ - Transaction link: ${chain.getTransactionUrl(approvalHash)}`,
381
+ };
382
+ }
383
+ return { success: true };
384
+ }
385
+ const sushiRouterActionProvider = () => new SushiRouterActionProvider();
386
+ exports.sushiRouterActionProvider = sushiRouterActionProvider;
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const sushiRouterSchemas_1 = require("./sushiRouterSchemas");
4
+ const evm_1 = require("sushi/evm");
5
+ const sushiRouterActionProvider_1 = require("./sushiRouterActionProvider");
6
+ const viem_1 = require("viem");
7
+ const constants_1 = require("./constants");
8
+ // Mock the entire module
9
+ jest.mock("sushi/evm", () => {
10
+ const originalModule = jest.requireActual("sushi/evm");
11
+ return {
12
+ __esModule: true,
13
+ ...originalModule,
14
+ getSwap: jest.fn(originalModule.getSwap),
15
+ };
16
+ });
17
+ const mockedGetSwap = evm_1.getSwap;
18
+ describe("Sushi Action Provider Input Schemas", () => {
19
+ describe("Swap Schema", () => {
20
+ it("should successfully parse valid input", () => {
21
+ const validInput = {
22
+ fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
23
+ amount: "0.0001",
24
+ toAssetAddress: "0x1234567890123456789012345678901234567890",
25
+ maxSlippage: 0.005,
26
+ };
27
+ const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(validInput);
28
+ expect(result.success).toBe(true);
29
+ expect(result.data).toEqual(validInput);
30
+ });
31
+ it("should fail parsing invalid fromAssetAddress", () => {
32
+ const invalidInput = {
33
+ fromAssetAddress: "invalid-address",
34
+ amount: "0.0001",
35
+ toAssetAddress: "0x1234567890123456789012345678901234567890",
36
+ maxSlippage: 0.005,
37
+ };
38
+ const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
39
+ expect(result.success).toBe(false);
40
+ });
41
+ it("should fail parsing invalid toAssetAddress", () => {
42
+ const invalidInput = {
43
+ fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
44
+ amount: "0.0001",
45
+ toAssetAddress: "invalid-address",
46
+ maxSlippage: 0.005,
47
+ };
48
+ const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
49
+ expect(result.success).toBe(false);
50
+ });
51
+ it("should fail parsing invalid maxSlippage (>1)", () => {
52
+ const invalidInput = {
53
+ fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
54
+ amount: "0.0001",
55
+ toAssetAddress: "0x1234567890123456789012345678901234567890",
56
+ maxSlippage: 1.1,
57
+ };
58
+ const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
59
+ expect(result.success).toBe(false);
60
+ });
61
+ it("should fail parsing invalid maxSlippage (<0)", () => {
62
+ const invalidInput = {
63
+ fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
64
+ amount: "0.0001",
65
+ toAssetAddress: "0x1234567890123456789012345678901234567890",
66
+ maxSlippage: -1.1,
67
+ };
68
+ const result = sushiRouterSchemas_1.SushiSwapSchema.safeParse(invalidInput);
69
+ expect(result.success).toBe(false);
70
+ });
71
+ });
72
+ describe("Quote Schema", () => {
73
+ it("should successfully parse valid input", () => {
74
+ const validInput = {
75
+ fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
76
+ amount: "0.0001",
77
+ toAssetAddress: "0x1234567890123456789012345678901234567890",
78
+ };
79
+ const result = sushiRouterSchemas_1.SushiQuoteSchema.safeParse(validInput);
80
+ expect(result.success).toBe(true);
81
+ expect(result.data).toEqual(validInput);
82
+ });
83
+ it("should fail parsing invalid fromAssetAddress", () => {
84
+ const invalidInput = {
85
+ fromAssetAddress: "invalid-address",
86
+ amount: "0.0001",
87
+ toAssetAddress: "0x1234567890123456789012345678901234567890",
88
+ maxSlippage: 0.005,
89
+ };
90
+ const result = sushiRouterSchemas_1.SushiQuoteSchema.safeParse(invalidInput);
91
+ expect(result.success).toBe(false);
92
+ });
93
+ it("should fail parsing invalid toAssetAddress", () => {
94
+ const invalidInput = {
95
+ fromAssetAddress: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83",
96
+ amount: "0.0001",
97
+ toAssetAddress: "invalid-address",
98
+ maxSlippage: 0.005,
99
+ };
100
+ const result = sushiRouterSchemas_1.SushiQuoteSchema.safeParse(invalidInput);
101
+ expect(result.success).toBe(false);
102
+ });
103
+ });
104
+ });
105
+ describe("Sushi Action Provider", () => {
106
+ let actionProvider;
107
+ let mockWallet;
108
+ const amountIn = BigInt(1000000);
109
+ const amountOut = BigInt(500000);
110
+ const nativeToken = {
111
+ address: evm_1.nativeAddress,
112
+ symbol: "ETH",
113
+ name: "Ether",
114
+ decimals: 18,
115
+ };
116
+ const tokenIn = {
117
+ address: "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa",
118
+ symbol: "TIN",
119
+ name: "Token In",
120
+ decimals: 18,
121
+ };
122
+ const tokenOut = {
123
+ address: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
124
+ symbol: "TOU",
125
+ name: "Token Out",
126
+ decimals: 18,
127
+ };
128
+ const user = "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF";
129
+ const txHash = "0xhash";
130
+ const chainId = 1;
131
+ const getRouteLog = ({ tokenIn, tokenOut, amountIn, amountOut, }) => [
132
+ {
133
+ data: (0, viem_1.encodeAbiParameters)((0, viem_1.parseAbiParameters)("address to, address tokenOut, uint256 amountIn, uint256 amountOut, int256 slippage, bytes32 diagnosticsFirst32"), [user, tokenOut.address, amountIn, amountOut, 0n, `0x${"00".repeat(32)}`]),
134
+ topics: (0, viem_1.encodeEventTopics)({
135
+ abi: constants_1.routeProcessor9Abi_Route,
136
+ eventName: "Route",
137
+ args: {
138
+ from: user,
139
+ tokenIn: tokenIn.address,
140
+ referralCode: 0,
141
+ },
142
+ }),
143
+ },
144
+ ];
145
+ const getSuccessfullSwapResponse = async ({ tokenIn, amountIn, tokenOut, amountOut, }) => ({
146
+ amountIn: String(amountIn),
147
+ assumedAmountOut: String(amountOut),
148
+ priceImpact: 0,
149
+ status: evm_1.RouteStatus.Success,
150
+ swapPrice: 1,
151
+ tokens: [tokenIn, tokenOut],
152
+ tokenFrom: tokenIn,
153
+ tokenTo: tokenOut,
154
+ tx: {
155
+ to: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
156
+ data: "0x",
157
+ from: user,
158
+ value: BigInt(0),
159
+ gas: "1000000",
160
+ gasPrice: 1000000000,
161
+ },
162
+ });
163
+ beforeEach(() => {
164
+ // Reset all mocks before each test
165
+ jest.clearAllMocks();
166
+ actionProvider = new sushiRouterActionProvider_1.SushiRouterActionProvider();
167
+ mockWallet = {
168
+ readContract: jest.fn(),
169
+ sendTransaction: jest.fn(),
170
+ waitForTransactionReceipt: jest.fn(),
171
+ getBalance: jest.fn(),
172
+ getNetwork: jest.fn().mockResolvedValue({
173
+ protocolFamily: "evm",
174
+ networkId: "ethereum-mainnet",
175
+ chainId: String(chainId),
176
+ }),
177
+ getAddress: jest.fn().mockReturnValue(user),
178
+ };
179
+ });
180
+ describe("swap", () => {
181
+ it("should successfully perform a swap (token -> token)", async () => {
182
+ const args = {
183
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
184
+ fromAssetAddress: tokenIn.address,
185
+ toAssetAddress: tokenOut.address,
186
+ maxSlippage: 0.005,
187
+ };
188
+ /*
189
+ * 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default)
190
+ * 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
191
+ * 3. Mock the readContract which checks for the approval (0, not approved)
192
+ */
193
+ mockWallet.readContract
194
+ .mockResolvedValueOnce(tokenIn.decimals)
195
+ .mockResolvedValueOnce(amountIn)
196
+ .mockResolvedValueOnce(BigInt(0));
197
+ mockWallet.sendTransaction.mockResolvedValue(txHash);
198
+ /*
199
+ * 1. Mock the waitForTransactionReceipt to return success for the approval tx
200
+ * 2. Mock the waitForTransactionReceipt to return success for the swap tx, including the Route log
201
+ */
202
+ mockWallet.waitForTransactionReceipt
203
+ .mockResolvedValueOnce({
204
+ status: "success",
205
+ })
206
+ .mockResolvedValueOnce({
207
+ status: "success",
208
+ logs: getRouteLog({
209
+ tokenIn,
210
+ tokenOut,
211
+ amountIn,
212
+ amountOut,
213
+ }),
214
+ });
215
+ mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
216
+ tokenIn,
217
+ amountIn,
218
+ tokenOut,
219
+ amountOut,
220
+ }));
221
+ const result = await actionProvider.swap(mockWallet, args);
222
+ expect(mockWallet.readContract).toHaveBeenCalledTimes(3); // Decimals + Balance + Approval
223
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(2); // Approval + Swap
224
+ expect(mockedGetSwap).toHaveBeenCalledTimes(2);
225
+ expect(result).toContain(`Swapped ${(0, viem_1.formatUnits)(amountIn, tokenIn.decimals)} of ${tokenIn.symbol} (${tokenIn.address}) for ${(0, viem_1.formatUnits)(amountOut, tokenOut.decimals)} of ${tokenOut.symbol} (${tokenOut.address})`);
226
+ expect(result).toContain(`Transaction hash: ${txHash}`);
227
+ expect(result).toContain(`Transaction link: ${(0, evm_1.getEvmChainById)(chainId).getTransactionUrl(txHash)}`);
228
+ expect(result).toContain(`on ${(0, evm_1.getEvmChainById)(chainId).shortName}`);
229
+ });
230
+ it("should successfully perform a swap (native -> token)", async () => {
231
+ const args = {
232
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
233
+ fromAssetAddress: nativeToken.address,
234
+ toAssetAddress: tokenOut.address,
235
+ maxSlippage: 0.005,
236
+ };
237
+ // Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
238
+ mockWallet.getBalance.mockResolvedValue(amountIn);
239
+ mockWallet.sendTransaction.mockResolvedValue(txHash);
240
+ // Mock the waitForTransactionReceipt to return success for the swap tx, including the Route log
241
+ mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({
242
+ status: "success",
243
+ logs: getRouteLog({
244
+ tokenIn: nativeToken,
245
+ tokenOut,
246
+ amountIn,
247
+ amountOut,
248
+ }),
249
+ });
250
+ mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
251
+ tokenIn: nativeToken,
252
+ amountIn,
253
+ tokenOut,
254
+ amountOut,
255
+ }));
256
+ const result = await actionProvider.swap(mockWallet, args);
257
+ expect(mockWallet.getBalance).toHaveBeenCalledTimes(1);
258
+ expect(mockWallet.readContract).toHaveBeenCalledTimes(0); // No balance check nor approval
259
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); // Swap
260
+ expect(mockedGetSwap).toHaveBeenCalledTimes(2);
261
+ expect(result).toContain(`Swapped ${(0, viem_1.formatUnits)(amountIn, nativeToken.decimals)} of ${nativeToken.symbol} (${nativeToken.address}) for ${(0, viem_1.formatUnits)(amountOut, tokenOut.decimals)} of ${tokenOut.symbol} (${tokenOut.address})`);
262
+ expect(result).toContain(`Transaction hash: ${txHash}`);
263
+ expect(result).toContain(`Transaction link: ${(0, evm_1.getEvmChainById)(chainId).getTransactionUrl(txHash)}`);
264
+ expect(result).toContain(`on ${(0, evm_1.getEvmChainById)(chainId).shortName}`);
265
+ });
266
+ it("should fail if there isn't enough balance (native)", async () => {
267
+ const args = {
268
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
269
+ fromAssetAddress: nativeToken.address,
270
+ toAssetAddress: tokenOut.address,
271
+ maxSlippage: 0.005,
272
+ };
273
+ // Mock the readContract which checks for the balance of the fromAssetAddress token (99, not enough balance)
274
+ mockWallet.getBalance.mockResolvedValue(amountIn - BigInt(1));
275
+ const result = await actionProvider.swap(mockWallet, args);
276
+ expect(mockWallet.getBalance).toHaveBeenCalledTimes(1);
277
+ expect(mockWallet.readContract).toHaveBeenCalledTimes(0);
278
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(0);
279
+ expect(mockedGetSwap).toHaveBeenCalledTimes(1);
280
+ expect(result).toContain(`Swap failed: Insufficient balance for ${nativeToken.symbol} (${nativeToken.address})`);
281
+ });
282
+ it("should fail if there isn't enough balance (token)", async () => {
283
+ const args = {
284
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
285
+ fromAssetAddress: tokenIn.address,
286
+ toAssetAddress: tokenOut.address,
287
+ maxSlippage: 0.005,
288
+ };
289
+ /*
290
+ * 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default)
291
+ * 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
292
+ */
293
+ mockWallet.readContract.mockResolvedValueOnce(18).mockResolvedValue(amountIn - BigInt(1));
294
+ mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
295
+ tokenIn,
296
+ amountIn,
297
+ tokenOut,
298
+ amountOut,
299
+ }));
300
+ const result = await actionProvider.swap(mockWallet, args);
301
+ expect(mockWallet.getBalance).toHaveBeenCalledTimes(0);
302
+ expect(mockWallet.readContract).toHaveBeenCalledTimes(2);
303
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(0);
304
+ expect(mockedGetSwap).toHaveBeenCalledTimes(1);
305
+ expect(result).toContain(`Swap failed: Insufficient balance for ${tokenIn.symbol} (${tokenIn.address})`);
306
+ });
307
+ it("should not approve if already approved", async () => {
308
+ const args = {
309
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
310
+ fromAssetAddress: tokenIn.address,
311
+ toAssetAddress: tokenOut.address,
312
+ maxSlippage: 0.005,
313
+ };
314
+ /*
315
+ * 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default)
316
+ * 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
317
+ * 3. Mock the readContract which checks for the approval (1000000, approved)
318
+ */
319
+ mockWallet.readContract
320
+ .mockResolvedValueOnce(tokenIn.decimals)
321
+ .mockResolvedValueOnce(amountIn)
322
+ .mockResolvedValueOnce(amountIn);
323
+ mockWallet.sendTransaction.mockResolvedValue(txHash);
324
+ // Mock the waitForTransactionReceipt to return success for the swap tx, including the Route log
325
+ mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({
326
+ status: "success",
327
+ logs: getRouteLog({
328
+ tokenIn,
329
+ tokenOut,
330
+ amountIn,
331
+ amountOut,
332
+ }),
333
+ });
334
+ mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
335
+ tokenIn,
336
+ amountIn,
337
+ tokenOut,
338
+ amountOut,
339
+ }));
340
+ const result = await actionProvider.swap(mockWallet, args);
341
+ expect(mockWallet.getBalance).toHaveBeenCalledTimes(0);
342
+ expect(mockWallet.readContract).toHaveBeenCalledTimes(3); // Decimals + Balance + Allowance
343
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); // Swap
344
+ expect(mockedGetSwap).toHaveBeenCalledTimes(2);
345
+ expect(result).toContain(`Swapped`);
346
+ });
347
+ it("should fail if there's no route", async () => {
348
+ const args = {
349
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
350
+ fromAssetAddress: tokenIn.address,
351
+ toAssetAddress: tokenOut.address,
352
+ maxSlippage: 0.005,
353
+ };
354
+ /*
355
+ * 1. Mock the readContract which checks for decimals of the fromAssetAddress token (18, default)
356
+ * 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance)
357
+ */
358
+ mockWallet.readContract
359
+ .mockResolvedValueOnce(tokenIn.decimals)
360
+ .mockResolvedValueOnce(amountIn);
361
+ mockedGetSwap.mockReturnValue(new Promise(r => r({
362
+ status: evm_1.RouteStatus.NoWay,
363
+ })));
364
+ const result = await actionProvider.swap(mockWallet, args);
365
+ expect(mockWallet.getBalance).toHaveBeenCalledTimes(0);
366
+ expect(mockWallet.readContract).toHaveBeenCalledTimes(1); // Decimals
367
+ expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(0);
368
+ expect(mockedGetSwap).toHaveBeenCalledTimes(1);
369
+ expect(result).toContain(`No route found to swap ${amountIn} of ${tokenIn.address} for ${tokenOut.address}`);
370
+ });
371
+ });
372
+ describe("quote", () => {
373
+ it("should successfully fetch a quote (token -> token)", async () => {
374
+ const args = {
375
+ amount: (0, viem_1.formatUnits)(amountIn, tokenIn.decimals),
376
+ fromAssetAddress: tokenIn.address,
377
+ toAssetAddress: tokenOut.address,
378
+ };
379
+ mockedGetSwap.mockReturnValue(getSuccessfullSwapResponse({
380
+ tokenIn,
381
+ amountIn,
382
+ tokenOut,
383
+ amountOut,
384
+ }));
385
+ const result = await actionProvider.quote(mockWallet, args);
386
+ expect(mockedGetSwap).toHaveBeenCalledTimes(1);
387
+ expect(result).toContain(`Found a quote for ${tokenIn.symbol} (${tokenIn.address}) -> ${tokenOut.symbol} (${tokenOut.address})`);
388
+ expect(result).toContain(`AmountIn: ${(0, viem_1.formatUnits)(amountIn, tokenIn.decimals)}`);
389
+ expect(result).toContain(`AmountOut: ${(0, viem_1.formatUnits)(amountOut, tokenOut.decimals)}`);
390
+ });
391
+ });
392
+ });
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Input schema for asset swap action
4
+ */
5
+ export declare const SushiSwapSchema: z.ZodObject<{
6
+ fromAssetAddress: z.ZodEffects<z.ZodString, `0x${string}`, string>;
7
+ amount: z.ZodString;
8
+ toAssetAddress: z.ZodEffects<z.ZodString, `0x${string}`, string>;
9
+ maxSlippage: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ amount: string;
12
+ fromAssetAddress: `0x${string}`;
13
+ toAssetAddress: `0x${string}`;
14
+ maxSlippage: number;
15
+ }, {
16
+ amount: string;
17
+ fromAssetAddress: string;
18
+ toAssetAddress: string;
19
+ maxSlippage?: number | undefined;
20
+ }>;
21
+ /**
22
+ * Input schema for quote action
23
+ */
24
+ export declare const SushiQuoteSchema: z.ZodObject<{
25
+ fromAssetAddress: z.ZodEffects<z.ZodString, `0x${string}`, string>;
26
+ amount: z.ZodString;
27
+ toAssetAddress: z.ZodEffects<z.ZodString, `0x${string}`, string>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ amount: string;
30
+ fromAssetAddress: `0x${string}`;
31
+ toAssetAddress: `0x${string}`;
32
+ }, {
33
+ amount: string;
34
+ fromAssetAddress: string;
35
+ toAssetAddress: string;
36
+ }>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SushiQuoteSchema = exports.SushiSwapSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * Input schema for asset swap action
7
+ */
8
+ exports.SushiSwapSchema = zod_1.z
9
+ .object({
10
+ fromAssetAddress: zod_1.z
11
+ .string()
12
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
13
+ .transform(val => val)
14
+ .describe("The Ethereum address of the input asset"),
15
+ amount: zod_1.z
16
+ .string()
17
+ .regex(/^\d+(\.\d+)?$/, "Invalid number format")
18
+ .describe("The amount of the input asset to swap, in the human readable format"),
19
+ toAssetAddress: zod_1.z
20
+ .string()
21
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
22
+ .transform(val => val)
23
+ .describe("The Ethereum address of the output asset"),
24
+ maxSlippage: zod_1.z
25
+ .number()
26
+ .min(0)
27
+ .max(1)
28
+ .optional()
29
+ .default(0.005) // 0.05%
30
+ .describe("The maximum slippage allowed for the swap, where 0 is 0% and 1 is 100%"),
31
+ })
32
+ .strip()
33
+ .describe("Instructions for trading assets");
34
+ /**
35
+ * Input schema for quote action
36
+ */
37
+ exports.SushiQuoteSchema = zod_1.z
38
+ .object({
39
+ fromAssetAddress: zod_1.z
40
+ .string()
41
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
42
+ .transform(val => val)
43
+ .describe("The Ethereum address of the input asset"),
44
+ amount: zod_1.z
45
+ .string()
46
+ .regex(/^\d+(\.\d+)?$/, "Invalid number format")
47
+ .describe("The amount of the input asset to get a quote for"),
48
+ toAssetAddress: zod_1.z
49
+ .string()
50
+ .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format")
51
+ .transform(val => val)
52
+ .describe("The Ethereum address of the output asset"),
53
+ })
54
+ .strip()
55
+ .describe("Instructions for fetching a quote for a trade");
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@coinbase/agentkit",
3
3
  "description": "Coinbase AgentKit core primitives",
4
4
  "repository": "https://github.com/coinbase/agentkit",
5
- "version": "0.0.0-nightly-20251116210417",
5
+ "version": "0.0.0-nightly-20251117210447",
6
6
  "author": "Coinbase Inc.",
7
7
  "license": "Apache-2.0",
8
8
  "main": "dist/index.js",
@@ -48,6 +48,7 @@
48
48
  "md5": "^2.3.0",
49
49
  "opensea-js": "^7.1.18",
50
50
  "reflect-metadata": "^0.2.2",
51
+ "sushi": "6.2.1",
51
52
  "@ensofinance/sdk": "^2.0.6",
52
53
  "twitter-api-v2": "^1.18.2",
53
54
  "viem": "^2.22.16",