Skip to content
Closed
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
29 changes: 29 additions & 0 deletions examples/with-vaultsfyi/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Turnkey root user (used to create policies)
TURNKEY_API_PUBLIC_KEY="<root API public key (starts with 02 or 03)>"
TURNKEY_API_PRIVATE_KEY="<root API private key>"
TURNKEY_BASE_URL="https://api.turnkey.com"
TURNKEY_ORGANIZATION_ID="<organization ID>"

## Turnkey non-root user (used to sign vault transactions)
NONROOT_USER_ID="<non-root user ID>"
NONROOT_API_PUBLIC_KEY="<non-root API public key>"
NONROOT_API_PRIVATE_KEY="<non-root API private key>"

## Wallet address held by the Turnkey wallet (Base mainnet)
SIGN_WITH="0x..."

## Base mainnet RPC URL
RPC_URL="https://base-mainnet.infura.io/v3/<your-key>"

## vaults.fyi API key (https://portal.vaults.fyi)
VAULTS_FYI_API_KEY="<vaults.fyi API key>"

## Vault selection (defaults below target Steakhouse USDC on Base via Morpho)
## This example targets Base mainnet. To target a different network, change
## the chain import in the source files (deposit.ts, redeem.ts, claimRewards.ts)
## and the network: "base" literal passed to the vaults.fyi SDK calls.
ASSET_ADDRESS="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
VAULT_ID="<vaultId from /v2/portfolio/best-deposit-options or /v2/detailed-vaults>"

## Deposit amount in base units (10000000 = 10 USDC at 6 decimals)
DEPOSIT_AMOUNT="10000000"
109 changes: 109 additions & 0 deletions examples/with-vaultsfyi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Example: `with-vaultsfyi`

