@augustdigital/sdk 9.5.0 → 9.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/lib/adapters/evm/index.d.ts +1 -1
  3. package/lib/adapters/evm/index.js +29 -29
  4. package/lib/adapters/solana/types.d.ts +8 -0
  5. package/lib/adapters/solana/vault.actions.d.ts +6 -1
  6. package/lib/adapters/solana/vault.actions.js +99 -21
  7. package/lib/core/analytics/app-name.js +1 -1
  8. package/lib/core/analytics/constants.d.ts +10 -0
  9. package/lib/core/analytics/constants.js +17 -1
  10. package/lib/core/analytics/index.d.ts +7 -4
  11. package/lib/core/analytics/index.js +8 -4
  12. package/lib/core/analytics/instrumentation.js +8 -4
  13. package/lib/core/analytics/method-taxonomy.js +13 -0
  14. package/lib/core/analytics/metrics.d.ts +25 -10
  15. package/lib/core/analytics/metrics.js +41 -15
  16. package/lib/core/analytics/sentry.d.ts +24 -3
  17. package/lib/core/analytics/sentry.js +49 -18
  18. package/lib/core/analytics/types.d.ts +11 -4
  19. package/lib/core/analytics/user-identity.d.ts +19 -5
  20. package/lib/core/analytics/user-identity.js +35 -21
  21. package/lib/core/analytics/version.d.ts +1 -1
  22. package/lib/core/analytics/version.js +1 -1
  23. package/lib/core/auth/verify.js +7 -3
  24. package/lib/core/base.class.d.ts +8 -1
  25. package/lib/core/base.class.js +7 -1
  26. package/lib/core/constants/core.d.ts +3 -0
  27. package/lib/core/constants/core.js +3 -0
  28. package/lib/core/constants/swap-router.d.ts +16 -0
  29. package/lib/core/constants/swap-router.js +16 -0
  30. package/lib/core/errors/index.d.ts +36 -1
  31. package/lib/core/errors/index.js +40 -1
  32. package/lib/core/fetcher.d.ts +24 -5
  33. package/lib/core/fetcher.js +43 -7
  34. package/lib/main.d.ts +6 -0
  35. package/lib/main.js +6 -0
  36. package/lib/modules/api/main.d.ts +6 -6
  37. package/lib/modules/api/main.js +6 -6
  38. package/lib/modules/sub-accounts/main.d.ts +7 -1
  39. package/lib/modules/sub-accounts/main.js +7 -1
  40. package/lib/modules/vaults/getters.d.ts +151 -10
  41. package/lib/modules/vaults/getters.js +226 -30
  42. package/lib/modules/vaults/main.d.ts +5 -0
  43. package/lib/modules/vaults/main.js +5 -0
  44. package/lib/modules/vaults/write.actions.d.ts +57 -1
  45. package/lib/modules/vaults/write.actions.js +86 -2
  46. package/lib/sdk.d.ts +13171 -12810
  47. package/lib/services/debank/fetcher.js +1 -1
  48. package/lib/services/subgraph/vaults.d.ts +18 -1
  49. package/lib/services/subgraph/vaults.js +19 -3
  50. package/lib/types/vaults.d.ts +5 -0
  51. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { type IAddress, type IHistoricalTimeseriesResponse, type INormalizedNumber, type ITokenizedVault, type ISubgraphWithdrawProccessed, type IVault, type IVaultAllocations, type IVaultAnnualizedApy, type IVaultAvailableRedemption, type IVaultLoan, type IVaultPosition, type IVaultRedemptionHistoryItem, type IVaultSummary, type IVaultWithdrawals, type IVaultPendingRedemptions, type IActiveStakingPosition, type IVaultHistoricalParams, type IVaultUserLifetimePnl, type IVaultBorrowerHealthFactor, type IVaultPnl, type VaultAddress } from '../../types';
1
+ import { type IAddress, type IHistoricalTimeseriesPoint, type IHistoricalTimeseriesResponse, type INormalizedNumber, type ISubgraphTransfer, type ITokenizedVault, type ISubgraphWithdrawProccessed, type IVault, type IVaultAllocations, type IVaultAnnualizedApy, type IVaultAvailableRedemption, type IVaultLoan, type IVaultPosition, type IVaultRedemptionHistoryItem, type IVaultSummary, type IVaultWithdrawals, type IVaultPendingRedemptions, type IActiveStakingPosition, type IVaultHistoricalParams, type IVaultUserLifetimePnl, type IVaultBorrowerHealthFactor, type IVaultPnl, type VaultAddress } from '../../types';
2
2
  import type { IVaultBaseOptions } from './types';
3
3
  /**
4
4
  * Vault Data Getters
@@ -337,15 +337,10 @@ export interface PnlHistoryConsistencyParams {
337
337
  * Pure and synchronous — no RPC, no I/O — so it is safe to call inside the hot
338
338
  * path with no added latency.
339
339
  *
340
- * Known imprecision — vault shares are transferable ERC-20 receipt tokens, so a
341
- * wallet that *received* shares via a direct token transfer (never deposited,
342
- * never withdrew) legitimately has `position > 0` with no history records and
343
- * will throw `HISTORY_UNAVAILABLE` even though its history loaded fine. This is
344
- * not a regression — the pre-guard formula reported that same position as 100%
345
- * profit, which was also wrong — but the error message is misleading for that
346
- * (rare) path. If receipt-token transfer becomes a real user flow, distinguish
347
- * it here (e.g. by checking for a share-transfer event) rather than widening
348
- * the guard.
340
+ * Shares received by a direct token transfer (never deposited, never withdrew)
341
+ * reach this guard as deposits: {@link getVaultUserLifetimePnl} prices them
342
+ * with {@link transferCostBasis} first. The guard still throws for such a
343
+ * wallet when its transfers cannot be read or priced.
349
344
  *
350
345
  * @param params - See {@link PnlHistoryConsistencyParams}.
351
346
  * @returns `void` when the history is consistent.
@@ -366,6 +361,53 @@ export interface PnlHistoryConsistencyParams {
366
361
  * ```
367
362
  */
