@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
|
@@ -459,6 +459,139 @@ const USER_STAKE_SEED_PREFIX = node_buffer.Buffer.from('user_stake');
|
|
|
459
459
|
// for merges without a mismatch on credits observed
|
|
460
460
|
const MINIMUM_ACTIVE_STAKE = 1000000;
|
|
461
461
|
|
|
462
|
+
/**
|
|
463
|
+
* BN-based layout for data decoding using buffer-layout.
|
|
464
|
+
* Used for decoding on-chain account data (StakePool, ValidatorList, etc.)
|
|
465
|
+
*/
|
|
466
|
+
class BNDataLayout extends bufferLayout.Layout {
|
|
467
|
+
constructor(span, signed, property) {
|
|
468
|
+
super(span, property);
|
|
469
|
+
this.blobLayout = bufferLayout.blob(span);
|
|
470
|
+
this.signed = signed;
|
|
471
|
+
}
|
|
472
|
+
decode(b, offset = 0) {
|
|
473
|
+
const num = new BN(this.blobLayout.decode(b, offset), 10, 'le');
|
|
474
|
+
if (this.signed) {
|
|
475
|
+
return num.fromTwos(this.span * 8).clone();
|
|
476
|
+
}
|
|
477
|
+
return num;
|
|
478
|
+
}
|
|
479
|
+
encode(src, b, offset = 0) {
|
|
480
|
+
if (this.signed) {
|
|
481
|
+
src = src.toTwos(this.span * 8);
|
|
482
|
+
}
|
|
483
|
+
return this.blobLayout.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Creates a u64 layout for data decoding (account layouts).
|
|
488
|
+
* Used in StakePoolLayout, ValidatorListLayout, etc.
|
|
489
|
+
*/
|
|
490
|
+
function u64(property) {
|
|
491
|
+
return new BNDataLayout(8, false, property);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* BN-based layout for 64-bit unsigned integers using @solana/buffer-layout.
|
|
495
|
+
* Used for encoding instruction data with support for values > MAX_SAFE_INTEGER.
|
|
496
|
+
*/
|
|
497
|
+
class BNInstructionLayout extends BufferLayout__namespace.Layout {
|
|
498
|
+
constructor(span, signed, property) {
|
|
499
|
+
super(span, property);
|
|
500
|
+
this.blobLayout = BufferLayout__namespace.blob(span);
|
|
501
|
+
this.signed = signed;
|
|
502
|
+
}
|
|
503
|
+
decode(b, offset = 0) {
|
|
504
|
+
const num = new BN(this.blobLayout.decode(b, offset), 10, 'le');
|
|
505
|
+
if (this.signed) {
|
|
506
|
+
return num.fromTwos(this.span * 8).clone();
|
|
507
|
+
}
|
|
508
|
+
return num;
|
|
509
|
+
}
|
|
510
|
+
encode(src, b, offset = 0) {
|
|
511
|
+
if (this.signed) {
|
|
512
|
+
src = src.toTwos(this.span * 8);
|
|
513
|
+
}
|
|
514
|
+
return this.blobLayout.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
|
515
|
+
}
|
|
516
|
+
getSpan(_b, _offset) {
|
|
517
|
+
return this.span;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Creates a u64 layout for instruction encoding.
|
|
522
|
+
* Properly handles BN values larger than Number.MAX_SAFE_INTEGER.
|
|
523
|
+
* Compatible with @solana/buffer-layout.struct().
|
|
524
|
+
*/
|
|
525
|
+
// eslint-disable-next-line ts/no-explicit-any
|
|
526
|
+
function u64Instruction(property) {
|
|
527
|
+
return new BNInstructionLayout(8, false, property);
|
|
528
|
+
}
|
|
529
|
+
class WrappedLayout extends bufferLayout.Layout {
|
|
530
|
+
constructor(layout, decoder, encoder, property) {
|
|
531
|
+
super(layout.span, property);
|
|
532
|
+
this.layout = layout;
|
|
533
|
+
this.decoder = decoder;
|
|
534
|
+
this.encoder = encoder;
|
|
535
|
+
}
|
|
536
|
+
decode(b, offset) {
|
|
537
|
+
return this.decoder(this.layout.decode(b, offset));
|
|
538
|
+
}
|
|
539
|
+
encode(src, b, offset) {
|
|
540
|
+
return this.layout.encode(this.encoder(src), b, offset);
|
|
541
|
+
}
|
|
542
|
+
getSpan(b, offset) {
|
|
543
|
+
return this.layout.getSpan(b, offset);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function publicKey(property) {
|
|
547
|
+
return new WrappedLayout(bufferLayout.blob(32), (b) => new web3_js.PublicKey(b), (key) => key.toBuffer(), property);
|
|
548
|
+
}
|
|
549
|
+
class OptionLayout extends bufferLayout.Layout {
|
|
550
|
+
constructor(layout, property) {
|
|
551
|
+
super(-1, property);
|
|
552
|
+
this.layout = layout;
|
|
553
|
+
this.discriminator = bufferLayout.u8();
|
|
554
|
+
}
|
|
555
|
+
encode(src, b, offset = 0) {
|
|
556
|
+
if (src === null || src === undefined) {
|
|
557
|
+
return this.discriminator.encode(0, b, offset);
|
|
558
|
+
}
|
|
559
|
+
this.discriminator.encode(1, b, offset);
|
|
560
|
+
return this.layout.encode(src, b, offset + 1) + 1;
|
|
561
|
+
}
|
|
562
|
+
decode(b, offset = 0) {
|
|
563
|
+
const discriminator = this.discriminator.decode(b, offset);
|
|
564
|
+
if (discriminator === 0) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
else if (discriminator === 1) {
|
|
568
|
+
return this.layout.decode(b, offset + 1);
|
|
569
|
+
}
|
|
570
|
+
throw new Error(`Invalid option ${this.property}`);
|
|
571
|
+
}
|
|
572
|
+
getSpan(b, offset = 0) {
|
|
573
|
+
const discriminator = this.discriminator.decode(b, offset);
|
|
574
|
+
if (discriminator === 0) {
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
else if (discriminator === 1) {
|
|
578
|
+
return this.layout.getSpan(b, offset + 1) + 1;
|
|
579
|
+
}
|
|
580
|
+
throw new Error(`Invalid option ${this.property}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function option(layout, property) {
|
|
584
|
+
return new OptionLayout(layout, property);
|
|
585
|
+
}
|
|
586
|
+
function vec(elementLayout, property) {
|
|
587
|
+
const length = bufferLayout.u32('length');
|
|
588
|
+
const layout = bufferLayout.struct([
|
|
589
|
+
length,
|
|
590
|
+
bufferLayout.seq(elementLayout, bufferLayout.offset(length, -length.span), 'values'),
|
|
591
|
+
]);
|
|
592
|
+
return new WrappedLayout(layout, ({ values }) => values, values => ({ values }), property);
|
|
593
|
+
}
|
|
594
|
+
|
|
462
595
|
/**
|
|
463
596
|
* Populate a buffer of instruction data using an InstructionType
|
|
464
597
|
* @internal
|
|
@@ -577,95 +710,6 @@ function findUserStakeProgramAddress(programId, userWallet, seed) {
|
|
|
577
710
|
return publicKey;
|
|
578
711
|
}
|
|
579
712
|
|
|
580
|
-
class BNLayout extends bufferLayout.Layout {
|
|
581
|
-
constructor(span, signed, property) {
|
|
582
|
-
super(span, property);
|
|
583
|
-
this.blob = bufferLayout.blob(span);
|
|
584
|
-
this.signed = signed;
|
|
585
|
-
}
|
|
586
|
-
decode(b, offset = 0) {
|
|
587
|
-
const num = new BN(this.blob.decode(b, offset), 10, 'le');
|
|
588
|
-
if (this.signed) {
|
|
589
|
-
return num.fromTwos(this.span * 8).clone();
|
|
590
|
-
}
|
|
591
|
-
return num;
|
|
592
|
-
}
|
|
593
|
-
encode(src, b, offset = 0) {
|
|
594
|
-
if (this.signed) {
|
|
595
|
-
src = src.toTwos(this.span * 8);
|
|
596
|
-
}
|
|
597
|
-
return this.blob.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
function u64(property) {
|
|
601
|
-
return new BNLayout(8, false, property);
|
|
602
|
-
}
|
|
603
|
-
class WrappedLayout extends bufferLayout.Layout {
|
|
604
|
-
constructor(layout, decoder, encoder, property) {
|
|
605
|
-
super(layout.span, property);
|
|
606
|
-
this.layout = layout;
|
|
607
|
-
this.decoder = decoder;
|
|
608
|
-
this.encoder = encoder;
|
|
609
|
-
}
|
|
610
|
-
decode(b, offset) {
|
|
611
|
-
return this.decoder(this.layout.decode(b, offset));
|
|
612
|
-
}
|
|
613
|
-
encode(src, b, offset) {
|
|
614
|
-
return this.layout.encode(this.encoder(src), b, offset);
|
|
615
|
-
}
|
|
616
|
-
getSpan(b, offset) {
|
|
617
|
-
return this.layout.getSpan(b, offset);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
function publicKey(property) {
|
|
621
|
-
return new WrappedLayout(bufferLayout.blob(32), (b) => new web3_js.PublicKey(b), (key) => key.toBuffer(), property);
|
|
622
|
-
}
|
|
623
|
-
class OptionLayout extends bufferLayout.Layout {
|
|
624
|
-
constructor(layout, property) {
|
|
625
|
-
super(-1, property);
|
|
626
|
-
this.layout = layout;
|
|
627
|
-
this.discriminator = bufferLayout.u8();
|
|
628
|
-
}
|
|
629
|
-
encode(src, b, offset = 0) {
|
|
630
|
-
if (src === null || src === undefined) {
|
|
631
|
-
return this.discriminator.encode(0, b, offset);
|
|
632
|
-
}
|
|
633
|
-
this.discriminator.encode(1, b, offset);
|
|
634
|
-
return this.layout.encode(src, b, offset + 1) + 1;
|
|
635
|
-
}
|
|
636
|
-
decode(b, offset = 0) {
|
|
637
|
-
const discriminator = this.discriminator.decode(b, offset);
|
|
638
|
-
if (discriminator === 0) {
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
else if (discriminator === 1) {
|
|
642
|
-
return this.layout.decode(b, offset + 1);
|
|
643
|
-
}
|
|
644
|
-
throw new Error(`Invalid option ${this.property}`);
|
|
645
|
-
}
|
|
646
|
-
getSpan(b, offset = 0) {
|
|
647
|
-
const discriminator = this.discriminator.decode(b, offset);
|
|
648
|
-
if (discriminator === 0) {
|
|
649
|
-
return 1;
|
|
650
|
-
}
|
|
651
|
-
else if (discriminator === 1) {
|
|
652
|
-
return this.layout.getSpan(b, offset + 1) + 1;
|
|
653
|
-
}
|
|
654
|
-
throw new Error(`Invalid option ${this.property}`);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
function option(layout, property) {
|
|
658
|
-
return new OptionLayout(layout, property);
|
|
659
|
-
}
|
|
660
|
-
function vec(elementLayout, property) {
|
|
661
|
-
const length = bufferLayout.u32('length');
|
|
662
|
-
const layout = bufferLayout.struct([
|
|
663
|
-
length,
|
|
664
|
-
bufferLayout.seq(elementLayout, bufferLayout.offset(length, -length.span), 'values'),
|
|
665
|
-
]);
|
|
666
|
-
return new WrappedLayout(layout, ({ values }) => values, values => ({ values }), property);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
713
|
const feeFields = [u64('denominator'), u64('numerator')];
|
|
670
714
|
var AccountType;
|
|
671
715
|
(function (AccountType) {
|
|
@@ -827,80 +871,84 @@ async function getValidatorListAccount(connection, pubkey) {
|
|
|
827
871
|
},
|
|
828
872
|
};
|
|
829
873
|
}
|
|
830
|
-
async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee) {
|
|
874
|
+
async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee, allowPartial, prefetchedData) {
|
|
831
875
|
var _a, _b;
|
|
832
876
|
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
877
|
+
// Use prefetched data if available, otherwise fetch from RPC
|
|
878
|
+
let validatorListData;
|
|
879
|
+
let minBalanceForRentExemption;
|
|
880
|
+
let stakeMinimumDelegation;
|
|
881
|
+
if (prefetchedData) {
|
|
882
|
+
validatorListData = prefetchedData.validatorListData;
|
|
883
|
+
minBalanceForRentExemption = prefetchedData.minBalanceForRentExemption;
|
|
884
|
+
stakeMinimumDelegation = prefetchedData.stakeMinimumDelegation;
|
|
837
885
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
886
|
+
else {
|
|
887
|
+
const [validatorListAcc, rentExemption, stakeMinimumDelegationResponse] = await Promise.all([
|
|
888
|
+
connection.getAccountInfo(stakePool.validatorList),
|
|
889
|
+
connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space),
|
|
890
|
+
connection.getStakeMinimumDelegation(),
|
|
891
|
+
]);
|
|
892
|
+
validatorListData = (_a = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _a !== void 0 ? _a : null;
|
|
893
|
+
minBalanceForRentExemption = rentExemption;
|
|
894
|
+
stakeMinimumDelegation = Number(stakeMinimumDelegationResponse.value);
|
|
895
|
+
}
|
|
896
|
+
if (!validatorListData) {
|
|
897
|
+
throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
|
|
898
|
+
}
|
|
899
|
+
const validatorList = ValidatorListLayout.decode(validatorListData);
|
|
900
|
+
if (!(validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators) || (validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators.length) === 0) {
|
|
901
|
+
throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
|
|
902
|
+
}
|
|
903
|
+
// minBalance = rent + max(stake_minimum_delegation, MINIMUM_ACTIVE_STAKE)
|
|
904
|
+
const minimumDelegation = Math.max(stakeMinimumDelegation, MINIMUM_ACTIVE_STAKE);
|
|
905
|
+
const minBalance = new BN(minBalanceForRentExemption + minimumDelegation);
|
|
906
|
+
// Threshold for has_active_stake check (ceiling division for lamports_per_pool_token)
|
|
907
|
+
const lamportsPerPoolToken = stakePool.totalLamports
|
|
908
|
+
.add(stakePool.poolTokenSupply)
|
|
909
|
+
.sub(new BN(1))
|
|
910
|
+
.div(stakePool.poolTokenSupply);
|
|
911
|
+
const minimumLamportsWithTolerance = minBalance.add(lamportsPerPoolToken);
|
|
912
|
+
const hasActiveStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
|
|
913
|
+
&& v.activeStakeLamports.gt(minimumLamportsWithTolerance));
|
|
914
|
+
const hasTransientStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
|
|
915
|
+
&& v.transientStakeLamports.gt(minimumLamportsWithTolerance));
|
|
916
|
+
// ValidatorRemoval mode: no validator above threshold
|
|
917
|
+
const isValidatorRemovalMode = !hasActiveStake && !hasTransientStake;
|
|
918
|
+
let accounts = [];
|
|
842
919
|
for (const validator of validatorList.validators) {
|
|
843
920
|
if (validator.status !== ValidatorStakeInfoStatus.Active) {
|
|
844
921
|
continue;
|
|
845
922
|
}
|
|
846
923
|
const stakeAccountAddress = await findStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
924
|
+
// ValidatorRemoval: full balance available; Normal: leave minBalance
|
|
925
|
+
const availableActiveLamports = isValidatorRemovalMode
|
|
926
|
+
? validator.activeStakeLamports
|
|
927
|
+
: validator.activeStakeLamports.sub(minBalance);
|
|
928
|
+
if (availableActiveLamports.gt(new BN(0))) {
|
|
929
|
+
const isPreferred = (_b = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _b === void 0 ? void 0 : _b.equals(validator.voteAccountAddress);
|
|
930
|
+
accounts.push({
|
|
851
931
|
type: isPreferred ? 'preferred' : 'active',
|
|
852
932
|
voteAddress: validator.voteAccountAddress,
|
|
853
933
|
stakeAddress: stakeAccountAddress,
|
|
934
|
+
lamports: availableActiveLamports,
|
|
854
935
|
});
|
|
855
936
|
}
|
|
856
|
-
|
|
857
|
-
|
|
937
|
+
const availableTransientLamports = isValidatorRemovalMode
|
|
938
|
+
? validator.transientStakeLamports
|
|
939
|
+
: validator.transientStakeLamports.sub(minBalance);
|
|
940
|
+
if (availableTransientLamports.gt(new BN(0))) {
|
|
858
941
|
const transientStakeAccountAddress = await findTransientStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress, validator.transientSeedSuffixStart);
|
|
859
|
-
|
|
942
|
+
accounts.push({
|
|
860
943
|
type: 'transient',
|
|
861
944
|
voteAddress: validator.voteAccountAddress,
|
|
862
945
|
stakeAddress: transientStakeAccountAddress,
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
// Fetch all stake accounts + reserve in one batch call
|
|
867
|
-
const addressesToFetch = [
|
|
868
|
-
...accountsToFetch.map(a => a.stakeAddress),
|
|
869
|
-
stakePool.reserveStake,
|
|
870
|
-
];
|
|
871
|
-
const accountInfos = await connection.getMultipleAccountsInfo(addressesToFetch);
|
|
872
|
-
// Build accounts list using actual on-chain balances
|
|
873
|
-
let accounts = [];
|
|
874
|
-
for (let i = 0; i < accountsToFetch.length; i++) {
|
|
875
|
-
const { type, voteAddress, stakeAddress } = accountsToFetch[i];
|
|
876
|
-
const accountInfo = accountInfos[i];
|
|
877
|
-
if (!accountInfo) {
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
// Use actual on-chain balance instead of validator list value
|
|
881
|
-
const actualLamports = new BN(accountInfo.lamports);
|
|
882
|
-
const availableLamports = actualLamports.sub(minBalance);
|
|
883
|
-
if (availableLamports.gt(new BN(0))) {
|
|
884
|
-
accounts.push({
|
|
885
|
-
type,
|
|
886
|
-
voteAddress,
|
|
887
|
-
stakeAddress,
|
|
888
|
-
lamports: availableLamports,
|
|
946
|
+
lamports: availableTransientLamports,
|
|
889
947
|
});
|
|
890
948
|
}
|
|
891
949
|
}
|
|
892
950
|
// Sort from highest to lowest balance
|
|
893
951
|
accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()));
|
|
894
|
-
// Add reserve stake using actual balance (last item in batch fetch)
|
|
895
|
-
const reserveAccountInfo = accountInfos[accountInfos.length - 1];
|
|
896
|
-
const reserveStakeBalance = new BN(((_b = reserveAccountInfo === null || reserveAccountInfo === void 0 ? void 0 : reserveAccountInfo.lamports) !== null && _b !== void 0 ? _b : 0) - minBalanceForRentExemption);
|
|
897
|
-
if (reserveStakeBalance.gt(new BN(0))) {
|
|
898
|
-
accounts.push({
|
|
899
|
-
type: 'reserve',
|
|
900
|
-
stakeAddress: stakePool.reserveStake,
|
|
901
|
-
lamports: reserveStakeBalance,
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
952
|
// Prepare the list of accounts to withdraw from
|
|
905
953
|
const withdrawFrom = [];
|
|
906
954
|
let remainingAmount = new BN(amount);
|
|
@@ -909,23 +957,24 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
|
|
|
909
957
|
numerator: fee.denominator.sub(fee.numerator),
|
|
910
958
|
denominator: fee.denominator,
|
|
911
959
|
};
|
|
912
|
-
for (const type of ['preferred', 'active', 'transient'
|
|
960
|
+
for (const type of ['preferred', 'active', 'transient']) {
|
|
913
961
|
const filteredAccounts = accounts.filter(a => a.type === type);
|
|
914
962
|
for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
|
|
915
|
-
if (lamports.lte(minBalance) && type === 'transient') {
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
963
|
let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports);
|
|
919
964
|
if (!skipFee && !inverseFee.numerator.isZero()) {
|
|
920
965
|
availableForWithdrawal = availableForWithdrawal
|
|
921
966
|
.mul(inverseFee.denominator)
|
|
922
967
|
.div(inverseFee.numerator);
|
|
923
968
|
}
|
|
969
|
+
// In ValidatorRemoval mode, must withdraw full validator balance (no partial)
|
|
970
|
+
// Skip if remaining amount is less than full validator balance
|
|
971
|
+
if (isValidatorRemovalMode && remainingAmount.lt(availableForWithdrawal)) {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
924
974
|
const poolAmount = BN.min(availableForWithdrawal, remainingAmount);
|
|
925
975
|
if (poolAmount.lte(new BN(0))) {
|
|
926
976
|
continue;
|
|
927
977
|
}
|
|
928
|
-
// Those accounts will be withdrawn completely with `claim` instruction
|
|
929
978
|
withdrawFrom.push({ stakeAddress, voteAddress, poolAmount });
|
|
930
979
|
remainingAmount = remainingAmount.sub(poolAmount);
|
|
931
980
|
if (remainingAmount.isZero()) {
|
|
@@ -938,7 +987,23 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
|
|
|
938
987
|
}
|
|
939
988
|
// Not enough stake to withdraw the specified amount
|
|
940
989
|
if (remainingAmount.gt(new BN(0))) {
|
|
941
|
-
|
|
990
|
+
if (allowPartial) {
|
|
991
|
+
const delayedAmount = amount.sub(remainingAmount);
|
|
992
|
+
return {
|
|
993
|
+
withdrawAccounts: withdrawFrom,
|
|
994
|
+
delayedAmount,
|
|
995
|
+
remainingAmount,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const availableAmount = amount.sub(remainingAmount);
|
|
999
|
+
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.`);
|
|
1000
|
+
}
|
|
1001
|
+
if (allowPartial) {
|
|
1002
|
+
return {
|
|
1003
|
+
withdrawAccounts: withdrawFrom,
|
|
1004
|
+
delayedAmount: amount,
|
|
1005
|
+
remainingAmount: new BN(0),
|
|
1006
|
+
};
|
|
942
1007
|
}
|
|
943
1008
|
return withdrawFrom;
|
|
944
1009
|
}
|
|
@@ -987,11 +1052,50 @@ function arrayChunk(array, size) {
|
|
|
987
1052
|
return result;
|
|
988
1053
|
}
|
|
989
1054
|
|
|
1055
|
+
/**
|
|
1056
|
+
* Converts various numeric types to BN for safe large number handling.
|
|
1057
|
+
* @internal
|
|
1058
|
+
*/
|
|
1059
|
+
function toBN(value) {
|
|
1060
|
+
if (BN.isBN(value)) {
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof value === 'bigint') {
|
|
1064
|
+
return new BN(value.toString());
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof value === 'string') {
|
|
1067
|
+
// Validate string is a valid non-negative integer
|
|
1068
|
+
const trimmed = value.trim();
|
|
1069
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
1070
|
+
throw new Error(`Invalid amount string: "${value}". Must be a non-negative integer.`);
|
|
1071
|
+
}
|
|
1072
|
+
return new BN(trimmed);
|
|
1073
|
+
}
|
|
1074
|
+
if (typeof value === 'number') {
|
|
1075
|
+
if (!Number.isFinite(value)) {
|
|
1076
|
+
throw new Error('Invalid amount: must be a finite number');
|
|
1077
|
+
}
|
|
1078
|
+
if (value < 0) {
|
|
1079
|
+
throw new Error('Invalid amount: must be non-negative');
|
|
1080
|
+
}
|
|
1081
|
+
if (!Number.isInteger(value)) {
|
|
1082
|
+
throw new Error('Invalid amount: must be an integer (lamports)');
|
|
1083
|
+
}
|
|
1084
|
+
// CRITICAL: Numbers > MAX_SAFE_INTEGER have already lost precision
|
|
1085
|
+
// We throw an error instead of silently corrupting data
|
|
1086
|
+
if (value > Number.MAX_SAFE_INTEGER) {
|
|
1087
|
+
throw new Error(`Amount ${value} exceeds Number.MAX_SAFE_INTEGER (9,007,199,254,740,991). ` +
|
|
1088
|
+
`Use BigInt or BN for large values to avoid precision loss.`);
|
|
1089
|
+
}
|
|
1090
|
+
return new BN(value);
|
|
1091
|
+
}
|
|
1092
|
+
throw new Error(`Invalid amount type: ${typeof value}`);
|
|
1093
|
+
}
|
|
990
1094
|
// 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts
|
|
991
1095
|
const MOVE_STAKE_LAYOUT = BufferLayout__namespace.struct([
|
|
992
1096
|
BufferLayout__namespace.u8('instruction'),
|
|
993
|
-
|
|
994
|
-
|
|
1097
|
+
u64Instruction('lamports'),
|
|
1098
|
+
u64Instruction('transientStakeSeed'),
|
|
995
1099
|
]);
|
|
996
1100
|
const UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = BufferLayout__namespace.struct([
|
|
997
1101
|
BufferLayout__namespace.u8('instruction'),
|
|
@@ -1066,7 +1170,7 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1066
1170
|
index: 10,
|
|
1067
1171
|
layout: BufferLayout__namespace.struct([
|
|
1068
1172
|
BufferLayout__namespace.u8('instruction'),
|
|
1069
|
-
|
|
1173
|
+
u64Instruction('poolTokens'),
|
|
1070
1174
|
]),
|
|
1071
1175
|
},
|
|
1072
1176
|
/// Deposit SOL directly into the pool's reserve account. The output is a "pool" token
|
|
@@ -1075,7 +1179,7 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1075
1179
|
index: 14,
|
|
1076
1180
|
layout: BufferLayout__namespace.struct([
|
|
1077
1181
|
BufferLayout__namespace.u8('instruction'),
|
|
1078
|
-
|
|
1182
|
+
u64Instruction('lamports'),
|
|
1079
1183
|
]),
|
|
1080
1184
|
},
|
|
1081
1185
|
/// Withdraw SOL directly from the pool's reserve account. Fails if the
|
|
@@ -1084,25 +1188,25 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1084
1188
|
index: 16,
|
|
1085
1189
|
layout: BufferLayout__namespace.struct([
|
|
1086
1190
|
BufferLayout__namespace.u8('instruction'),
|
|
1087
|
-
|
|
1191
|
+
u64Instruction('poolTokens'),
|
|
1088
1192
|
]),
|
|
1089
1193
|
},
|
|
1090
1194
|
IncreaseAdditionalValidatorStake: {
|
|
1091
1195
|
index: 19,
|
|
1092
1196
|
layout: BufferLayout__namespace.struct([
|
|
1093
1197
|
BufferLayout__namespace.u8('instruction'),
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1198
|
+
u64Instruction('lamports'),
|
|
1199
|
+
u64Instruction('transientStakeSeed'),
|
|
1200
|
+
u64Instruction('ephemeralStakeSeed'),
|
|
1097
1201
|
]),
|
|
1098
1202
|
},
|
|
1099
1203
|
DecreaseAdditionalValidatorStake: {
|
|
1100
1204
|
index: 20,
|
|
1101
1205
|
layout: BufferLayout__namespace.struct([
|
|
1102
1206
|
BufferLayout__namespace.u8('instruction'),
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1207
|
+
u64Instruction('lamports'),
|
|
1208
|
+
u64Instruction('transientStakeSeed'),
|
|
1209
|
+
u64Instruction('ephemeralStakeSeed'),
|
|
1106
1210
|
]),
|
|
1107
1211
|
},
|
|
1108
1212
|
DecreaseValidatorStakeWithReserve: {
|
|
@@ -1117,62 +1221,62 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
|
1117
1221
|
index: 23,
|
|
1118
1222
|
layout: BufferLayout__namespace.struct([
|
|
1119
1223
|
BufferLayout__namespace.u8('instruction'),
|
|
1120
|
-
|
|
1224
|
+
u64Instruction('lamports'),
|
|
1121
1225
|
]),
|
|
1122
1226
|
},
|
|
1123
1227
|
WithdrawStakeWithSlippage: {
|
|
1124
1228
|
index: 24,
|
|
1125
1229
|
layout: BufferLayout__namespace.struct([
|
|
1126
1230
|
BufferLayout__namespace.u8('instruction'),
|
|
1127
|
-
|
|
1128
|
-
|
|
1231
|
+
u64Instruction('poolTokensIn'),
|
|
1232
|
+
u64Instruction('minimumLamportsOut'),
|
|
1129
1233
|
]),
|
|
1130
1234
|
},
|
|
1131
1235
|
DepositSolWithSlippage: {
|
|
1132
1236
|
index: 25,
|
|
1133
1237
|
layout: BufferLayout__namespace.struct([
|
|
1134
1238
|
BufferLayout__namespace.u8('instruction'),
|
|
1135
|
-
|
|
1239
|
+
u64Instruction('lamports'),
|
|
1136
1240
|
]),
|
|
1137
1241
|
},
|
|
1138
1242
|
WithdrawSolWithSlippage: {
|
|
1139
1243
|
index: 26,
|
|
1140
1244
|
layout: BufferLayout__namespace.struct([
|
|
1141
1245
|
BufferLayout__namespace.u8('instruction'),
|
|
1142
|
-
|
|
1246
|
+
u64Instruction('lamports'),
|
|
1143
1247
|
]),
|
|
1144
1248
|
},
|
|
1145
1249
|
DepositWsolWithSession: {
|
|
1146
1250
|
index: 27,
|
|
1147
1251
|
layout: BufferLayout__namespace.struct([
|
|
1148
1252
|
BufferLayout__namespace.u8('instruction'),
|
|
1149
|
-
|
|
1150
|
-
|
|
1253
|
+
u64Instruction('lamportsIn'),
|
|
1254
|
+
u64Instruction('minimumPoolTokensOut'),
|
|
1151
1255
|
]),
|
|
1152
1256
|
},
|
|
1153
1257
|
WithdrawWsolWithSession: {
|
|
1154
1258
|
index: 28,
|
|
1155
1259
|
layout: BufferLayout__namespace.struct([
|
|
1156
1260
|
BufferLayout__namespace.u8('instruction'),
|
|
1157
|
-
|
|
1158
|
-
|
|
1261
|
+
u64Instruction('poolTokensIn'),
|
|
1262
|
+
u64Instruction('minimumLamportsOut'),
|
|
1159
1263
|
]),
|
|
1160
1264
|
},
|
|
1161
1265
|
WithdrawStakeWithSession: {
|
|
1162
1266
|
index: 29,
|
|
1163
1267
|
layout: BufferLayout__namespace.struct([
|
|
1164
1268
|
BufferLayout__namespace.u8('instruction'),
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1269
|
+
u64Instruction('poolTokensIn'),
|
|
1270
|
+
u64Instruction('minimumLamportsOut'),
|
|
1271
|
+
u64Instruction('userStakeSeed'),
|
|
1168
1272
|
]),
|
|
1169
1273
|
},
|
|
1170
1274
|
WithdrawFromStakeAccountWithSession: {
|
|
1171
1275
|
index: 30,
|
|
1172
1276
|
layout: BufferLayout__namespace.struct([
|
|
1173
1277
|
BufferLayout__namespace.u8('instruction'),
|
|
1174
|
-
|
|
1175
|
-
|
|
1278
|
+
u64Instruction('lamports'),
|
|
1279
|
+
u64Instruction('userStakeSeed'),
|
|
1176
1280
|
]),
|
|
1177
1281
|
},
|
|
1178
1282
|
});
|
|
@@ -1304,7 +1408,10 @@ class StakePoolInstruction {
|
|
|
1304
1408
|
static increaseValidatorStake(params) {
|
|
1305
1409
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, transientStake, validatorStake, validatorVote, lamports, transientStakeSeed, } = params;
|
|
1306
1410
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseValidatorStake;
|
|
1307
|
-
const data = encodeData(type, {
|
|
1411
|
+
const data = encodeData(type, {
|
|
1412
|
+
lamports: toBN(lamports),
|
|
1413
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1414
|
+
});
|
|
1308
1415
|
const keys = [
|
|
1309
1416
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1310
1417
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1334,7 +1441,11 @@ class StakePoolInstruction {
|
|
|
1334
1441
|
static increaseAdditionalValidatorStake(params) {
|
|
1335
1442
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, transientStake, validatorStake, validatorVote, lamports, transientStakeSeed, ephemeralStake, ephemeralStakeSeed, } = params;
|
|
1336
1443
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseAdditionalValidatorStake;
|
|
1337
|
-
const data = encodeData(type, {
|
|
1444
|
+
const data = encodeData(type, {
|
|
1445
|
+
lamports: toBN(lamports),
|
|
1446
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1447
|
+
ephemeralStakeSeed: toBN(ephemeralStakeSeed),
|
|
1448
|
+
});
|
|
1338
1449
|
const keys = [
|
|
1339
1450
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1340
1451
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1364,7 +1475,10 @@ class StakePoolInstruction {
|
|
|
1364
1475
|
static decreaseValidatorStake(params) {
|
|
1365
1476
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, validatorStake, transientStake, lamports, transientStakeSeed, } = params;
|
|
1366
1477
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseValidatorStake;
|
|
1367
|
-
const data = encodeData(type, {
|
|
1478
|
+
const data = encodeData(type, {
|
|
1479
|
+
lamports: toBN(lamports),
|
|
1480
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1481
|
+
});
|
|
1368
1482
|
const keys = [
|
|
1369
1483
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1370
1484
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1390,7 +1504,10 @@ class StakePoolInstruction {
|
|
|
1390
1504
|
static decreaseValidatorStakeWithReserve(params) {
|
|
1391
1505
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, validatorStake, transientStake, lamports, transientStakeSeed, } = params;
|
|
1392
1506
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseValidatorStakeWithReserve;
|
|
1393
|
-
const data = encodeData(type, {
|
|
1507
|
+
const data = encodeData(type, {
|
|
1508
|
+
lamports: toBN(lamports),
|
|
1509
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1510
|
+
});
|
|
1394
1511
|
const keys = [
|
|
1395
1512
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1396
1513
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1417,7 +1534,11 @@ class StakePoolInstruction {
|
|
|
1417
1534
|
static decreaseAdditionalValidatorStake(params) {
|
|
1418
1535
|
const { programId, stakePool, staker, withdrawAuthority, validatorList, reserveStake, validatorStake, transientStake, lamports, transientStakeSeed, ephemeralStakeSeed, ephemeralStake, } = params;
|
|
1419
1536
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseAdditionalValidatorStake;
|
|
1420
|
-
const data = encodeData(type, {
|
|
1537
|
+
const data = encodeData(type, {
|
|
1538
|
+
lamports: toBN(lamports),
|
|
1539
|
+
transientStakeSeed: toBN(transientStakeSeed),
|
|
1540
|
+
ephemeralStakeSeed: toBN(ephemeralStakeSeed),
|
|
1541
|
+
});
|
|
1421
1542
|
const keys = [
|
|
1422
1543
|
{ pubkey: stakePool, isSigner: false, isWritable: false },
|
|
1423
1544
|
{ pubkey: staker, isSigner: true, isWritable: false },
|
|
@@ -1474,7 +1595,7 @@ class StakePoolInstruction {
|
|
|
1474
1595
|
static depositSol(params) {
|
|
1475
1596
|
const { programId, stakePool, withdrawAuthority, depositAuthority, reserveStake, fundingAccount, destinationPoolAccount, managerFeeAccount, referralPoolAccount, poolMint, lamports, } = params;
|
|
1476
1597
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol;
|
|
1477
|
-
const data = encodeData(type, { lamports });
|
|
1598
|
+
const data = encodeData(type, { lamports: toBN(lamports) });
|
|
1478
1599
|
const keys = [
|
|
1479
1600
|
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
|
1480
1601
|
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
|
@@ -1507,8 +1628,8 @@ class StakePoolInstruction {
|
|
|
1507
1628
|
var _a;
|
|
1508
1629
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositWsolWithSession;
|
|
1509
1630
|
const data = encodeData(type, {
|
|
1510
|
-
lamportsIn: params.lamportsIn,
|
|
1511
|
-
minimumPoolTokensOut: params.minimumPoolTokensOut,
|
|
1631
|
+
lamportsIn: toBN(params.lamportsIn),
|
|
1632
|
+
minimumPoolTokensOut: toBN(params.minimumPoolTokensOut),
|
|
1512
1633
|
});
|
|
1513
1634
|
const keys = [
|
|
1514
1635
|
{ pubkey: params.stakePool, isSigner: false, isWritable: true },
|
|
@@ -1550,7 +1671,7 @@ class StakePoolInstruction {
|
|
|
1550
1671
|
static withdrawStake(params) {
|
|
1551
1672
|
const { programId, stakePool, validatorList, withdrawAuthority, validatorStake, destinationStake, destinationStakeAuthority, sourceTransferAuthority, sourcePoolAccount, managerFeeAccount, poolMint, poolTokens, } = params;
|
|
1552
1673
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStake;
|
|
1553
|
-
const data = encodeData(type, { poolTokens });
|
|
1674
|
+
const data = encodeData(type, { poolTokens: toBN(poolTokens) });
|
|
1554
1675
|
const keys = [
|
|
1555
1676
|
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
|
1556
1677
|
{ pubkey: validatorList, isSigner: false, isWritable: true },
|
|
@@ -1578,7 +1699,7 @@ class StakePoolInstruction {
|
|
|
1578
1699
|
static withdrawSol(params) {
|
|
1579
1700
|
const { programId, stakePool, withdrawAuthority, sourceTransferAuthority, sourcePoolAccount, reserveStake, destinationSystemAccount, managerFeeAccount, solWithdrawAuthority, poolMint, poolTokens, } = params;
|
|
1580
1701
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawSol;
|
|
1581
|
-
const data = encodeData(type, { poolTokens });
|
|
1702
|
+
const data = encodeData(type, { poolTokens: toBN(poolTokens) });
|
|
1582
1703
|
const keys = [
|
|
1583
1704
|
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
|
1584
1705
|
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
|
@@ -1613,8 +1734,8 @@ class StakePoolInstruction {
|
|
|
1613
1734
|
static withdrawWsolWithSession(params) {
|
|
1614
1735
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawWsolWithSession;
|
|
1615
1736
|
const data = encodeData(type, {
|
|
1616
|
-
poolTokensIn: params.poolTokensIn,
|
|
1617
|
-
minimumLamportsOut: params.minimumLamportsOut,
|
|
1737
|
+
poolTokensIn: toBN(params.poolTokensIn),
|
|
1738
|
+
minimumLamportsOut: toBN(params.minimumLamportsOut),
|
|
1618
1739
|
});
|
|
1619
1740
|
const keys = [
|
|
1620
1741
|
{ pubkey: params.stakePool, isSigner: false, isWritable: true },
|
|
@@ -1651,14 +1772,14 @@ class StakePoolInstruction {
|
|
|
1651
1772
|
}
|
|
1652
1773
|
/**
|
|
1653
1774
|
* Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
|
|
1654
|
-
* The stake account is created as a PDA and rent is
|
|
1775
|
+
* The stake account is created as a PDA and rent is funded from the reserve.
|
|
1655
1776
|
*/
|
|
1656
1777
|
static withdrawStakeWithSession(params) {
|
|
1657
1778
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession;
|
|
1658
1779
|
const data = encodeData(type, {
|
|
1659
|
-
poolTokensIn: params.poolTokensIn,
|
|
1660
|
-
minimumLamportsOut: params.minimumLamportsOut,
|
|
1661
|
-
userStakeSeed: params.userStakeSeed,
|
|
1780
|
+
poolTokensIn: toBN(params.poolTokensIn),
|
|
1781
|
+
minimumLamportsOut: toBN(params.minimumLamportsOut),
|
|
1782
|
+
userStakeSeed: toBN(params.userStakeSeed),
|
|
1662
1783
|
});
|
|
1663
1784
|
const keys = [
|
|
1664
1785
|
{ pubkey: params.stakePool, isSigner: false, isWritable: true },
|
|
@@ -1666,17 +1787,19 @@ class StakePoolInstruction {
|
|
|
1666
1787
|
{ pubkey: params.withdrawAuthority, isSigner: false, isWritable: false },
|
|
1667
1788
|
{ pubkey: params.stakeToSplit, isSigner: false, isWritable: true },
|
|
1668
1789
|
{ pubkey: params.stakeToReceive, isSigner: false, isWritable: true },
|
|
1669
|
-
{ pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
|
|
1670
|
-
{ pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (
|
|
1790
|
+
{ pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
|
|
1791
|
+
{ pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (unused in session path)
|
|
1671
1792
|
{ pubkey: params.burnFromPool, isSigner: false, isWritable: true },
|
|
1672
1793
|
{ pubkey: params.managerFeeAccount, isSigner: false, isWritable: true },
|
|
1673
1794
|
{ pubkey: params.poolMint, isSigner: false, isWritable: true },
|
|
1674
1795
|
{ pubkey: web3_js.SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
|
1675
1796
|
{ pubkey: params.tokenProgramId, isSigner: false, isWritable: false },
|
|
1676
1797
|
{ pubkey: web3_js.StakeProgram.programId, isSigner: false, isWritable: false },
|
|
1798
|
+
// Session-specific accounts
|
|
1677
1799
|
{ pubkey: params.programSigner, isSigner: false, isWritable: false },
|
|
1678
1800
|
{ pubkey: web3_js.SystemProgram.programId, isSigner: false, isWritable: false },
|
|
1679
|
-
{ pubkey: params.
|
|
1801
|
+
{ pubkey: params.reserveStake, isSigner: false, isWritable: true },
|
|
1802
|
+
{ pubkey: web3_js.SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
|
|
1680
1803
|
];
|
|
1681
1804
|
return new web3_js.TransactionInstruction({
|
|
1682
1805
|
programId: params.programId,
|
|
@@ -1691,8 +1814,8 @@ class StakePoolInstruction {
|
|
|
1691
1814
|
static withdrawFromStakeAccountWithSession(params) {
|
|
1692
1815
|
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawFromStakeAccountWithSession;
|
|
1693
1816
|
const data = encodeData(type, {
|
|
1694
|
-
lamports: params.lamports,
|
|
1695
|
-
userStakeSeed: params.userStakeSeed,
|
|
1817
|
+
lamports: toBN(params.lamports),
|
|
1818
|
+
userStakeSeed: toBN(params.userStakeSeed),
|
|
1696
1819
|
});
|
|
1697
1820
|
const keys = [
|
|
1698
1821
|
{ pubkey: params.userStakeAccount, isSigner: false, isWritable: true },
|
|
@@ -1981,10 +2104,18 @@ skipBalanceCheck = false) {
|
|
|
1981
2104
|
if (!skipBalanceCheck) {
|
|
1982
2105
|
const tokenAccountInfo = await connection.getTokenAccountBalance(wsolTokenAccount, 'confirmed');
|
|
1983
2106
|
const wsolBalance = tokenAccountInfo
|
|
1984
|
-
?
|
|
1985
|
-
: 0;
|
|
1986
|
-
|
|
1987
|
-
|
|
2107
|
+
? BigInt(tokenAccountInfo.value.amount)
|
|
2108
|
+
: BigInt(0);
|
|
2109
|
+
// Convert lamports to BigInt for comparison
|
|
2110
|
+
const lamportsBigInt = typeof lamports === 'bigint'
|
|
2111
|
+
? lamports
|
|
2112
|
+
: typeof lamports === 'string'
|
|
2113
|
+
? BigInt(lamports)
|
|
2114
|
+
: BN.isBN(lamports)
|
|
2115
|
+
? BigInt(lamports.toString())
|
|
2116
|
+
: BigInt(lamports);
|
|
2117
|
+
if (wsolBalance < lamportsBigInt) {
|
|
2118
|
+
throw new Error(`Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(Number(wsolBalance))} WSOL.`);
|
|
1988
2119
|
}
|
|
1989
2120
|
}
|
|
1990
2121
|
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
|
@@ -2030,7 +2161,15 @@ skipBalanceCheck = false) {
|
|
|
2030
2161
|
*/
|
|
2031
2162
|
async function depositSol(connection, stakePoolAddress, from, lamports, destinationTokenAccount, referrerTokenAccount, depositAuthority) {
|
|
2032
2163
|
const fromBalance = await connection.getBalance(from, 'confirmed');
|
|
2033
|
-
|
|
2164
|
+
// Convert lamports to BigInt for comparison
|
|
2165
|
+
const lamportsBigInt = typeof lamports === 'bigint'
|
|
2166
|
+
? lamports
|
|
2167
|
+
: typeof lamports === 'string'
|
|
2168
|
+
? BigInt(lamports)
|
|
2169
|
+
: BN.isBN(lamports)
|
|
2170
|
+
? BigInt(lamports.toString())
|
|
2171
|
+
: BigInt(lamports);
|
|
2172
|
+
if (BigInt(fromBalance) < lamportsBigInt) {
|
|
2034
2173
|
throw new Error(`Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(fromBalance)} SOL.`);
|
|
2035
2174
|
}
|
|
2036
2175
|
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
|
@@ -2044,7 +2183,7 @@ async function depositSol(connection, stakePoolAddress, from, lamports, destinat
|
|
|
2044
2183
|
instructions.push(web3_js.SystemProgram.transfer({
|
|
2045
2184
|
fromPubkey: from,
|
|
2046
2185
|
toPubkey: userSolTransfer.publicKey,
|
|
2047
|
-
lamports,
|
|
2186
|
+
lamports: lamportsBigInt,
|
|
2048
2187
|
}));
|
|
2049
2188
|
// Create token account if not specified
|
|
2050
2189
|
if (!destinationTokenAccount) {
|
|
@@ -2416,37 +2555,51 @@ async function getUserStakeAccounts(connection, programId, userPubkey, maxSeed =
|
|
|
2416
2555
|
* Withdraws stake from a stake pool using a Fogo session.
|
|
2417
2556
|
*
|
|
2418
2557
|
* The on-chain program creates stake account PDAs. The rent for these accounts
|
|
2419
|
-
* is
|
|
2558
|
+
* is funded from the reserve stake.
|
|
2420
2559
|
*
|
|
2421
2560
|
* @param connection - Solana connection
|
|
2422
2561
|
* @param stakePoolAddress - The stake pool to withdraw from
|
|
2423
2562
|
* @param signerOrSession - The session signer public key
|
|
2424
2563
|
* @param userPubkey - User's wallet (used for PDA derivation and token ownership)
|
|
2425
|
-
* @param payer - Payer for stake account rent (typically paymaster)
|
|
2426
2564
|
* @param amount - Amount of pool tokens to withdraw
|
|
2427
2565
|
* @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
|
|
2428
2566
|
* @param useReserve - Whether to withdraw from reserve (default: false)
|
|
2429
2567
|
* @param voteAccountAddress - Optional specific validator to withdraw from
|
|
2430
2568
|
* @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
|
|
2431
2569
|
* @param validatorComparator - Optional comparator for validator selection
|
|
2570
|
+
* @param allowPartial - If true, returns partial results instead of throwing when not enough stake available
|
|
2432
2571
|
*/
|
|
2433
|
-
async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey,
|
|
2434
|
-
|
|
2572
|
+
async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator, allowPartial = false) {
|
|
2573
|
+
var _c;
|
|
2435
2574
|
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
|
|
2575
|
+
// First fetch: get stake pool to know other account addresses
|
|
2576
|
+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
|
2436
2577
|
const stakePool = stakePoolAccount.account.data;
|
|
2437
2578
|
const poolTokens = solToLamports(amount);
|
|
2438
2579
|
const poolAmount = new BN(poolTokens);
|
|
2439
2580
|
const poolTokenAccount = splToken.getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey);
|
|
2440
|
-
|
|
2581
|
+
// Second fetch: get ALL remaining data in parallel
|
|
2582
|
+
const [tokenAccount, stakeAccountRentExemption, validatorListAcc, stakeMinimumDelegationResponse] = await Promise.all([
|
|
2583
|
+
splToken.getAccount(connection, poolTokenAccount),
|
|
2584
|
+
connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space),
|
|
2585
|
+
connection.getAccountInfo(stakePool.validatorList),
|
|
2586
|
+
connection.getStakeMinimumDelegation(),
|
|
2587
|
+
]);
|
|
2588
|
+
// Pre-fetch data to avoid duplicate RPC calls in prepareWithdrawAccounts
|
|
2589
|
+
const prefetchedData = {
|
|
2590
|
+
validatorListData: (_c = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _c !== void 0 ? _c : null,
|
|
2591
|
+
minBalanceForRentExemption: stakeAccountRentExemption,
|
|
2592
|
+
stakeMinimumDelegation: Number(stakeMinimumDelegationResponse.value),
|
|
2593
|
+
};
|
|
2441
2594
|
if (tokenAccount.amount < poolTokens) {
|
|
2442
2595
|
throw new Error(`Not enough token balance to withdraw ${amount} pool tokens.
|
|
2443
2596
|
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`);
|
|
2444
2597
|
}
|
|
2445
2598
|
const [programSigner] = web3_js.PublicKey.findProgramAddressSync([Buffer.from('fogo_session_program_signer')], stakePoolProgramId);
|
|
2446
2599
|
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(stakePoolProgramId, stakePoolAddress);
|
|
2447
|
-
const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space);
|
|
2448
2600
|
// Determine which stake accounts to withdraw from
|
|
2449
2601
|
const withdrawAccounts = [];
|
|
2602
|
+
let partialRemainingAmount;
|
|
2450
2603
|
if (useReserve) {
|
|
2451
2604
|
withdrawAccounts.push({
|
|
2452
2605
|
stakeAddress: stakePool.reserveStake,
|
|
@@ -2477,7 +2630,14 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2477
2630
|
}
|
|
2478
2631
|
else {
|
|
2479
2632
|
// Get the list of accounts to withdraw from automatically
|
|
2480
|
-
|
|
2633
|
+
if (allowPartial) {
|
|
2634
|
+
const result = await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), true, prefetchedData);
|
|
2635
|
+
withdrawAccounts.push(...result.withdrawAccounts);
|
|
2636
|
+
partialRemainingAmount = result.remainingAmount;
|
|
2637
|
+
}
|
|
2638
|
+
else {
|
|
2639
|
+
withdrawAccounts.push(...(await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), undefined, prefetchedData)));
|
|
2640
|
+
}
|
|
2481
2641
|
}
|
|
2482
2642
|
const instructions = [];
|
|
2483
2643
|
const stakeAccountPubkeys = [];
|
|
@@ -2494,7 +2654,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2494
2654
|
const stakeReceiverPubkey = findUserStakeProgramAddress(stakePoolProgramId, userPubkey, userStakeSeed);
|
|
2495
2655
|
stakeAccountPubkeys.push(stakeReceiverPubkey);
|
|
2496
2656
|
userStakeSeeds.push(userStakeSeed);
|
|
2497
|
-
// The on-chain program creates the stake account PDA and rent is
|
|
2657
|
+
// The on-chain program creates the stake account PDA and rent is funded from reserve.
|
|
2498
2658
|
instructions.push(StakePoolInstruction.withdrawStakeWithSession({
|
|
2499
2659
|
programId: stakePoolProgramId,
|
|
2500
2660
|
stakePool: stakePoolAddress,
|
|
@@ -2508,7 +2668,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2508
2668
|
poolMint: stakePool.poolMint,
|
|
2509
2669
|
tokenProgramId: stakePool.tokenProgramId,
|
|
2510
2670
|
programSigner,
|
|
2511
|
-
|
|
2671
|
+
reserveStake: stakePool.reserveStake,
|
|
2512
2672
|
poolTokensIn: withdrawAccount.poolAmount.toNumber(),
|
|
2513
2673
|
minimumLamportsOut,
|
|
2514
2674
|
userStakeSeed,
|
|
@@ -2519,6 +2679,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
|
|
|
2519
2679
|
instructions,
|
|
2520
2680
|
stakeAccountPubkeys,
|
|
2521
2681
|
userStakeSeeds,
|
|
2682
|
+
remainingPoolTokens: partialRemainingAmount ? lamportsToSol(partialRemainingAmount) : 0,
|
|
2522
2683
|
};
|
|
2523
2684
|
}
|
|
2524
2685
|
async function addValidatorToPool(connection, stakePoolAddress, validatorVote, seed) {
|