@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 +58 -23
- package/dist/index.js +370 -84
- package/package.json +3 -1
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
|
|
8961
|
-
"
|
|
8962
|
-
"
|
|
8963
|
-
"
|
|
8964
|
-
"
|
|
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
|
|
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": "
|
|
11905
|
+
"name": "paddingBool";
|
|
11904
11906
|
"docs": [
|
|
11905
|
-
"
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
16355
|
-
private
|
|
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
|
|
16362
|
-
*
|
|
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
|
-
*
|
|
16366
|
-
*
|
|
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
|
|
10467
|
-
"
|
|
10468
|
-
"
|
|
10469
|
-
"
|
|
10470
|
-
"
|
|
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
|
|
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": "
|
|
13412
|
+
"name": "_padding_bool",
|
|
13410
13413
|
"docs": [
|
|
13411
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
19883
|
+
if (settledPrice !== void 0) {
|
|
19884
|
+
holding.price = settledPrice;
|
|
19885
|
+
holding.lastUpdateTs = latestTs;
|
|
19886
|
+
}
|
|
19819
19887
|
holding.externalAmount = settledExternalAmount;
|
|
19820
|
-
holding.
|
|
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
|
-
|
|
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}
|
|
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 ?
|
|
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
|
-
|
|
19861
|
-
regularValue = settleTrancheNav(
|
|
19931
|
+
const regularGrossValue = settleTrancheNav(
|
|
19862
19932
|
args.tranche,
|
|
19863
|
-
|
|
19864
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
20551
|
-
log(`vault ${target.vault}: no
|
|
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
|
-
|
|
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
|
-
|
|
20685
|
+
reportableHoldings
|
|
20560
20686
|
);
|
|
20561
|
-
const updates = await this.buildUpdates(
|
|
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,
|
|
21015
|
+
async gatherPricingInputs(target, vaultState, reportableHoldings) {
|
|
20782
21016
|
const manager = (0, import_common41.fromWeb3Pk)(vaultState.roles.manager);
|
|
20783
|
-
const
|
|
20784
|
-
({ holding }) => (
|
|
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) :
|
|
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 =
|
|
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
|
-
/**
|
|
20841
|
-
async
|
|
20842
|
-
if (!this.deps.yieldTracker || !cfg?.yieldAccountNames?.length)
|
|
20843
|
-
|
|
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
|
|
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(
|
|
21092
|
+
async buildUpdates(reportableHoldings, inputs) {
|
|
20850
21093
|
const updates = [];
|
|
20851
|
-
for (const entry of
|
|
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
|
|
20861
|
-
|
|
20862
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
20926
|
-
*
|
|
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
|
-
*
|
|
20930
|
-
*
|
|
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 (
|
|
20942
|
-
const
|
|
20943
|
-
|
|
20944
|
-
|
|
20945
|
-
|
|
20946
|
-
|
|
20947
|
-
|
|
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 -
|
|
21221
|
+
const apyBps = (prospectiveNav - physicalNavBefore) * SECONDS_PER_YEAR3 * 10000n / (physicalNavBefore * elapsedSecs);
|
|
20953
21222
|
log(
|
|
20954
|
-
`vault ${vault}: prospective NAV ${prospectiveNav} vs
|
|
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
|
|
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()) &&
|
|
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
|
|
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) &&
|
|
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 =
|
|
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.
|
|
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",
|