@elemental-stv-core/sdk 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fetchJlpPoolDetails = fetchJlpPoolDetails;
4
4
  exports.computeAddLiquidityFee = computeAddLiquidityFee;
5
5
  exports.fetchJlpCustodyData = fetchJlpCustodyData;
6
+ exports.computeEffectiveUsd = computeEffectiveUsd;
7
+ exports.fetchJlpEffectiveWeights = fetchJlpEffectiveWeights;
6
8
  const web3_js_1 = require("@solana/web3.js");
7
9
  const constants_1 = require("./constants");
8
10
  const CUSTODIES = [
@@ -403,3 +405,170 @@ async function fetchJlpCustodyData(connection) {
403
405
  custodies: results,
404
406
  };
405
407
  }
408
+ /** Map JLP custody internal name to public symbol used by effective-weights API. */
409
+ function toEffSymbol(name) {
410
+ return name === "WBTC" ? "BTC" : name;
411
+ }
412
+ /** Convert a float USD price to an 8-decimal-scaled bigint.
413
+ *
414
+ * Uses 12-decimal intermediate to preserve sub-1¢ precision before truncation. */
415
+ function priceToScaled8(priceFloat) {
416
+ if (!isFinite(priceFloat) || priceFloat <= 0)
417
+ return 0n;
418
+ // Round to 12 decimals first to avoid float artifacts, then scale to 8.
419
+ const scaled12 = BigInt(Math.round(priceFloat * 1e12));
420
+ return scaled12 / 10000n; // 12 → 8 decimals
421
+ }
422
+ /** Compute effective USD (price-sensitivity-weighted) for a single custody.
423
+ *
424
+ * Formula (in 8-decimal USD):
425
+ * netDebtTokens = max(debt − borrowLendInterestsAccrued, 0) / DEBT_MULTIPLIER
426
+ * spot = (owned + netDebtTokens − locked) × price_8dec / 10^decimals
427
+ * short = shortSize_6dec × price_8dec / avgShortPrice_6dec (zeroed if either operand is 0)
428
+ * effUsd = max(spot, 0) + short
429
+ *
430
+ * Algebra for unit normalization:
431
+ * - owned/locked/netDebt is in 10^decimals base units; price is 10^8 USD.
432
+ * spot = (owned + netDebt − locked) × price_8dec / 10^decimals → 10^8 USD ✓
433
+ * - debt is stored as u128 scaled by DEBT_MULTIPLIER (1e9) on top of 10^decimals.
434
+ * netDebtTokens = (debt − borrowLendInterestsAccrued) / DEBT_MULTIPLIER
435
+ * (integer division — small rounding under one base unit, acceptable).
436
+ * - shortSize and avgShortPrice are both 10^6; ratio is dimensionless.
437
+ * short = shortSize × price_8dec / avgShortPrice → 10^8 USD ✓
438
+ */
439
+ function computeEffectiveUsd(input) {
440
+ const { owned, locked, debt, borrowLendInterestsAccrued, shortSizeUsd, avgShortPriceUsd, decimals, priceScaled8, } = input;
441
+ const tokScale = 10n ** BigInt(decimals);
442
+ // Net debt tokens: (debt − interestsAccrued) / DEBT_MULTIPLIER, clamped to 0.
443
+ // Matches `calculateDebtTokens` in `calculateCustodyAum` to keep both code paths
444
+ // consistent. DEBT_MULTIPLIER is 1e9, scaled before token decimals.
445
+ const DEBT_MULTIPLIER_BIG = 1000000000n; // 1e9, matches DEBT_MULTIPLIER constant
446
+ let netDebtTokens = 0n;
447
+ if (debt > borrowLendInterestsAccrued) {
448
+ netDebtTokens = (debt - borrowLendInterestsAccrued) / DEBT_MULTIPLIER_BIG;
449
+ }
450
+ // Spot term: (owned + netDebt − locked) × price / 10^decimals
451
+ // Use signed bigint subtraction; clamp to 0 if (owned + netDebt) < locked.
452
+ const netTokens = owned + netDebtTokens - locked; // bigint — can go negative
453
+ let spotEffUsd;
454
+ let spotClamped = false;
455
+ if (netTokens <= 0n) {
456
+ spotEffUsd = 0n;
457
+ if (netTokens < 0n)
458
+ spotClamped = true;
459
+ }
460
+ else {
461
+ spotEffUsd = (netTokens * priceScaled8) / tokScale;
462
+ }
463
+ // Short term: only contributes when both shortSize and avgShortPrice > 0.
464
+ // Numerator stays in u128 range comfortably (shortSize_6dec × price_8dec).
465
+ let shortEffUsd = 0n;
466
+ if (shortSizeUsd > 0n && avgShortPriceUsd > 0n && priceScaled8 > 0n) {
467
+ shortEffUsd = (shortSizeUsd * priceScaled8) / avgShortPriceUsd;
468
+ }
469
+ return {
470
+ spotEffUsd,
471
+ shortEffUsd,
472
+ effUsd: spotEffUsd + shortEffUsd,
473
+ spotClamped,
474
+ netDebtTokens,
475
+ };
476
+ }
477
+ /** Compute basis-point shares for each effective USD value, summing to 10000.
478
+ *
479
+ * Uses largest-remainder allocation to guarantee the bps sum equals exactly 10000
480
+ * even with rounding. (Naive `round(eff_i / total × 10000)` per-asset can drift
481
+ * by up to `n` bps cumulatively.) Returns 0s for all assets if total is 0.
482
+ */
483
+ function computeBpsShares(effs) {
484
+ const total = effs.reduce((s, e) => s + e, 0n);
485
+ if (total === 0n)
486
+ return effs.map(() => 0);
487
+ // Floor allocation
488
+ const exact = effs.map((e) => Number((e * 10000n) / total));
489
+ const allocated = exact.reduce((s, b) => s + b, 0);
490
+ let remaining = 10000 - allocated;
491
+ if (remaining <= 0)
492
+ return exact;
493
+ // Distribute remaining bps to the assets with largest fractional remainders.
494
+ // Compute remainder = (e × 10000) mod total, ranked descending.
495
+ const remainders = effs.map((e, i) => ({
496
+ i,
497
+ rem: (e * 10000n) % total,
498
+ }));
499
+ remainders.sort((a, b) => (a.rem === b.rem ? 0 : a.rem > b.rem ? -1 : 1));
500
+ const result = exact.slice();
501
+ for (let k = 0; k < remainders.length && remaining > 0; k++) {
502
+ result[remainders[k].i]++;
503
+ remaining--;
504
+ }
505
+ return result;
506
+ }
507
+ /**
508
+ * Fetch JLP pool effective-weight composition.
509
+ *
510
+ * Returns price-sensitivity-weighted shares per asset, dropping `guaranteed_usd`
511
+ * from the per-asset weight calculation. Suitable for rebalance planners that
512
+ * want to track the pool's true delta-1 composition rather than reported aumUsd.
513
+ *
514
+ * Reuses the same on-chain custody parser and Jupiter Price API source as
515
+ * `fetchJlpCustodyData` to keep numbers consistent across callers.
516
+ *
517
+ * @returns assets in fixed order: BTC, ETH, SOL, USDC, USDT.
518
+ */
519
+ async function fetchJlpEffectiveWeights(connection) {
520
+ const [custodies, pricesByMint] = await Promise.all([
521
+ fetchAllCustodies(connection),
522
+ fetchPrices(),
523
+ ]);
524
+ // Resolve price per custody (stables fall back to $1.00 when API is missing them).
525
+ const priceFloat = {};
526
+ for (const cfg of CUSTODIES) {
527
+ const p = pricesByMint[cfg.mintAddress];
528
+ if (p === undefined || p === null) {
529
+ if (cfg.isStable) {
530
+ priceFloat[cfg.name] = 1;
531
+ }
532
+ else {
533
+ throw new Error(`Missing price for non-stable asset ${cfg.name} (mint: ${cfg.mintAddress})`);
534
+ }
535
+ }
536
+ else {
537
+ priceFloat[cfg.name] = p;
538
+ }
539
+ }
540
+ // Compute effective USD per custody.
541
+ const computed = custodies.map((raw) => {
542
+ const effInput = {
543
+ owned: raw.owned,
544
+ locked: raw.locked,
545
+ debt: raw.debt,
546
+ borrowLendInterestsAccrued: raw.borrowLendInterestsAccrued,
547
+ shortSizeUsd: raw.globalShortSizes,
548
+ avgShortPriceUsd: raw.globalShortAveragePrices,
549
+ guaranteedUsd: raw.guaranteedUsd,
550
+ decimals: raw.cfg.decimals,
551
+ priceScaled8: priceToScaled8(priceFloat[raw.cfg.name]),
552
+ };
553
+ return { raw, out: computeEffectiveUsd(effInput) };
554
+ });
555
+ const effs = computed.map((c) => c.out.effUsd);
556
+ const bpsShares = computeBpsShares(effs);
557
+ const assets = computed.map((c, i) => ({
558
+ mint: new web3_js_1.PublicKey(c.raw.cfg.mintAddress),
559
+ symbol: toEffSymbol(c.raw.cfg.name),
560
+ effUsd: c.out.effUsd,
561
+ effShareBps: bpsShares[i],
562
+ spotClamped: c.out.spotClamped,
563
+ }));
564
+ const totalEffUsd = effs.reduce((s, e) => s + e, 0n);
565
+ // Total pool aumUsd in 8-decimal USD = aumUsd from `calculateCustodyAum` (float)
566
+ // re-aggregated. We reuse the existing path but normalize to bigint 8-decimals.
567
+ // Each calculateCustodyAum return is a float USD; multiply by 1e8 and round.
568
+ const totalAumFloat = custodies.reduce((sum, raw) => {
569
+ const a = calculateCustodyAum(raw, priceFloat[raw.cfg.name] ?? 1);
570
+ return sum + a.aumUsd;
571
+ }, 0);
572
+ const totalAumUsd = BigInt(Math.round(totalAumFloat * 1e8));
573
+ return { assets, totalEffUsd, totalAumUsd };
574
+ }
@@ -8,3 +8,4 @@ export * from "./remaining-accounts";
8
8
  export * from "./send-tx";