368
363
  export declare function assertPnlHistoryConsistent({ vault, wallet, hasDeposits, totalWithdrawnRaw, currentPositionRaw, }: PnlHistoryConsistencyParams): void;
364
+ /** Inputs the lifetime-PnL price guard inspects. */
365
+ export interface PnlTokenPriceParams {
366
+ /** Vault contract address, used for the error message and context. */
367
+ vault: IAddress;
368
+ /** User wallet address, used for the error message and context. */
369
+ wallet: IAddress;
370
+ /**
371
+ * The vault's underlying asset (`asset()`) that was priced, or `undefined`
372
+ * when `asset()` could not be read and no price lookup was possible.
373
+ */
374
+ asset: IAddress | undefined;
375
+ /**
376
+ * USD price of `asset` as resolved by the price server. `0` means the lookup
377
+ * failed or the token is unknown to it (`fetchTokenPriceByAddress` never
378
+ * throws).
379
+ */
380
+ tokenPriceUsd: number;
381
+ }
382
+ /**
383
+ * Guard against converting lifetime PnL to USD with a fabricated price.
384
+ *
385
+ * Every USD field `getVaultUserLifetimePnl` returns is a native amount times
386
+ * the underlying asset's USD price. When the price server has no price, the
387
+ * only honest outcome is "no USD value" — substituting a stand-in (the getter
388
+ * previously used `1`) reports a non-dollar vault's position at $1 per asset,
389
+ * a plausible-looking but wrong number that callers cannot detect.
390
+ *
391
+ * Pure and synchronous — no RPC, no I/O.
392
+ *
393
+ * @param params - See {@link PnlTokenPriceParams}.
394
+ * @returns `tokenPriceUsd`, unchanged, when it is a finite number above zero.
395
+ * @throws {@link AugustPriceUnavailableError} with code `PRICE_UNAVAILABLE`
396
+ * and `context: { vault, wallet, asset }` when the price is zero, negative,
397
+ * or not finite. The message carries the
398
+ * `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
399
+ * other errors leaving the getter.
400
+ * @example
401
+ * ```ts
402
+ * const price = assertPnlTokenPrice({
403
+ * vault,
404
+ * wallet,
405
+ * asset: underlyingAssetAddress,
406
+ * tokenPriceUsd: await pricePromise,
407
+ * });
408
+ * ```
409
+ */
410
+ export declare function assertPnlTokenPrice({ vault, wallet, asset, tokenPriceUsd, }: PnlTokenPriceParams): number;
369
411
  /**
370
412
  * Sum a wallet's per-asset deposits into the vault's own decimals, at face
371
413
  * value — 1 unit of any deposited asset is treated as 1 unit of the vault's
@@ -391,6 +433,88 @@ export declare function assertPnlHistoryConsistent({ vault, wallet, hasDeposits,
391
433
  * @returns Total deposited, in raw base units at `vaultDecimals`.
392
434
  */
393
435
  export declare function sumDepositsAtFaceValue(depositsByAsset: Map<string, bigint>, assetDecimals: Map<string, number>, vaultDecimals: number): bigint;
