@perena/vault-sdk 1.0.48 → 1.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -565,9 +565,10 @@ The builder auto-detects external liquidity slot 0 and, when it finds an active
565
565
  Marginfi position for the asset, emits `execute_withdraw_from_external` instead
566
566
  of `execute_withdraw`. That route can pull from the vault's external position in
567
567
  the same transaction, so it draws on far more liquidity than the plain
568
- `execute_withdraw`: the external-liquidity integrity service keeps only ~0.5% of
569
- each holding in the vault's own token account (see
570
- `DEFAULT_TARGET_LOCAL_BPS`) and deploys the rest into Marginfi, so a plain
568
+ `execute_withdraw`: the external-liquidity integrity service targets local USDC
569
+ worth 1.5% of total vault TVL (`DEFAULT_TARGET_LOCAL_USDC_TVL_BPS`) and ~0.5% of
570
+ each other holding (`DEFAULT_TARGET_LOCAL_BPS`), deploying the rest into Marginfi.
571
+ It rebalances once local balances drift 20% from those targets, so a plain
571
572
  withdrawal is capped at that small local buffer and will fail for anything
572
573
  larger.
573
574
 
@@ -90,6 +90,29 @@ declare function decodeNestWithdrawalRequest(args: {
90
90
  txBase64: string;
91
91
  }): Promise<VaultTransactionPlan>;
92
92
 
93
+ interface NestDepositPlan extends VaultTransactionPlan {
94
+ /** Pass through to prepareVaultTransaction; the keeper's signatures are not reusable. */
95
+ ephemeralSignerKeys: PublicKey[];
96
+ }
97
+ /** Build a manager-funded deposit, with one event account for Squads to sign. */
98
+ declare function buildNestDepositRequest(args: {
99
+ connection: Connection;
100
+ owner: PublicKey;
101
+ rawAmountUsdc: bigint;
102
+ apiOptions?: NestApiOptions;
103
+ }): Promise<NestDepositPlan>;
104
+ /**
105
+ * Validate the USDC burn and Nest/Solana recipient. Replace the keeper rent payer
106
+ * with the manager, and expose the CCTP event signer for Squads PDA substitution.
107
+ * Nest remains trusted for the complete cross-chain payload and keeper delivery.
108
+ */
109
+ declare function decodeNestDepositRequest(args: {
110
+ connection: Connection;
111
+ owner: PublicKey;
112
+ rawAmountUsdc: bigint;
113
+ txBase64: string;
114
+ }): Promise<NestDepositPlan>;
115
+
93
116
  /**
94
117
  * {@link PriceSource} adapter over the `nest` package's price API, bound to the
95
118
  * Perena vault's Nest slug and share mint.
@@ -187,6 +210,8 @@ declare function prepareVaultTransaction(args: {
187
210
  /** Skip execution simulation when proposing a transaction that depends on future funding. */
188
211
  skipSimulation?: boolean;
189
212
  addressLookupTableAccounts?: AddressLookupTableAccount[];
213
+ /** Temporary signer keys used only in account metas, replaced by Squads PDAs. */
214
+ ephemeralSignerKeys?: readonly PublicKey[];
190
215
  }): Promise<PreparedVaultTransaction>;
191
216
 