[vaults.fyi](https://vaults.fyi) is the infrastructure layer for DeFi yield. One API gives you discovery, ready-to-sign transaction payloads, and position tracking across 80+ protocols and 1,000+ yield strategies on Ethereum, Base, Arbitrum, Optimism, Polygon, and 15+ other networks.

This example shows how to use Turnkey to sign vaults.fyi transactions on Base Mainnet under a policy that restricts the signer to the contracts vaults.fyi will actually target. It provides the following scripts:

- `discover.ts` lists the top vaults vaults.fyi recommends for the user's wallet, ranked by APY across every supported protocol.
- `deposit.ts` deposits into a chosen vault by fetching the ordered transaction list from vaults.fyi (typically approve + deposit) and signing each step.
- `balance.ts` lists every vault position the user holds across every supported network and protocol, including positions opened outside this app.
- `redeem.ts` redeems the full position from a vault.
- `claimRewards.ts` claims every available reward on the configured network using the two-step rewards/context → rewards/claim flow.

On top of this we showcase the Turnkey policy engine restricting the non-root user to only the addresses vaults.fyi will actually target:

- `createPolicy.ts` runs a dry-run deposit and redeem against vaults.fyi, extracts the actual `tx.to` addresses from the responses, and creates a Turnkey policy that allowlists exactly those addresses. This works for ERC-4626 vaults that target the vault contract directly (Morpho, Aave, Euler) and for protocols that route through intermediary contracts (Veda Boring Vaults via a Teller, queue-based redemptions, etc.).

## Why one address allowlist instead of per-function-selector policies

Other yield-routing APIs require enumerating function selectors per protocol because the calldata they return varies. vaults.fyi handles all protocol-specific encoding internally, so an address allowlist is sufficient. The dry-run pattern in `createPolicy.ts` discovers the right addresses for any vault without you needing to hardcode them.

## Getting started

### 1/ Cloning the example

Make sure you have `Node.js` installed locally; we recommend using Node v20+.

```bash
$ git clone https://github.com/tkhq/sdk
$ cd sdk/
$ corepack enable # Install `pnpm`
$ pnpm install -r # Install dependencies
$ pnpm run build-all # Compile source code
$ cd examples/with-vaultsfyi/
```

### 2/ Setting up Turnkey

Follow the [Quickstart](https://docs.turnkey.com/getting-started/quickstart) and create:

- A root user with a public/private API key pair within the Turnkey parent organization
- An organization ID
- A non-root user with a separate API key, removed from the root quorum (see [updateRootQuorum.ts](https://github.com/tkhq/sdk/blob/main/examples/kitchen-sink/src/sdk-server/updateRootQuorum.ts))
- A wallet with an Ethereum account, funded with ETH for gas and USDC on Base mainnet

### 3/ Get a vaults.fyi API key

Sign up at the [vaults.fyi portal](https://portal.vaults.fyi).

### 4/ Configure environment

```bash
cp .env.local.example .env.local
```

Fill in the values:

- `TURNKEY_API_PUBLIC_KEY`, `TURNKEY_API_PRIVATE_KEY`, `TURNKEY_BASE_URL`, `TURNKEY_ORGANIZATION_ID`
- `NONROOT_USER_ID`, `NONROOT_API_PUBLIC_KEY`, `NONROOT_API_PRIVATE_KEY`
- `SIGN_WITH` (your Turnkey wallet's Base address)
- `RPC_URL` (a Base mainnet RPC URL)
- `VAULTS_FYI_API_KEY`
- `NETWORK`, `ASSET_ADDRESS`, `VAULT_ID`, `DEPOSIT_AMOUNT`

### 5/ Discover a vault to deposit into

```bash
pnpm discover
```

Pick a `vaultId` from the output and set it as `VAULT_ID` in `.env.local`.

### 6/ Set up the Turnkey policy for the chosen vault

```bash
pnpm createPolicy
```

This dry-runs deposit and redeem against vaults.fyi, extracts the target addresses, and creates a single allow-policy.

### 7/ Deposit

```bash
pnpm deposit
```

### 8/ Check positions

```bash
pnpm balance
```

### 9/ Redeem the full position

```bash
pnpm redeem
```

### 10/ Claim rewards

```bash
pnpm claimRewards
```

Note: reward claim transactions may target different contracts than deposit and redeem. If they do, you'll need to extend the policy or attach a separate one. The same dry-run pattern from `createPolicy.ts` works against `getRewardsClaimActions` to discover those addresses.

## Resources

- [Turnkey cookbook entry for vaults.fyi](https://docs.turnkey.com/cookbook/vaultsfyi)
- [vaults.fyi docs](https://docs.vaults.fyi) and [OpenAPI spec](https://api.vaults.fyi/v2/documentation/)
23 changes: 23 additions & 0 deletions examples/with-vaultsfyi/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@turnkey/with-vaultsfyi",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "pnpm -w run build-all",
"createPolicy": "tsx src/createPolicy.ts",
"discover": "tsx src/discover.ts",
"deposit": "tsx src/deposit.ts",
"balance": "tsx src/balance.ts",
"redeem": "tsx src/redeem.ts",
"claimRewards": "tsx src/claimRewards.ts",
"clean": "rimraf ./dist ./.cache",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@turnkey/sdk-server": "workspace:*",
"@turnkey/viem": "workspace:*",
"@vaultsfyi/sdk": "^2.3.1",
"dotenv": "^16.0.3",
"viem": "^2.24.2"
}
}
50 changes: 50 additions & 0 deletions examples/with-vaultsfyi/src/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Lists every vault position the user holds across every supported network and
* protocol. vaults.fyi reads on-chain state directly, so positions opened
* outside this app (e.g. through MetaMask, Phantom, or any other wallet) are
* included.
*
* Run with: pnpm balance
*/

import * as path from "path";
import * as dotenv from "dotenv";
import { VaultsSdk } from "@vaultsfyi/sdk";

dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

async function main() {
const vaultsFyi = new VaultsSdk({ apiKey: process.env.VAULTS_FYI_API_KEY! });
const userAddress = process.env.SIGN_WITH!;

const { data: positions } = await vaultsFyi.getPositions({
path: { userAddress },
});

if (positions.length === 0) {
console.log(`No vault positions found for ${userAddress}.`);
return;
}

console.log(`Positions for ${userAddress}:\n`);
for (const p of positions) {
const apyPct = (p.apy.total * 100).toFixed(2);
const balance =
p.asset.balanceUsd ??
`${p.asset.balanceNative ?? "?"} ${p.asset.symbol}`;
console.log(` ${p.protocol.name} / ${p.name} (${p.network.name})`);
console.log(
` balance: ${balance}${p.asset.balanceUsd ? " USD" : ""}`,
);
console.log(` APY: ${apyPct}%`);
if (p.asset.unclaimedUsd) {
console.log(` unclaimed rewards: ${p.asset.unclaimedUsd} USD`);
}
console.log(` vaultId: ${p.vaultId}\n`);
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
111 changes: 111 additions & 0 deletions examples/with-vaultsfyi/src/claimRewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Claims every available reward on the configured network in two steps:
* 1. getRewardsTransactionsContext returns claimable rewards keyed by
* network, each with a unique claimId.
* 2. getRewardsClaimActions takes the claimIds and returns ready-to-sign
* transactions per network.
*
* Note: reward claim transactions may target different contracts than the
* deposit/redeem flows. If you scoped the policy in createPolicy.ts to only
* deposit/redeem targets, you'll need to extend or add a policy with the
* reward target addresses before this can succeed.
*
* Run with: pnpm claimRewards
*/

import * as path from "path";
import * as dotenv from "dotenv";
import { base } from "viem/chains";
import { createAccount } from "@turnkey/viem";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
import {
createWalletClient,
createPublicClient,
http,
type Account,
} from "viem";
import { VaultsSdk } from "@vaultsfyi/sdk";

dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

async function main() {
const turnkeyClient = new TurnkeyServerSDK({
apiBaseUrl: process.env.TURNKEY_BASE_URL!,
apiPrivateKey: process.env.NONROOT_API_PRIVATE_KEY!,
apiPublicKey: process.env.NONROOT_API_PUBLIC_KEY!,
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
});

const turnkeyAccount = await createAccount({
client: turnkeyClient.apiClient(),
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
signWith: process.env.SIGN_WITH!,
});

const walletClient = createWalletClient({
account: turnkeyAccount as Account,
chain: base,
transport: http(process.env.RPC_URL!),
});

const publicClient = createPublicClient({
chain: base,
transport: http(process.env.RPC_URL!),
});

const vaultsFyi = new VaultsSdk({ apiKey: process.env.VAULTS_FYI_API_KEY! });

const userAddress = (turnkeyAccount as Account).address;
const targetNetwork = "base" as const;

const context = await vaultsFyi.getRewardsTransactionsContext({
path: { userAddress },
});
const networkRewards = context.claimable[targetNetwork] ?? [];

if (networkRewards.length === 0) {
console.log(`No claimable rewards on ${targetNetwork}.`);
return;
}

console.log(
`Found ${networkRewards.length} claimable reward(s) on ${targetNetwork}:`,
);
for (const reward of networkRewards) {
const sources = reward.sources.map((s) => s.protocol.name).join(", ");
console.log(
` - ${reward.asset.claimableAmount} ${reward.asset.symbol} from ${sources} (claimId: ${reward.claimId})`,
);
}

const claimIds = networkRewards.map((r) => r.claimId);
const claim = await vaultsFyi.getRewardsClaimActions({
path: { userAddress },
query: { claimIds },
});
const networkClaim = claim[targetNetwork];

if (!networkClaim || networkClaim.actions.length === 0) {
console.log("vaults.fyi returned no claim transactions.");
return;
}

const remaining = networkClaim.actions.slice(networkClaim.currentActionIndex);
console.log(`\nExecuting ${remaining.length} claim transaction(s):`);
for (const step of remaining) {
const hash = await walletClient.sendTransaction({
to: step.tx.to as `0x${string}`,
data: step.tx.data as `0x${string}` | undefined,
value: step.tx.value ? BigInt(step.tx.value) : undefined,
});
await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 });
console.log(` ${step.name}: https://basescan.org/tx/${hash}`);
}

console.log("\nClaim complete.");
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Loading