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

# Earn

> Deposit into yield vaults, track positions, performance and rewards, and withdraw with the Dynamic Swift SDK.

## Overview

Earn lets a user's wallet deposit into a yield vault, track what the position is worth, claim reward campaigns, and withdraw. The `earn` module on the SDK instance wraps the Earn API and signs and broadcasts the transactions it returns. Vaults run on EVM chains and on Solana, and the module executes an action on whichever chain family its payload targets.

For how vaults, shares, and yield work, see the [Earn overview](/docs/earn/overview.md).

Every Earn action returns an unsigned payload. You sign and broadcast it with `execute`, which sends an EVM payload's ERC-20 approval first and waits for it to confirm whenever the payload carries one. A Solana payload instead carries an ordered sequence of transactions that `execute` signs, sends, and confirms one at a time. See [Solana vaults](#solana-vaults).

## Prerequisites

* Dynamic SDK initialized (see [Quickstart](/docs/swift/quickstart.md))
* User authenticated (see [Authentication](/docs/swift/authentication.md))
* A wallet on the vault's chain family: an EVM wallet for an EVM vault, or a Solana wallet for a Solana vault (see [Wallet Creation](/docs/swift/wallet-creation.md))
* Earn enabled for your environment in the Dynamic dashboard. Until it is, every call throws an `EarnError` with `statusCode` 403.

Earn is available on the SDK instance as `sdk.earn`:

```swift theme={"system"}
import DynamicSDKSwift

let sdk = DynamicSDK.instance()
let earn = sdk.earn
```

## Browse vaults

`getVaults` returns the vaults your environment can access. `getVaultDetails` adds the on-chain fields you need to display and transact against one, including `assetDecimals`.

```swift theme={"system"}
let vaults = try await earn.getVaults()

for vault in vaults {
    print("\(vault.assetSymbol) by \(vault.curator) on chain \(vault.chainId)")
    print("Net APY: \(vault.netApy ?? 0), TVL: \(vault.tvlUsd ?? 0)")
}

guard let vaultId = vaults.first?.vaultId else {
    return // no vaults enabled for this environment
}
let vault = try await earn.getVaultDetails(vaultId: vaultId)
```

`netApy` and `tvlUsd` come from live market data on a best-effort basis, so both are `nil` when that data is unavailable.

A wallet can only transact on its own chain family, so filter the list by `chainName` before showing vaults for a wallet:

```swift theme={"system"}
let usableVaults = vaults.filter {
    $0.chainName.rawValue == wallet.chain.uppercased()
}
```

## Read a position

A position holds `shares` and `assets`, both in raw token units. Show `assets`, because it grows as yield accrues. Depending on the vault's provider, `shares` either stays flat or always equals `assets`, so it is not meaningful to show a user.

```swift theme={"system"}
let position = try await earn.getVaultPosition(
    vaultId: vault.vaultId,
    ownerAddress: wallet.address
)

if let assets = Double(position.assets) {
    let balance = assets / pow(10.0, Double(vault.assetDecimals))
    print("Balance: \(balance) \(vault.assetSymbol)")
}
```

## Read performance

`getVaultPerformance` returns a wallet's earnings in one vault: `netEarningsAssets` and `returnOnCapital`. Morpho indexes these values rather than reading them on-chain, so they trail the chain head; `getVaultPosition` stays the live source of truth for what the position is worth right now.

```swift theme={"system"}
let performance = try await earn.getVaultPerformance(
    vaultId: vault.vaultId,
    ownerAddress: wallet.address
)

print("Net earnings: \(performance.netEarningsAssets) raw units")
print("Return on capital: \(performance.returnOnCapital ?? 0)")
```

`netEarningsAssets` is a raw token-unit string, so divide it by the vault's `assetDecimals` before display, the same way you format a position's `assets`. `returnOnCapital` is a time-weighted, non-annualized decimal (for example `0.00413` means a 0.41% return) and is `nil` while the position has too little history to compute one.

