> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Reading Earn vault totals onchain

> Compute deposit and withdrawal totals for a Privy wallet directly from Morpho and Aave vault events.

The [get vault position](/wallets/actions/earn/get-vault-position) endpoint returns `total_deposited` and `total_withdrawn` for a wallet. For all vault providers, Privy computes these totals from the deposits and withdrawals made through Privy Earn. Deposits and withdrawals made directly onchain are not counted.

This recipe shows how your app can read the same values directly from the vault contract for Morpho and Aave vaults. The onchain totals include every deposit and withdrawal for the wallet, no matter how it was sent.

<Info>
  Your app only needs this recipe if wallets interact with a vault outside the Privy Earn
  [deposit](/wallets/actions/earn/deposit) and [withdraw](/wallets/actions/earn/withdraw) endpoints.
  For example, your app sends a vault `deposit` call with `sendTransaction`, or users deposit from
  another app. If all activity goes through Privy Earn, your app can use the endpoint totals.
</Info>

## Resources

<CardGroup cols={2}>
  <Card title="Get vault position" icon="vault" href="/wallets/actions/earn/get-vault-position" arrow>
    The Privy endpoint that returns a wallet's vault position.
  </Card>

  <Card title="ERC-4626 standard" icon="arrow-up-right-from-square" href="https://eips.ethereum.org/EIPS/eip-4626" arrow>
    The tokenized vault standard that Morpho and Aave Earn vaults implement.
  </Card>
</CardGroup>

***

## Field sources

The table maps each field of the get vault position response to its source in the endpoint and onchain.

| Response field | Endpoint source | Onchain source |
| - | - | - |
| `shares_in_vault` | Live read from the vault contract | `balanceOf(wallet)` on the vault contract |
| `assets_in_vault` | Live read from the vault contract | `convertToAssets(shares_in_vault)` on the vault contract |
| `total_deposited` | Privy Earn deposits | Sum of `assets` in the vault's `Deposit` events where `receiver` is the wallet |
| `total_withdrawn` | Privy Earn withdrawals | Sum of `assets` in the vault's `Withdraw` events where `owner` is the wallet |

The endpoint values for `shares_in_vault` and `assets_in_vault` match the onchain reads. This recipe also shows how to read them onchain, so that your app reads all four values at the same block.

<Note>This recipe covers Morpho and Aave vaults.</Note>

## Prerequisites

* A Privy app with an Earn vault. See [Earn setup](/wallets/actions/earn/setup).
* Your app ID and app secret, for the Privy API.
* An RPC URL for the vault's chain. The chain ID is the number after `eip155:` in the vault's `caip2`.
* The block number at which the vault contract was deployed. Find the contract creation transaction for the vault address on a block explorer for the vault's chain.

Install the Privy Node SDK and `viem`:

```bash theme={"system"}
npm install @privy-io/node viem
```

## 1. Get the vault and wallet addresses

The onchain reads need two addresses: the vault contract address and the wallet address. Get the vault address and chain with the `vaultDetails` method, which calls the [get vault details](/wallets/actions/earn/get-vault-details) endpoint. Get the wallet address with the [get wallet](/wallets/wallets/get-a-wallet/get-wallet-by-id) method.

```typescript theme={"system"}
import {PrivyClient} from '@privy-io/node';

const privy = new PrivyClient({
  appId: 'insert-your-app-id',
  appSecret: 'insert-your-app-secret'
});

export async function getVaultAndWallet({vaultId, walletId}: {vaultId: string; walletId: string}) {
  const [vault, wallet] = await Promise.all([
    privy.wallets().earn().ethereum().vaultDetails(vaultId),
    privy.wallets().get(walletId)
  ]);

  return {
    vaultAddress: vault.vault_address as `0x${string}`,
    caip2: vault.caip2,
    assetDecimals: vault.asset.decimals,
    walletAddress: wallet.address as `0x${string}`
  };
}
```

Store the vault address, `caip2`, and asset decimals with the vault ID. These values do not change, so your app only needs to fetch them once per vault.

## 2. Read the position

