Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/.changeset/sapien-vault-action-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added a Sapien Vault action provider for depositing SAPIEN, withdrawing or redeeming vSAPIEN, transferring shares, and reading vault totals and tranche position state on the Base mainnet ERC-4626 vault
29 changes: 29 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,35 @@ const agent = createAgent({
</table>
</details>
<details>
<summary><strong>Sapien Vault</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>deposit</code></td>
<td width="768">Deposits SAPIEN into the Sapien Vault (vSAPIEN) on Base mainnet, approving the vault internally only when needed.</td>
</tr>
<tr>
<td width="200"><code>withdraw</code></td>
<td width="768">Withdraws SAPIEN from the Sapien Vault by asset amount (ERC-4626 withdraw).</td>
</tr>
<tr>
<td width="200"><code>redeem</code></td>
<td width="768">Redeems vSAPIEN shares from the Sapien Vault for SAPIEN (ERC-4626 redeem).</td>
</tr>
<tr>
<td width="200"><code>transfer</code></td>
<td width="768">Transfers matured, unlocked vSAPIEN shares to a destination address.</td>
</tr>
<tr>
<td width="200"><code>get_position</code></td>
<td width="768">Reads vSAPIEN shares, SAPIEN assets, matured/pending tranches, available balance, and locked stake.</td>
</tr>
<tr>
<td width="200"><code>get_vault_totals</code></td>
<td width="768">Reads vault totalAssets and total shares (totalSupply).</td>
</tr>
</table>
</details>
<details>
<summary><strong>Superfluid</strong></summary>
<table width="100%">
<tr>
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export * from "./zerion";
export * from "./zerodev";
export * from "./zeroX";
export * from "./zora";
export * from "./sapienVault";
54 changes: 54 additions & 0 deletions typescript/agentkit/src/action-providers/sapienVault/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Sapien Vault Action Provider

This directory contains the **SapienVaultActionProvider**, which lets AgentKit agents deposit SAPIEN, withdraw or redeem vSAPIEN, transfer shares, and read vault totals and tranche position state on the Sapien Vault — an ERC-4626 vault on Base mainnet.

App: [https://vault.sapien.io](https://vault.sapien.io)

## Directory Structure

```
sapienVault/
├── sapienVaultActionProvider.ts # Main provider
├── sapienVaultActionProvider.test.ts # Unit tests
├── constants.ts # Base mainnet addresses and vault ABI
├── schemas.ts # Zod action schemas
├── utils.ts # Network guard and internal approve-if-needed
├── index.ts # Exports
└── README.md # This file
```

## Contracts (Base mainnet, EIP-55)

| Role | Address |
| --- | --- |
| Vault / vSAPIEN (ERC-4626) | `0x60Bf63729f688287a450299962b36Cef0aFfaa42` |
| Underlying SAPIEN | `0xC729777d0470F30612B1564Fd96E8Dd26f5814E3` |

## Actions

- `deposit`: ERC-4626 `deposit`. Approves SAPIEN internally only when allowance is insufficient (not a public action)
- `withdraw`: Exit by **asset** amount (`withdraw`)
- `redeem`: Exit by **share** amount (`redeem`)
- `transfer`: ERC-20 `transfer` of vSAPIEN shares to a destination (matured, unlocked shares only)
- `get_position`: Shares, assets, matured/pending tranches, available balance, locked stake, and `depositAgeStatus`
- `get_vault_totals`: Vault `totalAssets` and total shares (`totalSupply`)

There is no standalone `approve` action. ERC-20 approval is an implementation detail of `deposit`.

## Network Support

**Base mainnet only** (`networkId` `base-mainnet` or `chainId` `8453`).

`supportsNetwork` returns `false` on every other chain. Each action also returns a clear error if the connected wallet is not on Base mainnet (including Base Sepolia).

## Adding New Actions

1. Define the schema in `schemas.ts`
2. Implement the action in `sapienVaultActionProvider.ts`
3. Add tests in `sapienVaultActionProvider.test.ts`

## Notes

- Fresh deposits may be subject to `minDepositAge` before they can be withdrawn or transferred. Locked validator stake cannot be withdrawn or transferred until the engine unlocks it. Actions consult `maxDeposit` / `maxWithdraw` / `maxRedeem` / `maturedShares` / `availableBalance`.
- vSAPIEN share decimals are the underlying decimals plus an internal ERC-4626 offset. Share-denominated actions use the vault's `decimals()`.
- For protocol design, see the [SapienVault docs](https://github.com/Sapien-io/sapien-contracts/blob/main/docs/SapienVault.md).
186 changes: 186 additions & 0 deletions typescript/agentkit/src/action-providers/sapienVault/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Sapien Vault contracts on Base mainnet (EIP-55).
*
* @see https://vault.sapien.io
*/
export const SAPIEN_VAULT_ADDRESS = "0x60Bf63729f688287a450299962b36Cef0aFfaa42";
export const SAPIEN_TOKEN_ADDRESS = "0xC729777d0470F30612B1564Fd96E8Dd26f5814E3";

export const SAPIEN_VAULT_APP_URL = "https://vault.sapien.io";

export const SAPIEN_VAULT_NETWORK_ID = "base-mainnet";
export const SAPIEN_VAULT_CHAIN_ID = "8453";

/**
* Sapien Vault ABI: ERC-4626 + tranche / age views + ERC-20 share transfer.
*
* View signatures match ISapienVault / the live vault used by vault.sapien.io.
*/
export const SAPIEN_VAULT_ABI = [
{
type: "function",
name: "deposit",
inputs: [
{ name: "assets", type: "uint256", internalType: "uint256" },
{ name: "receiver", type: "address", internalType: "address" },
],
outputs: [{ name: "shares", type: "uint256", internalType: "uint256" }],
stateMutability: "nonpayable",
},
{
type: "function",
name: "withdraw",
inputs: [
{ name: "assets", type: "uint256", internalType: "uint256" },
{ name: "receiver", type: "address", internalType: "address" },
{ name: "owner", type: "address", internalType: "address" },
],
outputs: [{ name: "shares", type: "uint256", internalType: "uint256" }],
stateMutability: "nonpayable",
},
{
type: "function",
name: "redeem",
inputs: [
{ name: "shares", type: "uint256", internalType: "uint256" },
{ name: "receiver", type: "address", internalType: "address" },
{ name: "owner", type: "address", internalType: "address" },
],
outputs: [{ name: "assets", type: "uint256", internalType: "uint256" }],
stateMutability: "nonpayable",
},
{
type: "function",
name: "transfer",
inputs: [
{ name: "to", type: "address", internalType: "address" },
{ name: "value", type: "uint256", internalType: "uint256" },
],
outputs: [{ name: "", type: "bool", internalType: "bool" }],
stateMutability: "nonpayable",
},
{
type: "function",
name: "balanceOf",
inputs: [{ name: "account", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "totalSupply",
inputs: [],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "totalAssets",
inputs: [],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "convertToAssets",
inputs: [{ name: "shares", type: "uint256", internalType: "uint256" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "convertToShares",
inputs: [{ name: "assets", type: "uint256", internalType: "uint256" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "decimals",
inputs: [],
outputs: [{ name: "", type: "uint8", internalType: "uint8" }],
stateMutability: "view",
},
{
type: "function",
name: "maxDeposit",
inputs: [{ name: "receiver", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "maxWithdraw",
inputs: [{ name: "owner", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "maxRedeem",
inputs: [{ name: "owner", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "previewDeposit",
inputs: [{ name: "assets", type: "uint256", internalType: "uint256" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "maturedShares",
inputs: [{ name: "user", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "pendingShares",
inputs: [{ name: "user", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "availableBalance",
inputs: [{ name: "user", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "assetsOf",
inputs: [{ name: "user", type: "address", internalType: "address" }],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "depositAgeStatus",
inputs: [{ name: "user", type: "address", internalType: "address" }],
outputs: [
{ name: "matured", type: "uint256", internalType: "uint256" },
{ name: "pending", type: "uint256", internalType: "uint256" },
{ name: "minAge", type: "uint256", internalType: "uint256" },
{ name: "nextMaturityRemaining", type: "uint256", internalType: "uint256" },
],
stateMutability: "view",
},
{
type: "function",
name: "getStakeAccount",
inputs: [{ name: "user", type: "address", internalType: "address" }],
outputs: [
{
name: "",
type: "tuple",
internalType: "struct StakeAccount",
components: [{ name: "lockedAmount", type: "uint256", internalType: "uint256" }],
},
],
stateMutability: "view",
},
] as const;
3 changes: 3 additions & 0 deletions typescript/agentkit/src/action-providers/sapienVault/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./constants";
export * from "./schemas";
export * from "./sapienVaultActionProvider";
Loading