@perena/vault-sdk 1.0.24 → 1.0.26

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/dist/index.d.ts CHANGED
@@ -8957,11 +8957,11 @@ type Bankineco = {
8957
8957
  "name": "vaultOracle";
8958
8958
  "docs": [
8959
8959
  "Required when enabling tranching organically (`for_migration == false`)",
8960
- "on a vault that still has main shareholders accruing fixed APY: that APY",
8961
- "is only folded into `mint_share_price` while tranching is off, so a fresh",
8962
- "NAV must have been settled just before the switch or the accrued APY owed",
8963
- "to those shareholders is forfeited. Not needed on the migration path,",
8964
- "whose accounting is reconstructed wholesale from the legacy state.",
8960
+ "on a vault that still has main shareholders accruing fixed APY. A fresh",
8961
+ "settlement establishes the regular sleeve's booked NAV and physical",
8962
+ "surplus immediately before the new tranche sleeve is introduced. Not",
8963
+ "needed on the migration path, whose accounting is reconstructed wholesale",
8964
+ "from the legacy state.",
8965
8965
  "",
8966
8966
  "Kept LAST (a trailing optional account) so legacy clients that predate it",
8967
8967
  "— e.g. the migration path building against the frozen bankineco IDL —",
@@ -10146,7 +10146,9 @@ type Bankineco = {
10146
10146
  "Holdings are addressed by their slot index (1 byte) to keep instruction data",
10147
10147
  "small. To register a brand-new consensus holding, set `mint` to its mint",
10148
10148
  "(and pass the matching `asset_mint` account); the created slot's index is used",
10149
- "and `holding_index` is ignored. For existing holdings leave `mint` as `None`."
10149
+ "and `holding_index` is ignored. For existing holdings leave `mint` as `None`.",
10150
+ "The price is authoritative only for ConsensusOracle holdings. Pyth and nested-vault",
10151
+ "holdings use the report only for external_amount and retain their live-oracle price."
10150
10152
  ];
10151
10153
  "type": {
10152
10154
  "kind": "struct";
@@ -11891,7 +11893,7 @@ type Bankineco = {
11891
11893
  {
11892
11894
  "name": "lossesEnabled";
11893
11895
  "docs": [
11894
- "Allows consensus-oracle NAV updates below the vault's current TVL."
11896
+ "Allows consensus-oracle NAV losses beyond the protocol's small-loss tolerance. Losses inside that tolerance settle without this flag."
11895
11897
  ];
11896
11898
  "type": {
11897
11899
  "defined": {
@@ -11900,16 +11902,9 @@ type Bankineco = {
11900
11902
  };
11901
11903
  },
11902
11904
  {
11903
- "name": "migrationInProgress";
11905
+ "name": "paddingBool";
11904
11906
  "docs": [
11905
- "True while a legacy migration destination is still receiving backing",
11906
- "and receipt-supply accounting. Oracle mutation and NAV settlement stay",
11907
- "disabled until every configured receipt mint authority has moved to this vault",
11908
- "and migration finalization clears the flag.",
11909
- "",
11910
- "This occupies the former `apy_settle_skip` padding byte, preserving the",
11911
- "on-chain account layout for already-deployed vaults. Existing vaults read",
11912
- "it as false."
11907
+ "Reserved byte retained for on-chain account layout compatibility."
11913
11908
  ];
11914
11909
  "type": {
11915
11910
  "defined": {
@@ -15871,10 +15866,20 @@ interface YieldPaymentSnapshot {
15871
15866
  * helpers for tests and local operation.
15872
15867
  */
15873
15868
  interface YieldTracker {
15869
+ /** Account names tagged to a vault. Live prod discovery uses this automatically. */
15870
+ getAccountNamesForVault?(vault: Address): Promise<string[]>;
15874
15871
  /** All payments for an account, including payments not yet confirmed. */
15875
15872
  getYieldPayments(accountName: string): Promise<YieldPaymentSnapshot[]>;
15876
15873
  /** Sum of oracle-safe outstanding yield across the named accounts. */
15877
15874
  getTotalOutstandingYield(accountNames: string[], start?: Date, end?: Date): Promise<number>;
15875
+ /**
15876
+ * Sum the external principal and confirmed-payment-adjusted outstanding yield
15877
+ * across the named accounts. Both values are UI token amounts.
15878
+ */
15879
+ getTotalTrackedAmounts(accountNames: string[], start?: Date, end?: Date): Promise<{
15880
+ principal: number;
15881
+ outstandingYield: number;
15882
+ }>;
15878
15883
  }
15879
15884
  /** Injectable wall clock (ms since epoch). Lets tests pin time deterministically. */
15880
15885
  interface Clock {
@@ -15918,7 +15923,9 @@ interface HoldingUpdatePreview {
15918
15923
  lpAmount: bigint;
15919
15924
  /** Accrued yield attributed to this holding (base units). */
15920
15925
  yieldAmount: bigint;
15921
- /** Total external balance pushed = wallet + lp + yield. */
15926
+ /** Yield-tracker principal attributed to this holding (base units). */
15927
+ trackedPrincipalAmount: bigint;
15928
+ /** Total external balance pushed = wallet + LP + tracked principal + yield. */
15922
15929
  externalAmount: bigint;
15923
15930
  }
15924
15931
  interface VaultOracleResult {
@@ -15955,6 +15962,8 @@ interface DecodedHolding {
15955
15962
  mint: unknown;
15956
15963
  decimals: number;
15957
15964
  priceOracleType: unknown;
15965
+ /** Bound Pyth price-update or nested-vault account for live-priced holdings. */
15966
+ priceOracleAccount?: unknown;
15958
15967
  isBase: unknown;
15959
15968
  localAmount?: unknown;
15960
15969
  externalAmount?: unknown;
@@ -16035,6 +16044,12 @@ interface SettlementSimulation {
16035
16044
  grossNav?: bigint;
16036
16045
  settledAccountingNav?: bigint;
16037
16046
  current: SettlementSimulationSnapshot;
16047
+ /**
16048
+ * True when `projected` was calculated by replaying the otherwise-identical
16049
+ * settlement with the max-APY guard disabled. The real crank remains blocked
16050
+ * until the vault config is updated.
16051
+ */
16052
+ projectionRequiresMaxApyDisabled?: boolean;
16038
16053
  projected?: SettlementSimulationSnapshot;
16039
16054
  performanceFees?: {
16040
16055
  curator: bigint;
@@ -16235,6 +16250,8 @@ declare function runLiveConsensusOracle(env: VaultEnv, oracleSigner: Keypair, op
16235
16250
  */
16236
16251
 
16237
16252
  interface MockYieldAccountConfig {
16253
+ /** Optional vault address used by production-style account discovery. */
16254
+ tag?: string;
16238
16255
  aprBps: number;
16239
16256
  /** Initial principal (UI amount). Defaults to 0. */
16240
16257
  principal?: number;
@@ -16252,10 +16269,15 @@ declare class MockYieldTracker implements YieldTracker {
16252
16269
  /** Register (or replace) a tracked account. */
16253
16270
  setAccount(name: string, cfg: MockYieldAccountConfig): void;
16254
16271
  private require;
16272
+ getAccountNamesForVault(vault: string): Promise<string[]>;
16255
16273
  getYieldPayments(accountName: string): Promise<YieldPaymentSnapshot[]>;
16256
16274
  getYield(accountName: string, start?: Date, end?: Date): Promise<YieldAccountSnapshot>;
16257
16275
  getTotalYield(accountNames: string[], start?: Date, end?: Date): Promise<number>;
16258
16276
  getTotalOutstandingYield(accountNames: string[], start?: Date, end?: Date): Promise<number>;
16277
+ getTotalTrackedAmounts(accountNames: string[], start?: Date, end?: Date): Promise<{
16278
+ principal: number;
16279
+ outstandingYield: number;
16280
+ }>;
16259
16281
  getBalance(accountName: string, atTime?: Date): Promise<number>;
16260
16282
  addCashflow(params: {
16261
16283
  accountName: string;
@@ -16324,6 +16346,12 @@ declare class ConsensusOracleService {
16324
16346
  * balance source used by this service. Until then, no oracle update is safe.
16325
16347
  */
16326
16348
  private assertNoUnconfirmedYieldPayments;
16349
+ /**
16350
+ * In production, yield accounts are tagged with their vault address. Attach
16351
+ * newly discovered accounts to the base holding, while preserving explicit
16352
+ * per-mint mappings supplied by config or CLI flags.
16353
+ */
16354
+ private withDiscoveredYieldAccounts;
16327
16355
  /**
16328
16356
  * Drive several vaults in sequence. Non-fatal: a failure on one vault is
16329
16357
  * recorded and the rest still run (mirrors the oracle-updater daemon).
@@ -16341,6 +16369,12 @@ declare class ConsensusOracleService {
16341
16369
  log?: (msg: string) => void;
16342
16370
  }): Promise<ConsensusOracleTarget[]>;
16343
16371
  private fetchDecodedVault;
16372
+ /**
16373
+ * Keep active permissionless price sources warm so they cannot block the NAV
16374
+ * crank after the consensus report lands. Prices are refreshed at least once
16375
+ * per hour, or at the vault's shorter staleness interval when configured.
16376
+ */
16377
+ private refreshLiveOraclePrices;
16344
16378
  /**
16345
16379
  * Receipt mints issued by this vault are liabilities/shares, not underlying
16346
16380
  * assets for its own NAV. Exclude them even if they were registered as
@@ -16351,19 +16385,20 @@ declare class ConsensusOracleService {
16351
16385
  private gatherPricingInputs;
16352
16386
  /** Live-LP balances for the target, summed by mint (empty if no provider). */
16353
16387
  private fetchExternalPositions;
16354
- /** Accrued yield (base units) attributed to a holding via its configured accounts. */
16355
- private resolveYieldAmount;
16388
+ /** Tracked external principal + outstanding yield attributed to a holding. */
16389
+ private resolveTrackedAmounts;
16356
16390
  private buildUpdates;
16357
16391
  private buildHoldingUpdate;
16358
16392
  /** Push the consensus report for all holdings, then settle NAV. */
16359
16393
  private settleVault;
16360
16394
  /**
16361
- * Fetch the current settled NAV from the oracle account and log the APY
16362
- * implied by the prospective update, so MaxApyExceeded failures are diagnosable.
16395
+ * Fetch the current settlement timestamp and log the APY implied by the
16396
+ * prospective update, so MaxApyExceeded failures are diagnosable.
16363
16397
  *
16364
16398
  * NAV is expressed in the vault's accounting unit (scaled by assetDecimals).
16365
- * prospective NAV = Σ ((localAmount_i + externalAmount_i) * price_i)
16366
- * / 10^assetDecimals
16399
+ * Mirrors `gross_nav_from_holdings`: consensus updates overlay their target
16400
+ * slots, while live-priced and otherwise unchanged holdings retain their
16401
+ * current price/external amount.
16367
16402
  */
16368
16403
  private logProspectiveApy;
16369
16404
  }
package/dist/index.js CHANGED
@@ -1532,7 +1532,8 @@ var PROTOCOL_FEE_RECIPIENT = (0, import_kit.address)(
1532
1532
  var PROGRAM_FEE_EXEMPT_OWNER_WHITELIST = [
1533
1533
  (0, import_kit.address)("Hobbakk1LmW2DhE4nAKnCeU1iK7V7pKHTq3ihaE2i8qG"),
1534
1534
  (0, import_kit.address)("7C39GeKrqLygmgV9D4bicUaiyFhKrX6Eevk6PDACwuj7"),
1535
- (0, import_kit.address)("LYC8YiiSzQfPpxUW2tpxfuPKGZwywAJhXKUfDP2B66f")
1535
+ (0, import_kit.address)("LYC8YiiSzQfPpxUW2tpxfuPKGZwywAJhXKUfDP2B66f"),
1536
+ (0, import_kit.address)("soBBAHNhFyWF4QBCK9BJLVkX6d6NZt37ozNaPV5aq3H")
1536
1537
  ];
1537
1538
  var VAULT_CREATOR_WHITELIST = [
1538
1539
  (0, import_kit.address)("4jMRBkpWrfhhwFYTwG3o7hmsXQwbJUwdyUX8htB6KwLJ")
@@ -10463,11 +10464,11 @@ var IDL = {
10463
10464
  "name": "vault_oracle",
10464
10465
  "docs": [
10465
10466
  "Required when enabling tranching organically (`for_migration == false`)",
10466
- "on a vault that still has main shareholders accruing fixed APY: that APY",
10467
- "is only folded into `mint_share_price` while tranching is off, so a fresh",
10468
- "NAV must have been settled just before the switch or the accrued APY owed",
10469
- "to those shareholders is forfeited. Not needed on the migration path,",
10470
- "whose accounting is reconstructed wholesale from the legacy state.",
10467
+ "on a vault that still has main shareholders accruing fixed APY. A fresh",
10468
+ "settlement establishes the regular sleeve's booked NAV and physical",
10469
+ "surplus immediately before the new tranche sleeve is introduced. Not",
10470
+ "needed on the migration path, whose accounting is reconstructed wholesale",
10471
+ "from the legacy state.",
10471
10472
  "",
10472
10473
  "Kept LAST (a trailing optional account) so legacy clients that predate it",
10473
10474
  "\u2014 e.g. the migration path building against the frozen bankineco IDL \u2014",
@@ -11652,7 +11653,9 @@ var IDL = {
11652
11653
  "Holdings are addressed by their slot index (1 byte) to keep instruction data",
11653
11654
  "small. To register a brand-new consensus holding, set `mint` to its mint",
11654
11655
  "(and pass the matching `asset_mint` account); the created slot's index is used",
11655
- "and `holding_index` is ignored. For existing holdings leave `mint` as `None`."
11656
+ "and `holding_index` is ignored. For existing holdings leave `mint` as `None`.",
11657
+ "The price is authoritative only for ConsensusOracle holdings. Pyth and nested-vault",
11658
+ "holdings use the report only for external_amount and retain their live-oracle price."
11656
11659
  ],
11657
11660
  "type": {
11658
11661
  "kind": "struct",
@@ -13397,7 +13400,7 @@ var IDL = {
13397
13400
  {
13398
13401
  "name": "losses_enabled",
13399
13402
  "docs": [
13400
- "Allows consensus-oracle NAV updates below the vault's current TVL."
13403
+ "Allows consensus-oracle NAV losses beyond the protocol's small-loss tolerance. Losses inside that tolerance settle without this flag."
13401
13404
  ],
13402
13405
  "type": {
13403
13406
  "defined": {
@@ -13406,16 +13409,9 @@ var IDL = {
13406
13409
  }
13407
13410
  },
13408
13411
  {
13409
- "name": "migration_in_progress",
13412
+ "name": "_padding_bool",
13410
13413
  "docs": [
13411
- "True while a legacy migration destination is still receiving backing",
13412
- "and receipt-supply accounting. Oracle mutation and NAV settlement stay",
13413
- "disabled until every configured receipt mint authority has moved to this vault",
13414
- "and migration finalization clears the flag.",
13415
- "",
13416
- "This occupies the former `apy_settle_skip` padding byte, preserving the",
13417
- "on-chain account layout for already-deployed vaults. Existing vaults read",
13418
- "it as false."
13414
+ "Reserved byte retained for on-chain account layout compatibility."
13419
13415
  ],
13420
13416
  "type": {
13421
13417
  "defined": {
@@ -19595,6 +19591,7 @@ var MarginfiPositionProvider = class {
19595
19591
  };
19596
19592
 
19597
19593
  // src/services/consensusOracle/liveConsensusOracleDeps.ts
19594
+ var YIELD_TRACKER_DECIMALS = 6;
19598
19595
  function createLiveConsensusOracleDeps(connection, opts = {}) {
19599
19596
  const log = opts.log ?? (() => {
19600
19597
  });
@@ -19605,7 +19602,27 @@ function createLiveConsensusOracleDeps(connection, opts = {}) {
19605
19602
  log
19606
19603
  }),
19607
19604
  yieldTracker: {
19608
- getTotalOutstandingYield,
19605
+ async getAccountNamesForVault(vault) {
19606
+ const accounts2 = await getAccounts();
19607
+ return accounts2.filter((account) => account.tag === vault.toString()).map((account) => account.name);
19608
+ },
19609
+ async getTotalOutstandingYield(accountNames, start, end) {
19610
+ const raw = await getTotalOutstandingYield(accountNames, start, end);
19611
+ return raw / 10 ** YIELD_TRACKER_DECIMALS;
19612
+ },
19613
+ async getTotalTrackedAmounts(accountNames, start, end) {
19614
+ let principal = 0;
19615
+ let outstandingYield = 0;
19616
+ for (const accountName of accountNames) {
19617
+ const snapshot2 = await getYield(accountName, start, end);
19618
+ principal += snapshot2.principal_at_end;
19619
+ outstandingYield += snapshot2.outstanding_yield;
19620
+ }
19621
+ return {
19622
+ principal: principal / 10 ** YIELD_TRACKER_DECIMALS,
19623
+ outstandingYield: outstandingYield / 10 ** YIELD_TRACKER_DECIMALS
19624
+ };
19625
+ },
19609
19626
  getYieldPayments
19610
19627
  }
19611
19628
  };
@@ -19630,6 +19647,7 @@ var import_kit14 = require("@solana/kit");
19630
19647
  var import_web326 = require("@solana/web3.js");
19631
19648
  var DEFAULT_SHARE_PRICE = 1000000n;
19632
19649
  var BPS_DENOMINATOR2 = 10000n;
19650
+ var NAV_LOSS_TOLERANCE_BPS = 25n;
19633
19651
  var SECONDS_PER_YEAR = 31536000n;
19634
19652
  var DEFAULT_STALENESS_SECS = 86400n;
19635
19653
  var CONSENSUS_MAX_DIFF_BPS = 8n;
@@ -19744,6 +19762,7 @@ function simulateConsensusOracleSettlement(args) {
19744
19762
  hwmSharePrice: bigint(vaultAccounting.hwmSharePrice),
19745
19763
  fixedApyBps: number(apyConfig.fixedApyBps),
19746
19764
  maxApyBps: number(apyConfig.maxApyBps),
19765
+ accruedApyBalance: bigint(apyConfig.accruedApyBalance),
19747
19766
  apyAnchorSharePrice: bigint(apyConfig.anchorSharePrice),
19748
19767
  apyAnchorTs: bigint(apyConfig.anchorTs),
19749
19768
  tranche: currentTranche
@@ -19763,7 +19782,52 @@ function simulateConsensusOracleSettlement(args) {
19763
19782
  performanceFees: settlement.performanceFees
19764
19783
  };
19765
19784
  } catch (error) {
19766
- blockers.push(message(error));
19785
+ const blocker = message(error);
19786
+ blockers.push(blocker);
19787
+ if (blocker.includes("MaxApyExceeded")) {
19788
+ try {
19789
+ const settlement = settleVault({
19790
+ grossNav,
19791
+ nowTs: args.nowTs,
19792
+ currentTvl,
19793
+ currentSharePrice: bigint(vaultAccounting.mintSharePrice),
19794
+ totalSupply: reconciledSupply,
19795
+ lossesEnabled: bool(args.vault.lossesEnabled),
19796
+ accountingLastUpdateTs: bigint(vaultAccounting.lastUpdateTs),
19797
+ priorSettledNavTs: oracle.settledNavTs,
19798
+ performanceFeeBps: number(feesConfig.performanceFeeBps),
19799
+ protocolFeeBps: number(feesConfig.protocolFeeBps),
19800
+ hwmSharePrice: bigint(vaultAccounting.hwmSharePrice),
19801
+ fixedApyBps: number(apyConfig.fixedApyBps),
19802
+ maxApyBps: 0,
19803
+ accruedApyBalance: bigint(apyConfig.accruedApyBalance),
19804
+ apyAnchorSharePrice: bigint(apyConfig.anchorSharePrice),
19805
+ apyAnchorTs: bigint(apyConfig.anchorTs),
19806
+ tranche: currentTranche
19807
+ });
19808
+ return {
19809
+ nowTs: args.nowTs,
19810
+ assetDecimals: args.vault.config.assetDecimals,
19811
+ ...consensusSummary,
19812
+ canSettle: false,
19813
+ blockers,
19814
+ warnings,
19815
+ consensus,
19816
+ grossNav,
19817
+ settledAccountingNav: settlement.settledAccountingNav,
19818
+ current,
19819
+ projectionRequiresMaxApyDisabled: true,
19820
+ projected: settlement.projected,
19821
+ performanceFees: settlement.performanceFees
19822
+ };
19823
+ } catch (projectionError) {
19824
+ warnings.push(
19825
+ `could not calculate the uncapped projection: ${message(
19826
+ projectionError
19827
+ )}`
19828
+ );
19829
+ }
19830
+ }
19767
19831
  return {
19768
19832
  nowTs: args.nowTs,
19769
19833
  assetDecimals: args.vault.config.assetDecimals,
@@ -19781,18 +19845,19 @@ function applyConsensus(holdings, entries, nowTs, stalenessThresholdSecs) {
19781
19845
  const results = [];
19782
19846
  for (let index = 0; index < holdings.length; index += 1) {
19783
19847
  const holding = holdings[index];
19784
- if (holding.cleared || holding.priceOracleType !== "consensusoracle") {
19848
+ if (holding.cleared || holding.priceOracleType === "none") {
19785
19849
  continue;
19786
19850
  }
19787
19851
  const reports = entries.filter(
19788
19852
  (entry) => entry.signer !== import_web326.PublicKey.default.toBase58() && isFresh(entry.lastUpdateTs, nowTs, stalenessThresholdSecs) && (entry.prices[index] ?? 0n) > 0n
19789
19853
  );
19854
+ const usesConsensusPrice = holding.priceOracleType === "consensusoracle";
19790
19855
  const prices = reports.map((entry) => entry.prices[index] ?? 0n);
19791
19856
  const amounts = reports.map((entry) => entry.externalAmounts[index] ?? 0n);
19792
19857
  let reason;
19793
19858
  if (reports.length < MIN_CONSENSUS_SIGNERS) {
19794
19859
  reason = `only ${reports.length} fresh priced report(s); need ${MIN_CONSENSUS_SIGNERS}`;
19795
- } else if (!withinDeviation(prices)) {
19860
+ } else if (usesConsensusPrice && !withinDeviation(prices)) {
19796
19861
  reason = `price reports exceed ${CONSENSUS_MAX_DIFF_BPS}bps pairwise deviation`;
19797
19862
  } else if (!withinDeviation(amounts)) {
19798
19863
  reason = `external-amount reports exceed ${CONSENSUS_MAX_DIFF_BPS}bps pairwise deviation`;
@@ -19807,18 +19872,20 @@ function applyConsensus(holdings, entries, nowTs, stalenessThresholdSecs) {
19807
19872
  });
19808
19873
  continue;
19809
19874
  }
19810
- const settledPrice = average(prices);
19875
+ const settledPrice = usesConsensusPrice ? average(prices) : void 0;
19811
19876
  const settledExternalAmount = average(amounts);
19812
19877
  const latestTs = reports.reduce(
19813
19878
  (latest, entry) => entry.lastUpdateTs > latest ? entry.lastUpdateTs : latest,
19814
19879
  0n
19815
19880
  );
19816
- const wouldUpdateHolding = latestTs > holding.lastUpdateTs && latestTs <= nowTs;
19881
+ const wouldUpdateHolding = usesConsensusPrice ? latestTs > holding.lastUpdateTs && latestTs <= nowTs : latestTs <= nowTs;
19817
19882
  if (wouldUpdateHolding) {
19818
- holding.price = settledPrice;
19883
+ if (settledPrice !== void 0) {
19884
+ holding.price = settledPrice;
19885
+ holding.lastUpdateTs = latestTs;
19886
+ }
19819
19887
  holding.externalAmount = settledExternalAmount;
19820
- holding.lastUpdateTs = latestTs;
19821
- if (!holding.isBase && holding.localAmount === 0n && holding.externalAmount === 0n) {
19888
+ if (usesConsensusPrice && !holding.isBase && holding.localAmount === 0n && holding.externalAmount === 0n) {
19822
19889
  holding.cleared = true;
19823
19890
  }
19824
19891
  }
@@ -19835,16 +19902,20 @@ function applyConsensus(holdings, entries, nowTs, stalenessThresholdSecs) {
19835
19902
  return results;
19836
19903
  }
19837
19904
  function settleVault(args) {
19838
- if (args.grossNav < args.currentTvl && !args.lossesEnabled) {
19905
+ const regularSurplusBefore = args.tranche ? max(args.accruedApyBalance, 0n) : 0n;
19906
+ const physicalNavBefore = args.currentTvl + regularSurplusBefore;
19907
+ const loss = args.grossNav < physicalNavBefore ? physicalNavBefore - args.grossNav : 0n;
19908
+ const lossExceedsTolerance = loss * BPS_DENOMINATOR2 > physicalNavBefore * NAV_LOSS_TOLERANCE_BPS;
19909
+ if (lossExceedsTolerance && !args.lossesEnabled) {
19839
19910
  throw new Error(
19840
- `crank would fail LossesDisabled: gross NAV ${args.grossNav} < current TVL ${args.currentTvl}`
19911
+ `crank would fail LossesDisabled: gross NAV ${args.grossNav} is more than ${NAV_LOSS_TOLERANCE_BPS}bps below physical NAV ${physicalNavBefore}`
19841
19912
  );
19842
19913
  }
19843
19914
  const priorTs = args.priorSettledNavTs > 0n ? args.priorSettledNavTs : max(args.accountingLastUpdateTs, args.apyAnchorTs);
19844
19915
  const nextApyValue = args.tranche ? args.grossNav : sharePriceForValue(args.grossNav, args.totalSupply);
19845
19916
  if (nextApyValue !== void 0) {
19846
19917
  validateRealizedApy(
19847
- args.tranche ? args.currentTvl : args.currentSharePrice,
19918
+ args.tranche ? physicalNavBefore : args.currentSharePrice,
19848
19919
  priorTs,
19849
19920
  nextApyValue,
19850
19921
  args.nowTs,
@@ -19857,13 +19928,22 @@ function settleVault(args) {
19857
19928
  let protocolFee = 0n;
19858
19929
  let settledAccountingNav;
19859
19930
  if (args.tranche) {
19860
- settledAccountingNav = args.grossNav;
19861
- regularValue = settleTrancheNav(
19931
+ const regularGrossValue = settleTrancheNav(
19862
19932
  args.tranche,
19863
- args.currentTvl,
19864
- settledAccountingNav,
19933
+ physicalNavBefore,
19934
+ args.grossNav,
19865
19935
  args.nowTs
19866
19936
  );
19937
+ regularValue = settleFixedApyNav({
19938
+ currentSharePrice: args.currentSharePrice,
19939
+ reportedNav: regularGrossValue,
19940
+ nowTs: args.nowTs,
19941
+ totalSupply: args.totalSupply,
19942
+ fixedApyBps: args.fixedApyBps,
19943
+ anchorSharePrice: args.apyAnchorSharePrice,
19944
+ anchorTs: args.apyAnchorTs
19945
+ });
19946
+ settledAccountingNav = regularValue + args.tranche.junior.value + args.tranche.senior.value;
19867
19947
  const juniorFees = settleShareClassPerformanceFee(
19868
19948
  args.tranche.junior,
19869
19949
  args.performanceFeeBps,
@@ -20031,7 +20111,8 @@ function settleRegularPerformanceFee(grossValue, supply, hwmSharePrice, performa
20031
20111
  };
20032
20112
  }
20033
20113
  function settleFixedApyNav(args) {
20034
- if (args.fixedApyBps === 0 || args.currentSharePrice === 0n || args.totalSupply === 0n || args.anchorSharePrice === 0n || args.anchorTs <= 0n) {
20114
+ if (args.totalSupply === 0n) return 0n;
20115
+ if (args.fixedApyBps === 0 || args.currentSharePrice === 0n || args.anchorSharePrice === 0n || args.anchorTs <= 0n) {
20035
20116
  return args.reportedNav;
20036
20117
  }
20037
20118
  if (args.nowTs <= args.anchorTs) {
@@ -20408,6 +20489,9 @@ function formatSettlementSimulation(simulation) {
20408
20489
  );
20409
20490
  }
20410
20491
  if (simulation.projected) {
20492
+ if (simulation.projectionRequiresMaxApyDisabled) {
20493
+ lines.push(" projection with max APY disabled (max_apy_bps=0):");
20494
+ }
20411
20495
  lines.push(
20412
20496
  ` TVL: ${formatTransition(
20413
20497
  simulation.current.tvl,
@@ -20478,8 +20562,27 @@ function formatUnits(value, decimals) {
20478
20562
 
20479
20563
  // src/services/consensusOracle/consensusOracleService.ts
20480
20564
  var SYSTEM_PROGRAM2 = "11111111111111111111111111111111";
20565
+ var PRICE_ORACLE_TYPES_BY_INDEX2 = [
20566
+ "none",
20567
+ "consensusOracle",
20568
+ "pyth",
20569
+ "perenaVault"
20570
+ ];
20481
20571
  var CONSENSUS_ORACLE_VARIANT = "consensusoracle";
20572
+ var LIVE_ORACLE_VARIANTS = /* @__PURE__ */ new Set(["pyth", "perenavault"]);
20573
+ var REPORTABLE_ORACLE_VARIANTS = /* @__PURE__ */ new Set([
20574
+ CONSENSUS_ORACLE_VARIANT,
20575
+ ...LIVE_ORACLE_VARIANTS
20576
+ ]);
20577
+ var LIVE_PRICE_REFRESH_INTERVAL_SECS = 60n * 60n;
20578
+ var DEFAULT_PRICE_STALENESS_THRESHOLD_SECS3 = 24n * 60n * 60n;
20482
20579
  function variantName2(value) {
20580
+ if (typeof value === "number") {
20581
+ return PRICE_ORACLE_TYPES_BY_INDEX2[value] ?? String(value);
20582
+ }
20583
+ if (typeof value === "bigint") {
20584
+ return PRICE_ORACLE_TYPES_BY_INDEX2[Number(value)] ?? String(value);
20585
+ }
20483
20586
  if (value && typeof value === "object") return Object.keys(value)[0] ?? "";
20484
20587
  return String(value ?? "");
20485
20588
  }
@@ -20489,6 +20592,13 @@ function coerceBool3(value) {
20489
20592
  const record2 = value;
20490
20593
  return Boolean(record2?.value ?? record2?.[0]);
20491
20594
  }
20595
+ function toBigInt2(value) {
20596
+ if (typeof value === "bigint") return value;
20597
+ if (typeof value === "number") return BigInt(Math.trunc(value));
20598
+ if (typeof value === "string" && value.length > 0) return BigInt(value);
20599
+ if (value !== null && value !== void 0) return BigInt(String(value));
20600
+ return 0n;
20601
+ }
20492
20602
  function priceInAccountingUnit(usd, baseUsd, assetDecimals) {
20493
20603
  return BigInt(Math.round(usd / baseUsd * 10 ** assetDecimals));
20494
20604
  }
@@ -20533,9 +20643,19 @@ var ConsensusOracleService = class {
20533
20643
  const log = opts.log ?? (() => {
20534
20644
  });
20535
20645
  const dryRun = opts.dryRun ?? false;
20536
- const vaultState = await this.fetchDecodedVault(target.vault);
20646
+ let vaultState = await this.fetchDecodedVault(target.vault);
20647
+ const refreshedLivePrices = await this.refreshLiveOraclePrices(
20648
+ signer,
20649
+ target.vault,
20650
+ vaultState,
20651
+ log,
20652
+ dryRun
20653
+ );
20654
+ if (refreshedLivePrices) {
20655
+ vaultState = await this.fetchDecodedVault(target.vault);
20656
+ }
20537
20657
  const receiptMints = await this.fetchReceiptMints(target.vault, vaultState);
20538
- const consensusHoldings = selectConsensusHoldings(
20658
+ const reportableHoldings = selectReportableHoldings(
20539
20659
  vaultState.holdings,
20540
20660
  receiptMints
20541
20661
  );
@@ -20547,18 +20667,24 @@ var ConsensusOracleService = class {
20547
20667
  `vault ${target.vault}: excluded ${filteredCount} vault/tranche receipt mint holding(s)`
20548
20668
  );
20549
20669
  }
20550
- if (consensusHoldings.length === 0) {
20551
- log(`vault ${target.vault}: no consensus holdings, skipping`);
20670
+ if (reportableHoldings.length === 0) {
20671
+ log(`vault ${target.vault}: no oracle-priced holdings, skipping`);
20552
20672
  return { vault: target.vault, updates: [], dryRun };
20553
20673
  }
20554
- const filteredTarget = filterTargetHoldings(target, receiptMints);
20674
+ let filteredTarget = filterTargetHoldings(target, receiptMints);
20675
+ filteredTarget = await this.withDiscoveredYieldAccounts(
20676
+ filteredTarget,
20677
+ vaultState,
20678
+ receiptMints,
20679
+ log
20680
+ );
20555
20681
  await this.assertNoUnconfirmedYieldPayments(filteredTarget);
20556
20682
  const inputs = await this.gatherPricingInputs(
20557
20683
  filteredTarget,
20558
20684
  vaultState,
20559
- consensusHoldings
20685
+ reportableHoldings
20560
20686
  );
20561
- const updates = await this.buildUpdates(consensusHoldings, inputs);
20687
+ const updates = await this.buildUpdates(reportableHoldings, inputs);
20562
20688
  if (dryRun) {
20563
20689
  log(`vault ${target.vault}: dry run, ${updates.length} holding(s)`);
20564
20690
  try {
@@ -20667,6 +20793,56 @@ var ConsensusOracleService = class {
20667
20793
  }
20668
20794
  }
20669
20795
  }
20796
+ /**
20797
+ * In production, yield accounts are tagged with their vault address. Attach
20798
+ * newly discovered accounts to the base holding, while preserving explicit
20799
+ * per-mint mappings supplied by config or CLI flags.
20800
+ */
20801
+ async withDiscoveredYieldAccounts(target, vaultState, excludedMints, log) {
20802
+ const yieldTracker = this.deps.yieldTracker;
20803
+ if (!yieldTracker?.getAccountNamesForVault) return target;
20804
+ const discovered = [
20805
+ ...new Set(await yieldTracker.getAccountNamesForVault(target.vault))
20806
+ ];
20807
+ const alreadyConfigured = new Set(
20808
+ (target.holdings ?? []).flatMap(
20809
+ (holding) => holding.yieldAccountNames ?? []
20810
+ )
20811
+ );
20812
+ const namesToAttach = discovered.filter(
20813
+ (name) => !alreadyConfigured.has(name)
20814
+ );
20815
+ if (namesToAttach.length === 0) return target;
20816
+ const baseHolding = vaultState.holdings.find((holding) => {
20817
+ const mint = (0, import_common41.fromWeb3Pk)(holding.mint).toString();
20818
+ return coerceBool3(holding.isBase) && !excludedMints.has(mint);
20819
+ });
20820
+ if (!baseHolding) {
20821
+ throw new Error(
20822
+ `vault ${target.vault}: tagged yield accounts found but no base holding is available`
20823
+ );
20824
+ }
20825
+ const baseMint = (0, import_common41.fromWeb3Pk)(baseHolding.mint);
20826
+ const holdings = [...target.holdings ?? []];
20827
+ const existingIndex = holdings.findIndex(
20828
+ (holding) => holding.mint.toString() === baseMint.toString()
20829
+ );
20830
+ if (existingIndex >= 0) {
20831
+ const existing = holdings[existingIndex];
20832
+ holdings[existingIndex] = {
20833
+ ...existing,
20834
+ yieldAccountNames: [
20835
+ .../* @__PURE__ */ new Set([...existing.yieldAccountNames ?? [], ...namesToAttach])
20836
+ ]
20837
+ };
20838
+ } else {
20839
+ holdings.push({ mint: baseMint, yieldAccountNames: namesToAttach });
20840
+ }
20841
+ log(
20842
+ `vault ${target.vault}: attached ${namesToAttach.length} tagged yield account(s) to base holding ${baseMint}`
20843
+ );
20844
+ return { ...target, holdings };
20845
+ }
20670
20846
  /**
20671
20847
  * Drive several vaults in sequence. Non-fatal: a failure on one vault is
20672
20848
  * recorded and the rest still run (mirrors the oracle-updater daemon).
@@ -20755,6 +20931,64 @@ var ConsensusOracleService = class {
20755
20931
  fresh: true
20756
20932
  });
20757
20933
  }
20934
+ /**
20935
+ * Keep active permissionless price sources warm so they cannot block the NAV
20936
+ * crank after the consensus report lands. Prices are refreshed at least once
20937
+ * per hour, or at the vault's shorter staleness interval when configured.
20938
+ */
20939
+ async refreshLiveOraclePrices(signer, vault, vaultState, log, dryRun) {
20940
+ const nowSecs = BigInt(Math.floor(this.clock.now() / 1e3));
20941
+ const configuredStaleness = toBigInt2(
20942
+ vaultState.config.priceStalenessThresholdSecs
20943
+ );
20944
+ const stalenessThreshold = configuredStaleness > 0n ? configuredStaleness : DEFAULT_PRICE_STALENESS_THRESHOLD_SECS3;
20945
+ const refreshInterval = stalenessThreshold < LIVE_PRICE_REFRESH_INTERVAL_SECS ? stalenessThreshold : LIVE_PRICE_REFRESH_INTERVAL_SECS;
20946
+ const targets = [];
20947
+ for (const holding of vaultState.holdings) {
20948
+ const oracleType = variantName2(holding.priceOracleType).toLowerCase();
20949
+ if (!LIVE_ORACLE_VARIANTS.has(oracleType)) continue;
20950
+ const mint = (0, import_common41.fromWeb3Pk)(holding.mint);
20951
+ if (mint.toString() === SYSTEM_PROGRAM2) continue;
20952
+ const activeAmount = toBigInt2(holding.localAmount) + toBigInt2(holding.externalAmount);
20953
+ if (activeAmount === 0n) continue;
20954
+ const lastUpdateTs = toBigInt2(holding.lastUpdateTs);
20955
+ const ageSecs = nowSecs - lastUpdateTs;
20956
+ if (lastUpdateTs > 0n && ageSecs >= 0n && ageSecs < refreshInterval) {
20957
+ continue;
20958
+ }
20959
+ if (!holding.priceOracleAccount) {
20960
+ log(
20961
+ `vault ${vault}: cannot refresh ${mint}; ${oracleType} holding has no oracle account`
20962
+ );
20963
+ continue;
20964
+ }
20965
+ const oracleAccount = (0, import_common41.fromWeb3Pk)(holding.priceOracleAccount);
20966
+ if (oracleAccount.toString() === SYSTEM_PROGRAM2) {
20967
+ log(
20968
+ `vault ${vault}: cannot refresh ${mint}; ${oracleType} holding has no oracle account`
20969
+ );
20970
+ continue;
20971
+ }
20972
+ targets.push({ mint, oracleAccount });
20973
+ }
20974
+ if (targets.length === 0) return false;
20975
+ if (dryRun) {
20976
+ log(
20977
+ `vault ${vault}: dry run, would refresh ${targets.length} live asset price(s)`
20978
+ );
20979
+ return false;
20980
+ }
20981
+ log(
20982
+ `vault ${vault}: refreshing ${targets.length} live asset price(s) due for hourly refresh`
20983
+ );
20984
+ const results = await this.oracle.refreshAssetPrices(
20985
+ signer,
20986
+ vault,
20987
+ targets,
20988
+ log
20989
+ );
20990
+ return results.some((result) => Boolean(result.signature));
20991
+ }
20758
20992
  /**
20759
20993
  * Receipt mints issued by this vault are liabilities/shares, not underlying
20760
20994
  * assets for its own NAV. Exclude them even if they were registered as
@@ -20778,17 +21012,21 @@ var ConsensusOracleService = class {
20778
21012
  return receiptMints;
20779
21013
  }
20780
21014
  /** Fetch every external input the per-holding updates draw on, in parallel-ish. */
20781
- async gatherPricingInputs(target, vaultState, consensusHoldings) {
21015
+ async gatherPricingInputs(target, vaultState, reportableHoldings) {
20782
21016
  const manager = (0, import_common41.fromWeb3Pk)(vaultState.roles.manager);
20783
- const holdingMints = consensusHoldings.map(
20784
- ({ holding }) => (0, import_common41.fromWeb3Pk)(holding.mint)
20785
- );
21017
+ const consensusMints = reportableHoldings.filter(
21018
+ ({ holding }) => variantName2(holding.priceOracleType).toLowerCase() === CONSENSUS_ORACLE_VARIANT
21019
+ ).map(({ holding }) => (0, import_common41.fromWeb3Pk)(holding.mint));
20786
21020
  const baseHolding = vaultState.holdings.find((h) => coerceBool3(h.isBase));
20787
- const baseMint = baseHolding ? (0, import_common41.fromWeb3Pk)(baseHolding.mint) : holdingMints[0];
21021
+ const baseMint = baseHolding ? (0, import_common41.fromWeb3Pk)(baseHolding.mint) : consensusMints[0];
20788
21022
  if (!baseMint) {
20789
21023
  throw new Error(`vault has no holdings \u2014 cannot determine base asset`);
20790
21024
  }
20791
- const mintsToPrice = holdingMints.includes(baseMint) ? holdingMints : [...holdingMints, baseMint];
21025
+ const mintsToPrice = [
21026
+ ...new Map(
21027
+ [...consensusMints, baseMint].map((mint) => [mint.toString(), mint])
21028
+ ).values()
21029
+ ];
20792
21030
  const usdPrices = await this.deps.priceSource.fetchUsdPrices(mintsToPrice);
20793
21031
  const baseUsd = usdPrices[baseMint.toString()];
20794
21032
  if (!baseUsd) {
@@ -20837,18 +21075,23 @@ var ConsensusOracleService = class {
20837
21075
  });
20838
21076
  return sumPositionsByMint(positions);
20839
21077
  }
20840
- /** Accrued yield (base units) attributed to a holding via its configured accounts. */
20841
- async resolveYieldAmount(cfg, decimals) {
20842
- if (!this.deps.yieldTracker || !cfg?.yieldAccountNames?.length) return 0n;
20843
- const totalYieldUi = await this.deps.yieldTracker.getTotalOutstandingYield(
21078
+ /** Tracked external principal + outstanding yield attributed to a holding. */
21079
+ async resolveTrackedAmounts(cfg, decimals) {
21080
+ if (!this.deps.yieldTracker || !cfg?.yieldAccountNames?.length) {
21081
+ return { principalAmount: 0n, yieldAmount: 0n };
21082
+ }
21083
+ const tracked = await this.deps.yieldTracker.getTotalTrackedAmounts(
20844
21084
  cfg.yieldAccountNames
20845
21085
  );
20846
- return totalYieldUi > 0 ? (0, import_common41.fromUiAmount)(totalYieldUi, decimals) : 0n;
21086
+ return {
21087
+ principalAmount: tracked.principal > 0 ? (0, import_common41.fromUiAmount)(tracked.principal, decimals) : 0n,
21088
+ yieldAmount: tracked.outstandingYield > 0 ? (0, import_common41.fromUiAmount)(tracked.outstandingYield, decimals) : 0n
21089
+ };
20847
21090
  }
20848
21091
  // ── Aggregation ──────────────────────────────────────────────────────────────
20849
- async buildUpdates(consensusHoldings, inputs) {
21092
+ async buildUpdates(reportableHoldings, inputs) {
20850
21093
  const updates = [];
20851
- for (const entry of consensusHoldings) {
21094
+ for (const entry of reportableHoldings) {
20852
21095
  updates.push(await this.buildHoldingUpdate(entry, inputs));
20853
21096
  }
20854
21097
  return updates;
@@ -20857,18 +21100,25 @@ var ConsensusOracleService = class {
20857
21100
  const { holding, holdingIndex } = entry;
20858
21101
  const mint = (0, import_common41.fromWeb3Pk)(holding.mint);
20859
21102
  const mintKey = mint.toString();
20860
- const usd = coerceBool3(holding.isBase) ? inputs.baseUsd : inputs.usdPrices[mintKey];
20861
- if (!usd) {
20862
- throw new Error(`no USD price for holding ${mint}`);
21103
+ const oracleType = variantName2(holding.priceOracleType).toLowerCase();
21104
+ let usd;
21105
+ let price;
21106
+ if (oracleType === CONSENSUS_ORACLE_VARIANT) {
21107
+ usd = coerceBool3(holding.isBase) ? inputs.baseUsd : inputs.usdPrices[mintKey];
21108
+ if (!usd) throw new Error(`no USD price for holding ${mint}`);
21109
+ price = priceInAccountingUnit(usd, inputs.baseUsd, inputs.assetDecimals);
21110
+ } else {
21111
+ price = toBigInt2(holding.price);
21112
+ if (price <= 0n) {
21113
+ throw new Error(
21114
+ `live-oracle holding ${mint} has no on-chain price to preserve`
21115
+ );
21116
+ }
21117
+ usd = Number(price) / 10 ** inputs.assetDecimals * inputs.baseUsd;
20863
21118
  }
20864
- const price = priceInAccountingUnit(
20865
- usd,
20866
- inputs.baseUsd,
20867
- inputs.assetDecimals
20868
- );
20869
21119
  const walletAmount = inputs.walletBalances[mintKey] ?? 0n;
20870
21120
  const lpAmount = inputs.lpByMint.get(mintKey) ?? 0n;
20871
- const yieldAmount = await this.resolveYieldAmount(
21121
+ const { principalAmount, yieldAmount } = await this.resolveTrackedAmounts(
20872
21122
  inputs.configByMint.get(mintKey),
20873
21123
  holding.decimals
20874
21124
  );
@@ -20881,7 +21131,8 @@ var ConsensusOracleService = class {
20881
21131
  walletAmount,
20882
21132
  lpAmount,
20883
21133
  yieldAmount,
20884
- externalAmount: walletAmount + lpAmount + yieldAmount
21134
+ trackedPrincipalAmount: principalAmount,
21135
+ externalAmount: walletAmount + lpAmount + principalAmount + yieldAmount
20885
21136
  };
20886
21137
  }
20887
21138
  // ── Settlement ───────────────────────────────────────────────────────────────
@@ -20893,7 +21144,7 @@ var ConsensusOracleService = class {
20893
21144
  );
20894
21145
  for (const update of updates) {
20895
21146
  log(
20896
- ` holding #${update.holdingIndex} ${update.mint}: price=${update.price} external_amount=${update.externalAmount} [wallet=${update.walletAmount} lp=${update.lpAmount} yield=${update.yieldAmount}]`
21147
+ ` holding #${update.holdingIndex} ${update.mint}: price=${update.price} external_amount=${update.externalAmount} [wallet=${update.walletAmount} lp=${update.lpAmount} principal=${update.trackedPrincipalAmount} yield=${update.yieldAmount}]`
20897
21148
  );
20898
21149
  }
20899
21150
  await this.logProspectiveApy(vault, updates, vaultState, log);
@@ -20922,12 +21173,13 @@ var ConsensusOracleService = class {
20922
21173
  };
20923
21174
  }
20924
21175
  /**
20925
- * Fetch the current settled NAV from the oracle account and log the APY
20926
- * implied by the prospective update, so MaxApyExceeded failures are diagnosable.
21176
+ * Fetch the current settlement timestamp and log the APY implied by the
21177
+ * prospective update, so MaxApyExceeded failures are diagnosable.
20927
21178
  *
20928
21179
  * NAV is expressed in the vault's accounting unit (scaled by assetDecimals).
20929
- * prospective NAV = Σ ((localAmount_i + externalAmount_i) * price_i)
20930
- * / 10^assetDecimals
21180
+ * Mirrors `gross_nav_from_holdings`: consensus updates overlay their target
21181
+ * slots, while live-priced and otherwise unchanged holdings retain their
21182
+ * current price/external amount.
20931
21183
  */
20932
21184
  async logProspectiveApy(vault, updates, vaultState, log) {
20933
21185
  try {
@@ -20936,22 +21188,39 @@ var ConsensusOracleService = class {
20936
21188
  });
20937
21189
  const data = oracle.data;
20938
21190
  if (!data || data.length < 16) return;
20939
- const settledNav = readI64LE(data, 0);
20940
21191
  const settledNavTs = readI64LE(data, 8);
20941
- if (settledNav <= 0n || settledNavTs <= 0n) return;
20942
- const scale = BigInt(10 ** vaultState.config.assetDecimals);
20943
- const prospectiveNav = updates.reduce((sum, u) => {
20944
- const holding = vaultState.holdings[u.holdingIndex];
20945
- const localAmount = BigInt(holding?.localAmount?.toString() ?? "0");
20946
- return sum + (localAmount + u.externalAmount) * u.price / scale;
20947
- }, 0n);
21192
+ if (settledNavTs <= 0n) return;
21193
+ const updatesByIndex = new Map(
21194
+ updates.map((update) => [update.holdingIndex, update])
21195
+ );
21196
+ const prospectiveNav = vaultState.holdings.reduce(
21197
+ (sum, holding, holdingIndex) => {
21198
+ const mint = (0, import_common41.fromWeb3Pk)(holding.mint).toString();
21199
+ if (mint === SYSTEM_PROGRAM2) return sum;
21200
+ const update = updatesByIndex.get(holdingIndex);
21201
+ const localAmount = toBigInt2(holding.localAmount);
21202
+ const externalAmount = update ? update.externalAmount : toBigInt2(holding.externalAmount);
21203
+ const amount = localAmount + externalAmount;
21204
+ if (amount === 0n) return sum;
21205
+ const usesConsensusPrice = variantName2(holding.priceOracleType).toLowerCase() === CONSENSUS_ORACLE_VARIANT;
21206
+ const price = update && usesConsensusPrice ? update.price : toBigInt2(holding.price);
21207
+ if (price <= 0n) return sum;
21208
+ const scale = 10n ** BigInt(holding.decimals);
21209
+ return sum + amount * price / scale;
21210
+ },
21211
+ 0n
21212
+ );
21213
+ const currentTvl = toBigInt2(vaultState.accounting?.tvl);
21214
+ const accruedApyBalance = coerceBool3(vaultState.tranchingEnabled) ? toBigInt2(vaultState.config.apy?.accruedApyBalance) : 0n;
21215
+ const physicalNavBefore = currentTvl + (accruedApyBalance > 0n ? accruedApyBalance : 0n);
21216
+ if (physicalNavBefore <= 0n) return;
20948
21217
  const nowSecs = BigInt(Math.floor(this.clock.now() / 1e3));
20949
21218
  const elapsedSecs = nowSecs - settledNavTs;
20950
21219
  if (elapsedSecs <= 0n) return;
20951
21220
  const SECONDS_PER_YEAR3 = 365n * 24n * 3600n;
20952
- const apyBps = (prospectiveNav - settledNav) * SECONDS_PER_YEAR3 * 10000n / (settledNav * elapsedSecs);
21221
+ const apyBps = (prospectiveNav - physicalNavBefore) * SECONDS_PER_YEAR3 * 10000n / (physicalNavBefore * elapsedSecs);
20953
21222
  log(
20954
- `vault ${vault}: prospective NAV ${prospectiveNav} vs settled ${settledNav} (elapsed ${elapsedSecs}s) \u2192 implied APY ${apyBps}bps`
21223
+ `vault ${vault}: prospective NAV ${prospectiveNav} vs physical baseline ${physicalNavBefore} (elapsed ${elapsedSecs}s) \u2192 implied APY ${apyBps}bps`
20955
21224
  );
20956
21225
  } catch {
20957
21226
  }
@@ -20991,10 +21260,12 @@ function signerInOracleData(data, signerBytes) {
20991
21260
  }
20992
21261
  return false;
20993
21262
  }
20994
- function selectConsensusHoldings(holdings, excludedMints = /* @__PURE__ */ new Set()) {
21263
+ function selectReportableHoldings(holdings, excludedMints = /* @__PURE__ */ new Set()) {
20995
21264
  return holdings.map((holding, holdingIndex) => ({ holding, holdingIndex })).filter(({ holding }) => {
20996
21265
  const mint = (0, import_common41.fromWeb3Pk)(holding.mint);
20997
- return mint !== SYSTEM_PROGRAM2 && !excludedMints.has(mint.toString()) && variantName2(holding.priceOracleType).toLowerCase() === CONSENSUS_ORACLE_VARIANT;
21266
+ return mint !== SYSTEM_PROGRAM2 && !excludedMints.has(mint.toString()) && REPORTABLE_ORACLE_VARIANTS.has(
21267
+ variantName2(holding.priceOracleType).toLowerCase()
21268
+ );
20998
21269
  });
20999
21270
  }
21000
21271
  function filterTargetHoldings(target, excludedMints) {
@@ -21036,6 +21307,7 @@ var MockYieldTracker = class {
21036
21307
  this.clock = opts.clock ?? systemClock;
21037
21308
  for (const [name, cfg] of Object.entries(opts.accounts ?? {})) {
21038
21309
  this.accounts.set(name, {
21310
+ tag: cfg.tag,
21039
21311
  aprBps: cfg.aprBps,
21040
21312
  initialPrincipal: cfg.principal ?? 0,
21041
21313
  startTs: cfg.startTs ?? this.clock.now(),
@@ -21047,6 +21319,7 @@ var MockYieldTracker = class {
21047
21319
  /** Register (or replace) a tracked account. */
21048
21320
  setAccount(name, cfg) {
21049
21321
  this.accounts.set(name, {
21322
+ tag: cfg.tag,
21050
21323
  aprBps: cfg.aprBps,
21051
21324
  initialPrincipal: cfg.principal ?? 0,
21052
21325
  startTs: cfg.startTs ?? this.clock.now(),
@@ -21061,6 +21334,9 @@ var MockYieldTracker = class {
21061
21334
  }
21062
21335
  return state;
21063
21336
  }
21337
+ async getAccountNamesForVault(vault) {
21338
+ return [...this.accounts.entries()].filter(([, state]) => state.tag === vault.toString()).map(([name]) => name);
21339
+ }
21064
21340
  async getYieldPayments(accountName) {
21065
21341
  return this.require(accountName).yieldPayments.map((payment) => ({
21066
21342
  id: payment.paymentId,
@@ -21122,6 +21398,16 @@ var MockYieldTracker = class {
21122
21398
  }
21123
21399
  return total;
21124
21400
  }
21401
+ async getTotalTrackedAmounts(accountNames, start, end) {
21402
+ let principal = 0;
21403
+ let outstandingYield = 0;
21404
+ for (const name of accountNames) {
21405
+ const snapshot2 = await this.getYield(name, start, end);
21406
+ principal += snapshot2.principalAtEnd;
21407
+ outstandingYield += snapshot2.outstandingYield;
21408
+ }
21409
+ return { principal, outstandingYield };
21410
+ }
21125
21411
  async getBalance(accountName, atTime) {
21126
21412
  const snapshot2 = await this.getYield(accountName, void 0, atTime);
21127
21413
  return snapshot2.principalAtEnd + snapshot2.outstandingYield;
@@ -21210,7 +21496,7 @@ function parseActiveSlots(externalLiquidity) {
21210
21496
  }
21211
21497
  return results;
21212
21498
  }
21213
- function toBigInt2(value) {
21499
+ function toBigInt3(value) {
21214
21500
  if (typeof value === "bigint") return value;
21215
21501
  if (value !== null && value !== void 0) return BigInt(String(value));
21216
21502
  return 0n;
@@ -21290,7 +21576,7 @@ var ExternalLiquidityIntegrityService = class {
21290
21576
  }
21291
21577
  const holdings = vaultState.holdings.filter((h) => {
21292
21578
  const mint = pubkeyStr(h.mint);
21293
- return !isZeroPubkey2(mint) && toBigInt2(h.localAmount) > 0n;
21579
+ return !isZeroPubkey2(mint) && toBigInt3(h.localAmount) > 0n;
21294
21580
  });
21295
21581
  if (holdings.length === 0) {
21296
21582
  log(`Vault ${vault} slot ${slot.index}: no local balance to deploy`);
@@ -21299,7 +21585,7 @@ var ExternalLiquidityIntegrityService = class {
21299
21585
  }
21300
21586
  for (const holding of holdings) {
21301
21587
  const mintAddress = (0, import_kit15.address)(pubkeyStr(holding.mint));
21302
- const localAmount = toBigInt2(holding.localAmount);
21588
+ const localAmount = toBigInt3(holding.localAmount);
21303
21589
  const marginfiAccounts = (0, import_marginfi2.marginfiAccountsForMint)(mintAddress);
21304
21590
  if (!marginfiAccounts) {
21305
21591
  log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perena/vault-sdk",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Vault program helpers for Bankineco integrations.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,6 +38,7 @@
38
38
  "create-vault": "tsx scripts/createVault.ts",
39
39
  "init-vault-atas": "tsx scripts/initVaultAtas.ts",
40
40
  "inspect": "tsx scripts/inspect.ts",
41
+ "vault-data": "tsx scripts/fetchVaultData.ts",
41
42
  "simulate-withdraw": "tsx scripts/simulate-withdraw.ts",
42
43
  "simulate-message": "tsx scripts/simulate-message.ts",
43
44
  "run-external-liquidity-service": "tsx scripts/runExternalLiquidityService.ts"
@@ -56,6 +57,7 @@
56
57
  },
57
58
  "devDependencies": {
58
59
  "common": "file:../common",
60
+ "jupiter": "file:../jupiter",
59
61
  "marginfi": "file:../marginfi",
60
62
  "@types/node": "^24.3.1",
61
63
  "prettier": "^2.6.2",