@augustdigital/sdk 8.3.1 → 8.5.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/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 +262 -0
- package/lib/adapters/evm/index.js +268 -1
- package/lib/adapters/evm/utils.d.ts +6 -0
- package/lib/adapters/evm/utils.js +7 -0
- package/lib/adapters/solana/constants.js +4 -1
- package/lib/adapters/solana/getters.d.ts +8 -0
- 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 +55 -0
- package/lib/adapters/solana/index.js +57 -0
- package/lib/adapters/solana/utils.d.ts +28 -0
- package/lib/adapters/solana/utils.js +79 -4
- package/lib/adapters/solana/vault.actions.d.ts +19 -0
- package/lib/adapters/solana/vault.actions.js +47 -3
- package/lib/adapters/stellar/actions.d.ts +25 -0
- 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 +48 -0
- package/lib/adapters/stellar/index.js +48 -0
- package/lib/adapters/stellar/soroban.d.ts +20 -0
- package/lib/adapters/stellar/soroban.js +46 -0
- package/lib/adapters/stellar/submit.d.ts +12 -0
- package/lib/adapters/stellar/submit.js +19 -0
- 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 +6 -0
- package/lib/adapters/sui/getters.js +6 -0
- package/lib/adapters/sui/index.d.ts +15 -0
- package/lib/adapters/sui/index.js +15 -0
- package/lib/adapters/sui/transformer.d.ts +6 -0
- package/lib/adapters/sui/transformer.js +7 -0
- 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 +19 -0
- package/lib/core/analytics/sentry.d.ts +45 -0
- package/lib/core/analytics/sentry.js +115 -2
- package/lib/core/analytics/types.d.ts +27 -0
- package/lib/core/analytics/user-identity.d.ts +34 -0
- package/lib/core/analytics/user-identity.js +42 -0
- 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 +75 -0
- package/lib/core/base.class.js +56 -0
- package/lib/core/cache.d.ts +5 -0
- package/lib/core/cache.js +6 -0
- package/lib/core/constants/adapters.d.ts +15 -0
- package/lib/core/constants/adapters.js +28 -8
- package/lib/core/constants/core.d.ts +12 -1
- package/lib/core/constants/core.js +12 -0
- package/lib/core/constants/swap-router.d.ts +46 -0
- package/lib/core/constants/swap-router.js +50 -0
- package/lib/core/constants/vaults.d.ts +56 -0
- package/lib/core/constants/vaults.js +66 -1
- package/lib/core/constants/web3.d.ts +3 -0
- package/lib/core/constants/web3.js +18 -5
- package/lib/core/errors/index.d.ts +36 -0
- package/lib/core/errors/index.js +29 -0
- package/lib/core/fetcher.d.ts +134 -0
- package/lib/core/fetcher.js +191 -5
- package/lib/core/helpers/adapters.d.ts +9 -0
- 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 +42 -0
- package/lib/core/helpers/core.js +66 -1
- package/lib/core/helpers/explorer-link.d.ts +14 -0
- package/lib/core/helpers/explorer-link.js +14 -0
- package/lib/core/helpers/signer.d.ts +26 -0
- package/lib/core/helpers/signer.js +39 -0
- package/lib/core/helpers/swap-router.d.ts +32 -0
- package/lib/core/helpers/swap-router.js +32 -0
- package/lib/core/helpers/vault-version.d.ts +1 -0
- package/lib/core/helpers/vault-version.js +2 -0
- package/lib/core/helpers/vaults.d.ts +8 -0
- package/lib/core/helpers/vaults.js +22 -8
- package/lib/core/helpers/web3.d.ts +152 -0
- package/lib/core/helpers/web3.js +183 -6
- 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 +6 -0
- package/lib/core/vault-metadata.js +6 -0
- package/lib/core/version-check.d.ts +52 -0
- package/lib/core/version-check.js +81 -0
- package/lib/evm/methods/crossChainVault.d.ts +90 -0
- package/lib/evm/methods/crossChainVault.js +186 -1
- 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 +11 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +19 -0
- package/lib/main.d.ts +288 -5
- package/lib/main.js +305 -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 +15 -0
- package/lib/modules/sub-accounts/fetcher.js +15 -0
- package/lib/modules/sub-accounts/main.d.ts +33 -0
- package/lib/modules/sub-accounts/main.js +38 -0
- package/lib/modules/sub-accounts/utils.d.ts +3 -0
- package/lib/modules/sub-accounts/utils.js +3 -0
- package/lib/modules/vaults/adapter.helpers.d.ts +18 -0
- package/lib/modules/vaults/adapter.helpers.js +34 -0
- package/lib/modules/vaults/fetcher.d.ts +20 -0
- package/lib/modules/vaults/fetcher.js +40 -3
- package/lib/modules/vaults/getters.d.ts +295 -0
- package/lib/modules/vaults/getters.js +552 -12
- 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 +379 -7
- package/lib/modules/vaults/read.actions.d.ts +168 -0
- 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 +47 -0
- 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 +70 -0
- package/lib/modules/vaults/utils.js +108 -5
- package/lib/modules/vaults/write.actions.d.ts +363 -3
- package/lib/modules/vaults/write.actions.js +364 -2
- package/lib/polyfills.js +2 -0
- package/lib/sdk.d.ts +23705 -0
- package/lib/services/coingecko/fetcher.d.ts +13 -0
- package/lib/services/coingecko/fetcher.js +17 -0
- package/lib/services/debank/fetcher.d.ts +14 -0
- package/lib/services/debank/fetcher.js +12 -0
- package/lib/services/debank/utils.js +17 -0
- package/lib/services/layerzero/deposits.d.ts +11 -0
- package/lib/services/layerzero/deposits.js +34 -11
- package/lib/services/layerzero/redeems.d.ts +10 -0
- 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 +7 -0
- package/lib/services/octavfi/fetcher.js +25 -0
- package/lib/services/octavfi/utils.d.ts +12 -0
- 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 +43 -2
- package/lib/services/swap-quotes/index.d.ts +71 -0
- package/lib/services/swap-quotes/index.js +25 -0
- package/lib/services/swap-quotes/paraswap.d.ts +17 -0
- package/lib/services/swap-quotes/paraswap.js +14 -0
- package/lib/types/pools.d.ts +3 -0
- package/lib/types/typed-contract.d.ts +64 -0
- package/lib/types/vaults.d.ts +139 -2
- package/lib/types/vaults.js +10 -0
- package/lib/types/web3.d.ts +8 -0
- package/lib/types/web3.js +1 -0
- package/lib/types/webserver.d.ts +45 -0
- 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,9 +595,11 @@ 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;
|
|
@@ -523,11 +608,13 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, }) {
|
|
|
523
608
|
let day;
|
|
524
609
|
let year;
|
|
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,7 +924,9 @@ 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))];
|
|
@@ -817,6 +937,7 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
817
937
|
blockTimestamps.set(block.number, block.timestamp);
|
|
818
938
|
});
|
|
819
939
|
}
|
|
940
|
+
// format — iterate logs directly to preserve transactionHash
|
|
820
941
|
const redemptions = [];
|
|
821
942
|
logs.forEach((log) => {
|
|
822
943
|
const ev = iface.parseLog(log);
|
|
@@ -831,7 +952,7 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
831
952
|
receiver: receiverAddr,
|
|
832
953
|
amount: (0, core_1.toNormalizedBn)(assetsAmount, decimals),
|
|
833
954
|
processed: blockDate,
|
|
834
|
-
requested: new Date(0),
|
|
955
|
+
requested: new Date(0), // evm-2 event has no requestedOn field; epoch 0 is a sentinel for "unknown"
|
|
835
956
|
vault,
|
|
836
957
|
transactionHash: log.transactionHash,
|
|
837
958
|
};
|
|
@@ -860,16 +981,25 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
|
|
|
860
981
|
return redemptions;
|
|
861
982
|
}
|
|
862
983
|
catch (e) {
|
|
984
|
+
// Log loudly and re-throw. Previously this caught all errors and returned
|
|
985
|
+
// [], which silently corrupted callers (notably getWithdrawalRequestsWithStatus,
|
|
986
|
+
// which would then report `ready_to_claim` for already-processed requests).
|
|
987
|
+
// Returning [] is reserved for the explicit early returns inside the try
|
|
988
|
+
// (unsupported chains, vault not in backend) — not for unexpected failures.
|
|
863
989
|
core_1.Logger.log.error('getVaultRedemptionHistory', e, { vault, wallet });
|
|
864
990
|
throw e;
|
|
865
991
|
}
|
|
866
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Vault Positions
|
|
995
|
+
*/
|
|
867
996
|
async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, options, }) {
|
|
868
997
|
try {
|
|
869
998
|
const tokenizedVaults = await (0, core_1.fetchTokenizedVault)(vault ? vault : undefined, options?.headers, false, false);
|
|
870
999
|
if (!tokenizedVaults || tokenizedVaults.length === 0) {
|
|
871
1000
|
return [];
|
|
872
1001
|
}
|
|
1002
|
+
// check if there are redemptions available or if you stake in this pool
|
|
873
1003
|
function renderStatus(_redemptions, _balance) {
|
|
874
1004
|
if (_redemptions?.length)
|
|
875
1005
|
return 'REDEEM';
|
|
@@ -879,10 +1009,18 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
879
1009
|
}
|
|
880
1010
|
const promises = await Promise.all(tokenizedVaults.map(async (v) => {
|
|
881
1011
|
try {
|
|
1012
|
+
// @solana: skip if not solana address
|
|
882
1013
|
if (utils_2.SolanaUtils.isSolanaAddress(v.address)) {
|
|
1014
|
+
// Default decimals: backend metadata, then SDK fallback. The mint's
|
|
1015
|
+
// own decimals (read below) override this once we have a real
|
|
1016
|
+
// tokenAmount response.
|
|
883
1017
|
const fallbackShareDecimals = v.solana_vault_metadata?.deposit_token_decimals ??
|
|
884
1018
|
SolanaConstants.fallbackDecimals;
|
|
885
1019
|
let balance = (0, core_1.toNormalizedBn)(0, fallbackShareDecimals);
|
|
1020
|
+
// Defensive: without a Solana adapter we can't fetch on-chain
|
|
1021
|
+
// state. The outer filter in modules/vaults/main.ts already gates
|
|
1022
|
+
// on `this.solanaService`, so this is a belt-and-suspenders guard
|
|
1023
|
+
// for any external caller that constructs `options` by hand.
|
|
886
1024
|
if (utils_2.SolanaUtils.isSolanaAddress(solanaWallet) &&
|
|
887
1025
|
options.solanaService) {
|
|
888
1026
|
const vaultProgramId = utils_2.SolanaUtils.resolveProgramId(String(v.address), v.solana_vault_metadata);
|
|
@@ -890,12 +1028,23 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
890
1028
|
const vaultState = vaultStateRes?.vaultState;
|
|
891
1029
|
const { shareMint } = vaultState ?? { shareMint: null };
|
|
892
1030
|
if (shareMint) {
|
|
1031
|
+
// Always use the raw helper — it returns the on-chain u64
|
|
1032
|
+
// string and the mint's true decimals. The legacy `uiAmount`
|
|
1033
|
+
// (JS number) path lost precision and silently encoded `.raw`
|
|
1034
|
+
// against 18 decimals when fed back through `toNormalizedBn`
|
|
1035
|
+
// without a decimals argument, which broke every downstream
|
|
1036
|
+
// BigInt consumer (redemptions, max actions, allowances).
|
|
893
1037
|
const raw = await options.solanaService.fetchUserShareBalanceRaw(solanaWallet, shareMint);
|
|
894
1038
|
const decimals = raw.decimals ?? fallbackShareDecimals;
|
|
895
1039
|
balance = (0, core_1.toNormalizedBn)(BigInt(raw.amount), decimals);
|
|
896
1040
|
}
|
|
897
1041
|
}
|
|
898
1042
|
return {
|
|
1043
|
+
// Mirror the Stellar branch — return the backend's `v.address`,
|
|
1044
|
+
// not the outer `vault` parameter (which can be undefined for
|
|
1045
|
+
// multi-vault calls). No `as IAddress` cast: Solana base58 is
|
|
1046
|
+
// not 0x-hex; the IAddress typing across non-EVM positions is a
|
|
1047
|
+
// known wart documented in IVaultPosition.
|
|
899
1048
|
vault: v.address,
|
|
900
1049
|
status: renderStatus([], balance),
|
|
901
1050
|
availableRedemptions: [],
|
|
@@ -913,6 +1062,9 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
913
1062
|
balance = (0, core_1.toNormalizedBn)(BigInt(position.shares), position.decimals);
|
|
914
1063
|
}
|
|
915
1064
|
else if (position) {
|
|
1065
|
+
// decimals() read failed: trusting the fallback (7) would
|
|
1066
|
+
// mis-scale the balance for ERC4626 offset vaults (AUGUST-6381).
|
|
1067
|
+
// Show zero rather than a confidently-wrong value.
|
|
916
1068
|
core_1.Logger.log.warn('getVaultPositions', 'Stellar decimals() unresolved — refusing to scale balance with fallback decimals (showing zero)', { vault: v.address, wallet: stellarWallet });
|
|
917
1069
|
}
|
|
918
1070
|
else {
|
|
@@ -922,6 +1074,7 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
922
1074
|
else if (stellarWallet) {
|
|
923
1075
|
core_1.Logger.log.warn('getVaultPositions', 'Invalid stellarWallet address format', { vault: v.address, stellarWallet });
|
|
924
1076
|
}
|
|
1077
|
+
// Stellar vaults have no on-chain redemption queue yet — redeemable is always 0
|
|
925
1078
|
return {
|
|
926
1079
|
vault: v.address,
|
|
927
1080
|
status: renderStatus([], balance),
|
|
@@ -931,14 +1084,18 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
931
1084
|
walletBalance: balance,
|
|
932
1085
|
};
|
|
933
1086
|
}
|
|
1087
|
+
// create provider
|
|
934
1088
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1089
|
+
// version split logic
|
|
935
1090
|
let decimals;
|
|
936
1091
|
const version = (0, core_1.getVaultVersionV2)(v);
|
|
1092
|
+
// Use ABI_LENDING_POOL_V2 as the common ABI for vault operations
|
|
937
1093
|
const vaultContract = (0, core_1.createContract)({
|
|
938
1094
|
provider,
|
|
939
1095
|
abi: abis_1.ABI_LENDING_POOL_V2,
|
|
940
1096
|
address: v.address,
|
|
941
1097
|
});
|
|
1098
|
+
// get vault metadata and balance based on version
|
|
942
1099
|
let bal = BigInt(0);
|
|
943
1100
|
if (version === 'evm-2') {
|
|
944
1101
|
const receiptAddress = await (0, core_1.getReceiptTokenAddress)(provider, vault);
|
|
@@ -959,12 +1116,15 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
959
1116
|
}
|
|
960
1117
|
}
|
|
961
1118
|
const balance = (0, core_1.toNormalizedBn)(bal, decimals);
|
|
1119
|
+
// check available redemptions
|
|
962
1120
|
const { availableRedemptions, pendingRedemptions } = await getVaultAvailableRedemptions({
|
|
963
1121
|
vault: v.address,
|
|
964
1122
|
wallet,
|
|
965
1123
|
options,
|
|
966
1124
|
});
|
|
1125
|
+
// get aggregate redemption amount
|
|
967
1126
|
const aggregateAvailableRedemptions = availableRedemptions.reduce((acc, curr) => acc + BigInt(curr.amount.raw), BigInt(0));
|
|
1127
|
+
// return obj
|
|
968
1128
|
return {
|
|
969
1129
|
vault,
|
|
970
1130
|
status: renderStatus(availableRedemptions, balance),
|
|
@@ -998,10 +1158,14 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
|
|
|
998
1158
|
throw new Error(`#getVaultPositions::${vault}:${e?.message}`);
|
|
999
1159
|
}
|
|
1000
1160
|
}
|
|
1161
|
+
/**
|
|
1162
|
+
* @deprecated use getVaultHistoricalTimeseries instead
|
|
1163
|
+
*/
|
|
1001
1164
|
async function getVaultApy({ vault, options, historical, }) {
|
|
1002
1165
|
try {
|
|
1003
1166
|
if (!vault)
|
|
1004
1167
|
throw new Error('Vault input parameter is undefined.');
|
|
1168
|
+
// only looking for current apy
|
|
1005
1169
|
if (!historical) {
|
|
1006
1170
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vault, options?.headers))?.[0];
|
|
1007
1171
|
return [
|
|
@@ -1012,6 +1176,7 @@ async function getVaultApy({ vault, options, historical, }) {
|
|
|
1012
1176
|
];
|
|
1013
1177
|
}
|
|
1014
1178
|
else {
|
|
1179
|
+
// historical apy
|
|
1015
1180
|
function paramBuilder(params, baseUrl) {
|
|
1016
1181
|
if (!params)
|
|
1017
1182
|
return '';
|
|
@@ -1051,15 +1216,16 @@ async function getVaultApy({ vault, options, historical, }) {
|
|
|
1051
1216
|
}
|
|
1052
1217
|
}
|
|
1053
1218
|
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';
|
|
1219
|
+
const REWARDS_CHAIN = 43114; // avalanche
|
|
1220
|
+
const REWARDS_SYMBOL = 'AVAX'; // avalanche
|
|
1221
|
+
const REWARDS_DECIMALS = 18; // avalanche
|
|
1222
|
+
const REWARDS_NAME = 'Avalanche'; // avalanche
|
|
1058
1223
|
const APR_MULTIPLIER = 31536000;
|
|
1059
1224
|
const UP_AUSD_SYMBOL = 'upAUSD';
|
|
1060
1225
|
try {
|
|
1061
1226
|
const provider = (0, core_1.createProvider)(rpcUrl);
|
|
1062
1227
|
const chainId = await (0, core_1.getChainId)(provider);
|
|
1228
|
+
// Validate chain has rewards distributor
|
|
1063
1229
|
if (chainId !== REWARDS_CHAIN) {
|
|
1064
1230
|
core_1.Logger.log.warn('getStakingPositions:unsupported_chain', chainId);
|
|
1065
1231
|
return [];
|
|
@@ -1068,15 +1234,19 @@ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
|
1068
1234
|
core_1.Logger.log.warn('getStakingPositions:invalid_wallet', wallet);
|
|
1069
1235
|
return [];
|
|
1070
1236
|
}
|
|
1237
|
+
// get reward distributor contract addresses
|
|
1071
1238
|
const rewardDistributorAddresses = (0, core_1.REWARD_DISTRIBUTOR_ADDRESS)(chainId);
|
|
1239
|
+
// loop through each reward distributor contract address
|
|
1072
1240
|
const positions = await Promise.all(rewardDistributorAddresses.map(async (contract, i) => {
|
|
1073
1241
|
const rewardContract = (0, core_1.createContract)({
|
|
1074
1242
|
address: contract,
|
|
1075
1243
|
provider: provider,
|
|
1076
1244
|
abi: abis_1.ABI_REWARD_DISTRIBUTOR,
|
|
1077
1245
|
});
|
|
1246
|
+
// get reward distributor metadata
|
|
1078
1247
|
const totalStaked = await rewardContract.totalStaked();
|
|
1079
1248
|
const rewardsPerSecond = await rewardContract.rewardsPerSecond();
|
|
1249
|
+
// get staking token metadata
|
|
1080
1250
|
const stakingTokenCalls = ['decimals', 'symbol', 'name', 'totalSupply'];
|
|
1081
1251
|
const stakingTokenAddress = (await rewardContract.stakingToken());
|
|
1082
1252
|
const stakingTokenContract = (0, core_1.createContract)({
|
|
@@ -1085,6 +1255,7 @@ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
|
1085
1255
|
abi: abis_1.ABI_ERC20,
|
|
1086
1256
|
});
|
|
1087
1257
|
const [decimals, symbol, name] = await Promise.all(stakingTokenCalls.map((staking) => stakingTokenContract[staking]()));
|
|
1258
|
+
// if wallet address is passed, pull user balance of staked tokens
|
|
1088
1259
|
let balance;
|
|
1089
1260
|
let earned;
|
|
1090
1261
|
if (wallet) {
|
|
@@ -1092,12 +1263,15 @@ async function getRewardsStakingPositions({ rpcUrl, wallet, coinGeckoKey, }) {
|
|
|
1092
1263
|
balance = await rewardContract.balanceOf(formattedWallet);
|
|
1093
1264
|
earned = await rewardContract.earned(formattedWallet);
|
|
1094
1265
|
}
|
|
1266
|
+
// get appropriate token price
|
|
1095
1267
|
const rewardTokenPriceInUsd = await (0, core_1.fetchTokenPrice)(REWARDS_SYMBOL, null, coinGeckoKey);
|
|
1096
1268
|
const normalizedRewardTokenPrice = (0, core_1.toNormalizedBn)(rewardTokenPriceInUsd);
|
|
1269
|
+
// normalize numbers
|
|
1097
1270
|
const normalizedTotalStakedInPool = (0, core_1.toNormalizedBn)(totalStaked, decimals);
|
|
1098
1271
|
const normalizedRedeemable = (0, core_1.toNormalizedBn)(earned, REWARDS_DECIMALS);
|
|
1099
1272
|
const normalizedRewardsPerSecond = (0, core_1.toNormalizedBn)(rewardsPerSecond, REWARDS_DECIMALS);
|
|
1100
1273
|
const normalizedTotalStakedBalance = (0, core_1.toNormalizedBn)(balance, decimals);
|
|
1274
|
+
// calculate APR's
|
|
1101
1275
|
const rewardTokenPriceToMultiply = symbol === UP_AUSD_SYMBOL
|
|
1102
1276
|
? Number(normalizedRewardTokenPrice?.normalized)
|
|
1103
1277
|
: 1;
|
|
@@ -1151,12 +1325,15 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1151
1325
|
try {
|
|
1152
1326
|
if (!vault)
|
|
1153
1327
|
throw new Error('Vault input parameter is undefined.');
|
|
1328
|
+
// Verify vault exists
|
|
1154
1329
|
const _vaultExists = (await (0, core_1.fetchTokenizedVault)(vault))?.[0];
|
|
1155
1330
|
if (!_vaultExists) {
|
|
1156
1331
|
throw new Error(`Vault ${vault} not found in backend`);
|
|
1157
1332
|
}
|
|
1333
|
+
// only wanting current APR
|
|
1158
1334
|
if (!historical) {
|
|
1159
1335
|
const version = (0, core_1.getVaultVersionV2)(_vaultExists);
|
|
1336
|
+
// Non-EVM early returns (no provider needed)
|
|
1160
1337
|
if (version === 'sol-0') {
|
|
1161
1338
|
return [
|
|
1162
1339
|
{
|
|
@@ -1209,17 +1386,33 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1209
1386
|
];
|
|
1210
1387
|
}
|
|
1211
1388
|
}
|
|
1389
|
+
// const vaultRes = await getVault({
|
|
1390
|
+
// vault,
|
|
1391
|
+
// options,
|
|
1392
|
+
// loans: false,
|
|
1393
|
+
// allocations: false,
|
|
1394
|
+
// });
|
|
1395
|
+
// const returnObj = [
|
|
1396
|
+
// { value: vaultRes.totalSupply, timestamp: new Date().toISOString() },
|
|
1397
|
+
// ];
|
|
1398
|
+
// Logger.log.info('getVaultTvl:current', returnObj);
|
|
1399
|
+
// return returnObj;
|
|
1212
1400
|
}
|
|
1213
1401
|
else {
|
|
1402
|
+
// historical tvl
|
|
1403
|
+
// check vault version early to guard non-EVM chains
|
|
1214
1404
|
const version = (0, core_1.getVaultVersionV2)(_vaultExists);
|
|
1215
1405
|
if (version === 'stellar-0' || version === 'sol-0') {
|
|
1216
1406
|
core_1.Logger.log.warn('getVaultTvl:historical', 'Historical TVL not supported for non-EVM vaults', { vault, version });
|
|
1217
1407
|
return [];
|
|
1218
1408
|
}
|
|
1409
|
+
// if no order is passed but inputParams are passed
|
|
1219
1410
|
if (typeof historical !== 'undefined' && !historical.order)
|
|
1220
1411
|
historical.order = 'desc';
|
|
1412
|
+
// days is default interval
|
|
1221
1413
|
if (typeof historical !== 'undefined' && !historical.interval)
|
|
1222
1414
|
historical.interval = 'days';
|
|
1415
|
+
// setup
|
|
1223
1416
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1224
1417
|
const vaultContract = (0, core_1.createContract)({
|
|
1225
1418
|
address: vault,
|
|
@@ -1244,6 +1437,8 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1244
1437
|
break;
|
|
1245
1438
|
}
|
|
1246
1439
|
}
|
|
1440
|
+
// block trackback setup
|
|
1441
|
+
// Get the latest finalized block to ensure all queried blocks are confirmed
|
|
1247
1442
|
const finalizedBlock = await provider.getBlock('finalized');
|
|
1248
1443
|
const finalizedTimestamp = finalizedBlock
|
|
1249
1444
|
? finalizedBlock.timestamp * 1000
|
|
@@ -1253,19 +1448,26 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1253
1448
|
const startDate = historical.daysAgo
|
|
1254
1449
|
? new Date(finalizedTimestamp - daysAgo)
|
|
1255
1450
|
: new Date(finalizedTimestamp);
|
|
1256
|
-
const blocks = await dater.getEvery(historical.interval,
|
|
1451
|
+
const blocks = await dater.getEvery(historical.interval, // Period
|
|
1452
|
+
startDate.toUTCString(), // Start date
|
|
1453
|
+
new Date(finalizedTimestamp).toUTCString());
|
|
1454
|
+
// order blocks array
|
|
1257
1455
|
const orderedBlocks = historical.order === 'desc' ? (0, core_1.orderObjArrByDate)(blocks) : blocks;
|
|
1456
|
+
// minimal ABI
|
|
1258
1457
|
const totalAssetsHistorical = await (0, core_1.promiseSettle)(orderedBlocks?.map(async ({ block, date }) => {
|
|
1458
|
+
// get totalAssets at said block
|
|
1259
1459
|
const totalAssetsAtBlock = await provider.call({
|
|
1260
1460
|
to: vaultContract,
|
|
1261
1461
|
data: new ethers_1.Interface(minAbi).encodeFunctionData(functionName, []),
|
|
1262
1462
|
blockTag: block,
|
|
1263
1463
|
});
|
|
1464
|
+
// cover case when data returned is 0
|
|
1264
1465
|
if (totalAssetsAtBlock === '0x')
|
|
1265
1466
|
return {
|
|
1266
1467
|
timestamp: (0, core_1.dateToUnix)(new Date(date)),
|
|
1267
1468
|
value: (0, core_1.toNormalizedBn)(0, 0),
|
|
1268
1469
|
};
|
|
1470
|
+
// decode back to readable
|
|
1269
1471
|
const readableTotalAssetsAtBlock = new ethers_1.Interface(minAbi).decodeFunctionResult(functionName, totalAssetsAtBlock);
|
|
1270
1472
|
return {
|
|
1271
1473
|
timestamp: (0, core_1.dateToUnix)(new Date(date)),
|
|
@@ -1281,9 +1483,27 @@ async function getVaultTvl({ vault, options, historical, }) {
|
|
|
1281
1483
|
throw new Error(`Failed to fetch TVL for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
1282
1484
|
}
|
|
1283
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Build the per-loan borrower / health-factor list for a single vault.
|
|
1488
|
+
*
|
|
1489
|
+
* Resilience: individual loan reads (`loanState()`, `borrower()`) and the
|
|
1490
|
+
* per-borrower August backend call are isolated with `Promise.allSettled` and
|
|
1491
|
+
* try/catch so one bad loan — a test loan whose address isn't a real contract,
|
|
1492
|
+
* a borrower the backend doesn't recognize, an intermittent RPC error — drops
|
|
1493
|
+
* that single row from the result instead of rejecting the whole batch. Failed
|
|
1494
|
+
* rows are logged via `Logger.log.warn`; the loan is still returned with
|
|
1495
|
+
* `health_factor: undefined` so callers can render an explicit empty state
|
|
1496
|
+
* rather than a perpetual loading skeleton.
|
|
1497
|
+
*
|
|
1498
|
+
* @param vault Vault address to scope the fetch to.
|
|
1499
|
+
* @param options Standard vault options — `rpcUrl` must point at `vault`'s chain.
|
|
1500
|
+
* @returns Array of `IVaultBorrowerHealthFactor`, one entry per active loan.
|
|
1501
|
+
*/
|
|
1284
1502
|
async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
1285
1503
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1286
1504
|
const loans = await getVaultLoans(vault, options);
|
|
1505
|
+
// Read each loan's on-chain state; drop loans whose RPC call fails so a
|
|
1506
|
+
// single bad contract (e.g. a test loan address) can't sink the batch.
|
|
1287
1507
|
const activeLoanResults = await Promise.allSettled(loans.map(async (l) => {
|
|
1288
1508
|
const loanContract = (0, core_1.createContract)({
|
|
1289
1509
|
provider,
|
|
@@ -1306,6 +1526,7 @@ async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
|
1306
1526
|
return undefined;
|
|
1307
1527
|
})
|
|
1308
1528
|
.filter((l) => l !== undefined);
|
|
1529
|
+
// Same fault-tolerance for the per-loan borrower() read.
|
|
1309
1530
|
const formattedSettled = await Promise.allSettled(activeLoans.map(async (l) => {
|
|
1310
1531
|
const loanContract = (0, core_1.createContract)({
|
|
1311
1532
|
provider,
|
|
@@ -1351,6 +1572,26 @@ async function getVaultBorrowerHealthFactor({ vault, options, }) {
|
|
|
1351
1572
|
});
|
|
1352
1573
|
return formattedLoansArray;
|
|
1353
1574
|
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Aggregate borrower-health-factor data, optionally scoped to a single vault.
|
|
1577
|
+
*
|
|
1578
|
+
* - When `vault` is provided, only that vault's tokenized record is fetched
|
|
1579
|
+
* and only its loans are read. This is the path callers should use when
|
|
1580
|
+
* they already know which vault they care about — it skips the cross-chain
|
|
1581
|
+
* fanout entirely.
|
|
1582
|
+
* - When `vault` is omitted, the function fetches every tokenized vault and
|
|
1583
|
+
* builds health factors for each, preserving the legacy behavior. Per-vault
|
|
1584
|
+
* failures are isolated with `Promise.allSettled` so one bad vault returns
|
|
1585
|
+
* an empty array instead of rejecting the whole map.
|
|
1586
|
+
*
|
|
1587
|
+
* The returned map is keyed by **lowercased** vault address so callers can
|
|
1588
|
+
* look up entries without worrying about EIP-55 checksum casing in either the
|
|
1589
|
+
* backend response or the caller's input.
|
|
1590
|
+
*
|
|
1591
|
+
* @param options Standard vault options — `rpcUrl` should match `vault`'s chain.
|
|
1592
|
+
* @param vault Optional vault address to scope the fetch to.
|
|
1593
|
+
* @returns Map of lowercased vault address → array of borrower-health-factor rows.
|
|
1594
|
+
*/
|
|
1354
1595
|
async function getHealthFactorOfBorrowersByVault({ options, vault, }) {
|
|
1355
1596
|
const vaults = vault
|
|
1356
1597
|
? ((await (0, core_1.fetchTokenizedVault)(vault, options?.headers)) ?? [])
|
|
@@ -1376,6 +1617,13 @@ async function getHealthFactorOfBorrowersByVault({ options, vault, }) {
|
|
|
1376
1617
|
});
|
|
1377
1618
|
return healthFactorsByPool;
|
|
1378
1619
|
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Fetch user points from the backend API endpoint.
|
|
1622
|
+
* This replaces client-side points calculation with server-side processing.
|
|
1623
|
+
* @param userAddress User wallet address (EVM or Solana)
|
|
1624
|
+
* @param options Request options including headers
|
|
1625
|
+
* @returns Points data from the backend API
|
|
1626
|
+
*/
|
|
1379
1627
|
async function getUserPoints({ userAddress, options, }) {
|
|
1380
1628
|
try {
|
|
1381
1629
|
if (!userAddress) {
|
|
@@ -1397,6 +1645,62 @@ async function getUserPoints({ userAddress, options, }) {
|
|
|
1397
1645
|
throw new Error(`#getUserPoints::${userAddress}:${errorMessage}`);
|
|
1398
1646
|
}
|
|
1399
1647
|
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Register a user for the points program, authenticated by a wallet signature.
|
|
1650
|
+
*
|
|
1651
|
+
* The caller must obtain a personal_sign (EIP-191) signature over the
|
|
1652
|
+
* canonical message template below — the backend reconstructs the same
|
|
1653
|
+
* string from these fields and verifies the signature recovers to
|
|
1654
|
+
* `userAddress`. Building the message client-side and server-side from the
|
|
1655
|
+
* same primitives means a hostile client cannot show one message in the
|
|
1656
|
+
* wallet prompt while submitting a different one for validation.
|
|
1657
|
+
*
|
|
1658
|
+
* Canonical message (`\n`-separated, no trailing newline):
|
|
1659
|
+
*
|
|
1660
|
+
* ```
|
|
1661
|
+
* Upshift: register {userAddress.toLowerCase()}
|
|
1662
|
+
* referrer: {referrerAddress.toLowerCase() || "none"}
|
|
1663
|
+
* chain: {chainId}
|
|
1664
|
+
* nonce: {nonce}
|
|
1665
|
+
* expires: {expiry}
|
|
1666
|
+
* ```
|
|
1667
|
+
*
|
|
1668
|
+
* @param userAddress EVM wallet address being registered. Must match the
|
|
1669
|
+
* signer that produced `signature`. Lowercased before being embedded in
|
|
1670
|
+
* the canonical message.
|
|
1671
|
+
* @param referrerAddress Optional EVM address that referred this user.
|
|
1672
|
+
* Lowercased when embedded; the literal string `"none"` is used when
|
|
1673
|
+
* absent. Tampering with this field after signing produces 401.
|
|
1674
|
+
* @param chainId Chain on which the wallet signed. For smart-contract wallets
|
|
1675
|
+
* (Safe etc.) this is the chain whose RPC the backend will use to run the
|
|
1676
|
+
* EIP-1271 `isValidSignature` check; for EOAs it still pins the signature
|
|
1677
|
+
* to a chain so it can't be replayed cross-chain. Must be one of the chains
|
|
1678
|
+
* Upshift supports (see backend `SUPPORTED_REGISTRATION_CHAINS`).
|
|
1679
|
+
* @param signature `0x`-prefixed hex of the personal_sign signature over
|
|
1680
|
+
* the canonical message. Single-use: replays within ~10 min are rejected.
|
|
1681
|
+
* @param nonce Caller-generated random string (8–128 chars). Pair with
|
|
1682
|
+
* `userAddress` to form the single-use key in the backend's nonce store.
|
|
1683
|
+
* @param expiry Unix timestamp (seconds). Must be in the future and within
|
|
1684
|
+
* the backend's TTL window (currently 10 min).
|
|
1685
|
+
* @param options Request options (headers).
|
|
1686
|
+
* @returns Raw `Response` from the backend. 200 on success, 401 for
|
|
1687
|
+
* expired / replayed / mismatched-signer / tampered-referrer, 422 for an
|
|
1688
|
+
* unsupported `chainId`.
|
|
1689
|
+
*
|
|
1690
|
+
* @example
|
|
1691
|
+
* ```ts
|
|
1692
|
+
* const nonce = crypto.randomUUID().replace(/-/g, '');
|
|
1693
|
+
* const expiry = Math.floor(Date.now() / 1000) + 300;
|
|
1694
|
+
* const message =
|
|
1695
|
+
* `Upshift: register ${address.toLowerCase()}\n` +
|
|
1696
|
+
* `referrer: ${referrer?.toLowerCase() ?? 'none'}\n` +
|
|
1697
|
+
* `chain: ${chainId}\n` +
|
|
1698
|
+
* `nonce: ${nonce}\n` +
|
|
1699
|
+
* `expires: ${expiry}`;
|
|
1700
|
+
* const signature = await walletClient.signMessage({ account: address, message });
|
|
1701
|
+
* await registerUserForPoints({ userAddress: address, referrerAddress: referrer, chainId, signature, nonce, expiry });
|
|
1702
|
+
* ```
|
|
1703
|
+
*/
|
|
1400
1704
|
async function registerUserForPoints({ userAddress, referrerAddress, chainId, signature, nonce, expiry, options, }) {
|
|
1401
1705
|
try {
|
|
1402
1706
|
if (!userAddress) {
|
|
@@ -1429,6 +1733,12 @@ async function registerUserForPoints({ userAddress, referrerAddress, chainId, si
|
|
|
1429
1733
|
throw new Error(`#registerUserForPoints::${userAddress}:${errorMessage}`);
|
|
1430
1734
|
}
|
|
1431
1735
|
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Fetch the points leaderboard data.
|
|
1738
|
+
* @param params Optional parameters for pagination and sorting
|
|
1739
|
+
* @param options Request options including headers
|
|
1740
|
+
* @returns Leaderboard response data
|
|
1741
|
+
*/
|
|
1432
1742
|
async function fetchPointsLeaderboard({ params, options, }) {
|
|
1433
1743
|
try {
|
|
1434
1744
|
const { page = 1, perPage = 10, sortBy = 'totalPoints' } = params || {};
|
|
@@ -1451,6 +1761,13 @@ async function fetchPointsLeaderboard({ params, options, }) {
|
|
|
1451
1761
|
throw new Error(`#fetchPointsLeaderboard::${errorMessage}`);
|
|
1452
1762
|
}
|
|
1453
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Fetch the timestamp when yield was last realized for a vault.
|
|
1766
|
+
* Returns the assetsUpdatedOn timestamp from the vault contract.
|
|
1767
|
+
* @param vault Vault contract address
|
|
1768
|
+
* @param options RPC configuration
|
|
1769
|
+
* @returns Timestamp (Unix timestamp in seconds) when yield was last realized
|
|
1770
|
+
*/
|
|
1454
1771
|
async function getYieldLastRealizedOn({ vault, options, }) {
|
|
1455
1772
|
try {
|
|
1456
1773
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
@@ -1484,6 +1801,8 @@ async function getYieldLastRealizedOn({ vault, options, }) {
|
|
|
1484
1801
|
return 0;
|
|
1485
1802
|
}
|
|
1486
1803
|
default: {
|
|
1804
|
+
// Old lending pools don't have assetsUpdatedOn function
|
|
1805
|
+
// Return 0 for legacy vaults
|
|
1487
1806
|
return 0;
|
|
1488
1807
|
}
|
|
1489
1808
|
}
|
|
@@ -1495,6 +1814,19 @@ async function getYieldLastRealizedOn({ vault, options, }) {
|
|
|
1495
1814
|
throw new Error(`#getYieldLastRealizedOn::${vault}:${errorMessage}`);
|
|
1496
1815
|
}
|
|
1497
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Calculate lifetime PnL for a user in a specific vault.
|
|
1819
|
+
*
|
|
1820
|
+
* Basic logic:
|
|
1821
|
+
* 1. Get user's list of deposits and withdrawals from subgraph
|
|
1822
|
+
* 2. Fetch user's current assets (balanceOf * sharePrice via convertToAssets)
|
|
1823
|
+
* 3. Calculate PnL = assets withdrawn + current assets - assets deposited
|
|
1824
|
+
*
|
|
1825
|
+
* @param vault Vault contract address
|
|
1826
|
+
* @param wallet User wallet address
|
|
1827
|
+
* @param options RPC configuration and service options
|
|
1828
|
+
* @returns Lifetime PnL data in both native token and USD
|
|
1829
|
+
*/
|
|
1498
1830
|
async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
1499
1831
|
try {
|
|
1500
1832
|
if (!vault)
|
|
@@ -1507,6 +1839,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1507
1839
|
if (!(0, ethers_1.isAddress)(wallet))
|
|
1508
1840
|
throw new Error(`Wallet input parameter is not an address: ${String(wallet)}`);
|
|
1509
1841
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
1842
|
+
// Parallelize independent operations
|
|
1510
1843
|
const [decimals, userHistory, positions, tokenizedVaultResult] = await Promise.all([
|
|
1511
1844
|
(0, core_1.getDecimals)(provider, vault),
|
|
1512
1845
|
(0, vaults_1.getSubgraphUserHistory)(wallet, provider, vault),
|
|
@@ -1521,6 +1854,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1521
1854
|
if (!tokenizedVault) {
|
|
1522
1855
|
throw new Error(`Vault ${vault} not found`);
|
|
1523
1856
|
}
|
|
1857
|
+
// Fetch LayerZero deposits/redeems for supported vaults
|
|
1524
1858
|
const lzVaultKey = (0, deposits_1.isLayerZeroVault)(vault);
|
|
1525
1859
|
let lzDeposits = [];
|
|
1526
1860
|
let lzRedeems = [];
|
|
@@ -1534,6 +1868,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1534
1868
|
core_1.Logger.log.warn('getVaultUserLifetimePnl:lzDeposits', `Failed to fetch LZ deposits for ${lzVaultKey}: ${e}`);
|
|
1535
1869
|
}),
|
|
1536
1870
|
];
|
|
1871
|
+
// Only earnAUSD has redeem events currently
|
|
1537
1872
|
if (lzVaultKey === 'earnAUSD') {
|
|
1538
1873
|
lzPromises.push((0, redeems_1.queryLayerZeroRedeems)(wallet)
|
|
1539
1874
|
.then((r) => {
|
|
@@ -1547,10 +1882,12 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1547
1882
|
}
|
|
1548
1883
|
const version = (0, core_1.getVaultVersionV2)(tokenizedVault);
|
|
1549
1884
|
const currentPosition = positions.find((pos) => pos.vault?.toLowerCase() === vault.toLowerCase());
|
|
1885
|
+
// Start underlyingSymbol fetch (runs in parallel with processHistory)
|
|
1550
1886
|
const underlyingSymbolPromise = (async () => {
|
|
1551
1887
|
let underlyingTokenSymbol = 'UNKNOWN';
|
|
1552
1888
|
let underlyingAssetAddress;
|
|
1553
1889
|
try {
|
|
1890
|
+
// Use ABI_LENDING_POOL_V2 which has the asset() method
|
|
1554
1891
|
const vaultContract = (0, core_1.createContract)({
|
|
1555
1892
|
provider,
|
|
1556
1893
|
address: vault,
|
|
@@ -1578,15 +1915,22 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1578
1915
|
}
|
|
1579
1916
|
return { underlyingTokenSymbol, underlyingAssetAddress };
|
|
1580
1917
|
})();
|
|
1918
|
+
// Process history - count all withdrawal requests (both processed and pending)
|
|
1919
|
+
// We don't count processed withdrawals separately to avoid double counting
|
|
1920
|
+
// Group deposit amounts by asset address to handle different decimals
|
|
1581
1921
|
const depositsByAsset = new Map();
|
|
1922
|
+
// Separate withdrawal tracking: shares need conversion, assets don't
|
|
1582
1923
|
let totalWithdrawalsInShares = BigInt(0);
|
|
1583
1924
|
let totalWithdrawalsInAssets = BigInt(0);
|
|
1584
1925
|
for (const historyItem of userHistory) {
|
|
1585
1926
|
const amount = BigInt(historyItem.amount);
|
|
1927
|
+
// ignore bad transactions when calculating pnl
|
|
1586
1928
|
if ((0, core_1.isBadTransaction)(historyItem.transactionHash_, tokenizedVault.chain)) {
|
|
1587
1929
|
continue;
|
|
1588
1930
|
}
|
|
1589
1931
|
if (historyItem.type === 'deposit') {
|
|
1932
|
+
// Use assetIn if available (V2 vaults), otherwise use a default key
|
|
1933
|
+
// Use proper type guard instead of type assertion
|
|
1590
1934
|
const assetKey = 'assetIn' in historyItem && typeof historyItem.assetIn === 'string'
|
|
1591
1935
|
? historyItem.assetIn.toLowerCase()
|
|
1592
1936
|
: 'default';
|
|
@@ -1594,15 +1938,21 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1594
1938
|
depositsByAsset.set(assetKey, current + amount);
|
|
1595
1939
|
}
|
|
1596
1940
|
else if (historyItem.type === 'withdraw-request') {
|
|
1941
|
+
// Withdraw requests are in shares, need conversion to assets
|
|
1597
1942
|
totalWithdrawalsInShares += amount;
|
|
1598
1943
|
}
|
|
1599
1944
|
else if (historyItem.type === 'withdraw-processed' &&
|
|
1600
1945
|
historyItem.isInstant) {
|
|
1946
|
+
// Instant withdrawals are already in assets, no conversion needed
|
|
1601
1947
|
totalWithdrawalsInAssets += amount;
|
|
1602
1948
|
}
|
|
1603
1949
|
}
|
|
1950
|
+
// Include LayerZero cross-chain deposits and redeems
|
|
1604
1951
|
if (lzVaultKey && lzDeposits.length > 0) {
|
|
1605
1952
|
for (const lzDeposit of lzDeposits) {
|
|
1953
|
+
// LZ deposits assetAmt is the underlying asset amount deposited in hub-chain decimals.
|
|
1954
|
+
// This assumes the LZ subgraph indexes amounts in the same decimals as the hub vault
|
|
1955
|
+
// (e.g. 6 for USDC). If source/hub decimals diverge, normalization would be needed here.
|
|
1606
1956
|
const assetKey = 'lz-default';
|
|
1607
1957
|
const amount = BigInt(lzDeposit.assetAmt);
|
|
1608
1958
|
const current = depositsByAsset.get(assetKey) ?? BigInt(0);
|
|
@@ -1611,9 +1961,11 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1611
1961
|
}
|
|
1612
1962
|
if (lzVaultKey && lzRedeems.length > 0) {
|
|
1613
1963
|
for (const lzRedeem of lzRedeems) {
|
|
1964
|
+
// LZ redeems shareAmt is in shares, needs conversion via share price
|
|
1614
1965
|
totalWithdrawalsInShares += BigInt(lzRedeem.shareAmt);
|
|
1615
1966
|
}
|
|
1616
1967
|
}
|
|
1968
|
+
// Fetch decimals for each unique asset in parallel
|
|
1617
1969
|
const assetDecimals = new Map();
|
|
1618
1970
|
await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
|
|
1619
1971
|
if (assetAddress === 'default' || assetAddress === 'lz-default') {
|
|
@@ -1624,15 +1976,18 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1624
1976
|
assetDecimals.set(assetAddress, dec);
|
|
1625
1977
|
}
|
|
1626
1978
|
}));
|
|
1979
|
+
// Store deposits as a map: assetAddress -> INormalizedNumber (with correct decimals)
|
|
1627
1980
|
const totalDepositedRaw = new Map();
|
|
1628
1981
|
for (const [assetAddress, rawAmount] of depositsByAsset) {
|
|
1629
1982
|
const assetDec = assetDecimals.get(assetAddress) ?? decimals;
|
|
1630
1983
|
totalDepositedRaw.set(assetAddress, (0, core_1.toNormalizedBn)(rawAmount, assetDec));
|
|
1631
1984
|
}
|
|
1985
|
+
// Fetch USD prices for each unique deposit asset in parallel
|
|
1632
1986
|
const assetPrices = new Map();
|
|
1633
1987
|
if (tokenizedVault.chain) {
|
|
1634
1988
|
await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
|
|
1635
1989
|
if (assetAddress === 'default' || assetAddress === 'lz-default') {
|
|
1990
|
+
// Will use underlying asset price later
|
|
1636
1991
|
assetPrices.set(assetAddress, 0);
|
|
1637
1992
|
}
|
|
1638
1993
|
else {
|
|
@@ -1648,6 +2003,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1648
2003
|
}));
|
|
1649
2004
|
}
|
|
1650
2005
|
const { underlyingTokenSymbol, underlyingAssetAddress } = await underlyingSymbolPromise;
|
|
2006
|
+
// Calculate current position value and fetch price in parallel
|
|
1651
2007
|
let currentPositionValue = (0, core_1.toNormalizedBn)(0, decimals);
|
|
1652
2008
|
const pricePromise = underlyingAssetAddress && tokenizedVault.chain
|
|
1653
2009
|
? (0, core_1.fetchTokenPriceByAddress)(underlyingAssetAddress, tokenizedVault.chain, options?.headers).catch(() => 0)
|
|
@@ -1674,7 +2030,10 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1674
2030
|
else {
|
|
1675
2031
|
currentShares = await vaultContract.balanceOf(wallet);
|
|
1676
2032
|
}
|
|
2033
|
+
// For LayerZero vaults, include receipt token balances on remote chains
|
|
1677
2034
|
if (lzVaultKey) {
|
|
2035
|
+
// Build list of remote chains to check: prefer receipt_token_integrations from API,
|
|
2036
|
+
// fall back to LAYERZERO_VAULTS spoke chain config (OVault shareOFT addresses)
|
|
1678
2037
|
const remoteChains = [];
|
|
1679
2038
|
if (tokenizedVault.receipt_token_integrations?.length) {
|
|
1680
2039
|
for (const rti of tokenizedVault.receipt_token_integrations) {
|
|
@@ -1689,6 +2048,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1689
2048
|
}
|
|
1690
2049
|
}
|
|
1691
2050
|
else {
|
|
2051
|
+
// Fallback: use spoke chain addresses from LAYERZERO_VAULTS config
|
|
1692
2052
|
const lzVaultConfig = deposits_1.LAYERZERO_VAULTS[lzVaultKey];
|
|
1693
2053
|
if (lzVaultConfig.spokeChains) {
|
|
1694
2054
|
for (const [chainIdStr, address] of Object.entries(lzVaultConfig.spokeChains)) {
|
|
@@ -1721,6 +2081,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1721
2081
|
remoteTokenContract.decimals(),
|
|
1722
2082
|
]);
|
|
1723
2083
|
const remDec = Number(remoteDecimals) || decimals;
|
|
2084
|
+
// Normalize remote balance to hub chain decimals
|
|
1724
2085
|
if (remDec > decimals) {
|
|
1725
2086
|
return rawBalance / BigInt(10 ** (remDec - decimals));
|
|
1726
2087
|
}
|
|
@@ -1748,7 +2109,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1748
2109
|
vaultContract.totalSupply(),
|
|
1749
2110
|
]);
|
|
1750
2111
|
if (totalSupply === BigInt(0)) {
|
|
1751
|
-
sharePriceRaw = BigInt(10 ** decimals);
|
|
2112
|
+
sharePriceRaw = BigInt(10 ** decimals); // 1:1 if no supply
|
|
1752
2113
|
}
|
|
1753
2114
|
else {
|
|
1754
2115
|
sharePriceRaw = (totalAssets * BigInt(10 ** decimals)) / totalSupply;
|
|
@@ -1763,22 +2124,28 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1763
2124
|
currentPositionValue = currentPosition.walletBalance;
|
|
1764
2125
|
}
|
|
1765
2126
|
}
|
|
2127
|
+
// Calculate totalWithdrawn combining shares (converted) and assets (as-is)
|
|
1766
2128
|
let totalWithdrawnRaw = totalWithdrawalsInAssets;
|
|
1767
2129
|
if (version === 'evm-2' && sharePriceRaw > BigInt(0)) {
|
|
2130
|
+
// Only convert shares to assets, instant withdrawals are already in assets
|
|
1768
2131
|
totalWithdrawnRaw +=
|
|
1769
2132
|
(totalWithdrawalsInShares * sharePriceRaw) / BigInt(10 ** decimals);
|
|
1770
2133
|
}
|
|
1771
2134
|
else {
|
|
2135
|
+
// For non-evm-2 or when share price is 0, treat shares as assets
|
|
1772
2136
|
totalWithdrawnRaw += totalWithdrawalsInShares;
|
|
1773
2137
|
}
|
|
1774
2138
|
const totalWithdrawn = (0, core_1.toNormalizedBn)(totalWithdrawnRaw, decimals);
|
|
1775
2139
|
const tokenPriceUsd = await pricePromise;
|
|
2140
|
+
// Validate token price and log warning if invalid
|
|
1776
2141
|
const effectiveTokenPrice = tokenPriceUsd > 0 ? tokenPriceUsd : 1;
|
|
1777
2142
|
if (tokenPriceUsd <= 0) {
|
|
1778
2143
|
core_1.Logger.log.warn('getVaultUserLifetimePnl:tokenPrice', `Invalid underlying token price: ${tokenPriceUsd}, using fallback of 1`);
|
|
1779
2144
|
}
|
|
2145
|
+
// Sum all deposited amounts in USD using each asset's own price
|
|
1780
2146
|
let totalDepositedUsd = 0;
|
|
1781
2147
|
for (const [assetAddress, normalizedAmount] of totalDepositedRaw) {
|
|
2148
|
+
// Use the asset-specific price, or underlying price for 'default'
|
|
1782
2149
|
const assetPrice = assetAddress === 'default' || assetAddress === 'lz-default'
|
|
1783
2150
|
? effectiveTokenPrice
|
|
1784
2151
|
: assetPrices.get(assetAddress) || effectiveTokenPrice;
|
|
@@ -1786,14 +2153,18 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1786
2153
|
}
|
|
1787
2154
|
const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * effectiveTokenPrice;
|
|
1788
2155
|
const currentPositionValueUsd = Number(currentPositionValue.normalized) * effectiveTokenPrice;
|
|
2156
|
+
// Calculate lifetimePnl in USD first (since deposits may be in different tokens with different prices)
|
|
1789
2157
|
const lifetimePnlUsd = currentPositionValueUsd + totalWithdrawnUsd - totalDepositedUsd;
|
|
1790
|
-
|
|
2158
|
+
// Use BigInt-scaled arithmetic to minimize precision loss when converting USD to native
|
|
2159
|
+
const scaleFactor = BigInt(10 ** 18); // High precision scale
|
|
1791
2160
|
const tokenPriceScaled = BigInt(Math.round(effectiveTokenPrice * Number(scaleFactor)));
|
|
2161
|
+
// Convert totalDepositedUsd to native units for backwards compatibility
|
|
1792
2162
|
const totalDepositedUsdScaled = BigInt(Math.round(totalDepositedUsd * Number(scaleFactor)));
|
|
1793
2163
|
const totalDepositedNativeRaw = tokenPriceScaled > BigInt(0)
|
|
1794
2164
|
? (totalDepositedUsdScaled * BigInt(10 ** decimals)) / tokenPriceScaled
|
|
1795
2165
|
: BigInt(0);
|
|
1796
2166
|
const totalDeposited = (0, core_1.toNormalizedBn)(totalDepositedNativeRaw, decimals);
|
|
2167
|
+
// Convert lifetimePnlUsd to native units
|
|
1797
2168
|
const lifetimePnlUsdScaled = BigInt(Math.round(lifetimePnlUsd * Number(scaleFactor)));
|
|
1798
2169
|
const lifetimePnlRaw = tokenPriceScaled > BigInt(0)
|
|
1799
2170
|
? (lifetimePnlUsdScaled * BigInt(10 ** decimals)) / tokenPriceScaled
|
|
@@ -1821,6 +2192,14 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
1821
2192
|
throw new Error(`#getVaultUserLifetimePnl::${vault}::${wallet}:${errorMessage}`);
|
|
1822
2193
|
}
|
|
1823
2194
|
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Calculate PnL for a vault (vault-level, not user-specific).
|
|
2197
|
+
* Returns the vault's overall profit and loss across all users.
|
|
2198
|
+
*
|
|
2199
|
+
* @param vault Vault contract address
|
|
2200
|
+
* @param options RPC configuration and service options
|
|
2201
|
+
* @returns Vault PnL in USD and notional value
|
|
2202
|
+
*/
|
|
1824
2203
|
async function getVaultPnl({ vault, options, }) {
|
|
1825
2204
|
try {
|
|
1826
2205
|
if (!vault)
|
|
@@ -1828,6 +2207,7 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1828
2207
|
(0, utils_3.assertNotStellar)(vault, 'Vault PnL');
|
|
1829
2208
|
if (!(0, ethers_1.isAddress)(vault))
|
|
1830
2209
|
throw new Error(`Vault input parameter is not an address: ${String(vault)}`);
|
|
2210
|
+
// Get vault data
|
|
1831
2211
|
const vaultData = await getVault({
|
|
1832
2212
|
vault,
|
|
1833
2213
|
loans: false,
|
|
@@ -1837,6 +2217,7 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1837
2217
|
if (!vaultData) {
|
|
1838
2218
|
throw new Error(`Vault ${vault} not found`);
|
|
1839
2219
|
}
|
|
2220
|
+
// Check if vault has daily_pnl_per_share and historical_snapshots
|
|
1840
2221
|
if (!vaultData.dailyPnlPerShare ||
|
|
1841
2222
|
!Array.isArray(vaultData.dailyPnlPerShare) ||
|
|
1842
2223
|
vaultData.dailyPnlPerShare.length === 0) {
|
|
@@ -1854,38 +2235,49 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1854
2235
|
};
|
|
1855
2236
|
}
|
|
1856
2237
|
const decimals = vaultData.decimals;
|
|
2238
|
+
// Filter out dates with zero PnL
|
|
1857
2239
|
const validDailyPnl = vaultData.dailyPnlPerShare.filter((dayData) => dayData.value && dayData.value !== 0);
|
|
2240
|
+
// Create a map of historical_snapshots by date (YYYY-MM-DD) for quick lookup
|
|
1858
2241
|
const snapshotsByDate = new Map();
|
|
1859
2242
|
vaultData.historical_snapshots.forEach((snapshot) => {
|
|
1860
2243
|
const snapshotDate = new Date(snapshot.snapshot_datetime);
|
|
1861
|
-
const dateKey = snapshotDate.toISOString().split('T')[0];
|
|
2244
|
+
const dateKey = snapshotDate.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
1862
2245
|
snapshotsByDate.set(dateKey, snapshot);
|
|
1863
2246
|
});
|
|
2247
|
+
// Calculate PnL for each day using historical_snapshots
|
|
1864
2248
|
let totalPnlRaw = BigInt(0);
|
|
1865
2249
|
validDailyPnl.forEach((dayData) => {
|
|
1866
2250
|
const dayTimestamp = dayData.timestamp;
|
|
1867
2251
|
const dailyPnlPerShare = dayData.value;
|
|
2252
|
+
// Get date key (YYYY-MM-DD) from timestamp
|
|
1868
2253
|
const dayDate = new Date(dayTimestamp);
|
|
1869
2254
|
const dateKey = dayDate.toISOString().split('T')[0];
|
|
2255
|
+
// Find matching snapshot for this date
|
|
1870
2256
|
const snapshot = snapshotsByDate.get(dateKey);
|
|
1871
2257
|
if (!snapshot || !snapshot.total_shares) {
|
|
1872
2258
|
core_1.Logger.log.warn('getVaultPnl:snapshotNotFound', `No snapshot found for date ${dateKey}`);
|
|
1873
2259
|
return;
|
|
1874
2260
|
}
|
|
2261
|
+
// Convert normalized numbers to BigInt using toNormalizedBn's normalized string
|
|
1875
2262
|
const dailyPnlNorm = (0, core_1.toNormalizedBn)(dailyPnlPerShare, decimals);
|
|
1876
2263
|
const totalSharesNorm = (0, core_1.toNormalizedBn)(snapshot.total_shares, decimals);
|
|
2264
|
+
// Convert normalized strings to BigInt (split, pad fraction, combine) - all inline
|
|
1877
2265
|
const [dailyWhole = '0', dailyFrac = ''] = dailyPnlNorm.normalized.split('.');
|
|
1878
2266
|
const dailyPnlPerShareScaled = BigInt(dailyWhole + (dailyFrac || '').padEnd(decimals, '0').slice(0, decimals));
|
|
1879
2267
|
const [sharesWhole = '0', sharesFrac = ''] = totalSharesNorm.normalized.split('.');
|
|
1880
2268
|
const totalSharesScaled = BigInt(sharesWhole +
|
|
1881
2269
|
(sharesFrac || '').padEnd(decimals, '0').slice(0, decimals));
|
|
2270
|
+
// Calculate: daily_pnl_per_share[day] * total_shares[day] (both in raw units)
|
|
1882
2271
|
const dayPnlRaw = (dailyPnlPerShareScaled * totalSharesScaled) / BigInt(10 ** decimals);
|
|
1883
2272
|
totalPnlRaw += dayPnlRaw;
|
|
1884
2273
|
});
|
|
2274
|
+
// Convert totalPnlRaw (BigInt) to normalized number using toNormalizedBn
|
|
1885
2275
|
const totalPnl = (0, core_1.toNormalizedBn)(totalPnlRaw, decimals);
|
|
1886
2276
|
const totalPnlNormalized = Number(totalPnl.normalized);
|
|
1887
2277
|
const chainId = vaultData.chainId;
|
|
2278
|
+
// Get receipt token address (HLPe) from vaultData - no need for on-chain lookup
|
|
1888
2279
|
const lpTokenAddress = vaultData.receipt.address;
|
|
2280
|
+
// Fetch HLPe (lpTokenAddress) price in USD
|
|
1889
2281
|
let tokenPriceUsd = 0;
|
|
1890
2282
|
try {
|
|
1891
2283
|
tokenPriceUsd = await (0, core_1.fetchTokenPriceByAddress)(lpTokenAddress, chainId, options?.headers);
|
|
@@ -1893,6 +2285,7 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1893
2285
|
catch (error) {
|
|
1894
2286
|
core_1.Logger.log.warn('getVaultPnl:priceFetch', `Failed to fetch HLPe token price: ${error}`);
|
|
1895
2287
|
}
|
|
2288
|
+
// Convert to USD and format to 6 decimal places
|
|
1896
2289
|
const pnlUsd = Number((totalPnlNormalized * tokenPriceUsd).toFixed(6));
|
|
1897
2290
|
return {
|
|
1898
2291
|
totalPnl,
|
|
@@ -1905,6 +2298,14 @@ async function getVaultPnl({ vault, options, }) {
|
|
|
1905
2298
|
throw new Error(`#getVaultPnl::${vault}:${errorMessage}`);
|
|
1906
2299
|
}
|
|
1907
2300
|
}
|
|
2301
|
+
/*
|
|
2302
|
+
* Fetch historical timeseries data for a vault.
|
|
2303
|
+
* Returns TVL, APY, PnL, share price, and other metrics over time.
|
|
2304
|
+
* @param vault Vault contract address
|
|
2305
|
+
* @param nDays Number of days of historical data (default 30, min 1)
|
|
2306
|
+
* @param options Request options including headers
|
|
2307
|
+
* @returns Historical timeseries data with date string keys
|
|
2308
|
+
*/
|
|
1908
2309
|
async function getVaultHistoricalTimeseries({ vault, nDays, options, }) {
|
|
1909
2310
|
try {
|
|
1910
2311
|
if (!vault)
|
|
@@ -1918,6 +2319,19 @@ async function getVaultHistoricalTimeseries({ vault, nDays, options, }) {
|
|
|
1918
2319
|
throw new Error(`#getVaultHistoricalTimeseries::${vault}:${errorMessage}`);
|
|
1919
2320
|
}
|
|
1920
2321
|
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Fetch annualized APY metrics for a vault.
|
|
2324
|
+
*
|
|
2325
|
+
* Supported Vaults: cUSDO, tETH, wstETH, rsETH
|
|
2326
|
+
*
|
|
2327
|
+
* @deprecated The `hgETH30dLiquidAPY` and `hgETH7dLiquidAPY` response fields are deprecated.
|
|
2328
|
+
* These fields will be removed on 2026-01-01.
|
|
2329
|
+
* Use `liquidAPY30Day` and `liquidAPY7Day` fields instead.
|
|
2330
|
+
*
|
|
2331
|
+
* @param vault Vault contract address
|
|
2332
|
+
* @param options Request options including headers
|
|
2333
|
+
* @returns Annualized APY data including liquidity APY
|
|
2334
|
+
*/
|
|
1921
2335
|
async function getVaultAnnualizedApy({ vault, options, }) {
|
|
1922
2336
|
try {
|
|
1923
2337
|
if (!vault) {
|
|
@@ -1942,6 +2356,12 @@ async function getVaultAnnualizedApy({ vault, options, }) {
|
|
|
1942
2356
|
throw new Error(`#getVaultAnnualizedApy::${vault}:${errorMessage}`);
|
|
1943
2357
|
}
|
|
1944
2358
|
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Fetch summary data for a vault (name, type, chain, recent returns).
|
|
2361
|
+
* @param vault Vault contract address
|
|
2362
|
+
* @param options Request options including headers
|
|
2363
|
+
* @returns Vault summary data
|
|
2364
|
+
*/
|
|
1945
2365
|
async function getVaultSummary({ vault, options, }) {
|
|
1946
2366
|
try {
|
|
1947
2367
|
if (!vault) {
|
|
@@ -1967,6 +2387,13 @@ async function getVaultSummary({ vault, options, }) {
|
|
|
1967
2387
|
throw new Error(`#getVaultSummary::${vault}:${errorMessage}`);
|
|
1968
2388
|
}
|
|
1969
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Fetch withdrawal summary and pending queue for a vault.
|
|
2392
|
+
* @param vault Vault contract address
|
|
2393
|
+
* @param chain Chain identifier (e.g., chain ID as string)
|
|
2394
|
+
* @param options Request options including headers
|
|
2395
|
+
* @returns Withdrawal summary and pending queue
|
|
2396
|
+
*/
|
|
1970
2397
|
async function getVaultWithdrawals({ vault, chain, options, }) {
|
|
1971
2398
|
try {
|
|
1972
2399
|
if (!vault) {
|
|
@@ -1995,6 +2422,14 @@ async function getVaultWithdrawals({ vault, chain, options, }) {
|
|
|
1995
2422
|
throw new Error(`#getVaultWithdrawals::${vault}::${chain}:${errorMessage}`);
|
|
1996
2423
|
}
|
|
1997
2424
|
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Fetch pending redemptions for a vault with liquidity analysis.
|
|
2427
|
+
* @param vault Vault contract address
|
|
2428
|
+
* @param pastDays Number of past days to include (default 7, min 1, max 30)
|
|
2429
|
+
* @param futureDays Number of future days to include (default 14, min 1, max 30)
|
|
2430
|
+
* @param options Request options including headers
|
|
2431
|
+
* @returns Pending redemptions grouped by date with liquidity summary
|
|
2432
|
+
*/
|
|
1998
2433
|
async function getVaultPendingRedemptions({ vault, pastDays, futureDays, options, }) {
|
|
1999
2434
|
try {
|
|
2000
2435
|
if (!vault)
|
|
@@ -2062,6 +2497,13 @@ async function getVaultPendingRedemptions({ vault, pastDays, futureDays, options
|
|
|
2062
2497
|
throw new Error(`#getVaultPendingRedemptions::${vault}:${errorMessage}`);
|
|
2063
2498
|
}
|
|
2064
2499
|
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Preview the amount of assets that would be received for redeeming shares (queued redemption).
|
|
2502
|
+
* @param vault Vault contract address
|
|
2503
|
+
* @param sharesAmount Amount of shares to redeem (human-readable, raw bigint, or string)
|
|
2504
|
+
* @param options RPC configuration
|
|
2505
|
+
* @returns The amount of assets as INormalizedNumber { normalized, raw }
|
|
2506
|
+
*/
|
|
2065
2507
|
async function getPreviewRedemption({ vault, sharesAmount, options, }) {
|
|
2066
2508
|
try {
|
|
2067
2509
|
if (!vault)
|
|
@@ -2109,6 +2551,13 @@ async function getPreviewRedemption({ vault, sharesAmount, options, }) {
|
|
|
2109
2551
|
throw new Error(`Failed to preview redemption for ${vault}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2110
2552
|
}
|
|
2111
2553
|
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Helper: Decode a processing transaction to extract (year, month, day).
|
|
2556
|
+
*
|
|
2557
|
+
* Note: Multicall-wrapped transactions cannot be decoded from calldata or logs
|
|
2558
|
+
* (the WithdrawalProcessed event doesn't include the date). When decoding fails,
|
|
2559
|
+
* the caller falls back to timestamp-based date estimation.
|
|
2560
|
+
*/
|
|
2112
2561
|
async function decodeProcessingTransaction(txHash, provider, iface) {
|
|
2113
2562
|
const tx = await provider.getTransaction(txHash);
|
|
2114
2563
|
if (!tx) {
|
|
@@ -2122,14 +2571,74 @@ async function decodeProcessingTransaction(txHash, provider, iface) {
|
|
|
2122
2571
|
}
|
|
2123
2572
|
return (0, call_data_decoder_1.decodeWithdrawalProcessing)(tx.data, iface);
|
|
2124
2573
|
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Returns all withdrawal requests for a vault, enriched with computed claimable dates
|
|
2576
|
+
* and current processing status.
|
|
2577
|
+
*
|
|
2578
|
+
* The unique identifier for matching requests to processed events is:
|
|
2579
|
+
* (receiver, claimableDate.year, claimableDate.month, claimableDate.day)
|
|
2580
|
+
*
|
|
2581
|
+
* This is the primary method for integrators to reliably track withdrawal status
|
|
2582
|
+
* when multiple requests are submitted by the same receiver.
|
|
2583
|
+
*
|
|
2584
|
+
* ## Data sources
|
|
2585
|
+
* - **Withdrawal requests** come from the subgraph (all historical requests).
|
|
2586
|
+
* - **Processed events** come from on-chain `WithdrawalProcessed` logs, which are
|
|
2587
|
+
* limited to a finite lookback window determined by the chain's block parameters.
|
|
2588
|
+
*
|
|
2589
|
+
* ## Lookback window & filtering
|
|
2590
|
+
* On-chain logs only cover a limited number of recent blocks (e.g. ~20 days on Monad,
|
|
2591
|
+
* ~21 days on Ethereum). The `lookbackBlocks` parameter controls how far back to scan
|
|
2592
|
+
* and is applied uniformly to both data sources: it bounds the `WithdrawalProcessed`
|
|
2593
|
+
* log query *and* drops any withdrawal request whose submission timestamp falls outside
|
|
2594
|
+
* the window — regardless of whether a matching processed event was found. Callers who
|
|
2595
|
+
* need deeper history should widen `lookbackBlocks`.
|
|
2596
|
+
*
|
|
2597
|
+
* ## Status determination
|
|
2598
|
+
* - `'processed'` — A matching `WithdrawalProcessed` event was found (by receiver + claimable date).
|
|
2599
|
+
* - `'ready_to_claim'` — Claimable date has passed (within the lookback window) but no processed event found.
|
|
2600
|
+
* - `'pending'` — Claimable date is still in the future.
|
|
2601
|
+
*
|
|
2602
|
+
* @param params - Query parameters
|
|
2603
|
+
* @param params.vault - Vault address to query
|
|
2604
|
+
* @param params.receiver - (Optional) Filter by receiver address
|
|
2605
|
+
* @param params.lookbackBlocks - (Optional) On-chain log lookback window in blocks.
|
|
2606
|
+
* Defaults to a chain-specific value (e.g. 150,000 for Ethereum, 3,456,000 for Monad).
|
|
2607
|
+
* Increasing this scans more history but requires more RPC calls.
|
|
2608
|
+
* @param params.options - Standard vault query options (provider, RPC URL, etc.)
|
|
2609
|
+
* @returns Array of withdrawal requests with status and claimable dates
|
|
2610
|
+
*
|
|
2611
|
+
* @example
|
|
2612
|
+
* ```typescript
|
|
2613
|
+
* const requests = await getWithdrawalRequestsWithStatus({
|
|
2614
|
+
* vault: '0xVault...',
|
|
2615
|
+
* receiver: '0xReceiver...'
|
|
2616
|
+
* });
|
|
2617
|
+
*
|
|
2618
|
+
* // Results show exact status + claimable date
|
|
2619
|
+
* requests.forEach(req => {
|
|
2620
|
+
* console.log(
|
|
2621
|
+
* `${req.claimableDate.year}-${req.claimableDate.month}-${req.claimableDate.day}: ${req.status}`
|
|
2622
|
+
* );
|
|
2623
|
+
* });
|
|
2624
|
+
*
|
|
2625
|
+
* // With custom lookback window (e.g. 30 days on Ethereum ≈ 216,000 blocks)
|
|
2626
|
+
* const moreResults = await getWithdrawalRequestsWithStatus({
|
|
2627
|
+
* vault: '0xVault...',
|
|
2628
|
+
* lookbackBlocks: 216_000,
|
|
2629
|
+
* });
|
|
2630
|
+
* ```
|
|
2631
|
+
*/
|
|
2125
2632
|
async function getWithdrawalRequestsWithStatus(params) {
|
|
2126
2633
|
try {
|
|
2127
2634
|
const { vault, receiver, lookbackBlocks, options } = params;
|
|
2635
|
+
// Input validation
|
|
2128
2636
|
if (!vault || !(0, ethers_1.isAddress)(vault)) {
|
|
2129
2637
|
throw new Error('Invalid vault address');
|
|
2130
2638
|
}
|
|
2131
2639
|
const vaultAddress = (0, ethers_1.getAddress)(vault);
|
|
2132
2640
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
2641
|
+
// Fetch vault metadata and lag duration
|
|
2133
2642
|
const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(vaultAddress, options?.headers, false, false))?.[0];
|
|
2134
2643
|
if (!tokenizedVault) {
|
|
2135
2644
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Vault not found in backend: ${vaultAddress}`);
|
|
@@ -2143,7 +2652,7 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2143
2652
|
abi: vaultAbi,
|
|
2144
2653
|
provider,
|
|
2145
2654
|
});
|
|
2146
|
-
let lagDuration = 86400;
|
|
2655
|
+
let lagDuration = 86400; // Default: 1 day
|
|
2147
2656
|
try {
|
|
2148
2657
|
const lagValue = await vaultContract.lagDuration?.();
|
|
2149
2658
|
if (lagValue !== undefined && lagValue !== null) {
|
|
@@ -2153,6 +2662,9 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2153
2662
|
catch (e) {
|
|
2154
2663
|
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
2664
|
}
|
|
2665
|
+
// Fetch subgraph requests, on-chain processed events, and the current block
|
|
2666
|
+
// in parallel. Current block is needed to derive the block-based lookback
|
|
2667
|
+
// cutoff applied uniformly to both data sources.
|
|
2156
2668
|
const [withdrawalRequests, processedEvents, currentBlock] = await Promise.all([
|
|
2157
2669
|
(0, vaults_1.getSubgraphWithdrawRequests)(vaultAddress, provider),
|
|
2158
2670
|
getVaultRedemptionHistory({
|
|
@@ -2170,6 +2682,10 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2170
2682
|
const effectiveLookbackBlocks = lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId);
|
|
2171
2683
|
const blockCutoff = currentBlock - effectiveLookbackBlocks;
|
|
2172
2684
|
const now = Math.floor(Date.now() / 1000);
|
|
2685
|
+
// Filter by receiver (if provided) and drop any request whose submission
|
|
2686
|
+
// block is outside the lookback window — uniformly, regardless of whether
|
|
2687
|
+
// a matching processed event exists. Using block_number (not timestamp)
|
|
2688
|
+
// keeps this consistent with the on-chain log scan's block-based cutoff.
|
|
2173
2689
|
const filteredRequests = withdrawalRequests.filter((req) => {
|
|
2174
2690
|
if (receiver &&
|
|
2175
2691
|
req.receiverAddr?.toLowerCase() !== receiver.toLowerCase()) {
|
|
@@ -2177,8 +2693,10 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2177
2693
|
}
|
|
2178
2694
|
return Number(req.block_number) >= blockCutoff;
|
|
2179
2695
|
});
|
|
2696
|
+
// Pre-decode all processed transactions in parallel for performance
|
|
2180
2697
|
const iface = new ethers_1.Interface(vaultAbi);
|
|
2181
2698
|
const decodedProcessed = await Promise.all((processedEvents || []).map(async (processed) => {
|
|
2699
|
+
// Try calldata decoding first (deterministic)
|
|
2182
2700
|
if (processed.transactionHash) {
|
|
2183
2701
|
try {
|
|
2184
2702
|
const decoded = await decodeProcessingTransaction(processed.transactionHash, provider, iface);
|
|
@@ -2197,8 +2715,12 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2197
2715
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Failed to decode processed transaction ${processed.transactionHash}: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
2198
2716
|
}
|
|
2199
2717
|
}
|
|
2718
|
+
// Fallback: use original request timestamp to compute claimable date,
|
|
2719
|
+
// or if unavailable (evm-2: requested=epoch 0), use the processed block
|
|
2720
|
+
// timestamp as the date directly (processing happens on the claimable date).
|
|
2200
2721
|
const requestedEpoch = Math.floor(processed.requested.getTime() / 1000);
|
|
2201
2722
|
if (requestedEpoch > 0) {
|
|
2723
|
+
// evm-1: we have the original request timestamp → compute claimable date
|
|
2202
2724
|
const fallbackDate = (0, date_utils_1.computeClaimableDate)(requestedEpoch, lagDuration);
|
|
2203
2725
|
return {
|
|
2204
2726
|
...processed,
|
|
@@ -2209,6 +2731,8 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2209
2731
|
},
|
|
2210
2732
|
};
|
|
2211
2733
|
}
|
|
2734
|
+
// evm-2: no request timestamp — use processed block date directly
|
|
2735
|
+
// (processing occurs on the claimable date, so the block date IS the date)
|
|
2212
2736
|
const processedDate = processed.processed;
|
|
2213
2737
|
return {
|
|
2214
2738
|
...processed,
|
|
@@ -2219,6 +2743,11 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2219
2743
|
},
|
|
2220
2744
|
};
|
|
2221
2745
|
}));
|
|
2746
|
+
// Build deterministic lookup map: (receiver + year + month + day) → processed events
|
|
2747
|
+
// A single WithdrawalProcessed event covers the entire day's batch for a
|
|
2748
|
+
// receiver — all requests whose claimable date falls on that day should
|
|
2749
|
+
// match it. We keep an array per key only to preserve every event we saw
|
|
2750
|
+
// for visibility; matching is a non-destructive peek at the first entry.
|
|
2222
2751
|
const processedMap = new Map();
|
|
2223
2752
|
for (const processed of decodedProcessed) {
|
|
2224
2753
|
if (!processed.decodedDate) {
|
|
@@ -2229,15 +2758,24 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2229
2758
|
const existing = processedMap.get(key) ?? [];
|
|
2230
2759
|
processedMap.set(key, [...existing, processed]);
|
|
2231
2760
|
}
|
|
2761
|
+
// Compute status for each withdrawal request. The lookback window was
|
|
2762
|
+
// already applied up-front when pruning filteredRequests, so every request
|
|
2763
|
+
// reaching this loop is within the window by construction.
|
|
2232
2764
|
const results = [];
|
|
2233
2765
|
for (const req of filteredRequests) {
|
|
2766
|
+
// timestamp_ is a Unix epoch string (seconds)
|
|
2234
2767
|
const requestTimestamp = Number(req.timestamp_);
|
|
2768
|
+
// Compute claimable date from timestamp + lagDuration (UTC)
|
|
2235
2769
|
const claimableDate = (0, date_utils_1.computeClaimableDate)(requestTimestamp, lagDuration);
|
|
2770
|
+
// Determine status: check processed first, then claimability
|
|
2236
2771
|
let status = 'pending';
|
|
2237
2772
|
let processedTxHash;
|
|
2238
2773
|
let assetsReceived;
|
|
2774
|
+
// Match deterministically by (receiver, year, month, day)
|
|
2239
2775
|
const dateKey = (0, date_utils_1.formatDateKey)(claimableDate.year, claimableDate.month, claimableDate.day);
|
|
2240
2776
|
const lookupKey = `${req.receiverAddr.toLowerCase()}:${dateKey}`;
|
|
2777
|
+
// Peek — do not consume. The processing event covers the whole day for
|
|
2778
|
+
// this receiver, so every request on that day matches the same event.
|
|
2241
2779
|
const processedEvent = processedMap.get(lookupKey)?.[0];
|
|
2242
2780
|
if (processedEvent) {
|
|
2243
2781
|
status = 'processed';
|
|
@@ -2250,6 +2788,7 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2250
2788
|
}
|
|
2251
2789
|
}
|
|
2252
2790
|
else if ((0, date_utils_1.isClaimableNow)(claimableDate.epoch, now)) {
|
|
2791
|
+
// Claimable date is recent (within lookback) and not yet processed
|
|
2253
2792
|
status = 'ready_to_claim';
|
|
2254
2793
|
}
|
|
2255
2794
|
results.push({
|
|
@@ -2260,6 +2799,7 @@ async function getWithdrawalRequestsWithStatus(params) {
|
|
|
2260
2799
|
return BigInt(req.shares);
|
|
2261
2800
|
}
|
|
2262
2801
|
catch {
|
|
2802
|
+
// Handle decimal strings from subgraph (e.g. "1234.0")
|
|
2263
2803
|
const truncated = Math.trunc(Number(req.shares));
|
|
2264
2804
|
if (!Number.isFinite(truncated)) {
|
|
2265
2805
|
core_1.Logger.log.warn('getWithdrawalRequestsWithStatus', `Cannot parse shares value "${req.shares}" — defaulting to 0`);
|