192
217
  interface SquadsProposalUpload {
@@ -205,4 +230,4 @@ declare function prepareSquadsProposalUpload(args: {
205
230
  prepared: PreparedSquadsTransaction;
206
231
  }): Promise<SquadsProposalUpload>;
207
232
 
208
- export { NEST_RWA_SHARE_MINT, NEST_VAULT_SLUG, NestApiError, type NestApiOptions, type NestRedemptionQuote, type NestRedemptionStatus, type PreparedDirectTransaction, type PreparedSquadsTransaction, type PreparedVaultTransaction, type ResolvedSquadsWalletRoute, SquadsProposalExecutionSimulationError, type SquadsProposalUpload, type SquadsWalletRouteConfig, buildNestWithdrawalRequest, decodeNestWithdrawalRequest, fetchNestRedemptionQuote, fetchNestRedemptionStatus, findSquadsWalletRoute, prepareSquadsProposalUpload, prepareVaultTransaction, resolveSquadsWalletRoute, simulateSquadsProposalExecution, validateSquadsWalletRoutes, vaultAuthorityForWallet };
233
+ export { NEST_RWA_SHARE_MINT, NEST_VAULT_SLUG, NestApiError, type NestApiOptions, type NestDepositPlan, type NestRedemptionQuote, type NestRedemptionStatus, type PreparedDirectTransaction, type PreparedSquadsTransaction, type PreparedVaultTransaction, type ResolvedSquadsWalletRoute, SquadsProposalExecutionSimulationError, type SquadsProposalUpload, type SquadsWalletRouteConfig, buildNestDepositRequest, buildNestWithdrawalRequest, decodeNestDepositRequest, decodeNestWithdrawalRequest, fetchNestRedemptionQuote, fetchNestRedemptionStatus, findSquadsWalletRoute, prepareSquadsProposalUpload, prepareVaultTransaction, resolveSquadsWalletRoute, simulateSquadsProposalExecution, validateSquadsWalletRoutes, vaultAuthorityForWallet };
@@ -454,14 +454,148 @@ async function decodeNestWithdrawalRequest(args) {
454
454
  };
455
455
  }
456
456
 
457
- // src/utils/squads.ts
458
- import * as squads from "@sqds/multisig";
457
+ // src/services/nestDepositService.ts
458
+ import { address as address5 } from "@solana/kit";
459
459
  import {
460
+ ASSOCIATED_TOKEN_PROGRAM_ID as ASSOCIATED_TOKEN_PROGRAM_ID3,
461
+ createAssociatedTokenAccountIdempotentInstruction,
462
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync3,
463
+ TOKEN_PROGRAM_ID as TOKEN_PROGRAM_ID2
464
+ } from "@solana/spl-token";
465
+ import {
466
+ ComputeBudgetProgram as ComputeBudgetProgram2,
460
467
  PublicKey as PublicKey4,
461
- Transaction,
468
+ SystemProgram,
462
469
  TransactionMessage as TransactionMessage3,
463
470
  VersionedTransaction as VersionedTransaction3
464
471
  } from "@solana/web3.js";