9
9
  export * from "./sol-wrap";
10
10
  export * from "./prices";
11
+ export * from "./lut";
@@ -24,3 +24,4 @@ __exportStar(require("./remaining-accounts"), exports);
24
24
  __exportStar(require("./send-tx"), exports);
25
25
  __exportStar(require("./sol-wrap"), exports);
26
26
  __exportStar(require("./prices"), exports);
27
+ __exportStar(require("./lut"), exports);
@@ -0,0 +1,41 @@
1
+ /**
2
+ * p-STV Core Address Lookup Table helpers.
3
+ *
4
+ * Packages the STATIC (per-program, not per-tx) p-stv-core accounts into a LUT
5
+ * so versioned transactions can compress them into a single 1-byte index.
6
+ *
7
+ * Usage:
8
+ * // Owner flow (one-time):
9
+ * const [createIx, lutAddress] = AddressLookupTableProgram.createLookupTable({...});
10
+ * const extendIxs = buildPstvCoreLutExtendIxs(payer, lutAddress);
11
+ * // send createIx + extendIxs
12
+ *
13
+ * // Consumer flow (every tx):
14
+ * const lutAccount = await resolvePstvCoreLut(connection, lutAddress);
15
+ * const msg = new TransactionMessage({...}).compileToV0Message([lutAccount]);
16
+ *
17
+ * NOTE: Only program IDs, the GlobalConfig PDA, and token/ATA/compute-budget
18
+ * program IDs. Per-vault PDAs (stv, evMint, vaultAta, withdrawRequest,
19
+ * managerRole) and strategy/lend state live in separate LUTs.
20
+ */
21
+ import { AddressLookupTableAccount, Connection, PublicKey, TransactionInstruction } from "@solana/web3.js";
22
+ /**
23
+ * Static p-stv-core accounts for the shared LUT. Order is deterministic so
24
+ * callers can cross-check against an on-chain LUT.
25
+ */
26
+ export declare function getPstvCoreLutAddresses(programId?: PublicKey): PublicKey[];
27
+ /**
28
+ * Build `extendLookupTable` ixs pushing every static p-stv-core account
29
+ * into `lutAddress`. Chunks at `MAX_ADDRS_PER_EXTEND` for safety.
30
+ *
31
+ * @param payer Wallet funding + authority of the LUT.
32
+ * @param lutAddress LUT to extend (must be created via
33
+ * `AddressLookupTableProgram.createLookupTable` first).
34
+ * @param programId Override for p-stv-core program ID (defaults to mainnet).
35
+ */
36
+ export declare function buildPstvCoreLutExtendIxs(payer: PublicKey, lutAddress: PublicKey, programId?: PublicKey): TransactionInstruction[];
37
+ /**
38
+ * Fetch an `AddressLookupTableAccount` by address for use in
39
+ * `TransactionMessage.compileToV0Message([lutAccount])`.
40
+ */
41
+ export declare function resolvePstvCoreLut(connection: Pick<Connection, "getAddressLookupTable">, lutAddress: PublicKey): Promise<AddressLookupTableAccount>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * p-STV Core Address Lookup Table helpers.
4
+ *
5
+ * Packages the STATIC (per-program, not per-tx) p-stv-core accounts into a LUT
6
+ * so versioned transactions can compress them into a single 1-byte index.
7
+ *
8
+ * Usage:
9
+ * // Owner flow (one-time):
10
+ * const [createIx, lutAddress] = AddressLookupTableProgram.createLookupTable({...});
11
+ * const extendIxs = buildPstvCoreLutExtendIxs(payer, lutAddress);
12
+ * // send createIx + extendIxs
13
+ *
14
+ * // Consumer flow (every tx):
15
+ * const lutAccount = await resolvePstvCoreLut(connection, lutAddress);
16
+ * const msg = new TransactionMessage({...}).compileToV0Message([lutAccount]);
17
+ *
18
+ * NOTE: Only program IDs, the GlobalConfig PDA, and token/ATA/compute-budget
19
+ * program IDs. Per-vault PDAs (stv, evMint, vaultAta, withdrawRequest,
20
+ * managerRole) and strategy/lend state live in separate LUTs.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.getPstvCoreLutAddresses = getPstvCoreLutAddresses;
24
+ exports.buildPstvCoreLutExtendIxs = buildPstvCoreLutExtendIxs;
25
+ exports.resolvePstvCoreLut = resolvePstvCoreLut;
26
+ const web3_js_1 = require("@solana/web3.js");
27
+ const spl_token_1 = require("@solana/spl-token");
28
+ const constants_1 = require("./constants");
29
+ const pda_1 = require("./pda");
30
+ /** Max addresses per `extendLookupTable` ix (Solana runtime limit). */
31
+ const MAX_ADDRS_PER_EXTEND = 20;
32
+ /**
33
+ * Static p-stv-core accounts for the shared LUT. Order is deterministic so
34
+ * callers can cross-check against an on-chain LUT.
35
+ */
36
+ function getPstvCoreLutAddresses(programId = constants_1.PROGRAM_ID) {
37
+ const [configPda] = (0, pda_1.findConfigPda)(programId);
38
+ return [
39
+ programId,
40
+ web3_js_1.SystemProgram.programId,
41
+ spl_token_1.TOKEN_PROGRAM_ID,
42
+ spl_token_1.TOKEN_2022_PROGRAM_ID,
43
+ spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID,
44
+ web3_js_1.ComputeBudgetProgram.programId,
45
+ configPda,
46
+ ];
47
+ }
48
+ /**
49
+ * Build `extendLookupTable` ixs pushing every static p-stv-core account
50
+ * into `lutAddress`. Chunks at `MAX_ADDRS_PER_EXTEND` for safety.
51
+ *
52
+ * @param payer Wallet funding + authority of the LUT.
53
+ * @param lutAddress LUT to extend (must be created via
54
+ * `AddressLookupTableProgram.createLookupTable` first).
55
+ * @param programId Override for p-stv-core program ID (defaults to mainnet).
56
+ */
57
+ function buildPstvCoreLutExtendIxs(payer, lutAddress, programId = constants_1.PROGRAM_ID) {
58
+ const addresses = getPstvCoreLutAddresses(programId);
59
+ const ixs = [];
60
+ for (let i = 0; i < addresses.length; i += MAX_ADDRS_PER_EXTEND) {
61
+ const chunk = addresses.slice(i, i + MAX_ADDRS_PER_EXTEND);
62
+ ixs.push(web3_js_1.AddressLookupTableProgram.extendLookupTable({
63
+ payer,
64
+ authority: payer,
65
+ lookupTable: lutAddress,
66
+ addresses: chunk,
67
+ }));
68
+ }
69
+ return ixs;
70
+ }
71
+ /**
72
+ * Fetch an `AddressLookupTableAccount` by address for use in
73
+ * `TransactionMessage.compileToV0Message([lutAccount])`.
74
+ */
75
+ async function resolvePstvCoreLut(connection, lutAddress) {
76
+ const { value } = await connection.getAddressLookupTable(lutAddress);
77
+ if (!value) {
78
+ throw new Error(`p-stv-core LUT not found at ${lutAddress.toBase58()} — create it first with buildPstvCoreLutExtendIxs`);
79
+ }
80
+ return value;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elemental-stv-core/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "TypeScript SDK for Elemental Vaults — p-STV Core, Elemental Lend, JLPD Strategy",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -46,6 +46,7 @@
46
46
  "scripts": {
47
47
  "build": "tsc",
48
48
  "clean": "rm -rf dist",
49
+ "test": "TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register --extensions ts \"src/**/__tests__/**/*.test.ts\"",
49
50
  "prepublishOnly": "npm run clean && npm run build"
50
51
  },
51
52
  "dependencies": {
@@ -55,6 +56,11 @@
55
56
  "@solana/spl-token": "^0.4.0",
56
57
  "@solana/web3.js": "^1.95.0",
57
58
  "@types/bn.js": "^5.2.0",
59
+ "@types/chai": "^4.3.20",
60
+ "@types/mocha": "^10.0.10",
61
+ "chai": "^4.5.0",
62
+ "mocha": "^10.8.2",
63
+ "ts-node": "^10.9.2",
58
64
  "typescript": "^5.3.3"
59
65
  },
60
66
  "peerDependencies": {