@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.
Files changed (56) hide show
  1. package/SDK_IMPROVEMENT_PLAN.md +197 -0
  2. package/arch.md +448 -0
  3. package/dist/__tests__/account.test.d.ts +1 -0
  4. package/dist/__tests__/account.test.js +36 -0
  5. package/dist/__tests__/indexer.test.d.ts +1 -0
  6. package/dist/__tests__/indexer.test.js +230 -0
  7. package/dist/__tests__/lifecycle.test.d.ts +1 -0
  8. package/dist/__tests__/lifecycle.test.js +186 -0
  9. package/dist/__tests__/reads.test.d.ts +1 -0
  10. package/dist/__tests__/reads.test.js +185 -0
  11. package/dist/__tests__/utils.test.d.ts +1 -0
  12. package/dist/__tests__/utils.test.js +185 -0
  13. package/dist/abi/factory.d.ts +308 -0
  14. package/dist/abi/factory.js +399 -0
  15. package/dist/abi/fundedAccount.d.ts +1074 -0
  16. package/dist/abi/fundedAccount.js +1373 -0
  17. package/dist/abi/mainVault.d.ts +1448 -0
  18. package/dist/abi/mainVault.js +1865 -0
  19. package/dist/account.d.ts +55 -0
  20. package/dist/account.js +290 -0
  21. package/dist/constants.d.ts +48 -0
  22. package/dist/constants.js +42 -0
  23. package/dist/factory.d.ts +23 -0
  24. package/dist/factory.js +139 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/index.js +14 -0
  27. package/dist/indexer.d.ts +57 -0
  28. package/dist/indexer.js +540 -0
  29. package/dist/sdk.d.ts +22 -0
  30. package/dist/sdk.js +89 -0
  31. package/dist/types.d.ts +384 -0
  32. package/dist/types.js +11 -0
  33. package/dist/utils.d.ts +21 -0
  34. package/dist/utils.js +89 -0
  35. package/dist/vault.d.ts +60 -0
  36. package/dist/vault.js +429 -0
  37. package/package.json +32 -0
  38. package/src/__tests__/account.test.ts +40 -0
  39. package/src/__tests__/indexer.test.ts +255 -0
  40. package/src/__tests__/lifecycle.test.ts +231 -0
  41. package/src/__tests__/reads.test.ts +209 -0
  42. package/src/__tests__/utils.test.ts +245 -0
  43. package/src/abi/factory.ts +399 -0
  44. package/src/abi/fundedAccount.ts +1373 -0
  45. package/src/abi/mainVault.ts +1865 -0
  46. package/src/account.ts +339 -0
  47. package/src/constants.ts +50 -0
  48. package/src/factory.ts +157 -0
  49. package/src/index.ts +16 -0
  50. package/src/indexer.ts +636 -0
  51. package/src/sdk.ts +93 -0
  52. package/src/types.ts +426 -0
  53. package/src/utils.ts +143 -0
  54. package/src/vault.ts +479 -0
  55. package/tsconfig.json +19 -0
  56. package/vitest.config.ts +8 -0
