@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.
- package/lib/stake.browser.js +188 -227
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +52 -52
- package/lib/stake.js +315 -331
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +188 -227
- package/lib/stake.m.js.map +1 -1
- package/package.json +1 -1
- package/src/networks/ethereum/clients/validator.client.ts +61 -0
- package/src/networks/ethereum/ethereum.ts +20 -0
- package/src/networks/ethereum/types.ts +9 -2
- package/src/networks/solana/clients/deposit.client.ts +2 -229
- package/src/networks/solana/clients/outpost.client.ts +4 -5
- package/src/networks/solana/clients/token.client.ts +2 -0
- package/src/networks/solana/constants.ts +3 -0
- package/src/networks/solana/solana.ts +265 -317
- package/src/networks/solana/utils.ts +1 -1
- package/src/types.ts +2 -0
|
@@ -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
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
473
|
-
|
|
474
|
-
|
|
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,
|
|
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 |
|
|
656
|
-
if (!this.squadsX) return
|
|
501
|
+
get squadsMultisigPDA(): SolPubKey | undefined {
|
|
502
|
+
if (!this.squadsX) return undefined;
|
|
657
503
|
return new SolPubKey(this.squadsX.multisigPDA);
|
|
658
504
|
}
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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
|
}
|