Skip to content
Open
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
8 changes: 7 additions & 1 deletion packages/sdk/.env.test.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@

# Privy Configuration - optional for supersim feature spec
PRIVY_APP_ID=your_privy_app_id_here
PRIVY_APP_SECRET=your_privy_app_secret_here
PRIVY_APP_SECRET=your_privy_app_secret_here
# Network fork tests (src/test/network/)
# Required in CI; falls back to public RPCs locally.
# Use a dedicated provider (Alchemy, Infura, etc.) for reliable test runs.
MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
OP_MAINNET_RPC=https://opt-mainnet.g.alchemy.com/v2/YOUR_KEY
BASE_MAINNET_RPC=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Network fork tests for ActionsLendNamespace (read-only).
*
* Validates that the read-only namespace correctly delegates to providers
* for market discovery and reads without requiring a wallet.
*
* Run: pnpm test:network
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest'

import type { SupportedChainId } from '@/constants/supportedChains.js'
import { ActionsLendNamespace } from '@/lend/namespaces/ActionsLendNamespace.js'
import { AaveLendProvider } from '@/lend/providers/aave/AaveLendProvider.js'
import { MorphoLendProvider } from '@/lend/providers/morpho/MorphoLendProvider.js'
import {
FORK_CHAINS,
MORPHO_VAULTS,
OP_WETH,
} from '@/test/network/fixtures/index.js'
import {
type AnvilFork,
startFork,
stopAllForks,
} from '@/test/network/harness/index.js'
import { createForkChainManager } from '@/test/network/harness/wallets.js'

let opFork: AnvilFork
let baseFork: AnvilFork

