diff --git a/typescript/.changeset/quiet-tools-retry.md b/typescript/.changeset/quiet-tools-retry.md new file mode 100644 index 000000000..f6c792d27 --- /dev/null +++ b/typescript/.changeset/quiet-tools-retry.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Fixed Base Account permission lookup errors so the retry policy can handle them. diff --git a/typescript/agentkit/src/action-providers/baseAccount/README.md b/typescript/agentkit/src/action-providers/baseAccount/README.md index 9e5ba83e3..37c239bea 100644 --- a/typescript/agentkit/src/action-providers/baseAccount/README.md +++ b/typescript/agentkit/src/action-providers/baseAccount/README.md @@ -58,5 +58,6 @@ The Base Account provider only supports **Base mainnet** (`base-mainnet`), as Ba - Permissions have time-based limits and spending allowances - Token amounts are automatically formatted with proper decimals and display token names - If no specific amount is provided, the full remaining allowance will be used +- Permission lookup failures are retried and reported instead of being treated as no permissions For more information on **Base Account spend permissions**, visit [Base Account Documentation](https://docs.base.org/base-account). diff --git a/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.test.ts b/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.test.ts index 61f7b968f..18783c95d 100644 --- a/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.test.ts @@ -6,6 +6,7 @@ import { } from "./schemas"; import { EvmWalletProvider } from "../../wallet-providers"; import { getTokenDetails } from "../erc20/utils"; +import { retryWithExponentialBackoff } from "../../utils"; // Mock the getTokenDetails function from ERC20 utils jest.mock("../erc20/utils", () => ({ @@ -20,7 +21,14 @@ jest.mock("@base-org/account/spend-permission", () => ({ prepareRevokeCallData: jest.fn(), })); +jest.mock("../../utils", () => ({ + retryWithExponentialBackoff: jest.fn(), +})); + const mockGetTokenDetails = getTokenDetails as jest.MockedFunction; +const mockRetryWithExponentialBackoff = retryWithExponentialBackoff as jest.MockedFunction< + typeof retryWithExponentialBackoff +>; // Get references to the mocked functions from the mocked modules const mockFetchPermissions = jest.fn(); @@ -198,6 +206,8 @@ describe("BaseAccountActionProvider", () => { mockPrepareSpendCallData.mockReset(); mockPrepareRevokeCallData.mockReset(); mockGetTokenDetails.mockReset(); + mockRetryWithExponentialBackoff.mockReset(); + mockRetryWithExponentialBackoff.mockImplementation(async fn => await fn()); }); describe("supportsNetwork", () => { @@ -236,6 +246,26 @@ describe("BaseAccountActionProvider", () => { expect(parsedResponse.permissionsCount).toBe(0); }); + it("should report permission lookup errors instead of an empty permission set", async () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + mockFetchPermissions.mockRejectedValue(new Error("permission service unavailable")); + + const response = await actionProvider.listBaseAccountSpendPermissions(mockWallet, { + baseAccount: MOCK_BASE_ACCOUNT, + }); + const parsedResponse = JSON.parse(response); + + expect(parsedResponse.success).toBe(false); + expect(parsedResponse.error).toContain("permission service unavailable"); + expect(parsedResponse.error).not.toContain("No spend permissions found"); + expect(mockRetryWithExponentialBackoff).toHaveBeenCalledWith(expect.any(Function), 3, 1000); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Error listing spend permissions:", + expect.any(Error), + ); + consoleErrorSpy.mockRestore(); + }); + it("should list permissions when found", async () => { mockFetchPermissions.mockResolvedValue([mockPermission]); diff --git a/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.ts b/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.ts index 9eeb27bef..1de3f05ee 100644 --- a/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.ts +++ b/typescript/agentkit/src/action-providers/baseAccount/baseAccountActionProvider.ts @@ -33,24 +33,19 @@ async function fetchUserSpendPermissions( spenderAccount: Address, tokenAddress?: Address, ): Promise { - try { - const permissions = await 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 []; + const permissions = await fetchPermissions({ + account: userAccount, + chainId: 8453, + spender: spenderAccount, + }); + + if (tokenAddress) { + return permissions.filter( + p => p.permission?.token?.toLowerCase() === tokenAddress.toLowerCase(), + ); } + + return permissions; } /**