> [!WARNING]
>
> `getVaultPerformance` currently supports vaults from these providers: Morpho. For a vault from any other provider, such as an Aave vault, the call throws an `EarnError` with `statusCode` 400. Use `getVaultPosition` to show such a position's balance, and see [Provider support](/docs/earn/overview.md#provider-support).

## Read earnings across vaults

`getEarnings` returns a wallet's aggregate earnings across every vault it holds on one EVM chain: USD totals plus a per-vault breakdown.

```swift theme={"system"}
let earnings = try await earn.getEarnings(
    chainId: 8453, // Base
    ownerAddress: wallet.address
)

print("Total PNL: $\(earnings.totalPnlUsd)")

for entry in earnings.vaults {
    if let pnlUsd = entry.pnlUsd {
        print("\(entry.vaultAddress): $\(pnlUsd)")
    }
}
```

Totals are USD-only, because per-vault earnings are denominated in different underlying assets. A vault with no USD price is skipped from the totals, not counted as zero. Positions in vaults the environment does not offer are still included, with `vaultId` set to `nil` for those entries.

Not every provider reports a cost basis for a position. For those vaults, the entry still carries `assets` and `assetsUsd`, but `pnl`, `pnlUsd`, and `roe` are `nil`. Those positions add to `totalAssetsUsd` and add nothing to `totalPnlUsd`.

> [!WARNING]
>
> `getEarnings` covers Morpho and Aave positions on EVM chains. It does not include positions in Solana vaults; read a Solana position with `getVaultPosition` instead.

## Read rewards

Rewards are a separate bonus some vaults pay in another token. They sit unclaimed until you claim them, unlike base yield, which shows up as `assets` growing.

```swift theme={"system"}
let rewards = try await earn.getVaultRewards(
    vaultId: vault.vaultId,
    ownerAddress: wallet.address
)

for reward in rewards {
    print("\(reward.accrued) raw units of \(reward.tokenSymbol)")
}
```