The wallet's position is its vault share balance. `convertToAssets` returns the value of these shares in the underlying asset, including accrued yield. These two reads return the same values as `shares_in_vault` and `assets_in_vault`.

```typescript {skip-check} theme={"system"}
import {type Address, createPublicClient, erc4626Abi, http} from 'viem';

// Use an RPC URL for the vault's chain.
const publicClient = createPublicClient({transport: http('insert-your-rpc-url')});

export async function getPosition({
  vaultAddress,
  walletAddress,
  blockNumber
}: {
  vaultAddress: Address;
  walletAddress: Address;
  blockNumber?: bigint;
}) {
  const sharesInVault = await publicClient.readContract({
    address: vaultAddress,
    abi: erc4626Abi,
    functionName: 'balanceOf',
    args: [walletAddress],
    blockNumber
  });

  const assetsInVault =
    sharesInVault > 0n
      ? await publicClient.readContract({
          address: vaultAddress,
          abi: erc4626Abi,
          functionName: 'convertToAssets',
          args: [sharesInVault],
          blockNumber
        })
      : 0n;

  return {sharesInVault, assetsInVault};
}
```

The optional `blockNumber` reads the position at a past block. Step 4 uses it to keep the position and the totals at the same block.

## 3. Compute the totals from vault events

Every ERC-4626 vault emits a `Deposit` event for each deposit and a `Withdraw` event for each withdrawal. Both events include `assets`, the amount of the underlying asset in the token's smallest unit.

* `Deposit` indexes `receiver`, the address that receives the vault shares.
* `Withdraw` indexes `owner`, the address whose vault shares are burned.

Filter `Deposit` events by `receiver` and `Withdraw` events by `owner`, then sum `assets`. Most RPC providers limit the block range of one `eth_getLogs` request, so the function below scans the range in chunks.

```typescript theme={"system"}
import {type Address, createPublicClient, erc4626Abi, http} from 'viem';

// Use an RPC URL for the vault's chain.
const publicClient = createPublicClient({transport: http('insert-your-rpc-url')});

// Set this to the `eth_getLogs` block range limit of your RPC provider.
const MAX_BLOCK_RANGE = 10_000n;

export async function getEarnTotals({
  vaultAddress,
  walletAddress,
  fromBlock,
  toBlock
}: {
  vaultAddress: Address;
  walletAddress: Address;
  fromBlock: bigint;
  toBlock: bigint;
}) {
  let totalDeposited = 0n;
  let totalWithdrawn = 0n;

  for (let start = fromBlock; start <= toBlock; start += MAX_BLOCK_RANGE) {
    const end = start + MAX_BLOCK_RANGE - 1n < toBlock ? start + MAX_BLOCK_RANGE - 1n : toBlock;

    const [deposits, withdrawals] = await Promise.all([
      publicClient.getContractEvents({
        address: vaultAddress,
        abi: erc4626Abi,
        eventName: 'Deposit',
        args: {receiver: walletAddress},
        fromBlock: start,
        toBlock: end
      }),
      publicClient.getContractEvents({
        address: vaultAddress,
        abi: erc4626Abi,
        eventName: 'Withdraw',
        args: {owner: walletAddress},
        fromBlock: start,
        toBlock: end
      })
    ]);

    for (const log of deposits) {
      totalDeposited += log.args.assets ?? 0n;
    }
    for (const log of withdrawals) {
      totalWithdrawn += log.args.assets ?? 0n;
    }
  }

  return {totalDeposited, totalWithdrawn};
}
```

To get the lifetime totals, scan from the vault's deployment block to the latest block. A full scan is the simplest option, but its cost grows with the age of the vault. For each new read, step 4 scans only the blocks since the last read.

## 4. Store a checkpoint and scan new blocks

Store the totals and the last scanned block for each wallet and vault. On each read, scan only the blocks after the checkpoint, and add the new amounts to the stored totals.

