This guide explains how to integrate with the Flow YieldVaults EVM contracts from a frontend application.
cd your-frontend
git submodule add https://github.com/AneraLabs/FlowYieldVaultsEVM evm-contracts
git submodule update --init --recursiveimport addresses from "./evm-contracts/deployments/contract-addresses.json";
import abi from "./evm-contracts/deployments/artifacts/FlowYieldVaultsRequests.json";
import { RequestType, RequestStatus } from "./evm-contracts/deployments/types";
const network = getCurrentNetworkKey(); // e.g. "mainnet" | "testnet"
const contractAddress =
addresses.contracts.FlowYieldVaultsRequests.addresses[network];
const networkConfig = addresses.metadata.networks[network];import { ethers } from "ethers";
// Get provider from wallet (e.g., MetaMask)
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// Create contract instance
const contract = new ethers.Contract(contractAddress, abi, signer);// Native FLOW deposit
const NATIVE_FLOW = "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF";
const amount = ethers.parseEther("10"); // 10 FLOW
const tx = await contract.createYieldVault(
NATIVE_FLOW, // tokenAddress
amount, // amount
vaultIdentifier, // "A.{addr}.FlowToken.Vault"
strategyIdentifier, // Strategy identifier
{ value: amount } // Send FLOW with transaction
);
const receipt = await tx.wait();const tx = await contract.depositToYieldVault(
yieldVaultId, // uint64
NATIVE_FLOW, // tokenAddress
amount, // amount
{ value: amount }
);
await tx.wait();const tx = await contract.withdrawFromYieldVault(
yieldVaultId, // uint64
amount // amount to withdraw
);
await tx.wait();const tx = await contract.closeYieldVault(yieldVaultId);
await tx.wait();const tx = await contract.cancelRequest(requestId);
await tx.wait();// Claims refund balance for a token (accumulated from cancelled/dropped/failed requests)
// Only claims actual refunds - does NOT touch funds escrowed for active pending requests
const tx = await contract.claimRefund(NATIVE_FLOW);
await tx.wait();const yieldVaultIds: bigint[] = await contract.getYieldVaultIdsForUser(
userAddress
);const owns: boolean = await contract.doesUserOwnYieldVault(
userAddress,
yieldVaultId
);const count: bigint = await contract.getUserPendingRequestCount(userAddress);Note: counts include both PENDING and PROCESSING requests (they remain in the pending queue until completion/cancel/drop).
// Funds tied to active pending requests (not yet processed by worker)
const escrowedBalance: bigint = await contract.getUserPendingBalance(
userAddress,
tokenAddress
);// Funds available to claim via claimRefund() (from cancelled/dropped/failed requests)
const claimableRefund: bigint = await contract.getClaimableRefund(
userAddress,
tokenAddress
);const totalPending: bigint = await contract.getPendingRequestCount();Note: counts include both PENDING and PROCESSING requests (they remain in the pending queue until completion/cancel/drop).
const request = await contract.getRequest(requestId);
// Returns: { id, user, requestType, status, tokenAddress, amount, yieldVaultId, timestamp, message, vaultIdentifier, strategyIdentifier }const [
ids,
requestTypes,
statuses,
tokenAddresses,
amounts,
yieldVaultIds,
timestamps,
messages,
vaultIdentifiers,
strategyIdentifiers,
balanceTokens,
pendingBalances,
claimableRefunds,
] = await contract.getPendingRequestsByUserUnpacked(userAddress);
// `balanceTokens` contains the tracked token set configured on the contract.
// The balance arrays are aligned by index and may include disabled tokens if users still have balances/refunds.
const pendingByToken = Object.fromEntries(
balanceTokens.map((token, i) => [token, pendingBalances[i]])
);
const claimableByToken = Object.fromEntries(
balanceTokens.map((token, i) => [token, claimableRefunds[i]])
);const [
ids,
users,
requestTypes,
statuses,
tokenAddresses,
amounts,
yieldVaultIds,
timestamps,
messages,
vaultIdentifiers,
strategyIdentifiers,
] = await contract.getPendingRequestsUnpacked(startIndex, count);
// Filter for specific user client-side
const userRequests = ids.filter(
(_, i) => users[i].toLowerCase() === userAddress.toLowerCase()
);- Requests revert when the contract is paused, the caller is blocklisted, or allowlist is enabled and the caller is not allowlisted.
- For CREATE/DEPOSIT only:
tokenAddressmust be configured inallowedTokensandamountmust meet the configuredminimumBalance. maxPendingRequestsPerUsercan cap in-flight requests (0 = unlimited).
1. User submits request → EVM tx succeeds → RequestCreated event
2. Request queued (status=PENDING); CREATE/DEPOSIT escrow funds in pendingUserBalances
3. Cadence Worker processes → RequestProcessed event (status=PROCESSING)
4. Operation completes → RequestProcessed event (status=COMPLETED or FAILED)
5. On completion:
- CREATE: YieldVault appears in user's list
- CLOSE: YieldVault is removed from user's list
- DEPOSIT/WITHDRAW: list unchanged
On failure/cancel/drop (CREATE/DEPOSIT only): Funds moved to claimableRefunds; user must call claimRefund()
| Scenario | What Happens | User Action |
|---|---|---|
| Request cancelled by user | CREATE/DEPOSIT funds → claimableRefunds |
Call claimRefund(tokenAddress) |
| Request dropped | CREATE/DEPOSIT funds → claimableRefunds |
Call claimRefund(tokenAddress) |
| Cadence processing fails | CREATE/DEPOSIT funds → claimableRefunds |
Call claimRefund(tokenAddress) |
Important: claimRefund() only withdraws actual refunds. It does NOT touch funds escrowed for active pending requests. WITHDRAW/CLOSE requests never escrow funds and never generate refunds.
contract.on(
"RequestCreated",
(
requestId,
user,
requestType,
tokenAddress,
amount,
yieldVaultId,
timestamp,
vaultIdentifier,
strategyIdentifier
) => {
console.log("New request created:", requestId.toString());
// Add to pending requests UI
}
);
contract.on(
"RequestProcessed",
(requestId, user, requestType, status, yieldVaultId, message) => {
console.log("Request processed:", requestId.toString(), "status:", status);
if (status === 2) {
// COMPLETED
// Refresh positions list
fetchPositions();
} else if (status === 3) {
// FAILED
// Show error message
showError(message);
}
}
);
contract.on(
"RequestCancelled",
(requestId, user, tokenAddress, claimableAmount) => {
console.log(
"Request cancelled, claimable:",
ethers.formatEther(claimableAmount)
);
// Prompt user to claim refund if claimableAmount > 0
if (claimableAmount > 0n) {
showClaimRefundPrompt(tokenAddress, claimableAmount);
}
}
);
contract.on("RefundCredited", (user, tokenAddress, amount, requestId) => {
console.log("Refund credited:", requestId, tokenAddress, amount.toString());
// Prompt user to claim refund if amount > 0
if (amount > 0n) {
showClaimRefundPrompt(tokenAddress, amount);
}
});
contract.on("RefundClaimed", (user, tokenAddress, amount) => {
console.log("Refund claimed:", tokenAddress, amount.toString());
});// BalanceUpdated fires when escrowed balance (pendingUserBalances) changes
// This happens on: request creation, startProcessingBatch, cancelRequest, dropRequests
contract.on("BalanceUpdated", (user, tokenAddress, newBalance) => {
if (user.toLowerCase() === currentUser.toLowerCase()) {
// Update UI with new escrowed balance for active pending requests
updateEscrowedBalance(newBalance);
}
});
// RefundClaimed fires when user claims accumulated refunds
contract.on("RefundClaimed", (user, tokenAddress, amount) => {
if (user.toLowerCase() === currentUser.toLowerCase()) {
// Refresh claimable balance (should be 0 after claim)
refreshClaimableBalance();
}
});The EVM contract only stores request queue data and ownership mappings. For actual YieldVault position data (balances, yields, strategies), query the Cadence side using Flow Client Library (FCL).
import * as fcl from "@onflow/fcl";
const network = getCurrentNetworkKey(); // e.g. "mainnet" | "testnet"
const cadenceConfig = getCadenceConfig(network);
// Configure FCL using your selected network
fcl.config({
"accessNode.api": cadenceConfig.accessNodeApi,
"flow.network": cadenceConfig.flowNetwork,
"0xFlowYieldVaultsEVM": cadenceConfig.flowYieldVaultsEVM,
"0xFlowYieldVaults": cadenceConfig.flowYieldVaults,
});const GET_USER_YIELDVAULTS = `
import FlowYieldVaultsEVM from 0xFlowYieldVaultsEVM
access(all) fun main(evmAddress: String): [UInt64] {
var normalizedAddress = evmAddress.toLower()
if normalizedAddress.length > 2 && normalizedAddress.slice(from: 0, upTo: 2) == "0x" {
normalizedAddress = normalizedAddress.slice(from: 2, upTo: normalizedAddress.length)
}
while normalizedAddress.length < 40 {
normalizedAddress = "0".concat(normalizedAddress)
}
return FlowYieldVaultsEVM.getYieldVaultIdsForEVMAddress(normalizedAddress)
}
`;
const yieldVaultIds = await fcl.query({
cadence: GET_USER_YIELDVAULTS,
args: (arg, t) => [arg(userEvmAddress, t.String)],
});This is critical for displaying actual position values. The balance lives on Cadence, not EVM.
const GET_YIELDVAULT_BALANCE = `
import FlowYieldVaults from 0xFlowYieldVaults
access(all) fun main(managerAddress: Address, yieldVaultId: UInt64): UFix64? {
let account = getAccount(managerAddress)
if let manager = account.capabilities.borrow<&FlowYieldVaults.YieldVaultManager>(
FlowYieldVaults.YieldVaultManagerPublicPath
) {
if let yieldVault = manager.borrowYieldVault(id: yieldVaultId) {
return yieldVault.getYieldVaultBalance()
}
}
return nil
}
`;
const balance = await fcl.query({
cadence: GET_YIELDVAULT_BALANCE,
args: (arg, t) => [
arg(cadenceConfig.flowYieldVaults, t.Address),
arg(yieldVaultId.toString(), t.UInt64),
],
});
// Returns UFix64 string like "100.00000000"const GET_YIELDVAULT_DETAILS = `
import FlowYieldVaults from 0xFlowYieldVaults
access(all) fun main(managerAddress: Address, yieldVaultId: UInt64): {String: AnyStruct}? {
let account = getAccount(managerAddress)
if let manager = account.capabilities.borrow<&FlowYieldVaults.YieldVaultManager>(
FlowYieldVaults.YieldVaultManagerPublicPath
) {
if let yieldVault = manager.borrowYieldVault(id: yieldVaultId) {
return {
"id": yieldVaultId,
"balance": yieldVault.getYieldVaultBalance(),
"supportedVaultTypes": yieldVault.getSupportedVaultTypes().keys
}
}
}
return nil
}
`;
const details = await fcl.query({
cadence: GET_YIELDVAULT_DETAILS,
args: (arg, t) => [
arg(cadenceConfig.flowYieldVaults, t.Address),
arg(yieldVaultId.toString(), t.UInt64),
],
});Combine EVM ownership with Cadence balance data:
async function getUserPositions(userEvmAddress: string) {
// 1. Get YieldVault IDs from Cadence (faster than EVM call)
const yieldVaultIds = await fcl.query({
cadence: GET_USER_YIELDVAULTS,
args: (arg, t) => [arg(userEvmAddress, t.String)],
});
// 2. Fetch balances for each YieldVault
const positions = await Promise.all(
yieldVaultIds.map(async (id: string) => {
const balance = await fcl.query({
cadence: GET_YIELDVAULT_BALANCE,
args: (arg, t) => [
arg(cadenceConfig.flowYieldVaults, t.Address),
arg(id, t.UInt64),
],
});
return {
yieldVaultId: id,
balance: parseFloat(balance || "0"),
};
})
);
return positions;
}const GET_SUPPORTED_STRATEGIES = `
import FlowYieldVaults from 0xFlowYieldVaults
access(all) fun main(): [String] {
let strategies = FlowYieldVaults.getSupportedStrategies()
let identifiers: [String] = []
for strategy in strategies {
identifiers.append(strategy.identifier)
}
return identifiers
}
`;
const strategies = await fcl.query({ cadence: GET_SUPPORTED_STRATEGIES });
// Returns: ["A.xxx.FlowYieldVaultsStrategies.IncrementFiLiquidStaking", ...]const CHECK_SYSTEM_STATUS = `
import FlowYieldVaultsEVM from 0xFlowYieldVaultsEVM
access(all) fun main(): {String: AnyStruct} {
return {
"flowYieldVaultsRequestsAddress": FlowYieldVaultsEVM.getFlowYieldVaultsRequestsAddress()?.toString() ?? "not set",
"totalEVMUsers": FlowYieldVaultsEVM.yieldVaultRegistry.keys.length
}
}
`;
const status = await fcl.query({ cadence: CHECK_SYSTEM_STATUS });| Data | Query From | Why |
|---|---|---|
| Pending requests | EVM | Request queue lives on EVM |
| Request status | EVM | Status updates happen on EVM |
| User owns YieldVault | Either | Both maintain ownership mappings |
| YieldVault balance | Cadence | Actual position value is on Cadence |
| Yield/rewards | Cadence | Strategy execution is on Cadence |
| Supported strategies | Cadence | Strategy registry is on Cadence |
// On page load
async function initializeUserDashboard(evmAddress: string) {
// 1. Get owned YieldVault IDs (Cadence - fast)
const yieldVaultIds = await fcl.query({
cadence: GET_USER_YIELDVAULTS,
args: (arg, t) => [arg(evmAddress, t.String)],
});
// 2. Get pending requests (EVM)
const pendingCount = await evmContract.getUserPendingRequestCount(evmAddress);
// 3. Fetch balances for active positions (Cadence)
const positions = await Promise.all(
yieldVaultIds.map((id) => getYieldVaultDetails(id))
);
// 4. Subscribe to EVM events for real-time updates
evmContract.on("RequestProcessed", handleRequestProcessed);
return { positions, pendingCount };
}"Users must know the exact status of their money at all times" - Tom
- Pending Requests Panel: Show all user's pending/processing requests
- Real-time Updates: PENDING → PROCESSING → COMPLETED/FAILED
- Clear Messaging: "Your transaction is being processed. This typically takes 15-60 seconds."
- Escrowed Balance: Show funds held in escrow during processing (
getUserPendingBalance) - Claimable Refunds: Show refunds available to claim (
getClaimableRefund) and provide a "Claim Refund" button
// Check if user has claimable refunds
const claimable = await contract.getClaimableRefund(userAddress, NATIVE_FLOW);
if (claimable > 0n) {
// Show "Claim Refund" banner/button
// Display: "You have X FLOW available to claim from cancelled/failed requests"
// On button click:
const tx = await contract.claimRefund(NATIVE_FLOW);
await tx.wait();
// Listen for RefundClaimed event to confirm
}Important behavior for EVM wallets:
- A
FAILEDCREATE_YIELDVAULTorDEPOSIT_TO_YIELDVAULTrequest means the funds were moved intoclaimableRefunds, not lost. - The claim action should be shown whenever
getClaimableRefund(userAddress, tokenAddress) > 0, even if the failed request row itself is no longer actionable. - Refunds are aggregated per user and token, not per request. One failed PYUSD0 request can increase an existing PYUSD0 claimable balance, so the UI should render a token-level claim CTA using the total returned by
getClaimableRefund(...). - The button should call
claimRefund(tokenAddress)onFlowYieldVaultsRequests.
| Aspect | Flow Wallet | EVM Wallet |
|---|---|---|
| Transaction Time | ~10 seconds | ~15-60 seconds (two-step) |
| Optimistic Updates | YES | NO |
| UI Behavior | Immediate position | Show in "Pending" first |
Important: Do NOT apply optimistic updates for EVM wallets!
if (wallet.type === "evm") {
// Show in pending requests UI
// Wait for RequestProcessed event with status=COMPLETED
// Then move to main positions list
} else {
// Existing Flow wallet optimistic logic
}// Sentinel address for native FLOW token
const NATIVE_FLOW = "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF";
// Sentinel value for "no YieldVault" (also used as placeholder for CREATE until processed)
const NO_YIELDVAULT_ID = 18446744073709551615n; // type(uint64).max
// CREATE requests start with yieldVaultId = NO_YIELDVAULT_ID until processed
// Request Types
enum RequestType {
CREATE_YIELD_VAULT = 0,
DEPOSIT_TO_YIELD_VAULT = 1,
WITHDRAW_FROM_YIELD_VAULT = 2,
CLOSE_YIELD_VAULT = 3,
}
// Request Status
enum RequestStatus {
PENDING = 0,
PROCESSING = 1,
COMPLETED = 2,
FAILED = 3,
}- User can connect EVM wallet
- Create YieldVault request submits successfully
- Request appears in Pending Requests panel immediately
- Status updates automatically (PENDING → PROCESSING → COMPLETED)
- Completed YieldVault appears in main positions list
- User can cancel pending request (only PENDING status)
- Failed requests show clear error messages
- Escrowed balance (
getUserPendingBalance) displays correctly - Claimable refunds (
getClaimableRefund) displays correctly after cancel/fail - User can claim refunds via
claimRefund()and receives funds
Full type definitions are available in deployments/types/index.ts:
import {
RequestType,
RequestStatus,
EVMRequest,
RequestCreatedEvent,
RequestProcessedEvent,
RequestCancelledEvent,
RefundCreditedEvent,
RefundClaimedEvent,
NO_YIELD_VAULT_ID,
NATIVE_FLOW_ADDRESS,
isRequestPending,
isRequestCompleted,
getRequestTypeName,
getRequestStatusName,
} from "./evm-contracts/deployments/types";- Contract ABI:
deployments/artifacts/FlowYieldVaultsRequests.json - Contract Addresses:
deployments/contract-addresses.json - Event Definitions: See TypeScript types or ABI
- FCL Documentation: developers.flow.com/tools/clients/fcl-js
- Cadence Scripts:
cadence/scripts/directorycheck_user_yieldvaults.cdc- Get YieldVault IDs for EVM addressget_pending_requests_for_evm_address.cdc- Get pending requests for an EVM addresscheck_yieldvault_details.cdc- Get system-wide YieldVault detailscheck_yieldvaultmanager_status.cdc- Comprehensive system statuscheck_worker_status.cdc- Worker health checksvalidate_create_yieldvault_params.cdc- Validate vault/strategy identifiers before CREATE
- Design Document: FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md