@augustdigital/sdk 8.3.2 → 8.6.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/README.md +207 -117
- package/lib/abis/ERC20_Bytes32.d.ts +4 -0
- package/lib/abis/ERC20_Bytes32.js +4 -0
- package/lib/abis/ERC4626.d.ts +1 -0
- package/lib/abis/ERC4626.js +1 -0
- package/lib/abis/ERC721.d.ts +1 -0
- package/lib/abis/ERC721.js +1 -0
- package/lib/abis/FeeOracle.js +1 -0
- package/lib/abis/LendingPool.js +1 -0
- package/lib/abis/LendingPoolV2.js +1 -0
- package/lib/abis/Multicall3.js +3 -0
- package/lib/abis/OFT.d.ts +20 -0
- package/lib/abis/OFT.js +20 -0
- package/lib/abis/SmartAccount.d.ts +1 -0
- package/lib/abis/SmartAccount.js +3 -0
- package/lib/abis/SwapRouter.d.ts +1 -0
- package/lib/abis/SwapRouter.js +1 -0
- package/lib/abis/UniversalSignatureValidator.js +3 -0
- package/lib/abis/index.d.ts +5 -0
- package/lib/abis/index.js +5 -0
- package/lib/adapters/evm/getters.d.ts +17 -2
- package/lib/adapters/evm/getters.js +35 -3
- package/lib/adapters/evm/index.d.ts +266 -4
- package/lib/adapters/evm/index.js +270 -1
- package/lib/adapters/evm/utils.d.ts +6 -0
- package/lib/adapters/evm/utils.js +7 -0
- package/lib/adapters/solana/constants.d.ts +1 -1
- package/lib/adapters/solana/constants.js +4 -1
- package/lib/adapters/solana/getters.d.ts +9 -1
- package/lib/adapters/solana/getters.js +21 -0
- package/lib/adapters/solana/idl/vault-idl.js +9 -0
- package/lib/adapters/solana/index.d.ts +59 -4
- package/lib/adapters/solana/index.js +61 -0
- package/lib/adapters/solana/types.d.ts +1 -1
- package/lib/adapters/solana/utils.d.ts +31 -3
- package/lib/adapters/solana/utils.js +79 -4
- package/lib/adapters/solana/vault.actions.d.ts +23 -4
- package/lib/adapters/solana/vault.actions.js +47 -3
- package/lib/adapters/stellar/actions.d.ts +26 -1
- package/lib/adapters/stellar/actions.js +33 -0
- package/lib/adapters/stellar/constants.d.ts +26 -0
- package/lib/adapters/stellar/constants.js +29 -0
- package/lib/adapters/stellar/getters.d.ts +56 -0
- package/lib/adapters/stellar/getters.js +81 -0
- package/lib/adapters/stellar/index.d.ts +70 -0
- package/lib/adapters/stellar/index.js +71 -0
- package/lib/adapters/stellar/soroban.d.ts +21 -1
- package/lib/adapters/stellar/soroban.js +47 -1
- package/lib/adapters/stellar/submit.d.ts +29 -0
- package/lib/adapters/stellar/submit.js +90 -5
- package/lib/adapters/stellar/types.d.ts +27 -0
- package/lib/adapters/stellar/types.js +3 -0
- package/lib/adapters/stellar/utils.d.ts +10 -0
- package/lib/adapters/stellar/utils.js +10 -0
- package/lib/adapters/sui/getters.d.ts +7 -1
- package/lib/adapters/sui/getters.js +6 -0
- package/lib/adapters/sui/index.d.ts +16 -1
- package/lib/adapters/sui/index.js +17 -0
- package/lib/adapters/sui/transformer.d.ts +8 -2
- package/lib/adapters/sui/transformer.js +7 -0
- package/lib/adapters/sui/types.d.ts +1 -1
- package/lib/adapters/sui/utils.d.ts +6 -0
- package/lib/adapters/sui/utils.js +6 -0
- package/lib/core/analytics/chain-name.d.ts +8 -0
- package/lib/core/analytics/chain-name.js +8 -0
- package/lib/core/analytics/constants.d.ts +4 -0
- package/lib/core/analytics/constants.js +4 -0
- package/lib/core/analytics/env.d.ts +25 -0
- package/lib/core/analytics/env.js +26 -0
- package/lib/core/analytics/index.d.ts +26 -0
- package/lib/core/analytics/index.js +35 -0
- package/lib/core/analytics/instrumentation.d.ts +26 -0
- package/lib/core/analytics/instrumentation.js +66 -2
- package/lib/core/analytics/method-taxonomy.d.ts +16 -0
- package/lib/core/analytics/method-taxonomy.js +18 -0
- package/lib/core/analytics/metrics.d.ts +23 -0
- package/lib/core/analytics/metrics.js +40 -0
- package/lib/core/analytics/sanitize.d.ts +38 -0
- package/lib/core/analytics/sanitize.js +46 -0
- package/lib/core/analytics/sentry-runtime.d.ts +11 -0
- package/lib/core/analytics/sentry-runtime.js +20 -1
- package/lib/core/analytics/sentry.d.ts +47 -2
- package/lib/core/analytics/sentry.js +115 -2
- package/lib/core/analytics/types.d.ts +28 -1
- package/lib/core/analytics/user-identity.d.ts +36 -2
- package/lib/core/analytics/user-identity.js +43 -1
- package/lib/core/analytics/version.d.ts +6 -1
- package/lib/core/analytics/version.js +6 -1
- package/lib/core/auth/verify.js +5 -0
- package/lib/core/base.class.d.ts +78 -3
- package/lib/core/base.class.js +66 -4
- package/lib/core/cache.d.ts +5 -0
- package/lib/core/cache.js +6 -0
- package/lib/core/constants/adapters.d.ts +16 -1
- package/lib/core/constants/adapters.js +28 -8
- package/lib/core/constants/core.d.ts +14 -1
- package/lib/core/constants/core.js +18 -0
- package/lib/core/constants/swap-router.d.ts +83 -1
- package/lib/core/constants/swap-router.js +91 -1
- package/lib/core/constants/vaults.d.ts +57 -1
- package/lib/core/constants/vaults.js +66 -1
- package/lib/core/constants/web3.d.ts +4 -1
- package/lib/core/constants/web3.js +15 -5
- package/lib/core/errors/index.d.ts +38 -2
- package/lib/core/errors/index.js +38 -0
- package/lib/core/fetcher.d.ts +136 -2
- package/lib/core/fetcher.js +229 -25
- package/lib/core/helpers/adapters.d.ts +10 -1
- package/lib/core/helpers/adapters.js +11 -0
- package/lib/core/helpers/chain-address.d.ts +10 -0
- package/lib/core/helpers/chain-address.js +11 -0
- package/lib/core/helpers/core.d.ts +49 -4
- package/lib/core/helpers/core.js +74 -7
- package/lib/core/helpers/explorer-link.d.ts +15 -1
- package/lib/core/helpers/explorer-link.js +14 -0
- package/lib/core/helpers/signer.d.ts +27 -1
- package/lib/core/helpers/signer.js +39 -0
- package/lib/core/helpers/swap-router.d.ts +33 -1
- package/lib/core/helpers/swap-router.js +32 -0
- package/lib/core/helpers/vault-version.d.ts +2 -1
- package/lib/core/helpers/vault-version.js +2 -0
- package/lib/core/helpers/vaults.d.ts +9 -1
- package/lib/core/helpers/vaults.js +22 -8
- package/lib/core/helpers/web3.d.ts +154 -2
- package/lib/core/helpers/web3.js +192 -15
- package/lib/core/logger/index.d.ts +55 -0
- package/lib/core/logger/index.js +19 -0
- package/lib/core/logger/slack.d.ts +3 -0
- package/lib/core/logger/slack.js +3 -0
- package/lib/core/vault-metadata.d.ts +7 -1
- package/lib/core/vault-metadata.js +6 -0
- package/lib/core/version-check.d.ts +52 -0
- package/lib/core/version-check.js +82 -1
- package/lib/evm/methods/crossChainVault.d.ts +90 -0
- package/lib/evm/methods/crossChainVault.js +187 -2
- package/lib/evm/methods/crossChainVaultRegistry.d.ts +93 -0
- package/lib/evm/methods/crossChainVaultRegistry.js +240 -0
- package/lib/evm/methods/index.d.ts +1 -0
- package/lib/evm/methods/index.js +1 -0
- package/lib/evm/types/crossChain.d.ts +202 -0
- package/lib/evm/types/crossChain.js +12 -1
- package/lib/index.d.ts +16 -0
- package/lib/index.js +19 -0
- package/lib/main.d.ts +289 -6
- package/lib/main.js +312 -0
- package/lib/modules/api/index.d.ts +1 -0
- package/lib/modules/api/index.js +6 -0
- package/lib/modules/api/main.d.ts +52 -0
- package/lib/modules/api/main.js +130 -0
- package/lib/modules/sub-accounts/fetcher.d.ts +17 -2
- package/lib/modules/sub-accounts/fetcher.js +15 -0
- package/lib/modules/sub-accounts/main.d.ts +35 -2
- package/lib/modules/sub-accounts/main.js +39 -1
- package/lib/modules/sub-accounts/utils.d.ts +4 -1
- package/lib/modules/sub-accounts/utils.js +3 -0
- package/lib/modules/vaults/adapter.helpers.d.ts +21 -3
- package/lib/modules/vaults/adapter.helpers.js +34 -0
- package/lib/modules/vaults/fetcher.d.ts +45 -0
- package/lib/modules/vaults/fetcher.js +91 -4
- package/lib/modules/vaults/getters.d.ts +295 -0
- package/lib/modules/vaults/getters.js +567 -26
- package/lib/modules/vaults/index.d.ts +12 -0
- package/lib/modules/vaults/index.js +12 -0
- package/lib/modules/vaults/main.d.ts +292 -4
- package/lib/modules/vaults/main.js +382 -8
- package/lib/modules/vaults/read.actions.d.ts +170 -2
- package/lib/modules/vaults/read.actions.js +143 -0
- package/lib/modules/vaults/types.d.ts +34 -0
- package/lib/modules/vaults/utils/call-data-decoder.d.ts +48 -1
- package/lib/modules/vaults/utils/call-data-decoder.js +56 -0
- package/lib/modules/vaults/utils/date-utils.d.ts +39 -0
- package/lib/modules/vaults/utils/date-utils.js +47 -1
- package/lib/modules/vaults/utils.d.ts +85 -0
- package/lib/modules/vaults/utils.js +139 -7
- package/lib/modules/vaults/write.actions.d.ts +363 -3
- package/lib/modules/vaults/write.actions.js +399 -3
- package/lib/polyfills.js +4 -2
- package/lib/sdk.d.ts +23847 -0
- package/lib/services/coingecko/fetcher.d.ts +15 -1
- package/lib/services/coingecko/fetcher.js +30 -9
- package/lib/services/debank/fetcher.d.ts +15 -1
- package/lib/services/debank/fetcher.js +12 -0
- package/lib/services/debank/utils.d.ts +1 -1
- package/lib/services/debank/utils.js +18 -1
- package/lib/services/layerzero/deposits.d.ts +12 -1
- package/lib/services/layerzero/deposits.js +34 -11
- package/lib/services/layerzero/redeems.d.ts +11 -1
- package/lib/services/layerzero/redeems.js +13 -0
- package/lib/services/layerzero/utils.d.ts +8 -0
- package/lib/services/layerzero/utils.js +11 -0
- package/lib/services/octavfi/fetcher.d.ts +8 -1
- package/lib/services/octavfi/fetcher.js +25 -0
- package/lib/services/octavfi/types.d.ts +1 -1
- package/lib/services/octavfi/utils.d.ts +14 -2
- package/lib/services/octavfi/utils.js +44 -10
- package/lib/services/subgraph/fetcher.js +4 -2
- package/lib/services/subgraph/vaults.d.ts +12 -0
- package/lib/services/subgraph/vaults.js +119 -2
- package/lib/services/swap-quotes/index.d.ts +80 -0
- package/lib/services/swap-quotes/index.js +27 -1
- package/lib/services/swap-quotes/paraswap.d.ts +25 -0
- package/lib/services/swap-quotes/paraswap.js +22 -0
- package/lib/types/pools.d.ts +5 -2
- package/lib/types/staking.d.ts +1 -1
- package/lib/types/sub-accounts.d.ts +1 -1
- package/lib/types/subgraph.d.ts +10 -1
- package/lib/types/typed-contract.d.ts +64 -0
- package/lib/types/vaults.d.ts +163 -2
- package/lib/types/vaults.js +10 -0
- package/lib/types/web3.d.ts +9 -1
- package/lib/types/web3.js +1 -0
- package/lib/types/webserver.d.ts +47 -2
- package/package.json +6 -5
|
@@ -84,6 +84,31 @@ const date_utils_1 = require("./utils/date-utils");
|
|
|
84
84
|
const call_data_decoder_1 = require("./utils/call-data-decoder");
|
|
85
85
|
const deposits_1 = require("../../services/layerzero/deposits");
|
|
86
86
|
const redeems_1 = require("../../services/layerzero/redeems");
|
|
87
|
+
/**
|
|
88
|
+
* Vault Data Getters
|
|
89
|
+
*
|
|
90
|
+
* Main query functions for vault data across chains:
|
|
91
|
+
* - getVault: Comprehensive vault details with loans/allocations
|
|
92
|
+
* - getVaultLoans: Active loan data with on-chain enrichment
|
|
93
|
+
* - getVaultAllocations: DeFi/CeFi/OTC allocation breakdowns
|
|
94
|
+
* - getVaultAvailableRedemptions: Claimable redemption requests
|
|
95
|
+
* - getVaultRedemptionHistory: Historical redemption data
|
|
96
|
+
* - getVaultPositions: User positions across vaults
|
|
97
|
+
* - getVaultApy: @deprecated Current and historical APY data
|
|
98
|
+
* - getRewardsStakingPositions: Staking positions and rewards
|
|
99
|
+
* - getVaultTvl: Current and historical TVL
|
|
100
|
+
* - getVaultHistoricalTimeseries: Time series data for vault metrics
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* Fetch comprehensive vault data including on-chain state and backend metadata.
|
|
104
|
+
* Routes to appropriate chain adapter (EVM v1/v2, Solana, or Stellar) based on vault version.
|
|
105
|
+
* Optionally enriches with loan and allocation data.
|
|
106
|
+
* @param vault - Vault contract address (EVM hex, Stellar C-address, or Solana program ID)
|
|
107
|
+
* @param loans - Include active loan data
|
|
108
|
+
* @param allocations - Include DeFi/CeFi allocation breakdowns
|
|
109
|
+
* @param options - RPC and service configuration
|
|
110
|
+
* @returns Complete vault object with optional enrichments
|
|
111
|
+
*/
|
|
87
112
|
async function getVault({ vault, loans = false, allocations = false, options, loadSubaccounts, loadSnapshots, }) {
|
|
88
113
|
let returnedVault;
|
|
89
114
|
try {
|
|
@@ -116,6 +141,8 @@ async function getVault({ vault, loans = false, allocations = false, options, lo
|
|
|
116
141
|
core_1.Logger.log.error('getVault', err, { vault });
|
|
117
142
|
throw new Error(`#getVault::${vault}: ${err?.message}`);
|
|
118
143
|
}
|
|
144
|
+
// Loans/allocations enrichment is only supported for EVM vaults.
|
|
145
|
+
// Non-EVM vaults already include empty defaults.
|
|
119
146
|
const isEvmVault = returnedVault.version !== 'sol-0' &&
|
|
120
147
|
returnedVault.version !== 'stellar-0' &&
|
|
121
148
|
returnedVault.version !== 'sui-0';
|
|
@@ -161,19 +188,25 @@ async function getVault({ vault, loans = false, allocations = false, options, lo
|
|
|
161
188
|
}
|
|
162
189
|
return returnedVault;
|
|
163
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Vault Loans
|
|
193
|
+
*/
|
|
164
194
|
async function getVaultLoans(vault, options) {
|
|
195
|
+
// sanitize
|
|
165
196
|
const vaultAddress = typeof vault === 'string' ? vault : vault.address;
|
|
166
197
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers))?.[0];
|
|
167
198
|
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
168
199
|
if (vaultVersion !== 'evm-0')
|
|
169
|
-
return [];
|
|
200
|
+
return []; // only evm-0 uses subaccount loans
|
|
170
201
|
try {
|
|
171
202
|
let poolTotalSupply;
|
|
172
203
|
let poolDecimals;
|
|
173
204
|
const chainId = options?.chainId || tokenizedVault?.chain;
|
|
174
205
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
175
206
|
const tokenizedVaultLoans = await (0, core_1.fetchTokenizedVaultLoans)(vaultAddress, chainId);
|
|
207
|
+
// if user passed IAddress or a full IVault
|
|
176
208
|
if (typeof vault === 'string') {
|
|
209
|
+
// fetch necessary data
|
|
177
210
|
const poolContract = (0, core_1.createContract)({
|
|
178
211
|
provider,
|
|
179
212
|
address: vaultAddress,
|
|
@@ -190,14 +223,21 @@ async function getVaultLoans(vault, options) {
|
|
|
190
223
|
poolTotalSupply = vault.totalSupply.raw;
|
|
191
224
|
poolDecimals = vault.decimals;
|
|
192
225
|
}
|
|
226
|
+
// fetch and format all the loans
|
|
193
227
|
const newLoans = (await Promise.all((Array.isArray(tokenizedVaultLoans) ? tokenizedVaultLoans : [])?.map(async (l) => {
|
|
228
|
+
// variables
|
|
194
229
|
const borrower = l.borrower;
|
|
230
|
+
// calculate allocation
|
|
195
231
|
const allocation = l.principal_amount /
|
|
196
232
|
Number((0, ethers_1.formatUnits)(poolTotalSupply, poolDecimals));
|
|
233
|
+
// get august loan fee from oracle
|
|
197
234
|
const loanFeeRate = await (0, core_1.getLoanOracleFeeRate)(provider, 'LOAN.REPAY.INTERESTS', l.address, chainId);
|
|
235
|
+
// calculate loan level apr
|
|
198
236
|
const loanApr = Number(l.apr || 0) / 100;
|
|
199
237
|
const loanAprAfterFees = loanApr * (1 - loanFeeRate / 100);
|
|
238
|
+
// determine if capital is deployed
|
|
200
239
|
const isIdleCapital = core_1.IDLE_CAPITAL_BORROWER_ADDRESS.includes(borrower);
|
|
240
|
+
// build new obj
|
|
201
241
|
const newLoanObj = {
|
|
202
242
|
vault: vaultAddress,
|
|
203
243
|
address: l.address,
|
|
@@ -228,7 +268,11 @@ async function getVaultLoans(vault, options) {
|
|
|
228
268
|
throw new Error(`#getVaultLoans::${vault}:${e?.message}`);
|
|
229
269
|
}
|
|
230
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Vault Subaccount Loans
|
|
273
|
+
*/
|
|
231
274
|
async function getVaultSubaccountLoans(vault, options) {
|
|
275
|
+
// sanitize
|
|
232
276
|
const vaultAddress = typeof vault === 'string' ? vault : vault.address;
|
|
233
277
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers))?.[0];
|
|
234
278
|
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
@@ -240,7 +284,9 @@ async function getVaultSubaccountLoans(vault, options) {
|
|
|
240
284
|
const chainId = options?.chainId || tokenizedVault?.chain;
|
|
241
285
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
242
286
|
const tokenizedVaultLoans = await (0, core_1.fetchTokenizedVaultSubaccountLoans)(vaultAddress, chainId);
|
|
287
|
+
// if user passed IAddress or a full IVault
|
|
243
288
|
if (typeof vault === 'string') {
|
|
289
|
+
// fetch necessary data
|
|
244
290
|
const poolContract = (0, core_1.createContract)({
|
|
245
291
|
provider,
|
|
246
292
|
address: vaultAddress,
|
|
@@ -257,14 +303,21 @@ async function getVaultSubaccountLoans(vault, options) {
|
|
|
257
303
|
poolTotalSupply = vault.totalSupply.raw;
|
|
258
304
|
poolDecimals = vault.decimals;
|
|
259
305
|
}
|
|
306
|
+
// fetch and format all the loans
|
|
260
307
|
const newLoans = (await Promise.all((Array.isArray(tokenizedVaultLoans) ? tokenizedVaultLoans : [])?.map(async (l) => {
|
|
308
|
+
// variables
|
|
261
309
|
const borrower = l.borrower;
|
|
310
|
+
// calculate allocation
|
|
262
311
|
const allocation = l.principal_amount /
|
|
263
312
|
Number((0, ethers_1.formatUnits)(poolTotalSupply, poolDecimals));
|
|
313
|
+
// get august loan fee from oracle
|
|
264
314
|
const loanFeeRate = await (0, core_1.getLoanOracleFeeRate)(provider, 'LOAN.REPAY.INTERESTS', l.address, chainId);
|
|
315
|
+
// calculate loan level apr
|
|
265
316
|
const loanApr = Number(l.apr || 0) / 100;
|
|
266
317
|
const loanAprAfterFees = loanApr * (1 - loanFeeRate / 100);
|
|
318
|
+
// determine if capital is deployed
|
|
267
319
|
const isIdleCapital = core_1.IDLE_CAPITAL_BORROWER_ADDRESS.includes(borrower);
|
|
320
|
+
// build new obj
|
|
268
321
|
const newLoanObj = {
|
|
269
322
|
vault: vaultAddress,
|
|
270
323
|
address: l.address,
|
|
@@ -295,7 +348,19 @@ async function getVaultSubaccountLoans(vault, options) {
|
|
|
295
348
|
throw new Error(`#getVaultSubaccountLoans::${vault}:${e?.message}`);
|
|
296
349
|
}
|
|
297
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Vault Allocations
|
|
353
|
+
*/
|
|
354
|
+
/**
|
|
355
|
+
* Fetch comprehensive vault asset allocation breakdown.
|
|
356
|
+
* Includes DeFi protocols (via DeBank), CeFi balances, OTC positions, and loans.
|
|
357
|
+
* Categorizes exposures by borrowing, supplying, lending, and wallet holdings.
|
|
358
|
+
* @param vault - Vault address
|
|
359
|
+
* @param options - RPC configuration
|
|
360
|
+
* @returns Detailed allocation data with exposure categorization
|
|
361
|
+
*/
|
|
298
362
|
async function getVaultAllocations(vault, options) {
|
|
363
|
+
// to be used on upshift UI
|
|
299
364
|
const protocolExposure = [];
|
|
300
365
|
const tokenExposure = [];
|
|
301
366
|
let cefiExposure = [];
|
|
@@ -322,8 +387,10 @@ async function getVaultAllocations(vault, options) {
|
|
|
322
387
|
: null;
|
|
323
388
|
},
|
|
324
389
|
};
|
|
390
|
+
// Determine wallets to fetch allocation responses from
|
|
325
391
|
let uniqueBorrowers = [];
|
|
326
392
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
393
|
+
// fetch all vault unique subaccounts/loan borrowers
|
|
327
394
|
try {
|
|
328
395
|
const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
329
396
|
if (vaultVersion === 'evm-0') {
|
|
@@ -341,25 +408,30 @@ async function getVaultAllocations(vault, options) {
|
|
|
341
408
|
catch (e) {
|
|
342
409
|
core_1.Logger.log.error('getVaultAllocations:borrowers', e);
|
|
343
410
|
}
|
|
411
|
+
// if hardcoded subaccount exists for vault, use that
|
|
344
412
|
const hardcodedSubaccount = Object.values(core_1.VAULT_ALLOCATION_SUBACCOUNTS).find((entry) => entry.address.toLowerCase() === vault.toLowerCase() && !entry.useDebank);
|
|
345
413
|
if (hardcodedSubaccount) {
|
|
346
414
|
const debankRes = await (0, debank_1.fetchDebankResponse)(hardcodedSubaccount.subaccount);
|
|
347
415
|
if (debankRes === false) {
|
|
348
416
|
debankErr = true;
|
|
349
417
|
}
|
|
418
|
+
// pull data from debank results
|
|
350
419
|
(0, debank_1.parseVaultLevelDebank)(debankRes, protocolExposure, tokenExposure, hardcodedSubaccount.subaccount, exposurePerCategory, netValue);
|
|
351
420
|
unfilteredTokens = debankRes?.subaccount?.tokens;
|
|
352
421
|
const debankPerLoan = (0, debank_1.parseLoanLevelDebank)(debankRes);
|
|
353
422
|
defiPerBorrower[hardcodedSubaccount.subaccount] = debankPerLoan;
|
|
354
423
|
}
|
|
355
424
|
else {
|
|
425
|
+
// Fetch DeBank data for all subaccounts in a single request (EVM only)
|
|
356
426
|
if ((0, core_1.getAddressChainType)(vault) === 'evm') {
|
|
357
427
|
const vaultDebankData = await (0, debank_1.fetchVaultDebankResponse)(vault, options?.headers);
|
|
358
428
|
if (vaultDebankData === false) {
|
|
359
429
|
debankErr = true;
|
|
360
430
|
}
|
|
361
431
|
else {
|
|
432
|
+
// Process DeBank data for each subaccount
|
|
362
433
|
for (const [subaccountAddress, subaccountData] of Object.entries(vaultDebankData)) {
|
|
434
|
+
// Skip if subaccount has an error
|
|
363
435
|
if ('error' in subaccountData) {
|
|
364
436
|
core_1.Logger.log.error('getVaultAllocations:debank:subaccount', {
|
|
365
437
|
subaccountAddress,
|
|
@@ -367,6 +439,7 @@ async function getVaultAllocations(vault, options) {
|
|
|
367
439
|
});
|
|
368
440
|
continue;
|
|
369
441
|
}
|
|
442
|
+
// Transform to match existing parser format: { subaccount: { positions, app_positions, tokens } }
|
|
370
443
|
const debankRes = {
|
|
371
444
|
subaccount: {
|
|
372
445
|
positions: subaccountData.positions || [],
|
|
@@ -381,6 +454,8 @@ async function getVaultAllocations(vault, options) {
|
|
|
381
454
|
}
|
|
382
455
|
}
|
|
383
456
|
}
|
|
457
|
+
// Fetch portfolio data for non-EVM borrowers (EVM handled by fetchVaultDebankResponse above).
|
|
458
|
+
// CeFi and OTC fetches below run for ALL borrowers regardless of chain type.
|
|
384
459
|
for (const borrower of uniqueBorrowers) {
|
|
385
460
|
const chainType = (0, core_1.getAddressChainType)(borrower);
|
|
386
461
|
if (chainType !== 'evm') {
|
|
@@ -414,6 +489,7 @@ async function getVaultAllocations(vault, options) {
|
|
|
414
489
|
debankErr = true;
|
|
415
490
|
}
|
|
416
491
|
}
|
|
492
|
+
// fetch cefi_positions
|
|
417
493
|
try {
|
|
418
494
|
const cefiResponse = await (0, core_1.fetchAugustWithKey)(options.augustKey, core_1.WEBSERVER_ENDPOINTS.subaccount.cefi(borrower), { headers: options?.headers });
|
|
419
495
|
if (cefiResponse.status !== 200) {
|
|
@@ -430,6 +506,7 @@ async function getVaultAllocations(vault, options) {
|
|
|
430
506
|
catch (e) {
|
|
431
507
|
core_1.Logger.log.error('getVaultAllocations:cefi', e, { borrower });
|
|
432
508
|
}
|
|
509
|
+
// fetch otc_positions
|
|
433
510
|
try {
|
|
434
511
|
const otcResponse = await (0, core_1.fetchAugustWithKey)(options.augustKey, core_1.WEBSERVER_ENDPOINTS.subaccount.otc_positions(borrower), { headers: options?.headers });
|
|
435
512
|
if (otcResponse.status !== 200) {
|
|
@@ -466,11 +543,14 @@ async function getVaultAllocations(vault, options) {
|
|
|
466
543
|
defiPerBorrower,
|
|
467
544
|
exposurePerCategory,
|
|
468
545
|
netValue: netValue.value,
|
|
546
|
+
// this would be the debank token response field without any parsing. Different from
|
|
547
|
+
// tokens. Could use a better name tho
|
|
469
548
|
unfilteredTokens: unfilteredTokens,
|
|
470
549
|
};
|
|
471
550
|
}
|
|
472
551
|
async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
473
552
|
try {
|
|
553
|
+
// Stellar vaults don't support on-chain redemptions yet
|
|
474
554
|
if ((0, utils_3.isStellarAddress)(vault)) {
|
|
475
555
|
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Available redemptions is not yet supported for Stellar vaults: ${vault}`);
|
|
476
556
|
return {
|
|
@@ -482,6 +562,7 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
482
562
|
}),
|
|
483
563
|
};
|
|
484
564
|
}
|
|
565
|
+
// setup
|
|
485
566
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
486
567
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
|
|
487
568
|
if (!tokenizedVault) {
|
|
@@ -496,8 +577,10 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
496
577
|
}),
|
|
497
578
|
};
|
|
498
579
|
}
|
|
580
|
+
// version split logic
|
|
499
581
|
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
500
582
|
let decimals;
|
|
583
|
+
// Use ABI_LENDING_POOL_V2 for both versions as it has the methods we need
|
|
501
584
|
const vaultContract = (0, core_1.createContract)({
|
|
502
585
|
address: vault,
|
|
503
586
|
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
@@ -512,22 +595,26 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
512
595
|
const ld = await vaultContract.lagDuration();
|
|
513
596
|
const lagDuration = Number(ld);
|
|
514
597
|
const { withdrawalRequesteds, withdrawalProcesseds } = await (0, subgraph_1.getSubgraphAllWithdrawals)(vault, provider);
|
|
598
|
+
// format
|
|
515
599
|
const availableRedemptions = [];
|
|
516
600
|
const pendingRedemptions = [];
|
|
517
601
|
for (const ev of withdrawalRequesteds) {
|
|
602
|
+
// ensure event is defined
|
|
518
603
|
if (!ev || typeof ev !== 'object') {
|
|
519
604
|
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping invalid event: ${ev}`);
|
|
520
605
|
continue;
|
|
521
606
|
}
|
|
522
|
-
let month;
|
|
523
|
-
let day;
|
|
524
|
-
let year;
|
|
607
|
+
let month = 0;
|
|
608
|
+
let day = 0;
|
|
609
|
+
let year = 0;
|
|
525
610
|
if (version === 'evm-2') {
|
|
611
|
+
// Use UTC-based computeClaimableDate to mirror contract logic exactly
|
|
526
612
|
const claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
|
|
527
613
|
year = claimable.year;
|
|
528
614
|
month = claimable.month;
|
|
529
615
|
day = claimable.day;
|
|
530
616
|
}
|
|
617
|
+
// Validate required event properties
|
|
531
618
|
if (!ev.year || !ev.month || !ev.day || !ev.receiverAddr) {
|
|
532
619
|
if (version !== 'evm-2') {
|
|
533
620
|
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Skipping event with missing required properties: ${ev}`);
|
|
@@ -545,9 +632,12 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
545
632
|
const fullDate = version === 'evm-2'
|
|
546
633
|
? new Date(Date.UTC(year, month - 1, day))
|
|
547
634
|
: new Date(Number(ev.year), Number(ev.month) - 1, Number(ev.day));
|
|
635
|
+
// Match request to processed event using deterministic UTC date key
|
|
548
636
|
const requestDateKey = version === 'evm-2'
|
|
549
637
|
? (0, date_utils_1.formatDateKey)(year, month, day)
|
|
550
638
|
: (0, date_utils_1.formatDateKey)(Number(ev.year), Number(ev.month), Number(ev.day));
|
|
639
|
+
// NOTE: evm-2 subgraph does not emit WithdrawalProcessed events, so matching
|
|
640
|
+
// relies on timestamp-based date comparison for evm-2 (deterministic for evm-1).
|
|
551
641
|
const foundRedemptionAgainstClaim = withdrawalProcesseds.find((h) => {
|
|
552
642
|
const proc = h;
|
|
553
643
|
if (proc.receiverAddr !== ev.receiverAddr)
|
|
@@ -562,9 +652,11 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
562
652
|
return procKey === requestDateKey;
|
|
563
653
|
});
|
|
564
654
|
if (wallet && (0, ethers_1.isAddress)(wallet)) {
|
|
655
|
+
// check if connected users address
|
|
565
656
|
if (ev?.receiverAddr?.toLowerCase() === wallet.toLowerCase()) {
|
|
566
657
|
const alreadyRedeemed = version === 'evm-2'
|
|
567
658
|
? withdrawalProcesseds.find((red) => {
|
|
659
|
+
// Match by receiver + UTC date key (deterministic)
|
|
568
660
|
if (red.receiverAddr.toLowerCase() !== wallet.toLowerCase()) {
|
|
569
661
|
return false;
|
|
570
662
|
}
|
|
@@ -576,9 +668,11 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
576
668
|
BigInt(red.month.raw) === BigInt(ev.month) &&
|
|
577
669
|
BigInt(red.year.raw) === BigInt(ev.year));
|
|
578
670
|
if (!(foundRedemptionAgainstClaim && alreadyRedeemed)) {
|
|
671
|
+
// double check if user has already claimed
|
|
579
672
|
try {
|
|
580
673
|
switch (version) {
|
|
581
674
|
case 'evm-2': {
|
|
675
|
+
// this is to avoid edge case where old withdrawal A is already redeemed but we are now looking at withdrawal B that is not yet pending
|
|
582
676
|
if (alreadyRedeemed) {
|
|
583
677
|
const index = withdrawalProcesseds.findIndex((item) => item.id === alreadyRedeemed.id);
|
|
584
678
|
if (index !== -1) {
|
|
@@ -586,12 +680,16 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
586
680
|
}
|
|
587
681
|
}
|
|
588
682
|
if (!alreadyRedeemed) {
|
|
683
|
+
// Recompute claimable date (includes lag + 300s manipulation window) to determine if claimable now
|
|
589
684
|
const claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
|
|
590
685
|
const pendingCondition = (0, date_utils_1.isClaimableNow)(claimable.epoch, Math.floor(Date.now() / 1000));
|
|
591
686
|
if (pendingCondition) {
|
|
687
|
+
// Has passed lag duration - check if claim is not settled before adding to pending
|
|
592
688
|
try {
|
|
593
689
|
const burnableAmount = (await vaultContract?.getBurnableAmountByReceiver?.(BigInt(year), BigInt(month), BigInt(day), (0, ethers_1.getAddress)(ev.receiverAddr))) || BigInt(0);
|
|
594
690
|
const claimAmount = BigInt(ev.shares) || BigInt(0);
|
|
691
|
+
// If burnableAmount >= claimAmount, claim hasn't been settled by processAllClaims
|
|
692
|
+
// Only add to pending if it's still unsettled
|
|
595
693
|
if (burnableAmount >= claimAmount) {
|
|
596
694
|
pendingRedemptions.push({
|
|
597
695
|
id: ev.transactionHash_ || ev.id,
|
|
@@ -610,9 +708,11 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
610
708
|
}
|
|
611
709
|
catch (burnableError) {
|
|
612
710
|
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Failed to check burnable amount for pending redemption: ${burnableError}`);
|
|
711
|
+
// If check fails, don't add to pending to be safe
|
|
613
712
|
}
|
|
614
713
|
}
|
|
615
714
|
else {
|
|
715
|
+
// Hasn't passed lag duration - add to available redemptions only
|
|
616
716
|
availableRedemptions.push({
|
|
617
717
|
id: ev.transactionHash_ || ev.id,
|
|
618
718
|
hash: ev.transactionHash_ || ev.id,
|
|
@@ -633,15 +733,19 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
633
733
|
default: {
|
|
634
734
|
const trueClaimableAmount = await vaultContract?.getClaimableAmountByReceiver?.(BigInt(ev.year), BigInt(ev.month), BigInt(ev.day), (0, ethers_1.getAddress)(wallet));
|
|
635
735
|
if (trueClaimableAmount > BigInt(0)) {
|
|
736
|
+
// Use computeClaimableDate for UTC-correct date with lag
|
|
636
737
|
const v1Claimable = (0, date_utils_1.computeClaimableDate)(Number(ev.timestamp_), lagDuration);
|
|
637
738
|
const pendingCondition = (0, date_utils_1.isClaimableNow)(v1Claimable.epoch, Math.floor(Date.now() / 1000));
|
|
638
739
|
if (pendingCondition) {
|
|
740
|
+
// Has passed lag duration - check if claim is not settled before adding to pending
|
|
639
741
|
try {
|
|
640
742
|
year = v1Claimable.year;
|
|
641
743
|
month = v1Claimable.month;
|
|
642
744
|
day = v1Claimable.day;
|
|
643
745
|
const burnableAmount = (await vaultContract?.getBurnableAmountByReceiver?.(BigInt(year), BigInt(month), BigInt(day), (0, ethers_1.getAddress)(ev.receiverAddr))) || BigInt(0);
|
|
644
746
|
const claimAmount = trueClaimableAmount || BigInt(0);
|
|
747
|
+
// If burnableAmount >= claimAmount, claim hasn't been settled by processAllClaims
|
|
748
|
+
// Only add to pending if it's still unsettled
|
|
645
749
|
if (burnableAmount >= claimAmount) {
|
|
646
750
|
pendingRedemptions.push({
|
|
647
751
|
id: ev.transactionHash_ || ev.id,
|
|
@@ -660,9 +764,11 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
660
764
|
}
|
|
661
765
|
catch (burnableError) {
|
|
662
766
|
core_1.Logger.log.warn('getVaultAvailableRedemptions', `Failed to check burnable amount for pending redemption: ${burnableError}`);
|
|
767
|
+
// If check fails, don't add to pending to be safe
|
|
663
768
|
}
|
|
664
769
|
}
|
|
665
770
|
else {
|
|
771
|
+
// Hasn't passed lag duration - add to available redemptions only
|
|
666
772
|
availableRedemptions.push({
|
|
667
773
|
id: ev.transactionHash_ || ev.id,
|
|
668
774
|
hash: ev.transactionHash_ || ev.id,
|
|
@@ -733,10 +839,12 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
733
839
|
}
|
|
734
840
|
async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, options, }) {
|
|
735
841
|
try {
|
|
842
|
+
// Stellar vaults: no on-chain redemption history yet
|
|
736
843
|
if ((0, utils_3.isStellarAddress)(vault)) {
|
|
737
844
|
core_1.Logger.log.warn('getVaultRedemptionHistory', `Redemption history is not yet supported for Stellar vaults: ${vault}`);
|
|
738
845
|
return [];
|
|
739
846
|
}
|
|
847
|
+
// setup
|
|
740
848
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
741
849
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers, false, false))?.[0];
|
|
742
850
|
if (!tokenizedVault) {
|
|
@@ -746,16 +854,18 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
746
854
|
const chainId = tokenizedVault.chain;
|
|
747
855
|
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
748
856
|
const isV2 = version === 'evm-2';
|
|
857
|
+
// Use correct ABI based on vault version — different WithdrawalProcessed event signatures
|
|
749
858
|
const vaultAbi = isV2 ? TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2 : abis_1.ABI_LENDING_POOL_V2;
|
|
750
859
|
const poolContract = (0, core_1.createContract)({
|
|
751
860
|
address: vault,
|
|
752
861
|
abi: vaultAbi,
|
|
753
862
|
provider,
|
|
754
863
|
});
|
|
864
|
+
// query getLogs
|
|
755
865
|
const currentBlock = await provider.getBlockNumber();
|
|
756
866
|
const blockSkip = (0, core_1.determineBlockSkipInternal)(chainId);
|
|
757
867
|
const cutoffBlock = currentBlock - (lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId));
|
|
758
|
-
const BATCH_SIZE = 20;
|
|
868
|
+
const BATCH_SIZE = 20; // Max concurrent RPC requests per batch
|
|
759
869
|
core_1.Logger.log.info('getVaultRedemptionHistory', {
|
|
760
870
|
vault,
|
|
761
871
|
version,
|
|
@@ -766,6 +876,7 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
766
876
|
blockSkip,
|
|
767
877
|
totalRangeBlocks: currentBlock - cutoffBlock,
|
|
768
878
|
});
|
|
879
|
+
// Build all query ranges
|
|
769
880
|
const ranges = [];
|
|
770
881
|
let endBlock = currentBlock;
|
|
771
882
|
while (endBlock >= cutoffBlock) {
|
|
@@ -779,6 +890,11 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
779
890
|
firstRange: ranges[0],
|
|
780
891
|
lastRange: ranges[ranges.length - 1],
|
|
781
892
|
});
|
|
893
|
+
// Execute in batches to avoid RPC rate limits on high-block-count chains.
|
|
894
|
+
// Use allSettled + explicit throw so a chunk failure surfaces to the caller
|
|
895
|
+
// instead of silently returning an empty result (which used to corrupt
|
|
896
|
+
// `getWithdrawalRequestsWithStatus` into reporting `ready_to_claim` for
|
|
897
|
+
// requests that were already processed).
|
|
782
898
|
const logs = [];
|
|
783
899
|
for (let i = 0; i < ranges.length; i += BATCH_SIZE) {
|
|
784
900
|
const batch = ranges.slice(i, i + BATCH_SIZE);
|
|
@@ -799,6 +915,8 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
799
915
|
const batchLogs = batchResults.flatMap((r) => r.status === 'fulfilled' ? r.value : []);
|
|
800
916
|
logs.push(...batchLogs);
|
|
801
917
|
}
|
|
918
|
+
// evm-1: WithdrawalProcessed(uint256 assetsAmount, uint256 processedOn, address receiverAddr, uint256 requestedOn)
|
|
919
|
+
// evm-2: WithdrawalProcessed(uint256 assetsAmount, address indexed receiverAddr)
|
|
802
920
|
const iface = new ethers_1.ethers.Interface(isV2
|
|
803
921
|
? [
|
|
804
922
|
'event WithdrawalProcessed(uint256 assetsAmount, address indexed receiverAddr)',
|
|
@@ -806,17 +924,21 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
806
924
|
: [
|
|
807
925
|
'event WithdrawalProcessed(uint256 assetsAmount, uint256 processedOn, address receiverAddr, uint256 requestedOn)',
|
|
808
926
|
]);
|
|
927
|
+
// get pool decimals
|
|
809
928
|
const decimals = await (0, core_1.getDecimals)(provider, vault);
|
|
929
|
+
// For evm-2 events (no timestamps in event), fetch block timestamps
|
|
810
930
|
let blockTimestamps;
|
|
811
931
|
if (isV2 && logs.length > 0) {
|
|
812
932
|
const uniqueBlocks = [...new Set(logs.map((l) => l.blockNumber))];
|
|
813
933
|
const blockResults = await Promise.all(uniqueBlocks.map((bn) => provider.getBlock(bn)));
|
|
814
|
-
|
|
934
|
+
const timestamps = new Map();
|
|
815
935
|
blockResults.forEach((block) => {
|
|
816
936
|
if (block)
|
|
817
|
-
|
|
937
|
+
timestamps.set(block.number, block.timestamp);
|
|
818
938
|
});
|
|
939
|
+
blockTimestamps = timestamps;
|
|
819
940
|
}
|
|
941
|
+
// format — iterate logs directly to preserve transactionHash
|
|
820
942
|
const redemptions = [];
|
|
821
943
|
logs.forEach((log) => {
|
|
822
944
|
const ev = iface.parseLog(log);
|
|
@@ -831,7 +953,7 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
831
953
|
receiver: receiverAddr,
|
|
832
954
|
amount: (0, core_1.toNormalizedBn)(assetsAmount, decimals),
|
|
833
955
|
processed: blockDate,
|
|
834
|
-
requested: new Date(0),
|
|
956
|
+
requested: new Date(0), // evm-2 event has no requestedOn field; epoch 0 is a sentinel for "unknown"
|
|
835
957
|
vault,
|
|
836
958
|
transactionHash: log.transactionHash,
|
|
837
959
|
};
|
|
@@ -860,16 +982,25 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
860
982
|
return redemptions;
|
|
861
983
|
}
|
|
862
984
|
catch (e) {
|
|
985
|
+
// Log loudly and re-throw. Previously this caught all errors and returned
|
|
986
|
+
// [], which silently corrupted callers (notably getWithdrawalRequestsWithStatus,
|
|
987
|
+
// which would then report `ready_to_claim` for already-processed requests).
|
|
988
|
+
// Returning [] is reserved for the explicit early returns inside the try
|
|
989
|
+
// (unsupported chains, vault not in backend) — not for unexpected failures.
|
|
863
990
|
core_1.Logger.log.error('getVaultRedemptionHistory', e, { vault, wallet });
|
|
864
991
|
throw e;
|
|
865
992
|
}
|
|
866
993
|
}
|
|
994
|
+
/**
|
|
995
|
+
* Vault Positions
|
|
996
|
+
*/
|
|
867
997
|
async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, options, }) {
|
|
868
998
|
try {
|
|
869
999
|
const tokenizedVaults = await (0, core_1.fetchTokenizedVault)(vault ? vault : undefined, options?.headers, false, false);
|
|
870
1000
|
if (!tokenizedVaults || tokenizedVaults.length === 0) {
|
|
871
1001
|
return [];
|
|
872
1002
|
}
|
|
1003
|
+
// check if there are redemptions available or if you stake in this pool
|
|
873
1004
|
function renderStatus(_redemptions, _balance) {
|
|
874
1005
|
if (_redemptions?.length)
|
|
875
1006
|
return 'REDEEM';
|
|
@@ -879,10 +1010,18 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
879
1010
|
}
|
|
880
1011
|
const promises = await Promise.all(tokenizedVaults.map(async (v) => {
|
|
881
1012
|
try {
|
|
1013
|
+
// @solana: skip if not solana address
|
|
882
1014
|
if (utils_2.SolanaUtils.isSolanaAddress(v.address)) {
|
|
1015
|
+
// Default decimals: backend metadata, then SDK fallback. The mint's
|
|
1016
|
+
// own decimals (read below) override this once we have a real
|
|
1017
|
+
// tokenAmount response.
|
|
883
1018
|
const fallbackShareDecimals = v.solana_vault_metadata?.deposit_token_decimals ??
|
|
884
1019
|
SolanaConstants.fallbackDecimals;
|
|
885
1020
|
let balance = (0, core_1.toNormalizedBn)(0, fallbackShareDecimals);
|
|
1021
|
+
// Defensive: without a Solana adapter we can't fetch on-chain
|
|
1022
|
+
// state. The outer filter in modules/vaults/main.ts already gates
|
|
1023
|
+
// on `this.solanaService`, so this is a belt-and-suspenders guard
|
|
1024
|
+
// for any external caller that constructs `options` by hand.
|
|
886
1025
|
if (utils_2.SolanaUtils.isSolanaAddress(solanaWallet) &&
|
|
887
1026
|
options.solanaService) {
|
|
888
1027
|
const vaultProgramId = utils_2.SolanaUtils.resolveProgramId(String(v.address), v.solana_vault_metadata);
|
|
@@ -890,12 +1029,23 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
890
1029
|
const vaultState = vaultStateRes?.vaultState;
|
|
891
1030
|
const { shareMint } = vaultState ?? { shareMint: null };
|
|
892
1031
|
if (shareMint) {
|
|
1032
|
+
// Always use the raw helper — it returns the on-chain u64
|
|
1033
|
+
// string and the mint's true decimals. The legacy `uiAmount`
|
|
1034
|
+
// (JS number) path lost precision and silently encoded `.raw`
|
|
1035
|
+
// against 18 decimals when fed back through `toNormalizedBn`
|
|
1036
|
+
// without a decimals argument, which broke every downstream
|
|
1037
|
+
// BigInt consumer (redemptions, max actions, allowances).
|
|
893
1038
|
const raw = await options.solanaService.fetchUserShareBalanceRaw(solanaWallet, shareMint);
|
|
894
1039
|
const decimals = raw.decimals ?? fallbackShareDecimals;
|
|
895
1040
|
balance = (0, core_1.toNormalizedBn)(BigInt(raw.amount), decimals);
|
|
896
1041
|
}
|
|
897
1042
|
}
|
|
898
1043
|
return {
|
|
1044
|
+
// Mirror the Stellar branch — return the backend's `v.address`,
|
|
1045
|
+
// not the outer `vault` parameter (which can be undefined for
|
|
1046
|
+
// multi-vault calls). No `as IAddress` cast: Solana base58 is
|
|
1047
|
+
// not 0x-hex; the IAddress typing across non-EVM positions is a
|
|
1048
|
+
// known wart documented in IVaultPosition.
|
|
899
1049
|
vault: v.address,
|
|
900
1050
|
status: renderStatus([], balance),
|
|
901
1051
|
availableRedemptions: [],
|
|
@@ -913,6 +1063,9 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
913
1063
|
balance = (0, core_1.toNormalizedBn)(BigInt(position.shares), position.decimals);
|
|
914
1064
|
}
|
|
915
1065
|
else if (position) {
|
|
1066
|
+
// decimals() read failed: trusting the fallback (7) would
|
|
1067
|
+
// mis-scale the balance for ERC4626 offset vaults (AUGUST-6381).
|
|
1068
|
+
// Show zero rather than a confidently-wrong value.
|
|
916
1069
|
core_1.Logger.log.warn('getVaultPositions', 'Stellar decimals() unresolved — refusing to scale balance with fallback decimals (showing zero)', { vault: v.address, wallet: stellarWallet });
|
|
917
1070
|
}
|
|
918
1071
|
else {
|
|
@@ -922,6 +1075,7 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
922
1075
|
else if (stellarWallet) {
|
|
923
1076
|
core_1.Logger.log.warn('getVaultPositions', 'Invalid stellarWallet address format', { vault: v.address, stellarWallet });
|
|
924
1077
|
}
|
|
1078
|
+
// Stellar vaults have no on-chain redemption queue yet — redeemable is always 0
|
|
925
1079
|
return {
|
|
926
1080
|
vault: v.address,
|
|
927
1081
|
status: renderStatus([], balance),
|
|
@@ -931,14 +1085,18 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
931
1085
|
walletBalance: balance,
|
|
932
1086
|
};
|
|
933
1087
|
}
|
|
1088
|
+
// create provider
|
|
934
1089
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1090
|
+
// version split logic
|
|
935
1091
|
let decimals;
|
|
936
1092
|
const version = (0, core_1.getVaultVersionV2)(v);
|
|
1093
|
+
// Use ABI_LENDING_POOL_V2 as the common ABI for vault operations
|
|
937
1094
|
const vaultContract = (0, core_1.createContract)({
|
|
938
1095
|
provider,
|
|
939
1096
|
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
940
1097
|
address: v.address,
|
|
941
1098
|
});
|
|
1099
|
+
// get vault metadata and balance based on version
|
|
942
1100
|
let bal = BigInt(0);
|
|
943
1101
|
if (version === 'evm-2') {
|
|
944
1102
|
const receiptAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
|
|
@@ -959,12 +1117,15 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
959
1117
|
}
|
|
960
1118
|
}
|
|
961
1119
|
const balance = (0, core_1.toNormalizedBn)(bal, decimals);
|
|
1120
|
+
// check available redemptions
|
|
962
1121
|
const { availableRedemptions, pendingRedemptions } = await getVaultAvailableRedemptions({
|
|
963
1122
|
vault: v.address,
|
|
964
1123
|
wallet,
|
|
965
1124
|
options,
|
|
966
1125
|
});
|
|
1126
|
+
// get aggregate redemption amount
|
|
967
1127
|
const aggregateAvailableRedemptions = availableRedemptions.reduce((acc, curr) => acc + BigInt(curr.amount.raw), BigInt(0));
|
|
1128
|
+
// return obj
|
|
968
1129
|
return {
|
|
969
1130
|
vault,
|
|
970
1131
|
status: renderStatus(availableRedemptions, balance),
|
|
@@ -998,10 +1159,14 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
998
1159
|
throw new Error(`#getVaultPositions::${vault}:${e?.message}`);
|
|
999
1160
|
}
|
|
1000
1161
|
}
|
|
1162
|
+
/**
|
|
1163
|
+
* @deprecated use getVaultHistoricalTimeseries instead
|
|
1164
|
+
*/
|
|
1001
1165
|
async function getVaultApy({ vault, options, historical, }) {
|
|
1002
1166
|
try {
|
|
1003
1167
|
if (!vault)
|
|
1004
1168
|
throw new Error('Vault input parameter is undefined.');
|
|
1169
|
+
// only looking for current apy
|
|
1005
1170
|
if (!historical) {
|
|
1006
1171
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
1007
1172
|
return [
|
|
@@ -1012,6 +1177,7 @@ async function getVaultApy({ vault, options, historical, }) {
|
|
|
1012
1177
|
];
|
|
1013
1178
|
}
|
|
1014
1179
|
else {
|
|
1180
|
+
// historical apy
|
|
1015
1181
|
function paramBuilder(params, baseUrl) {
|
|
1016
1182
|
if (!params)
|
|
1017
1183
|
return '';
|
|
@@ -1025,7 +1191,7 @@ async function getVaultApy({ vault, options, historical, }) {
|
|
|
1025
1191
|
const query = urlParams.toString();
|
|
1026
1192
|
if (!query)
|
|
1027
1193
|
return '';
|
|
1028
|
-
return baseUrl
|
|
1194
|
+
return baseUrl?.includes('?') ? `&${query}` : `?${query}`;
|
|
1029
1195
|
}
|
|
1030
1196
|
const baseUrl = core_1.WEBSERVER_ENDPOINTS.public.tokenizedVault.historicalApy(vault);
|
|
1031
1197
|
const historicalApy = await (0, core_1.fetchAugustPublic)(`${baseUrl}${paramBuilder(historical, baseUrl)}`, {
|
|
@@ -1051,15 +1217,16 @@ async function getVaultApy({ vault, options, historical, }) {
|
|
|
1051
1217
|
}
|
|
1052
1218
|
}
|
|
1053
1219
|
async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
1054
|
-
const REWARDS_CHAIN = 43114;
|
|
1055
|
-
const REWARDS_SYMBOL = 'AVAX';
|
|
1056
|
-
const REWARDS_DECIMALS = 18;
|
|
1057
|
-
const REWARDS_NAME = 'Avalanche';
|
|
1220
|
+
const REWARDS_CHAIN = 43114; // avalanche
|
|
1221
|
+
const REWARDS_SYMBOL = 'AVAX'; // avalanche
|
|
1222
|
+
const REWARDS_DECIMALS = 18; // avalanche
|
|
1223
|
+
const REWARDS_NAME = 'Avalanche'; // avalanche
|
|
1058
1224
|
const APR_MULTIPLIER = 31536000;
|
|
1059
1225
|
const UP_AUSD_SYMBOL = 'upAUSD';
|
|
1060
1226
|
try {
|
|
1061
1227
|
const provider = (0, core_1.createProvider)(rpcUrl);
|
|
1062
1228
|
const chainId = await (0, core_1.getChainId)(provider);
|
|
1229
|
+
// Validate chain has rewards distributor
|
|
1063
1230
|
if (chainId !== REWARDS_CHAIN) {
|
|
1064
1231
|
core_1.Logger.log.warn('getStakingPositions:unsupported_chain', chainId);
|
|
1065
1232
|
return [];
|
|
@@ -1068,15 +1235,19 @@ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
|
1068
1235
|
core_1.Logger.log.warn('getStakingPositions:invalid_wallet', wallet);
|
|
1069
1236
|
return [];
|
|
1070
1237
|
}
|
|
1238
|
+
// get reward distributor contract addresses
|
|
1071
1239
|
const rewardDistributorAddresses = (0, core_1.REWARD_DISTRIBUTOR_ADDRESS)(chainId);
|
|
1240
|
+
// loop through each reward distributor contract address
|
|
1072
1241
|
const positions = await Promise.all(rewardDistributorAddresses.map(async (contract, i) => {
|
|
1073
1242
|
const rewardContract = (0, core_1.createContract)({
|
|
1074
1243
|
address: contract,
|
|
1075
1244
|
provider: provider,
|
|
1076
1245
|
abi: abis_1.ABI_REWARD_DISTRIBUTOR,
|
|
1077
1246
|
});
|
|
1247
|
+
// get reward distributor metadata
|
|
1078
1248
|
const totalStaked = await rewardContract.totalStaked();
|
|
1079
1249
|
const rewardsPerSecond = await rewardContract.rewardsPerSecond();
|
|
1250
|
+
// get staking token metadata
|
|
1080
1251
|
const stakingTokenCalls = ['decimals', 'symbol', 'name', 'totalSupply'];
|
|
1081
1252
|
const stakingTokenAddress = (await rewardContract.stakingToken());
|
|
1082
1253
|
const stakingTokenContract = (0, core_1.createContract)({
|
|
@@ -1085,6 +1256,7 @@ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
|
1085
1256
|
abi: abis_1.ABI_ERC20,
|
|
1086
1257
|
});
|
|
1087
1258
|
const [decimals, symbol, name] = await Promise.all(stakingTokenCalls.map((staking) => stakingTokenContract[staking]()));
|
|
1259
|
+
// if wallet address is passed, pull user balance of staked tokens
|
|
1088
1260
|
let balance;
|
|
1089
1261
|
let earned;
|
|
1090
1262
|
if (wallet) {
|
|
@@ -1092,12 +1264,15 @@ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
|
1092
1264
|
balance = await rewardContract.balanceOf(formattedWallet);
|
|
1093
1265
|
earned = await rewardContract.earned(formattedWallet);
|
|
1094
1266
|
}
|
|
1267
|
+
// get appropriate token price
|
|
1095
1268
|
const rewardTokenPriceInUsd = await (0, core_1.fetchTokenPrice)(REWARDS_SYMBOL, null, coinGeckoKey);
|
|
1096
1269
|
const normalizedRewardTokenPrice = (0, core_1.toNormalizedBn)(rewardTokenPriceInUsd);
|
|
1270
|
+
// normalize numbers
|
|
1097
1271
|
const normalizedTotalStakedInPool = (0, core_1.toNormalizedBn)(totalStaked, decimals);
|
|
1098
1272
|
const normalizedRedeemable = (0, core_1.toNormalizedBn)(earned, REWARDS_DECIMALS);
|
|
1099
1273
|
const normalizedRewardsPerSecond = (0, core_1.toNormalizedBn)(rewardsPerSecond, REWARDS_DECIMALS);
|
|
1100
1274
|
const normalizedTotalStakedBalance = (0, core_1.toNormalizedBn)(balance, decimals);
|
|
1275
|
+
// calculate APR's
|
|
1101
1276
|
const rewardTokenPriceToMultiply = symbol === UP_AUSD_SYMBOL
|
|
1102
1277
|
? Number(normalizedRewardTokenPrice?.normalized)
|
|
1103
1278
|
: 1;
|
|
@@ -1151,12 +1326,15 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1151
1326
|
try {
|
|
1152
1327
|
if (!vault)
|
|
1153
1328
|
throw new Error('Vault input parameter is undefined.');
|
|
1329
|
+
// Verify vault exists
|
|
1154
1330
|
const _vaultExists = (await (0, core_1.fetchTokenizedVault)(vault))?.[0];
|
|
1155
1331
|
if (!_vaultExists) {
|
|
1156
1332
|
throw new Error(`Vault ${vault} not found in backend`);
|
|
1157
1333
|
}
|
|
1334
|
+
// only wanting current APR
|
|
1158
1335
|
if (!historical) {
|
|
1159
1336
|
const version = (0, core_1.getVaultVersionV2)(_vaultExists);
|
|
1337
|
+
// Non-EVM early returns (no provider needed)
|
|
1160
1338
|
if (version === 'sol-0') {
|
|
1161
1339
|
return [
|
|
1162
1340
|
{
|
|
@@ -1180,15 +1358,15 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1180
1358
|
case 'evm-2': {
|
|
1181
1359
|
const receiptTokenAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
|
|
1182
1360
|
const receiptTokenDecimals = await (0, core_1.getDecimals)(provider, receiptTokenAddress, false);
|
|
1183
|
-
const
|
|
1184
|
-
address:
|
|
1185
|
-
abi:
|
|
1361
|
+
const vaultContract = (0, core_1.createContract)({
|
|
1362
|
+
address: vault,
|
|
1363
|
+
abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
|
|
1186
1364
|
provider,
|
|
1187
1365
|
});
|
|
1188
|
-
const
|
|
1366
|
+
const totalAssets = await vaultContract.getTotalAssets();
|
|
1189
1367
|
return [
|
|
1190
1368
|
{
|
|
1191
|
-
value: (0, core_1.toNormalizedBn)(
|
|
1369
|
+
value: (0, core_1.toNormalizedBn)(totalAssets, receiptTokenDecimals),
|
|
1192
1370
|
timestamp: new Date().toISOString(),
|
|
1193
1371
|
},
|
|
1194
1372
|
];
|
|
@@ -1200,26 +1378,42 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1200
1378
|
provider,
|
|
1201
1379
|
});
|
|
1202
1380
|
const decimals = await (0, core_1.getDecimals)(provider, vault);
|
|
1203
|
-
const
|
|
1381
|
+
const totalAssets = await vaultContract.totalAssets();
|
|
1204
1382
|
return [
|
|
1205
1383
|
{
|
|
1206
|
-
value: (0, core_1.toNormalizedBn)(
|
|
1384
|
+
value: (0, core_1.toNormalizedBn)(totalAssets, decimals),
|
|
1207
1385
|
timestamp: new Date().toISOString(),
|
|
1208
1386
|
},
|
|
1209
1387
|
];
|
|
1210
1388
|
}
|
|
1211
1389
|
}
|
|
1390
|
+
// const vaultRes = await getVault({
|
|
1391
|
+
// vault,
|
|
1392
|
+
// options,
|
|
1393
|
+
// loans: false,
|
|
1394
|
+
// allocations: false,
|
|
1395
|
+
// });
|
|
1396
|
+
// const returnObj = [
|
|
1397
|
+
// { value: vaultRes.totalSupply, timestamp: new Date().toISOString() },
|
|
1398
|
+
// ];
|
|
1399
|
+
// Logger.log.info('getVaultTvl:current', returnObj);
|
|
1400
|
+
// return returnObj;
|
|
1212
1401
|
}
|
|
1213
1402
|
else {
|
|
1403
|
+
// historical tvl
|
|
1404
|
+
// check vault version early to guard non-EVM chains
|
|
1214
1405
|
const version = (0, core_1.getVaultVersionV2)(_vaultExists);
|
|
1215
1406
|
if (version === 'stellar-0' || version === 'sol-0') {
|
|
1216
1407
|
core_1.Logger.log.warn('getVaultTvl:historical', 'Historical TVL not supported for non-EVM vaults', { vault, version });
|
|
1217
1408
|
return [];
|
|
1218
1409
|
}
|
|
1410
|
+
// if no order is passed but inputParams are passed
|
|
1219
1411
|
if (typeof historical !== 'undefined' && !historical.order)
|
|
1220
1412
|
historical.order = 'desc';
|
|
1413
|
+
// days is default interval
|
|
1221
1414
|
if (typeof historical !== 'undefined' && !historical.interval)
|
|
1222
1415
|
historical.interval = 'days';
|
|
1416
|
+
// setup
|
|
1223
1417
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1224
1418
|
const vaultContract = (0, core_1.createContract)({
|
|
1225
1419
|
address: vault,
|
|
@@ -1244,6 +1438,8 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1244
1438
|
break;
|
|
1245
1439
|
}
|
|
1246
1440
|
}
|
|
1441
|
+
// block trackback setup
|
|
1442
|
+
// Get the latest finalized block to ensure all queried blocks are confirmed
|
|
1247
1443
|
const finalizedBlock = await provider.getBlock('finalized');
|
|
1248
1444
|
const finalizedTimestamp = finalizedBlock
|
|
1249
1445
|
? finalizedBlock.timestamp * 1000
|
|
@@ -1253,19 +1449,26 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1253
1449
|
const startDate = historical.daysAgo
|
|
1254
1450
|
? new Date(finalizedTimestamp - daysAgo)
|
|
1255
1451
|
: new Date(finalizedTimestamp);
|
|
1256
|
-
const blocks = await dater.getEvery(historical.interval,
|
|
1452
|
+
const blocks = await dater.getEvery(historical.interval, // Period
|
|
1453
|
+
startDate.toUTCString(), // Start date
|
|
1454
|
+
new Date(finalizedTimestamp).toUTCString());
|
|
1455
|
+
// order blocks array
|
|
1257
1456
|
const orderedBlocks = historical.order === 'desc' ? (0, core_1.orderObjArrByDate)(blocks) : blocks;
|
|
1457
|
+
// minimal ABI
|
|
1258
1458
|
const totalAssetsHistorical = await (0, core_1.promiseSettle)(orderedBlocks?.map(async ({ block, date }) => {
|
|
1459
|
+
// get totalAssets at said block
|
|
1259
1460
|
const totalAssetsAtBlock = await provider.call({
|
|
1260
1461
|
to: vaultContract,
|
|
1261
1462
|
data: new ethers_1.Interface(minAbi).encodeFunctionData(functionName, []),
|
|
1262
1463
|
blockTag: block,
|
|
1263
1464
|
});
|
|
1465
|
+
// cover case when data returned is 0
|
|
1264
1466
|
if (totalAssetsAtBlock === '0x')
|
|
1265
1467
|
return {
|
|
1266
1468
|
timestamp: (0, core_1.dateToUnix)(new Date(date)),
|
|
1267
1469
|
value: (0, core_1.toNormalizedBn)(0, 0),
|
|
1268
1470
|
};
|
|
1471
|
+
// decode back to readable
|
|
1269
1472
|
const readableTotalAssetsAtBlock = new ethers_1.Interface(minAbi).decodeFunctionResult(functionName, totalAssetsAtBlock);
|
|
1270
1473
|
return {
|
|
1271
1474
|
timestamp: (0, core_1.dateToUnix)(new Date(date)),
|
|
@@ -1281,9 +1484,27 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1281
1484
|
throw new Error(`Failed to fetch TVL for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
1282
1485
|
}
|
|
1283
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Build the per-loan borrower / health-factor list for a single vault.
|
|
1489
|
+
*
|
|
1490
|
+
* Resilience: individual loan reads (`loanState()`, `borrower()`) and the
|
|
1491
|
+
* per-borrower August backend call are isolated with `Promise.allSettled` and
|
|
1492
|
+
* try/catch so one bad loan — a test loan whose address isn't a real contract,
|
|
1493
|
+
* a borrower the backend doesn't recognize, an intermittent RPC error — drops
|
|
1494
|
+
* that single row from the result instead of rejecting the whole batch. Failed
|
|
1495
|
+
* rows are logged via `Logger.log.warn`; the loan is still returned with
|
|
1496
|
+
* `health_factor: undefined` so callers can render an explicit empty state
|
|
1497
|
+
* rather than a perpetual loading skeleton.
|
|
1498
|
+
*
|
|
1499
|
+
* @param vault - Vault address to scope the fetch to.
|
|
1500
|
+
* @param options - Standard vault options — `rpcUrl` must point at `vault`'s chain.
|
|
1501
|
+
* @returns Array of `IVaultBorrowerHealthFactor`, one entry per active loan.
|
|
1502
|
+
*/
|
|
1284
1503
|
async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
1285
1504
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1286
1505
|
const loans = await getVaultLoans(vault, options);
|
|
1506
|
+
// Read each loan's on-chain state; drop loans whose RPC call fails so a
|
|
1507
|
+
// single bad contract (e.g. a test loan address) can't sink the batch.
|
|
1287
1508
|
const activeLoanResults = await Promise.allSettled(loans.map(async (l) => {
|
|
1288
1509
|
const loanContract = (0, core_1.createContract)({
|
|
1289
1510
|
provider,
|
|
@@ -1306,6 +1527,7 @@ async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
|
1306
1527
|
return undefined;
|
|
1307
1528
|
})
|
|
1308
1529
|
.filter((l) => l !== undefined);
|
|
1530
|
+
// Same fault-tolerance for the per-loan borrower() read.
|
|
1309
1531
|
const formattedSettled = await Promise.allSettled(activeLoans.map(async (l) => {
|
|
1310
1532
|
const loanContract = (0, core_1.createContract)({
|
|
1311
1533
|
provider,
|
|
@@ -1351,6 +1573,26 @@ async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
|
1351
1573
|
});
|
|
1352
1574
|
return formattedLoansArray;
|
|
1353
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Aggregate borrower-health-factor data, optionally scoped to a single vault.
|
|
1578
|
+
*
|
|
1579
|
+
* - When `vault` is provided, only that vault's tokenized record is fetched
|
|
1580
|
+
* and only its loans are read. This is the path callers should use when
|
|
1581
|
+
* they already know which vault they care about — it skips the cross-chain
|
|
1582
|
+
* fanout entirely.
|
|
1583
|
+
* - When `vault` is omitted, the function fetches every tokenized vault and
|
|
1584
|
+
* builds health factors for each, preserving the legacy behavior. Per-vault
|
|
1585
|
+
* failures are isolated with `Promise.allSettled` so one bad vault returns
|
|
1586
|
+
* an empty array instead of rejecting the whole map.
|
|
1587
|
+
*
|
|
1588
|
+
* The returned map is keyed by **lowercased** vault address so callers can
|
|
1589
|
+
* look up entries without worrying about EIP-55 checksum casing in either the
|
|
1590
|
+
* backend response or the caller's input.
|
|
1591
|
+
*
|
|
1592
|
+
* @param options - Standard vault options — `rpcUrl` should match `vault`'s chain.
|
|
1593
|
+
* @param vault - Optional vault address to scope the fetch to.
|
|
1594
|
+
* @returns Map of lowercased vault address → array of borrower-health-factor rows.
|
|
1595
|
+
*/
|
|
1354
1596
|
async function getHealthFactorOfBorrowersByVault({ options, vault, }) {
|
|
1355
1597
|
const vaults = vault
|
|
1356
1598
|
? ((await (0, core_1.fetchTokenizedVault)(vault, options?.headers)) ?? [])
|
|
@@ -1376,6 +1618,13 @@ async function getHealthFactorOfBorrowersByVault({ options, vault, }) {
|
|
|
1376
1618
|
});
|
|
1377
1619
|
return healthFactorsByPool;
|
|
1378
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Fetch user points from the backend API endpoint.
|
|
1623
|
+
* This replaces client-side points calculation with server-side processing.
|
|
1624
|
+
* @param userAddress - User wallet address (EVM or Solana)
|
|
1625
|
+
* @param options - Request options including headers
|
|
1626
|
+
* @returns Points data from the backend API
|
|
1627
|
+
*/
|
|
1379
1628
|
async function getUserPoints({ userAddress, options, }) {
|
|
1380
1629
|
try {
|
|
1381
1630
|
if (!userAddress) {
|
|
@@ -1397,6 +1646,62 @@ async function getUserPoints({ userAddress, options, }) {
|
|
|
1397
1646
|
throw new Error(`#getUserPoints::${userAddress}:${errorMessage}`);
|
|
1398
1647
|
}
|
|
1399
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Register a user for the points program, authenticated by a wallet signature.
|
|
1651
|
+
*
|
|
1652
|
+
* The caller must obtain a personal_sign (EIP-191) signature over the
|
|
1653
|
+
* canonical message template below — the backend reconstructs the same
|
|
1654
|
+
* string from these fields and verifies the signature recovers to
|
|
1655
|
+
* `userAddress`. Building the message client-side and server-side from the
|
|
1656
|
+
* same primitives means a hostile client cannot show one message in the
|
|
1657
|
+
* wallet prompt while submitting a different one for validation.
|
|
1658
|
+
*
|
|
1659
|
+
* Canonical message (`\n`-separated, no trailing newline):
|
|
1660
|
+
*
|
|
1661
|
+
* ```
|
|
1662
|
+
* Upshift: register {userAddress.toLowerCase()}
|
|
1663
|
+
* referrer: {referrerAddress.toLowerCase() || "none"}
|
|
1664
|
+
* chain: {chainId}
|
|
1665
|
+
* nonce: {nonce}
|
|
1666
|
+
* expires: {expiry}
|
|
1667
|
+
* ```
|
|
1668
|
+
*
|
|
1669
|
+
* @param userAddress - EVM wallet address being registered. Must match the
|
|
1670
|
+
* signer that produced `signature`. Lowercased before being embedded in
|
|
1671
|
+
* the canonical message.
|
|
1672
|
+
* @param referrerAddress - Optional EVM address that referred this user.
|
|
1673
|
+
* Lowercased when embedded; the literal string `"none"` is used when
|
|
1674
|
+
* absent. Tampering with this field after signing produces 401.
|
|
1675
|
+
* @param chainId - Chain on which the wallet signed. For smart-contract wallets
|
|
1676
|
+
* (Safe etc.) this is the chain whose RPC the backend will use to run the
|
|
1677
|
+
* EIP-1271 `isValidSignature` check; for EOAs it still pins the signature
|
|
1678
|
+
* to a chain so it can't be replayed cross-chain. Must be one of the chains
|
|
1679
|
+
* Upshift supports (see backend `SUPPORTED_REGISTRATION_CHAINS`).
|
|
1680
|
+
* @param signature - `0x`-prefixed hex of the personal_sign signature over
|
|
1681
|
+
* the canonical message. Single-use: replays within ~10 min are rejected.
|
|
1682
|
+
* @param nonce - Caller-generated random string (8–128 chars). Pair with
|
|
1683
|
+
* `userAddress` to form the single-use key in the backend's nonce store.
|
|
1684
|
+
* @param expiry - Unix timestamp (seconds). Must be in the future and within
|
|
1685
|
+
* the backend's TTL window (currently 10 min).
|
|
1686
|
+
* @param options - Request options (headers).
|
|
1687
|
+
* @returns Raw `Response` from the backend. 200 on success, 401 for
|
|
1688
|
+
* expired / replayed / mismatched-signer / tampered-referrer, 422 for an
|
|
1689
|
+
* unsupported `chainId`.
|
|
1690
|
+
*
|
|
1691
|
+
* @example
|
|
1692
|
+
* ```ts
|
|
1693
|
+
* const nonce = crypto.randomUUID().replace(/-/g, '');
|
|
1694
|
+
* const expiry = Math.floor(Date.now() / 1000) + 300;
|
|
1695
|
+
* const message =
|
|
1696
|
+
* `Upshift: register ${address.toLowerCase()}\n` +
|
|
1697
|
+
* `referrer: ${referrer?.toLowerCase() ?? 'none'}\n` +
|
|
1698
|
+
* `chain: ${chainId}\n` +
|
|
1699
|
+
* `nonce: ${nonce}\n` +
|
|
1700
|
+
* `expires: ${expiry}`;
|
|
1701
|
+
* const signature = await walletClient.signMessage({ account: address, message });
|
|
1702
|
+
* await registerUserForPoints({ userAddress: address, referrerAddress: referrer, chainId, signature, nonce, expiry });
|
|
1703
|
+
* ```
|
|
1704
|
+
*/
|
|
1400
1705
|
async function registerUserForPoints({ userAddress, referrerAddress, chainId, signature, nonce, expiry, options, }) {
|
|
1401
1706
|
try {
|
|
1402
1707
|
if (!userAddress) {
|
|
@@ -1429,6 +1734,12 @@ async function registerUserForPoints({ userAddress, referrerAddress, chainId, si
|
|
|
1429
1734
|
throw new Error(`#registerUserForPoints::${userAddress}:${errorMessage}`);
|
|
1430
1735
|
}
|
|
1431
1736
|
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Fetch the points leaderboard data.
|
|
1739
|
+
* @param params - Optional parameters for pagination and sorting
|
|
1740
|
+
* @param options - Request options including headers
|
|
1741
|
+
* @returns Leaderboard response data
|
|
1742
|
+
*/
|
|
1432
1743
|
async function fetchPointsLeaderboard({ params, options, }) {
|
|
1433
1744
|
try {
|
|
1434
1745
|
const { page = 1, perPage = 10, sortBy = 'totalPoints' } = params || {};
|
|
@@ -1451,6 +1762,13 @@ async function fetchPointsLeaderboard({ params, options, }) {
|
|
|
1451
1762
|
throw new Error(`#fetchPointsLeaderboard::${errorMessage}`);
|
|
1452
1763
|
}
|
|
1453
1764
|
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Fetch the timestamp when yield was last realized for a vault.
|
|
1767
|
+
* Returns the assetsUpdatedOn timestamp from the vault contract.
|
|
1768
|
+
* @param vault - Vault contract address
|
|
1769
|
+
* @param options - RPC configuration
|
|
1770
|
+
* @returns Timestamp (Unix timestamp in seconds) when yield was last realized
|
|
1771
|
+
*/
|
|
1454
1772
|
async function getYieldLastRealizedOn({ vault, options, }) {
|
|
1455
1773
|
try {
|
|
1456
1774
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
@@ -1484,6 +1802,8 @@ async function getYieldLastRealizedOn({ vault, options, }) {
|
|
|
1484
1802
|
return 0;
|
|
1485
1803
|
}
|
|
1486
1804
|
default: {
|
|
1805
|
+
// Old lending pools don't have assetsUpdatedOn function
|
|
1806
|
+
// Return 0 for legacy vaults
|
|
1487
1807
|
return 0;
|
|
1488
1808
|
}
|
|
1489
1809
|
}
|
|
@@ -1495,6 +1815,19 @@ async function getYieldLastRealizedOn({ vault, options, }) {
|
|
|
1495
1815
|
throw new Error(`#getYieldLastRealizedOn::${vault}:${errorMessage}`);
|
|
1496
1816
|
}
|
|
1497
1817
|
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Calculate lifetime PnL for a user in a specific vault.
|
|
1820
|
+
*
|
|
1821
|
+
* Basic logic:
|
|
1822
|
+
* 1. Get user's list of deposits and withdrawals from subgraph
|
|
1823
|
+
* 2. Fetch user's current assets (balanceOf * sharePrice via convertToAssets)
|
|
1824
|
+
* 3. Calculate PnL = assets withdrawn + current assets - assets deposited
|
|
1825
|
+
*
|
|
1826
|
+
* @param vault - Vault contract address
|
|
1827
|
+
* @param wallet - User wallet address
|
|
1828
|
+
* @param options - RPC configuration and service options
|
|
1829
|
+
* @returns Lifetime PnL data in both native token and USD
|
|
1830
|
+
*/
|
|
1498
1831
|
async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
1499
1832
|
try {
|
|
1500
1833
|
if (!vault)
|
|
@@ -1507,6 +1840,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1507
1840
|
if (!(0, ethers_1.isAddress)(wallet))
|
|
1508
1841
|
throw new Error(`Wallet input parameter is not an address: ${String(wallet)}`);
|
|
1509
1842
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1843
|
+
// Parallelize independent operations
|
|
1510
1844
|
const [decimals, userHistory, positions, tokenizedVaultResult] = await Promise.all([
|
|
1511
1845
|
(0, core_1.getDecimals)(provider, vault),
|
|
1512
1846
|
(0, vaults_1.getSubgraphUserHistory)(wallet, provider, vault),
|
|
@@ -1521,6 +1855,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1521
1855
|
if (!tokenizedVault) {
|
|
1522
1856
|
throw new Error(`Vault ${vault} not found`);
|
|
1523
1857
|
}
|
|
1858
|
+
// Fetch LayerZero deposits/redeems for supported vaults
|
|
1524
1859
|
const lzVaultKey = (0, deposits_1.isLayerZeroVault)(vault);
|
|
1525
1860
|
let lzDeposits = [];
|
|
1526
1861
|
let lzRedeems = [];
|
|
@@ -1534,6 +1869,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1534
1869
|
core_1.Logger.log.warn('getVaultUserLifetimePnl:lzDeposits', `Failed to fetch LZ deposits for ${lzVaultKey}: ${e}`);
|
|
1535
1870
|
}),
|
|
1536
1871
|
];
|
|
1872
|
+
// Only earnAUSD has redeem events currently
|
|
1537
1873
|
if (lzVaultKey === 'earnAUSD') {
|
|
1538
1874
|
lzPromises.push((0, redeems_1.queryLayerZeroRedeems)(wallet)
|
|
1539
1875
|
.then((r) => {
|
|
@@ -1547,10 +1883,12 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1547
1883
|
}
|
|
1548
1884
|
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
1549
1885
|
const currentPosition = positions.find((pos) => pos.vault?.toLowerCase() === vault.toLowerCase());
|
|
1886
|
+
// Start underlyingSymbol fetch (runs in parallel with processHistory)
|
|
1550
1887
|
const underlyingSymbolPromise = (async () => {
|
|
1551
1888
|
let underlyingTokenSymbol = 'UNKNOWN';
|
|
1552
1889
|
let underlyingAssetAddress;
|
|
1553
1890
|
try {
|
|
1891
|
+
// Use ABI_LENDING_POOL_V2 which has the asset() method
|
|
1554
1892
|
const vaultContract = (0, core_1.createContract)({
|
|
1555
1893
|
provider,
|
|
1556
1894
|
address: vault,
|
|
@@ -1578,15 +1916,22 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1578
1916
|
}
|
|
1579
1917
|
return { underlyingTokenSymbol, underlyingAssetAddress };
|
|
1580
1918
|
})();
|
|
1919
|
+
// Process history - count all withdrawal requests (both processed and pending)
|
|
1920
|
+
// We don't count processed withdrawals separately to avoid double counting
|
|
1921
|
+
// Group deposit amounts by asset address to handle different decimals
|
|
1581
1922
|
const depositsByAsset = new Map();
|
|
1923
|
+
// Separate withdrawal tracking: shares need conversion, assets don't
|
|
1582
1924
|
let totalWithdrawalsInShares = BigInt(0);
|
|
1583
1925
|
let totalWithdrawalsInAssets = BigInt(0);
|
|
1584
1926
|
for (const historyItem of userHistory) {
|
|
1585
1927
|
const amount = BigInt(historyItem.amount);
|
|
1928
|
+
// ignore bad transactions when calculating pnl
|
|
1586
1929
|
if ((0, core_1.isBadTransaction)(historyItem.transactionHash_, tokenizedVault.chain)) {
|
|
1587
1930
|
continue;
|
|
1588
1931
|
}
|
|
1589
1932
|
if (historyItem.type === 'deposit') {
|
|
1933
|
+
// Use assetIn if available (V2 vaults), otherwise use a default key
|
|
1934
|
+
// Use proper type guard instead of type assertion
|
|
1590
1935
|
const assetKey = 'assetIn' in historyItem && typeof historyItem.assetIn === 'string'
|
|
1591
1936
|
? historyItem.assetIn.toLowerCase()
|
|
1592
1937
|
: 'default';
|
|
@@ -1594,15 +1939,21 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1594
1939
|
depositsByAsset.set(assetKey, current + amount);
|
|
1595
1940
|
}
|
|
1596
1941
|
else if (historyItem.type === 'withdraw-request') {
|
|
1942
|
+
// Withdraw requests are in shares, need conversion to assets
|
|
1597
1943
|
totalWithdrawalsInShares += amount;
|
|
1598
1944
|
}
|
|
1599
1945
|
else if (historyItem.type === 'withdraw-processed' &&
|
|
1600
1946
|
historyItem.isInstant) {
|
|
1947
|
+
// Instant withdrawals are already in assets, no conversion needed
|
|
1601
1948
|
totalWithdrawalsInAssets += amount;
|
|
1602
1949
|
}
|
|
1603
1950
|
}
|
|
1951
|
+
// Include LayerZero cross-chain deposits and redeems
|
|
1604
1952
|
if (lzVaultKey && lzDeposits.length > 0) {
|
|
1605
1953
|
for (const lzDeposit of lzDeposits) {
|
|
1954
|
+
// LZ deposits assetAmt is the underlying asset amount deposited in hub-chain decimals.
|
|
1955
|
+
// This assumes the LZ subgraph indexes amounts in the same decimals as the hub vault
|
|
1956
|
+
// (e.g. 6 for USDC). If source/hub decimals diverge, normalization would be needed here.
|
|
1606
1957
|
const assetKey = 'lz-default';
|
|
1607
1958
|
const amount = BigInt(lzDeposit.assetAmt);
|
|
1608
1959
|
const current = depositsByAsset.get(assetKey) ?? BigInt(0);
|
|
@@ -1611,9 +1962,11 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1611
1962
|
}
|
|
1612
1963
|
if (lzVaultKey && lzRedeems.length > 0) {
|
|
1613
1964
|
for (const lzRedeem of lzRedeems) {
|
|
1965
|
+
// LZ redeems shareAmt is in shares, needs conversion via share price
|
|
1614
1966
|
totalWithdrawalsInShares += BigInt(lzRedeem.shareAmt);
|
|
1615
1967
|
}
|
|
1616
1968
|
}
|
|
1969
|
+
// Fetch decimals for each unique asset in parallel
|
|
1617
1970
|
const assetDecimals = new Map();
|
|
1618
1971
|
await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
|
|
1619
1972
|
if (assetAddress === 'default' || assetAddress === 'lz-default') {
|
|
@@ -1624,15 +1977,18 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1624
1977
|
assetDecimals.set(assetAddress, dec);
|
|
1625
1978
|
}
|
|
1626
1979
|
}));
|
|
1980
|
+
// Store deposits as a map: assetAddress -> INormalizedNumber (with correct decimals)
|
|
1627
1981
|
const totalDepositedRaw = new Map();
|
|
1628
1982
|
for (const [assetAddress, rawAmount] of depositsByAsset) {
|
|
1629
1983
|
const assetDec = assetDecimals.get(assetAddress) ?? decimals;
|
|
1630
1984
|
totalDepositedRaw.set(assetAddress, (0, core_1.toNormalizedBn)(rawAmount, assetDec));
|
|
1631
1985
|
}
|
|
1986
|
+
// Fetch USD prices for each unique deposit asset in parallel
|
|
1632
1987
|
const assetPrices = new Map();
|
|
1633
1988
|
if (tokenizedVault.chain) {
|
|
1634
1989
|
await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
|
|
1635
1990
|
if (assetAddress === 'default' || assetAddress === 'lz-default') {
|
|
1991
|
+
// Will use underlying asset price later
|
|
1636
1992
|
assetPrices.set(assetAddress, 0);
|
|
1637
1993
|
}
|
|
1638
1994
|
else {
|
|
@@ -1648,6 +2004,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1648
2004
|
}));
|
|
1649
2005
|
}
|
|
1650
2006
|
const { underlyingTokenSymbol, underlyingAssetAddress } = await underlyingSymbolPromise;
|
|
2007
|
+
// Calculate current position value and fetch price in parallel
|
|
1651
2008
|
let currentPositionValue = (0, core_1.toNormalizedBn)(0, decimals);
|
|
1652
2009
|
const pricePromise = underlyingAssetAddress && tokenizedVault.chain
|
|
1653
2010
|
? (0, core_1.fetchTokenPriceByAddress)(underlyingAssetAddress, tokenizedVault.chain, options?.headers).catch(() => 0)
|
|
@@ -1674,7 +2031,10 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1674
2031
|
else {
|
|
1675
2032
|
currentShares = await vaultContract.balanceOf(wallet);
|
|
1676
2033
|
}
|
|
2034
|
+
// For LayerZero vaults, include receipt token balances on remote chains
|
|
1677
2035
|
if (lzVaultKey) {
|
|
2036
|
+
// Build list of remote chains to check: prefer receipt_token_integrations from API,
|
|
2037
|
+
// fall back to LAYERZERO_VAULTS spoke chain config (OVault shareOFT addresses)
|
|
1678
2038
|
const remoteChains = [];
|
|
1679
2039
|
if (tokenizedVault.receipt_token_integrations?.length) {
|
|
1680
2040
|
for (const rti of tokenizedVault.receipt_token_integrations) {
|
|
@@ -1689,6 +2049,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1689
2049
|
}
|
|
1690
2050
|
}
|
|
1691
2051
|
else {
|
|
2052
|
+
// Fallback: use spoke chain addresses from LAYERZERO_VAULTS config
|
|
1692
2053
|
const lzVaultConfig = deposits_1.LAYERZERO_VAULTS[lzVaultKey];
|
|
1693
2054
|
if (lzVaultConfig.spokeChains) {
|
|
1694
2055
|
for (const [chainIdStr, address] of Object.entries(lzVaultConfig.spokeChains)) {
|
|
@@ -1721,6 +2082,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1721
2082
|
remoteTokenContract.decimals(),
|
|
1722
2083
|
]);
|
|
1723
2084
|
const remDec = Number(remoteDecimals) || decimals;
|
|
2085
|
+
// Normalize remote balance to hub chain decimals
|
|
1724
2086
|
if (remDec > decimals) {
|
|
1725
2087
|
return rawBalance / BigInt(10 ** (remDec - decimals));
|
|
1726
2088
|
}
|
|
@@ -1748,7 +2110,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1748
2110
|
vaultContract.totalSupply(),
|
|
1749
2111
|
]);
|
|
1750
2112
|
if (totalSupply === BigInt(0)) {
|
|
1751
|
-
sharePriceRaw = BigInt(10 ** decimals);
|
|
2113
|
+
sharePriceRaw = BigInt(10 ** decimals); // 1:1 if no supply
|
|
1752
2114
|
}
|
|
1753
2115
|
else {
|
|
1754
2116
|
sharePriceRaw = (totalAssets * BigInt(10 ** decimals)) / totalSupply;
|
|
@@ -1763,22 +2125,28 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1763
2125
|
currentPositionValue = currentPosition.walletBalance;
|
|
1764
2126
|
}
|
|
1765
2127
|
}
|
|
2128
|
+
// Calculate totalWithdrawn combining shares (converted) and assets (as-is)
|
|
1766
2129
|
let totalWithdrawnRaw = totalWithdrawalsInAssets;
|
|
1767
2130
|
if (version === 'evm-2' && sharePriceRaw > BigInt(0)) {
|
|
2131
|
+
// Only convert shares to assets, instant withdrawals are already in assets
|
|
1768
2132
|
totalWithdrawnRaw +=
|
|
1769
2133
|
(totalWithdrawalsInShares * sharePriceRaw) / BigInt(10 ** decimals);
|
|
1770
2134
|
}
|
|
1771
2135
|
else {
|
|
2136
|
+
// For non-evm-2 or when share price is 0, treat shares as assets
|
|
1772
2137
|
totalWithdrawnRaw += totalWithdrawalsInShares;
|
|
1773
2138
|
}
|
|
1774
2139
|
const totalWithdrawn = (0, core_1.toNormalizedBn)(totalWithdrawnRaw, decimals);
|
|
1775
2140
|
const tokenPriceUsd = await pricePromise;
|
|
2141
|
+
// Validate token price and log warning if invalid
|
|
1776
2142
|
const effectiveTokenPrice = tokenPriceUsd > 0 ? tokenPriceUsd : 1;
|
|
1777
2143
|
if (tokenPriceUsd <= 0) {
|
|
1778
2144
|
core_1.Logger.log.warn('getVaultUserLifetimePnl:tokenPrice', `Invalid underlying token price: ${tokenPriceUsd}, using fallback of 1`);
|
|
1779
2145
|
}
|
|
2146
|
+
// Sum all deposited amounts in USD using each asset's own price
|
|
1780
2147
|
let totalDepositedUsd = 0;
|
|
1781
2148
|
for (const [assetAddress, normalizedAmount] of totalDepositedRaw) {
|
|
2149
|
+
// Use the asset-specific price, or underlying price for 'default'
|
|
1782
2150
|
const assetPrice = assetAddress === 'default' || assetAddress === 'lz-default'
|
|
1783
2151
|
? effectiveTokenPrice
|
|
1784
2152
|
: assetPrices.get(assetAddress) || effectiveTokenPrice;
|
|
@@ -1786,14 +2154,18 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1786
2154
|
}
|
|
1787
2155
|
const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * effectiveTokenPrice;
|
|
1788
2156
|
const currentPositionValueUsd = Number(currentPositionValue.normalized) * effectiveTokenPrice;
|
|
2157
|
+
// Calculate lifetimePnl in USD first (since deposits may be in different tokens with different prices)
|
|
1789
2158
|
const lifetimePnlUsd = currentPositionValueUsd + totalWithdrawnUsd - totalDepositedUsd;
|
|
1790
|
-
|
|
2159
|
+
// Use BigInt-scaled arithmetic to minimize precision loss when converting USD to native
|
|
2160
|
+
const scaleFactor = BigInt(10 ** 18); // High precision scale
|
|
1791
2161
|
const tokenPriceScaled = BigInt(Math.round(effectiveTokenPrice * Number(scaleFactor)));
|
|
2162
|
+
// Convert totalDepositedUsd to native units for backwards compatibility
|
|
1792
2163
|
const totalDepositedUsdScaled = BigInt(Math.round(totalDepositedUsd * Number(scaleFactor)));
|
|
1793
2164
|
const totalDepositedNativeRaw = tokenPriceScaled > BigInt(0)
|
|
1794
2165
|
? (totalDepositedUsdScaled * BigInt(10 ** decimals)) / tokenPriceScaled
|
|
1795
2166
|
: BigInt(0);
|
|
1796
2167
|
const totalDeposited = (0, core_1.toNormalizedBn)(totalDepositedNativeRaw, decimals);
|
|
2168
|
+
// Convert lifetimePnlUsd to native units
|
|
1797
2169
|
const lifetimePnlUsdScaled = BigInt(Math.round(lifetimePnlUsd * Number(scaleFactor)));
|
|
1798
2170
|
const lifetimePnlRaw = tokenPriceScaled > BigInt(0)
|
|
1799
2171
|
? (lifetimePnlUsdScaled * BigInt(10 ** decimals)) / tokenPriceScaled
|
|
@@ -1821,6 +2193,14 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1821
2193
|
throw new Error(`#getVaultUserLifetimePnl::${vault}::${wallet}:${errorMessage}`);
|
|
1822
2194
|
}
|
|
1823
2195
|
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Calculate PnL for a vault (vault-level, not user-specific).
|
|
2198
|
+
* Returns the vault's overall profit and loss across all users.
|
|
2199
|
+
*
|
|
2200
|
+
* @param vault - Vault contract address
|
|
2201
|
+
* @param options - RPC configuration and service options
|
|
2202
|
+
* @returns Vault PnL in USD and notional value
|
|
2203
|
+
*/
|
|
1824
2204
|
async function getVaultPnl({ vault, options, }) {
|
|
1825
2205
|
try {
|
|
1826
2206
|
if (!vault)
|
|
@@ -1828,6 +2208,7 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1828
2208
|
(0, utils_3.assertNotStellar)(vault, 'Vault PnL');
|
|
1829
2209
|
if (!(0, ethers_1.isAddress)(vault))
|
|
1830
2210
|
throw new Error(`Vault input parameter is not an address: ${String(vault)}`);
|
|
2211
|
+
// Get vault data
|
|
1831
2212
|
const vaultData = await getVault({
|
|
1832
2213
|
vault,
|
|
1833
2214
|
loans: false,
|
|
@@ -1837,6 +2218,7 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1837
2218
|
if (!vaultData) {
|
|
1838
2219
|
throw new Error(`Vault ${vault} not found`);
|
|
1839
2220
|
}
|
|
2221
|
+
// Check if vault has daily_pnl_per_share and historical_snapshots
|
|
1840
2222
|
if (!vaultData.dailyPnlPerShare ||
|
|
1841
2223
|
!Array.isArray(vaultData.dailyPnlPerShare) ||
|
|
1842
2224
|
vaultData.dailyPnlPerShare.length === 0) {
|
|
@@ -1854,38 +2236,49 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1854
2236
|
};
|
|
1855
2237
|
}
|
|
1856
2238
|
const decimals = vaultData.decimals;
|
|
2239
|
+
// Filter out dates with zero PnL
|
|
1857
2240
|
const validDailyPnl = vaultData.dailyPnlPerShare.filter((dayData) => dayData.value && dayData.value !== 0);
|
|
2241
|
+
// Create a map of historical_snapshots by date (YYYY-MM-DD) for quick lookup
|
|
1858
2242
|
const snapshotsByDate = new Map();
|
|
1859
2243
|
vaultData.historical_snapshots.forEach((snapshot) => {
|
|
1860
2244
|
const snapshotDate = new Date(snapshot.snapshot_datetime);
|
|
1861
|
-
const dateKey = snapshotDate.toISOString().split('T')[0];
|
|
2245
|
+
const dateKey = snapshotDate.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
1862
2246
|
snapshotsByDate.set(dateKey, snapshot);
|
|
1863
2247
|
});
|
|
2248
|
+
// Calculate PnL for each day using historical_snapshots
|
|
1864
2249
|
let totalPnlRaw = BigInt(0);
|
|
1865
2250
|
validDailyPnl.forEach((dayData) => {
|
|
1866
2251
|
const dayTimestamp = dayData.timestamp;
|
|
1867
2252
|
const dailyPnlPerShare = dayData.value;
|
|
2253
|
+
// Get date key (YYYY-MM-DD) from timestamp
|
|
1868
2254
|
const dayDate = new Date(dayTimestamp);
|
|
1869
2255
|
const dateKey = dayDate.toISOString().split('T')[0];
|
|
2256
|
+
// Find matching snapshot for this date
|
|
1870
2257
|
const snapshot = snapshotsByDate.get(dateKey);
|
|
1871
|
-
if (!snapshot
|
|
2258
|
+
if (!snapshot?.total_shares) {
|
|
1872
2259
|
core_1.Logger.log.warn('getVaultPnl:snapshotNotFound', `No snapshot found for date ${dateKey}`);
|
|
1873
2260
|
return;
|
|
1874
2261
|
}
|
|
2262
|
+
// Convert normalized numbers to BigInt using toNormalizedBn's normalized string
|
|
1875
2263
|
const dailyPnlNorm = (0, core_1.toNormalizedBn)(dailyPnlPerShare, decimals);
|
|
1876
2264
|
const totalSharesNorm = (0, core_1.toNormalizedBn)(snapshot.total_shares, decimals);
|
|
2265
|
+
// Convert normalized strings to BigInt (split, pad fraction, combine) - all inline
|
|
1877
2266
|
const [dailyWhole = '0', dailyFrac = ''] = dailyPnlNorm.normalized.split('.');
|
|
1878
2267
|
const dailyPnlPerShareScaled = BigInt(dailyWhole + (dailyFrac || '').padEnd(decimals, '0').slice(0, decimals));
|
|
1879
2268
|
const [sharesWhole = '0', sharesFrac = ''] = totalSharesNorm.normalized.split('.');
|
|
1880
2269
|
const totalSharesScaled = BigInt(sharesWhole +
|
|
1881
2270
|
(sharesFrac || '').padEnd(decimals, '0').slice(0, decimals));
|
|
2271
|
+
// Calculate: daily_pnl_per_share[day] * total_shares[day] (both in raw units)
|
|
1882
2272
|
const dayPnlRaw = (dailyPnlPerShareScaled * totalSharesScaled) / BigInt(10 ** decimals);
|
|
1883
2273
|
totalPnlRaw += dayPnlRaw;
|
|
1884
2274
|
});
|
|
2275
|
+
// Convert totalPnlRaw (BigInt) to normalized number using toNormalizedBn
|
|
1885
2276
|
const totalPnl = (0, core_1.toNormalizedBn)(totalPnlRaw, decimals);
|
|
1886
2277
|
const totalPnlNormalized = Number(totalPnl.normalized);
|
|
1887
2278
|
const chainId = vaultData.chainId;
|
|
2279
|
+
// Get receipt token address (HLPe) from vaultData - no need for on-chain lookup
|
|
1888
2280
|
const lpTokenAddress = vaultData.receipt.address;
|
|
2281
|
+
// Fetch HLPe (lpTokenAddress) price in USD
|
|
1889
2282
|
let tokenPriceUsd = 0;
|
|
1890
2283
|
try {
|
|
1891
2284
|
tokenPriceUsd = await (0, core_1.fetchTokenPriceByAddress)(lpTokenAddress, chainId, options?.headers);
|
|
@@ -1893,6 +2286,7 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1893
2286
|
catch (error) {
|
|
1894
2287
|
core_1.Logger.log.warn('getVaultPnl:priceFetch', `Failed to fetch HLPe token price: ${error}`);
|
|
1895
2288
|
}
|
|
2289
|
+
// Convert to USD and format to 6 decimal places
|
|
1896
2290
|
const pnlUsd = Number((totalPnlNormalized * tokenPriceUsd).toFixed(6));
|
|
1897
2291
|
return {
|
|
1898
2292
|
totalPnl,
|
|
@@ -1905,6 +2299,14 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1905
2299
|
throw new Error(`#getVaultPnl::${vault}:${errorMessage}`);
|
|
1906
2300
|
}
|
|
1907
2301
|
}
|
|
2302
|
+
/*
|
|
2303
|
+
* Fetch historical timeseries data for a vault.
|
|
2304
|
+
* Returns TVL, APY, PnL, share price, and other metrics over time.
|
|
2305
|
+
* @param vault - Vault contract address
|
|
2306
|
+
* @param nDays - Number of days of historical data (default 30, min 1)
|
|
2307
|
+
* @param options - Request options including headers
|
|
2308
|
+
* @returns Historical timeseries data with date string keys
|
|
2309
|
+
*/
|
|
1908
2310
|
async function getVaultHistoricalTimeseries({ vault, nDays, options, }) {
|
|
1909
2311
|
try {
|
|
1910
2312
|
if (!vault)
|
|
@@ -1918,6 +2320,19 @@ async function getVaultHistoricalTimeseries({ vault, nDays, options, }) {
|
|
|
1918
2320
|
throw new Error(`#getVaultHistoricalTimeseries::${vault}:${errorMessage}`);
|
|
1919
2321
|
}
|
|
1920
2322
|
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Fetch annualized APY metrics for a vault.
|
|
2325
|
+
*
|
|
2326
|
+
* Supported Vaults: cUSDO, tETH, wstETH, rsETH
|
|
2327
|
+
*
|
|
2328
|
+
* @deprecated The `hgETH30dLiquidAPY` and `hgETH7dLiquidAPY` response fields are deprecated.
|
|
2329
|
+
* These fields will be removed on 2026-01-01.
|
|
2330
|
+
* Use `liquidAPY30Day` and `liquidAPY7Day` fields instead.
|
|
2331
|
+
*
|
|
2332
|
+
* @param vault - Vault contract address
|
|
2333
|
+
* @param options - Request options including headers
|
|
2334
|
+
* @returns Annualized APY data including liquidity APY
|
|
2335
|
+
*/
|
|
1921
2336
|
async function getVaultAnnualizedApy({ vault, options, }) {
|
|
1922
2337
|
try {
|
|
1923
2338
|
if (!vault) {
|
|
@@ -1942,6 +2357,12 @@ async function getVaultAnnualizedApy({ vault, options, }) {
|
|
|
1942
2357
|
throw new Error(`#getVaultAnnualizedApy::${vault}:${errorMessage}`);
|
|
1943
2358
|
}
|
|
1944
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Fetch summary data for a vault (name, type, chain, recent returns).
|
|
2362
|
+
* @param vault - Vault contract address
|
|
2363
|
+
* @param options - Request options including headers
|
|
2364
|
+
* @returns Vault summary data
|
|
2365
|
+
*/
|
|
1945
2366
|
async function getVaultSummary({ vault, options, }) {
|
|
1946
2367
|
try {
|
|
1947
2368
|
if (!vault) {
|
|
@@ -1967,6 +2388,13 @@ async function getVaultSummary({ vault, options, }) {
|
|
|
1967
2388
|
throw new Error(`#getVaultSummary::${vault}:${errorMessage}`);
|
|
1968
2389
|
}
|
|
1969
2390
|
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Fetch withdrawal summary and pending queue for a vault.
|
|
2393
|
+
* @param vault - Vault contract address
|
|
2394
|
+
* @param chain - Chain identifier (e.g., chain ID as string)
|
|
2395
|
+
* @param options - Request options including headers
|
|
2396
|
+
* @returns Withdrawal summary and pending queue
|
|
2397
|
+
*/
|
|
1970
2398
|
async function getVaultWithdrawals({ vault, chain, options, }) {
|
|
1971
2399
|
try {
|
|
1972
2400
|
if (!vault) {
|
|
@@ -1995,6 +2423,14 @@ async function getVaultWithdrawals({ vault, chain, options, }) {
|
|
|
1995
2423
|
throw new Error(`#getVaultWithdrawals::${vault}::${chain}:${errorMessage}`);
|
|
1996
2424
|
}
|
|
1997
2425
|
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Fetch pending redemptions for a vault with liquidity analysis.
|
|
2428
|
+
* @param vault - Vault contract address
|
|
2429
|
+
* @param pastDays - Number of past days to include (default 7, min 1, max 30)
|
|
2430
|
+
* @param futureDays - Number of future days to include (default 14, min 1, max 30)
|
|
2431
|
+
* @param options - Request options including headers
|
|
2432
|
+
* @returns Pending redemptions grouped by date with liquidity summary
|
|
2433
|
+
*/
|
|
1998
2434
|
async function getVaultPendingRedemptions({ vault, pastDays, futureDays, options, }) {
|
|
1999
2435
|
try {
|
|
2000
2436
|
if (!vault)
|
|
@@ -2062,6 +2498,13 @@ async function getVaultPendingRedemptions({ vault, pastDays, futureDays, options
|
|
|
2062
2498
|
throw new Error(`#getVaultPendingRedemptions::${vault}:${errorMessage}`);
|
|
2063
2499
|
}
|
|
2064
2500
|
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Preview the amount of assets that would be received for redeeming shares (queued redemption).
|
|
2503
|
+
* @param vault - Vault contract address
|
|
2504
|
+
* @param sharesAmount - Amount of shares to redeem (human-readable, raw bigint, or string)
|
|
2505
|
+
* @param options - RPC configuration
|
|
2506
|
+
* @returns The amount of assets as an INormalizedNumber with `normalized` and `raw` fields
|
|
2507
|
+
*/
|
|
2065
2508
|
async function getPreviewRedemption({ vault, sharesAmount, options, }) {
|
|
2066
2509
|
try {
|
|
2067
2510
|
if (!vault)
|
|
@@ -2109,6 +2552,13 @@ async function getPreviewRedemption({ vault, sharesAmount, options, }) {
|
|
|
2109
2552
|
throw new Error(`Failed to preview redemption for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2110
2553
|
}
|
|
2111
2554
|
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Helper: Decode a processing transaction to extract (year, month, day).
|
|
2557
|
+
*
|
|
2558
|
+
* Note: Multicall-wrapped transactions cannot be decoded from calldata or logs
|
|
2559
|
+
* (the WithdrawalProcessed event doesn't include the date). When decoding fails,
|
|
2560
|
+
* the caller falls back to timestamp-based date estimation.
|
|
2561
|
+
*/
|
|
2112
2562
|
async function decodeProcessingTransaction(txHash, provider, iface) {
|
|
2113
2563
|
const tx = await provider.getTransaction(txHash);
|
|
2114
2564
|
if (!tx) {
|
|
@@ -2122,14 +2572,74 @@ async function decodeProcessingTransaction(txHash, provider, iface) {
|
|
|
2122
2572
|
}
|
|
2123
2573
|
return (0, call_data_decoder_1.decodeWithdrawalProcessing)(tx.data, iface);
|
|
2124
2574
|
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Returns all withdrawal requests for a vault, enriched with computed claimable dates
|
|
2577
|
+
* and current processing status.
|
|
2578
|
+
*
|
|
2579
|
+
* The unique identifier for matching requests to processed events is:
|
|
2580
|
+
* (receiver, claimableDate.year, claimableDate.month, claimableDate.day)
|
|
2581
|
+
*
|
|
2582
|
+
* This is the primary method for integrators to reliably track withdrawal status
|
|
2583
|
+
* when multiple requests are submitted by the same receiver.
|
|
2584
|
+
*
|
|
2585
|
+
* ## Data sources
|
|
2586
|
+
* - **Withdrawal requests** come from the subgraph (all historical requests).
|
|
2587
|
+
* - **Processed events** come from on-chain `WithdrawalProcessed` logs, which are
|
|
2588
|
+
* limited to a finite lookback window determined by the chain's block parameters.
|
|
2589
|
+
*
|
|
2590
|
+
* ## Lookback window & filtering
|
|
2591
|
+
* On-chain logs only cover a limited number of recent blocks (e.g. ~20 days on Monad,
|
|
2592
|
+
* ~21 days on Ethereum). The `lookbackBlocks` parameter controls how far back to scan
|
|
2593
|
+
* and is applied uniformly to both data sources: it bounds the `WithdrawalProcessed`
|
|
2594
|
+
* log query *and* drops any withdrawal request whose submission timestamp falls outside
|
|
2595
|
+
* the window — regardless of whether a matching processed event was found. Callers who
|
|
2596
|
+
* need deeper history should widen `lookbackBlocks`.
|
|
2597
|
+
*
|
|
2598
|
+
* ## Status determination
|
|
2599
|
+
* - `'processed'` — A matching `WithdrawalProcessed` event was found (by receiver + claimable date).
|
|
2600
|
+
* - `'ready_to_claim'` — Claimable date has passed (within the lookback window) but no processed event found.
|
|
2601
|
+
* - `'pending'` — Claimable date is still in the future.
|
|
2602
|
+
*
|
|
2603
|
+
* @param params - Query parameters:
|
|
2604
|
+
* - `vault` — Vault address to query.
|
|
2605
|
+
* - `receiver` — (Optional) Filter by receiver address.
|
|
2606
|
+
* - `lookbackBlocks` — (Optional) On-chain log lookback window in blocks.
|
|
2607
|
+
* Defaults to a chain-specific value (e.g. 150,000 for Ethereum, 3,456,000 for Monad).
|
|
2608
|
+
* Increasing this scans more history but requires more RPC calls.
|
|
2609
|
+
* - `options` — Standard vault query options (provider, RPC URL, etc.).
|
|
2610
|
+
* @returns Array of withdrawal requests with status and claimable dates
|
|
2611
|
+
*
|
|
2612
|
+
* @example
|
|
2613
|
+
* ```typescript
|
|
2614
|
+
* const requests = await getWithdrawalRequestsWithStatus({
|
|
2615
|
+
* vault: '0xVault...',
|
|
2616
|
+
* receiver: '0xReceiver...'
|
|
2617
|
+
* });
|
|
2618
|
+
*
|
|
2619
|
+
* // Results show exact status + claimable date
|
|
2620
|
+
* requests.forEach(req => {
|
|
2621
|
+
* console.log(
|
|
2622
|
+
* `${req.claimableDate.year}-${req.claimableDate.month}-${req.claimableDate.day}: ${req.status}`
|
|
2623
|
+
* );
|
|
2624
|
+
* });
|
|
2625
|
+
*
|
|
2626
|
+
* // With custom lookback window (e.g. 30 days on Ethereum ≈ 216,000 blocks)
|
|
2627
|
+
* const moreResults = await getWithdrawalRequestsWithStatus({
|
|
2628
|
+
* vault: '0xVault...',
|
|
2629
|
+
* lookbackBlocks: 216_000,
|
|
2630
|
+
* });
|
|
2631
|
+
* ```
|
|
2632
|
+
*/
|
|
2125
2633
|
async function getWithdrawalRequestsWithStatus(params) {
|
|
2126
2634
|
try {
|
|
2127
2635
|
const { vault, receiver, lookbackBlocks, options } = params;
|
|
2636
|
+
// Input validation
|
|
2128
2637
|
if (!vault || !(0, ethers_1.isAddress)(vault)) {
|
|
2129
2638
|
throw new Error('Invalid vault address');
|
|
2130
2639
|
}
|
|
2131
2640
|
const vaultAddress = (0, ethers_1.getAddress)(vault);
|
|
2132
2641
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
2642
|
+
// Fetch vault metadata and lag duration
|
|
2133
2643
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers, false, false))?.[0];
|
|
2134
2644
|
if (!tokenizedVault) {
|
|
2135
2645
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Vault not found in backend: ${vaultAddress}`);
|
|
@@ -2143,7 +2653,7 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2143
2653
|
abi: vaultAbi,
|
|
2144
2654
|
provider,
|
|
2145
2655
|
});
|
|
2146
|
-
let lagDuration = 86400;
|
|
2656
|
+
let lagDuration = 86400; // Default: 1 day
|
|
2147
2657
|
try {
|
|
2148
2658
|
const lagValue = await vaultContract.lagDuration?.();
|
|
2149
2659
|
if (lagValue !== undefined && lagValue !== null) {
|
|
@@ -2153,6 +2663,9 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2153
2663
|
catch (e) {
|
|
2154
2664
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Failed to fetch lagDuration from vault ${vaultAddress}, using default 1 day — status determinations may be inaccurate: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2155
2665
|
}
|
|
2666
|
+
// Fetch subgraph requests, on-chain processed events, and the current block
|
|
2667
|
+
// in parallel. Current block is needed to derive the block-based lookback
|
|
2668
|
+
// cutoff applied uniformly to both data sources.
|
|
2156
2669
|
const [withdrawalRequests, processedEvents, currentBlock] = await Promise.all([
|
|
2157
2670
|
(0, vaults_1.getSubgraphWithdrawRequests)(vaultAddress, provider),
|
|
2158
2671
|
getVaultRedemptionHistory({
|
|
@@ -2170,6 +2683,10 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2170
2683
|
const effectiveLookbackBlocks = lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId);
|
|
2171
2684
|
const blockCutoff = currentBlock - effectiveLookbackBlocks;
|
|
2172
2685
|
const now = Math.floor(Date.now() / 1000);
|
|
2686
|
+
// Filter by receiver (if provided) and drop any request whose submission
|
|
2687
|
+
// block is outside the lookback window — uniformly, regardless of whether
|
|
2688
|
+
// a matching processed event exists. Using block_number (not timestamp)
|
|
2689
|
+
// keeps this consistent with the on-chain log scan's block-based cutoff.
|
|
2173
2690
|
const filteredRequests = withdrawalRequests.filter((req) => {
|
|
2174
2691
|
if (receiver &&
|
|
2175
2692
|
req.receiverAddr?.toLowerCase() !== receiver.toLowerCase()) {
|
|
@@ -2177,8 +2694,10 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2177
2694
|
}
|
|
2178
2695
|
return Number(req.block_number) >= blockCutoff;
|
|
2179
2696
|
});
|
|
2697
|
+
// Pre-decode all processed transactions in parallel for performance
|
|
2180
2698
|
const iface = new ethers_1.Interface(vaultAbi);
|
|
2181
2699
|
const decodedProcessed = await Promise.all((processedEvents || []).map(async (processed) => {
|
|
2700
|
+
// Try calldata decoding first (deterministic)
|
|
2182
2701
|
if (processed.transactionHash) {
|
|
2183
2702
|
try {
|
|
2184
2703
|
const decoded = await decodeProcessingTransaction(processed.transactionHash, provider, iface);
|
|
@@ -2197,8 +2716,12 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2197
2716
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Failed to decode processed transaction ${processed.transactionHash}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2198
2717
|
}
|
|
2199
2718
|
}
|
|
2719
|
+
// Fallback: use original request timestamp to compute claimable date,
|
|
2720
|
+
// or if unavailable (evm-2: requested=epoch 0), use the processed block
|
|
2721
|
+
// timestamp as the date directly (processing happens on the claimable date).
|
|
2200
2722
|
const requestedEpoch = Math.floor(processed.requested.getTime() / 1000);
|
|
2201
2723
|
if (requestedEpoch > 0) {
|
|
2724
|
+
// evm-1: we have the original request timestamp → compute claimable date
|
|
2202
2725
|
const fallbackDate = (0, date_utils_1.computeClaimableDate)(requestedEpoch, lagDuration);
|
|
2203
2726
|
return {
|
|
2204
2727
|
...processed,
|
|
@@ -2209,6 +2732,8 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2209
2732
|
},
|
|
2210
2733
|
};
|
|
2211
2734
|
}
|
|
2735
|
+
// evm-2: no request timestamp — use processed block date directly
|
|
2736
|
+
// (processing occurs on the claimable date, so the block date IS the date)
|
|
2212
2737
|
const processedDate = processed.processed;
|
|
2213
2738
|
return {
|
|
2214
2739
|
...processed,
|
|
@@ -2219,6 +2744,11 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2219
2744
|
},
|
|
2220
2745
|
};
|
|
2221
2746
|
}));
|
|
2747
|
+
// Build deterministic lookup map: (receiver + year + month + day) → processed events
|
|
2748
|
+
// A single WithdrawalProcessed event covers the entire day's batch for a
|
|
2749
|
+
// receiver — all requests whose claimable date falls on that day should
|
|
2750
|
+
// match it. We keep an array per key only to preserve every event we saw
|
|
2751
|
+
// for visibility; matching is a non-destructive peek at the first entry.
|
|
2222
2752
|
const processedMap = new Map();
|
|
2223
2753
|
for (const processed of decodedProcessed) {
|
|
2224
2754
|
if (!processed.decodedDate) {
|
|
@@ -2229,15 +2759,24 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2229
2759
|
const existing = processedMap.get(key) ?? [];
|
|
2230
2760
|
processedMap.set(key, [...existing, processed]);
|
|
2231
2761
|
}
|
|
2762
|
+
// Compute status for each withdrawal request. The lookback window was
|
|
2763
|
+
// already applied up-front when pruning filteredRequests, so every request
|
|
2764
|
+
// reaching this loop is within the window by construction.
|
|
2232
2765
|
const results = [];
|
|
2233
2766
|
for (const req of filteredRequests) {
|
|
2767
|
+
// timestamp_ is a Unix epoch string (seconds)
|
|
2234
2768
|
const requestTimestamp = Number(req.timestamp_);
|
|
2769
|
+
// Compute claimable date from timestamp + lagDuration (UTC)
|
|
2235
2770
|
const claimableDate = (0, date_utils_1.computeClaimableDate)(requestTimestamp, lagDuration);
|
|
2771
|
+
// Determine status: check processed first, then claimability
|
|
2236
2772
|
let status = 'pending';
|
|
2237
2773
|
let processedTxHash;
|
|
2238
2774
|
let assetsReceived;
|
|
2775
|
+
// Match deterministically by (receiver, year, month, day)
|
|
2239
2776
|
const dateKey = (0, date_utils_1.formatDateKey)(claimableDate.year, claimableDate.month, claimableDate.day);
|
|
2240
2777
|
const lookupKey = `${req.receiverAddr.toLowerCase()}:${dateKey}`;
|
|
2778
|
+
// Peek — do not consume. The processing event covers the whole day for
|
|
2779
|
+
// this receiver, so every request on that day matches the same event.
|
|
2241
2780
|
const processedEvent = processedMap.get(lookupKey)?.[0];
|
|
2242
2781
|
if (processedEvent) {
|
|
2243
2782
|
status = 'processed';
|
|
@@ -2250,6 +2789,7 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2250
2789
|
}
|
|
2251
2790
|
}
|
|
2252
2791
|
else if ((0, date_utils_1.isClaimableNow)(claimableDate.epoch, now)) {
|
|
2792
|
+
// Claimable date is recent (within lookback) and not yet processed
|
|
2253
2793
|
status = 'ready_to_claim';
|
|
2254
2794
|
}
|
|
2255
2795
|
results.push({
|
|
@@ -2260,6 +2800,7 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2260
2800
|
return BigInt(req.shares);
|
|
2261
2801
|
}
|
|
2262
2802
|
catch {
|
|
2803
|
+
// Handle decimal strings from subgraph (e.g. "1234.0")
|
|
2263
2804
|
const truncated = Math.trunc(Number(req.shares));
|
|
2264
2805
|
if (!Number.isFinite(truncated)) {
|
|
2265
2806
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Cannot parse shares value "${req.shares}" — defaulting to 0`);
|