@ignitionfi/fogo-stake-pool 1.0.2 → 1.1.0
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/codecs.d.ts +10 -0
- package/dist/index.browser.cjs.js +369 -208
- package/dist/index.browser.cjs.js.map +1 -1
- package/dist/index.browser.esm.js +369 -208
- package/dist/index.browser.esm.js.map +1 -1
- package/dist/index.cjs.js +369 -208
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.esm.js +369 -208
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +403 -242
- package/dist/index.iife.js.map +1 -1
- package/dist/index.iife.min.js +1 -1
- package/dist/index.iife.min.js.map +1 -1
- package/dist/instructions.d.ts +31 -22
- package/dist/utils/stake.d.ts +19 -2
- package/package.json +1 -1
- package/src/codecs.ts +59 -6
- package/src/constants.ts +1 -1
- package/src/index.ts +79 -26
- package/src/instructions.ts +140 -67
- package/src/utils/stake.ts +133 -75
|
@@ -438,6 +438,139 @@ const USER_STAKE_SEED_PREFIX = Buffer$1.from('user_stake');
|
|
|
438
438
|
// for merges without a mismatch on credits observed
|
|
439
439
|
const MINIMUM_ACTIVE_STAKE = 1000000;
|
|
440
440
|
|
|
441
|
+
/**
|
|
442
|
+
* BN-based layout for data decoding using buffer-layout.
|
|
443
|
+
* Used for decoding on-chain account data (StakePool, ValidatorList, etc.)
|
|
444
|
+
*/
|
|
445
|
+
class BNDataLayout extends Layout {
|
|
446
|
+
constructor(span, signed, property) {
|
|
447
|
+
super(span, property);
|
|
448
|
+
this.blobLayout = blob(span);
|
|
449
|
+
this.signed = signed;
|
|
450
|
+
}
|
|
451
|
+
decode(b, offset = 0) {
|
|
452
|
+
const num = new BN(this.blobLayout.decode(b, offset), 10, 'le');
|
|
453
|
+
if (this.signed) {
|
|
454
|
+
return num.fromTwos(this.span * 8).clone();
|
|
455
|
+
}
|
|
456
|
+
return num;
|
|
457
|
+
}
|
|
458
|
+
encode(src, b, offset = 0) {
|
|
459
|
+
if (this.signed) {
|
|
460
|
+
src = src.toTwos(this.span * 8);
|
|
461
|
+
}
|
|
462
|
+
return this.blobLayout.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Creates a u64 layout for data decoding (account layouts).
|
|
467
|
+
* Used in StakePoolLayout, ValidatorListLayout, etc.
|
|
468
|
+
*/
|
|
469
|
+
function u64(property) {
|
|
470
|
+
return new BNDataLayout(8, false, property);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* BN-based layout for 64-bit unsigned integers using @solana/buffer-layout.
|
|
474
|
+
* Used for encoding instruction data with support for values > MAX_SAFE_INTEGER.
|
|
475
|
+
*/
|
|
476
|
+
class BNInstructionLayout extends BufferLayout.Layout {
|
|
477
|
+
constructor(span, signed, property) {
|
|
478
|
+
super(span, property);
|
|
479
|
+
this.blobLayout = BufferLayout.blob(span);
|
|
480
|
+
this.signed = signed;
|
|
481
|
+
}
|
|
482
|
+
decode(b, offset = 0) {
|
|
483
|
+
const num = new BN(this.blobLayout.decode(b, offset), 10, 'le');
|
|
484
|
+
if (this.signed) {
|
|
485
|
+
return num.fromTwos(this.span * 8).clone();
|
|
486
|
+
}
|
|
487
|
+
return num;
|
|
488
|
+
}
|
|
489
|
+
encode(src, b, offset = 0) {
|
|
490
|
+
if (this.signed) {
|
|
491
|
+
src = src.toTwos(this.span * 8);
|
|
492
|
+
}
|
|
493
|
+
return this.blobLayout.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
|
494
|
+
}
|
|
495
|
+
getSpan(_b, _offset) {
|
|
496
|
+
return this.span;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Creates a u64 layout for instruction encoding.
|
|
501
|
+
* Properly handles BN values larger than Number.MAX_SAFE_INTEGER.
|
|
502
|
+
* Compatible with @solana/buffer-layout.struct().
|
|
503
|
+
*/
|
|
504
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
505
|
+
function u64Instruction(property) {
|
|
506
|
+
return new BNInstructionLayout(8, false, property);
|
|
507
|
+
}
|
|
508
|
+
class WrappedLayout extends Layout {
|
|
509
|
+
constructor(layout, decoder, encoder, property) {
|
|
510
|
+
super(layout.span, property);
|
|
511
|
+
this.layout = layout;
|
|
512
|
+
this.decoder = decoder;
|
|
513
|
+
this.encoder = encoder;
|
|
514
|
+
}
|
|
515
|
+
decode(b, offset) {
|
|
516
|
+
return this.decoder(this.layout.decode(b, offset));
|
|
517
|
+
}
|
|
518
|
+
encode(src, b, offset) {
|
|
519
|
+
return this.layout.encode(this.encoder(src), b, offset);
|
|
520
|
+
}
|
|
521
|
+
getSpan(b, offset) {
|
|
522
|
+
return this.layout.getSpan(b, offset);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function publicKey(property) {
|
|
526
|
+
return new WrappedLayout(blob(32), (b) => new PublicKey(b), (key) => key.toBuffer(), property);
|
|
527
|
+
}
|
|
528
|
+
class OptionLayout extends Layout {
|
|
529
|
+
constructor(layout, property) {
|
|
530
|
+
super(-1, property);
|
|
531
|
+
this.layout = layout;
|
|
532
|
+
this.discriminator = u8();
|
|
533
|
+
}
|
|
534
|
+
encode(src, b, offset = 0) {
|
|
535
|
+
if (src === null || src === undefined) {
|
|
536
|
+
return this.discriminator.encode(0, b, offset);
|
|
537
|
+
}
|
|
538
|
+
this.discriminator.encode(1, b, offset);
|
|
539
|
+
return this.layout.encode(src, b, offset + 1) + 1;
|
|
540
|
+
}
|
|
541
|
+
decode(b, offset = 0) {
|
|
542
|
+
const discriminator = this.discriminator.decode(b, offset);
|
|
543
|
+
if (discriminator === 0) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
else if (discriminator === 1) {
|
|
547
|
+
return this.layout.decode(b, offset + 1);
|
|
548
|
+
}
|
|
549
|
+
throw new Error(`Invalid option ${this.property}`);
|
|
550
|
+
}
|
|
551
|
+
getSpan(b, offset = 0) {
|
|
552
|
+
const discriminator = this.discriminator.decode(b, offset);
|
|
553
|
+
if (discriminator === 0) {
|
|
554
|
+
return 1;
|
|
555
|
+
}
|
|
556
|
+
else if (discriminator === 1) {
|
|
557
|
+
return this.layout.getSpan(b, offset + 1) + 1;
|
|
558
|
+
}
|
|
559
|
+
throw new Error(`Invalid option ${this.property}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function option(layout, property) {
|
|
563
|
+
return new OptionLayout(layout, property);
|
|
564
|
+
}
|
|
565
|
+
function vec(elementLayout, property) {
|
|
566
|
+
const length = u32('length');
|
|
567
|
+
const layout = struct([
|
|
568
|
+
length,
|
|
569
|
+
seq(elementLayout, offset(length, -length.span), 'values'),
|
|
570
|
+
]);
|
|
571
|
+
return new WrappedLayout(layout, ({ values }) => values, values => ({ values }), property);
|
|
572
|
+
}
|
|
573
|
+
|
|
441
574
|
/**
|
|
442
575
|
* Populate a buffer of instruction data using an InstructionType
|
|
443
576
|
* @internal
|
|
@@ -556,95 +689,6 @@ function findUserStakeProgramAddress(programId, userWallet, seed) {
|
|
|
556
689
|
return publicKey;
|
|
557
690
|
}
|
|
558
691
|
|
|
559
|
-
class BNLayout extends Layout {
|
|
560
|
-
constructor(span, signed, property) {
|
|
561
|
-
super(span, property);
|
|
562
|
-
this.blob = blob(span);
|
|
563
|
-
this.signed = signed;
|
|
564
|
-
}
|
|
565
|
-
decode(b, offset = 0) {
|
|
566
|
-
const num = new BN(this.blob.decode(b, offset), 10, 'le');
|
|
567
|
-
if (this.signed) {
|
|
568
|
-
return num.fromTwos(this.span * 8).clone();
|
|
569
|
-
}
|
|
570
|
-
return num;
|
|
571
|
-
}
|
|
572
|
-
encode(src, b, offset = 0) {
|
|
573
|
-
if (this.signed) {
|
|
574
|
-
src = src.toTwos(this.span * 8);
|
|
575
|
-
}
|
|
576
|
-
return this.blob.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
function u64(property) {
|
|
580
|
-
return new BNLayout(8, false, property);
|
|
581
|
-
}
|
|
582
|
-
class WrappedLayout extends Layout {
|
|
583
|
-
constructor(layout, decoder, encoder, property) {
|
|
584
|
-
super(layout.span, property);
|
|
585
|
-
this.layout = layout;
|
|
586
|
-
this.decoder = decoder;
|
|
587
|
-
this.encoder = encoder;
|
|
588
|
-
}
|
|
589
|
-
decode(b, offset) {
|
|
590
|
-
return this.decoder(this.layout.decode(b, offset));
|
|
591
|
-
}
|
|
592
|
-
encode(src, b, offset) {
|
|
593
|
-
return this.layout.encode(this.encoder(src), b, offset);
|
|
594
|
-
}
|
|
595
|
-
getSpan(b, offset) {
|
|
596
|
-
return this.layout.getSpan(b, offset);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function publicKey(property) {
|
|
600
|
-
return new WrappedLayout(blob(32), (b) => new PublicKey(b), (key) => key.toBuffer(), property);
|
|
601
|
-
}
|
|
602
|
-
class OptionLayout extends Layout {
|
|
603
|
-
constructor(layout, property) {
|
|
604
|
-
super(-1, property);
|
|
605
|
-
this.layout = layout;
|
|
606
|
-
this.discriminator = u8();
|
|
607
|
-
}
|
|
608
|
-
encode(src, b, offset = 0) {
|
|
609
|
-
if (src === null || src === undefined) {
|
|
610
|
-
return this.discriminator.encode(0, b, offset);
|
|
611
|
-
}
|
|
612
|
-
this.discriminator.encode(1, b, offset);
|
|
613
|
-
return this.layout.encode(src, b, offset + 1) + 1;
|
|
614
|
-
}
|
|
615
|
-
decode(b, offset = 0) {
|
|
616
|
-
const discriminator = this.discriminator.decode(b, offset);
|
|
617
|
-
if (discriminator === 0) {
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
|
-
else if (discriminator === 1) {
|
|
621
|
-
return this.layout.decode(b, offset + 1);
|
|
622
|
-
}
|
|
623
|
-
throw new Error(`Invalid option ${this.property}`);
|
|
624
|
-
}
|
|
625
|
-
getSpan(b, offset = 0) {
|
|
626
|
-
const discriminator = this.discriminator.decode(b, offset);
|
|
627
|
-
if (discriminator === 0) {
|
|
628
|
-
return 1;
|
|
629
|
-
}
|
|
630
|
-
else if (discriminator === 1) {
|
|
631
|
-
return this.layout.getSpan(b, offset + 1) + 1;
|
|
632
|
-
}
|
|
633
|
-
throw new Error(`Invalid option ${this.property}`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
function option(layout, property) {
|
|
637
|
-
return new OptionLayout(layout, property);
|
|
638
|
-
}
|
|
639
|
-
function vec(elementLayout, property) {
|
|
640
|
-
const length = u32('length');
|
|
641
|
-
const layout = struct([
|
|
642
|
-
length,
|
|
643
|
-
seq(elementLayout, offset(length, -length.span), 'values'),
|
|
644
|
-
]);
|
|
645
|
-
return new WrappedLayout(layout, ({ values }) => values, values => ({ values }), property);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
692
|
const feeFields = [u64('denominator'), u64('numerator')];
|
|
649
693
|
var AccountType;
|
|
650
694
|
(function (AccountType) {
|
|
@@ -806,80 +850,84 @@ async function getValidatorListAccount(connection, pubkey) {
|
|
|
806
850
|
},
|
|
807
851
|
};
|
|
808
852
|
}
|
|
809
|
-
async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee) {
|
|
853
|
+
async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee, allowPartial, prefetchedData) {
|
|
810
854
|
var _a, _b;
|
|
811
855
|
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
856
|
+
// Use prefetched data if available, otherwise fetch from RPC
|
|
857
|
+
let validatorListData;
|
|
858
|
+
let minBalanceForRentExemption;
|
|
859
|
+
let stakeMinimumDelegation;
|
|
860
|
+
if (prefetchedData) {
|
|
861
|
+
validatorListData = prefetchedData.validatorListData;
|
|
862
|
+
minBalanceForRentExemption = prefetchedData.minBalanceForRentExemption;
|
|
863
|
+
stakeMinimumDelegation = prefetchedData.stakeMinimumDelegation;
|
|
816
864
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
865
|
+
else {
|
|
866
|
+
const [validatorListAcc, rentExemption, stakeMinimumDelegationResponse] = await Promise.all([
|
|
867
|
+
connection.getAccountInfo(stakePool.validatorList),
|
|
868
|
+
connection.getMinimumBalanceForRentExemption(StakeProgram.space),
|
|
869
|
+
connection.getStakeMinimumDelegation(),
|
|
870
|
+
]);
|
|
871
|
+
validatorListData = (_a = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _a !== void 0 ? _a : null;
|
|
872
|
+
minBalanceForRentExemption = rentExemption;
|
|
873
|
+
stakeMinimumDelegation = Number(stakeMinimumDelegationResponse.value);
|
|
874
|
+
}
|
|
875
|
+
if (!validatorListData) {
|
|
876
|
+
throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
|
|
877
|
+
}
|
|
878
|
+
const validatorList = ValidatorListLayout.decode(validatorListData);
|
|
879
|
+
if (!(validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators) || (validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators.length) === 0) {
|
|
880
|
+
throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
|
|
881
|
+
}
|
|
882
|
+
// minBalance = rent + max(stake_minimum_delegation, MINIMUM_ACTIVE_STAKE)
|
|
883
|
+
const minimumDelegation = Math.max(stakeMinimumDelegation, MINIMUM_ACTIVE_STAKE);
|
|
884
|
+
const minBalance = new BN(minBalanceForRentExemption + minimumDelegation);
|
|
885
|
+
// Threshold for has_active_stake check (ceiling division for lamports_per_pool_token)
|
|
886
|
+
const lamportsPerPoolToken = stakePool.totalLamports
|
|
887
|
+
.add(stakePool.poolTokenSupply)
|
|
888
|
+
.sub(new BN(1))
|
|
889
|
+
.div(stakePool.poolTokenSupply);
|
|
890
|
+
const minimumLamportsWithTolerance = minBalance.add(lamportsPerPoolToken);
|
|
891
|
+
const hasActiveStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
|
|
892
|
+
&& v.activeStakeLamports.gt(minimumLamportsWithTolerance));
|
|
893
|
+
const hasTransientStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
|
|
894
|
+
&& v.transientStakeLamports.gt(minimumLamportsWithTolerance));
|
|
895
|
+
// ValidatorRemoval mode: no validator above threshold
|
|
896
|
+
const isValidatorRemovalMode = !hasActiveStake && !hasTransientStake;
|
|
897
|
+
let accounts = [];
|
|
821
898
|
for (const validator of validatorList.validators) {
|
|
822
899
|
if (validator.status !== ValidatorStakeInfoStatus.Active) {
|
|
823
900
|
continue;
|
|
824
901
|
}
|
|
825
902
|
const stakeAccountAddress = await findStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
903
|
+
// ValidatorRemoval: full balance available; Normal: leave minBalance
|
|
904
|
+
const availableActiveLamports = isValidatorRemovalMode
|
|
905
|
+
? validator.activeStakeLamports
|
|
906
|
+
: validator.activeStakeLamports.sub(minBalance);
|
|
907
|
+
if (availableActiveLamports.gt(new BN(0))) {
|
|
908
|
+
const isPreferred = (_b = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _b === void 0 ? void 0 : _b.equals(validator.voteAccountAddress);
|
|
909
|
+
accounts.push({
|
|
830
910
|
type: isPreferred ? 'preferred' : 'active',
|
|
831
911
|
voteAddress: validator.voteAccountAddress,
|
|
832
912
|
stakeAddress: stakeAccountAddress,
|
|
913
|
+
lamports: availableActiveLamports,
|
|
833
914
|
});
|
|
834
915
|
}
|
|
835
|
-
|
|
836
|
-
|
|
916
|
+
const availableTransientLamports = isValidatorRemovalMode
|
|
917
|
+
? validator.transientStakeLamports
|
|
918
|
+
: validator.transientStakeLamports.sub(minBalance);
|
|
919
|
+
if (availableTransientLamports.gt(new BN(0))) {
|
|
837
920
|
const transientStakeAccountAddress = await findTransientStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress, validator.transientSeedSuffixStart);
|
|
838
|
-
|
|
921
|
+
accounts.push({
|
|
839
922
|
type: 'transient',
|
|
840
923
|
voteAddress: validator.voteAccountAddress,
|
|
841
924
|
stakeAddress: transientStakeAccountAddress,
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
// Fetch all stake accounts + reserve in one batch call
|
|
846
|
-
const addressesToFetch = [
|
|
847
|
-
...accountsToFetch.map(a => a.stakeAddress),
|
|
848
|
-
stakePool.reserveStake,
|
|
849
|
-
];
|
|
850
|
-
const accountInfos = await connection.getMultipleAccountsInfo(addressesToFetch);
|
|
851
|
-
// Build accounts list using actual on-chain balances
|
|
852
|
-
let accounts = [];
|
|
853
|
-
for (let i = 0; i < accountsToFetch.length; i++) {
|
|
854
|
-
const { type, voteAddress, stakeAddress } = accountsToFetch[i];
|
|
855
|
-
const accountInfo = accountInfos[i];
|
|
856
|
-
if (!accountInfo) {
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
// Use actual on-chain balance instead of validator list value
|
|
860
|
-
const actualLamports = new BN(accountInfo.lamports);
|
|
861
|
-
const availableLamports = actualLamports.sub(minBalance);
|
|
862
|
-
if (availableLamports.gt(new BN(0))) {
|
|
863
|
-
accounts.push({
|
|
864
|
-
type,
|
|
865
|
-
voteAddress,
|
|
866
|
-
stakeAddress,
|
|
867
|
-
lamports: availableLamports,
|
|
925
|
+
lamports: availableTransientLamports,
|
|
868
926
|
});
|
|
869
927
|
}
|
|
870
928
|
}
|
|
871
929
|
// Sort from highest to lowest balance
|
|
872
930
|
accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()));
|
|
873
|
-
// Add reserve stake using actual balance (last item in batch fetch)
|
|
874
|
-
const reserveAccountInfo = accountInfos[accountInfos.length - 1];
|
|
875
|
-
const reserveStakeBalance = new BN(((_b = reserveAccountInfo === null || reserveAccountInfo === void 0 ? void 0 : reserveAccountInfo.lamports) !== null && _b !== void 0 ? _b : 0) - minBalanceForRentExemption);
|
|
876
|
-
if (reserveStakeBalance.gt(new BN(0))) {
|
|
877
|
-
accounts.push({
|
|
878
|
-
type: 'reserve',
|
|
879
|
-
stakeAddress: stakePool.reserveStake,
|
|
880
|
-
lamports: reserveStakeBalance,
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
931
|
// Prepare the list of accounts to withdraw from
|
|
884
932
|
const withdrawFrom = [];
|
|
885
933
|
let remainingAmount = new BN(amount);
|
|
@@ -888,23 +936,24 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
|
|
|
888
936
|
numerator: fee.denominator.sub(fee.numerator),
|
|
889
937
|
denominator: fee.denominator,
|
|
890
938
|
};
|
|
891
|
-
for (const type of ['preferred', 'active', 'transient'
|
|
939
|
+
for (const type of ['preferred', 'active', 'transient']) {
|
|
892
940
|
const filteredAccounts = accounts.filter(a => a.type === type);
|
|
893
941
|
for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
|
|
894
|
-
if (lamports.lte(minBalance) && type === 'transient') {
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
942
|
let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports);
|
|
898
943
|
if (!skipFee && !inverseFee.numerator.isZero()) {
|
|
899
944
|
availableForWithdrawal = availableForWithdrawal
|
|
900
945
|
.mul(inverseFee.denominator)
|
|
901
946
|
.div(inverseFee.numerator);
|
|
902
947
|
}
|
|
948
|
+
// In ValidatorRemoval mode, must withdraw full validator balance (no partial)
|
|
949
|
+
// Skip if remaining amount is less than full validator balance
|
|
950
|
+
if (isValidatorRemovalMode && remainingAmount.lt(availableForWithdrawal)) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
903
953
|
const poolAmount = BN.min(availableForWithdrawal, remainingAmount);
|
|
904
954
|
if (poolAmount.lte(new BN(0))) {
|
|
905
955
|
continue;
|
|
906
956
|
}
|
|
907
|
-
// Those accounts will be withdrawn completely with `claim` instruction
|
|
908
957
|
withdrawFrom.push({ stakeAddress, voteAddress, poolAmount });
|
|
909
958
|
remainingAmount = remainingAmount.sub(poolAmount);
|
|
910
959
|
if (remainingAmount.isZero()) {
|
|
@@ -917,7 +966,23 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
|
|
|
917
966
|
}
|
|
918
967
|
// Not enough stake to withdraw the specified amount
|
|
919
968
|
if (remainingAmount.gt(new BN(0))) {
|
|
920
|
-
|
|
969
|
+
if (allowPartial) {
|
|
970
|
+
const delayedAmount = amount.sub(remainingAmount);
|
|
971
|
+
return {
|
|
972
|
+
withdrawAccounts: withdrawFrom,
|
|
973
|
+
delayedAmount,
|
|
974
|
+
remainingAmount,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const availableAmount = amount.sub(remainingAmount);
|
|
978
|
+
throw new Error(`Not enough staked funds for delayed unstake. Requested ${lamportsToSol(amount)} iFOGO, but only ${lamportsToSol(availableAmount)} available. Use instant unstake for the remaining amount.`);
|
|
979
|
+
}
|
|
980
|
+
if (allowPartial) {
|
|
981
|
+
return {
|
|
982
|
+
withdrawAccounts: withdrawFrom,
|
|
983
|
+
delayedAmount: amount,
|
|
984
|
+
remainingAmount: new BN(0),
|
|
985
|
+
};
|
|
921
986
|
}
|
|
922
987
|
return withdrawFrom;
|
|
923
988
|
}
|
|
@@ -966,11 +1031,50 @@ function arrayChunk(array, size) {
|
|
|
966
1031
|
return result;
|
|
967
1032
|
}
|
|
968
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Converts various numeric types to BN for safe large number handling.
|
|
1036
|
+
* @internal
|
|
1037
|
+
*/
|
|
1038
|
+
function toBN(value) {
|
|
1039
|
+
if (BN.isBN(value)) {
|
|
1040
|
+
return value;
|
|
1041
|
+
}
|
|
1042
|
+
if (typeof value === 'bigint') {
|
|
1043
|
+
return new BN(value.toString());
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof value === 'string') {
|
|
1046
|
+
// Validate string is a valid non-negative integer
|
|
1047
|
+
const trimmed = value.trim();
|
|
1048
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
1049
|
+
throw new Error(`Invalid amount string: "${value}". Must be a non-negative integer.`);
|
|
1050
|
+
}
|
|
1051
|
+
return new BN(trimmed);
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof value === 'number') {
|
|
1054
|
+
if (!Number.isFinite(value)) {
|
|
1055
|
+
throw new Error('Invalid amount: must be a finite number');
|
|
1056
|
+
}
|
|
1057
|
+
if (value < 0) {
|
|
1058
|
+
throw new Error('Invalid amount: must be non-negative');
|
|
1059
|
+
}
|
|
1060
|
+
if (!Number.isInteger(value)) {
|
|
1061
|
+
throw new Error('Invalid amount: must be an integer (lamports)');
|
|
1062
|
+
}
|
|
1063
|
+
// CRITICAL: Numbers > MAX_SAFE_INTEGER have already lost precision
|
|
1064
|
+
// We throw an error instead of silently corrupting data
|
|
1065
|
+
if (value > Number.MAX_SAFE_INTEGER) {
|
|
1066
|
+
throw new Error(`Amount ${value} exceeds Number.MAX_SAFE_INTEGER (9,007,199,254,740,991). ` +
|
|
1067
|
+
`Use BigInt or BN for large values to avoid precision loss.`);
|
|
1068
|
+
}
|
|
1069
|
+
return new BN(value);
|
|
1070
|
+
}
|
|
1071
|
+
throw new Error(`Invalid amount type: ${typeof value}`);
|
|
1072
|
+
}
|
|
969
1073
|
// 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts
|
|
970
1074
|
const MOVE_STAKE_LAYOUT = BufferLayout.struct([
|
|
971
1075
|
BufferLayout.u8('instruction'),
|
|
972
|
-
|
|
973
|
-
|
|
1076
|
+
u64Instruction('lamports'),
|
|
1077
|
+
u64Instruction('transientStakeSeed'),
|
|
974
1078
|
]);
|
|
975
1079
|
const UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = BufferLayout.struct([
|
|
976
1080
|
BufferLayout.u8('instruction'),
|
|
@@ -1045,7 +1149,7 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1045
1149
|
index: 10,
|
|
1046
1150
|
layout: BufferLayout.struct([
|
|
1047
1151
|
BufferLayout.u8('instruction'),
|
|
1048
|
-
|
|
1152
|
+
u64Instruction('poolTokens'),
|
|
1049
1153
|
]),
|
|
1050
1154
|
},
|
|
1051
1155
|
/// Deposit SOL directly into the pool's reserve account. The output is a "pool" token
|
|
@@ -1054,7 +1158,7 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1054
1158
|
index: 14,
|
|
1055
1159
|
layout: BufferLayout.struct([
|
|
1056
1160
|
BufferLayout.u8('instruction'),
|
|
1057
|
-
|
|
1161
|
+
u64Instruction('lamports'),
|
|
1058
1162
|
]),
|
|
1059
1163
|
},
|
|
1060
1164
|
/// Withdraw SOL directly from the pool's reserve account. Fails if the
|
|
@@ -1063,25 +1167,25 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1063
1167
|
index: 16,
|
|
1064
1168
|
layout: BufferLayout.struct([
|
|
1065
1169
|
BufferLayout.u8('instruction'),
|
|
1066
|
-
|
|
1170
|
+
u64Instruction('poolTokens'),
|
|
1067
1171
|
]),
|
|
1068
1172
|
},
|
|
1069
1173
|
IncreaseAdditionalValidatorStake: {
|
|
1070
1174
|
index: 19,
|
|
1071
1175
|
layout: BufferLayout.struct([
|
|
1072
1176
|
BufferLayout.u8('instruction'),
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1177
|
+
u64Instruction('lamports'),
|
|
1178
|
+
u64Instruction('transientStakeSeed'),
|
|
1179
|
+
u64Instruction('ephemeralStakeSeed'),
|
|
1076
1180
|
]),
|
|
1077
1181
|
},
|
|
1078
1182
|
DecreaseAdditionalValidatorStake: {
|
|
1079
1183
|
index: 20,
|
|
1080
1184
|
layout: BufferLayout.struct([
|
|
1081
1185
|
BufferLayout.u8('instruction'),
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1186
|
+
u64Instruction('lamports'),
|
|
1187
|
+
u64Instruction('transientStakeSeed'),
|
|
1188
|
+
u64Instruction('ephemeralStakeSeed'),
|
|
1085
1189
|
]),
|
|
1086
1190
|
},
|
|
1087
1191
|
DecreaseValidatorStakeWithReserve: {
|
|
@@ -1096,62 +1200,62 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1096
1200
|
index: 23,
|
|
1097
1201
|
layout: BufferLayout.struct([
|
|
1098
1202
|
BufferLayout.u8('instruction'),
|
|
1099
|
-
|
|
1203
|
+
u64Instruction('lamports'),
|
|
1100
1204
|
]),
|
|
1101
1205
|
},
|
|
1102
1206
|
WithdrawStakeWithSlippage: {
|
|
1103
1207
|
index: 24,
|
|
1104
1208
|
layout: BufferLayout.struct([
|
|
1105
1209
|
BufferLayout.u8('instruction'),
|
|
1106
|
-
|
|
1107
|
-
|
|
1210
|
+
u64Instruction('poolTokensIn'),
|
|
1211
|
+
u64Instruction('minimumLamportsOut'),
|
|
1108
1212
|
]),
|
|
1109
1213
|
},
|
|
1110
1214
|
DepositSolWithSlippage: {
|
|
1111
1215
|
index: 25,
|
|
1112
1216
|
layout: BufferLayout.struct([
|
|
1113
1217
|
BufferLayout.u8('instruction'),
|
|
1114
|
-
|
|
1218
|
+
u64Instruction('lamports'),
|
|
1115
1219
|
]),
|
|
1116
1220
|
},
|
|
1117
1221
|
WithdrawSolWithSlippage: {
|
|
1118
1222
|
index: 26,
|
|
1119
1223
|
layout: BufferLayout.struct([
|
|
1120
1224
|
BufferLayout.u8('instruction'),
|
|
1121
|
-
|
|
1225
|
+
u64Instruction('lamports'),
|
|
1122
1226
|
]),
|
|
1123
1227
|
},
|
|
1124
1228
|
DepositWsolWithSession: {
|
|
1125
1229
|
index: 27,
|
|
1126
1230
|
layout: BufferLayout.struct([
|
|
1127
1231
|
BufferLayout.u8('instruction'),
|
|
1128
|
-
|
|
1129
|
-
|
|
1232
|
+
u64Instruction('lamportsIn'),
|
|
1233
|
+
u64Instruction('minimumPoolTokensOut'),
|
|
1130
1234
|
]),
|
|
1131
1235
|
},
|
|
1132
1236
|
WithdrawWsolWithSession: {
|
|
1133
1237
|
index: 28,
|
|
1134
1238
|
layout: BufferLayout.struct([
|
|
1135
1239
|
BufferLayout.u8('instruction'),
|
|
1136
|
-
|
|
1137
|
-
|
|
1240
|
+
u64Instruction('poolTokensIn'),
|
|
1241
|
+
u64Instruction('minimumLamportsOut'),
|
|
1138
1242
|
]),
|
|
1139
1243
|
},
|
|
1140
1244
|
WithdrawStakeWithSession: {
|
|
1141
1245
|
index: 29,
|
|
1142
1246
|
layout: BufferLayout.struct([
|
|
1143
1247
|
BufferLayout.u8('instruction'),
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1248
|
+
u64Instruction('poolTokensIn'),
|
|
1249
|
+
u64Instruction('minimumLamportsOut'),
|
|
1250
|
+
u64Instruction('userStakeSeed'),
|
|
1147
1251
|
]),
|
|
1148
1252
|
},
|
|
1149
1253
|
WithdrawFromStakeAccountWithSession: {
|
|
1150
1254
|
index: 30,
|
|
1151
1255
|
layout: BufferLayout.struct([
|
|
1152
1256
|
BufferLayout.u8('instruction'),
|
|
1153
|
-
|
|
1154
|
-
|
|
1257
|
+
u64Instruction('lamports'),
|
|
1258
|
+
u64Instruction('userStakeSeed'),
|
|
1155
1259
|
]),
|
|
1156
1260
|
},
|
|
1157
1261
|
});
|
|
@@ -1283,7 +1387,10 @@ class StakePoolInstruction {
|
|
|
1283
1387
|
static increaseValidatorStake(params) {
|
|
1284
1388
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, transientStake, validatorStake, validatorVote, lamports, transientStakeSeed, } = params;
|
|
1285
1389
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseValidatorStake;
|
|
1286
|
-
const data = encodeData(type, {
|
|
1390
|
+
const data = encodeData(type, {
|
|
1391
|
+
lamports: toBN(lamports),
|
|
1392
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1393
|
+
});
|
|
1287
1394
|
const keys = [
|
|
1288
1395
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1289
1396
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1313,7 +1420,11 @@ class StakePoolInstruction {
|
|
|
1313
1420
|
static increaseAdditionalValidatorStake(params) {
|
|
1314
1421
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, transientStake, validatorStake, validatorVote, lamports, transientStakeSeed, ephemeralStake, ephemeralStakeSeed, } = params;
|
|
1315
1422
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseAdditionalValidatorStake;
|
|
1316
|
-
const data = encodeData(type, {
|
|
1423
|
+
const data = encodeData(type, {
|
|
1424
|
+
lamports: toBN(lamports),
|
|
1425
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1426
|
+
ephemeralStakeSeed: toBN(ephemeralStakeSeed),
|
|
1427
|
+
});
|
|
1317
1428
|
const keys = [
|
|
1318
1429
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1319
1430
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1343,7 +1454,10 @@ class StakePoolInstruction {
|
|
|
1343
1454
|
static decreaseValidatorStake(params) {
|
|
1344
1455
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, validatorStake, transientStake, lamports, transientStakeSeed, } = params;
|
|
1345
1456
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseValidatorStake;
|
|
1346
|
-
const data = encodeData(type, {
|
|
1457
|
+
const data = encodeData(type, {
|
|
1458
|
+
lamports: toBN(lamports),
|
|
1459
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1460
|
+
});
|
|
1347
1461
|
const keys = [
|
|
1348
1462
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1349
1463
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1369,7 +1483,10 @@ class StakePoolInstruction {
|
|
|
1369
1483
|
static decreaseValidatorStakeWithReserve(params) {
|
|
1370
1484
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, validatorStake, transientStake, lamports, transientStakeSeed, } = params;
|
|
1371
1485
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseValidatorStakeWithReserve;
|
|
1372
|
-
const data = encodeData(type, {
|
|
1486
|
+
const data = encodeData(type, {
|
|
1487
|
+
lamports: toBN(lamports),
|
|
1488
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1489
|
+
});
|
|
1373
1490
|
const keys = [
|
|
1374
1491
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1375
1492
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1396,7 +1513,11 @@ class StakePoolInstruction {
|
|
|
1396
1513
|
static decreaseAdditionalValidatorStake(params) {
|
|
1397
1514
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, validatorStake, transientStake, lamports, transientStakeSeed, ephemeralStakeSeed, ephemeralStake, } = params;
|
|
1398
1515
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseAdditionalValidatorStake;
|
|
1399
|
-
const data = encodeData(type, {
|
|
1516
|
+
const data = encodeData(type, {
|
|
1517
|
+
lamports: toBN(lamports),
|
|
1518
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1519
|
+
ephemeralStakeSeed: toBN(ephemeralStakeSeed),
|
|
1520
|
+
});
|
|
1400
1521
|
const keys = [
|
|
1401
1522
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1402
1523
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1453,7 +1574,7 @@ class StakePoolInstruction {
|
|
|
1453
1574
|
static depositSol(params) {
|
|
1454
1575
|
const { programId, stakePool, withdrawAuthority, depositAuthority, reserveStake, fundingAccount, destinationPoolAccount, managerFeeAccount, referralPoolAccount, poolMint, lamports, } = params;
|
|
1455
1576
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol;
|
|
1456
|
-
const data = encodeData(type, { lamports });
|
|
1577
|
+
const data = encodeData(type, { lamports: toBN(lamports) });
|
|
1457
1578
|
const keys = [
|
|
1458
1579
|
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
|
1459
1580
|
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
|
@@ -1486,8 +1607,8 @@ class StakePoolInstruction {
|
|
|
1486
1607
|
var _a;
|
|
1487
1608
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositWsolWithSession;
|
|
1488
1609
|
const data = encodeData(type, {
|
|
1489
|
-
lamportsIn: params.lamportsIn,
|
|
1490
|
-
minimumPoolTokensOut: params.minimumPoolTokensOut,
|
|
1610
|
+
lamportsIn: toBN(params.lamportsIn),
|
|
1611
|
+
minimumPoolTokensOut: toBN(params.minimumPoolTokensOut),
|
|
1491
1612
|
});
|
|
1492
1613
|
const keys = [
|
|
1493
1614
|
{ pubkey: params.stakePool, isSigner: false, isWritable: true },
|
|
@@ -1529,7 +1650,7 @@ class StakePoolInstruction {
|
|
|
1529
1650
|
static withdrawStake(params) {
|
|
1530
1651
|
const { programId, stakePool, validatorList, withdrawAuthority, validatorStake, destinationStake, destinationStakeAuthority, sourceTransferAuthority, sourcePoolAccount, managerFeeAccount, poolMint, poolTokens, } = params;
|
|
1531
1652
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStake;
|
|
1532
|
-
const data = encodeData(type, { poolTokens });
|
|
1653
|
+
const data = encodeData(type, { poolTokens: toBN(poolTokens) });
|
|
1533
1654
|
const keys = [
|
|
1534
1655
|
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
|
1535
1656
|
{ pubkey: validatorList, isSigner: false, isWritable: true },
|
|
@@ -1557,7 +1678,7 @@ class StakePoolInstruction {
|
|
|
1557
1678
|
static withdrawSol(params) {
|
|
1558
1679
|
const { programId, stakePool, withdrawAuthority, sourceTransferAuthority, sourcePoolAccount, reserveStake, destinationSystemAccount, managerFeeAccount, solWithdrawAuthority, poolMint, poolTokens, } = params;
|
|
1559
1680
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawSol;
|
|
1560
|
-
const data = encodeData(type, { poolTokens });
|
|
1681
|
+
const data = encodeData(type, { poolTokens: toBN(poolTokens) });
|
|
1561
1682
|
const keys = [
|
|
1562
1683
|
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
|
1563
1684
|
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
|
@@ -1592,8 +1713,8 @@ class StakePoolInstruction {
|
|
|
1592
1713
|
static withdrawWsolWithSession(params) {
|
|
1593
1714
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawWsolWithSession;
|
|
1594
1715
|
const data = encodeData(type, {
|
|
1595
|
-
poolTokensIn: params.poolTokensIn,
|
|
1596
|
-
minimumLamportsOut: params.minimumLamportsOut,
|
|
1716
|
+
poolTokensIn: toBN(params.poolTokensIn),
|
|
1717
|
+
minimumLamportsOut: toBN(params.minimumLamportsOut),
|
|
1597
1718
|
});
|
|
1598
1719
|
const keys = [
|
|
1599
1720
|
{ pubkey: params.stakePool, isSigner: false, isWritable: true },
|
|
@@ -1630,14 +1751,14 @@ class StakePoolInstruction {
|
|
|
1630
1751
|
}
|
|
1631
1752
|
/**
|
|
1632
1753
|
* Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
|
|
1633
|
-
* The stake account is created as a PDA and rent is
|
|
1754
|
+
* The stake account is created as a PDA and rent is funded from the reserve.
|
|
1634
1755
|
*/
|
|
1635
1756
|
static withdrawStakeWithSession(params) {
|
|
1636
1757
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession;
|
|
1637
1758
|
const data = encodeData(type, {
|
|
1638
|
-
poolTokensIn: params.poolTokensIn,
|
|
1639
|
-
minimumLamportsOut: params.minimumLamportsOut,
|
|
1640
|
-
userStakeSeed: params.userStakeSeed,
|
|
1759
|
+
poolTokensIn: toBN(params.poolTokensIn),
|
|
1760
|
+
minimumLamportsOut: toBN(params.minimumLamportsOut),
|
|
1761
|
+
userStakeSeed: toBN(params.userStakeSeed),
|
|
1641
1762
|
});
|
|
1642
1763
|
const keys = [
|
|
1643
1764
|
{ pubkey: params.stakePool, isSigner: false, isWritable: true },
|
|
@@ -1645,17 +1766,19 @@ class StakePoolInstruction {
|
|
|
1645
1766
|
{ pubkey: params.withdrawAuthority, isSigner: false, isWritable: false },
|
|
1646
1767
|
{ pubkey: params.stakeToSplit, isSigner: false, isWritable: true },
|
|
1647
1768
|
{ pubkey: params.stakeToReceive, isSigner: false, isWritable: true },
|
|
1648
|
-
{ pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
|
|
1649
|
-
{ pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (
|
|
1769
|
+
{ pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
|
|
1770
|
+
{ pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (unused in session path)
|
|
1650
1771
|
{ pubkey: params.burnFromPool, isSigner: false, isWritable: true },
|
|
1651
1772
|
{ pubkey: params.managerFeeAccount, isSigner: false, isWritable: true },
|
|
1652
1773
|
{ pubkey: params.poolMint, isSigner: false, isWritable: true },
|
|
1653
1774
|
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
|
1654
1775
|
{ pubkey: params.tokenProgramId, isSigner: false, isWritable: false },
|
|
1655
1776
|
{ pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
|
|
1777
|
+
// Session-specific accounts
|
|
1656
1778
|
{ pubkey: params.programSigner, isSigner: false, isWritable: false },
|
|
1657
1779
|
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
1658
|
-
{ pubkey: params.
|
|
1780
|
+
{ pubkey: params.reserveStake, isSigner: false, isWritable: true },
|
|
1781
|
+
{ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
|
|
1659
1782
|
];
|
|
1660
1783
|
return new TransactionInstruction({
|
|
1661
1784
|
programId: params.programId,
|
|
@@ -1670,8 +1793,8 @@ class StakePoolInstruction {
|
|
|
1670
1793
|
static withdrawFromStakeAccountWithSession(params) {
|
|
1671
1794
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawFromStakeAccountWithSession;
|
|
1672
1795
|
const data = encodeData(type, {
|
|
1673
|
-
lamports: params.lamports,
|
|
1674
|
-
userStakeSeed: params.userStakeSeed,
|
|
1796
|
+
lamports: toBN(params.lamports),
|
|
1797
|
+
userStakeSeed: toBN(params.userStakeSeed),
|
|
1675
1798
|
});
|
|
1676
1799
|
const keys = [
|
|
1677
1800
|
{ pubkey: params.userStakeAccount, isSigner: false, isWritable: true },
|
|
@@ -1960,10 +2083,18 @@ skipBalanceCheck = false) {
|
|
|
1960
2083
|
if (!skipBalanceCheck) {
|
|
1961
2084
|
const tokenAccountInfo = await connection.getTokenAccountBalance(wsolTokenAccount, 'confirmed');
|
|
1962
2085
|
const wsolBalance = tokenAccountInfo
|
|
1963
|
-
?
|
|
1964
|
-
: 0;
|
|
1965
|
-
|
|
1966
|
-
|
|
2086
|
+
? BigInt(tokenAccountInfo.value.amount)
|
|
2087
|
+
: BigInt(0);
|
|
2088
|
+
// Convert lamports to BigInt for comparison
|
|
2089
|
+
const lamportsBigInt = typeof lamports === 'bigint'
|
|
2090
|
+
? lamports
|
|
2091
|
+
: typeof lamports === 'string'
|
|
2092
|
+
? BigInt(lamports)
|
|
2093
|
+
: BN.isBN(lamports)
|
|
2094
|
+
? BigInt(lamports.toString())
|
|
2095
|
+
: BigInt(lamports);
|
|
2096
|
+
if (wsolBalance < lamportsBigInt) {
|
|
2097
|
+
throw new Error(`Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(Number(wsolBalance))} WSOL.`);
|
|
1967
2098
|
}
|
|
1968
2099
|
}
|
|
1969
2100
|
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
|
@@ -2009,7 +2140,15 @@ skipBalanceCheck = false) {
|
|
|
2009
2140
|
*/
|
|
2010
2141
|
async function depositSol(connection, stakePoolAddress, from, lamports, destinationTokenAccount, referrerTokenAccount, depositAuthority) {
|
|
2011
2142
|
const fromBalance = await connection.getBalance(from, 'confirmed');
|
|
2012
|
-
|
|
2143
|
+
// Convert lamports to BigInt for comparison
|
|
2144
|
+
const lamportsBigInt = typeof lamports === 'bigint'
|
|
2145
|
+
? lamports
|
|
2146
|
+
: typeof lamports === 'string'
|
|
2147
|
+
? BigInt(lamports)
|
|
2148
|
+
: BN.isBN(lamports)
|
|
2149
|
+
? BigInt(lamports.toString())
|
|
2150
|
+
: BigInt(lamports);
|
|
2151
|
+
if (BigInt(fromBalance) < lamportsBigInt) {
|
|
2013
2152
|
throw new Error(`Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(fromBalance)} SOL.`);
|
|
2014
2153
|
}
|
|
2015
2154
|
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
|
@@ -2023,7 +2162,7 @@ async function depositSol(connection, stakePoolAddress, from, lamports, destinat
|
|
|
2023
2162
|
instructions.push(SystemProgram.transfer({
|
|
2024
2163
|
fromPubkey: from,
|
|
2025
2164
|
toPubkey: userSolTransfer.publicKey,
|
|
2026
|
-
lamports,
|
|
2165
|
+
lamports: lamportsBigInt,
|
|
2027
2166
|
}));
|
|
2028
2167
|
// Create token account if not specified
|
|
2029
2168
|
if (!destinationTokenAccount) {
|
|
@@ -2395,37 +2534,51 @@ async function getUserStakeAccounts(connection, programId, userPubkey, maxSeed =
|
|
|
2395
2534
|
* Withdraws stake from a stake pool using a Fogo session.
|
|
2396
2535
|
*
|
|
2397
2536
|
* The on-chain program creates stake account PDAs. The rent for these accounts
|
|
2398
|
-
* is
|
|
2537
|
+
* is funded from the reserve stake.
|
|
2399
2538
|
*
|
|
2400
2539
|
* @param connection - Solana connection
|
|
2401
2540
|
* @param stakePoolAddress - The stake pool to withdraw from
|
|
2402
2541
|
* @param signerOrSession - The session signer public key
|
|
2403
2542
|
* @param userPubkey - User's wallet (used for PDA derivation and token ownership)
|
|
2404
|
-
* @param payer - Payer for stake account rent (typically paymaster)
|
|
2405
2543
|
* @param amount - Amount of pool tokens to withdraw
|
|
2406
2544
|
* @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
|
|
2407
2545
|
* @param useReserve - Whether to withdraw from reserve (default: false)
|
|
2408
2546
|
* @param voteAccountAddress - Optional specific validator to withdraw from
|
|
2409
2547
|
* @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
|
|
2410
2548
|
* @param validatorComparator - Optional comparator for validator selection
|
|
2549
|
+
* @param allowPartial - If true, returns partial results instead of throwing when not enough stake available
|
|
2411
2550
|
*/
|
|
2412
|
-
async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey,
|
|
2413
|
-
|
|
2551
|
+
async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator, allowPartial = false) {
|
|
2552
|
+
var _c;
|
|
2414
2553
|
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
|
|
2554
|
+
// First fetch: get stake pool to know other account addresses
|
|
2555
|
+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
|
2415
2556
|
const stakePool = stakePoolAccount.account.data;
|
|
2416
2557
|
const poolTokens = solToLamports(amount);
|
|
2417
2558
|
const poolAmount = new BN(poolTokens);
|
|
2418
2559
|
const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey);
|
|
2419
|
-
|
|
2560
|
+
// Second fetch: get ALL remaining data in parallel
|
|
2561
|
+
const [tokenAccount, stakeAccountRentExemption, validatorListAcc, stakeMinimumDelegationResponse] = await Promise.all([
|
|
2562
|
+
getAccount(connection, poolTokenAccount),
|
|
2563
|
+
connection.getMinimumBalanceForRentExemption(StakeProgram.space),
|
|
2564
|
+
connection.getAccountInfo(stakePool.validatorList),
|
|
2565
|
+
connection.getStakeMinimumDelegation(),
|
|
2566
|
+
]);
|
|
2567
|
+
// Pre-fetch data to avoid duplicate RPC calls in prepareWithdrawAccounts
|
|
2568
|
+
const prefetchedData = {
|
|
2569
|
+
validatorListData: (_c = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _c !== void 0 ? _c : null,
|
|
2570
|
+
minBalanceForRentExemption: stakeAccountRentExemption,
|
|
2571
|
+
stakeMinimumDelegation: Number(stakeMinimumDelegationResponse.value),
|
|
2572
|
+
};
|
|
2420
2573
|
if (tokenAccount.amount < poolTokens) {
|
|
2421
2574
|
throw new Error(`Not enough token balance to withdraw ${amount} pool tokens.
|
|
2422
2575
|
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`);
|
|
2423
2576
|
}
|
|
2424
2577
|
const [programSigner] = PublicKey.findProgramAddressSync([Buffer.from('fogo_session_program_signer')], stakePoolProgramId);
|
|
2425
2578
|
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(stakePoolProgramId, stakePoolAddress);
|
|
2426
|
-
const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
|
|
2427
2579
|
// Determine which stake accounts to withdraw from
|
|
2428
2580
|
const withdrawAccounts = [];
|
|
2581
|
+
let partialRemainingAmount;
|
|
2429
2582
|
if (useReserve) {
|
|
2430
2583
|
withdrawAccounts.push({
|
|
2431
2584
|
stakeAddress: stakePool.reserveStake,
|
|
@@ -2456,7 +2609,14 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2456
2609
|
}
|
|
2457
2610
|
else {
|
|
2458
2611
|
// Get the list of accounts to withdraw from automatically
|
|
2459
|
-
|
|
2612
|
+
if (allowPartial) {
|
|
2613
|
+
const result = await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), true, prefetchedData);
|
|
2614
|
+
withdrawAccounts.push(...result.withdrawAccounts);
|
|
2615
|
+
partialRemainingAmount = result.remainingAmount;
|
|
2616
|
+
}
|
|
2617
|
+
else {
|
|
2618
|
+
withdrawAccounts.push(...(await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), undefined, prefetchedData)));
|
|
2619
|
+
}
|
|
2460
2620
|
}
|
|
2461
2621
|
const instructions = [];
|
|
2462
2622
|
const stakeAccountPubkeys = [];
|
|
@@ -2473,7 +2633,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2473
2633
|
const stakeReceiverPubkey = findUserStakeProgramAddress(stakePoolProgramId, userPubkey, userStakeSeed);
|
|
2474
2634
|
stakeAccountPubkeys.push(stakeReceiverPubkey);
|
|
2475
2635
|
userStakeSeeds.push(userStakeSeed);
|
|
2476
|
-
// The on-chain program creates the stake account PDA and rent is
|
|
2636
|
+
// The on-chain program creates the stake account PDA and rent is funded from reserve.
|
|
2477
2637
|
instructions.push(StakePoolInstruction.withdrawStakeWithSession({
|
|
2478
2638
|
programId: stakePoolProgramId,
|
|
2479
2639
|
stakePool: stakePoolAddress,
|
|
@@ -2487,7 +2647,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2487
2647
|
poolMint: stakePool.poolMint,
|
|
2488
2648
|
tokenProgramId: stakePool.tokenProgramId,
|
|
2489
2649
|
programSigner,
|
|
2490
|
-
|
|
2650
|
+
reserveStake: stakePool.reserveStake,
|
|
2491
2651
|
poolTokensIn: withdrawAccount.poolAmount.toNumber(),
|
|
2492
2652
|
minimumLamportsOut,
|
|
2493
2653
|
userStakeSeed,
|
|
@@ -2498,6 +2658,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2498
2658
|
instructions,
|
|
2499
2659
|
stakeAccountPubkeys,
|
|
2500
2660
|
userStakeSeeds,
|
|
2661
|
+
remainingPoolTokens: partialRemainingAmount ? lamportsToSol(partialRemainingAmount) : 0,
|
|
2501
2662
|
};
|
|
2502
2663
|
}
|
|
2503
2664
|
async function addValidatorToPool(connection, stakePoolAddress, validatorVote, seed) {
|