@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
387 lines (384 loc) • 16.9 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sushiRouterActionProvider = exports.SushiRouterActionProvider = void 0;
const zod_1 = require("zod");
const wallet_providers_1 = require("../../wallet-providers");
const actionDecorator_1 = require("../actionDecorator");
const actionProvider_1 = require("../actionProvider");
const sushiRouterSchemas_1 = require("./sushiRouterSchemas");
const evm_1 = require("sushi/evm");
const viem_1 = require("viem");
const constants_1 = require("./constants");
/**
* SushiRouterActionProvider is an action provider for Sushi.
*
* This provider is used for any action that uses the Sushi Router API.
*/
class SushiRouterActionProvider extends actionProvider_1.ActionProvider {
/**
* Constructor for the SushiRouterActionProvider class.
*/
constructor() {
super("sushi-router", []);
}
/**
* Swaps a specified amount of a from token to a to token for the wallet.
*
* @param walletProvider - The wallet provider to swap the tokens from.
* @param args - The input arguments for the action.
* @returns A message containing the swap details.
*/
async swap(walletProvider, args) {
try {
const chainId = Number((await walletProvider.getNetwork()).chainId);
// Compatible chainId is expected since it should be pre-checked in supportsNetwork
if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
return `Unsupported chainId: ${chainId}`;
}
const chain = (0, evm_1.getEvmChainById)(chainId);
const decimalsIn = await fetchDecimals({ walletProvider, token: args.fromAssetAddress });
if (!decimalsIn.success) {
return decimalsIn.message;
}
const amountIn = (0, viem_1.parseUnits)(args.amount, decimalsIn.decimals);
// First fetch to see if the swap is even possible
const firstSwap = await handleGetSwap({
amount: amountIn,
chainId,
tokenIn: args.fromAssetAddress,
tokenOut: args.toAssetAddress,
maxSlippage: args.maxSlippage,
recipient: walletProvider.getAddress(),
});
if (firstSwap.swap.status !== evm_1.RouteStatus.Success) {
return firstSwap.message;
}
// Check if the wallet has enough balance to perform the swap
const balance = await handleBalance({
walletProvider,
token: firstSwap.swap.tokenFrom,
minAmount: amountIn,
});
if (!balance.success) {
return balance.message;
}
const approval = await handleApproval({
walletProvider,
token: args.fromAssetAddress,
to: firstSwap.swap.tx.to,
amount: amountIn,
});
if (!approval.success) {
return approval.message;
}
// Refetch in case the route changed during approval
const secondSwap = await handleGetSwap({
amount: amountIn,
chainId,
tokenIn: args.fromAssetAddress,
tokenOut: args.toAssetAddress,
maxSlippage: args.maxSlippage,
recipient: walletProvider.getAddress(),
});
if (secondSwap.swap.status !== evm_1.RouteStatus.Success) {
return secondSwap.message;
}
const swapHash = await walletProvider.sendTransaction({
from: secondSwap.swap.tx.from,
to: secondSwap.swap.tx.to,
data: secondSwap.swap.tx.data,
value: BigInt(secondSwap.swap.tx.value || 0),
});
const swapReceipt = await walletProvider.waitForTransactionReceipt(swapHash);
if (swapReceipt.status === "reverted" || swapReceipt.status === "failed") {
return `Swap failed: Transaction Reverted.\n - Transaction hash: ${swapHash}\n - Transaction link: ${chain.getTransactionUrl(swapHash)}`;
}
// Find the Route event log, which includes the actual amountOut
const [routeLog] = swapReceipt.logs
.filter(log => (0, viem_1.encodeEventTopics)({
abi: constants_1.routeProcessor9Abi_Route,
eventName: "Route",
})[0] === log.topics[0])
.map(log => (0, viem_1.decodeEventLog)({
abi: constants_1.routeProcessor9Abi_Route,
data: log.data,
topics: log.topics,
}));
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}
- Transaction hash: ${swapHash}
- Transaction link: ${chain.getTransactionUrl(swapHash)}`;
}
catch (error) {
return `Error swapping tokens: ${error}`;
}
}
/**
* Gets a quote for a specified amount of a from token to a to token
*
* @param walletProvider - The wallet provider to swap the tokens from.
* @param args - The input arguments for the action.
* @returns A message containing the quote details.
*/
async quote(walletProvider, args) {
try {
const chainId = Number((await walletProvider.getNetwork()).chainId);
// Compatible chainId is expected since it should be pre-checked in supportsNetwork
if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
return `Unsupported chainId: ${chainId}`;
}
const decimalsIn = await fetchDecimals({ walletProvider, token: args.fromAssetAddress });
if (!decimalsIn.success) {
return decimalsIn.message;
}
const amountIn = (0, viem_1.parseUnits)(args.amount, decimalsIn.decimals);
const swap = await handleGetSwap({
amount: amountIn,
chainId,
tokenIn: args.fromAssetAddress,
tokenOut: args.toAssetAddress,
maxSlippage: 0.0005, // 0.05%
recipient: walletProvider.getAddress(),
});
return swap.message;
}
catch (error) {
return `Error quoting for tokens: ${error}`;
}
}
/**
* Custom action providers are supported on all networks
*
* @param network - The network to checkpointSaver
* @returns True if the network is supported, false otherwise
*/
supportsNetwork(network) {
if (network.protocolFamily !== "evm" || !network.chainId) {
return false;
}
return (0, evm_1.isSwapApiSupportedChainId)(Number(network.chainId));
}
}
exports.SushiRouterActionProvider = SushiRouterActionProvider;
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "swap",
description: `This tool will swap a specified amount of a 'from token' (erc20) to a 'to token' (erc20) for the wallet.
It takes the following inputs:
- The human-readable amount of the 'from token' to swap
- The from token address to trade
- The token address to receive from the swap
- The maximum slippage allowed for the swap, where 0 is 0% and 1 is 100%, the default is 0.005 (0.05%)
Important notes:
- The native asset (ie 'eth' on 'ethereum-mainnet') is represented by ${evm_1.nativeAddress} (not the wrapped native asset!)
- Fetch a quote first before the swap action. Stop, ask the user if they want to proceed. If the user answers affirmatively, then swap
- If you are not absolutely sure about token addresses, either use an action to fetch the token address or ask the user
`,
schema: sushiRouterSchemas_1.SushiSwapSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], SushiRouterActionProvider.prototype, "swap", null);
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "quote",
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).
It takes the following inputs:
- The human-readable amount of the 'from token' to fetch a quote for
- The from token address to fetch a quote for
- The token address to receive from the quoted swap
Important notes:
- The native asset (ie 'eth' on 'ethereum-mainnet') is represented by ${evm_1.nativeAddress} (not the wrapped native asset!)
- This action does not require any on-chain transactions or gas
- If you are not 100% certain about token addresses, use an action to fetch the token address first or ask the user
- 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')
`,
schema: sushiRouterSchemas_1.SushiQuoteSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], SushiRouterActionProvider.prototype, "quote", null);
/**
* Fetches the number of decimals for the token
*
* @param root0 - The input arguments for the action
* @param root0.walletProvider - The wallet provider to fetch the decimals from
* @param root0.token - The token address to fetch the decimals for
*
* @returns The number of decimals for the token
*/
async function fetchDecimals({ walletProvider, token, }) {
const chainId = Number((await walletProvider.getNetwork()).chainId);
if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
return {
success: false,
message: `Unsupported chainId: ${chainId}`,
};
}
if (token === evm_1.nativeAddress) {
return { success: true, decimals: evm_1.EvmNative.fromChainId(chainId).decimals };
}
const decimals = (await walletProvider.readContract({
address: token,
abi: viem_1.erc20Abi,
functionName: "decimals",
}));
return { success: true, decimals };
}
/**
* Checks if the wallet has enough balance to perform the swap
*
* @param root0 - The input arguments for the action
* @param root0.walletProvider - The wallet provider to fetch the balance from
* @param root0.token - The token address to fetch the balance for
* @param root0.token.address - The token address to fetch the balance for
* @param root0.token.symbol - The token symbol to fetch the balance for
* @param root0.token.decimals - The token decimals to fetch the balance for
* @param root0.minAmount - The minimum amount to check for
*
* @returns The balance of the wallet
*/
async function handleBalance({ walletProvider, token, minAmount, }) {
let balance;
if (token.address.toLowerCase() === evm_1.nativeAddress) {
balance = await walletProvider.getBalance();
}
else {
balance = (await walletProvider.readContract({
address: token.address,
abi: viem_1.erc20Abi,
functionName: "balanceOf",
args: [walletProvider.getAddress()],
}));
}
if (balance < minAmount) {
return {
success: false,
message: `Swap failed: Insufficient balance for ${token.symbol} (${token.address})
- Balance: ${(0, viem_1.formatUnits)(balance, token.decimals)}
- Required: ${(0, viem_1.formatUnits)(minAmount, token.decimals)}`,
};
}
return {
success: true,
};
}
/**
*
* Wraps the getSwap function, providing messages for possible states
*
* @param root0 - The input arguments for the action
* @param root0.amount - The amount to swap
* @param root0.chainId - The chainId to swap on
* @param root0.tokenIn - The input token address
* @param root0.tokenOut - The output token address
* @param root0.maxSlippage - The maximum slippage allowed
* @param root0.recipient - The recipient of the swap
*
* @returns The result of the swap and a message
*/
async function handleGetSwap({ amount, chainId, tokenIn, tokenOut, maxSlippage, recipient, }) {
if (!(0, evm_1.isRedSnwapperChainId)(chainId)) {
return {
swap: { status: evm_1.RouteStatus.NoWay },
message: `Unsupported chainId: ${chainId}`,
};
}
const swap = await (0, evm_1.getSwap)({
amount,
chainId,
tokenIn,
tokenOut,
maxSlippage,
sender: recipient,
recipient,
});
const chain = (0, evm_1.getEvmChainById)(chainId);
if (swap.status === evm_1.RouteStatus.NoWay) {
return {
swap,
message: `No route found to swap ${amount} of ${tokenIn} for ${tokenOut} on ${chain.shortName}`,
};
}
if (swap.status === evm_1.RouteStatus.Partial) {
return {
swap,
message: `Found a partial quote for ${swap.tokenFrom.symbol} -> ${swap.tokenTo.symbol}. Swapping the full amount is not possible.
- AmountIn: ${(0, viem_1.formatUnits)(BigInt(swap.amountIn), swap.tokenFrom.decimals)}
- AmountOut: ${(0, viem_1.formatUnits)(BigInt(swap.assumedAmountOut), swap.tokenTo.decimals)}`,
};
}
return {
swap,
message: `Found a quote for ${swap.tokenFrom.symbol} (${swap.tokenFrom.address}) -> ${swap.tokenTo.symbol} (${swap.tokenTo.address})
- AmountIn: ${(0, viem_1.formatUnits)(BigInt(swap.amountIn), swap.tokenFrom.decimals)} ${swap.tokenFrom.symbol}
- AmountOut: ${(0, viem_1.formatUnits)(BigInt(swap.assumedAmountOut), swap.tokenTo.decimals)} ${swap.tokenTo.symbol}`,
};
}
/**
*
* Handles the approval for the token
*
* @param root0 - The input arguments for the action
* @param root0.walletProvider - The wallet provider to handle the approval with
* @param root0.token - The token address to approve
* @param root0.to - The address to approve the token to
* @param root0.amount - The amount to approve
*
* @returns Either success: true on success or success: false with a message containing the reason for failure
*/
async function handleApproval({ walletProvider, token, to, amount, }) {
// No need to approve if the token is the native token
if (token.toLowerCase() === evm_1.nativeAddress) {
return { success: true };
}
// Check if the wallet already has enough allowance
const allowance = (await walletProvider.readContract({
address: token,
abi: viem_1.erc20Abi,
functionName: "allowance",
args: [walletProvider.getAddress(), to],
}));
if (allowance >= amount) {
return { success: true };
}
// Exact approval
const approvalHash = await walletProvider.sendTransaction({
to: token,
data: (0, viem_1.encodeFunctionData)({
abi: viem_1.erc20Abi,
functionName: "approve",
args: [to, BigInt(amount)],
}),
});
const approvalReceipt = await walletProvider.waitForTransactionReceipt(approvalHash);
const chainId = Number((await walletProvider.getNetwork()).chainId);
if (!(0, evm_1.isSwapApiSupportedChainId)(chainId)) {
return {
success: false,
message: `Unsupported chainId: ${chainId}`,
};
}
const chain = (0, evm_1.getEvmChainById)(chainId);
if (approvalReceipt.status === "reverted") {
return {
success: false,
message: `Swap failed: Approval Reverted.
- Transaction hash: ${approvalHash}
- Transaction link: ${chain.getTransactionUrl(approvalHash)}`,
};
}
return { success: true };
}
const sushiRouterActionProvider = () => new SushiRouterActionProvider();
exports.sushiRouterActionProvider = sushiRouterActionProvider;