```typescript {skip-check} theme={"system"}
type EarnTotalsCheckpoint = {
  totalDeposited: bigint;
  totalWithdrawn: bigint;
  lastScannedBlock: bigint;
};

// Number of block confirmations to wait before counting events. The most recent blocks stay
// unscanned, so that a chain reorganization is unlikely to change counted events.
// This value is an approximation. Some chains, such as Base, have no fixed number of blocks
// after which a block is final. Increase this value for a stronger guarantee.
const CONFIRMATIONS = 12n;

export async function syncEarnTotals({
  vaultAddress,
  walletAddress,
  checkpoint
}: {
  vaultAddress: Address;
  walletAddress: Address;
  checkpoint: EarnTotalsCheckpoint;
}): Promise<EarnTotalsCheckpoint> {
  const toBlock = (await publicClient.getBlockNumber()) - CONFIRMATIONS;
  if (toBlock <= checkpoint.lastScannedBlock) {
    return checkpoint;
  }

  const newTotals = await getEarnTotals({
    vaultAddress,
    walletAddress,
    fromBlock: checkpoint.lastScannedBlock + 1n,
    toBlock
  });

  return {
    totalDeposited: checkpoint.totalDeposited + newTotals.totalDeposited,
    totalWithdrawn: checkpoint.totalWithdrawn + newTotals.totalWithdrawn,
    lastScannedBlock: toBlock
  };
}
```

For the first read of a wallet, start from an empty checkpoint at the block before the vault's deployment:

```typescript {skip-check} theme={"system"}
const initialCheckpoint: EarnTotalsCheckpoint = {
  totalDeposited: 0n,
  totalWithdrawn: 0n,
  lastScannedBlock: vaultDeploymentBlock - 1n
};
```

Save the returned checkpoint to your database after each sync. Store the amounts as strings or as a numeric type with enough precision, because they can exceed the safe range of a JavaScript `number`.

## 5. Calculate earned yield

Read the position at the checkpoint's `lastScannedBlock`. This keeps the position and the totals at the same block. If the position is read at the latest block, a deposit in the unscanned blocks increases `assetsInVault` before it increases `totalDeposited`, and the earned yield is too high.

```typescript {skip-check} theme={"system"}
const checkpoint = await syncEarnTotals({
  vaultAddress,
  walletAddress,
  checkpoint: storedCheckpoint
});

const {assetsInVault} = await getPosition({
  vaultAddress,
  walletAddress,
  blockNumber: checkpoint.lastScannedBlock
});

const earnedYield = assetsInVault - (checkpoint.totalDeposited - checkpoint.totalWithdrawn);
```

The formula is the same one that the [get vault position](/wallets/actions/earn/get-vault-position#calculate-earned-yield) guide uses with the endpoint fields. All amounts are in the asset's smallest unit. Divide by `10^decimals` before your app shows them to users.

## Key integration tips

1. **Read events from the vault address.** Use the `vault_address` from get vault details. For Morpho, this is the Privy fee wrapper vault, not the underlying Morpho vault.
2. **Share transfers are not deposits.** If a wallet receives vault shares through an ERC-20 `Transfer`, no `Deposit` event names the wallet. The shares count in `assets_in_vault`, but not in `total_deposited`. The same applies to shares that the wallet sends away.
3. **Use archive access for past blocks.** Reads at a past `blockNumber` need an RPC node that keeps historical state. Most hosted RPC providers support this.
4. **Check the block range limit.** If `eth_getLogs` fails with a range error, lower `MAX_BLOCK_RANGE` to your provider's limit.

## Next steps

<CardGroup cols={2}>
  <Card title="Earn webhooks" icon="bell" href="/wallets/actions/earn/webhooks" arrow>
    Track Privy Earn deposits and withdrawals in real time.
  </Card>

  <Card title="Get vault details" icon="vault" href="/wallets/actions/earn/get-vault-details" arrow>
    Retrieve vault-level information like APY, TVL, and available liquidity.
  </Card>
</CardGroup>

<Warning title="Disclaimer">
  Privy does not control DeFi vaults or underlying protocols. Vault information is provided for
  reference only and may change or be inaccurate. Earnings are generated from third-party vaults and
  are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for
  general information purposes only and are not investment advice or a recommendation or
  solicitation to engage in any specific transaction. You are responsible for evaluating vaults at
  your own discretion. Privy does not provide investment, financial, legal, or tax advice.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.