@wireio/stake 2.1.1 → 2.2.2

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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AddressLookupTableAccount,
2
3
  Commitment,
3
4
  ComputeBudgetProgram,
4
5
  Connection,
@@ -220,138 +221,21 @@ export class SolanaStakingClient implements IStakingClient {
220
221
  // IStakingClient core methods
221
222
  // ---------------------------------------------------------------------
222
223
 
223
-
224
- async createVaultLiqsolAtaOneShot(params: {
225
- connection: Connection;
226
- payer: SolPubKey; // user's wallet pubkey (signer)
227
- vaultPda: SolPubKey; // squads vault PDA (off-curve owner)
228
- }): Promise<{ tx: Transaction; vaultAta: SolPubKey } | null> {
229
- const { connection, payer, vaultPda } = params;
230
-
231
- const liqsolMint = this.program.deriveLiqsolMintPda();
232
-
233
- const vaultAta = getAssociatedTokenAddressSync(
234
- liqsolMint,
235
- vaultPda,
236
- true, // allowOwnerOffCurve
237
- TOKEN_2022_PROGRAM_ID,
238
- ASSOCIATED_TOKEN_PROGRAM_ID,
239
- );
240
-
241
- // If it already exists, just no-op
242
- const info = await connection.getAccountInfo(vaultAta, "confirmed");
243
-
244
- if (info) return null;
245
-
246
- const ix = createAssociatedTokenAccountInstruction(
247
- payer, // payer = user
248
- vaultAta, // ata address
249
- vaultPda, // owner = vault
250
- liqsolMint,
251
- TOKEN_2022_PROGRAM_ID,
252
- ASSOCIATED_TOKEN_PROGRAM_ID,
253
- );
254
-
255
- const tx = new Transaction().add(ix);
256
- return { tx, vaultAta };
257
- }
258
-
259
- private async prepSquadsIxs(ix: TransactionInstruction): Promise<TransactionInstruction[]> {
260
- if (!this.squadsX) throw new Error('Attempting to wrap Squads instruction without SquadsX config');
261
-
262
- const multisigPda = this.squadsMultisigPDA!;
263
- const vaultPda = this.squadsVaultPDA!;
264
- const vaultIndex = this.squadsX?.vaultIndex ?? 0;
265
- const creator = this.solPubKey;
266
-
267
- // compute next transactionIndex
268
- const ms = await multisig.accounts.Multisig.fromAccountAddress(this.connection, multisigPda);
269
- const current = BigInt(ms.transactionIndex?.toString() ?? 0);
270
- const transactionIndex = current + BigInt(1);
271
-
272
- // inner message uses vault as payer
273
- // const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 });
274
- const { blockhash } = await this.connection.getLatestBlockhash("confirmed");
275
- const transactionMessage = new TransactionMessage({
276
- payerKey: vaultPda,
277
- recentBlockhash: blockhash,
278
- instructions: [ix],
279
- });
280
-
281
- const createVaultTxIx = await multisig.instructions.vaultTransactionCreate({
282
- multisigPda,
283
- transactionIndex,
284
- creator,
285
- vaultIndex,
286
- transactionMessage,
287
- ephemeralSigners: 0,
288
- });
289
-
290
- const createProposalIx = await multisig.instructions.proposalCreate({
291
- multisigPda,
292
- transactionIndex,
293
- creator,
294
- });
295
-
296
- return [createVaultTxIx, createProposalIx];
297
- }
298
-
299
224
  /**
300
225
  * Deposit native SOL into liqSOL (liqsol_core::deposit).
301
226
  * Handles tx build, sign, send, and confirmation.
302
227
  */
303
228
  async deposit(amountLamports: bigint): Promise<string> {
304
229
  this.ensureUser();
305
- if (amountLamports <= BigInt(0)) {
230
+ if (amountLamports <= BigInt(0))
306
231
  throw new Error('Deposit amount must be greater than zero.');
307
- }
308
232
 
309
233
  try {
310
- const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
311
- // console.log('amountLamports', amountLamports);
312
-
313
- // if (!!this.squadsX) {
314
-
315
- // const createVaultTx = await this.createVaultLiqsolAtaOneShot({
316
- // connection: this.connection,
317
- // payer: this.solPubKey,
318
- // vaultPda: this.squadsVaultPDA!,
319
- // })
320
-
321
- // if (createVaultTx !== null) {
322
- // // console.log('need to create vault ata first...');
323
- // const tx0 = new Transaction().add(createVaultTx.tx);
324
- // const prepared0 = await this.prepareTx(tx0);
325
- // const signed0 = await this.signTransaction(prepared0.tx);
326
- // const sent0 = await this.sendAndConfirmHttp(signed0, prepared0);
327
- // // console.log('create Vault ATA', sent0);
328
- // }
329
-
330
- // const ix = await this.depositClient.buildDepositTx(amountLamports, this.squadsVaultPDA!)
331
- // const squadIxs = await this.prepSquadsIxs(ix)
332
-
333
- // const tx1 = new Transaction().add(cuIx, squadIxs[0]);
334
- // const prepared1 = await this.prepareTx(tx1);
335
- // const signed1 = await this.signTransaction(prepared1.tx);
336
- // const sent1 = await this.sendAndConfirmHttp(signed1, prepared1);
337
- // console.log('SENT 1', sent1);
338
-
339
- // const tx2 = new Transaction().add(cuIx, squadIxs[1]);
340
- // const prepared2 = await this.prepareTx(tx2);
341
- // const signed2 = await this.signTransaction(prepared2.tx);
342
- // const sent2 = await this.sendAndConfirmHttp(signed2, prepared2);
343
- // console.log('SENT 2', sent2);
344
-
345
- // return sent2;
346
- // }
347
- // else {
348
- const ix = await this.depositClient.buildDepositTx(amountLamports)
349
- const tx = new Transaction().add(ix);
350
- const prepared = await this.prepareTx(tx);
351
- const signed = await this.signTransaction(prepared.tx);
352
-
353
- return this.sendAndConfirmHttp(signed, prepared);
354
- // }
234
+ const ix = await this.depositClient.buildDepositTx(amountLamports, this.squadsVaultPDA)
235
+ return !!this.squadsX
236
+ ? await this.sendSquadsIxs(ix)
237
+ : await this.buildAndSendIx(ix)
238
+
355
239
  } catch (err) {
356
240
  console.log(`Failed to deposit Solana: ${err}`);
357
241
  throw err;
@@ -370,23 +254,15 @@ export class SolanaStakingClient implements IStakingClient {
370
254
  */
371
255
  async withdraw(amountLamports: bigint): Promise<string> {
372
256
  this.ensureUser();
373
- if (amountLamports <= BigInt(0)) {
257
+ if (amountLamports <= BigInt(0))
374
258
  throw new Error('Withdraw amount must be greater than zero.');
375
- }
376
259
 
377
260
  try {
378
- // Build compute budget increase instruction
379
- const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
261
+ const ix = await this.depositClient.buildWithdrawTx(amountLamports, this.squadsVaultPDA)
262
+ return !!this.squadsX
263
+ ? await this.sendSquadsIxs(ix)
264
+ : await this.buildAndSendIx(ix)
380
265
 
381
- // Build the Outpost synd instruction
382
- const ix = await this.depositClient.buildWithdrawTx(amountLamports);
383
-
384
- // Wrap in a transaction and send
385
- const tx = new Transaction().add(cuIx, ix);
386
- const prepared = await this.prepareTx(tx);
387
- const signed = await this.signTransaction(prepared.tx);
388
-
389
- return this.sendAndConfirmHttp(signed, prepared);
390
266
  } catch (err) {
391
267
  console.log(`Failed to withdraw Solana: ${err}`);
392
268
  throw err;
@@ -398,26 +274,15 @@ export class SolanaStakingClient implements IStakingClient {
398
274
  */
399
275
  async stake(amountLamports: bigint): Promise<string> {
400
276
  this.ensureUser();
401
-
402
- if (!amountLamports || amountLamports <= BigInt(0)) {
277
+ if (!amountLamports || amountLamports <= BigInt(0))
403
278
  throw new Error('Stake amount must be greater than zero.');
404
- }
405
279
 
406
280
  try {
407
- const user = this.solPubKey;
408
-
409
- // Build compute budget increase instruction
410
- const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
411
-
412
- // Build the Outpost synd instruction
413
- const ix = await this.outpostClient.buildStakeIx(amountLamports, user);
281
+ const ix = await this.outpostClient.buildStakeIx(amountLamports, this.squadsVaultPDA)
282
+ return !!this.squadsX
283
+ ? await this.sendSquadsIxs(ix)
284
+ : await this.buildAndSendIx(ix)
414
285
 
415
- // Wrap in a transaction and send
416
- const tx = new Transaction().add(cuIx, ix);
417
- const prepared = await this.prepareTx(tx);
418
- const signed = await this.signTransaction(prepared.tx);
419
-
420
- return this.sendAndConfirmHttp(signed, prepared);
421
286
  } catch (err) {
422
287
  console.log(`Failed to stake Solana: ${err}`);
423
288
  throw err;
@@ -429,26 +294,14 @@ export class SolanaStakingClient implements IStakingClient {
429
294
  */
430
295
  async unstake(amountLamports: bigint): Promise<string> {
431
296
  this.ensureUser();
432
-
433
- if (!amountLamports || amountLamports <= BigInt(0)) {
297
+ if (!amountLamports || amountLamports <= BigInt(0))
434
298
  throw new Error('Unstake amount must be greater than zero.');
435
- }
436
299
 
437
300
  try {
438
- const user = this.solPubKey;
439
-
440
- // Build compute budget increase instruction
441
- const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
442
-
443
- // Build the Outpost desynd instruction
444
- const ix = await this.outpostClient.buildUnstakeIx(amountLamports, user);
445
-
446
- // Wrap in a transaction and send
447
- const tx = new Transaction().add(cuIx, ix);
448
- const prepared = await this.prepareTx(tx);
449
- const signed = await this.signTransaction(prepared.tx);
450
-
451
- return this.sendAndConfirmHttp(signed, prepared);
301
+ const ix = await this.outpostClient.buildUnstakeIx(amountLamports, this.squadsVaultPDA)
302
+ return !!this.squadsX
303
+ ? await this.sendSquadsIxs(ix)
304
+ : await this.buildAndSendIx(ix)
452
305
  }
453
306
  catch (err) {
454
307
  console.log(`Failed to unstake Solana: ${err}`);
@@ -464,20 +317,14 @@ export class SolanaStakingClient implements IStakingClient {
464
317
  */
465
318
  async buy(amountLamports: bigint): Promise<string> {
466
319
  this.ensureUser();
467
- if (!amountLamports || amountLamports <= BigInt(0)) {
320
+ if (!amountLamports || amountLamports <= BigInt(0))
468
321
  throw new Error('liqSOL pretoken purchase requires a positive amount.');
469
- }
470
322
 
471
323
  try {
472
- const user = this.solPubKey;
473
- const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
474
- const ix = await this.tokenClient.buildPurchaseIx(amountLamports, user);
475
-
476
- const tx = new Transaction().add(cuIx, ix);
477
- const prepared = await this.prepareTx(tx);
478
- const signed = await this.signTransaction(prepared.tx);
479
-
480
- return this.sendAndConfirmHttp(signed, prepared);
324
+ const ix = await this.tokenClient.buildPurchaseIx(amountLamports, this.squadsVaultPDA)
325
+ return !!this.squadsX
326
+ ? await this.sendSquadsIxs(ix)
327
+ : await this.buildAndSendIx(ix)
481
328
  }
482
329
  catch (err) {
483
330
  console.log(`Failed to buy liqSOL pretokens: ${err}`);
@@ -495,8 +342,7 @@ export class SolanaStakingClient implements IStakingClient {
495
342
  * - extras: useful internal addresses and raw state for debugging/UX
496
343
  */
497
344
  async getPortfolio(): Promise<Portfolio> {
498
- if (!this.pubKey) throw new Error('User pubKey is undefined');
499
-
345
+ // if (!this.pubKey) throw new Error('User pubKey is undefined');
500
346
  try {
501
347
  const user = !!this.squadsX ? this.squadsVaultPDA! : this.solPubKey;
502
348
 
@@ -507,7 +353,7 @@ export class SolanaStakingClient implements IStakingClient {
507
353
  const userLiqsolAta = getAssociatedTokenAddressSync(
508
354
  liqsolMint,
509
355
  user,
510
- true, //set to true to allow off curve (e.g. PDA for squadsx wallet)
356
+ true,
511
357
  TOKEN_2022_PROGRAM_ID,
512
358
  ASSOCIATED_TOKEN_PROGRAM_ID,
513
359
  );
@@ -652,18 +498,224 @@ export class SolanaStakingClient implements IStakingClient {
652
498
  // SquadsX Helpers
653
499
  // ---------------------------------------------------------------------
654
500
 
655
- get squadsMultisigPDA(): SolPubKey | null {
656
- if (!this.squadsX) return null;
501
+ get squadsMultisigPDA(): SolPubKey | undefined {
502
+ if (!this.squadsX) return undefined;
657
503
  return new SolPubKey(this.squadsX.multisigPDA);
658
504
  }
659
- get squadsVaultPDA(): SolPubKey | null {
660
- if (!this.squadsX || !this.squadsMultisigPDA) return null;
505
+
506
+ get squadsVaultPDA(): SolPubKey | undefined {
507
+ if (!this.squadsX || !this.squadsMultisigPDA) return undefined;
661
508
  const multisigPda = this.squadsMultisigPDA;
662
509
  const index = this.squadsX.vaultIndex ?? 0;
663
510
  const pda = multisig.getVaultPda({ multisigPda, index });
664
511
  return pda[0];
665
512
  }
666
513
 
514
+ private async sendSquadsIxs(ix: TransactionInstruction): Promise<string> {
515
+ if (!this.squadsX) throw new Error('Attempting to wrap Squads instruction without SquadsX config');
516
+
517
+ const multisigPda = this.squadsMultisigPDA!;
518
+ const vaultPda = this.squadsVaultPDA!;
519
+ const vaultIndex = this.squadsX?.vaultIndex ?? 0;
520
+ const creator = this.solPubKey;
521
+
522
+ const ms = await multisig.accounts.Multisig.fromAccountAddress(this.connection, multisigPda);
523
+ const current = BigInt(ms.transactionIndex?.toString() ?? 0);
524
+ const transactionIndex = current + BigInt(1);
525
+
526
+ const altAddress = this.program.PROGRAM_IDS.ALT_LOOKUP_TABLE;
527
+ const altAccount = await this.connection.getAddressLookupTable(altAddress);
528
+ if (!altAccount.value) throw new Error("ALT not found on-chain or not yet active.");
529
+
530
+ const lookupTable: AddressLookupTableAccount = altAccount.value;
531
+ const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
532
+ const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 2000 });
533
+
534
+ const { blockhash } = await this.connection.getLatestBlockhash("confirmed");
535
+ const transactionMessage = new TransactionMessage({
536
+ payerKey: vaultPda,
537
+ recentBlockhash: blockhash,
538
+ instructions: [computeLimitIx, computePriceIx, ix],
539
+ });
540
+
541
+ const createVaultTxIx = await multisig.instructions.vaultTransactionCreate({
542
+ multisigPda,
543
+ transactionIndex,
544
+ creator,
545
+ vaultIndex,
546
+ transactionMessage,
547
+ ephemeralSigners: 0,
548
+ addressLookupTableAccounts: [lookupTable],
549
+ });
550
+
551
+ const vaultTransactionCreate = await this.buildAndSendIx(createVaultTxIx)
552
+ console.log('SQUADSX: vaultTransactionCreate', vaultTransactionCreate);
553
+
554
+ const createProposalIx = await multisig.instructions.proposalCreate({
555
+ multisigPda,
556
+ transactionIndex,
557
+ creator,
558
+ });
559
+
560
+ const proposalCreate = await this.buildAndSendIx(createProposalIx)
561
+ console.log('SQUADSX: proposalCreate', proposalCreate);
562
+
563
+ return proposalCreate;
564
+ }
565
+
566
+ // async finish(multisigPda: string, transactionIndex: number): Promise<void> {
567
+ // const vaultExecute = await multisig.instructions.vaultTransactionExecute({
568
+ // connection: this.connection,
569
+ // multisigPda: new SolPubKey(multisigPda),
570
+ // transactionIndex: BigInt(transactionIndex),
571
+ // member: this.solPubKey
572
+ // });
573
+
574
+ // const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
575
+ // const tx1 = new Transaction().add(cuIx, vaultExecute.instruction);
576
+ // const prepared1 = await this.prepareTx(tx1);
577
+ // const signed1 = await this.signTransaction(prepared1.tx);
578
+ // const sent1 = await this.sendAndConfirmHttp(signed1, prepared1);
579
+ // console.log('SENT FINISH', sent1);
580
+ // }
581
+
582
+ // ---------------------------------------------------------------------
583
+ // Tx helpers
584
+ // ---------------------------------------------------------------------
585
+
586
+ async buildAndSendIx(ix: TransactionInstruction | TransactionInstruction[]): Promise<string> {
587
+ const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
588
+ const ixs = Array.isArray(ix) ? ix : [ix];
589
+ const tx = new Transaction().add(cuIx, ...ixs);
590
+ const prepared = await this.prepareTx(tx);
591
+ const signed = await this.signTransaction(prepared.tx);
592
+
593
+ return this.sendAndConfirmHttp(signed, prepared);
594
+ }
595
+
596
+
597
+ /**
598
+ * Send a signed transaction over HTTP RPC.
599
+ *
600
+ * - Returns immediately on successful sendRawTransaction.
601
+ * - If sendRawTransaction throws a SendTransactionError with
602
+ * "already been processed", we treat it as success and
603
+ * just return a derived signature.
604
+ * - No confirmTransaction / polling / blockheight handling.
605
+ */
606
+ private async sendAndConfirmHttp(
607
+ signed: SolanaTransaction,
608
+ _ctx: { blockhash: string; lastValidBlockHeight: number },
609
+ ): Promise<string> {
610
+ this.ensureUser();
611
+
612
+ const rawTx = signed.serialize();
613
+
614
+ try {
615
+ // Normal happy path: RPC accepts the tx and returns a signature
616
+ const signature = await this.connection.sendRawTransaction(rawTx, {
617
+ skipPreflight: false,
618
+ preflightCommitment: commitment,
619
+ maxRetries: 3,
620
+ });
621
+ return signature;
622
+ } catch (e: any) {
623
+ const msg = e?.message ?? '';
624
+ const isSendTxError =
625
+ e instanceof SendTransactionError || e?.name === 'SendTransactionError';
626
+
627
+ // Benign duplicate case: tx is already in the ledger / cache
628
+ if (isSendTxError && msg.includes('already been processed')) {
629
+ console.warn(
630
+ 'sendRawTransaction reports "already been processed"; ' +
631
+ 'treating as success without further confirmation.',
632
+ );
633
+
634
+ // Try to derive a signature from the signed Transaction.
635
+ // If SolanaTransaction is a legacy Transaction, this works.
636
+ const legacy = signed as unknown as Transaction;
637
+ const first = legacy.signatures?.[0]?.signature;
638
+
639
+ if (first) {
640
+ return bs58.encode(first);
641
+ }
642
+
643
+ // Fallback: return a dummy string
644
+ return 'already-processed';
645
+ }
646
+
647
+ // Any other send error is a real failure
648
+ throw e;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Sign a single Solana transaction using the connected wallet adapter.
654
+ */
655
+ async signTransaction(
656
+ tx: SolanaTransaction,
657
+ ): Promise<SolanaTransaction> {
658
+ this.ensureUser();
659
+ return this.anchor.wallet.signTransaction(tx);
660
+ }
661
+
662
+ /**
663
+ * Generic "fire and forget" send helper if the caller already
664
+ * prepared and signed the transaction.
665
+ */
666
+ async sendTransaction(
667
+ signed: SolanaTransaction,
668
+ ): Promise<TransactionSignature> {
669
+ this.ensureUser();
670
+ return this.anchor.sendAndConfirm(signed);
671
+ }
672
+
673
+ /**
674
+ * Attach recent blockhash + fee payer to a transaction.
675
+ * Required before signing and sending.
676
+ */
677
+ async prepareTx(
678
+ tx: Transaction,
679
+ ): Promise<{
680
+ tx: Transaction;
681
+ blockhash: string;
682
+ lastValidBlockHeight: number;
683
+ }> {
684
+ const { blockhash, lastValidBlockHeight } =
685
+ await this.connection.getLatestBlockhash('confirmed');
686
+ tx.recentBlockhash = blockhash;
687
+ tx.feePayer = this.feePayer;
688
+ return { tx, blockhash, lastValidBlockHeight };
689
+ }
690
+
691
+ /**
692
+ * Guard for all write operations (deposit/withdraw/stake/unstake/buy).
693
+ * Ensures we have a Wire pubKey and an Anchor wallet pubKey, and that they match.
694
+ */
695
+ ensureUser() {
696
+ if (!this.pubKey) throw new Error('User pubKey is undefined');
697
+
698
+ const wallet = this.anchor?.wallet as any;
699
+ const pk = wallet?.publicKey as SolPubKey | undefined;
700
+
701
+ if (!pk) throw new Error('Wallet not connected');
702
+ if (typeof wallet.signTransaction !== 'function') {
703
+ throw new Error('Wallet does not support signTransaction');
704
+ }
705
+
706
+ // if (!this.pubKey || !this.anchor.wallet.publicKey) {
707
+ // throw new Error('User Authorization required: pubKey is undefined');
708
+ // }
709
+ // if (
710
+ // this.solPubKey.toBase58() !==
711
+ // this.anchor.wallet.publicKey.toBase58()
712
+ // ) {
713
+ // throw new Error(
714
+ // 'Write access requires connected wallet to match pubKey',
715
+ // );
716
+ // }
717
+ }
718
+
667
719
  // ---------------------------------------------------------------------
668
720
  // READ-ONLY Public Methods
669
721
  // ---------------------------------------------------------------------
@@ -745,13 +797,34 @@ export class SolanaStakingClient implements IStakingClient {
745
797
  return Number(DEFAULT_AVERAGE_PAY_RATE) / Number(PAY_RATE_SCALE_FACTOR);
746
798
  }
747
799
 
748
- // Latest entry is at (currentIndex - 1 + maxEntries) % maxEntries
749
800
  const currentIndex = Number(payRateHistory.currentIndex);
750
- const latestIdx = (currentIndex - 1 + maxEntries) % maxEntries;
751
- const latestEntry = payRateHistory.entries[latestIdx];
752
801
 
753
- const raw = BigInt(latestEntry.scaledRate.toString());
754
- return Number(raw) / Number(PAY_RATE_SCALE_FACTOR);
802
+ // Only average entries that have actually been processed (written by add_entry),
803
+ // not the default-initialized slots. After 10+ entries, processedCount === maxEntries.
804
+ const processedCount = Math.min(totalEntriesAdded, maxEntries);
805
+
806
+ let sum = BigInt(0);
807
+ let validCount = 0;
808
+ // Walk backward from most recent entry
809
+ let idx = (currentIndex - 1 + maxEntries) % maxEntries;
810
+
811
+ for (let i = 0; i < processedCount; i++) {
812
+ const entry = payRateHistory.entries[idx];
813
+ const scaledRate = BigInt(entry.scaledRate.toString());
814
+ if (scaledRate > BigInt(0)) {
815
+ sum += scaledRate;
816
+ validCount++;
817
+ }
818
+ idx = (idx - 1 + maxEntries) % maxEntries;
819
+ }
820
+
821
+ if (validCount === 0) {
822
+ return Number(DEFAULT_AVERAGE_PAY_RATE) / Number(PAY_RATE_SCALE_FACTOR);
823
+ }
824
+
825
+ // Average the processed entries
826
+ const average = Number(sum / BigInt(validCount));
827
+ return average / Number(PAY_RATE_SCALE_FACTOR);
755
828
  }
756
829
 
757
830
  // Simple cache so we don’t hammer RPC
@@ -1029,130 +1102,5 @@ export class SolanaStakingClient implements IStakingClient {
1029
1102
  return singleTxFeeLamports;
1030
1103
  }
1031
1104
 
1032
- // ---------------------------------------------------------------------
1033
- // Tx helpers
1034
- // ---------------------------------------------------------------------
1035
-
1036
- /**
1037
- * Send a signed transaction over HTTP RPC.
1038
- *
1039
- * - Returns immediately on successful sendRawTransaction.
1040
- * - If sendRawTransaction throws a SendTransactionError with
1041
- * "already been processed", we treat it as success and
1042
- * just return a derived signature.
1043
- * - No confirmTransaction / polling / blockheight handling.
1044
- */
1045
- private async sendAndConfirmHttp(
1046
- signed: SolanaTransaction,
1047
- _ctx: { blockhash: string; lastValidBlockHeight: number },
1048
- ): Promise<string> {
1049
- this.ensureUser();
1050
-
1051
- const rawTx = signed.serialize();
1052
-
1053
- try {
1054
- // Normal happy path: RPC accepts the tx and returns a signature
1055
- const signature = await this.connection.sendRawTransaction(rawTx, {
1056
- skipPreflight: false,
1057
- preflightCommitment: commitment,
1058
- maxRetries: 3,
1059
- });
1060
- return signature;
1061
- } catch (e: any) {
1062
- const msg = e?.message ?? '';
1063
- const isSendTxError =
1064
- e instanceof SendTransactionError || e?.name === 'SendTransactionError';
1065
-
1066
- // Benign duplicate case: tx is already in the ledger / cache
1067
- if (isSendTxError && msg.includes('already been processed')) {
1068
- console.warn(
1069
- 'sendRawTransaction reports "already been processed"; ' +
1070
- 'treating as success without further confirmation.',
1071
- );
1072
-
1073
- // Try to derive a signature from the signed Transaction.
1074
- // If SolanaTransaction is a legacy Transaction, this works.
1075
- const legacy = signed as unknown as Transaction;
1076
- const first = legacy.signatures?.[0]?.signature;
1077
-
1078
- if (first) {
1079
- return bs58.encode(first);
1080
- }
1081
-
1082
- // Fallback: return a dummy string
1083
- return 'already-processed';
1084
- }
1085
-
1086
- // Any other send error is a real failure
1087
- throw e;
1088
- }
1089
- }
1090
-
1091
- /**
1092
- * Sign a single Solana transaction using the connected wallet adapter.
1093
- */
1094
- async signTransaction(
1095
- tx: SolanaTransaction,
1096
- ): Promise<SolanaTransaction> {
1097
- this.ensureUser();
1098
- return this.anchor.wallet.signTransaction(tx);
1099
- }
1100
-
1101
- /**
1102
- * Generic "fire and forget" send helper if the caller already
1103
- * prepared and signed the transaction.
1104
- */
1105
- async sendTransaction(
1106
- signed: SolanaTransaction,
1107
- ): Promise<TransactionSignature> {
1108
- this.ensureUser();
1109
- return this.anchor.sendAndConfirm(signed);
1110
- }
1111
-
1112
- /**
1113
- * Attach recent blockhash + fee payer to a transaction.
1114
- * Required before signing and sending.
1115
- */
1116
- async prepareTx(
1117
- tx: Transaction,
1118
- ): Promise<{
1119
- tx: Transaction;
1120
- blockhash: string;
1121
- lastValidBlockHeight: number;
1122
- }> {
1123
- const { blockhash, lastValidBlockHeight } =
1124
- await this.connection.getLatestBlockhash('confirmed');
1125
- tx.recentBlockhash = blockhash;
1126
- tx.feePayer = this.feePayer;
1127
- return { tx, blockhash, lastValidBlockHeight };
1128
- }
1129
-
1130
- /**
1131
- * Guard for all write operations (deposit/withdraw/stake/unstake/buy).
1132
- * Ensures we have a Wire pubKey and an Anchor wallet pubKey, and that they match.
1133
- */
1134
- ensureUser() {
1135
- if (!this.pubKey) throw new Error('User pubKey is undefined');
1136
-
1137
- const wallet = this.anchor?.wallet as any;
1138
- const pk = wallet?.publicKey as SolPubKey | undefined;
1139
-
1140
- if (!pk) throw new Error('Wallet not connected');
1141
- if (typeof wallet.signTransaction !== 'function') {
1142
- throw new Error('Wallet does not support signTransaction');
1143
- }
1144
-
1145
- // if (!this.pubKey || !this.anchor.wallet.publicKey) {
1146
- // throw new Error('User Authorization required: pubKey is undefined');
1147
- // }
1148
- // if (
1149
- // this.solPubKey.toBase58() !==
1150
- // this.anchor.wallet.publicKey.toBase58()
1151
- // ) {
1152
- // throw new Error(
1153
- // 'Write access requires connected wallet to match pubKey',
1154
- // );
1155
- // }
1156
- }
1157
1105
 
1158
1106
  }