436
+ /** The receipt-token transfer fields the transfer cost basis reads. */
437
+ export type PnlTransfer = Pick<ISubgraphTransfer, 'from' | 'to' | 'value' | 'timestamp_'>;
438
+ /**
439
+ * Keep the receipt-token transfers that moved shares between `wallet` and
440
+ * another holder.
441
+ *
442
+ * Vault shares are transferable ERC-20 tokens, so a wallet can hold a position
443
+ * it never deposited into — a migration airdrop, a DEX purchase, a transfer from
444
+ * another wallet. Mints (from the zero address), burns (to it) and transfers to
445
+ * or from the vault itself are dropped: those are the deposit and withdrawal
446
+ * legs the subgraph history already records, and counting them again would
447
+ * double the cost basis.
448
+ *
449
+ * @param transfers - Receipt-token transfers touching `wallet`, as returned by
450
+ * the vault's transfer subgraph.
451
+ * @param wallet - The wallet whose position is being valued.
452
+ * @param vault - The vault contract address.
453
+ * @returns The peer-to-peer transfers, in input order. Addresses compare
454
+ * case-insensitively.
455
+ * @example
456
+ * ```ts
457
+ * const peer = selectPeerTransfers(transfers, wallet, vault);
458
+ * ```
459
+ */
460
+ export declare function selectPeerTransfers<T extends PnlTransfer>(transfers: T[], wallet: IAddress, vault: IAddress): T[];
461
+ /** Inputs {@link transferCostBasis} prices. */
462
+ export interface TransferCostBasisParams {
463
+ /** Peer transfers touching `wallet` — see {@link selectPeerTransfers}. */
464
+ transfers: PnlTransfer[];
465
+ /** The wallet whose cost basis is being computed. */
466
+ wallet: IAddress;
467
+ /** The vault contract address, used for the error message and context. */
468
+ vault: IAddress;
469
+ /**
470
+ * The vault's daily history from `fetchVaultHistoricalTimeseries`, keyed by
471
+ * UTC date (`YYYY-MM-DD`). Only `asset_share_ratio` (assets per share) is
472
+ * read.
473
+ */
474
+ history: Record<string, Pick<IHistoricalTimeseriesPoint, 'asset_share_ratio'>>;
475
+ }
476
+ /** Asset amounts that peer transfers moved into and out of a position. */
477
+ export interface TransferCostBasis {
478
+ /** Assets the received shares were worth on arrival, raw at vault decimals. */
479
+ receivedRaw: bigint;
480
+ /** Assets the sent shares were worth when they left, raw at vault decimals. */
481
+ sentRaw: bigint;
482
+ }
483
+ /**
484
+ * Value peer share transfers in the vault's asset, at the vault's share ratio
485
+ * on the day each one happened.
486
+ *
487
+ * Used by {@link getVaultUserLifetimePnl} when a wallet has no indexed
488
+ * deposits: received shares become its cost basis and sent shares count as
489
+ * withdrawn, so lifetime PnL for a transfer recipient reads "PnL since the
490
+ * shares arrived" rather than failing or reporting the whole position as
491
+ * profit. A day with no usable ratio takes the latest earlier day's, and a
492
+ * transfer older than the history takes the earliest day's.
493
+ *
494
+ * Sent shares count only against a received basis. A wallet that sent shares
495
+ * but received none gets a zero basis, so the lifetime-PnL history guard still
496
+ * decides for it instead of reporting the sent shares as profit.
497
+ *
498
+ * Pure and synchronous. Assumes, as the getter does, that shares and assets
499
+ * share the vault's decimals.
500
+ *
501
+ * @param params - See {@link TransferCostBasisParams}.
502
+ * @returns The received and sent asset amounts, raw at the vault's decimals;
503
+ * both zero when no shares were received.
504
+ * @throws {@link AugustHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when no
505
+ * day in `history` carries a positive, finite `asset_share_ratio`.
506
+ * @example
507
+ * ```ts
508
+ * const { data } = await fetchVaultHistoricalTimeseries(vault, nDays);
509
+ * const { receivedRaw, sentRaw } = transferCostBasis({
510
+ * transfers: selectPeerTransfers(transfers, wallet, vault),
511
+ * wallet,
512
+ * vault,
513
+ * history: data,
514
+ * });
515
+ * ```
516
+ */
517
+ export declare function transferCostBasis({ transfers, wallet, vault, history, }: TransferCostBasisParams): TransferCostBasis;
394
518
  /**
395
519
  * Calculate lifetime PnL for a user in a specific vault.
396
520
  *
@@ -408,10 +532,27 @@ export declare function sumDepositsAtFaceValue(depositsByAsset: Map<string, bigi
408
532
  * non-pegged deposit asset would need deposit-time price conversion, which
409
533
  * this function does not perform.
410
534
  *
535
+ * Every USD field is the native amount times the underlying asset's USD price
536
+ * from the price server. There is no stand-in price: when none resolves the
537
+ * getter throws rather than returning USD values it cannot know.
538
+ *
539
+ * Shares received by transfer: when history has no deposit, peer transfers of
540
+ * the share token (see {@link selectPeerTransfers}) are valued at the vault's
541
+ * daily share ratio ({@link transferCostBasis}) — received shares become
542
+ * `totalDeposited`, sent shares add to `totalWithdrawn`. PnL for such a wallet
543
+ * therefore runs from the day its shares arrived. This costs one extra
544
+ * subgraph query, plus one backend timeseries request when transfers exist,
545
+ * and only for wallets with no indexed deposit.
546
+ *
411
547
  * @param vault - Vault contract address
412
548
  * @param wallet - User wallet address
413
549
  * @param options - RPC configuration and service options
414
550
  * @returns Lifetime PnL data in both native token and USD
551
+ * @throws {@link AugustHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when a
552
+ * live position has no indexed deposits, withdrawals or priceable share
553
+ * transfers.
554
+ * @throws {@link AugustPriceUnavailableError} (`PRICE_UNAVAILABLE`) when the
555
+ * underlying asset has no USD price, or `asset()` cannot be read.
415
556
  */
