Skip to content

Commit 34a6123

Browse files
authored
chore: plan deposit steps based on balances from spectrum etc. (#11708)
see in-person discussion test output (TODO, actually submit steps): ``` % yarn test test/deposit-tools.test.ts ✔ planDepositTransfers works in a handful of cases TODO: detail TODO: submit steps [ { src: '@Agoric', dest: '@noble', amount: { brand: Object [Alleged: USDC brand] {}, value: 1000n } }, { src: '@noble', dest: 'USDNVault', amount: { brand: Object [Alleged: USDC brand] {}, value: 475n } }, { src: '@noble', dest: '@Arbitrum', amount: { brand: Object [Alleged: USDC brand] {}, value: 305n } }, { src: '@Arbitrum', dest: 'Aave_Arbitrum', amount: { brand: Object [Alleged: USDC brand] {}, value: 305n } }, { src: '@noble', dest: '@Arbitrum', amount: { brand: Object [Alleged: USDC brand] {}, value: 220n } }, { src: '@Arbitrum', dest: 'Compound_Arbitrum', amount: { brand: Object [Alleged: USDC brand] {}, value: 220n } } ] ✔ handleDeposit handles missing targetAllocation gracefully ✔ handleDeposit works with mocked dependencies ✔ handleDeposit handles different position types correctly TODO: fees TODO: fees TODO: detail TODO: detail TODO: fees TODO: fees TODO: submit steps [ { src: '@Agoric', dest: '@noble', amount: { brand: Object [Alleged: USDC brand] {}, value: 1000n } }, { src: '@noble', dest: 'USDNVault', amount: { brand: Object [Alleged: USDC brand] {}, value: 430n } }, { src: '@noble', dest: 'USDNVault', amount: { brand: Object [Alleged: USDC brand] {}, value: 65n } }, { src: '@noble', dest: '@Avalanche', amount: { brand: Object [Alleged: USDC brand] {}, value: 306n } }, { src: '@Avalanche', dest: 'Aave_Avalanche', amount: { brand: Object [Alleged: USDC brand] {}, value: 306n } }, { src: '@noble', dest: '@polygon', amount: { brand: Object [Alleged: USDC brand] {}, value: 198n } }, { src: '@polygon', dest: 'Compound_Polygon', amount: { brand: Object [Alleged: USDC brand] {}, value: 198n } } ] ─ 4 tests passed ``` cc @turadg
2 parents 405ed25 + 718453d commit 34a6123

File tree

4 files changed

+594
-6
lines changed

4 files changed

+594
-6
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
/** @file test for deposit tools */
2+
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
3+
import { AmountMath } from '@agoric/ertp';
4+
import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js';
5+
import { planDepositTransfers } from '../tools/portfolio-actors.js';
6+
import { makeIssuerKit } from '@agoric/ertp';
7+
import type { TargetAllocation } from '../src/type-guards.js';
8+
import { handleDeposit } from '@aglocal/portfolio-planner/src/plan-deposit.ts';
9+
import type { VstorageKit } from '@agoric/client-utils';
10+
import { SpectrumClient } from '@aglocal/portfolio-planner/src/spectrum-client.ts';
11+
import { CosmosRestClient } from '@aglocal/portfolio-planner/src/cosmos-rest-client.ts';
12+
13+
const { brand } = makeIssuerKit('USDC');
14+
15+
test('planDepositTransfers works in a handful of cases', t => {
16+
const make = value => AmountMath.make(brand, value);
17+
18+
// Test case 1: Empty current balances, equal target allocation
19+
const deposit1 = make(1000n);
20+
const currentBalances1 = {};
21+
const targetAllocation1: TargetAllocation = {
22+
USDN: 50n,
23+
Aave_Arbitrum: 30n,
24+
Compound_Arbitrum: 20n,
25+
};
26+
27+
const result1 = planDepositTransfers(
28+
deposit1,
29+
currentBalances1,
30+
targetAllocation1,
31+
);
32+
33+
t.deepEqual(result1, {
34+
USDN: make(500n),
35+
Aave_Arbitrum: make(300n),
36+
Compound_Arbitrum: make(200n),
37+
});
38+
39+
// Test case 2: Existing balances, need rebalancing
40+
const deposit2 = make(500n);
41+
const currentBalances2 = {
42+
USDN: make(200n),
43+
Aave_Arbitrum: make(100n),
44+
Compound_Arbitrum: make(0n),
45+
};
46+
const targetAllocation2: TargetAllocation = {
47+
USDN: 40n,
48+
Aave_Arbitrum: 40n,
49+
Compound_Arbitrum: 20n,
50+
};
51+
52+
const result2 = planDepositTransfers(
53+
deposit2,
54+
currentBalances2,
55+
targetAllocation2,
56+
);
57+
58+
// Total after deposit: 300 + 500 = 800
59+
// Targets: USDN=320, Aave=320, Compound=160
60+
// Transfers needed: USDN=120, Aave=220, Compound=160
61+
t.deepEqual(result2, {
62+
USDN: make(120n),
63+
Aave_Arbitrum: make(220n),
64+
Compound_Arbitrum: make(160n),
65+
});
66+
67+
// Test case 3: Some positions already over-allocated
68+
const deposit3 = make(300n);
69+
const currentBalances3 = {
70+
USDN: make(600n), // already over target
71+
Aave_Arbitrum: make(100n),
72+
Compound_Arbitrum: make(50n),
73+
};
74+
const targetAllocation3: TargetAllocation = {
75+
USDN: 50n,
76+
Aave_Arbitrum: 30n,
77+
Compound_Arbitrum: 20n,
78+
};
79+
80+
const result3 = planDepositTransfers(
81+
deposit3,
82+
currentBalances3,
83+
targetAllocation3,
84+
);
85+
86+
// Total after deposit: 750 + 300 = 1050
87+
// Targets: USDN=525, Aave=315, Compound=210
88+
// USDN is already over target (600 > 525), so no transfer
89+
// Need transfers: Aave=215, Compound=160, total=375
90+
// But deposit is only 300, so scale down proportionally:
91+
// Aave: 215 * (300/375) = 172, Compound: 160 * (300/375) = 128
92+
t.deepEqual(result3, {
93+
Aave_Arbitrum: make(172n),
94+
Compound_Arbitrum: make(128n),
95+
});
96+
97+
// Test case 4: Transfer amounts exceed deposit (scaling needed)
98+
const deposit4 = make(100n);
99+
const currentBalances4 = {
100+
USDN: make(0n),
101+
Aave_Arbitrum: make(0n),
102+
Compound_Arbitrum: make(0n),
103+
};
104+
const targetAllocation4: TargetAllocation = {
105+
USDN: 60n,
106+
Aave_Arbitrum: 30n,
107+
Compound_Arbitrum: 10n,
108+
};
109+
110+
const result4 = planDepositTransfers(
111+
deposit4,
112+
currentBalances4,
113+
targetAllocation4,
114+
);
115+
116+
// Should allocate proportionally to the 100 deposit
117+
t.deepEqual(result4, {
118+
USDN: make(60n),
119+
Aave_Arbitrum: make(30n),
120+
Compound_Arbitrum: make(10n),
121+
});
122+
123+
// Test case 5: Single position target
124+
const deposit5 = make(1000n);
125+
const currentBalances5 = { USDN: make(500n) };
126+
const targetAllocation5: TargetAllocation = { USDN: 100n };
127+
128+
const result5 = planDepositTransfers(
129+
deposit5,
130+
currentBalances5,
131+
targetAllocation5,
132+
);
133+
134+
// Total after: 1500, target: 1500, current: 500, transfer: 1000
135+
t.deepEqual(result5, {
136+
USDN: make(1000n),
137+
});
138+
});
139+
140+
test('handleDeposit works with mocked dependencies', async t => {
141+
const make = value => AmountMath.make(brand, value);
142+
const deposit = make(1000n);
143+
const portfolioKey = 'test.portfolios.portfolio1' as const;
144+
145+
// Mock VstorageKit readPublished
146+
const mockReadPublished = async (path: string) => {
147+
if (path === portfolioKey) {
148+
return {
149+
positionKeys: ['USDN', 'Aave_Arbitrum', 'Compound_Arbitrum'],
150+
flowCount: 0,
151+
accountIdByChain: {
152+
noble: 'noble:test:addr1',
153+
Arbitrum: 'arbitrum:test:addr2',
154+
},
155+
targetAllocation: {
156+
USDN: 50n,
157+
Aave_Arbitrum: 30n,
158+
Compound_Arbitrum: 20n,
159+
},
160+
};
161+
}
162+
throw new Error(`Unexpected path: ${path}`);
163+
};
164+
165+
// Mock SpectrumClient
166+
class MockSpectrumClient extends SpectrumClient {
167+
async getPoolBalance(chain: any, pool: any, addr: any) {
168+
// Return different balances for different pools
169+
if (pool === 'aave' && chain === 'arbitrum') {
170+
return {
171+
pool,
172+
chain,
173+
address: addr,
174+
balance: { supplyBalance: 100, borrowAmount: 0 },
175+
};
176+
}
177+
if (pool === 'compound' && chain === 'arbitrum') {
178+
return {
179+
pool,
180+
chain,
181+
address: addr,
182+
balance: { supplyBalance: 50, borrowAmount: 0 },
183+
};
184+
}
185+
return {
186+
pool,
187+
chain,
188+
address: addr,
189+
balance: { supplyBalance: 0, borrowAmount: 0 },
190+
};
191+
}
192+
}
193+
const mockSpectrumClient = new MockSpectrumClient();
194+
195+
// Mock CosmosRestClient
196+
class MockCosmosRestClient extends CosmosRestClient {
197+
async getAccountBalance(chainName: string, addr: string, denom: string) {
198+
if (chainName === 'noble' && denom === 'usdn') {
199+
return { denom, amount: '200' };
200+
}
201+
return { denom, amount: '0' };
202+
}
203+
}
204+
const mockCosmosRestClient = new MockCosmosRestClient();
205+
206+
// Mock VstorageKit
207+
const mockVstorageKit: VstorageKit = {
208+
readPublished: mockReadPublished,
209+
} as VstorageKit;
210+
211+
try {
212+
await handleDeposit(
213+
deposit,
214+
portfolioKey,
215+
mockVstorageKit.readPublished,
216+
mockSpectrumClient,
217+
mockCosmosRestClient,
218+
);
219+
t.fail('Expected handleDeposit to throw Error with "moar TODO"');
220+
} catch (error) {
221+
t.is(error.message, 'moar TODO');
222+
}
223+
});
224+
225+
test('handleDeposit handles missing targetAllocation gracefully', async t => {
226+
const make = value => AmountMath.make(brand, value);
227+
const deposit = make(1000n);
228+
const portfolioKey = 'test.portfolios.portfolio1' as const;
229+
230+
// Mock VstorageKit readPublished with no targetAllocation
231+
const mockReadPublished = async (path: string) => {
232+
if (path === portfolioKey) {
233+
return {
234+
positionKeys: ['USDN', 'Aave_Arbitrum'],
235+
flowCount: 0,
236+
accountIdByChain: {
237+
noble: 'noble:test:addr1',
238+
Arbitrum: 'arbitrum:test:addr2',
239+
},
240+
// No targetAllocation
241+
};
242+
}
243+
throw new Error(`Unexpected path: ${path}`);
244+
};
245+
246+
// Mock SpectrumClient
247+
class MockSpectrumClient2 extends SpectrumClient {
248+
async getPoolBalance(chain: any, pool: any, addr: any) {
249+
return {
250+
pool,
251+
chain,
252+
address: addr,
253+
balance: { supplyBalance: 0, borrowAmount: 0 },
254+
};
255+
}
256+
}
257+
const mockSpectrumClient = new MockSpectrumClient2();
258+
259+
// Mock CosmosRestClient
260+
class MockCosmosRestClient2 extends CosmosRestClient {
261+
async getAccountBalance(chainName: string, addr: string, denom: string) {
262+
return { denom, amount: '0' };
263+
}
264+
}
265+
const mockCosmosRestClient = new MockCosmosRestClient2();
266+
267+
// Mock VstorageKit
268+
const mockVstorageKit: VstorageKit = {
269+
readPublished: mockReadPublished,
270+
} as VstorageKit;
271+
272+
const result = await handleDeposit(
273+
deposit,
274+
portfolioKey,
275+
mockVstorageKit.readPublished,
276+
mockSpectrumClient,
277+
mockCosmosRestClient,
278+
);
279+
280+
t.is(result, undefined);
281+
});
282+
283+
test('handleDeposit handles different position types correctly', async t => {
284+
const make = value => AmountMath.make(brand, value);
285+
const deposit = make(1000n);
286+
const portfolioKey = 'test.portfolios.portfolio1' as const;
287+
288+
// Mock VstorageKit readPublished with various position types
289+
const mockReadPublished = async (path: string) => {
290+
if (path === portfolioKey) {
291+
return {
292+
positionKeys: [
293+
'USDN',
294+
'USDNVault',
295+
'Aave_Avalanche',
296+
'Compound_Polygon',
297+
],
298+
flowCount: 0,
299+
accountIdByChain: {
300+
noble: 'noble:test:addr1',
301+
Avalanche: 'avalanche:test:addr2',
302+
Polygon: 'polygon:test:addr3',
303+
},
304+
targetAllocation: {
305+
USDN: 40n,
306+
USDNVault: 20n,
307+
Aave_Avalanche: 25n,
308+
Compound_Polygon: 15n,
309+
},
310+
};
311+
}
312+
throw new Error(`Unexpected path: ${path}`);
313+
};
314+
315+
// Mock SpectrumClient with different chain responses
316+
class MockSpectrumClient3 extends SpectrumClient {
317+
async getPoolBalance(chain: any, pool: any, addr: any) {
318+
if (chain === 'avalanche' && pool === 'aave') {
319+
return {
320+
pool,
321+
chain,
322+
address: addr,
323+
balance: { supplyBalance: 150, borrowAmount: 0 },
324+
};
325+
}
326+
if (chain === 'polygon' && pool === 'compound') {
327+
return {
328+
pool,
329+
chain,
330+
address: addr,
331+
balance: { supplyBalance: 75, borrowAmount: 0 },
332+
};
333+
}
334+
return {
335+
pool,
336+
chain,
337+
address: addr,
338+
balance: { supplyBalance: 0, borrowAmount: 0 },
339+
};
340+
}
341+
}
342+
const mockSpectrumClient = new MockSpectrumClient3();
343+
344+
// Mock CosmosRestClient
345+
class MockCosmosRestClient3 extends CosmosRestClient {
346+
async getAccountBalance(chainName: string, addr: string, denom: string) {
347+
if (chainName === 'noble' && denom === 'usdn') {
348+
return { denom, amount: '300' };
349+
}
350+
return { denom, amount: '0' };
351+
}
352+
}
353+
const mockCosmosRestClient = new MockCosmosRestClient3();
354+
355+
// Mock VstorageKit
356+
const mockVstorageKit: VstorageKit = {
357+
readPublished: mockReadPublished,
358+
} as VstorageKit;
359+
360+
try {
361+
await handleDeposit(
362+
deposit,
363+
portfolioKey,
364+
mockVstorageKit.readPublished,
365+
mockSpectrumClient,
366+
mockCosmosRestClient,
367+
);
368+
t.fail('Expected handleDeposit to throw Error with "moar TODO"');
369+
} catch (error) {
370+
t.is(error.message, 'moar TODO');
371+
}
372+
});

0 commit comments

Comments
 (0)