diff --git a/.changeset/gblin-action-provider.md b/.changeset/gblin-action-provider.md new file mode 100644 index 000000000..8149f5132 --- /dev/null +++ b/.changeset/gblin-action-provider.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Added a GBLIN action provider to buy, redeem, and read GBLIN — a collateral-backed treasury index on Base. diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 37b14207f..d80922078 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -485,6 +485,23 @@ const agent = createAgent({
+GBLIN + + + + + + + + + + + + + +
buy_gblinBuys GBLIN, a collateral-backed treasury index on Base, with ETH. Derives a slippage-bounded minimum output from the on-chain quote.
sell_gblin_for_ethRedeems GBLIN back to ETH through the GBLIN Zap, approving the shares first if needed. Derives a slippage-bounded minimum output from the on-chain quote.
get_gblin_stateReads the ETH value of one GBLIN at NAV, the total supply, and whether the NAV is reliable.
+
+
Messari diff --git a/typescript/agentkit/src/action-providers/gblin/README.md b/typescript/agentkit/src/action-providers/gblin/README.md new file mode 100644 index 000000000..c7ac7bfb1 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/README.md @@ -0,0 +1,41 @@ +# GBLIN Action Provider + +Actions for interacting with [GBLIN](https://gblin.digital), a collateral-backed treasury index +on Base (cbBTC / WETH / USDC). The vault mints at NAV, redeems pro rata in kind and reduces the +weight of a basket asset on-chain when it draws down. It is intended for parking surplus agent +capital in managed crypto exposure — **not** a stablecoin and not financial advice. + +| Contract | Address | +| --- | --- | +| Vault (share token) | [`0xc2181d975c05c8c724b334bcED0764c0b86B1D53`](https://basescan.org/address/0xc2181d975c05c8c724b334bcED0764c0b86B1D53#code) | +| Lens (quotes) | [`0xfCFea8027019E8551A1f09AD91532471F5D26f61`](https://basescan.org/address/0xfCFea8027019E8551A1f09AD91532471F5D26f61#code) | +| Zap (exit to ETH) | [`0x0E9D6Ceb6D313b021622C121Cda9C62e86e60200`](https://basescan.org/address/0x0E9D6Ceb6D313b021622C121Cda9C62e86e60200#code) | + +## Actions + +| Action | Description | +| --- | --- | +| `buy_gblin` | Buy GBLIN with ETH. Reads `quoteBuy` from the Lens and calls `buyGBLIN` on the vault with a slippage-bounded minimum output. | +| `sell_gblin_for_eth` | Redeem GBLIN back to ETH (e.g. to fund an x402 payment). Reads `quoteSell` from the Lens, approves the shares to the Zap if needed, and calls `GBLINZap.sellGBLINForEth`, which redeems in kind and sells every leg, all or nothing. | +| `get_gblin_state` | Read the ETH value of one GBLIN at NAV, the total supply, and whether the vault reports its NAV as reliable. | + +Every state-changing action derives its minimum output from an on-chain quote, and both +refuse to trade while the vault reports its NAV as not reliable. + +## Network support + +Base mainnet (`base-mainnet`) only. + +## Example + +```typescript +import { gblinActionProvider } from "@coinbase/agentkit"; + +const provider = gblinActionProvider(); +``` + +## Notes + +- Fees: 0.10% on mint and a 0.50% yearly management fee accrued as new shares; redemption pays no protocol fee. +- The vault enforces a 20-second redemption cooldown after a mint for oneself. +- The vault is owned by a 48-hour timelock; its parameters can change through it. diff --git a/typescript/agentkit/src/action-providers/gblin/constants.ts b/typescript/agentkit/src/action-providers/gblin/constants.ts new file mode 100644 index 000000000..bf2a18fa8 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/constants.ts @@ -0,0 +1,113 @@ +/** + * GBLIN vault (Global Balanced Liquidity Index) on Base mainnet. The vault is the ERC-20 share token. + * Verified source: https://basescan.org/address/0xc2181d975c05c8c724b334bcED0764c0b86B1D53#code + */ +export const GBLIN_ADDRESS = "0xc2181d975c05c8c724b334bcED0764c0b86B1D53"; + +/** + * GBLIN Lens: read-only quotes and state for the vault. + */ +export const GBLIN_LENS_ADDRESS = "0xfCFea8027019E8551A1f09AD91532471F5D26f61"; + +/** + * GBLIN Zap: redeems shares in kind and sells every basket leg for ETH in one transaction. + */ +export const GBLIN_ZAP_ADDRESS = "0x0E9D6Ceb6D313b021622C121Cda9C62e86e60200"; + +/** + * Uniswap V3 fee tier (0.05%) used to route every basket leg in the Zap exit. + */ +export const VENUE_FEE_TIER = 500; + +export const GBLIN_ABI = [ + { + type: "function", + name: "buyGBLIN", + stateMutability: "payable", + inputs: [{ name: "minOut", type: "uint256" }], + outputs: [], + }, + { + type: "function", + name: "isNavReliable", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "bool" }], + }, + { + type: "function", + name: "totalSupply", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "allowance", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "approve", + stateMutability: "nonpayable", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "", type: "bool" }], + }, +] as const; + +export const GBLIN_LENS_ABI = [ + { + type: "function", + name: "quoteBuy", + stateMutability: "view", + inputs: [ + { name: "vault", type: "address" }, + { name: "ethValue", type: "uint256" }, + ], + outputs: [ + { name: "out", type: "uint256" }, + { name: "protocolFee", type: "uint256" }, + { name: "stabilityFee", type: "uint256" }, + ], + }, + { + type: "function", + name: "quoteSell", + stateMutability: "view", + inputs: [ + { name: "vault", type: "address" }, + { name: "gblinAmount", type: "uint256" }, + ], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "basketLength", + stateMutability: "view", + inputs: [{ name: "vault", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; + +export const GBLIN_ZAP_ABI = [ + { + type: "function", + name: "sellGBLINForEth", + stateMutability: "nonpayable", + inputs: [ + { name: "shares", type: "uint256" }, + { name: "minEthOut", type: "uint256" }, + { name: "venueData", type: "bytes[]" }, + { name: "receiver", type: "address" }, + ], + outputs: [{ name: "ethOut", type: "uint256" }], + }, +] as const; diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts new file mode 100644 index 000000000..413148a13 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.test.ts @@ -0,0 +1,195 @@ +import { encodeAbiParameters, encodeFunctionData, parseEther, parseUnits } from "viem"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { GblinActionProvider } from "./gblinActionProvider"; +import { + GBLIN_ABI, + GBLIN_ADDRESS, + GBLIN_LENS_ADDRESS, + GBLIN_ZAP_ABI, + GBLIN_ZAP_ADDRESS, +} from "./constants"; + +const MOCK_TX_HASH = "0xabcdef1234567890"; +const MOCK_RECEIPT = { status: "success", blockNumber: 1234567n }; +const MOCK_ADDRESS = "0x9876543210987654321098765432109876543210"; +const BPS = 10_000n; +const DEFAULT_SLIPPAGE_BPS = 100n; +const VENUE = encodeAbiParameters([{ type: "uint24" }], [500]); + +describe("GBLIN Action Provider", () => { + const actionProvider = new GblinActionProvider(); + let mockWallet: jest.Mocked; + + beforeEach(() => { + mockWallet = { + getAddress: jest.fn().mockReturnValue(MOCK_ADDRESS), + getNetwork: jest.fn().mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet" }), + sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH as `0x${string}`), + waitForTransactionReceipt: jest.fn().mockResolvedValue(MOCK_RECEIPT), + readContract: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe("buyGblin", () => { + it("should buy GBLIN on the vault with a quote-derived minOut", async () => { + const expectedOut = parseUnits("0.03", 18); + const minOut = (expectedOut * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; + mockWallet.readContract + .mockResolvedValueOnce(true) // isNavReliable + .mockResolvedValueOnce([expectedOut, 0n, 0n]); // Lens.quoteBuy + + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + + expect(mockWallet.readContract).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + address: GBLIN_LENS_ADDRESS, + functionName: "quoteBuy", + args: [GBLIN_ADDRESS, parseEther("0.1")], + }), + ); + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: GBLIN_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "buyGBLIN", + args: [minOut], + }), + value: parseEther("0.1"), + }); + expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should report a reverted purchase as an error", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) + .mockResolvedValueOnce([10n ** 16n, 0n, 0n]); + mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({ status: "reverted" }); + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(response).toContain("reverted"); + }); + + it("should reject a non-positive amount", async () => { + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0" }); + expect(response).toContain("must be greater than 0"); + }); + + it("should not buy while the NAV is not reliable", async () => { + mockWallet.readContract.mockResolvedValueOnce(false); + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(response).toContain("not reliable"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should handle a zero on-chain quote", async () => { + mockWallet.readContract.mockResolvedValueOnce(true).mockResolvedValueOnce([0n, 0n, 0n]); + const response = await actionProvider.buyGblin(mockWallet, { ethAmount: "0.1" }); + expect(response).toContain("zero"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + }); + }); + + describe("sellGblinForEth", () => { + const shares = parseUnits("5", 18); + const expectedEth = parseEther("0.2"); + const minEthOut = (expectedEth * (BPS - DEFAULT_SLIPPAGE_BPS)) / BPS; + const zapCall = { + to: GBLIN_ZAP_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ZAP_ABI, + functionName: "sellGBLINForEth", + args: [shares, minEthOut, [VENUE, VENUE, VENUE], MOCK_ADDRESS], + }), + }; + + it("should approve the Zap and redeem through it when the allowance is short", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) // isNavReliable + .mockResolvedValueOnce(expectedEth) // Lens.quoteSell + .mockResolvedValueOnce(3n) // Lens.basketLength + .mockResolvedValueOnce(0n); // allowance + + const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + + expect(mockWallet.sendTransaction).toHaveBeenNthCalledWith(1, { + to: GBLIN_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "approve", + args: [GBLIN_ZAP_ADDRESS, shares], + }), + }); + expect(mockWallet.sendTransaction).toHaveBeenNthCalledWith(2, zapCall); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should skip the approval when the allowance already covers the shares", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(expectedEth) + .mockResolvedValueOnce(3n) + .mockResolvedValueOnce(shares); + + await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); + expect(mockWallet.sendTransaction).toHaveBeenCalledWith(zapCall); + }); + + it("should not sell while the NAV is not reliable", async () => { + mockWallet.readContract.mockResolvedValueOnce(false); + const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + expect(response).toContain("not reliable"); + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should handle errors when redeeming", async () => { + mockWallet.readContract + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(expectedEth) + .mockResolvedValueOnce(3n) + .mockResolvedValueOnce(shares); + mockWallet.sendTransaction.mockRejectedValue(new Error("Failed")); + const response = await actionProvider.sellGblinForEth(mockWallet, { gblinAmount: "5" }); + expect(response).toContain("Error redeeming GBLIN"); + }); + }); + + describe("getGblinState", () => { + it("should return live state as JSON", async () => { + mockWallet.readContract + .mockResolvedValueOnce(parseEther("0.04")) // Lens.quoteSell(1) + .mockResolvedValueOnce(parseUnits("0.5", 18)) // totalSupply + .mockResolvedValueOnce(true); // isNavReliable + + const response = await actionProvider.getGblinState(mockWallet, {}); + const parsed = JSON.parse(response); + expect(parsed.contract).toBe(GBLIN_ADDRESS); + expect(parsed.network).toBe("base-mainnet"); + expect(parsed.ethValuePerGblin).toBe("0.04"); + expect(parsed.totalSupply).toBe("0.5"); + expect(parsed.navReliable).toBe(true); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for Base Mainnet", () => { + expect( + actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "base-mainnet" }), + ).toBe(true); + }); + + it("should return false for other EVM networks", () => { + expect(actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum" })).toBe( + false, + ); + }); + + it("should return false for non-EVM networks", () => { + expect( + actionProvider.supportsNetwork({ protocolFamily: "bitcoin", networkId: "base-mainnet" }), + ).toBe(false); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts new file mode 100644 index 000000000..9c7de8eaa --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/gblinActionProvider.ts @@ -0,0 +1,295 @@ +import { z } from "zod"; +import { Decimal } from "decimal.js"; +import { + encodeAbiParameters, + encodeFunctionData, + formatEther, + formatUnits, + Hex, + parseEther, + parseUnits, +} from "viem"; +import { ActionProvider } from "../actionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + GBLIN_ABI, + GBLIN_ADDRESS, + GBLIN_LENS_ABI, + GBLIN_LENS_ADDRESS, + GBLIN_ZAP_ABI, + GBLIN_ZAP_ADDRESS, + VENUE_FEE_TIER, +} from "./constants"; +import { BuyGblinSchema, SellGblinForEthSchema, GetTreasuryStateSchema } from "./schemas"; + +const SUPPORTED_NETWORKS = ["base-mainnet"]; + +const DEFAULT_SLIPPAGE_BPS = 100n; // 1% +const BPS = 10_000n; + +/** + * GblinActionProvider lets an agent hold and redeem GBLIN, a collateral-backed treasury index + * on Base (cbBTC, WETH and USDC) whose basket weights are reduced on-chain when an asset draws + * down. The vault mints at NAV and never swaps; the exit to ETH goes through the GBLIN Zap. + * Every state-changing action sets a minimum output derived from the on-chain quote. + * + * Vault (verified): https://basescan.org/address/0xc2181d975c05c8c724b334bcED0764c0b86B1D53 + */ +export class GblinActionProvider extends ActionProvider { + /** + * Constructor for the GblinActionProvider class. + */ + constructor() { + super("gblin", []); + } + + /** + * Buys GBLIN with ETH, protected by a min-out derived from the on-chain quote. + * + * @param wallet - The wallet instance to execute the transaction + * @param args - The input arguments for the action + * @returns A success message with transaction details or an error message + */ + @CreateAction({ + name: "buy_gblin", + description: ` +Buy GBLIN, a collateral-backed treasury index on Base (cbBTC, WETH, USDC), using ETH. +Use this to park surplus agent capital in managed crypto exposure. GBLIN is NOT a stablecoin: its NAV moves with BTC and ETH. + +It takes: +- ethAmount: amount of ETH to spend in whole units (e.g. "0.1") +- slippageBps: optional max slippage vs the on-chain quote, in basis points (default 100 = 1%) + +The action reads the quote from the GBLIN Lens and calls buyGBLIN on the vault with a safe minimum output. The mint fee is 0.10%; a 0.50% yearly management fee accrues as new shares. +`, + schema: BuyGblinSchema, + }) + async buyGblin(wallet: EvmWalletProvider, args: z.infer): Promise { + const eth = new Decimal(args.ethAmount); + if (eth.comparedTo(new Decimal(0)) != 1) { + return "Error: ethAmount must be greater than 0"; + } + + try { + const valueWei = parseEther(args.ethAmount); + const slippage = args.slippageBps != null ? BigInt(args.slippageBps) : DEFAULT_SLIPPAGE_BPS; + + const reliable = (await wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "isNavReliable", + args: [], + })) as boolean; + if (!reliable) { + return "Error: the vault reports its NAV as not reliable right now (a price feed or a basket balance is unavailable). Try again later."; + } + + const quote = (await wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "quoteBuy", + args: [GBLIN_ADDRESS as Hex, valueWei], + })) as readonly [bigint, bigint, bigint]; + + const expectedOut = quote[0]; + if (expectedOut <= 0n) { + return "Error: on-chain quote returned zero. Try again shortly."; + } + const minOut = (expectedOut * (BPS - slippage)) / BPS; + + const data = encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "buyGBLIN", + args: [minOut], + }); + + const txHash = await wallet.sendTransaction({ + to: GBLIN_ADDRESS as `0x${string}`, + data, + value: valueWei, + }); + const receipt = await wallet.waitForTransactionReceipt(txHash); + if (receipt?.status === "reverted") { + return `Error buying GBLIN: transaction ${txHash} reverted.`; + } + + return `Bought GBLIN with ${args.ethAmount} ETH (min out ${formatUnits(minOut, 18)} GBLIN, expected ${formatUnits(expectedOut, 18)}). Transaction hash: ${txHash}`; + } catch (error) { + return `Error buying GBLIN: ${error}`; + } + } + + /** + * Redeems GBLIN back to ETH through the GBLIN Zap, protected by a min-out derived from the + * on-chain quote. + * + * @param wallet - The wallet instance to execute the transaction + * @param args - The input arguments for the action + * @returns A success message with transaction details or an error message + */ + @CreateAction({ + name: "sell_gblin_for_eth", + description: ` +Redeem GBLIN back to ETH (e.g. to free capital for an x402 payment). + +It takes: +- gblinAmount: amount of GBLIN to redeem in whole units (e.g. "5") +- slippageBps: optional max slippage vs the on-chain NAV quote, in basis points (default 100 = 1%) + +The GBLIN Zap redeems the shares in kind and sells every basket leg for ETH, all or nothing. If the shares are not yet approved to the Zap, the action first sends an approval. +Note: the vault enforces a 20-second redemption cooldown after a mint for oneself; if you just bought, wait before selling. +`, + schema: SellGblinForEthSchema, + }) + async sellGblinForEth( + wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + const amount = new Decimal(args.gblinAmount); + if (amount.comparedTo(new Decimal(0)) != 1) { + return "Error: gblinAmount must be greater than 0"; + } + + try { + const shares = parseUnits(args.gblinAmount, 18); + const slippage = args.slippageBps != null ? BigInt(args.slippageBps) : DEFAULT_SLIPPAGE_BPS; + const owner = (await wallet.getAddress()) as Hex; + + const reliable = (await wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "isNavReliable", + args: [], + })) as boolean; + if (!reliable) { + return "Error: the vault reports its NAV as not reliable right now (a price feed or a basket balance is unavailable). Try again later."; + } + + const expectedEth = (await wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "quoteSell", + args: [GBLIN_ADDRESS as Hex, shares], + })) as bigint; + if (expectedEth <= 0n) { + return "Error: on-chain quote returned zero. Try again shortly."; + } + const minEthOut = (expectedEth * (BPS - slippage)) / BPS; + + const rows = (await wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "basketLength", + args: [GBLIN_ADDRESS as Hex], + })) as bigint; + const venue = encodeAbiParameters([{ type: "uint24" }], [VENUE_FEE_TIER]); + const venueData = Array.from({ length: Number(rows) }, () => venue); + + const allowance = (await wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "allowance", + args: [owner, GBLIN_ZAP_ADDRESS as Hex], + })) as bigint; + if (allowance < shares) { + const approveHash = await wallet.sendTransaction({ + to: GBLIN_ADDRESS as `0x${string}`, + data: encodeFunctionData({ + abi: GBLIN_ABI, + functionName: "approve", + args: [GBLIN_ZAP_ADDRESS as Hex, shares], + }), + }); + const approval = await wallet.waitForTransactionReceipt(approveHash); + if (approval?.status === "reverted") { + return `Error redeeming GBLIN: approval ${approveHash} reverted.`; + } + } + + const data = encodeFunctionData({ + abi: GBLIN_ZAP_ABI, + functionName: "sellGBLINForEth", + args: [shares, minEthOut, venueData, owner], + }); + + const txHash = await wallet.sendTransaction({ + to: GBLIN_ZAP_ADDRESS as `0x${string}`, + data, + }); + const receipt = await wallet.waitForTransactionReceipt(txHash); + if (receipt?.status === "reverted") { + return `Error redeeming GBLIN: transaction ${txHash} reverted.`; + } + + return `Redeemed ${args.gblinAmount} GBLIN for at least ${formatEther(minEthOut)} ETH. Transaction hash: ${txHash}`; + } catch (error) { + return `Error redeeming GBLIN: ${error}`; + } + } + + /** + * Reads live GBLIN state (per-share ETH value, supply and NAV reliability). + * + * @param wallet - The wallet instance used for on-chain reads + * @param _ - Empty args + * @returns A JSON string with the vault state or an error message + */ + @CreateAction({ + name: "get_gblin_state", + description: + "Read live GBLIN state: ETH value of one GBLIN at NAV (from the GBLIN Lens), total supply, and whether the vault reports its NAV as reliable. Use before buying or redeeming.", + schema: GetTreasuryStateSchema, + }) + async getGblinState( + wallet: EvmWalletProvider, + _: z.infer, + ): Promise { + try { + const oneGblin = parseUnits("1", 18); + const [ethPerGblin, supply, navReliable] = await Promise.all([ + wallet.readContract({ + address: GBLIN_LENS_ADDRESS as Hex, + abi: GBLIN_LENS_ABI, + functionName: "quoteSell", + args: [GBLIN_ADDRESS as Hex, oneGblin], + }) as Promise, + wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "totalSupply", + args: [], + }) as Promise, + wallet.readContract({ + address: GBLIN_ADDRESS as Hex, + abi: GBLIN_ABI, + functionName: "isNavReliable", + args: [], + }) as Promise, + ]); + + return JSON.stringify({ + contract: GBLIN_ADDRESS, + network: "base-mainnet", + ethValuePerGblin: formatEther(ethPerGblin), + totalSupply: formatUnits(supply, 18), + navReliable, + note: "Managed crypto exposure (cbBTC, WETH, USDC) with on-chain drawdown protection; not a stablecoin.", + }); + } catch (error) { + return `Error reading GBLIN state: ${error}`; + } + } + + /** + * Checks if the GBLIN action provider supports the given network. + * + * @param network - The network to check. + * @returns True if supported (Base mainnet), false otherwise. + */ + supportsNetwork = (network: Network) => + network.protocolFamily === "evm" && SUPPORTED_NETWORKS.includes(network.networkId!); +} + +export const gblinActionProvider = () => new GblinActionProvider(); diff --git a/typescript/agentkit/src/action-providers/gblin/index.ts b/typescript/agentkit/src/action-providers/gblin/index.ts new file mode 100644 index 000000000..25157ec30 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/index.ts @@ -0,0 +1,2 @@ +export * from "./gblinActionProvider"; +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/gblin/schemas.ts b/typescript/agentkit/src/action-providers/gblin/schemas.ts new file mode 100644 index 000000000..ed7be81c2 --- /dev/null +++ b/typescript/agentkit/src/action-providers/gblin/schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +/** + * Input schema for buying GBLIN with ETH. + */ +export const BuyGblinSchema = z + .object({ + ethAmount: z + .string() + .describe("Amount of ETH to spend, in whole units (e.g. '0.1' for 0.1 ETH)"), + slippageBps: z + .number() + .int() + .min(0) + .max(2000) + .optional() + .describe("Max slippage in basis points applied to the on-chain quote. Default 100 (1%)."), + }) + .strip() + .describe("Instructions for buying GBLIN with ETH"); + +/** + * Input schema for redeeming GBLIN back to ETH. + */ +export const SellGblinForEthSchema = z + .object({ + gblinAmount: z + .string() + .describe("Amount of GBLIN to redeem, in whole units (e.g. '5' for 5 GBLIN)"), + slippageBps: z + .number() + .int() + .min(0) + .max(2000) + .optional() + .describe("Max slippage in basis points applied to the on-chain quote. Default 100 (1%)."), + }) + .strip() + .describe("Instructions for redeeming GBLIN back to ETH"); + +/** + * Input schema for reading GBLIN treasury state (no arguments). + */ +export const GetTreasuryStateSchema = z + .object({}) + .strip() + .describe("Read live GBLIN state (ETH value per share, supply, NAV reliability)"); diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..d6088292f 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -17,6 +17,7 @@ export * from "./erc20"; export * from "./erc721"; export * from "./erc8004"; export * from "./farcaster"; +export * from "./gblin"; export * from "./jupiter"; export * from "./messari"; export * from "./pyth";