@@ -0,0 +1,540 @@
1
+ export class IndexerModule {
2
+ graphqlUrl;
3
+ graphqlWsUrl;
4
+ transports;
5
+ ws = null;
6
+ wsId = 0;
7
+ subscriptions = new Map();
8
+ reconnectTimer = null;
9
+ reconnectDelay = 1000;
10
+ constructor(graphqlUrl, graphqlWsUrl, transports = {}) {
11
+ this.graphqlUrl = graphqlUrl;
12
+ this.graphqlWsUrl = graphqlWsUrl;
13
+ this.transports = transports;
14
+ }
15
+ static PAGE_SIZE = 1000;
16
+ static VAULT_FIELDS = `id address totalDeposits totalWithdrawals totalActivationFees cumulativeActivationFeesToVault totalAllocated totalShares cumulativeProfitShares cumulativeCancelledProfit cumulativeSubscriptionFees cumulativeProtocolFeesRouted cumulativeHypercoreFees cumulativeAccountGains cumulativeAccountLosses cumulativeRescuedFunds totalAccountsActivated totalAccountsActive totalAccountsClosed paused subscriptionFeeMultiplier protocolFeeBps protocolFeeReceiver updatedAt`;
17
+ static ACCOUNT_FIELDS = `id owner builder { id address vaultAddress } vaultAddress factoryAddress state accountSize drawdownBps dailyDrawdownEnabled activationFee highWaterMark profitableDaysCount lastCheckpointDay totalCheckpoints subscriptionPaidUntil subscriptionCancelled pendingSubscriptionFee subscriptionFeeMultiplier dayStartValue apiWallet riskEngineWallet builderRiskEngineWallet createdAt activatedAt closingInitiatedAt returnInitiatedAt closedAt returnedAmount breachType breachValue breachFloor closePendingSubFee closePendingProfit lastWithdrawalId updatedAt`;
18
+ static MAIN_VAULT_CONFIG_FIELDS = `id vaultAddress factoryAddress owner pendingOwner relayer paused protocolFeeReceiver feeMultiplier subscriptionFeeMultiplier dailyDdDiscountBps minBuilderShares builderLeverage protocolFeeBps minAccountSize maxAccountSize minDrawdownBps maxDrawdownBps maxUtilizationBps maxSingleAccountBps hypeForSpot maxCap createdAt updatedAt`;
19
+ static FACTORY_CONFIG_FIELDS = `id factoryAddress vaultAddress owner pendingOwner createdAt updatedAt`;
20
+ static CONFIG_CHANGE_FIELDS = `id contractAddress contractType field oldValue newValue blockNumber timestamp txHash`;
21
+ static VAULT_DAY_DATA_FIELDS = `id date dayNumber evmBalance totalAllocated totalAssets totalShares sharePrice dailyDeposits dailyWithdrawals dailyNetLpFlow dailyActivationFees dailyActivationFeesToVault dailyProfitShares dailyCancelledProfit dailySubscriptionFees dailyProtocolFeesRouted dailyHypercoreFees dailyRescuedFunds dailyAccountReturnAmount dailyAccountPrincipalReturned dailyAccountGains dailyAccountLosses dailyPerformancePnl totalAccountsActivated totalAccountsActive totalAccountsClosed accountsActivatedToday accountsClosedToday navChange24h sharePriceChange24h return24hBps paused updatedAt`;
22
+ static LP_DAY_DATA_FIELDS = `id lp { id } date dayNumber shares sharesValue dailyDeposits dailyWithdrawals depositCount withdrawCount`;
23
+ // Explicit vault address always wins. ProtocolConfig is only a convenience default for
24
+ // indexer-scoped reads when the caller does not provide one.
25
+ async resolveVaultAddress(vaultAddress) {
26
+ if (vaultAddress)
27
+ return vaultAddress.toLowerCase();
28
+ const protocol = await this.getProtocolConfig();
29
+ return protocol?.vaultAddress?.toLowerCase() ?? null;
30
+ }
31
+ // Explicit factory address always wins. ProtocolConfig is only a convenience default for
32
+ // indexer-scoped reads when the caller does not provide one.
33
+ async resolveFactoryAddress(factoryAddress) {
34
+ if (factoryAddress)
35
+ return factoryAddress.toLowerCase();
36
+ const protocol = await this.getProtocolConfig();
37
+ return protocol?.factoryAddress?.toLowerCase() ?? null;
38
+ }
39
+ // ── GraphQL fetch helper ──────────────────────────
40
+ async query(query, variables) {
41
+ const fetchFn = this.transports.fetchFn ?? globalThis.fetch;
42
+ if (!fetchFn)
43
+ throw new Error("fetch is not available; pass fetchFn in SDKConfig");
44
+ const res = await fetchFn(this.graphqlUrl, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ query, variables }),
48
+ });
49
+ if (!res.ok)
50
+ throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
51
+ const json = (await res.json());
52
+ if (json.errors?.length)
53
+ throw new Error(`GraphQL error: ${json.errors[0].message}`);
54
+ return json.data;
55
+ }
56
+ // ── VAULT ─────────────────────────────────────────
57
+ async getVault(vaultAddress) {
58
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
59
+ if (!resolvedVault) {
60
+ const data = await this.query(`
61
+ query {
62
+ Vault(order_by: { updatedAt: desc }, limit: 1) { ${IndexerModule.VAULT_FIELDS} }
63
+ }
64
+ `);
65
+ return data.Vault[0] ?? null;
66
+ }
67
+ const data = await this.query(`
68
+ query($id: String!) {
69
+ Vault(where: { id: { _eq: $id } }) { ${IndexerModule.VAULT_FIELDS} }
70
+ }
71
+ `, { id: resolvedVault });
72
+ return data.Vault[0] ?? null;
73
+ }
74
+ // ── LP ────────────────────────────────────────────
75
+ async getLP(address) {
76
+ const data = await this.query(`
77
+ query($id: String!) { LiquidityProvider(where: { id: { _ilike: $id } }) { id totalDeposited totalWithdrawn depositCount withdrawCount lastActionAt } }
78
+ `, { id: address.toLowerCase() });
79
+ return data.LiquidityProvider[0] ?? null;
80
+ }
81
+ async getLPDeposits(address, p) {
82
+ const data = await this.query(`
83
+ query($lp: String!, $limit: Int, $offset: Int) {
84
+ VaultDeposit(where: { lp_id: { _ilike: $lp } }, order_by: { timestamp: desc }, limit: $limit, offset: $offset) {
85
+ id lp { id } amount sharesReceived timestamp txHash
86
+ }
87
+ }
88
+ `, { lp: address.toLowerCase(), limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
89
+ return data.VaultDeposit;
90
+ }
91
+ async getLPWithdrawals(address, p) {
92
+ const data = await this.query(`
93
+ query($lp: String!, $limit: Int, $offset: Int) {
94
+ VaultWithdrawal(where: { lp_id: { _ilike: $lp } }, order_by: { timestamp: desc }, limit: $limit, offset: $offset) {
95
+ id lp { id } amount sharesBurned timestamp txHash
96
+ }
97
+ }
98
+ `, { lp: address.toLowerCase(), limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
99
+ return data.VaultWithdrawal;
100
+ }
101
+ // ── BUILDER ───────────────────────────────────────
102
+ async getBuilder(address, vaultAddress) {
103
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
104
+ if (!resolvedVault) {
105
+ const data = await this.query(`
106
+ query($address: String!) {
107
+ Builder(where: { address: { _ilike: $address } }) {
108
+ id
109
+ address
110
+ vaultAddress
111
+ active
112
+ totalAccountsActivated
113
+ totalAccountsClosed
114
+ activeAccounts
115
+ registeredAt
116
+ deactivatedAt
117
+ }
118
+ }
119
+ `, { address: address.toLowerCase() });
120
+ return data.Builder[0] ?? null;
121
+ }
122
+ const data = await this.query(`
123
+ query($address: String!, $vaultAddress: String) {
124
+ Builder(
125
+ where: {
126
+ address: { _ilike: $address }
127
+ vaultAddress: { _ilike: $vaultAddress }
128
+ }
129
+ ) {
130
+ id
131
+ address
132
+ vaultAddress
133
+ active
134
+ totalAccountsActivated
135
+ totalAccountsClosed
136
+ activeAccounts
137
+ registeredAt
138
+ deactivatedAt
139
+ }
140
+ }
141
+ `, { address: address.toLowerCase(), vaultAddress: resolvedVault });
142
+ return data.Builder[0] ?? null;
143
+ }
144
+ async getDelegates(builder, vaultAddress) {
145
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
146
+ if (!resolvedVault) {
147
+ const data = await this.query(`
148
+ query($builder: String!) {
149
+ Delegate(where: { builderAddress: { _ilike: $builder } }) {
150
+ id
151
+ vaultAddress
152
+ delegateAddress
153
+ builderAddress
154
+ }
155
+ }
156
+ `, { builder: builder.toLowerCase() });
157
+ return data.Delegate;
158
+ }
159
+ const data = await this.query(`
160
+ query($builder: String!, $vaultAddress: String) {
161
+ Delegate(
162
+ where: {
163
+ builderAddress: { _ilike: $builder }
164
+ vaultAddress: { _ilike: $vaultAddress }
165
+ }
166
+ ) {
167
+ id
168
+ vaultAddress
169
+ delegateAddress
170
+ builderAddress
171
+ }
172
+ }
173
+ `, { builder: builder.toLowerCase(), vaultAddress: resolvedVault });
174
+ return data.Delegate;
175
+ }
176
+ // ── ACCOUNTS ──────────────────────────────────────
177
+ async getAccount(address) {
178
+ const data = await this.query(`
179
+ query($id: String!) { FundedAccount(where: { id: { _ilike: $id } }) { ${IndexerModule.ACCOUNT_FIELDS} } }
180
+ `, { id: address.toLowerCase() });
181
+ return data.FundedAccount[0] ?? null;
182
+ }
183
+ async getAccountsByOwner(owner) {
184
+ const data = await this.query(`
185
+ query($owner: String!) { FundedAccount(where: { owner: { _ilike: $owner } }, order_by: { createdAt: desc }) { ${IndexerModule.ACCOUNT_FIELDS} } }
186
+ `, { owner: owner.toLowerCase() });
187
+ return data.FundedAccount;
188
+ }
189
+ async getAccountsByBuilder(builder, vaultAddress) {
190
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
191
+ if (!resolvedVault) {
192
+ const data = await this.query(`
193
+ query($builder: String!) {
194
+ FundedAccount(
195
+ where: { builder: { address: { _ilike: $builder } } },
196
+ order_by: { createdAt: desc }
197
+ ) { ${IndexerModule.ACCOUNT_FIELDS} }
198
+ }
199
+ `, { builder: builder.toLowerCase() });
200
+ return data.FundedAccount;
201
+ }
202
+ const data = await this.query(`
203
+ query($builder: String!, $vaultAddress: String) {
204
+ FundedAccount(
205
+ where: {
206
+ builder: {
207
+ address: { _ilike: $builder }
208
+ vaultAddress: { _ilike: $vaultAddress }
209
+ }
210
+ },
211
+ order_by: { createdAt: desc }
212
+ ) { ${IndexerModule.ACCOUNT_FIELDS} }
213
+ }
214
+ `, { builder: builder.toLowerCase(), vaultAddress: resolvedVault });
215
+ return data.FundedAccount;
216
+ }
217
+ async getAccountsByState(state) {
218
+ const data = await this.query(`
219
+ query($state: accountstate!) { FundedAccount(where: { state: { _eq: $state } }, order_by: { updatedAt: desc }) { ${IndexerModule.ACCOUNT_FIELDS} } }
220
+ `, { state });
221
+ return data.FundedAccount;
222
+ }
223
+ async getAccountsByStates(states) {
224
+ const all = [];
225
+ let offset = 0;
226
+ while (true) {
227
+ const data = await this.query(`
228
+ query($states: [accountstate!]!, $limit: Int!, $offset: Int!) {
229
+ FundedAccount(where: { state: { _in: $states } }, order_by: { updatedAt: desc }, limit: $limit, offset: $offset) { ${IndexerModule.ACCOUNT_FIELDS} }
230
+ }
231
+ `, { states, limit: IndexerModule.PAGE_SIZE, offset });
232
+ all.push(...data.FundedAccount);
233
+ if (data.FundedAccount.length < IndexerModule.PAGE_SIZE)
234
+ break;
235
+ offset += IndexerModule.PAGE_SIZE;
236
+ }
237
+ return all;
238
+ }
239
+ async getAccountsUpdatedSince(since) {
240
+ const all = [];
241
+ let offset = 0;
242
+ while (true) {
243
+ const data = await this.query(`
244
+ query($since: numeric!, $limit: Int!, $offset: Int!) {
245
+ FundedAccount(where: { updatedAt: { _gt: $since } }, order_by: { updatedAt: asc }, limit: $limit, offset: $offset) { ${IndexerModule.ACCOUNT_FIELDS} }
246
+ }
247
+ `, { since, limit: IndexerModule.PAGE_SIZE, offset });
248
+ all.push(...data.FundedAccount);
249
+ if (data.FundedAccount.length < IndexerModule.PAGE_SIZE)
250
+ break;
251
+ offset += IndexerModule.PAGE_SIZE;
252
+ }
253
+ return all;
254
+ }
255
+ // ── HISTORY ───────────────────────────────────────
256
+ async getCheckpoints(account, p) {
257
+ const data = await this.query(`
258
+ query($account: String!, $limit: Int, $offset: Int) {
259
+ Checkpoint(where: { account_id: { _ilike: $account } }, order_by: { dayNumber: desc }, limit: $limit, offset: $offset) {
260
+ id dayNumber accountValue previousValue profitable profitableDaysCount timestamp
261
+ }
262
+ }
263
+ `, { account: account.toLowerCase(), limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
264
+ return data.Checkpoint;
265
+ }
266
+ async getWithdrawals(account, p) {
267
+ const data = await this.query(`
268
+ query($account: String!, $limit: Int, $offset: Int) {
269
+ ProfitWithdrawal(where: { account_id: { _ilike: $account } }, order_by: { requestedAt: desc }, limit: $limit, offset: $offset) {
270
+ id profit ownerAmount vaultAmount protocolAmount status requestedAt claimedAt cancelledAt
271
+ }
272
+ }
273
+ `, { account: account.toLowerCase(), limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
274
+ return data.ProfitWithdrawal;
275
+ }
276
+ async getSubscriptionPayments(account, p) {
277
+ const data = await this.query(`
278
+ query($account: String!, $limit: Int, $offset: Int) {
279
+ SubscriptionPayment(where: { account_id: { _ilike: $account } }, order_by: { timestamp: desc }, limit: $limit, offset: $offset) {
280
+ id amount paidUntil timestamp
281
+ }
282
+ }
283
+ `, { account: account.toLowerCase(), limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
284
+ return data.SubscriptionPayment;
285
+ }
286
+ async getEvents(account, p) {
287
+ const data = await this.query(`
288
+ query($account: String!, $limit: Int, $offset: Int) {
289
+ AccountEvent(where: { account_id: { _ilike: $account } }, order_by: { blockNumber: desc }, limit: $limit, offset: $offset) {
290
+ id eventType data timestamp blockNumber txHash
291
+ }
292
+ }
293
+ `, { account: account.toLowerCase(), limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
294
+ return data.AccountEvent;
295
+ }
296
+ async getEventsByType(type, limit) {
297
+ const data = await this.query(`
298
+ query($type: String!, $limit: Int) {
299
+ AccountEvent(where: { eventType: { _eq: $type } }, order_by: { blockNumber: desc }, limit: $limit) {
300
+ id eventType data timestamp blockNumber txHash
301
+ }
302
+ }
303
+ `, { type, limit: limit ?? 50 });
304
+ return data.AccountEvent;
305
+ }
306
+ // ── PROTOCOL CONFIG ─────────────────────────────
307
+ async getProtocolConfig() {
308
+ const data = await this.query(`{
309
+ ProtocolConfig { id vaultAddress factoryAddress updatedAt }
310
+ }`);
311
+ return data.ProtocolConfig[0] ?? null;
312
+ }
313
+ // ── CONFIG ────────────────────────────────────────
314
+ async getMainVaultConfig(vaultAddress) {
315
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
316
+ if (!resolvedVault) {
317
+ const data = await this.query(`
318
+ query {
319
+ MainVaultConfig(order_by: { updatedAt: desc }, limit: 1) { ${IndexerModule.MAIN_VAULT_CONFIG_FIELDS} }
320
+ }
321
+ `);
322
+ return data.MainVaultConfig[0] ?? null;
323
+ }
324
+ const data = await this.query(`
325
+ query($id: String!) {
326
+ MainVaultConfig(where: { id: { _eq: $id } }) { ${IndexerModule.MAIN_VAULT_CONFIG_FIELDS} }
327
+ }
328
+ `, { id: resolvedVault });
329
+ return data.MainVaultConfig[0] ?? null;
330
+ }
331
+ async getFactoryConfig(factoryAddress) {
332
+ const resolvedFactory = await this.resolveFactoryAddress(factoryAddress);
333
+ if (!resolvedFactory) {
334
+ const data = await this.query(`
335
+ query {
336
+ FactoryConfig(order_by: { updatedAt: desc }, limit: 1) { ${IndexerModule.FACTORY_CONFIG_FIELDS} }
337
+ }
338
+ `);
339
+ return data.FactoryConfig[0] ?? null;
340
+ }
341
+ const data = await this.query(`
342
+ query($id: String!) {
343
+ FactoryConfig(where: { id: { _eq: $id } }) { ${IndexerModule.FACTORY_CONFIG_FIELDS} }
344
+ }
345
+ `, { id: resolvedFactory });
346
+ return data.FactoryConfig[0] ?? null;
347
+ }
348
+ async getConfigChanges(filters) {
349
+ const variables = {
350
+ limit: filters?.limit ?? 50,
351
+ offset: filters?.offset ?? 0,
352
+ };
353
+ const variableDefs = ["$limit: Int", "$offset: Int"];
354
+ const whereClauses = [];
355
+ if (filters?.contractAddress) {
356
+ variableDefs.push("$contractAddress: String!");
357
+ whereClauses.push("contractAddress: { _ilike: $contractAddress }");
358
+ variables.contractAddress = filters.contractAddress.toLowerCase();
359
+ }
360
+ if (filters?.contractType) {
361
+ variableDefs.push("$contractType: String!");
362
+ whereClauses.push("contractType: { _eq: $contractType }");
363
+ variables.contractType = filters.contractType;
364
+ }
365
+ if (filters?.field) {
366
+ variableDefs.push("$field: String!");
367
+ whereClauses.push("field: { _eq: $field }");
368
+ variables.field = filters.field;
369
+ }
370
+ const whereBlock = whereClauses.length > 0
371
+ ? `where: { ${whereClauses.join(" ")} }`
372
+ : "";
373
+ const data = await this.query(`
374
+ query(${variableDefs.join(", ")}) {
375
+ ConfigChange(
376
+ ${whereBlock}
377
+ order_by: [{ timestamp: desc }, { blockNumber: desc }]
378
+ limit: $limit
379
+ offset: $offset
380
+ ) { ${IndexerModule.CONFIG_CHANGE_FIELDS} }
381
+ }
382
+ `, variables);
383
+ return data.ConfigChange;
384
+ }
385
+ async getVaultDayData(vaultAddress, p) {
386
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
387
+ if (!resolvedVault) {
388
+ const data = await this.query(`
389
+ query($limit: Int, $offset: Int) {
390
+ VaultDayData(order_by: [{ date: desc }, { updatedAt: desc }], limit: $limit, offset: $offset) {
391
+ ${IndexerModule.VAULT_DAY_DATA_FIELDS}
392
+ }
393
+ }
394
+ `, { limit: p?.limit ?? 50, offset: p?.offset ?? 0 });
395
+ return data.VaultDayData;
396
+ }
397
+ const data = await this.query(`
398
+ query($vaultAddress: String!, $limit: Int, $offset: Int) {
399
+ VaultDayData(
400
+ where: { id: { _ilike: $vaultAddress } }
401
+ order_by: [{ date: desc }, { updatedAt: desc }]
402
+ limit: $limit
403
+ offset: $offset
404
+ ) { ${IndexerModule.VAULT_DAY_DATA_FIELDS} }
405
+ }
406
+ `, {
407
+ vaultAddress: `${resolvedVault}-%`,
408
+ limit: p?.limit ?? 50,
409
+ offset: p?.offset ?? 0,
410
+ });
411
+ return data.VaultDayData;
412
+ }
413
+ async getLPDayData(address, p) {
414
+ const data = await this.query(`
415
+ query($lp: String!, $limit: Int, $offset: Int) {
416
+ LiquidityProviderDayData(
417
+ where: { lp_id: { _ilike: $lp } }
418
+ order_by: [{ date: desc }, { dayNumber: desc }]
419
+ limit: $limit
420
+ offset: $offset
421
+ ) { ${IndexerModule.LP_DAY_DATA_FIELDS} }
422
+ }
423
+ `, {
424
+ lp: address.toLowerCase(),
425
+ limit: p?.limit ?? 50,
426
+ offset: p?.offset ?? 0,
427
+ });
428
+ return data.LiquidityProviderDayData;
429
+ }
430
+ async getAllowedPerps(vaultAddress) {
431
+ const resolvedVault = await this.resolveVaultAddress(vaultAddress);
432
+ if (!resolvedVault) {
433
+ const data = await this.query(`
434
+ query {
435
+ AllowedPerp(where: { allowed: { _eq: true } }) { id vaultAddress perpIndex allowed }
436
+ }
437
+ `);
438
+ return data.AllowedPerp;
439
+ }
440
+ const data = await this.query(`
441
+ query($vaultAddress: String) {
442
+ AllowedPerp(
443
+ where: {
444
+ allowed: { _eq: true }
445
+ vaultAddress: { _ilike: $vaultAddress }
446
+ }
447
+ ) { id vaultAddress perpIndex allowed }
448
+ }
449
+ `, { vaultAddress: resolvedVault });
450
+ return data.AllowedPerp;
451
+ }
452
+ // ── SUBSCRIPTIONS (Hasura graphql-transport-ws) ───
453
+ ensureWs() {
454
+ const WebSocketCtor = (this.transports.webSocketCtor ?? globalThis.WebSocket);
455
+ if (!WebSocketCtor)
456
+ throw new Error("WebSocket is not available; pass webSocketCtor in SDKConfig");
457
+ if (this.ws && this.ws.readyState === WebSocketCtor.OPEN)
458
+ return this.ws;
459
+ if (!this.graphqlWsUrl)
460
+ throw new Error("graphqlWsUrl required for subscriptions");
461
+ const ws = new WebSocketCtor(this.graphqlWsUrl, "graphql-transport-ws");
462
+ this.ws = ws;
463
+ ws.onopen = () => {
464
+ this.reconnectDelay = 1000;
465
+ ws.send(JSON.stringify({ type: "connection_init" }));
466
+ };
467
+ ws.onmessage = (event) => {
468
+ const msg = JSON.parse(String(event.data));
469
+ if (msg.type === "connection_ack") {
470
+ for (const [id, sub] of this.subscriptions) {
471
+ ws.send(JSON.stringify({
472
+ id: String(id),
473
+ type: "subscribe",
474
+ payload: { query: sub.query, variables: sub.variables },
475
+ }));
476
+ }
477
+ }
478
+ if (msg.type === "next" && msg.id && msg.payload?.data) {
479
+ const sub = this.subscriptions.get(Number(msg.id));
480
+ if (sub)
481
+ sub.cb(msg.payload.data);
482
+ }
483
+ };
484
+ ws.onclose = () => {
485
+ this.reconnectTimer = setTimeout(() => {
486
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
487
+ this.ensureWs();
488
+ }, this.reconnectDelay);
489
+ };
490
+ ws.onerror = () => {
491
+ ws.close();
492
+ };
493
+ return ws;
494
+ }
495
+ subscribe(query, variables, cb) {
496
+ const id = ++this.wsId;
497
+ this.subscriptions.set(id, { query, variables, cb: cb });
498
+ const ws = this.ensureWs();
499
+ const WebSocketCtor = (this.transports.webSocketCtor ?? globalThis.WebSocket);
500
+ if (WebSocketCtor && ws.readyState === WebSocketCtor.OPEN) {
501
+ ws.send(JSON.stringify({
502
+ id: String(id),
503
+ type: "subscribe",
504
+ payload: { query, variables },
505
+ }));
506
+ }
507
+ return () => {
508
+ this.subscriptions.delete(id);
509
+ const WebSocketCtor = (this.transports.webSocketCtor ?? globalThis.WebSocket);
510
+ if (this.ws && WebSocketCtor && this.ws.readyState === WebSocketCtor.OPEN) {
511
+ this.ws.send(JSON.stringify({ id: String(id), type: "complete" }));
512
+ }
513
+ };
514
+ }
515
+ subscribeActiveAccounts(cb) {
516
+ return this.subscribe(`subscription { FundedAccount(where: { state: { _eq: ACTIVE } }) { ${IndexerModule.ACCOUNT_FIELDS} } }`, undefined, (data) => cb(data.FundedAccount));
517
+ }
518
+ subscribeAccount(address, cb) {
519
+ return this.subscribe(`subscription($id: String!) { FundedAccount(where: { id: { _ilike: $id } }) { ${IndexerModule.ACCOUNT_FIELDS} } }`, { id: address.toLowerCase() }, (data) => { if (data.FundedAccount[0])
520
+ cb(data.FundedAccount[0]); });
521
+ }
522
+ subscribeAccountEvents(address, cb) {
523
+ return this.subscribe(`subscription($account: String!) { AccountEvent(where: { account_id: { _ilike: $account } }, order_by: { blockNumber: desc }, limit: 10) { id eventType data timestamp blockNumber txHash } }`, { account: address.toLowerCase() }, (data) => cb(data.AccountEvent));
524
+ }
525
+ subscribeStateChanges(cb) {
526
+ return this.subscribe(`subscription { FundedAccount(where: { state: { _neq: CLOSED } }, order_by: { updatedAt: desc }) { ${IndexerModule.ACCOUNT_FIELDS} } }`, undefined, (data) => cb(data.FundedAccount));
527
+ }
528
+ disconnect() {
529
+ if (this.reconnectTimer) {
530
+ clearTimeout(this.reconnectTimer);
531
+ this.reconnectTimer = null;
532
+ }
533
+ this.subscriptions.clear();
534
+ if (this.ws) {
535
+ this.ws.onclose = null;
536
+ this.ws.close();
537
+ this.ws = null;
538
+ }
539
+ }
540
+ }
package/dist/sdk.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { VaultModule } from "./vault.js";
2
+ import { AccountModule } from "./account.js";
3
+ import { FactoryModule } from "./factory.js";
4
+ import { IndexerModule } from "./indexer.js";
5
+ import type { SDKConfig } from "./types.js";
6
+ export declare class ProportionSDK {
7
+ readonly vault: VaultModule;
8
+ readonly account: AccountModule;
9
+ readonly factory: FactoryModule;
10
+ readonly indexer: IndexerModule;
11
+ /**
12
+ * Async factory: resolves addresses from explicit config > ENV > ProtocolConfig > hardcoded constants.
13
+ *
14
+ * ProtocolConfig is only a convenience fallback from the indexer. It is not the source of truth
15
+ * for protocol configuration and should not replace explicit deployment config in applications.
16
+ *
17
+ * ENV vars: PROPORTION_VAULT_ADDRESS, PROPORTION_FACTORY_ADDRESS, PROPORTION_USDC_ADDRESS
18
+ */
19
+ static create(config: SDKConfig): Promise<ProportionSDK>;
20
+ constructor(config: SDKConfig);
21
+ disconnect(): void;
22
+ }
package/dist/sdk.js ADDED
@@ -0,0 +1,89 @@
1
+ import { createPublicClient, http } from "viem";
2
+ import { ADDRESSES, hyperEvm } from "./constants.js";
3
+ import { VaultModule } from "./vault.js";
4
+ import { AccountModule } from "./account.js";
5
+ import { FactoryModule } from "./factory.js";
6
+ import { IndexerModule } from "./indexer.js";
7
+ import { mainVaultAbi } from "./abi/mainVault.js";
8
+ export class ProportionSDK {
9
+ vault;
10
+ account;
11
+ factory;
12
+ indexer;
13
+ /**
14
+ * Async factory: resolves addresses from explicit config > ENV > ProtocolConfig > hardcoded constants.
15
+ *
16
+ * ProtocolConfig is only a convenience fallback from the indexer. It is not the source of truth
17
+ * for protocol configuration and should not replace explicit deployment config in applications.
18
+ *
19
+ * ENV vars: PROPORTION_VAULT_ADDRESS, PROPORTION_FACTORY_ADDRESS, PROPORTION_USDC_ADDRESS
20
+ */
21
+ static async create(config) {
22
+ const resolved = { ...config.addresses };
23
+ // ENV fallbacks (Node; safely ignored in browsers)
24
+ const env = typeof process !== "undefined" ? process.env : undefined;
25
+ if (!resolved.MAIN_VAULT && env?.PROPORTION_VAULT_ADDRESS)
26
+ resolved.MAIN_VAULT = env.PROPORTION_VAULT_ADDRESS;
27
+ if (!resolved.FACTORY && env?.PROPORTION_FACTORY_ADDRESS)
28
+ resolved.FACTORY = env.PROPORTION_FACTORY_ADDRESS;
29
+ if (!resolved.USDC && env?.PROPORTION_USDC_ADDRESS)
30
+ resolved.USDC = env.PROPORTION_USDC_ADDRESS;
31
+ // Fetch missing addresses from the indexer only as a convenience fallback.
32
+ // Primary address sources remain explicit config.addresses and ENV.
33
+ if (!resolved.MAIN_VAULT || !resolved.FACTORY) {
34
+ try {
35
+ const indexer = new IndexerModule(config.graphqlUrl, undefined, {
36
+ fetchFn: config.fetchFn,
37
+ webSocketCtor: config.webSocketCtor,
38
+ });
39
+ const protocol = await indexer.getProtocolConfig();
40
+ if (protocol) {
41
+ if (!resolved.MAIN_VAULT && protocol.vaultAddress)
42
+ resolved.MAIN_VAULT = protocol.vaultAddress;
43
+ if (!resolved.FACTORY && protocol.factoryAddress)
44
+ resolved.FACTORY = protocol.factoryAddress;
45
+ }
46
+ }
47
+ catch { /* fall back to hardcoded constants */ }
48
+ }
49
+ if (resolved.MAIN_VAULT && !resolved.USDC) {
50
+ try {
51
+ const publicClient = createPublicClient({
52
+ chain: hyperEvm,
53
+ transport: http(config.rpcUrl ?? hyperEvm.rpcUrls.default.http[0]),
54
+ });
55
+ resolved.USDC = await publicClient.readContract({
56
+ address: resolved.MAIN_VAULT,
57
+ abi: mainVaultAbi,
58
+ functionName: "usdc",
59
+ });
60
+ }
61
+ catch { /* fall back to hardcoded constants */ }
62
+ }
63
+ return new ProportionSDK({ ...config, addresses: resolved });
64
+ }
65
+ constructor(config) {
66
+ const addresses = {
67
+ vault: (config.addresses?.MAIN_VAULT ?? ADDRESSES.MAIN_VAULT),
68
+ usdc: (config.addresses?.USDC ?? ADDRESSES.USDC),
69
+ factory: (config.addresses?.FACTORY ?? ADDRESSES.FACTORY),
70
+ };
71
+ const publicClient = createPublicClient({
72
+ chain: hyperEvm,
73
+ transport: http(config.rpcUrl ?? hyperEvm.rpcUrls.default.http[0]),
74
+ });
75
+ this.vault = new VaultModule(publicClient, config.walletClient, hyperEvm, {
76
+ vault: addresses.vault,
77
+ usdc: addresses.usdc,
78
+ });
79
+ this.account = new AccountModule(publicClient, config.walletClient, hyperEvm);
80
+ this.factory = new FactoryModule(publicClient, addresses.factory);
81
+ this.indexer = new IndexerModule(config.graphqlUrl, config.graphqlWsUrl, {
82
+ fetchFn: config.fetchFn,
83
+ webSocketCtor: config.webSocketCtor,
84
+ });
85
+ }
86
+ disconnect() {
87
+ this.indexer.disconnect();
88
+ }
89
+ }