472
+ var Usdc = new PublicKey4("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
473
+ var Messenger = new PublicKey4("CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe");
474
+ var Transmitter = new PublicKey4(
475
+ "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC"
476
+ );
477
+ var Router = Buffer.from(
478
+ "0000000000000000000000007de01896d36bea9cf072ac64e41685418941d8be",
479
+ "hex"
480
+ );
481
+ var Composer = Buffer.from("908dcb5531691c2124c54e30bb645cf11647090d", "hex");
482
+ var BurnWithHook = Buffer.from([111, 245, 62, 131, 204, 108, 223, 155]);
483
+ var PerenaAssetId = Buffer.from(
484
+ "355750bed2d05a1eb92ab578335cc3ea6d572dd5f57a086088c68667f11e50d2",
485
+ "hex"
486
+ );
487
+ var MintAndSend = Buffer.from("fe030ec4", "hex");
488
+ async function buildNestDepositRequest(args) {
489
+ if (args.rawAmountUsdc <= 0n || args.rawAmountUsdc > BigInt(Number.MAX_SAFE_INTEGER)) {
490
+ throw new Error(
491
+ "Nest deposit must be a positive, safely representable USDC base-unit amount"
492
+ );
493
+ }
494
+ const options = args.apiOptions ?? {};
495
+ const response = await (options.fetchFn ?? fetch)(
496
+ `${(options.baseUrl ?? NEST_API_BASE_URL).replace(
497
+ /\/$/,
498
+ ""
499
+ )}/solana/nest/mint/build-tx`,
500
+ {
501
+ method: "POST",
502
+ headers: { "Content-Type": "application/json" },
503
+ signal: options.signal,
504
+ body: JSON.stringify({
505
+ rawAmountUsdc: Number(args.rawAmountUsdc),
506
+ receiver: args.owner.toBase58(),
507
+ nestVaultSlug: NEST_VAULT_SLUG,
508
+ finality: "standard"
509
+ })
510
+ }
511
+ );
512
+ const payload = await response.json().catch(() => null);
513
+ if (!response.ok)
514
+ throw new Error(
515
+ `Nest deposit API (${response.status}): ${typeof payload?.error === "string" ? payload.error : response.statusText}`
516
+ );
517
+ if (typeof payload?.data?.txBase64 !== "string")
518
+ throw new Error("Nest API returned no deposit transaction");
519
+ return decodeNestDepositRequest({ ...args, txBase64: payload.data.txBase64 });
520
+ }
521
+ async function decodeNestDepositRequest(args) {
522
+ if (args.rawAmountUsdc <= 0n || args.rawAmountUsdc > BigInt(Number.MAX_SAFE_INTEGER)) {
523
+ throw new Error(
524
+ "Nest deposit must be a positive, safely representable USDC base-unit amount"
525
+ );
526
+ }
527
+ const message = VersionedTransaction3.deserialize(
528
+ Buffer.from(args.txBase64, "base64")
529
+ ).message;
530
+ if (!message.staticAccountKeys[0].equals(args.owner))
531
+ throw new Error("Nest deposit payer must be the manager");
532
+ const tables = await Promise.all(
533
+ message.addressTableLookups.map(async (lookup) => {
534
+ const { value } = await args.connection.getAddressLookupTable(
535
+ lookup.accountKey
536
+ );
537
+ if (!value)
538
+ throw new Error(`Nest lookup table not found: ${lookup.accountKey}`);
539
+ return value;
540
+ })
541
+ );
542
+ const instructions2 = TransactionMessage3.decompile(message, {
543
+ addressLookupTableAccounts: tables
544
+ }).instructions;
545
+ const shareMint = new PublicKey4(NEST_RWA_SHARE_MINT2);
546
+ const shareAta = getAssociatedTokenAddressSync3(shareMint, args.owner, true);
547
+ const usdcAta = getAssociatedTokenAddressSync3(Usdc, args.owner, true);
548
+ let burn;
549
+ for (const ix of instructions2) {
550
+ if (ix.programId.equals(ComputeBudgetProgram2.programId)) continue;
551
+ if (ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID3)) {
552
+ if (ix.data.length !== 1 || ix.data[0] !== 1 || !ix.keys[0]?.pubkey.equals(args.owner) || !ix.keys[1]?.pubkey.equals(shareAta) || !ix.keys[2]?.pubkey.equals(args.owner) || !ix.keys[3]?.pubkey.equals(shareMint) || !ix.keys[4]?.pubkey.equals(SystemProgram.programId) || !ix.keys[5]?.pubkey.equals(TOKEN_PROGRAM_ID2))
553
+ throw new Error("Unexpected Nest deposit token account creation");
554
+ continue;
555
+ }
556
+ const d = ix.data;
557
+ const matches = (index, key) => ix.keys[index]?.pubkey.equals(key);
558
+ if (burn || !ix.programId.equals(Messenger) || ix.keys.length !== 18 || d.length < 316 || !d.subarray(0, 8).equals(BurnWithHook) || d.readBigUInt64LE(8) !== args.rawAmountUsdc || d.readUInt32LE(16) !== 22 || !d.subarray(20, 52).equals(Router) || !d.subarray(52, 84).equals(Router) || d.readBigUInt64LE(84) !== 0n || d.readUInt32LE(92) !== 2e3 || d.readUInt32LE(96) !== d.length - 100 || !d.subarray(100, 120).equals(Composer) || !d.subarray(120, 124).equals(MintAndSend) || !d.subarray(124, 156).equals(PerenaAssetId) || BigInt(`0x${d.subarray(156, 188).toString("hex")}`) !== args.rawAmountUsdc || BigInt(`0x${d.subarray(188, 220).toString("hex")}`) !== 128n || !d.subarray(220, 252).equals(Buffer.concat([Buffer.alloc(12), Composer])) || BigInt(`0x${d.subarray(252, 284).toString("hex")}`) !== 30168n || !d.subarray(284, 316).equals(args.owner.toBuffer()) || !matches(0, args.owner) || !matches(3, usdcAta) || !matches(10, Usdc) || !matches(12, Transmitter) || !matches(13, Messenger) || !matches(14, TOKEN_PROGRAM_ID2) || !matches(15, SystemProgram.programId) || !matches(17, Messenger) || ![0, 1, 11].every((index) => ix.keys[index].isSigner) || ix.keys.some(
559
+ (meta, index) => meta.isSigner && ![0, 1, 11].includes(index)
560
+ )) {
561
+ throw new Error(
562
+ "Unexpected Nest deposit instruction, amount, or recipient"
563
+ );
564
+ }
565
+ burn = ix;
566
+ }
567
+ if (!burn) throw new Error("Nest deposit must contain one CCTP burn");
568
+ const eventKey = burn.keys[11].pubkey;
569
+ if (eventKey.equals(args.owner) || burn.keys.some(
570
+ (meta, index) => index !== 11 && meta.pubkey.equals(eventKey)
571
+ )) {
572
+ throw new Error("Nest deposit event signer must be a separate account");
573
+ }
574
+ burn.keys[1] = { pubkey: args.owner, isSigner: true, isWritable: true };
575
+ return {
576
+ instructions: [
577
+ createAssociatedTokenAccountIdempotentInstruction(
578
+ args.owner,
579
+ shareAta,
580
+ args.owner,
581
+ shareMint
582
+ ),
583
+ burn
584
+ ].map(toKitInstruction),
585
+ lookupTables: tables.map((table) => address5(table.key.toBase58())),
586
+ ephemeralSignerKeys: [eventKey]
587
+ };
588
+ }
589
+
590
+ // src/utils/squads.ts
591
+ import * as squads from "@sqds/multisig";
592
+ import {
593
+ PublicKey as PublicKey5,
594
+ Transaction,
595
+ TransactionInstruction as TransactionInstruction2,
596
+ TransactionMessage as TransactionMessage4,
597
+ VersionedTransaction as VersionedTransaction4
598
+ } from "@solana/web3.js";
465
599
  var SQUADS_SIMULATION_COMMITMENT = "processed";
