@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
403 lines (396 loc) • 20.9 kB
JavaScript
"use strict";
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.baseAccountActionProvider = exports.BaseAccountActionProvider = void 0;
const zod_1 = require("zod");
const viem_1 = require("viem");
const actionProvider_1 = require("../actionProvider");
const actionDecorator_1 = require("../actionDecorator");
const wallet_providers_1 = require("../../wallet-providers");
const utils_1 = require("../erc20/utils");
const schemas_1 = require("./schemas");
const utils_2 = require("./utils");
const utils_3 = require("../../utils");
const spend_permission_1 = require("@base-org/account/spend-permission");
/**
* Fetch spend permissions for a user account from a spender address
*
* @param userAccount - The user account address
* @param spenderAccount - The spender account address
* @param tokenAddress - Optional token address to filter by. If not provided, returns all permissions
* @returns Promise resolving to array of fetched permissions
*/
async function fetchUserSpendPermissions(userAccount, spenderAccount, tokenAddress) {
try {
const permissions = await (0, spend_permission_1.fetchPermissions)({
account: userAccount,
chainId: 8453,
spender: spenderAccount,
});
if (tokenAddress) {
return permissions.filter(p => p.permission?.token?.toLowerCase() === tokenAddress.toLowerCase());
}
return permissions;
}
catch (error) {
console.error("❌ Failed to fetch spend permissions:", error);
return [];
}
}
/**
* BaseAccountActionProvider provides actions to interact with Base Account spend permissions.
*/
class BaseAccountActionProvider extends actionProvider_1.ActionProvider {
/**
* Constructor for the BaseAccountActionProvider.
*/
constructor() {
super("base_account", []);
/**
* Checks if the Base Account action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Base Account action provider supports the network, false otherwise.
*/
this.supportsNetwork = (network) => {
// Base Account spend permissions are only supported on Base mainnet
return network.networkId === "base-mainnet";
};
}
/**
* Lists spend permissions that have been granted to the current wallet by a Base Account.
*
* @param walletProvider - The wallet provider to use for listing permissions.
* @param args - The input arguments for listing spend permissions.
* @returns A JSON string with the list of spend permissions.
*/
async listBaseAccountSpendPermissions(walletProvider, args) {
try {
// Validate and normalize addresses
const baseAccount = (0, viem_1.getAddress)(args.baseAccount);
const spenderAddress = (0, viem_1.getAddress)(walletProvider.getAddress());
// Fetch permissions with retry logic
const permissions = await (0, utils_3.retryWithExponentialBackoff)(() => fetchUserSpendPermissions(baseAccount, spenderAddress), 3, // Max 3 retries
1000);
if (permissions.length === 0) {
return JSON.stringify({
success: false,
error: "No spend permissions found for Base Account. The user needs to grant spend permissions first through the UI.",
baseAccount: baseAccount,
spender: spenderAddress,
permissionsCount: 0,
});
}
// Format the permissions with token details
const formattedPermissions = await Promise.all(permissions.map(async (permission, index) => {
const tokenAddress = permission.permission?.token || "";
const allowanceRaw = permission.permission?.allowance || 0;
// Get token details for proper formatting
let tokenName = "Unknown Token";
let allowanceFormatted;
if (tokenAddress) {
const tokenDetails = await (0, utils_1.getTokenDetails)(walletProvider, tokenAddress);
if (tokenDetails) {
tokenName = tokenDetails.name;
allowanceFormatted = (0, viem_1.formatUnits)(allowanceRaw, tokenDetails.decimals);
}
}
return {
permissionIndex: index + 1,
token: tokenAddress,
tokenName: tokenName,
allowance: allowanceFormatted || allowanceRaw.toString(),
createdAt: permission.createdAt ? (0, utils_2.formatTimestamp)(permission.createdAt) : "Unknown",
period: permission.permission?.period
? (0, utils_2.formatPeriod)(permission.permission.period)
: "Unknown",
start: permission.permission?.start
? (0, utils_2.formatTimestamp)(permission.permission.start)
: "Unknown",
end: permission.permission?.end
? (0, utils_2.formatTimestamp)(permission.permission.end)
: "Unknown",
};
}));
return JSON.stringify({
success: true,
permissionsCount: permissions.length,
baseAccount: baseAccount,
spender: spenderAddress,
permissions: formattedPermissions,
});
}
catch (error) {
console.error("Error listing spend permissions:", error);
return JSON.stringify({
success: false,
error: `Error listing spend permissions: ${error instanceof Error ? error.message : "Unknown error"}`,
baseAccount: args.baseAccount,
spender: walletProvider.getAddress(),
});
}
}
/**
* Uses a spend permission to transfer tokens from a Base Account to the current wallet.
*
* @param walletProvider - The wallet provider to use for the spend operation.
* @param args - The input arguments for using the spend permission.
* @returns A JSON string with the spend operation results.
*/
async spendFromBaseAccountPermission(walletProvider, args) {
try {
// Validate and normalize addresses
const baseAccount = (0, viem_1.getAddress)(args.baseAccount);
const spenderAddress = (0, viem_1.getAddress)(walletProvider.getAddress());
const tokenAddress = args.tokenAddress ? (0, viem_1.getAddress)(args.tokenAddress) : undefined;
// Fetch permissions to find a valid one with retry logic
const permissions = await (0, utils_3.retryWithExponentialBackoff)(() => fetchUserSpendPermissions(baseAccount, spenderAddress, tokenAddress), 3, // Max 3 retries
1000);
if (permissions.length === 0) {
const errorMsg = tokenAddress
? `No spend permissions found for Base Account with token ${tokenAddress}.`
: "No spend permissions found for Base Account.";
return JSON.stringify({
success: false,
error: `${errorMsg} The user needs to grant spend permissions first through the UI.`,
baseAccount: baseAccount,
spender: spenderAddress,
tokenAddress: tokenAddress,
});
}
// Determine which permission to use
const permissionIndex = args.permissionIndex || 1;
if (permissionIndex > permissions.length) {
return JSON.stringify({
success: false,
error: `Permission index ${permissionIndex} is out of range. Only ${permissions.length} permission(s) available.`,
baseAccount: baseAccount,
spender: spenderAddress,
availablePermissions: permissions.length,
});
}
// Use the specified permission (convert to 0-based index)
const _permission = permissions[permissionIndex - 1];
const permissionTokenAddress = _permission.permission?.token;
if (!permissionTokenAddress) {
return JSON.stringify({
success: false,
error: "Permission does not have a valid token address",
baseAccount: baseAccount,
spender: spenderAddress,
});
}
// Get token details for proper conversion
const tokenDetails = await (0, utils_1.getTokenDetails)(walletProvider, permissionTokenAddress);
if (!tokenDetails) {
return JSON.stringify({
success: false,
error: `Failed to get token details for address: ${permissionTokenAddress}`,
baseAccount: baseAccount,
spender: spenderAddress,
tokenAddress: permissionTokenAddress,
});
}
// Check permission status and prepare spend call data
const status = await (0, utils_3.retryWithExponentialBackoff)(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => (0, spend_permission_1.getPermissionStatus)(_permission), 3, // Max 3 retries
1000, // 1s base delay
2000);
// Determine amount to spend - use provided amount or full remaining allowance
const amountInAtomicUnits = args.amount
? (0, viem_1.parseUnits)(args.amount.toString(), tokenDetails.decimals)
: status.remainingSpend;
if (status.remainingSpend < amountInAtomicUnits) {
const requestedAmountFormatted = args.amount?.toString() || (0, viem_1.formatUnits)(amountInAtomicUnits, tokenDetails.decimals);
return JSON.stringify({
success: false,
error: `Insufficient remaining allowance. Requested: ${requestedAmountFormatted} ${tokenDetails.name}, Available: ${(0, viem_1.formatUnits)(status.remainingSpend, tokenDetails.decimals)} ${tokenDetails.name}`,
baseAccount: baseAccount,
spender: spenderAddress,
tokenAddress: permissionTokenAddress,
tokenName: tokenDetails.name,
requestedAmount: requestedAmountFormatted,
availableAmount: (0, viem_1.formatUnits)(status.remainingSpend, tokenDetails.decimals),
});
}
// Prepare the spend transaction - returns an array of calls
const spendCalls = await (0, utils_3.retryWithExponentialBackoff)(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => (0, spend_permission_1.prepareSpendCallData)(_permission, amountInAtomicUnits), 3, // Max 3 retries
1000, // 1s base delay
2000);
// Take the first call from the array (there should be one for spend operations)
if (!spendCalls || spendCalls.length === 0) {
throw new Error("No transaction calls returned from prepareSpendCallData");
}
const callData = spendCalls[0];
// Execute the spend transaction
const txHash = await walletProvider.sendTransaction({
to: callData.to,
data: callData.data,
value: BigInt(callData.value || "0x0"),
});
const amountSpentFormatted = (0, viem_1.formatUnits)(amountInAtomicUnits, tokenDetails.decimals);
return JSON.stringify({
success: true,
transactionHash: txHash,
amountSpent: amountSpentFormatted,
tokenAddress: permissionTokenAddress,
tokenName: tokenDetails.name,
baseAccount: baseAccount,
spender: spenderAddress,
permissionIndex: permissionIndex,
remainingAllowance: (0, viem_1.formatUnits)(status.remainingSpend - amountInAtomicUnits, tokenDetails.decimals),
});
}
catch (error) {
console.error("Error using spend permission:", error);
return JSON.stringify({
success: false,
error: `Error using spend permission: ${error instanceof Error ? error.message : "Unknown error"}`,
baseAccount: args.baseAccount,
spender: walletProvider.getAddress(),
amountRequested: args.amount?.toString() || "full allowance",
tokenAddress: args.tokenAddress,
permissionIndex: args.permissionIndex,
});
}
}
/**
* Revokes a spend permission that was previously granted by a Base Account to the current wallet.
*
* @param walletProvider - The wallet provider to use for the revoke operation.
* @param args - The input arguments for revoking the spend permission.
* @returns A JSON string with the revoke operation results.
*/
async revokeBaseAccountSpendPermission(walletProvider, args) {
try {
// Validate and normalize addresses
const baseAccount = (0, viem_1.getAddress)(args.baseAccount);
const spenderAddress = (0, viem_1.getAddress)(walletProvider.getAddress());
// Fetch permissions to find the one to revoke with retry logic
const permissions = await (0, utils_3.retryWithExponentialBackoff)(() => fetchUserSpendPermissions(baseAccount, spenderAddress), 3, // Max 3 retries
1000);
if (permissions.length === 0) {
return JSON.stringify({
success: false,
error: "No spend permissions found for Base Account. There are no permissions to revoke.",
baseAccount: baseAccount,
spender: spenderAddress,
});
}
// Determine which permission to revoke
const permissionIndex = args.permissionIndex || 1;
if (permissionIndex > permissions.length) {
return JSON.stringify({
success: false,
error: `Permission index ${permissionIndex} is out of range. Only ${permissions.length} permission(s) available.`,
baseAccount: baseAccount,
spender: spenderAddress,
availablePermissions: permissions.length,
});
}
// Use the specified permission (convert to 0-based index)
const _permission = permissions[permissionIndex - 1];
// Prepare the revoke transaction
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const revokeCall = await (0, spend_permission_1.prepareRevokeCallData)(_permission);
// Execute the revoke transaction
const txHash = await walletProvider.sendTransaction({
to: revokeCall.to,
data: revokeCall.data,
value: BigInt(revokeCall.value || "0x0"),
});
return JSON.stringify({
success: true,
transactionHash: txHash,
revokedPermissionIndex: permissionIndex,
baseAccount: baseAccount,
spender: spenderAddress,
message: "Spend permission successfully revoked",
});
}
catch (error) {
console.error("Error revoking spend permission:", error);
return JSON.stringify({
success: false,
error: `Error revoking spend permission: ${error instanceof Error ? error.message : "Unknown error"}`,
baseAccount: args.baseAccount,
spender: walletProvider.getAddress(),
permissionIndex: args.permissionIndex,
});
}
}
}
exports.BaseAccountActionProvider = BaseAccountActionProvider;
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "list_base_account_spend_permissions",
description: `
This tool lists spend permissions that have been granted to the current wallet by a Base Account.
It takes the following inputs:
- baseAccount: The Base Account address to check if it has granted spend permissions
Important notes:
- This tool is specifically designed for Base Account spend permissions
- The returned period is the time duration for resetting the used allowance on a recurring basis within the start and end timestamps
- The returned allowance is the maximum allowed value to spend within each period
`,
schema: schemas_1.ListBaseAccountSpendPermissionsSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], BaseAccountActionProvider.prototype, "listBaseAccountSpendPermissions", null);
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "spend_from_base_account_permission",
description: `
This tool uses a spend permission to transfer tokens from a Base Account to the current wallet.
It takes the following inputs:
- baseAccount: The Base Account address that has granted the spend permission
- amount: (Optional) The amount to spend in whole units of the token (e.g. 4.6 for 4.6 tokens). If not provided, will withdraw the full remaining allowance
- tokenAddress: (Optional) The token contract address. If not provided, will use the first available permission token
- permissionIndex: (Optional) The index of the permission to use (1-based). If not provided, the first permission will be used
Important notes:
- The Base Account must have previously granted a spend permission to the current agent
- This tool automatically finds and uses the specified token spend permission, or the first valid permission if no token is specified
- The funds are transferred from the Base Account to the current wallet after successful execution
- This is specifically designed for Base Account spend permissions on Base mainnet
`,
schema: schemas_1.UseBaseAccountSpendPermissionSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], BaseAccountActionProvider.prototype, "spendFromBaseAccountPermission", null);
__decorate([
(0, actionDecorator_1.CreateAction)({
name: "revoke_base_account_spend_permission",
description: `
This tool revokes a spend permission that was previously granted by a Base Account to the current wallet.
It takes the following inputs:
- baseAccount: The Base Account address that has granted the spend permission
- permissionIndex: (Optional) The index of the permission to revoke (1-based). If not provided, the first permission will be revoked
Important notes:
- This permanently revokes the spend permission - it cannot be undone
- The Base Account must have previously granted a spend permission to the current agent
- This tool automatically finds and revokes the specified token spend permission
- This is specifically designed for Base Account spend permissions on Base mainnet
`,
schema: schemas_1.RevokeBaseAccountSpendPermissionSchema,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [wallet_providers_1.EvmWalletProvider, void 0]),
__metadata("design:returntype", Promise)
], BaseAccountActionProvider.prototype, "revokeBaseAccountSpendPermission", null);
const baseAccountActionProvider = () => new BaseAccountActionProvider();
exports.baseAccountActionProvider = baseAccountActionProvider;