@kehtus/proportion-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SDK_IMPROVEMENT_PLAN.md +197 -0
- package/arch.md +448 -0
- package/dist/__tests__/account.test.d.ts +1 -0
- package/dist/__tests__/account.test.js +36 -0
- package/dist/__tests__/indexer.test.d.ts +1 -0
- package/dist/__tests__/indexer.test.js +230 -0
- package/dist/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/__tests__/lifecycle.test.js +186 -0
- package/dist/__tests__/reads.test.d.ts +1 -0
- package/dist/__tests__/reads.test.js +185 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +185 -0
- package/dist/abi/factory.d.ts +308 -0
- package/dist/abi/factory.js +399 -0
- package/dist/abi/fundedAccount.d.ts +1074 -0
- package/dist/abi/fundedAccount.js +1373 -0
- package/dist/abi/mainVault.d.ts +1448 -0
- package/dist/abi/mainVault.js +1865 -0
- package/dist/account.d.ts +55 -0
- package/dist/account.js +290 -0
- package/dist/constants.d.ts +48 -0
- package/dist/constants.js +42 -0
- package/dist/factory.d.ts +23 -0
- package/dist/factory.js +139 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +14 -0
- package/dist/indexer.d.ts +57 -0
- package/dist/indexer.js +540 -0
- package/dist/sdk.d.ts +22 -0
- package/dist/sdk.js +89 -0
- package/dist/types.d.ts +384 -0
- package/dist/types.js +11 -0
- package/dist/utils.d.ts +21 -0
- package/dist/utils.js +89 -0
- package/dist/vault.d.ts +60 -0
- package/dist/vault.js +429 -0
- package/package.json +32 -0
- package/src/__tests__/account.test.ts +40 -0
- package/src/__tests__/indexer.test.ts +255 -0
- package/src/__tests__/lifecycle.test.ts +231 -0
- package/src/__tests__/reads.test.ts +209 -0
- package/src/__tests__/utils.test.ts +245 -0
- package/src/abi/factory.ts +399 -0
- package/src/abi/fundedAccount.ts +1373 -0
- package/src/abi/mainVault.ts +1865 -0
- package/src/account.ts +339 -0
- package/src/constants.ts +50 -0
- package/src/factory.ts +157 -0
- package/src/index.ts +16 -0
- package/src/indexer.ts +636 -0
- package/src/sdk.ts +93 -0
- package/src/types.ts +426 -0
- package/src/utils.ts +143 -0
- package/src/vault.ts +479 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { IndexerModule } from "../indexer.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
|
|
5
|
+
// ── Setup ────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const GRAPHQL_URL = process.env.GRAPHQL_URL;
|
|
8
|
+
if (!GRAPHQL_URL) throw new Error("GRAPHQL_URL required in .env for indexer tests");
|
|
9
|
+
|
|
10
|
+
const indexer = new IndexerModule(GRAPHQL_URL);
|
|
11
|
+
|
|
12
|
+
// We'll find an existing account address from the indexer to test detail queries
|
|
13
|
+
let existingAccountId: string | null = null;
|
|
14
|
+
let existingOwner: string | null = null;
|
|
15
|
+
let activeVaultId: string | null = null;
|
|
16
|
+
let activeFactoryId: string | null = null;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
try {
|
|
20
|
+
const protocol = await indexer.getProtocolConfig();
|
|
21
|
+
if (protocol) {
|
|
22
|
+
activeVaultId = protocol.vaultAddress;
|
|
23
|
+
activeFactoryId = protocol.factoryAddress;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// protocol config may be unavailable on older indexers
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const accounts = await indexer.getAccountsByState("ACTIVE");
|
|
31
|
+
if (accounts.length > 0) {
|
|
32
|
+
existingAccountId = accounts[0].id;
|
|
33
|
+
existingOwner = accounts[0].owner;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// indexer may have no active accounts
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── Protocol ───────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe("indexer: protocol", () => {
|
|
43
|
+
it("getProtocolConfig returns deployment pointer or null", async () => {
|
|
44
|
+
const protocol = await indexer.getProtocolConfig();
|
|
45
|
+
if (!protocol) {
|
|
46
|
+
console.log(" [SKIP] no ProtocolConfig entity in indexer yet");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
expect(protocol.id).toBeTruthy();
|
|
50
|
+
expect(protocol.vaultAddress).toBeTruthy();
|
|
51
|
+
expect(protocol.factoryAddress).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── Vault ────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
describe("indexer: vault", () => {
|
|
58
|
+
it("getVault returns vault data or null", async () => {
|
|
59
|
+
const vault = await indexer.getVault();
|
|
60
|
+
if (!vault) {
|
|
61
|
+
console.log(" [SKIP] no Vault entity in indexer yet");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
expect(vault.id).toBeTruthy();
|
|
65
|
+
expect(vault.address).toBeTruthy();
|
|
66
|
+
expect(typeof vault.totalAccountsActivated).toBe("number");
|
|
67
|
+
expect(typeof vault.paused).toBe("boolean");
|
|
68
|
+
expect(typeof vault.totalAccountsActive).toBe("number");
|
|
69
|
+
expect(vault).toHaveProperty("cumulativeProtocolFeesRouted");
|
|
70
|
+
expect(vault).toHaveProperty("cumulativeRescuedFunds");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── Accounts ─────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("indexer: accounts", () => {
|
|
77
|
+
it("getAccountsByState returns array", async () => {
|
|
78
|
+
const accounts = await indexer.getAccountsByState("ACTIVE");
|
|
79
|
+
expect(Array.isArray(accounts)).toBe(true);
|
|
80
|
+
if (accounts.length > 0) {
|
|
81
|
+
expect(accounts[0].state).toBe("ACTIVE");
|
|
82
|
+
expect(accounts[0].id).toBeTruthy();
|
|
83
|
+
expect(accounts[0].owner).toBeTruthy();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("getAccountsByStates returns array", async () => {
|
|
88
|
+
const accounts = await indexer.getAccountsByStates(["ACTIVE", "CLOSING"]);
|
|
89
|
+
expect(Array.isArray(accounts)).toBe(true);
|
|
90
|
+
for (const acc of accounts) {
|
|
91
|
+
expect(["ACTIVE", "CLOSING"]).toContain(acc.state);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("getAccount returns single account or null", async () => {
|
|
96
|
+
if (!existingAccountId) {
|
|
97
|
+
console.log(" [SKIP] no existing accounts");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const acc = await indexer.getAccount(existingAccountId);
|
|
101
|
+
expect(acc).not.toBeNull();
|
|
102
|
+
expect(acc!.id).toBe(existingAccountId);
|
|
103
|
+
expect(acc!.owner).toBeTruthy();
|
|
104
|
+
expect(acc!.vaultAddress).toBeTruthy();
|
|
105
|
+
expect(acc!.factoryAddress).toBeTruthy();
|
|
106
|
+
expect(typeof acc!.drawdownBps).toBe("number");
|
|
107
|
+
expect(typeof acc!.dailyDrawdownEnabled).toBe("boolean");
|
|
108
|
+
expect(acc).toHaveProperty("closePendingSubFee");
|
|
109
|
+
expect(acc).toHaveProperty("closePendingProfit");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("getAccountsByOwner returns accounts", async () => {
|
|
113
|
+
if (!existingOwner) {
|
|
114
|
+
console.log(" [SKIP] no existing owner");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const accounts = await indexer.getAccountsByOwner(existingOwner);
|
|
118
|
+
expect(Array.isArray(accounts)).toBe(true);
|
|
119
|
+
expect(accounts.length).toBeGreaterThan(0);
|
|
120
|
+
expect(accounts[0].owner.toLowerCase()).toBe(existingOwner.toLowerCase());
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── History ──────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe("indexer: history", () => {
|
|
127
|
+
it("getCheckpoints returns array", async () => {
|
|
128
|
+
if (!existingAccountId) return;
|
|
129
|
+
const checkpoints = await indexer.getCheckpoints(existingAccountId);
|
|
130
|
+
expect(Array.isArray(checkpoints)).toBe(true);
|
|
131
|
+
if (checkpoints.length > 0) {
|
|
132
|
+
expect(checkpoints[0].dayNumber).toBeTruthy();
|
|
133
|
+
expect(typeof checkpoints[0].profitable).toBe("boolean");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("getWithdrawals returns array", async () => {
|
|
138
|
+
if (!existingAccountId) return;
|
|
139
|
+
const withdrawals = await indexer.getWithdrawals(existingAccountId);
|
|
140
|
+
expect(Array.isArray(withdrawals)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("getSubscriptionPayments returns array", async () => {
|
|
144
|
+
if (!existingAccountId) return;
|
|
145
|
+
const payments = await indexer.getSubscriptionPayments(existingAccountId);
|
|
146
|
+
expect(Array.isArray(payments)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("getEvents returns array", async () => {
|
|
150
|
+
if (!existingAccountId) return;
|
|
151
|
+
const events = await indexer.getEvents(existingAccountId);
|
|
152
|
+
expect(Array.isArray(events)).toBe(true);
|
|
153
|
+
if (events.length > 0) {
|
|
154
|
+
expect(events[0].eventType).toBeTruthy();
|
|
155
|
+
expect(events[0].txHash).toBeTruthy();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("getEventsByType returns array", async () => {
|
|
160
|
+
const events = await indexer.getEventsByType("Initialized", 5);
|
|
161
|
+
expect(Array.isArray(events)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── LP ───────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("indexer: LP", () => {
|
|
168
|
+
it("getLP returns null for unknown address", async () => {
|
|
169
|
+
const lp = await indexer.getLP("0x0000000000000000000000000000000000000001");
|
|
170
|
+
expect(lp).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("getLPDeposits returns array", async () => {
|
|
174
|
+
if (!existingOwner) return;
|
|
175
|
+
const deposits = await indexer.getLPDeposits(existingOwner);
|
|
176
|
+
expect(Array.isArray(deposits)).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("getLPWithdrawals returns array", async () => {
|
|
180
|
+
if (!existingOwner) return;
|
|
181
|
+
const withdrawals = await indexer.getLPWithdrawals(existingOwner);
|
|
182
|
+
expect(Array.isArray(withdrawals)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("getLPDayData returns array", async () => {
|
|
186
|
+
const dayData = await indexer.getLPDayData(existingOwner ?? "0x0000000000000000000000000000000000000001");
|
|
187
|
+
expect(Array.isArray(dayData)).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── Builder ──────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe("indexer: builder", () => {
|
|
194
|
+
it("getBuilder returns null for unknown address", async () => {
|
|
195
|
+
const builder = await indexer.getBuilder("0x0000000000000000000000000000000000000001");
|
|
196
|
+
expect(builder).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("getDelegates returns array", async () => {
|
|
200
|
+
if (!existingOwner) return;
|
|
201
|
+
const delegates = await indexer.getDelegates(existingOwner);
|
|
202
|
+
expect(Array.isArray(delegates)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── Config ───────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
describe("indexer: config", () => {
|
|
209
|
+
it("getMainVaultConfig returns config or null", async () => {
|
|
210
|
+
if (!activeVaultId) {
|
|
211
|
+
console.log(" [SKIP] no active vault in ProtocolConfig");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const config = await indexer.getMainVaultConfig(activeVaultId);
|
|
215
|
+
expect(config).not.toBeNull();
|
|
216
|
+
expect(config!.vaultAddress.toLowerCase()).toBe(activeVaultId.toLowerCase());
|
|
217
|
+
expect(typeof config!.subscriptionFeeMultiplier).toBe("number");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("getFactoryConfig returns config or null", async () => {
|
|
221
|
+
if (!activeFactoryId) {
|
|
222
|
+
console.log(" [SKIP] no active factory in ProtocolConfig");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const config = await indexer.getFactoryConfig(activeFactoryId);
|
|
226
|
+
expect(config).not.toBeNull();
|
|
227
|
+
expect(config!.factoryAddress.toLowerCase()).toBe(activeFactoryId.toLowerCase());
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("getConfigChanges returns array", async () => {
|
|
231
|
+
const changes = await indexer.getConfigChanges({
|
|
232
|
+
contractAddress: activeVaultId ?? undefined,
|
|
233
|
+
limit: 5,
|
|
234
|
+
});
|
|
235
|
+
expect(Array.isArray(changes)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("getAllowedPerps returns array", async () => {
|
|
239
|
+
const perps = await indexer.getAllowedPerps();
|
|
240
|
+
expect(Array.isArray(perps)).toBe(true);
|
|
241
|
+
if (perps.length > 0) {
|
|
242
|
+
expect(typeof perps[0].perpIndex).toBe("number");
|
|
243
|
+
expect(perps[0].allowed).toBe(true);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("getVaultDayData returns array", async () => {
|
|
248
|
+
const days = await indexer.getVaultDayData(activeVaultId ?? undefined, { limit: 5 });
|
|
249
|
+
expect(Array.isArray(days)).toBe(true);
|
|
250
|
+
if (days.length > 0) {
|
|
251
|
+
expect(typeof days[0].dayNumber).toBe("number");
|
|
252
|
+
expect(days[0].totalAssets).toBeTruthy();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createPublicClient, createWalletClient, http, parseEther, type Address } from "viem";
|
|
3
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
4
|
+
import { hyperEvm, ADDRESSES, HYPE_FOR_GAS } from "../constants.js";
|
|
5
|
+
import { VaultModule } from "../vault.js";
|
|
6
|
+
import { AccountModule } from "../account.js";
|
|
7
|
+
import { FactoryModule } from "../factory.js";
|
|
8
|
+
import { AccountState } from "../types.js";
|
|
9
|
+
import { toUsdc } from "../utils.js";
|
|
10
|
+
import "dotenv/config";
|
|
11
|
+
|
|
12
|
+
// ── Config ───────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const PRIVATE_KEY = process.env.PRIVATE_KEY;
|
|
15
|
+
if (!PRIVATE_KEY) throw new Error("PRIVATE_KEY required in .env for lifecycle tests");
|
|
16
|
+
|
|
17
|
+
const RPC_URL = process.env.RPC_URL ?? hyperEvm.rpcUrls.default.http[0];
|
|
18
|
+
|
|
19
|
+
// Test amounts — keep small
|
|
20
|
+
const DEPOSIT_AMOUNT = toUsdc(200); // $200 USDC
|
|
21
|
+
const ACCOUNT_SIZE = toUsdc(100); // $100 funded account
|
|
22
|
+
const DRAWDOWN_BPS = 500; // 5%
|
|
23
|
+
const DAILY_DD = false;
|
|
24
|
+
|
|
25
|
+
// ── Setup ────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
|
|
28
|
+
|
|
29
|
+
const publicClient = createPublicClient({
|
|
30
|
+
chain: hyperEvm,
|
|
31
|
+
transport: http(RPC_URL),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const walletClient = createWalletClient({
|
|
35
|
+
account,
|
|
36
|
+
chain: hyperEvm,
|
|
37
|
+
transport: http(RPC_URL),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const addresses = {
|
|
41
|
+
vault: ADDRESSES.MAIN_VAULT as Address,
|
|
42
|
+
usdc: ADDRESSES.USDC as Address,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const vault = new VaultModule(publicClient, walletClient, hyperEvm, addresses);
|
|
46
|
+
const acc = new AccountModule(publicClient, walletClient, hyperEvm);
|
|
47
|
+
const factory = new FactoryModule(publicClient, ADDRESSES.FACTORY as Address);
|
|
48
|
+
|
|
49
|
+
let fundedAccountAddress: Address | null = null;
|
|
50
|
+
|
|
51
|
+
async function waitForTx(hash: `0x${string}`) {
|
|
52
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 });
|
|
53
|
+
if (receipt.status !== "success") {
|
|
54
|
+
throw new Error(`Transaction reverted: ${hash}`);
|
|
55
|
+
}
|
|
56
|
+
return receipt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Lifecycle (sequential) ───────────────────────────
|
|
60
|
+
|
|
61
|
+
describe("lifecycle", { sequential: true }, () => {
|
|
62
|
+
// ── 1. Approve USDC ────────────────────────────────
|
|
63
|
+
|
|
64
|
+
it("1. approve USDC for vault", async () => {
|
|
65
|
+
const hash = await vault.approveUsdc(DEPOSIT_AMOUNT);
|
|
66
|
+
console.log(` approve tx: ${hash}`);
|
|
67
|
+
const receipt = await waitForTx(hash);
|
|
68
|
+
expect(receipt.status).toBe("success");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── 2. Deposit to vault ────────────────────────────
|
|
72
|
+
|
|
73
|
+
it("2. deposit USDC to vault", async () => {
|
|
74
|
+
const beforePos = await vault.getLPPosition(account.address);
|
|
75
|
+
const hash = await vault.deposit(DEPOSIT_AMOUNT);
|
|
76
|
+
console.log(` deposit tx: ${hash}`);
|
|
77
|
+
const receipt = await waitForTx(hash);
|
|
78
|
+
expect(receipt.status).toBe("success");
|
|
79
|
+
|
|
80
|
+
const afterPos = await vault.getLPPosition(account.address);
|
|
81
|
+
expect(afterPos.shares).toBeGreaterThan(beforePos.shares);
|
|
82
|
+
console.log(` shares: ${beforePos.shares} → ${afterPos.shares}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── 3. Register builder ────────────────────────────
|
|
86
|
+
|
|
87
|
+
it("3. register as builder (skip if already registered)", async () => {
|
|
88
|
+
const info = await vault.getBuilderInfo(account.address);
|
|
89
|
+
if (info.active) {
|
|
90
|
+
console.log(" builder already registered, skipping");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hash = await vault.registerBuilder();
|
|
95
|
+
console.log(` registerBuilder tx: ${hash}`);
|
|
96
|
+
const receipt = await waitForTx(hash);
|
|
97
|
+
expect(receipt.status).toBe("success");
|
|
98
|
+
|
|
99
|
+
const after = await vault.getBuilderInfo(account.address);
|
|
100
|
+
expect(after.active).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── 4. Activate funded account ─────────────────────
|
|
104
|
+
|
|
105
|
+
it("4. activate funded account", async () => {
|
|
106
|
+
// Predict address first
|
|
107
|
+
fundedAccountAddress = await factory.predictAddress(account.address, account.address);
|
|
108
|
+
console.log(` predicted account: ${fundedAccountAddress}`);
|
|
109
|
+
|
|
110
|
+
const hash = await vault.activateFunded({
|
|
111
|
+
trader: account.address,
|
|
112
|
+
accountSize: ACCOUNT_SIZE,
|
|
113
|
+
drawdownBps: DRAWDOWN_BPS,
|
|
114
|
+
dailyDrawdownEnabled: DAILY_DD,
|
|
115
|
+
});
|
|
116
|
+
console.log(` activateFunded tx: ${hash}`);
|
|
117
|
+
const receipt = await waitForTx(hash);
|
|
118
|
+
expect(receipt.status).toBe("success");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── 5. Read account details ────────────────────────
|
|
122
|
+
|
|
123
|
+
it("5. read created account details", async () => {
|
|
124
|
+
expect(fundedAccountAddress).not.toBeNull();
|
|
125
|
+
|
|
126
|
+
const details = await acc.getDetails(fundedAccountAddress!);
|
|
127
|
+
console.log(` state: ${AccountState[details.state]}`);
|
|
128
|
+
console.log(` accountSize: ${details.accountSize}`);
|
|
129
|
+
console.log(` drawdownBps: ${details.drawdownBps}`);
|
|
130
|
+
console.log(` owner: ${details.owner}`);
|
|
131
|
+
|
|
132
|
+
expect(details.state).toBe(AccountState.Active);
|
|
133
|
+
expect(details.owner.toLowerCase()).toBe(account.address.toLowerCase());
|
|
134
|
+
expect(details.accountSize).toBe(ACCOUNT_SIZE);
|
|
135
|
+
expect(details.drawdownBps).toBe(DRAWDOWN_BPS);
|
|
136
|
+
expect(details.dailyDrawdownEnabled).toBe(DAILY_DD);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── 6. Set API wallet ──────────────────────────────
|
|
140
|
+
|
|
141
|
+
it("6. set API wallet", async () => {
|
|
142
|
+
expect(fundedAccountAddress).not.toBeNull();
|
|
143
|
+
|
|
144
|
+
const newApiWallet = "0x0000000000000000000000000000000000000042" as Address;
|
|
145
|
+
const hash = await acc.setApiWallet(fundedAccountAddress!, newApiWallet);
|
|
146
|
+
console.log(` setApiWallet tx: ${hash}`);
|
|
147
|
+
const receipt = await waitForTx(hash);
|
|
148
|
+
expect(receipt.status).toBe("success");
|
|
149
|
+
|
|
150
|
+
const details = await acc.getDetails(fundedAccountAddress!);
|
|
151
|
+
expect(details.apiWallet.toLowerCase()).toBe(newApiWallet.toLowerCase());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── 7. Checkpoint (soft fail — 24h gate) ───────────
|
|
155
|
+
|
|
156
|
+
it("7. checkpoint (may revert if < 24h)", async () => {
|
|
157
|
+
expect(fundedAccountAddress).not.toBeNull();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const hash = await acc.checkpoint(fundedAccountAddress!);
|
|
161
|
+
console.log(` checkpoint tx: ${hash}`);
|
|
162
|
+
const receipt = await waitForTx(hash);
|
|
163
|
+
expect(receipt.status).toBe("success");
|
|
164
|
+
} catch (e: unknown) {
|
|
165
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
166
|
+
console.log(` [EXPECTED] checkpoint reverted: ${msg.slice(0, 120)}`);
|
|
167
|
+
// Not a failure — time-gated operation
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── 8. Initiate close ──────────────────────────────
|
|
172
|
+
|
|
173
|
+
it("8. initiate close", async () => {
|
|
174
|
+
expect(fundedAccountAddress).not.toBeNull();
|
|
175
|
+
|
|
176
|
+
const hash = await acc.initiateClose(fundedAccountAddress!);
|
|
177
|
+
console.log(` initiateClose tx: ${hash}`);
|
|
178
|
+
const receipt = await waitForTx(hash);
|
|
179
|
+
expect(receipt.status).toBe("success");
|
|
180
|
+
|
|
181
|
+
const state = await acc.getState(fundedAccountAddress!);
|
|
182
|
+
expect(state).toBe(AccountState.Closing);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── 9. Close and return (soft fail — open positions or orders may still block) ─
|
|
186
|
+
|
|
187
|
+
it("9. closeAndReturn (may revert if < 3 days)", async () => {
|
|
188
|
+
expect(fundedAccountAddress).not.toBeNull();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const hash = await acc.closeAndReturn(fundedAccountAddress!);
|
|
192
|
+
console.log(` closeAndReturn tx: ${hash}`);
|
|
193
|
+
const receipt = await waitForTx(hash);
|
|
194
|
+
expect(receipt.status).toBe("success");
|
|
195
|
+
} catch (e: unknown) {
|
|
196
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
197
|
+
console.log(` [EXPECTED] closeAndReturn reverted (positions/orders still block return): ${msg.slice(0, 120)}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── 10. Finalize return (soft fail) ────────────────
|
|
202
|
+
|
|
203
|
+
it("10. finalizeReturn (may revert if not in Returning state)", async () => {
|
|
204
|
+
expect(fundedAccountAddress).not.toBeNull();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const hash = await acc.finalizeReturn(fundedAccountAddress!);
|
|
208
|
+
console.log(` finalizeReturn tx: ${hash}`);
|
|
209
|
+
const receipt = await waitForTx(hash);
|
|
210
|
+
expect(receipt.status).toBe("success");
|
|
211
|
+
} catch (e: unknown) {
|
|
212
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
213
|
+
console.log(` [EXPECTED] finalizeReturn reverted: ${msg.slice(0, 120)}`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ── 11. Withdraw LP shares ─────────────────────────
|
|
218
|
+
|
|
219
|
+
it("11. withdraw LP shares", async () => {
|
|
220
|
+
const pos = await vault.getLPPosition(account.address);
|
|
221
|
+
if (pos.withdrawableShares === 0n) {
|
|
222
|
+
console.log(" no withdrawable shares, skipping");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const hash = await vault.withdraw(pos.withdrawableShares);
|
|
227
|
+
console.log(` withdraw tx: ${hash}`);
|
|
228
|
+
const receipt = await waitForTx(hash);
|
|
229
|
+
expect(receipt.status).toBe("success");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createPublicClient, http, type Address } from "viem";
|
|
3
|
+
import { hyperEvm, ADDRESSES } from "../constants.js";
|
|
4
|
+
import { VaultModule } from "../vault.js";
|
|
5
|
+
import { AccountModule } from "../account.js";
|
|
6
|
+
import { FactoryModule } from "../factory.js";
|
|
7
|
+
import { AccountState } from "../types.js";
|
|
8
|
+
import { activationFee } from "../utils.js";
|
|
9
|
+
|
|
10
|
+
// ── Setup ────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const publicClient = createPublicClient({
|
|
13
|
+
chain: hyperEvm,
|
|
14
|
+
transport: http(hyperEvm.rpcUrls.default.http[0]),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const vault = new VaultModule(publicClient, undefined, hyperEvm, {
|
|
18
|
+
vault: ADDRESSES.MAIN_VAULT as Address,
|
|
19
|
+
usdc: ADDRESSES.USDC as Address,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const factory = new FactoryModule(publicClient, ADDRESSES.FACTORY as Address);
|
|
23
|
+
const account = new AccountModule(publicClient, undefined, hyperEvm);
|
|
24
|
+
|
|
25
|
+
// We'll find an existing account to test reads against
|
|
26
|
+
let existingAccount: Address | null = null;
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const count = await factory.getAccountsCount();
|
|
31
|
+
if (count > 0n) {
|
|
32
|
+
const accounts = await factory.getAccounts(0n, 1n);
|
|
33
|
+
existingAccount = accounts[0];
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// factory enumeration may be unavailable on empty or unreachable networks
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── Vault reads ──────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe("vault reads", () => {
|
|
43
|
+
it("getStats returns valid vault stats", async () => {
|
|
44
|
+
const stats = await vault.getStats();
|
|
45
|
+
expect(stats.totalAssets).toBeTypeOf("bigint");
|
|
46
|
+
expect(stats.sharePrice).toBeTypeOf("bigint");
|
|
47
|
+
expect(stats.utilizationBps).toBeTypeOf("bigint");
|
|
48
|
+
expect(stats.availableForAllocation).toBeTypeOf("bigint");
|
|
49
|
+
expect(stats.maxAccountSize).toBeTypeOf("bigint");
|
|
50
|
+
expect(stats.remainingCap).toBeTypeOf("bigint");
|
|
51
|
+
expect(typeof stats.paused).toBe("boolean");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("getLPPosition returns zeroes for random address", async () => {
|
|
55
|
+
const pos = await vault.getLPPosition("0x0000000000000000000000000000000000000001");
|
|
56
|
+
expect(pos.shares).toBe(0n);
|
|
57
|
+
expect(pos.sharesValue).toBe(0n);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("getAllowedPerps returns array of numbers", async () => {
|
|
61
|
+
const perps = await vault.getAllowedPerps();
|
|
62
|
+
expect(Array.isArray(perps)).toBe(true);
|
|
63
|
+
for (const p of perps) {
|
|
64
|
+
expect(typeof p).toBe("number");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("isAllowedPerp returns boolean", async () => {
|
|
69
|
+
const result = await vault.isAllowedPerp(0);
|
|
70
|
+
expect(typeof result).toBe("boolean");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("isPaused returns boolean", async () => {
|
|
74
|
+
const paused = await vault.isPaused();
|
|
75
|
+
expect(typeof paused).toBe("boolean");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("calculateFee matches utils.activationFee (cross-validation)", async () => {
|
|
79
|
+
const size = 5_000_000_000n; // $5000 USDC (6-dec)
|
|
80
|
+
const bps = 500;
|
|
81
|
+
|
|
82
|
+
const stats = await vault.getStats();
|
|
83
|
+
const fm = stats.feeMultiplier;
|
|
84
|
+
|
|
85
|
+
const onChain = await vault.calculateFee(size, bps, false);
|
|
86
|
+
const local = activationFee(size, bps, false, fm);
|
|
87
|
+
expect(local).toBe(onChain);
|
|
88
|
+
|
|
89
|
+
const onChainDD = await vault.calculateFee(size, bps, true);
|
|
90
|
+
const localDD = activationFee(size, bps, true, fm);
|
|
91
|
+
expect(localDD).toBe(onChainDD);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── Factory reads ────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("factory reads", () => {
|
|
98
|
+
it("getAccountsCount returns bigint >= 0", async () => {
|
|
99
|
+
const count = await factory.getAccountsCount();
|
|
100
|
+
expect(count).toBeTypeOf("bigint");
|
|
101
|
+
expect(count >= 0n).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("getAccounts returns array of addresses", async () => {
|
|
105
|
+
const count = await factory.getAccountsCount();
|
|
106
|
+
if (count === 0n) return; // skip if no accounts
|
|
107
|
+
|
|
108
|
+
const accounts = await factory.getAccounts(0n, BigInt(Math.min(Number(count), 5)));
|
|
109
|
+
expect(Array.isArray(accounts)).toBe(true);
|
|
110
|
+
expect(accounts.length).toBeGreaterThan(0);
|
|
111
|
+
for (const addr of accounts) {
|
|
112
|
+
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("predictAddress returns valid address", async () => {
|
|
117
|
+
const owner = "0x0000000000000000000000000000000000000001" as Address;
|
|
118
|
+
const builder = "0x0000000000000000000000000000000000000002" as Address;
|
|
119
|
+
const predicted = await factory.predictAddress(owner, builder);
|
|
120
|
+
expect(predicted).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("getTraderBuilderNonce returns bigint", async () => {
|
|
124
|
+
const owner = "0x0000000000000000000000000000000000000001" as Address;
|
|
125
|
+
const builder = "0x0000000000000000000000000000000000000002" as Address;
|
|
126
|
+
const nonce = await factory.getTraderBuilderNonce(owner, builder);
|
|
127
|
+
expect(nonce).toBeTypeOf("bigint");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Account reads ────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe("account reads", () => {
|
|
134
|
+
it("getDetails returns full structure for existing account", async () => {
|
|
135
|
+
if (!existingAccount) {
|
|
136
|
+
console.log(" [SKIP] no existing accounts on-chain");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const details = await account.getDetails(existingAccount);
|
|
141
|
+
expect(details.address).toBe(existingAccount);
|
|
142
|
+
expect(details.owner).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
143
|
+
expect(details.builder).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
144
|
+
expect(details.accountSize).toBeTypeOf("bigint");
|
|
145
|
+
expect(typeof details.drawdownBps).toBe("number");
|
|
146
|
+
expect(typeof details.dailyDrawdownEnabled).toBe("boolean");
|
|
147
|
+
expect(typeof details.subscriptionFeeMultiplier).toBe("number");
|
|
148
|
+
expect(details.highWaterMark).toBeTypeOf("bigint");
|
|
149
|
+
expect(details.dayStartValue).toBeTypeOf("bigint");
|
|
150
|
+
expect(typeof details.checkpointIndex).toBe("number");
|
|
151
|
+
expect(typeof details.subscriptionCancelled).toBe("boolean");
|
|
152
|
+
expect(typeof details.isSubscriptionActive).toBe("boolean");
|
|
153
|
+
expect(typeof details.canWithdrawProfit).toBe("boolean");
|
|
154
|
+
expect(Array.isArray(details.checkpoints)).toBe(true);
|
|
155
|
+
|
|
156
|
+
// State should be a valid enum value
|
|
157
|
+
expect(details.state).toBeGreaterThanOrEqual(AccountState.Uninitialized);
|
|
158
|
+
expect(details.state).toBeLessThanOrEqual(AccountState.Closed);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("getState returns valid enum", async () => {
|
|
162
|
+
if (!existingAccount) return;
|
|
163
|
+
const state = await account.getState(existingAccount);
|
|
164
|
+
expect(state).toBeGreaterThanOrEqual(AccountState.Uninitialized);
|
|
165
|
+
expect(state).toBeLessThanOrEqual(AccountState.Closed);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("getTrailingFloor returns bigint", async () => {
|
|
169
|
+
if (!existingAccount) return;
|
|
170
|
+
const floor = await account.getTrailingFloor(existingAccount);
|
|
171
|
+
expect(floor).toBeTypeOf("bigint");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("getDailyFloor returns bigint", async () => {
|
|
175
|
+
if (!existingAccount) return;
|
|
176
|
+
const floor = await account.getDailyFloor(existingAccount);
|
|
177
|
+
expect(floor).toBeTypeOf("bigint");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("canWithdrawProfit returns boolean", async () => {
|
|
181
|
+
if (!existingAccount) return;
|
|
182
|
+
const can = await account.canWithdrawProfit(existingAccount);
|
|
183
|
+
expect(typeof can).toBe("boolean");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("isSubscriptionActive returns boolean", async () => {
|
|
187
|
+
if (!existingAccount) return;
|
|
188
|
+
const active = await account.isSubscriptionActive(existingAccount);
|
|
189
|
+
expect(typeof active).toBe("boolean");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("getCheckpoints returns array", async () => {
|
|
193
|
+
if (!existingAccount) return;
|
|
194
|
+
const checkpoints = await account.getCheckpoints(existingAccount);
|
|
195
|
+
expect(Array.isArray(checkpoints)).toBe(true);
|
|
196
|
+
for (const cp of checkpoints) {
|
|
197
|
+
expect(cp.accountValue).toBeTypeOf("bigint");
|
|
198
|
+
expect(typeof cp.timestamp).toBe("number");
|
|
199
|
+
expect(typeof cp.profitable).toBe("boolean");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("getPendingWithdrawal returns struct", async () => {
|
|
204
|
+
if (!existingAccount) return;
|
|
205
|
+
const pw = await account.getPendingWithdrawal(existingAccount);
|
|
206
|
+
expect(pw.profit).toBeTypeOf("bigint");
|
|
207
|
+
expect(typeof pw.requestedAt).toBe("number");
|
|
208
|
+
});
|
|
209
|
+
});
|