416
557
  export declare function getVaultUserLifetimePnl({ vault, wallet, options, }: {
417
558
  vault: IAddress;
@@ -54,7 +54,10 @@ exports.registerUserForPoints = registerUserForPoints;
54
54
  exports.fetchPointsLeaderboard = fetchPointsLeaderboard;
55
55
  exports.getYieldLastRealizedOn = getYieldLastRealizedOn;
56
56
  exports.assertPnlHistoryConsistent = assertPnlHistoryConsistent;
57
+ exports.assertPnlTokenPrice = assertPnlTokenPrice;
57
58
  exports.sumDepositsAtFaceValue = sumDepositsAtFaceValue;
59
+ exports.selectPeerTransfers = selectPeerTransfers;
60
+ exports.transferCostBasis = transferCostBasis;
58
61
  exports.getVaultUserLifetimePnl = getVaultUserLifetimePnl;
59
62
  exports.getVaultPnl = getVaultPnl;
60
63
  exports.getVaultHistoricalTimeseries = getVaultHistoricalTimeseries;
@@ -1540,13 +1543,26 @@ async function getVaultPositions({ vault, wallet, solanaWallet, stellarWallet, o
1540
1543
  }
1541
1544
  }
1542
1545
  const balance = (0, core_1.toNormalizedBn)(bal, decimals);
1543
- // check available redemptions
1544
- const { availableRedemptions, pendingRedemptions } = await getVaultAvailableRedemptions({
1545
- vault: v.address,
1546
- wallet,
1547
- options,
1548
- prefetchedReads: prefetched,
1549
- });
1546
+ // Redemption requests are keyed by receiver, so without an EVM
1547
+ // wallet there is nothing of the caller's to find. Skip the read
1548
+ // rather than delegate: the wallet-less path of
1549
+ // `getVaultAvailableRedemptions` lists EVERY receiver's processed
1550
+ // requests for the vault, which in a positions read surfaced other
1551
+ // users' withdrawals as the caller's and flipped the status to
1552
+ // REDEEM for every EVM vault in a Stellar/Solana-only session
1553
+ // (AUGUST-7938). Skipping also saves one subgraph query per EVM
1554
+ // vault for those callers.
1555
+ const { availableRedemptions, pendingRedemptions } = wallet && (0, ethers_1.isAddress)(wallet)
1556
+ ? await getVaultAvailableRedemptions({
1557
+ vault: v.address,
1558
+ wallet,
1559
+ options,
1560
+ prefetchedReads: prefetched,
1561
+ })
1562
+ : {
1563
+ availableRedemptions: [],
1564
+ pendingRedemptions: [],
1565
+ };
1550
1566
  // get aggregate redemption amount
1551
1567
  const aggregateAvailableRedemptions = availableRedemptions.reduce((acc, curr) => acc + BigInt(curr.amount.raw), BigInt(0));
1552
1568
  // return obj
@@ -2260,15 +2276,10 @@ async function getYieldLastRealizedOn({ vault, options, }) {
2260
2276
  * Pure and synchronous — no RPC, no I/O — so it is safe to call inside the hot
2261
2277
  * path with no added latency.
2262
2278
  *
2263
- * Known imprecision — vault shares are transferable ERC-20 receipt tokens, so a
2264
- * wallet that *received* shares via a direct token transfer (never deposited,
2265
- * never withdrew) legitimately has `position > 0` with no history records and
2266
- * will throw `HISTORY_UNAVAILABLE` even though its history loaded fine. This is
2267
- * not a regression — the pre-guard formula reported that same position as 100%
2268
- * profit, which was also wrong — but the error message is misleading for that
2269
- * (rare) path. If receipt-token transfer becomes a real user flow, distinguish
2270
- * it here (e.g. by checking for a share-transfer event) rather than widening
2271
- * the guard.
2279
+ * Shares received by a direct token transfer (never deposited, never withdrew)
2280
+ * reach this guard as deposits: {@link getVaultUserLifetimePnl} prices them
2281
+ * with {@link transferCostBasis} first. The guard still throws for such a
2282
+ * wallet when its transfers cannot be read or priced.
2272
2283
  *
2273
2284
  * @param params - See {@link PnlHistoryConsistencyParams}.
2274
2285
  * @returns `void` when the history is consistent.
@@ -2295,6 +2306,39 @@ function assertPnlHistoryConsistent({ vault, wallet, hasDeposits, totalWithdrawn
2295
2306
  throw new errors_1.AugustHistoryUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:transaction history unavailable — a live position exists but no deposits or withdrawals were indexed, so lifetime PnL cannot be computed without over-reporting the entire position as profit`, { context: { vault, wallet } });
2296
2307
  }
2297
2308
  }
2309
+ /**
2310
+ * Guard against converting lifetime PnL to USD with a fabricated price.
2311
+ *
2312
+ * Every USD field `getVaultUserLifetimePnl` returns is a native amount times
2313
+ * the underlying asset's USD price. When the price server has no price, the
2314
+ * only honest outcome is "no USD value" — substituting a stand-in (the getter
2315
+ * previously used `1`) reports a non-dollar vault's position at $1 per asset,
2316
+ * a plausible-looking but wrong number that callers cannot detect.
2317
+ *
2318
+ * Pure and synchronous — no RPC, no I/O.
2319
+ *
2320
+ * @param params - See {@link PnlTokenPriceParams}.
2321
+ * @returns `tokenPriceUsd`, unchanged, when it is a finite number above zero.
2322
+ * @throws {@link AugustPriceUnavailableError} with code `PRICE_UNAVAILABLE`
2323
+ * and `context: { vault, wallet, asset }` when the price is zero, negative,
2324
+ * or not finite. The message carries the
2325
+ * `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
2326
+ * other errors leaving the getter.
2327
+ * @example
2328
+ * ```ts
2329
+ * const price = assertPnlTokenPrice({
2330
+ * vault,
2331
+ * wallet,
2332
+ * asset: underlyingAssetAddress,
2333
+ * tokenPriceUsd: await pricePromise,
2334
+ * });
2335
+ * ```
2336
+ */
2337
+ function assertPnlTokenPrice({ vault, wallet, asset, tokenPriceUsd, }) {
2338
+ if (Number.isFinite(tokenPriceUsd) && tokenPriceUsd > 0)
2339
+ return tokenPriceUsd;
2340
+ throw new errors_1.AugustPriceUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:underlying token price unavailable for ${asset ?? 'an unreadable asset()'} — USD values cannot be computed without fabricating a price`, { context: { vault, wallet, asset: asset ?? null } });
2341
+ }
2298
2342
  /**
2299
2343
  * Sum a wallet's per-asset deposits into the vault's own decimals, at face
2300
2344
  * value — 1 unit of any deposited asset is treated as 1 unit of the vault's
@@ -2335,6 +2379,130 @@ function sumDepositsAtFaceValue(depositsByAsset, assetDecimals, vaultDecimals) {
2335
2379
  }
2336
2380
  return total;
2337
2381
  }
2382
+ /**
2383
+ * Keep the receipt-token transfers that moved shares between `wallet` and
2384
+ * another holder.
2385
+ *
2386
+ * Vault shares are transferable ERC-20 tokens, so a wallet can hold a position
2387
+ * it never deposited into — a migration airdrop, a DEX purchase, a transfer from
2388
+ * another wallet. Mints (from the zero address), burns (to it) and transfers to
2389
+ * or from the vault itself are dropped: those are the deposit and withdrawal
2390
+ * legs the subgraph history already records, and counting them again would
2391
+ * double the cost basis.
2392
+ *
2393
+ * @param transfers - Receipt-token transfers touching `wallet`, as returned by
2394
+ * the vault's transfer subgraph.
2395
+ * @param wallet - The wallet whose position is being valued.
2396
+ * @param vault - The vault contract address.
2397
+ * @returns The peer-to-peer transfers, in input order. Addresses compare
2398
+ * case-insensitively.
2399
+ * @example
2400
+ * ```ts
2401
+ * const peer = selectPeerTransfers(transfers, wallet, vault);
2402
+ * ```
2403
+ */
2404
+ function selectPeerTransfers(transfers, wallet, vault) {
2405
+ const self = wallet.toLowerCase();
2406
+ const excluded = new Set([ethers_1.ZeroAddress.toLowerCase(), vault.toLowerCase()]);
2407
+ return transfers.filter((transfer) => {
2408
+ const from = transfer.from.toLowerCase();
2409
+ const to = transfer.to.toLowerCase();
2410
+ if (from === to || excluded.has(from) || excluded.has(to))
2411
+ return false;
2412
+ return from === self || to === self;
2413
+ });
2414
+ }
2415
+ /**
2416
+ * Value peer share transfers in the vault's asset, at the vault's share ratio
2417
+ * on the day each one happened.
2418
+ *
2419
+ * Used by {@link getVaultUserLifetimePnl} when a wallet has no indexed
2420
+ * deposits: received shares become its cost basis and sent shares count as
2421
+ * withdrawn, so lifetime PnL for a transfer recipient reads "PnL since the
2422
+ * shares arrived" rather than failing or reporting the whole position as
2423
+ * profit. A day with no usable ratio takes the latest earlier day's, and a
2424
+ * transfer older than the history takes the earliest day's.
2425
+ *
2426
+ * Sent shares count only against a received basis. A wallet that sent shares
2427
+ * but received none gets a zero basis, so the lifetime-PnL history guard still
2428
+ * decides for it instead of reporting the sent shares as profit.
2429
+ *
2430
+ * Pure and synchronous. Assumes, as the getter does, that shares and assets
2431
+ * share the vault's decimals.
2432
+ *
2433
+ * @param params - See {@link TransferCostBasisParams}.
2434
+ * @returns The received and sent asset amounts, raw at the vault's decimals;
2435
+ * both zero when no shares were received.
2436
+ * @throws {@link AugustHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when no
2437
+ * day in `history` carries a positive, finite `asset_share_ratio`.
2438
+ * @example
2439
+ * ```ts
2440
+ * const { data } = await fetchVaultHistoricalTimeseries(vault, nDays);
2441
+ * const { receivedRaw, sentRaw } = transferCostBasis({
2442
+ * transfers: selectPeerTransfers(transfers, wallet, vault),
2443
+ * wallet,
2444
+ * vault,
2445
+ * history: data,
2446
+ * });
2447
+ * ```
2448
+ */
2449
+ function transferCostBasis({ transfers, wallet, vault, history, }) {
2450
+ const ratios = Object.entries(history)
2451
+ .filter(([, day]) => Number.isFinite(day.asset_share_ratio) &&
2452
+ (day.asset_share_ratio ?? 0) > 0)
2453
+ .map(([date, day]) => ({ date, ratio: day.asset_share_ratio }))
2454
+ .sort((a, b) => a.date.localeCompare(b.date));
2455
+ if (ratios.length === 0) {
2456
+ throw new errors_1.AugustHistoryUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:no share ratio history to price shares received by transfer`, { context: { vault, wallet } });
2457
+ }
2458
+ const scale = BigInt(10) ** BigInt(18);
2459
+ const assetsAt = (shares, timestamp) => {
2460
+ const date = new Date(Number(timestamp) * 1000).toISOString().slice(0, 10);
2461
+ let ratio = ratios[0].ratio;
2462
+ for (const entry of ratios) {
2463
+ if (entry.date > date)
2464
+ break;
2465
+ ratio = entry.ratio;
2466
+ }
2467
+ return (shares * BigInt(Math.round(ratio * 1e18))) / scale;
2468
+ };
2469
+ const self = wallet.toLowerCase();
2470
+ let receivedRaw = BigInt(0);
2471
+ let sentRaw = BigInt(0);
2472
+ for (const transfer of transfers) {
2473
+ const shares = BigInt(transfer.value);
2474
+ if (transfer.to.toLowerCase() === self) {
2475
+ receivedRaw += assetsAt(shares, transfer.timestamp_);
2476
+ }
2477
+ else if (transfer.from.toLowerCase() === self) {
2478
+ sentRaw += assetsAt(shares, transfer.timestamp_);
2479
+ }
2480
+ }
2481
+ if (receivedRaw === BigInt(0))
2482
+ return { receivedRaw, sentRaw: BigInt(0) };
2483
+ return { receivedRaw, sentRaw };
2484
+ }
2485
+ // One receipt-transfer subgraph query, plus one backend timeseries request when
2486
+ // peer transfers exist. Any failure yields a zero basis, which leaves the
2487
+ // getter exactly where it was without transfers (the history guard decides).
2488
+ async function fetchTransferCostBasis(vault, wallet, provider, headers) {
2489
+ const none = { receivedRaw: BigInt(0), sentRaw: BigInt(0) };
2490
+ try {
2491
+ const transfers = selectPeerTransfers(await (0, vaults_1.getSubgraphUserTransfers)(wallet, provider, vault, undefined, {
2492
+ receiptSubgraph: true,
2493
+ }), wallet, vault);
2494
+ if (transfers.length === 0)
2495
+ return none;
2496
+ const earliest = Math.min(...transfers.map((t) => Number(t.timestamp_)));
2497
+ const nDays = Math.ceil((Date.now() / 1000 - earliest) / 86_400) + 1;
2498
+ const { data } = await (0, core_1.fetchVaultHistoricalTimeseries)(vault, nDays, headers);
2499
+ return transferCostBasis({ transfers, wallet, vault, history: data });
2500
+ }
2501
+ catch (e) {
2502
+ core_1.Logger.log.warn('getVaultUserLifetimePnl:transferCostBasis', `Failed to price shares received by transfer: ${e}`);
2503
+ return none;
2504
+ }
2505
+ }
2338
2506
  /**
2339
2507
  * Calculate lifetime PnL for a user in a specific vault.
2340
2508
  *
@@ -2352,10 +2520,27 @@ function sumDepositsAtFaceValue(depositsByAsset, assetDecimals, vaultDecimals) {
2352
2520
  * non-pegged deposit asset would need deposit-time price conversion, which
2353
2521
  * this function does not perform.
2354
2522
  *
2523
+ * Every USD field is the native amount times the underlying asset's USD price
2524
+ * from the price server. There is no stand-in price: when none resolves the
2525
+ * getter throws rather than returning USD values it cannot know.
2526
+ *
2527
+ * Shares received by transfer: when history has no deposit, peer transfers of
2528
+ * the share token (see {@link selectPeerTransfers}) are valued at the vault's
2529
+ * daily share ratio ({@link transferCostBasis}) — received shares become
2530
+ * `totalDeposited`, sent shares add to `totalWithdrawn`. PnL for such a wallet
2531
+ * therefore runs from the day its shares arrived. This costs one extra
2532
+ * subgraph query, plus one backend timeseries request when transfers exist,
2533
+ * and only for wallets with no indexed deposit.
2534
+ *
2355
2535
  * @param vault - Vault contract address
2356
2536
  * @param wallet - User wallet address
2357
2537
  * @param options - RPC configuration and service options
2358
2538
  * @returns Lifetime PnL data in both native token and USD
2539
+ * @throws {@link AugustHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when a
2540
+ * live position has no indexed deposits, withdrawals or priceable share
2541
+ * transfers.
2542
+ * @throws {@link AugustPriceUnavailableError} (`PRICE_UNAVAILABLE`) when the
2543
+ * underlying asset has no USD price, or `asset()` cannot be read.
2359
2544
  */
2360
2545
  async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
2361
2546
  try {
@@ -2504,6 +2689,15 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
2504
2689
  totalWithdrawalsInShares += BigInt(lzRedeem.shareAmt);
2505
2690
  }
2506
2691
  }
2692
+ // A wallet can hold shares it never deposited (a migration airdrop, a DEX
2693
+ // purchase). Only when history has no deposit at all do received shares
2694
+ // become the cost basis, so a depositor's PnL is computed as before.
2695
+ if (depositsByAsset.size === 0) {
2696
+ const { receivedRaw, sentRaw } = await fetchTransferCostBasis(vault, wallet, provider, options?.headers);
2697
+ if (receivedRaw > BigInt(0))
2698
+ depositsByAsset.set('default', receivedRaw);
2699
+ totalWithdrawalsInAssets += sentRaw;
2700
+ }
2507
2701
  // Fetch decimals for each unique asset in parallel
2508
2702
  const assetDecimals = new Map();
2509
2703
  await Promise.all(Array.from(depositsByAsset.keys()).map(async (assetAddress) => {
@@ -2663,25 +2857,25 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
2663
2857
  totalWithdrawnRaw,
2664
2858
  currentPositionRaw: BigInt(currentPositionValue.raw),
2665
2859
  });
2666
- const tokenPriceUsd = await pricePromise;
2667
- // Validate token price and log warning if invalid
2668
- const effectiveTokenPrice = tokenPriceUsd > 0 ? tokenPriceUsd : 1;
2669
- if (tokenPriceUsd <= 0) {
2670
- core_1.Logger.log.warn('getVaultUserLifetimePnl:tokenPrice', `Invalid underlying token price: ${tokenPriceUsd}, using fallback of 1`);
2671
- }
2860
+ const tokenPriceUsd = assertPnlTokenPrice({
2861
+ vault,
2862
+ wallet,
2863
+ asset: underlyingAssetAddress,
2864
+ tokenPriceUsd: await pricePromise,
2865
+ });
2672
2866
  // totalDeposited is already in the vault's own decimals (face value,
2673
2867
  // computed above) — convert to USD with the same underlying price used
2674
2868
  // for the other two legs below, so all three legs share one conversion
2675
2869
  // factor instead of drifting independently against each other.
2676
2870
  const totalDeposited = (0, core_1.toNormalizedBn)(totalDepositedNativeRaw, decimals);
2677
- const totalDepositedUsd = Number(totalDeposited.normalized) * effectiveTokenPrice;
2678
- const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * effectiveTokenPrice;
2679
- const currentPositionValueUsd = Number(currentPositionValue.normalized) * effectiveTokenPrice;
2871
+ const totalDepositedUsd = Number(totalDeposited.normalized) * tokenPriceUsd;
2872
+ const totalWithdrawnUsd = Number(totalWithdrawn.normalized) * tokenPriceUsd;
2873
+ const currentPositionValueUsd = Number(currentPositionValue.normalized) * tokenPriceUsd;
2680
2874
  // Calculate lifetimePnl in USD first (since deposits may be in different tokens with different prices)
2681
2875
  const lifetimePnlUsd = currentPositionValueUsd + totalWithdrawnUsd - totalDepositedUsd;
2682
2876
  // Use BigInt-scaled arithmetic to minimize precision loss when converting USD to native
2683
2877
  const scaleFactor = BigInt(10 ** 18); // High precision scale
2684
- const tokenPriceScaled = BigInt(Math.round(effectiveTokenPrice * Number(scaleFactor)));
2878
+ const tokenPriceScaled = BigInt(Math.round(tokenPriceUsd * Number(scaleFactor)));
2685
2879
  // Convert lifetimePnlUsd to native units
2686
2880
  const lifetimePnlUsdScaled = BigInt(Math.round(lifetimePnlUsd * Number(scaleFactor)));
2687
2881
  const lifetimePnlRaw = tokenPriceScaled > BigInt(0)
@@ -2706,10 +2900,12 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
2706
2900
  }
2707
2901
  catch (e) {
2708
2902
  core_1.Logger.log.error('getVaultUserLifetimePnl', e, { vault, wallet });
2709
- // Preserve the typed history guard so callers can distinguish an
2710
- // unavailable-history failure from a generic getter error. Its message
2711
- // already carries the `#getVaultUserLifetimePnl::vault::wallet` prefix.
2712
- if (e instanceof errors_1.AugustHistoryUnavailableError)
2903
+ // Preserve the typed history and price guards so callers can distinguish
2904
+ // an unavailable-history or unavailable-price failure from a generic getter
2905
+ // error. Their messages already carry the
2906
+ // `#getVaultUserLifetimePnl::vault::wallet` prefix.
2907
+ if (e instanceof errors_1.AugustHistoryUnavailableError ||
2908
+ e instanceof errors_1.AugustPriceUnavailableError)
2713
2909
  throw e;
2714
2910
  const errorMessage = e instanceof Error ? e.message : String(e);
2715
2911
  throw new Error(`#getVaultUserLifetimePnl::${vault}::${wallet}:${errorMessage}`);
@@ -244,6 +244,11 @@ export declare class AugustVaults extends AugustBase {
244
244
  * Get lifetime PnL for a user in a specific vault.
245
245
  * @param props Vault address, wallet address, and optional chain ID
246
246
  * @returns Lifetime PnL data including realized and unrealized PnL
247
+ * @throws {@link AugustHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when a
248
+ * live position has no indexed deposits, withdrawals or priceable share
249
+ * transfers.
250
+ * @throws {@link AugustPriceUnavailableError} (`PRICE_UNAVAILABLE`) when the
251
+ * vault's underlying asset has no USD price.
247
252
  */
248
253
  getVaultUserLifetimePnl({ vault, wallet, chainId, }: {
249
254
  vault: IAddress;
@@ -933,6 +933,11 @@ class AugustVaults extends core_1.AugustBase {
933
933
  * Get lifetime PnL for a user in a specific vault.
934
934
  * @param props Vault address, wallet address, and optional chain ID
935
935
  * @returns Lifetime PnL data including realized and unrealized PnL
936
+ * @throws {@link AugustHistoryUnavailableError} (`HISTORY_UNAVAILABLE`) when a
937
+ * live position has no indexed deposits, withdrawals or priceable share
938
+ * transfers.
939
+ * @throws {@link AugustPriceUnavailableError} (`PRICE_UNAVAILABLE`) when the
940
+ * vault's underlying asset has no USD price.
936
941
  */
937
942
  async getVaultUserLifetimePnl({ vault, wallet, chainId, }) {
938
943
  if (!(0, ethers_1.isAddress)(wallet))
@@ -535,6 +535,14 @@ export declare function depositViaSwapRouter(signer: Signer | Wallet, options: I
535
535
  * before depositing — only valid when the vault's reference asset equals
536
536
  * the wrapped-native token.
537
537
  *
538
+ * **Wrap-only limitation**: the on-chain SwapRouter has no ETH→other-asset
539
+ * swap leg, so this only ever reaches a WETH-accepting vault (see
540
+ * {@link SWAP_ROUTER_WRAPPED_NATIVE}). There is no atomic native-ETH deposit
541
+ * into a non-WETH-reference vault (e.g. a USDC vault) — deposit the wrapped
542
+ * native token instead (already whitelisted, routes through the swap leg for
543
+ * a non-reference-asset vault). Use {@link isNativeSwapRouterDepositSupported}
544
+ * to check eligibility before offering this path in a UI.
545
+ *
538
546
  * No ERC-20 approval is needed; `amount` travels as `msg.value`.
539
547
  *
540
548
  * @param signer - ethers `Signer` or `Wallet`.
@@ -545,6 +553,51 @@ export declare function depositViaSwapRouter(signer: Signer | Wallet, options: I
545
553
  * addresses, or zero amount.
546
554
  */
547
555
  export declare function depositNativeViaSwapRouter(signer: Signer | Wallet, options: ISwapRouterNativeDepositOptions): Promise<string>;
556
+ /**
557
+ * Whether a vault can take a native-ETH (chain-native token) SwapRouter
558
+ * deposit — the pre-flight check for {@link depositNativeViaSwapRouter} and
559
+ * {@link swapRouterDeposit} with a zero-address `depositAsset`.
560
+ *
561
+ * The on-chain SwapRouter only wraps native ETH to the chain's wrapped-native
562
+ * token and deposits that — it has no ETH→other-asset swap leg (see
563
+ * {@link SWAP_ROUTER_WRAPPED_NATIVE}). So a native deposit needs all three: a
564
+ * SwapRouter deployed on `chainId`, the vault registered on that router with
565
+ * the wrapped-native token as its `vaultInfo(vault).referenceAsset` (a
566
+ * deposit into an unregistered vault reverts with `InvalidVault`), and the
567
+ * vault's own on-chain `asset()` equal to that same token. Use this to gate a
568
+ * "deposit ETH" UI option before the user picks it, instead of discovering
569
+ * the answer via a thrown {@link AugustValidationError} or an on-chain revert.
570
+ *
571
+ * It checks configuration only — a deposit that passes this check can still
572
+ * fail on balance, gas, a paused vault, or a deposit cap.
573
+ *
574
+ * Read-only and provider-agnostic (no signer required) — safe to call before
575
+ * a wallet is connected. Makes no backend call.
576
+ *
577
+ * @param chainId - EVM chain ID the vault is deployed on.
578
+ * @param vault - August vault address to check.
579
+ * @param options - `rpcUrl` for `chainId`, passed to `createProvider`.
580
+ * @returns `true` iff a SwapRouter exists on `chainId`, the vault is
581
+ * registered on it with the wrapped-native reference asset, and the vault's
582
+ * `asset()` is that token. `false` — never throws — for a chain with no
583
+ * SwapRouter deployment, a missing/invalid `rpcUrl`, an unreadable vault or
584
+ * router, an unregistered vault, or a vault whose reference asset isn't the
585
+ * wrapped-native token.
586
+ * @worstCaseRpcCalls 2 — the vault's `asset()` and the router's
587
+ * `vaultInfo(vault)`, read in parallel. None on a chain with no SwapRouter.
588
+ * @example
589
+ * ```ts
590
+ * const canDepositEth = await isNativeSwapRouterDepositSupported(
591
+ * 1,
592
+ * '0x1234000000000000000000000000000000005678',
593
+ * { rpcUrl: 'https://eth-mainnet.example/v2/KEY' },
594
+ * );
595
+ * // true only if that vault is a router-registered WETH vault on mainnet
596
+ * ```
597
+ */
598
+ export declare function isNativeSwapRouterDepositSupported(chainId: number, vault: IAddress, options: {
599
+ rpcUrl: string;
600
+ }): Promise<boolean>;
548
601
  /**
549
602
  * Deposit into an August vault through the on-chain `SwapRouter`, selecting the
550
603
  * correct router path from the deposit asset. This is the high-level, explicit
@@ -564,7 +617,10 @@ export declare function depositNativeViaSwapRouter(signer: Signer | Wallet, opti
564
617
  * - equals the reference asset → direct router deposit, no swap
565
618
  * ({@link depositViaSwapRouter});
566
619
  * - the zero address → native-token deposit ({@link depositNativeViaSwapRouter}),
567
- * valid only when the reference asset is the chain's wrapped-native token;
620
+ * valid only when the reference asset is the chain's wrapped-native token —
621
+ * the router only wraps ETH, it never swaps it, so this throws for any
622
+ * other reference asset (see {@link SWAP_ROUTER_WRAPPED_NATIVE} for why, and
623
+ * {@link isNativeSwapRouterDepositSupported} to check first);
568
624
  * - any other ERC-20 → a swap to the reference asset (Paraswap quote pinned to
569
625
  * the chain's whitelisted aggregator, fail-closed on router/selector drift)
570
626
  * bundled into {@link swapAndDeposit}.