@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
package/dist/indexer.js
ADDED
|
@@ -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
|
+
}
|