@augustdigital/sdk 9.8.0 → 9.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/lib/adapters/evm/index.d.ts +23 -5
- package/lib/adapters/evm/index.js +25 -4
- package/lib/core/analytics/method-taxonomy.js +2 -0
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/core/attribution.d.ts +37 -5
- package/lib/core/attribution.js +39 -3
- package/lib/core/base.class.d.ts +6 -0
- package/lib/core/constants/core.d.ts +1 -0
- package/lib/core/constants/core.js +1 -0
- package/lib/core/constants/swap-router.d.ts +21 -4
- package/lib/core/constants/swap-router.js +22 -5
- package/lib/core/helpers/swap-router.d.ts +66 -5
- package/lib/core/helpers/swap-router.js +158 -7
- package/lib/main.d.ts +21 -3
- package/lib/main.js +20 -3
- package/lib/modules/vaults/getters.d.ts +287 -14
- package/lib/modules/vaults/getters.js +458 -48
- package/lib/modules/vaults/main.d.ts +18 -1
- package/lib/modules/vaults/main.js +23 -0
- package/lib/modules/vaults/write.actions.d.ts +119 -29
- package/lib/modules/vaults/write.actions.js +384 -77
- package/lib/sdk.d.ts +11675 -11107
- package/lib/services/layerzero/deposits.d.ts +6 -0
- package/lib/services/layerzero/deposits.js +8 -0
- package/lib/services/octavfi/fetcher.d.ts +30 -6
- package/lib/services/octavfi/fetcher.js +27 -91
- package/lib/services/subgraph/schema.d.ts +5 -2
- package/lib/services/subgraph/schema.js +22 -10
- package/lib/services/subgraph/vaults.d.ts +23 -1
- package/lib/services/subgraph/vaults.js +65 -0
- package/lib/types/subgraph.d.ts +10 -0
- package/lib/types/vaults.d.ts +38 -6
- package/package.json +1 -1
|
@@ -58,6 +58,11 @@ exports.assertPnlTokenPrice = assertPnlTokenPrice;
|
|
|
58
58
|
exports.sumDepositsAtFaceValue = sumDepositsAtFaceValue;
|
|
59
59
|
exports.selectPeerTransfers = selectPeerTransfers;
|
|
60
60
|
exports.transferCostBasis = transferCostBasis;
|
|
61
|
+
exports.historyCoversMints = historyCoversMints;
|
|
62
|
+
exports.selectReserveDeposits = selectReserveDeposits;
|
|
63
|
+
exports.countWithdrawalRequestsOnce = countWithdrawalRequestsOnce;
|
|
64
|
+
exports.settleV2Redemptions = settleV2Redemptions;
|
|
65
|
+
exports.pnlCostBasis = pnlCostBasis;
|
|
61
66
|
exports.getVaultUserLifetimePnl = getVaultUserLifetimePnl;
|
|
62
67
|
exports.getVaultPnl = getVaultPnl;
|
|
63
68
|
exports.getVaultHistoricalTimeseries = getVaultHistoricalTimeseries;
|
|
@@ -636,11 +641,14 @@ async function getVaultAllocations(vault, options, tokenizedVault) {
|
|
|
636
641
|
// afterwards, and a borrower on a chain with portfolio coverage would otherwise
|
|
637
642
|
// have its rows silently dropped rather than double-counted.
|
|
638
643
|
let untangledTokenCount = 0;
|
|
644
|
+
let vaultOctav;
|
|
639
645
|
const borrowerPortfolioFetchers = {
|
|
640
646
|
solana: async (borrower) => {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
647
|
+
vaultOctav ??= (0, octavfi_1.fetchVaultOctavResponse)(vault, options?.headers);
|
|
648
|
+
const response = await vaultOctav;
|
|
649
|
+
const portfolio = response ? response[borrower] : undefined;
|
|
650
|
+
return portfolio && !('error' in portfolio)
|
|
651
|
+
? (0, octavfi_1.transformOctavfiToDebank)(portfolio)
|
|
644
652
|
: null;
|
|
645
653
|
},
|
|
646
654
|
};
|
|
@@ -2388,12 +2396,15 @@ function sumDepositsAtFaceValue(depositsByAsset, assetDecimals, vaultDecimals) {
|
|
|
2388
2396
|
* another wallet. Mints (from the zero address), burns (to it) and transfers to
|
|
2389
2397
|
* or from the vault itself are dropped: those are the deposit and withdrawal
|
|
2390
2398
|
* legs the subgraph history already records, and counting them again would
|
|
2391
|
-
* double the cost basis.
|
|
2399
|
+
* double the cost basis. Transfers to or from `exclude` are dropped too.
|
|
2392
2400
|
*
|
|
2393
2401
|
* @param transfers - Receipt-token transfers touching `wallet`, as returned by
|
|
2394
2402
|
* the vault's transfer subgraph.
|
|
2395
2403
|
* @param wallet - The wallet whose position is being valued.
|
|
2396
2404
|
* @param vault - The vault contract address.
|
|
2405
|
+
* @param exclude - Other counterparties whose transfers are not peer
|
|
2406
|
+
* transfers, such as a LayerZero OVault's hub composer and share OFT
|
|
2407
|
+
* (`hubComposer` / `hubShareOFT` in `LAYERZERO_VAULTS`). Defaults to none.
|
|
2397
2408
|
* @returns The peer-to-peer transfers, in input order. Addresses compare
|
|
2398
2409
|
* case-insensitively.
|
|
2399
2410
|
* @example
|
|
@@ -2401,9 +2412,9 @@ function sumDepositsAtFaceValue(depositsByAsset, assetDecimals, vaultDecimals) {
|
|
|
2401
2412
|
* const peer = selectPeerTransfers(transfers, wallet, vault);
|
|
2402
2413
|
* ```
|
|
2403
2414
|
*/
|
|
2404
|
-
function selectPeerTransfers(transfers, wallet, vault) {
|
|
2415
|
+
function selectPeerTransfers(transfers, wallet, vault, exclude = []) {
|
|
2405
2416
|
const self = wallet.toLowerCase();
|
|
2406
|
-
const excluded = new Set([ethers_1.ZeroAddress.
|
|
2417
|
+
const excluded = new Set([ethers_1.ZeroAddress, vault, ...exclude].map((address) => address.toLowerCase()));
|
|
2407
2418
|
return transfers.filter((transfer) => {
|
|
2408
2419
|
const from = transfer.from.toLowerCase();
|
|
2409
2420
|
const to = transfer.to.toLowerCase();
|
|
@@ -2423,9 +2434,11 @@ function selectPeerTransfers(transfers, wallet, vault) {
|
|
|
2423
2434
|
* profit. A day with no usable ratio takes the latest earlier day's, and a
|
|
2424
2435
|
* transfer older than the history takes the earliest day's.
|
|
2425
2436
|
*
|
|
2426
|
-
* Sent shares count only against a received basis. A wallet that
|
|
2427
|
-
* but received none gets a zero basis, so the lifetime-PnL history
|
|
2428
|
-
* decides for it instead of reporting the sent shares as profit.
|
|
2437
|
+
* Sent shares count only against a received basis by default. A wallet that
|
|
2438
|
+
* sent shares but received none gets a zero basis, so the lifetime-PnL history
|
|
2439
|
+
* guard still decides for it instead of reporting the sent shares as profit.
|
|
2440
|
+
* `countSentWithoutReceipt: true` reports them anyway, for a caller that knows
|
|
2441
|
+
* the wallet also deposited.
|
|
2429
2442
|
*
|
|
2430
2443
|
* Pure and synchronous. Assumes, as the getter does, that shares and assets
|
|
2431
2444
|
* share the vault's decimals.
|
|
@@ -2446,7 +2459,7 @@ function selectPeerTransfers(transfers, wallet, vault) {
|
|
|
2446
2459
|
* });
|
|
2447
2460
|
* ```
|
|
2448
2461
|
*/
|
|
2449
|
-
function transferCostBasis({ transfers, wallet, vault, history, }) {
|
|
2462
|
+
function transferCostBasis({ transfers, wallet, vault, history, countSentWithoutReceipt = false, }) {
|
|
2450
2463
|
const ratios = Object.entries(history)
|
|
2451
2464
|
.filter(([, day]) => Number.isFinite(day.asset_share_ratio) &&
|
|
2452
2465
|
(day.asset_share_ratio ?? 0) > 0)
|
|
@@ -2478,29 +2491,341 @@ function transferCostBasis({ transfers, wallet, vault, history, }) {
|
|
|
2478
2491
|
sentRaw += assetsAt(shares, transfer.timestamp_);
|
|
2479
2492
|
}
|
|
2480
2493
|
}
|
|
2481
|
-
if (receivedRaw === BigInt(0))
|
|
2494
|
+
if (receivedRaw === BigInt(0) && !countSentWithoutReceipt) {
|
|
2482
2495
|
return { receivedRaw, sentRaw: BigInt(0) };
|
|
2496
|
+
}
|
|
2483
2497
|
return { receivedRaw, sentRaw };
|
|
2484
2498
|
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Check that a wallet's indexed deposit history accounts for every share mint
|
|
2501
|
+
* its share-token ledger shows.
|
|
2502
|
+
*
|
|
2503
|
+
* A deposit mints shares to the wallet in the same transaction, so a mint with
|
|
2504
|
+
* no deposit of the same hash means the history is missing that deposit (an
|
|
2505
|
+
* older deposit the subgraph never indexed, or one a third party made for the
|
|
2506
|
+
* wallet). Folding peer transfers into such a history would credit the wallet
|
|
2507
|
+
* for shares it sent while missing what it paid for them, so
|
|
2508
|
+
* {@link getVaultUserLifetimePnl} only folds transfers for a depositor when
|
|
2509
|
+
* this holds.
|
|
2510
|
+
*
|
|
2511
|
+
* Pure and synchronous. Hashes and addresses compare case-insensitively.
|
|
2512
|
+
*
|
|
2513
|
+
* @param params - See {@link HistoryCoversMintsParams}.
|
|
2514
|
+
* @returns `true` when every mint to `wallet` has a matching deposit, including
|
|
2515
|
+
* when there are no mints.
|
|
2516
|
+
* @example
|
|
2517
|
+
* ```ts
|
|
2518
|
+
* const covered = historyCoversMints({
|
|
2519
|
+
* transfers,
|
|
2520
|
+
* wallet,
|
|
2521
|
+
* depositTransactionHashes: history
|
|
2522
|
+
* .filter((item) => item.type === 'deposit')
|
|
2523
|
+
* .map((item) => item.transactionHash_),
|
|
2524
|
+
* });
|
|
2525
|
+
* ```
|
|
2526
|
+
*/
|
|
2527
|
+
function historyCoversMints({ transfers, wallet, depositTransactionHashes, }) {
|
|
2528
|
+
const self = wallet.toLowerCase();
|
|
2529
|
+
const zero = ethers_1.ZeroAddress.toLowerCase();
|
|
2530
|
+
const deposits = new Set(depositTransactionHashes.map((hash) => hash.toLowerCase()));
|
|
2531
|
+
return transfers.every((transfer) => transfer.from.toLowerCase() !== zero ||
|
|
2532
|
+
transfer.to.toLowerCase() !== self ||
|
|
2533
|
+
deposits.has(transfer.transactionHash_.toLowerCase()));
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Keep the `ReserveDeposit` events that explain a share mint the indexed
|
|
2537
|
+
* deposit history misses.
|
|
2538
|
+
*
|
|
2539
|
+
* High Growth ETH mints shares for deposits made through Kelp's gain adapter
|
|
2540
|
+
* with a `ReserveDeposit` event instead of an ERC-4626 `Deposit`, so those
|
|
2541
|
+
* deposits are absent from the user history and fail {@link historyCoversMints}.
|
|
2542
|
+
* An event counts only when its transaction mints shares to `wallet` and holds
|
|
2543
|
+
* no indexed deposit, so a deposit is never counted twice.
|
|
2544
|
+
*
|
|
2545
|
+
* Pure and synchronous. Hashes and addresses compare case-insensitively.
|
|
2546
|
+
*
|
|
2547
|
+
* @param params - See {@link SelectReserveDepositsParams}.
|
|
2548
|
+
* @returns The matching events, in input order.
|
|
2549
|
+
* @example
|
|
2550
|
+
* ```ts
|
|
2551
|
+
* const reserve = selectReserveDeposits({
|
|
2552
|
+
* reserveDeposits: await getSubgraphUserReserveDeposits(wallet, provider, vault),
|
|
2553
|
+
* transfers,
|
|
2554
|
+
* wallet,
|
|
2555
|
+
* depositTransactionHashes,
|
|
2556
|
+
* });
|
|
2557
|
+
* ```
|
|
2558
|
+
*/
|
|
2559
|
+
function selectReserveDeposits({ reserveDeposits, transfers, wallet, depositTransactionHashes, }) {
|
|
2560
|
+
const self = wallet.toLowerCase();
|
|
2561
|
+
const zero = ethers_1.ZeroAddress.toLowerCase();
|
|
2562
|
+
const deposits = new Set(depositTransactionHashes.map((hash) => hash.toLowerCase()));
|
|
2563
|
+
const unmatchedMints = new Set(transfers
|
|
2564
|
+
.filter((transfer) => transfer.from.toLowerCase() === zero &&
|
|
2565
|
+
transfer.to.toLowerCase() === self &&
|
|
2566
|
+
!deposits.has(transfer.transactionHash_.toLowerCase()))
|
|
2567
|
+
.map((transfer) => transfer.transactionHash_.toLowerCase()));
|
|
2568
|
+
return reserveDeposits.filter((deposit) => deposit.account.toLowerCase() === self &&
|
|
2569
|
+
unmatchedMints.has(deposit.transactionHash_.toLowerCase()));
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Drop withdrawal-request rows the vault logged more than once for a single
|
|
2573
|
+
* request.
|
|
2574
|
+
*
|
|
2575
|
+
* A redemption requested through Kelp's gain adapter makes High Growth ETH emit
|
|
2576
|
+
* `WithdrawalRequested` twice for one share transfer, and the subgraph indexes
|
|
2577
|
+
* both. In a transaction where the share ledger shows the wallet sending shares
|
|
2578
|
+
* to the vault `n` times, at most `n` request rows are kept. A transaction the
|
|
2579
|
+
* ledger shows no such transfer for keeps every row, so a missing ledger leaves
|
|
2580
|
+
* the history unchanged.
|
|
2581
|
+
*
|
|
2582
|
+
* Pure and synchronous. Hashes and addresses compare case-insensitively.
|
|
2583
|
+
*
|
|
2584
|
+
* @param params - See {@link CountWithdrawalRequestsOnceParams}.
|
|
2585
|
+
* @returns The history without the surplus request rows, in input order.
|
|
2586
|
+
* @example
|
|
2587
|
+
* ```ts
|
|
2588
|
+
* const history = countWithdrawalRequestsOnce({ history: userHistory, transfers, wallet, vault });
|
|
2589
|
+
* ```
|
|
2590
|
+
*/
|
|
2591
|
+
function countWithdrawalRequestsOnce({ history, transfers, wallet, vault, }) {
|
|
2592
|
+
const self = wallet.toLowerCase();
|
|
2593
|
+
const target = vault.toLowerCase();
|
|
2594
|
+
const remaining = new Map();
|
|
2595
|
+
for (const transfer of transfers) {
|
|
2596
|
+
if (transfer.from.toLowerCase() !== self ||
|
|
2597
|
+
transfer.to.toLowerCase() !== target)
|
|
2598
|
+
continue;
|
|
2599
|
+
const hash = transfer.transactionHash_.toLowerCase();
|
|
2600
|
+
remaining.set(hash, (remaining.get(hash) ?? 0) + 1);
|
|
2601
|
+
}
|
|
2602
|
+
return history.filter((item) => {
|
|
2603
|
+
if (item.type !== 'withdraw-request')
|
|
2604
|
+
return true;
|
|
2605
|
+
const hash = item.transactionHash_.toLowerCase();
|
|
2606
|
+
const left = remaining.get(hash);
|
|
2607
|
+
if (left === undefined)
|
|
2608
|
+
return true;
|
|
2609
|
+
if (left === 0)
|
|
2610
|
+
return false;
|
|
2611
|
+
remaining.set(hash, left - 1);
|
|
2612
|
+
return true;
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Value a wallet's Tokenized Vault V2 redemption requests, which carry only
|
|
2617
|
+
* shares.
|
|
2618
|
+
*
|
|
2619
|
+
* Every processed withdrawal (`WithdrawalProcessed.assetsAmount`) counts at the
|
|
2620
|
+
* assets the vault paid. Its shares, estimated at the vault's
|
|
2621
|
+
* `asset_share_ratio` on the day it was processed, retire the oldest requests
|
|
2622
|
+
* first. Request shares left over count at the ratio on their claim date (see
|
|
2623
|
+
* `computeClaimableDate`) once claimable: the subgraph lacks some processed rows
|
|
2624
|
+
* (none for earnAUSD before 2026-02-01), and that ratio lands within about
|
|
2625
|
+
* 0.05% of what the vault paid. Shares not yet claimable are left to the
|
|
2626
|
+
* current share price. Matching by amount, not by claim date, keeps the result
|
|
2627
|
+
* right when a vault processes a request before the date today's `lagDuration`
|
|
2628
|
+
* implies. A leftover under 0.5% of a request that a later processed
|
|
2629
|
+
* withdrawal followed is rounding from the daily ratio, so it is dropped.
|
|
2630
|
+
*
|
|
2631
|
+
* Pure and synchronous. Assumes, as the getter does, that shares and assets
|
|
2632
|
+
* share the vault's decimals. A day with no usable ratio takes the latest
|
|
2633
|
+
* earlier day's, and a date older than the history takes the earliest day's.
|
|
2634
|
+
*
|
|
2635
|
+
* @param params - See {@link SettleV2RedemptionsParams}.
|
|
2636
|
+
* @returns The settled assets and the shares left to the current price; every
|
|
2637
|
+
* requested share is pending when `ratios` holds no usable day.
|
|
2638
|
+
* @example
|
|
2639
|
+
* ```ts
|
|
2640
|
+
* const { settledAssetsRaw, pendingShares } = settleV2Redemptions({
|
|
2641
|
+
* history,
|
|
2642
|
+
* lagDuration: 259_200,
|
|
2643
|
+
* ratios: (await fetchVaultHistoricalTimeseries(vault, nDays)).data,
|
|
2644
|
+
* now: Math.floor(Date.now() / 1000),
|
|
2645
|
+
* });
|
|
2646
|
+
* ```
|
|
2647
|
+
*/
|
|
2648
|
+
function settleV2Redemptions({ history, lagDuration, ratios, now, }) {
|
|
2649
|
+
const requests = history
|
|
2650
|
+
.filter((item) => item.type === 'withdraw-request')
|
|
2651
|
+
.sort((a, b) => Number(a.timestamp_) - Number(b.timestamp_));
|
|
2652
|
+
const days = Object.entries(ratios)
|
|
2653
|
+
.filter(([, day]) => Number.isFinite(day.asset_share_ratio) &&
|
|
2654
|
+
(day.asset_share_ratio ?? 0) > 0)
|
|
2655
|
+
.map(([date, day]) => ({
|
|
2656
|
+
date,
|
|
2657
|
+
ratio: BigInt(Math.round(day.asset_share_ratio * 1e18)),
|
|
2658
|
+
}))
|
|
2659
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
2660
|
+
if (days.length === 0) {
|
|
2661
|
+
return {
|
|
2662
|
+
settledAssetsRaw: BigInt(0),
|
|
2663
|
+
pendingShares: requests.reduce((sum, item) => sum + BigInt(item.amount), BigInt(0)),
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
const scale = BigInt(10) ** BigInt(18);
|
|
2667
|
+
const ratioOn = (date) => days.filter((day) => day.date <= date).pop()?.ratio ?? days[0].ratio;
|
|
2668
|
+
const utcDay = (timestamp) => new Date(Number(timestamp) * 1000).toISOString().slice(0, 10);
|
|
2669
|
+
let settledAssetsRaw = BigInt(0);
|
|
2670
|
+
let retiredShares = BigInt(0);
|
|
2671
|
+
let lastProcessedAt = Number.NEGATIVE_INFINITY;
|
|
2672
|
+
for (const item of history) {
|
|
2673
|
+
if (item.type !== 'withdraw-processed' || item.isInstant)
|
|
2674
|
+
continue;
|
|
2675
|
+
const assets = BigInt(item.amount);
|
|
2676
|
+
settledAssetsRaw += assets;
|
|
2677
|
+
retiredShares += (assets * scale) / ratioOn(utcDay(item.timestamp_));
|
|
2678
|
+
lastProcessedAt = Math.max(lastProcessedAt, Number(item.timestamp_));
|
|
2679
|
+
}
|
|
2680
|
+
let pendingShares = BigInt(0);
|
|
2681
|
+
for (const item of requests) {
|
|
2682
|
+
const shares = BigInt(item.amount);
|
|
2683
|
+
const retired = retiredShares < shares ? retiredShares : shares;
|
|
2684
|
+
retiredShares -= retired;
|
|
2685
|
+
const open = shares - retired;
|
|
2686
|
+
// A sliver left on a request processed later is ratio rounding.
|
|
2687
|
+
if (open === BigInt(0) ||
|
|
2688
|
+
(open * BigInt(200) <= shares &&
|
|
2689
|
+
lastProcessedAt >= Number(item.timestamp_)))
|
|
2690
|
+
continue;
|
|
2691
|
+
const claim = (0, date_utils_1.computeClaimableDate)(Number(item.timestamp_), lagDuration);
|
|
2692
|
+
if ((0, date_utils_1.isClaimableNow)(claim.epoch, now)) {
|
|
2693
|
+
const key = (0, date_utils_1.formatDateKey)(claim.year, claim.month, claim.day);
|
|
2694
|
+
settledAssetsRaw += (open * ratioOn(key)) / scale;
|
|
2695
|
+
}
|
|
2696
|
+
else {
|
|
2697
|
+
pendingShares += open;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return { settledAssetsRaw, pendingShares };
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Fold peer share transfers into a wallet's lifetime-PnL cost basis.
|
|
2704
|
+
*
|
|
2705
|
+
* Shares a wallet received from another holder (a migration airdrop, a DEX
|
|
2706
|
+
* purchase, a transfer from another wallet) cost the wallet their value on
|
|
2707
|
+
* arrival, so they add to the deposited amount; shares it sent away count as
|
|
2708
|
+
* withdrawn. This applies whether or not the wallet also deposited, so a holder
|
|
2709
|
+
* who received shares and then topped up is not credited with the received
|
|
2710
|
+
* shares as profit, and a depositor who moved its shares elsewhere (a lending
|
|
2711
|
+
* market, a DEX) is not shown as having lost them. A wallet with no deposit and
|
|
2712
|
+
* no received shares keeps a zero basis, so the history guard still decides.
|
|
2713
|
+
*
|
|
2714
|
+
* A depositor whose history misses a mint ({@link historyCoversMints}) is the
|
|
2715
|
+
* exception: its transfers do not count, so the result matches what the getter
|
|
2716
|
+
* returned before transfers counted for depositors. On a LayerZero OVault the
|
|
2717
|
+
* caller leaves the hub composer and share OFT out of the transfers (see
|
|
2718
|
+
* {@link selectPeerTransfers}): the composer's deliveries are already counted
|
|
2719
|
+
* as LayerZero deposits, and shares bridged to a spoke chain still count in the
|
|
2720
|
+
* position.
|
|
2721
|
+
*
|
|
2722
|
+
* Pure and synchronous.
|
|
2723
|
+
*
|
|
2724
|
+
* @param params - See {@link PnlCostBasisParams}.
|
|
2725
|
+
* @returns The deposited and withdrawn amounts to compute PnL from, and the
|
|
2726
|
+
* transfer-priced part of the deposited amount (zero when transfers did not
|
|
2727
|
+
* count).
|
|
2728
|
+
* @example
|
|
2729
|
+
* ```ts
|
|
2730
|
+
* const basis = pnlCostBasis({
|
|
2731
|
+
* depositedRaw,
|
|
2732
|
+
* withdrawnAssetsRaw,
|
|
2733
|
+
* hasDeposits: depositedRaw > BigInt(0),
|
|
2734
|
+
* mintsMatchHistory: historyCoversMints({
|
|
2735
|
+
* transfers,
|
|
2736
|
+
* wallet,
|
|
2737
|
+
* depositTransactionHashes,
|
|
2738
|
+
* }),
|
|
2739
|
+
* transfers: transferCostBasis({
|
|
2740
|
+
* transfers,
|
|
2741
|
+
* wallet,
|
|
2742
|
+
* vault,
|
|
2743
|
+
* history,
|
|
2744
|
+
* countSentWithoutReceipt: true,
|
|
2745
|
+
* }),
|
|
2746
|
+
* });
|
|
2747
|
+
* ```
|
|
2748
|
+
*/
|
|
2749
|
+
function pnlCostBasis({ depositedRaw, withdrawnAssetsRaw, hasDeposits, mintsMatchHistory, transfers, }) {
|
|
2750
|
+
if (hasDeposits && !mintsMatchHistory) {
|
|
2751
|
+
return {
|
|
2752
|
+
depositedRaw,
|
|
2753
|
+
withdrawnAssetsRaw,
|
|
2754
|
+
receivedByTransferRaw: BigInt(0),
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
const sentRaw = hasDeposits || transfers.receivedRaw > BigInt(0)
|
|
2758
|
+
? transfers.sentRaw
|
|
2759
|
+
: BigInt(0);
|
|
2760
|
+
return {
|
|
2761
|
+
depositedRaw: depositedRaw + transfers.receivedRaw,
|
|
2762
|
+
withdrawnAssetsRaw: withdrawnAssetsRaw + sentRaw,
|
|
2763
|
+
receivedByTransferRaw: transfers.receivedRaw,
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2485
2766
|
// One receipt-transfer subgraph query, plus one backend timeseries request when
|
|
2486
2767
|
// peer transfers exist. Any failure yields a zero basis, which leaves the
|
|
2487
2768
|
// getter exactly where it was without transfers (the history guard decides).
|
|
2488
|
-
async function fetchTransferCostBasis(vault, wallet, provider, headers) {
|
|
2769
|
+
async function fetchTransferCostBasis(vault, wallet, provider, headers, exclude = []) {
|
|
2489
2770
|
const none = { receivedRaw: BigInt(0), sentRaw: BigInt(0) };
|
|
2771
|
+
let shareTransfers = [];
|
|
2490
2772
|
try {
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
}), wallet, vault);
|
|
2773
|
+
shareTransfers = await (0, vaults_1.getSubgraphUserTransfers)(wallet, provider, vault, undefined, { receiptSubgraph: true });
|
|
2774
|
+
const transfers = selectPeerTransfers(shareTransfers, wallet, vault, exclude);
|
|
2494
2775
|
if (transfers.length === 0)
|
|
2495
|
-
return none;
|
|
2496
|
-
const
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2776
|
+
return { basis: none, shareTransfers };
|
|
2777
|
+
const data = await fetchShareRatios(vault, Math.min(...transfers.map((t) => Number(t.timestamp_))), headers);
|
|
2778
|
+
return {
|
|
2779
|
+
basis: transferCostBasis({
|
|
2780
|
+
transfers,
|
|
2781
|
+
wallet,
|
|
2782
|
+
vault,
|
|
2783
|
+
history: data,
|
|
2784
|
+
countSentWithoutReceipt: true,
|
|
2785
|
+
}),
|
|
2786
|
+
shareTransfers,
|
|
2787
|
+
};
|
|
2500
2788
|
}
|
|
2501
2789
|
catch (e) {
|
|
2502
2790
|
core_1.Logger.log.warn('getVaultUserLifetimePnl:transferCostBasis', `Failed to price shares received by transfer: ${e}`);
|
|
2503
|
-
return none;
|
|
2791
|
+
return { basis: none, shareTransfers };
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
// The vault's daily history from `since` (unix seconds) on. A vault whose
|
|
2795
|
+
// history stops before then (a closed vault) is read in full instead, so the
|
|
2796
|
+
// latest earlier day still prices it.
|
|
2797
|
+
async function fetchShareRatios(vault, since, headers) {
|
|
2798
|
+
const nDays = Math.ceil((Date.now() / 1000 - since) / 86_400) + 1;
|
|
2799
|
+
const { data } = await (0, core_1.fetchVaultHistoricalTimeseries)(vault, nDays, headers);
|
|
2800
|
+
const usable = Object.values(data ?? {}).some((day) => Number.isFinite(day.asset_share_ratio) &&
|
|
2801
|
+
(day.asset_share_ratio ?? 0) > 0);
|
|
2802
|
+
if (usable)
|
|
2803
|
+
return data;
|
|
2804
|
+
return (await (0, core_1.fetchVaultHistoricalTimeseries)(vault, 3650, headers)).data;
|
|
2805
|
+
}
|
|
2806
|
+
// Values a Tokenized Vault V2 wallet's redemption requests: one lagDuration()
|
|
2807
|
+
// read and one backend timeseries request. Undefined when either fails, so the
|
|
2808
|
+
// caller values every request at the current share price.
|
|
2809
|
+
async function readV2Redemptions(provider, vault, history, headers) {
|
|
2810
|
+
try {
|
|
2811
|
+
const contract = (0, core_1.createContract)({
|
|
2812
|
+
provider,
|
|
2813
|
+
address: vault,
|
|
2814
|
+
abi: TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2,
|
|
2815
|
+
});
|
|
2816
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2817
|
+
const earliest = Math.min(...history
|
|
2818
|
+
.filter((item) => item.type === 'withdraw-request')
|
|
2819
|
+
.map((item) => Number(item.timestamp_)));
|
|
2820
|
+
const [lagDuration, data] = await Promise.all([
|
|
2821
|
+
readLagDuration(contract, vault, 'getVaultUserLifetimePnl'),
|
|
2822
|
+
fetchShareRatios(vault, earliest, headers),
|
|
2823
|
+
]);
|
|
2824
|
+
return settleV2Redemptions({ history, lagDuration, ratios: data, now });
|
|
2825
|
+
}
|
|
2826
|
+
catch (e) {
|
|
2827
|
+
core_1.Logger.log.warn('getVaultUserLifetimePnl:v2Redemptions', `Failed to value redemption requests: ${e}`);
|
|
2828
|
+
return undefined;
|
|
2504
2829
|
}
|
|
2505
2830
|
}
|
|
2506
2831
|
/**
|
|
@@ -2524,13 +2849,39 @@ async function fetchTransferCostBasis(vault, wallet, provider, headers) {
|
|
|
2524
2849
|
* from the price server. There is no stand-in price: when none resolves the
|
|
2525
2850
|
* getter throws rather than returning USD values it cannot know.
|
|
2526
2851
|
*
|
|
2527
|
-
* Shares
|
|
2528
|
-
*
|
|
2529
|
-
*
|
|
2530
|
-
*
|
|
2531
|
-
*
|
|
2532
|
-
*
|
|
2533
|
-
*
|
|
2852
|
+
* Shares moved by transfer: peer transfers of the share token (see
|
|
2853
|
+
* {@link selectPeerTransfers}) are valued at the vault's daily share ratio
|
|
2854
|
+
* ({@link transferCostBasis}) and folded in by {@link pnlCostBasis} — received
|
|
2855
|
+
* shares add to `totalDeposited` (and are reported again as
|
|
2856
|
+
* `receivedByTransfer`), sent shares add to `totalWithdrawn`. This holds
|
|
2857
|
+
* alongside indexed deposits, so a wallet that received shares and also
|
|
2858
|
+
* deposited is not credited with the received shares as profit. LayerZero
|
|
2859
|
+
* OVaults count transfers only when history holds no deposit, because their
|
|
2860
|
+
* composer delivers cross-chain deposits by transfer.
|
|
2861
|
+
*
|
|
2862
|
+
* Gaps in the indexed history: a share mint with no `Deposit` event counts as
|
|
2863
|
+
* a deposit of the assets in its `ReserveDeposit` event
|
|
2864
|
+
* ({@link selectReserveDeposits}; High Growth ETH emits it for Kelp gain-adapter
|
|
2865
|
+
* deposits), and a withdrawal request the vault logged twice counts once
|
|
2866
|
+
* ({@link countWithdrawalRequestsOnce}). Transfers fold in for a depositor only
|
|
2867
|
+
* once every mint is explained.
|
|
2868
|
+
*
|
|
2869
|
+
* Tokenized Vault V2 redemptions carry only shares, so a settled one counts at
|
|
2870
|
+
* the assets the vault paid, or at the share ratio on its claim date when the
|
|
2871
|
+
* subgraph has no processed row for it ({@link settleV2Redemptions}). Only a
|
|
2872
|
+
* request not yet claimable is valued at the current share price. When the lag
|
|
2873
|
+
* or the ratio history cannot be read, every request is valued at the current
|
|
2874
|
+
* share price instead.
|
|
2875
|
+
*
|
|
2876
|
+
* Request cost: one receipt-transfer subgraph query (and a `symbol()` RPC read)
|
|
2877
|
+
* per call, plus one backend timeseries request when peer transfers exist and
|
|
2878
|
+
* one `reserveDeposits` subgraph query when a mint has no indexed deposit, and
|
|
2879
|
+
* for a Tokenized Vault V2 wallet with redemption requests, a `lagDuration()`
|
|
2880
|
+
* read plus one backend timeseries request. A timeseries window with no usable
|
|
2881
|
+
* ratio (a closed vault whose history stopped earlier) is read again in full,
|
|
2882
|
+
* so the latest earlier day still prices it. For
|
|
2883
|
+
* non-LayerZero vaults the transfer query runs in parallel with the history
|
|
2884
|
+
* read; for LayerZero vaults it runs only when history holds no deposit.
|
|
2534
2885
|
*
|
|
2535
2886
|
* @param vault - Vault contract address
|
|
2536
2887
|
* @param wallet - User wallet address
|
|
@@ -2554,6 +2905,17 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2554
2905
|
if (!(0, ethers_1.isAddress)(wallet))
|
|
2555
2906
|
throw new Error(`Wallet input parameter is not an address: ${String(wallet)}`);
|
|
2556
2907
|
const provider = (0, core_1.createProvider)(options.rpcUrl);
|
|
2908
|
+
const lzVaultKey = (0, deposits_1.isLayerZeroVault)(vault);
|
|
2909
|
+
const headers = options?.headers;
|
|
2910
|
+
// Peer transfers always count (see pnlCostBasis), so start that read
|
|
2911
|
+
// alongside the history instead of after it. A LayerZero OVault's hub
|
|
2912
|
+
// composer and share OFT are not peers.
|
|
2913
|
+
const transferCostBasisPromise = fetchTransferCostBasis(vault, wallet, provider, headers, lzVaultKey
|
|
2914
|
+
? [
|
|
2915
|
+
deposits_1.LAYERZERO_VAULTS[lzVaultKey].hubComposer,
|
|
2916
|
+
deposits_1.LAYERZERO_VAULTS[lzVaultKey].hubShareOFT,
|
|
2917
|
+
]
|
|
2918
|
+
: []);
|
|
2557
2919
|
// Parallelize independent operations
|
|
2558
2920
|
const [decimals, userHistory, positions, tokenizedVaultResult] = await Promise.all([
|
|
2559
2921
|
(0, core_1.getDecimals)(provider, vault),
|
|
@@ -2579,7 +2941,6 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2579
2941
|
throw new Error(`Failed to resolve decimals for vault ${vault}`);
|
|
2580
2942
|
}
|
|
2581
2943
|
// Fetch LayerZero deposits/redeems for supported vaults
|
|
2582
|
-
const lzVaultKey = (0, deposits_1.isLayerZeroVault)(vault);
|
|
2583
2944
|
let lzDeposits = [];
|
|
2584
2945
|
let lzRedeems = [];
|
|
2585
2946
|
if (lzVaultKey) {
|
|
@@ -2639,19 +3000,25 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2639
3000
|
}
|
|
2640
3001
|
return { underlyingTokenSymbol, underlyingAssetAddress };
|
|
2641
3002
|
})();
|
|
3003
|
+
const { basis: transfers, shareTransfers } = await transferCostBasisPromise;
|
|
2642
3004
|
// Process history - count all withdrawal requests (both processed and pending)
|
|
2643
|
-
// We don't count processed withdrawals separately to avoid double counting
|
|
3005
|
+
// We don't count processed withdrawals separately to avoid double counting,
|
|
3006
|
+
// except on Tokenized Vault V2 (below), whose requests carry only shares.
|
|
2644
3007
|
// Group deposit amounts by asset address to handle different decimals
|
|
2645
3008
|
const depositsByAsset = new Map();
|
|
2646
3009
|
// Separate withdrawal tracking: shares need conversion, assets don't
|
|
2647
3010
|
let totalWithdrawalsInShares = BigInt(0);
|
|
2648
3011
|
let totalWithdrawalsInAssets = BigInt(0);
|
|
2649
|
-
|
|
3012
|
+
let requestedShares = BigInt(0);
|
|
3013
|
+
// ignore bad transactions when calculating pnl
|
|
3014
|
+
const history = countWithdrawalRequestsOnce({
|
|
3015
|
+
history: userHistory,
|
|
3016
|
+
transfers: shareTransfers,
|
|
3017
|
+
wallet,
|
|
3018
|
+
vault,
|
|
3019
|
+
}).filter((item) => !(0, core_1.isBadTransaction)(item.transactionHash_, tokenizedVault.chain));
|
|
3020
|
+
for (const historyItem of history) {
|
|
2650
3021
|
const amount = BigInt(historyItem.amount);
|
|
2651
|
-
// ignore bad transactions when calculating pnl
|
|
2652
|
-
if ((0, core_1.isBadTransaction)(historyItem.transactionHash_, tokenizedVault.chain)) {
|
|
2653
|
-
continue;
|
|
2654
|
-
}
|
|
2655
3022
|
if (historyItem.type === 'deposit') {
|
|
2656
3023
|
// Use assetIn if available (V2 vaults), otherwise use a default key
|
|
2657
3024
|
// Use proper type guard instead of type assertion
|
|
@@ -2663,7 +3030,7 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2663
3030
|
}
|
|
2664
3031
|
else if (historyItem.type === 'withdraw-request') {
|
|
2665
3032
|
// Withdraw requests are in shares, need conversion to assets
|
|
2666
|
-
|
|
3033
|
+
requestedShares += amount;
|
|
2667
3034
|
}
|
|
2668
3035
|
else if (historyItem.type === 'withdraw-processed' &&
|
|
2669
3036
|
historyItem.isInstant) {
|
|
@@ -2671,6 +3038,18 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2671
3038
|
totalWithdrawalsInAssets += amount;
|
|
2672
3039
|
}
|
|
2673
3040
|
}
|
|
3041
|
+
// A V2 request is worth what the vault paid when it settled it, not the
|
|
3042
|
+
// current share price (see settleV2Redemptions).
|
|
3043
|
+
const v2Redemptions = version === 'evm-2' && requestedShares > BigInt(0)
|
|
3044
|
+
? await readV2Redemptions(provider, vault, history, headers)
|
|
3045
|
+
: undefined;
|
|
3046
|
+
if (v2Redemptions) {
|
|
3047
|
+
totalWithdrawalsInShares += v2Redemptions.pendingShares;
|
|
3048
|
+
totalWithdrawalsInAssets += v2Redemptions.settledAssetsRaw;
|
|
3049
|
+
}
|
|
3050
|
+
else {
|
|
3051
|
+
totalWithdrawalsInShares += requestedShares;
|
|
3052
|
+
}
|
|
2674
3053
|
// Include LayerZero cross-chain deposits and redeems
|
|
2675
3054
|
if (lzVaultKey && lzDeposits.length > 0) {
|
|
2676
3055
|
for (const lzDeposit of lzDeposits) {
|
|
@@ -2689,15 +3068,35 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2689
3068
|
totalWithdrawalsInShares += BigInt(lzRedeem.shareAmt);
|
|
2690
3069
|
}
|
|
2691
3070
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
3071
|
+
const depositTransactionHashes = userHistory
|
|
3072
|
+
.filter((item) => item.type === 'deposit')
|
|
3073
|
+
.map((item) => item.transactionHash_);
|
|
3074
|
+
let mintsMatchHistory = historyCoversMints({
|
|
3075
|
+
transfers: shareTransfers,
|
|
3076
|
+
wallet,
|
|
3077
|
+
depositTransactionHashes,
|
|
3078
|
+
});
|
|
3079
|
+
if (!mintsMatchHistory) {
|
|
3080
|
+
const reserveDeposits = selectReserveDeposits({
|
|
3081
|
+
reserveDeposits: await (0, vaults_1.getSubgraphUserReserveDeposits)(wallet, provider, vault),
|
|
3082
|
+
transfers: shareTransfers,
|
|
3083
|
+
wallet,
|
|
3084
|
+
depositTransactionHashes,
|
|
3085
|
+
}).filter((deposit) => !(0, core_1.isBadTransaction)(deposit.transactionHash_, tokenizedVault.chain));
|
|
3086
|
+
for (const deposit of reserveDeposits) {
|
|
3087
|
+
depositsByAsset.set('default', (depositsByAsset.get('default') ?? BigInt(0)) +
|
|
3088
|
+
BigInt(deposit.assets));
|
|
3089
|
+
}
|
|
3090
|
+
mintsMatchHistory = historyCoversMints({
|
|
3091
|
+
transfers: shareTransfers,
|
|
3092
|
+
wallet,
|
|
3093
|
+
depositTransactionHashes: [
|
|
3094
|
+
...depositTransactionHashes,
|
|
3095
|
+
...reserveDeposits.map((deposit) => deposit.transactionHash_),
|
|
3096
|
+
],
|
|
3097
|
+
});
|
|
2700
3098
|
}
|
|
3099
|
+
const hasDeposits = depositsByAsset.size > 0;
|
|
2701
3100
|
// Fetch decimals for each unique asset in parallel
|
|
2702
3101
|
const assetDecimals = new Map();
|
|
2703
3102
|
await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
|
|
@@ -2845,7 +3244,14 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2845
3244
|
// For non-evm-2 or when share price is 0, treat shares as assets
|
|
2846
3245
|
totalWithdrawnRaw += totalWithdrawalsInShares;
|
|
2847
3246
|
}
|
|
2848
|
-
const
|
|
3247
|
+
const costBasis = pnlCostBasis({
|
|
3248
|
+
depositedRaw: totalDepositedNativeRaw,
|
|
3249
|
+
withdrawnAssetsRaw: totalWithdrawnRaw,
|
|
3250
|
+
hasDeposits,
|
|
3251
|
+
mintsMatchHistory,
|
|
3252
|
+
transfers,
|
|
3253
|
+
});
|
|
3254
|
+
const totalWithdrawn = (0, core_1.toNormalizedBn)(costBasis.withdrawnAssetsRaw, decimals);
|
|
2849
3255
|
// Guard the 2026-07-09 over-inflation incident: a live position with no
|
|
2850
3256
|
// deposits *and* no withdrawals means the history failed to load, and the
|
|
2851
3257
|
// PnL formula would report the whole position as profit. Fail loudly with a
|
|
@@ -2853,8 +3259,8 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2853
3259
|
assertPnlHistoryConsistent({
|
|
2854
3260
|
vault,
|
|
2855
3261
|
wallet,
|
|
2856
|
-
hasDeposits:
|
|
2857
|
-
totalWithdrawnRaw,
|
|
3262
|
+
hasDeposits: hasDeposits || costBasis.receivedByTransferRaw > BigInt(0),
|
|
3263
|
+
totalWithdrawnRaw: costBasis.withdrawnAssetsRaw,
|
|
2858
3264
|
currentPositionRaw: BigInt(currentPositionValue.raw),
|
|
2859
3265
|
});
|
|
2860
3266
|
const tokenPriceUsd = assertPnlTokenPrice({
|
|
@@ -2867,8 +3273,10 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2867
3273
|
// computed above) — convert to USD with the same underlying price used
|
|
2868
3274
|
// for the other two legs below, so all three legs share one conversion
|
|
2869
3275
|
// factor instead of drifting independently against each other.
|
|
2870
|
-
const totalDeposited = (0, core_1.toNormalizedBn)(
|
|
3276
|
+
const totalDeposited = (0, core_1.toNormalizedBn)(costBasis.depositedRaw, decimals);
|
|
2871
3277
|
const totalDepositedUsd = Number(totalDeposited.normalized) * tokenPriceUsd;
|
|
3278
|
+
const receivedByTransfer = (0, core_1.toNormalizedBn)(costBasis.receivedByTransferRaw, decimals);
|
|
3279
|
+
const receivedByTransferUsd = Number(receivedByTransfer.normalized) * tokenPriceUsd;
|
|
2872
3280
|
const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * tokenPriceUsd;
|
|
2873
3281
|
const currentPositionValueUsd = Number(currentPositionValue.normalized) * tokenPriceUsd;
|
|
2874
3282
|
// Calculate lifetimePnl in USD first (since deposits may be in different tokens with different prices)
|
|
@@ -2896,6 +3304,8 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2896
3304
|
lifetimePnl,
|
|
2897
3305
|
lifetimePnlUsd,
|
|
2898
3306
|
pnlPercentage,
|
|
3307
|
+
receivedByTransfer,
|
|
3308
|
+
receivedByTransferUsd,
|
|
2899
3309
|
};
|
|
2900
3310
|
}
|
|
2901
3311
|
catch (e) {
|