The list is empty when the wallet has accrued nothing in the vault. Rewards are supported only for vaults from some providers. For a vault from any other provider, the API rejects the rewards and claim requests with an HTTP 400 error, which surfaces as an `EarnError`. See [Provider support](/docs/earn/overview.md#provider-support).

## Deposit

`deposit` builds the payload, `execute` sends it. `amount` is human-readable, so `"12.5"` deposits 12.5 USDC.

```swift theme={"system"}
let deposit = try await earn.deposit(
    vaultId: vault.vaultId,
    ownerAddress: wallet.address,
    amount: "12.5"
)

let transactionHash = try await earn.execute(
    payload: deposit.signingPayload,
    wallet: wallet,
    onStepChange: { step in
        switch step {
        case .approval:
            print("Approving the vault to spend the deposit")
        case .transaction:
            print("Sending the vault transaction")
        }
    }
)
```

`execute` returns the vault transaction hash (the last signature on Solana). When an EVM payload carries an `evmApproval`, it sends that approval first and waits for its receipt, so `onStepChange` reports `.approval` before `.transaction`. An approval is only requested when the vault needs an allowance it does not already have.

To send the shares to a different wallet, set `receiverAddress` on the request. It defaults to `ownerAddress`.

## Withdraw

Withdrawals redeem shares the wallet already holds, so they never need an approval.

```swift theme={"system"}
let withdrawal = try await earn.withdraw(
    vaultId: vault.vaultId,
    ownerAddress: wallet.address,
    amount: "12.5"
)

let transactionHash = try await earn.execute(
    payload: withdrawal.signingPayload,
    wallet: wallet
)
```

## Claim rewards

```swift theme={"system"}
do {
    let claim = try await earn.claim(
        vaultId: vault.vaultId,
        ownerAddress: wallet.address
    )

    let transactionHash = try await earn.execute(
        payload: claim.signingPayload,
        wallet: wallet
    )
} catch let error as EarnError where error.nothingToClaim {
    print("Nothing to claim in this vault")
}
```

## Solana vaults

A Solana vault action is split into up to three transactions, such as the deposit itself and a step that stakes the deposited shares in the vault's farm. `execute` signs, sends, and confirms them one at a time in `ordinal` order, because a later transaction can depend on the state an earlier one wrote. `onTransactionStepChange` reports each transaction's position and label as it begins:

```swift theme={"system"}
let transactionHash = try await earn.execute(
    payload: deposit.signingPayload,
    wallet: solanaWallet,
    onTransactionStepChange: { step in
        print("Transaction \(step.ordinal + 1) of \(step.total): \(step.label ?? "")")
    }
)
```

All of an action's transactions are built against the same blockhash, which stays valid only briefly. If the user takes too long to approve, the remaining transactions expire and `execute` throws an `EarnSolanaStepError`. When a later transaction fails or expires, the earlier ones have already confirmed on-chain, so read the wallet's position with `getVaultPosition` before retrying. Repeating the whole action could apply the earlier steps twice. See [Solana vaults](/docs/earn/overview.md#solana-vaults) in the Earn overview.

## Gas sponsorship

The `sponsored` parameter on `execute` picks how the transactions' fees are paid:

* `true` requires gas sponsorship and throws `EarnSponsorError` when it is unavailable.
* `false` always pays the fee from the wallet.
* `nil` (the default) sponsors when the environment enables it and falls back to a wallet-paid transaction otherwise.

```swift theme={"system"}
let transactionHash = try await earn.execute(
    payload: deposit.signingPayload,
    wallet: wallet,
    sponsored: true
)
```

On EVM, a sponsored execution batches the ERC-20 approval and the vault call into one gasless transaction. On Solana, sponsorship works through SVM Gas Sponsorship; see [SVM Gas Sponsorship](/docs/swift/wallets/solana/svm-gas-sponsorship.md) for how to enable it. Note that an unset `sponsored` sponsors a Solana action when SVM Gas Sponsorship is enabled, which differs from the JavaScript SDK, where a Solana vault action is wallet-paid unless `sponsored` is `true`.

## Amount units

Amounts cross the wire as strings in two different units:

* The `amount` parameter on `deposit` and `withdraw` is human-readable: `"12.5"` means 12.5 USDC.
* `EarnVaultPosition`, `EarnVaultReward`, and the amounts inside a signing payload are raw token units. They can exceed `Int.max`, so they stay strings; convert a position or payload amount with the vault's `assetDecimals`, and a reward amount with the reward's own `tokenDecimals`, before display.

> [!WARNING]
>
> Mixing the two units up silently transacts the wrong value. Human-readable amounts also need a
> leading zero: `"0.01"` works, `".01"` is rejected by the API.

## Handle errors

Every failed Earn call throws an `EarnError` carrying the HTTP `statusCode`:

| Condition | What it means |
| - | - |
| `statusCode` 403 | Earn is not enabled for your environment |
| `nothingToClaim` (422 on claim) | The wallet has no claimable rewards in the vault |
| `statusCode` 400 | The vault's provider does not support the request, such as rewards |
| Other status codes | The API rejected the request, for example an invalid amount or address |

```swift theme={"system"}
do {
    let vaults = try await earn.getVaults()
} catch let error as EarnError {
    print("Earn failed (\(error.statusCode ?? 0)): \(error.message)")
}
```

`execute` throws an `EarnError` when the wallet's chain does not match the payload's chain family, when the payload carries no transaction, or when an approval reverts or is not confirmed in time. Two error types are specific to execution:

* `EarnSolanaStepError` carries the `ordinal` and `total` of the Solana transaction that stopped and a `code` of `expired`, `failed`, or `sendFailed`. Transactions before it already confirmed, so check the position before retrying.
* `EarnSponsorError` means `sponsored: true` was required but sponsorship was unavailable or the sponsored transaction failed.

## Next steps

* [ERC-20 Token Transfers](/docs/swift/wallets/evm/erc20-transfers.md) to move the vault asset in and out of the wallet
* [SVM Gas Sponsorship](/docs/swift/wallets/solana/svm-gas-sponsorship.md) to pay Solana fees for your users
* [Earn overview](/docs/earn/overview.md) for vault, share, and yield concepts


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