describe('ActionsLendNamespace network fork tests (read-only)', () => {
beforeAll(async () => {
;[opFork, baseFork] = await Promise.all([
startFork(FORK_CHAINS.optimism),
startFork(FORK_CHAINS.base),
])
}, 60_000)

afterAll(() => stopAllForks())

describe('single provider — Aave on Optimism', () => {
it('getMarkets returns non-empty list', async () => {
const forkMap = new Map<SupportedChainId, AnvilFork>()
forkMap.set(opFork.config.chainId, opFork)
const chainManager = createForkChainManager(forkMap)

const aave = new AaveLendProvider({}, chainManager)
const ns = new ActionsLendNamespace({ aave })

const markets = await ns.getMarkets({
chainId: opFork.config.chainId,
})

expect(markets.length).toBeGreaterThan(0)
for (const m of markets) {
expect(m.marketId.chainId).toBe(opFork.config.chainId)
}
})

it('getMarket returns valid WETH data', async () => {
const forkMap = new Map<SupportedChainId, AnvilFork>()
forkMap.set(opFork.config.chainId, opFork)
const chainManager = createForkChainManager(forkMap)

const aave = new AaveLendProvider({}, chainManager)
const ns = new ActionsLendNamespace({ aave })

const wethAddress = OP_WETH.address[opFork.config.chainId]!

const market = await ns.getMarket({
address: wethAddress as `0x${string}`,
chainId: opFork.config.chainId,
})

expect(market.marketId.address).toBe(wethAddress)
expect(market.apy.total).toBeGreaterThanOrEqual(0)
})

it('supportedChainIds includes Optimism', () => {
const forkMap = new Map<SupportedChainId, AnvilFork>()
forkMap.set(opFork.config.chainId, opFork)
const chainManager = createForkChainManager(forkMap)

const aave = new AaveLendProvider({}, chainManager)
const ns = new ActionsLendNamespace({ aave })

expect(ns.supportedChainIds()).toContain(opFork.config.chainId)
})
})

describe('single provider — Aave on Base', () => {
it('getMarkets returns non-empty list', async () => {
const forkMap = new Map<SupportedChainId, AnvilFork>()
forkMap.set(baseFork.config.chainId, baseFork)
const chainManager = createForkChainManager(forkMap)

const aave = new AaveLendProvider({}, chainManager)
const ns = new ActionsLendNamespace({ aave })

const markets = await ns.getMarkets({
chainId: baseFork.config.chainId,
})

expect(markets.length).toBeGreaterThan(0)
})
})

describe('multi-provider — Aave + Morpho on Optimism', () => {
it('getMarkets aggregates from both providers', async () => {
const forkMap = new Map<SupportedChainId, AnvilFork>()
forkMap.set(opFork.config.chainId, opFork)
const chainManager = createForkChainManager(forkMap)

const aave = new AaveLendProvider({}, chainManager)
const morpho = new MorphoLendProvider(
{ marketAllowlist: [MORPHO_VAULTS.opSteakhouseUSDC] },
chainManager,
)

const ns = new ActionsLendNamespace({ aave, morpho })

const markets = await ns.getMarkets({
chainId: opFork.config.chainId,
})

// Aave alone returns many markets on OP, so a count check is not enough.
// Explicitly verify the pinned Morpho vault is present.
const marketAddresses = markets.map((m) =>
m.marketId.address.toLowerCase(),
)
expect(marketAddresses).toContain(
MORPHO_VAULTS.opSteakhouseUSDC.address.toLowerCase(),
)
// At least one Aave market must also be present
const nonMorphoMarkets = markets.filter(
(m) =>
m.marketId.address.toLowerCase() !==
MORPHO_VAULTS.opSteakhouseUSDC.address.toLowerCase(),
)
expect(nonMorphoMarkets.length).toBeGreaterThan(0)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Network fork system tests for WalletLendNamespace.
*
* Tests the full lend lifecycle: openPosition -> getPosition -> closePosition -> verify.
* Uses a real EOAWallet subclass backed by deterministic Anvil accounts.
*
* Run: pnpm test:network
*/
import type { Address } from 'viem'
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
} from 'vitest'

import type { SupportedChainId } from '@/constants/supportedChains.js'
import { WalletLendNamespace } from '@/lend/namespaces/WalletLendNamespace.js'
import { AaveLendProvider } from '@/lend/providers/aave/AaveLendProvider.js'
import { FORK_CHAINS, OP_USDC } from '@/test/network/fixtures/index.js'
import { expectReceiptSuccess } from '@/test/network/harness/assertions.js'
import {
type AnvilFork,
fundERC20,
fundETH,
revert,
snapshot,
startFork,
stopAllForks,
} from '@/test/network/harness/index.js'
import {
createForkChainManager,
TestEOAWallet,
} from '@/test/network/harness/wallets.js'

let opFork: AnvilFork
let forkMap: Map<SupportedChainId, AnvilFork>
let snapshotId: string

describe('WalletLendNamespace network fork tests', () => {
beforeAll(async () => {
opFork = await startFork(FORK_CHAINS.optimism)
forkMap = new Map<SupportedChainId, AnvilFork>()
forkMap.set(opFork.config.chainId, opFork)
}, 60_000)

afterAll(() => stopAllForks())

beforeEach(async () => {
snapshotId = await snapshot(opFork)
})

afterEach(async () => {
await revert(opFork, snapshotId)
})

describe('Optimism — Aave USDC lend lifecycle', () => {
it('openPosition -> getPosition -> closePosition', async () => {
const chainManager = createForkChainManager(forkMap)
const wallet = await TestEOAWallet.create(chainManager)
const chainId = opFork.config.chainId
const usdcAddress = OP_USDC.address[chainId]! as Address

await fundETH(opFork, wallet.address)
await fundERC20(opFork, wallet.address, OP_USDC, 1_000_000_000n) // 1000 USDC

const provider = new AaveLendProvider({}, chainManager)

const lendNs = new WalletLendNamespace({ aave: provider }, wallet)

const marketId = { address: usdcAddress, chainId }

const openReceipt = await lendNs.openPosition({
marketId,
amount: 100,
asset: OP_USDC,
})
expectReceiptSuccess(openReceipt)

const position = await lendNs.getPosition({
marketId,
asset: OP_USDC,
})
expect(position.balance).toBeGreaterThan(0)
expect(position.balanceFormatted).toBeGreaterThan(0)

const closeReceipt = await lendNs.closePosition({
marketId,
amount: 50,
asset: OP_USDC,
})
expectReceiptSuccess(closeReceipt)

const positionAfter = await lendNs.getPosition({
marketId,
asset: OP_USDC,
})
expect(positionAfter.balance).toBeLessThan(position.balance)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Network fork tests for AaveLendProvider.
*
* Validates market discovery, position reads, and transaction construction
* against real deployed Aave V3 contracts on Optimism and Base.
*
* Run: pnpm test:network
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest'

import type { SupportedChainId } from '@/constants/supportedChains.js'
import { AaveLendProvider } from '@/lend/providers/aave/AaveLendProvider.js'
import {
BASE_USDC,
BASE_WETH,
FORK_CHAINS,
OP_USDC,
OP_WETH,
} from '@/test/network/fixtures/index.js'
import {
type AnvilFork,
startFork,
stopAllForks,
} from '@/test/network/harness/index.js'
import { createForkChainManager } from '@/test/network/harness/wallets.js'
import type { LendProviderConfig } from '@/types/actions.js'

let opFork: AnvilFork
let baseFork: AnvilFork

function createProvider(
forks: AnvilFork[],
config: LendProviderConfig = {},
): AaveLendProvider {
const forkMap = new Map<SupportedChainId, AnvilFork>()
for (const f of forks) forkMap.set(f.config.chainId, f)
const chainManager = createForkChainManager(forkMap)
return new AaveLendProvider(config, chainManager)
}

describe('AaveLendProvider network fork tests', () => {
beforeAll(async () => {
;[opFork, baseFork] = await Promise.all([
startFork(FORK_CHAINS.optimism),
startFork(FORK_CHAINS.base),
])
}, 60_000)

afterAll(() => stopAllForks())

describe('Optimism', () => {
it('getMarkets returns non-empty market list', async () => {
const provider = createProvider([opFork])

const markets = await provider.getMarkets({
chainId: opFork.config.chainId,
})

expect(markets.length).toBeGreaterThan(0)
for (const market of markets) {
expect(market.marketId.chainId).toBe(opFork.config.chainId)
expect(market.asset).toBeDefined()
expect(market.apy).toBeDefined()
}
})

it('getMarket returns valid WETH market data', async () => {
const provider = createProvider([opFork])
const wethAddress = OP_WETH.address[opFork.config.chainId]!

const market = await provider.getMarket({
address: wethAddress as `0x${string}`,
chainId: opFork.config.chainId,
})

expect(market.marketId.address).toBe(wethAddress)
expect(market.marketId.chainId).toBe(opFork.config.chainId)
expect(market.apy.total).toBeGreaterThanOrEqual(0)
expect(market.supply.totalAssets).toBeGreaterThan(0)
})

it('openPosition builds valid supply transaction for USDC', async () => {
const provider = createProvider([opFork])
const usdcAddress = OP_USDC.address[opFork.config.chainId]!

const tx = await provider.openPosition({
marketId: {
address: usdcAddress as `0x${string}`,
chainId: opFork.config.chainId,
},
amount: 100,
asset: OP_USDC,
walletAddress: '0x000000000000000000000000000000000000dEaD',
})

expect(tx.transactionData.position.to).toMatch(/^0x/)
expect(tx.transactionData.position.data).toMatch(/^0x/)
expect(tx.amount).toBeGreaterThan(0n)
expect(tx.asset).toBe(usdcAddress)
// Aave ERC20 supply requires an approval
expect(tx.transactionData.approval).toBeDefined()
expect(tx.transactionData.approval!.to).toBe(usdcAddress)
})
})

describe('Base', () => {
it('getMarkets returns non-empty market list', async () => {
const provider = createProvider([baseFork])

const markets = await provider.getMarkets({
chainId: baseFork.config.chainId,
})

expect(markets.length).toBeGreaterThan(0)
})

it('getMarket returns valid USDC market data', async () => {
const provider = createProvider([baseFork])
const usdcAddress = BASE_USDC.address[baseFork.config.chainId]!

const market = await provider.getMarket({
address: usdcAddress as `0x${string}`,
chainId: baseFork.config.chainId,
})

expect(market.marketId.address).toBe(usdcAddress)
expect(market.apy.total).toBeGreaterThanOrEqual(0)
})
})
})
Loading