466
600
  var SquadsProposalExecutionSimulationError = class extends Error {
467
601
  constructor(transactionError, logs, unitsConsumed) {
@@ -485,8 +619,8 @@ function validateVaultIndex(vaultIndex) {
485
619
  }
486
620
  function resolveSquadsWalletRoute(route) {
487
621
  validateVaultIndex(route.vaultIndex);
488
- const wallet = new PublicKey4(route.wallet);
489
- const multisigPda = new PublicKey4(route.multisigPda);
622
+ const wallet = new PublicKey5(route.wallet);
623
+ const multisigPda = new PublicKey5(route.multisigPda);
490
624
  const [vaultPda] = squads.getVaultPda({
491
625
  multisigPda,
492
626
  index: route.vaultIndex
@@ -494,7 +628,7 @@ function resolveSquadsWalletRoute(route) {
494
628
  return { ...route, wallet, multisigPda, vaultPda };
495
629
  }
496
630
  function findSquadsWalletRoute(wallet, routes) {
497
- const walletKey = typeof wallet === "string" ? new PublicKey4(wallet) : wallet;
631
+ const walletKey = typeof wallet === "string" ? new PublicKey5(wallet) : wallet;
498
632
  const route = routes.find(
499
633
  (candidate) => candidate.wallet === walletKey.toBase58()
500
634
  );
@@ -512,18 +646,18 @@ function validateSquadsWalletRoutes(routes) {
512
646
  }
513
647
  }
514
648
  function vaultAuthorityForWallet(wallet, routes) {
515
- const walletKey = typeof wallet === "string" ? new PublicKey4(wallet) : wallet;
649
+ const walletKey = typeof wallet === "string" ? new PublicKey5(wallet) : wallet;
516
650
  return findSquadsWalletRoute(walletKey, routes)?.vaultPda ?? walletKey;
517
651
  }
518
652
  async function simulateSquadsProposalExecution(args) {
519
653
  const { connection, feePayer, instructions: instructions2 } = args;
520
654
  const recentBlockhash = args.recentBlockhash ?? (await connection.getLatestBlockhash(SQUADS_SIMULATION_COMMITMENT)).blockhash;
521
- const message = new TransactionMessage3({
655
+ const message = new TransactionMessage4({
522
656
  payerKey: feePayer,
523
657
  recentBlockhash,
524
658
  instructions: [...instructions2]
525
659
  }).compileToV0Message(args.addressLookupTableAccounts);
526
- const transaction = new VersionedTransaction3(message);
660
+ const transaction = new VersionedTransaction4(message);
527
661
  const { value } = await connection.simulateTransaction(transaction, {
528
662
  sigVerify: false,
529
663
  commitment: SQUADS_SIMULATION_COMMITMENT,
@@ -539,9 +673,13 @@ async function simulateSquadsProposalExecution(args) {
539
673
  return value;
540
674
  }
541
675
  async function prepareVaultTransaction(args) {
542
- const { connection, proposer, instructions: instructions2 } = args;
676
+ const { connection, proposer } = args;
677
+ let instructions2 = [...args.instructions];
678
+ const ephemeralKeys = args.ephemeralSignerKeys ?? [];
543
679
  const route = findSquadsWalletRoute(proposer, args.squadsRoutes ?? []);
544
680
  if (!route) {
681
+ if (ephemeralKeys.length)
682
+ throw new Error("Ephemeral signers require a Squads route");
545
683
  return {
546
684
  kind: "direct",
547
685
  transaction: new Transaction().add(...instructions2),
@@ -569,6 +707,44 @@ async function prepareVaultTransaction(args) {
569
707
  );
570
708
  }
571
709
  const transactionIndex = BigInt(multisig.transactionIndex.toString()) + 1n;
710
+ if (ephemeralKeys.length > 255 || new Set(ephemeralKeys.map(String)).size !== ephemeralKeys.length) {
711
+ throw new Error("Invalid Squads ephemeral signer keys");
712
+ }
713
+ const [transactionPda] = squads.getTransactionPda({
714
+ multisigPda: route.multisigPda,
715
+ index: transactionIndex
716
+ });
717
+ const replacements = /* @__PURE__ */ new Map();
718
+ ephemeralKeys.forEach((key, index) => {
719
+ if (key.equals(proposer) || key.equals(route.vaultPda) || !instructions2.some(
720
+ (ix) => ix.keys.some((meta) => meta.isSigner && meta.pubkey.equals(key))
721
+ ) || instructions2.some(
722
+ (ix) => ix.programId.equals(key) || ix.data.includes(key.toBuffer())
723
+ )) {
724
+ throw new Error(
725
+ "Ephemeral signer must be a separate account-meta signer, never embedded in instruction data"
726
+ );
727
+ }
728
+ replacements.set(
729
+ key.toBase58(),
730
+ squads.getEphemeralSignerPda({
731
+ transactionPda,
732
+ ephemeralSignerIndex: index
733
+ })[0]
734
+ );
735
+ });
736
+ if (replacements.size) {
737
+ instructions2 = instructions2.map(
738
+ (ix) => new TransactionInstruction2({
739
+ programId: ix.programId,
740
+ data: ix.data,
741
+ keys: ix.keys.map((meta) => ({
742
+ ...meta,
743
+ pubkey: replacements.get(meta.pubkey.toBase58()) ?? meta.pubkey
744
+ }))
745
+ })
746
+ );
747
+ }
572
748
  const { blockhash } = await connection.getLatestBlockhash("confirmed");
573
749
  if (!args.skipSimulation) {
574
750
  await simulateSquadsProposalExecution({
@@ -579,7 +755,7 @@ async function prepareVaultTransaction(args) {
579
755
  addressLookupTableAccounts: args.addressLookupTableAccounts
580
756
  });
581
757
  }
582
- const transactionMessage = new TransactionMessage3({
758
+ const transactionMessage = new TransactionMessage4({
583
759
  payerKey: route.vaultPda,
584
760
  recentBlockhash: blockhash,
585
761
  instructions: [...instructions2]
@@ -589,7 +765,7 @@ async function prepareVaultTransaction(args) {
589
765
  transactionIndex,
590
766
  creator: proposer,
591
767
  vaultIndex: route.vaultIndex,
592
- ephemeralSigners: 0,
768
+ ephemeralSigners: ephemeralKeys.length,
593
769
  transactionMessage,
594
770
  addressLookupTableAccounts: args.addressLookupTableAccounts,
595
771
  memo: route.memo
@@ -625,8 +801,8 @@ async function prepareVaultTransaction(args) {
625
801
  // src/utils/squadsBuffer.ts
626
802
  import * as squads2 from "@sqds/multisig";
627
803
  import {
628
- PublicKey as PublicKey5,
629
- SystemProgram,
804
+ PublicKey as PublicKey6,
805
+ SystemProgram as SystemProgram2,
630
806
  Transaction as Transaction2
631
807
  } from "@solana/web3.js";
632
808
  var PacketSize = 1232;
@@ -637,7 +813,7 @@ async function prepareSquadsProposalUpload(args) {
637
813
  const transaction = prepared.transaction;
638
814
  const sizeProbe = new Transaction2({
639
815
  feePayer: proposer,
640
- recentBlockhash: PublicKey5.default.toBase58()
816
+ recentBlockhash: PublicKey6.default.toBase58()
641
817
  }).add(...transaction.instructions);
642
818
  let fits = false;
643
819
  try {
@@ -657,7 +833,7 @@ async function prepareSquadsProposalUpload(args) {
657
833
  let bufferPda;
658
834
  let bufferIndex = 0;
659
835
  for (; bufferIndex <= 255; bufferIndex++) {
660
- const [candidate] = PublicKey5.findProgramAddressSync(
836
+ const [candidate] = PublicKey6.findProgramAddressSync(
661
837
  [
662
838
  Buffer.from("multisig"),
663
839
  prepared.multisigPda.toBuffer(),
@@ -718,7 +894,7 @@ async function prepareSquadsProposalUpload(args) {
718
894
  vaultTransactionCreateItemTransaction: prepared.vaultTransactionPda,
719
895
  vaultTransactionCreateItemCreator: proposer,
720
896
  vaultTransactionCreateItemRentPayer: proposer,
721
- vaultTransactionCreateItemSystemProgram: SystemProgram.programId,
897
+ vaultTransactionCreateItemSystemProgram: SystemProgram2.programId,
722
898
  transactionBuffer: bufferPda,
723
899
  creator: proposer
724
900
  },
@@ -743,7 +919,9 @@ export {
743
919
  NEST_VAULT_SLUG,
744
920
  NestApiError,
745
921
  SquadsProposalExecutionSimulationError,
922
+ buildNestDepositRequest,
746
923
  buildNestWithdrawalRequest,
924
+ decodeNestDepositRequest,
747
925
  decodeNestWithdrawalRequest,
748
926
  fetchNestRedemptionQuote,
749
927
  fetchNestRedemptionStatus,