Skip to content

Commit 757d263

Browse files
authored
Merge pull request #102 from ethereum-optimism/kevin-67/refactor-lendprovider
refactor LendProvider
2 parents c07f6a2 + 750687f commit 757d263

File tree

17 files changed

+625
-47
lines changed

17 files changed

+625
-47
lines changed

packages/demo/backend/src/services/lend.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,13 @@ export async function deposit(
117117
throw new Error(`Wallet not found for user ID: ${walletId}`)
118118
}
119119

120-
return await wallet.lend(amount, token.toLowerCase(), chainId)
120+
if ('lendExecute' in wallet && typeof wallet.lendExecute === 'function') {
121+
return await wallet.lendExecute(amount, token.toLowerCase(), chainId)
122+
} else {
123+
throw new Error(
124+
'Lend functionality not yet implemented for this wallet type.',
125+
)
126+
}
121127
}
122128

123129
export async function executeLendTransaction(
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
import { createMockLendProvider } from '@/test/MockLendProvider.js'
4+
import { getRandomAddress } from '@/test/utils.js'
5+
import type { LendProvider } from '@/types/lend.js'
6+
7+
import { VerbsLendNamespace } from './VerbsLendNamespace.js'
8+
9+
describe('VerbsLendNamespace', () => {
10+
let mockProvider: LendProvider
11+
12+
beforeEach(() => {
13+
mockProvider = createMockLendProvider()
14+
})
15+
16+
it('should create an instance with a lend provider', () => {
17+
const namespace = new VerbsLendNamespace(mockProvider)
18+
19+
expect(namespace).toBeInstanceOf(VerbsLendNamespace)
20+
})
21+
22+
it('should delegate getVaults to provider', async () => {
23+
const namespace = new VerbsLendNamespace(mockProvider)
24+
const mockVaults = [
25+
{
26+
chainId: 130,
27+
address: getRandomAddress(),
28+
name: 'Test Vault',
29+
asset: getRandomAddress(),
30+
totalAssets: BigInt('1000000'),
31+
totalShares: BigInt('1000000'),
32+
apy: 0.05,
33+
apyBreakdown: {
34+
nativeApy: 0.04,
35+
totalRewardsApr: 0.01,
36+
performanceFee: 0.0,
37+
netApy: 0.05,
38+
},
39+
owner: getRandomAddress(),
40+
curator: getRandomAddress(),
41+
fee: 0.1,
42+
lastUpdate: Date.now(),
43+
},
44+
]
45+
46+
vi.mocked(mockProvider.getVaults).mockResolvedValue(mockVaults)
47+
48+
const result = await namespace.getVaults()
49+
50+
expect(mockProvider.getVaults).toHaveBeenCalled()
51+
expect(result).toBe(mockVaults)
52+
})
53+
54+
it('should delegate getVault to provider', async () => {
55+
const namespace = new VerbsLendNamespace(mockProvider)
56+
const vaultAddress = getRandomAddress()
57+
const mockVault = {
58+
chainId: 130,
59+
address: vaultAddress,
60+
name: 'Test Vault',
61+
asset: getRandomAddress(),
62+
totalAssets: BigInt('1000000'),
63+
totalShares: BigInt('1000000'),
64+
apy: 0.05,
65+
apyBreakdown: {
66+
nativeApy: 0.04,
67+
totalRewardsApr: 0.01,
68+
performanceFee: 0.0,
69+
netApy: 0.05,
70+
},
71+
owner: getRandomAddress(),
72+
curator: getRandomAddress(),
73+
fee: 0.1,
74+
lastUpdate: Date.now(),
75+
}
76+
77+
vi.mocked(mockProvider.getVault).mockResolvedValue(mockVault)
78+
79+
const result = await namespace.getVault(vaultAddress)
80+
81+
expect(mockProvider.getVault).toHaveBeenCalledWith(vaultAddress)
82+
expect(result).toBe(mockVault)
83+
})
84+
85+
it('should delegate getVaultBalance to provider', async () => {
86+
const namespace = new VerbsLendNamespace(mockProvider)
87+
const vaultAddress = getRandomAddress()
88+
const walletAddress = getRandomAddress()
89+
const mockBalance = {
90+
balance: BigInt('500000'),
91+
balanceFormatted: '500.000',
92+
shares: BigInt('500000'),
93+
sharesFormatted: '0.5',
94+
chainId: 130,
95+
}
96+
97+
vi.mocked(mockProvider.getVaultBalance).mockResolvedValue(mockBalance)
98+
99+
const result = await namespace.getVaultBalance(vaultAddress, walletAddress)
100+
101+
expect(mockProvider.getVaultBalance).toHaveBeenCalledWith(
102+
vaultAddress,
103+
walletAddress,
104+
)
105+
expect(result).toBe(mockBalance)
106+
})
107+
108+
it('should delegate supportedNetworkIds to provider', () => {
109+
const namespace = new VerbsLendNamespace(mockProvider)
110+
const mockNetworkIds = [130, 8453]
111+
112+
vi.mocked(mockProvider.supportedNetworkIds).mockReturnValue(mockNetworkIds)
113+
114+
const result = namespace.supportedNetworkIds()
115+
116+
expect(mockProvider.supportedNetworkIds).toHaveBeenCalled()
117+
expect(result).toBe(mockNetworkIds)
118+
})
119+
120+
it('should provide access to the underlying provider', () => {
121+
const namespace = new VerbsLendNamespace(mockProvider)
122+
123+
expect(namespace['provider']).toBe(mockProvider)
124+
})
125+
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Address } from 'viem'
2+
3+
import type { LendProvider } from '@/lend/provider.js'
4+
import type { LendVaultInfo } from '@/types/lend.js'
5+
6+
/**
7+
* Verbs Lend Namespace
8+
* @description Read-only lending operations available on verbs.lend
9+
*/
10+
export class VerbsLendNamespace {
11+
constructor(protected readonly provider: LendProvider) {}
12+
13+
/**
14+
* Get list of available lending vaults
15+
*/
16+
getVaults(): Promise<LendVaultInfo[]> {
17+
return this.provider.getVaults()
18+
}
19+
20+
/**
21+
* Get detailed information for a specific vault
22+
*/
23+
getVault(vaultAddress: Address): Promise<LendVaultInfo> {
24+
return this.provider.getVault(vaultAddress)
25+
}
26+
27+
/**
28+
* Get vault balance for a specific wallet
29+
*/
30+
getVaultBalance(
31+
vaultAddress: Address,
32+
walletAddress: Address,
33+
): Promise<{
34+
balance: bigint
35+
balanceFormatted: string
36+
shares: bigint
37+
sharesFormatted: string
38+
chainId: number
39+
}> {
40+
return this.provider.getVaultBalance(vaultAddress, walletAddress)
41+
}
42+
43+
/**
44+
* Get list of supported network IDs
45+
*/
46+
supportedNetworkIds(): number[] {
47+
return this.provider.supportedNetworkIds()
48+
}
49+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
import { createMockLendProvider } from '@/test/MockLendProvider.js'
4+
import { getRandomAddress } from '@/test/utils.js'
5+
import type { LendProvider } from '@/types/lend.js'
6+
7+
import { WalletLendNamespace } from './WalletLendNamespace.js'
8+
9+
describe('WalletLendNamespace', () => {
10+
const mockWalletAddress = getRandomAddress()
11+
let mockProvider: LendProvider
12+
13+
beforeEach(() => {
14+
mockProvider = createMockLendProvider()
15+
})
16+
17+
it('should create an instance with a lend provider and wallet address', () => {
18+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
19+
20+
expect(namespace).toBeInstanceOf(WalletLendNamespace)
21+
})
22+
23+
it('should inherit read operations from VerbsLendNamespace', async () => {
24+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
25+
const mockVaults = [
26+
{
27+
chainId: 130,
28+
address: getRandomAddress(),
29+
name: 'Test Vault',
30+
asset: getRandomAddress(),
31+
totalAssets: BigInt('1000000'),
32+
totalShares: BigInt('1000000'),
33+
apy: 0.05,
34+
apyBreakdown: {
35+
nativeApy: 0.04,
36+
totalRewardsApr: 0.01,
37+
performanceFee: 0.0,
38+
netApy: 0.05,
39+
},
40+
owner: getRandomAddress(),
41+
curator: getRandomAddress(),
42+
fee: 0.1,
43+
lastUpdate: Date.now(),
44+
},
45+
]
46+
47+
vi.mocked(mockProvider.getVaults).mockResolvedValue(mockVaults)
48+
49+
const result = await namespace.getVaults()
50+
51+
expect(mockProvider.getVaults).toHaveBeenCalled()
52+
expect(result).toBe(mockVaults)
53+
})
54+
55+
describe('lendExecute', () => {
56+
it('should call provider lend with wallet address as receiver', async () => {
57+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
58+
const asset = getRandomAddress()
59+
const amount = BigInt('1000000')
60+
const marketId = 'test-market'
61+
const mockTransaction = {
62+
amount,
63+
asset,
64+
marketId,
65+
apy: 0.05,
66+
timestamp: Date.now(),
67+
transactionData: {
68+
deposit: {
69+
to: asset,
70+
value: 0n,
71+
data: '0x' as const,
72+
},
73+
},
74+
slippage: 50,
75+
}
76+
77+
vi.mocked(mockProvider.lend).mockResolvedValue(mockTransaction)
78+
79+
const result = await namespace.lendExecute(asset, amount, marketId)
80+
81+
expect(mockProvider.lend).toHaveBeenCalledWith(asset, amount, marketId, {
82+
receiver: mockWalletAddress,
83+
})
84+
expect(result).toBe(mockTransaction)
85+
})
86+
87+
it('should preserve custom receiver in options', async () => {
88+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
89+
const asset = getRandomAddress()
90+
const amount = BigInt('1000000')
91+
const customReceiver = getRandomAddress()
92+
const options = { receiver: customReceiver, slippage: 100 }
93+
94+
await namespace.lendExecute(asset, amount, undefined, options)
95+
96+
expect(mockProvider.lend).toHaveBeenCalledWith(
97+
asset,
98+
amount,
99+
undefined,
100+
options,
101+
)
102+
})
103+
})
104+
105+
describe('deposit', () => {
106+
it('should delegate to lendExecute', async () => {
107+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
108+
const asset = getRandomAddress()
109+
const amount = BigInt('1000000')
110+
const marketId = 'test-market'
111+
const options = { slippage: 75 }
112+
113+
const lendExecuteSpy = vi.spyOn(namespace, 'lendExecute')
114+
const mockTransaction = {
115+
amount,
116+
asset,
117+
marketId,
118+
apy: 0.05,
119+
timestamp: Date.now(),
120+
transactionData: {
121+
deposit: {
122+
to: asset,
123+
value: 0n,
124+
data: '0x' as const,
125+
},
126+
},
127+
slippage: 75,
128+
}
129+
lendExecuteSpy.mockResolvedValue(mockTransaction)
130+
131+
const result = await namespace.deposit(asset, amount, marketId, options)
132+
133+
expect(lendExecuteSpy).toHaveBeenCalledWith(
134+
asset,
135+
amount,
136+
marketId,
137+
options,
138+
)
139+
expect(result).toBe(mockTransaction)
140+
})
141+
})
142+
143+
describe('withdraw', () => {
144+
it('should call provider withdraw with wallet address as receiver', async () => {
145+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
146+
const asset = getRandomAddress()
147+
const amount = BigInt('500000')
148+
const marketId = 'test-market'
149+
const mockTransaction = {
150+
amount,
151+
asset,
152+
marketId,
153+
apy: 0.05,
154+
timestamp: Date.now(),
155+
transactionData: {
156+
deposit: {
157+
to: asset,
158+
value: 0n,
159+
data: '0x' as const,
160+
},
161+
},
162+
slippage: 50,
163+
}
164+
165+
vi.mocked(mockProvider.withdraw).mockResolvedValue(mockTransaction)
166+
167+
const result = await namespace.withdraw(asset, amount, marketId)
168+
169+
expect(mockProvider.withdraw).toHaveBeenCalledWith(
170+
asset,
171+
amount,
172+
marketId,
173+
{
174+
receiver: mockWalletAddress,
175+
},
176+
)
177+
expect(result).toBe(mockTransaction)
178+
})
179+
180+
it('should preserve custom receiver in options', async () => {
181+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
182+
const asset = getRandomAddress()
183+
const amount = BigInt('500000')
184+
const customReceiver = getRandomAddress()
185+
const options = { receiver: customReceiver, slippage: 200 }
186+
187+
await namespace.withdraw(asset, amount, undefined, options)
188+
189+
expect(mockProvider.withdraw).toHaveBeenCalledWith(
190+
asset,
191+
amount,
192+
undefined,
193+
options,
194+
)
195+
})
196+
})
197+
198+
it('should store the wallet address', () => {
199+
const namespace = new WalletLendNamespace(mockProvider, mockWalletAddress)
200+
201+
expect(namespace['address']).toBe(mockWalletAddress)
202+
})
203+
})

0 commit comments

Comments
 (0)