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
2 changes: 1 addition & 1 deletion src/abis/morpho-market-v1-adapter-factory.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Abi } from "viem";

export const adapterFactoryAbi = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"parentVault","type":"address"},{"indexed":true,"internalType":"address","name":"morpho","type":"address"},{"indexed":true,"internalType":"address","name":"morphoMarketV1Adapter","type":"address"}],"name":"CreateMorphoMarketV1Adapter","type":"event"},{"inputs":[{"internalType":"address","name":"parentVault","type":"address"},{"internalType":"address","name":"morpho","type":"address"}],"name":"createMorphoMarketV1Adapter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"isMorphoMarketV1Adapter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"parentVault","type":"address"},{"internalType":"address","name":"morpho","type":"address"}],"name":"morphoMarketV1Adapter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}] as const satisfies Abi;
export const adapterFactoryAbi = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"parentVault","type":"address"},{"indexed":true,"internalType":"address","name":"morphoMarketV1AdapterV2","type":"address"}],"name":"CreateMorphoMarketV1AdapterV2","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"morpho","type":"address"},{"indexed":true,"internalType":"address","name":"adaptiveCurveIrm","type":"address"}],"name":"CreateMorphoMarketV1AdapterV2Factory","type":"event"},{"inputs":[],"name":"adaptiveCurveIrm","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"parentVault","type":"address"}],"name":"createMorphoMarketV1AdapterV2","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"isMorphoMarketV1AdapterV2","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"morpho","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"parentVault","type":"address"}],"name":"morphoMarketV1AdapterV2","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}] as const satisfies Abi;
2 changes: 1 addition & 1 deletion src/components/common/table-container-with-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function TableContainerWithHeader({ title, actions, children, className =
<div className={`bg-surface rounded-md font-zen shadow-sm ${className}`}>
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-800 px-6 py-0.5">
<h3 className="font-monospace text-xs uppercase text-secondary">{title}</h3>
{actions && <div className="flex items-center gap-2">{actions}</div>}
{<div className="flex items-center gap-2 min-h-8">{actions}</div>}
</div>
<div className="overflow-x-auto pb-4">{children}</div>
</div>
Expand Down
166 changes: 166 additions & 0 deletions src/data-sources/morpho-api/v2-vaults-full.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { Address } from 'viem';
import type { VaultV2Details } from '@/data-sources/morpho-api/v2-vaults';
import { vaultV2sOwnerQuery } from '@/graphql/morpho-api-queries';
import { type SupportedNetworks, networks, isAgentAvailable } from '@/utils/networks';
import { morphoGraphqlFetcher } from '@/data-sources/morpho-api/fetchers';
import { getDeployedVaults } from '@/utils/vault-storage';

// Vault address with network information
export type UserVaultV2Address = {
address: string;
networkId: SupportedNetworks;
};

// User vault with full details and network info
// This is used by the autovault page to display user's vaults
export type UserVaultV2 = VaultV2Details & {
networkId: SupportedNetworks;
balance?: bigint; // User's redeemable assets (from previewRedeem)
adapter?: Address; // MorphoMarketV1Adapter address
};

// Lightweight API response for owner lookup
type VaultV2OwnerItem = {
address: string;
owner: { address: string } | null;
chain: { id: number };
};

type VaultV2sOwnerApiResponse = {
data?: {
vaultV2s?: {
items: VaultV2OwnerItem[];
pageInfo: {
countTotal: number;
};
};
};
errors?: { message: string }[];
};

/**
* Fetches V2 vaults for a specific network from the Morpho API and filters by owner.
* Merges with locally cached vaults for newly deployed vaults not yet indexed.
*/
export const fetchUserVaultV2Addresses = async (owner: string, network: SupportedNetworks): Promise<UserVaultV2Address[]> => {
if (!isAgentAvailable(network)) {
return [];
}

const normalizedOwner = owner.toLowerCase();
const PAGE_SIZE = 100;
const apiVaults: UserVaultV2Address[] = [];
let skip = 0;
let hasMore = true;

// Step 1: Get locally cached vaults for this network
const localVaults = getDeployedVaults(owner)
.filter((v) => v.chainId === network)
.map((v) => ({
address: v.address,
networkId: network,
}));

// Step 2: Fetch from Morpho API
try {
while (hasMore) {
const response = await morphoGraphqlFetcher<VaultV2sOwnerApiResponse>(vaultV2sOwnerQuery, {
first: PAGE_SIZE,
skip,
});

if (!response?.data?.vaultV2s) {
break;
}

const items = response.data.vaultV2s.items;

for (const vault of items) {
if (vault.chain.id === network && vault.owner?.address.toLowerCase() === normalizedOwner) {
apiVaults.push({
address: vault.address,
networkId: network,
});
}
}

const totalCount = response.data.vaultV2s.pageInfo.countTotal;
skip += PAGE_SIZE;
hasMore = skip < totalCount && items.length === PAGE_SIZE;
}
} catch (error) {
console.error(`Error fetching V2 vault addresses for owner ${owner} on network ${network}:`, error);
}

// Step 3: Merge and deduplicate
const seenAddresses = new Set(apiVaults.map((v) => v.address.toLowerCase()));
const uniqueLocalVaults = localVaults.filter((v) => !seenAddresses.has(v.address.toLowerCase()));

return [...uniqueLocalVaults, ...apiVaults];
};

/**
* Fetches vault addresses from all networks that support V2 vaults.
* Merges results from Morpho API with locally cached vaults (for newly deployed vaults
* that haven't been indexed yet).
*/
export const fetchUserVaultV2AddressesAllNetworks = async (owner: string): Promise<UserVaultV2Address[]> => {
const normalizedOwner = owner.toLowerCase();
const PAGE_SIZE = 100;
const apiVaults: UserVaultV2Address[] = [];
let skip = 0;
let hasMore = true;

const supportedNetworkIds = new Set(networks.filter((network) => isAgentAvailable(network.network)).map((network) => network.network));

if (supportedNetworkIds.size === 0) {
return [];
}

// Step 1: Get locally cached vaults (recently deployed, may not be indexed yet)
const localVaults = getDeployedVaults(owner)
.filter((v) => supportedNetworkIds.has(v.chainId as SupportedNetworks))
.map((v) => ({
address: v.address,
networkId: v.chainId as SupportedNetworks,
}));

// Step 2: Fetch from Morpho API
try {
while (hasMore) {
const response = await morphoGraphqlFetcher<VaultV2sOwnerApiResponse>(vaultV2sOwnerQuery, {
first: PAGE_SIZE,
skip,
});

if (!response?.data?.vaultV2s) {
break;
}

const items = response.data.vaultV2s.items;

for (const vault of items) {
if (supportedNetworkIds.has(vault.chain.id as SupportedNetworks) && vault.owner?.address.toLowerCase() === normalizedOwner) {
apiVaults.push({
address: vault.address,
networkId: vault.chain.id as SupportedNetworks,
});
}
}

const totalCount = response.data.vaultV2s.pageInfo.countTotal;
skip += PAGE_SIZE;
hasMore = skip < totalCount && items.length === PAGE_SIZE;
}
} catch (error) {
console.error('Error fetching V2 vault addresses across networks:', error);
// Continue with local vaults even if API fails
}

// Step 3: Merge and deduplicate (API results take precedence for ordering)
const seenAddresses = new Set(apiVaults.map((v) => v.address.toLowerCase()));
const uniqueLocalVaults = localVaults.filter((v) => !seenAddresses.has(v.address.toLowerCase()));

// Local vaults first (newest), then API vaults
return [...uniqueLocalVaults, ...apiVaults];
Comment on lines +160 to +165
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Deduping by address only can drop vaults on other chains.

Same address can exist on multiple networks; include networkId in the dedupe key.

Proposed fix
-  const seenAddresses = new Set(apiVaults.map((v) => v.address.toLowerCase()));
-  const uniqueLocalVaults = localVaults.filter((v) => !seenAddresses.has(v.address.toLowerCase()));
+  const seenKeys = new Set(apiVaults.map((v) => `${v.networkId}:${v.address.toLowerCase()}`));
+  const uniqueLocalVaults = localVaults.filter(
+    (v) => !seenKeys.has(`${v.networkId}:${v.address.toLowerCase()}`),
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Step 3: Merge and deduplicate (API results take precedence for ordering)
const seenAddresses = new Set(apiVaults.map((v) => v.address.toLowerCase()));
const uniqueLocalVaults = localVaults.filter((v) => !seenAddresses.has(v.address.toLowerCase()));
const results = await Promise.all(promises);
return results.flat();
// Local vaults first (newest), then API vaults
return [...uniqueLocalVaults, ...apiVaults];
// Step 3: Merge and deduplicate (API results take precedence for ordering)
const seenKeys = new Set(apiVaults.map((v) => `${v.networkId}:${v.address.toLowerCase()}`));
const uniqueLocalVaults = localVaults.filter(
(v) => !seenKeys.has(`${v.networkId}:${v.address.toLowerCase()}`),
);
// Local vaults first (newest), then API vaults
return [...uniqueLocalVaults, ...apiVaults];
🤖 Prompt for AI Agents
In `@src/data-sources/subgraph/v2-vaults.ts` around lines 160 - 165, The dedupe
currently uses only address and will drop vaults that share the same address on
different chains; update the keying logic so both address and networkId are used
when building seenAddresses from apiVaults and when filtering localVaults.
Specifically, change the Set population (seenAddresses) to store a composite key
derived from apiVaults (e.g., address.toLowerCase() + a delimiter + networkId)
and update the uniqueLocalVaults filter to compute the same composite key for
each localVault before checking membership; keep the final return order
[...uniqueLocalVaults, ...apiVaults].

};
5 changes: 3 additions & 2 deletions src/data-sources/morpho-api/v2-vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type VaultV2Details = {
allocators: string[];
sentinels: string[];
caps: VaultV2Cap[];
adapters: string[];
adapters: string[]; // MorphoMarketV1 adapters, both MorphoMarketV1Adapter and MorphoMarketV1AdapterV2 are included
avgApy?: number;
};

Expand Down Expand Up @@ -63,6 +63,7 @@ type ApiVaultV2 = {
adapters: {
items: {
address: string;
type: 'MorphoMarketV1' | 'MorphoMarketV2';
}[];
};
caps: {
Expand Down Expand Up @@ -104,7 +105,7 @@ function transformVault(apiVault: ApiVaultV2): VaultV2Details {
allocators: apiVault.allocators.map((a) => a.allocator.address),
sentinels: [], // Not available in API response
caps: apiVault.caps.items.map(transformCap),
adapters: apiVault.adapters.items.map((a) => a.address),
adapters: apiVault.adapters.items.filter((a) => a.type === 'MorphoMarketV1').map((a) => a.address),
avgApy: apiVault.avgApy,
};
}
Expand Down
45 changes: 0 additions & 45 deletions src/data-sources/subgraph/morpho-market-v1-adapters.ts

This file was deleted.

89 changes: 0 additions & 89 deletions src/data-sources/subgraph/v2-vaults.ts

This file was deleted.

Loading