[stake-pool] js: web3 stake pool bindings (#2604)
* - add stake-pool-web3-bindings (preview) * - update readme * - add tests * - remove package-lock from foreign project * - revert changes from foreign project * - review improvenments * - refactor regarding review suggestions * - fix test - fix readme * Update stake-pool/js/src/instructions.ts Co-authored-by: Jon Cinque <jon.cinque@gmail.com> * - add withdraw authority as optional parameter Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
This commit is contained in:
parent
1f4b65153f
commit
cd8d79a2b4
|
@ -1 +1,3 @@
|
|||
dist
|
||||
.idea
|
||||
node_modules
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing":false,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
arrowParens: "avoid"
|
||||
bracketSpacing: false
|
||||
jsxBracketSameLine: false
|
||||
semi: true
|
||||
singleQuote: true
|
||||
tabWidth: 2
|
||||
trailingComma: "all"
|
|
@ -29,19 +29,5 @@ Sample output:
|
|||
|
||||
```
|
||||
> stake-pool-js@0.0.1 test
|
||||
> ./node_modules/mocha/bin/mocha -p ./dist
|
||||
```
|
||||
|
||||
|
||||
schema.decode
|
||||
StakePoolAccount
|
||||
✓ should successfully decode StakePoolAccount account data
|
||||
ValidatorListAccount
|
||||
✓ should successfully decode ValidatorListAccount account data
|
||||
✓ should successfully decode ValidatorListAccount with nonempty ValidatorInfo
|
||||
|
||||
index.ts/PrettyPrintPubkey
|
||||
✓ should successfully pretty print a pubkey
|
||||
|
||||
|
||||
4 passing (610ms)
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
// it's needed for jest - https://jestjs.io/docs/getting-started#using-typescript
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -1,27 +1,37 @@
|
|||
{
|
||||
"name": "@solana/spl-stake-pool",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"description": "SPL Stake Pool Program JS API",
|
||||
"scripts": {
|
||||
"lint": "npx prettier src/*.ts -w",
|
||||
"build": "tsc",
|
||||
"test": "./node_modules/mocha/bin/mocha -p ./dist"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Lieu Zheng Hong",
|
||||
"authors": [
|
||||
"Lieu Zheng Hong",
|
||||
"mFactory Team (https://mfactory.ch)"
|
||||
],
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.2.2",
|
||||
"mocha": "^8.4.0",
|
||||
"prettier": "^2.2.1",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/web3.js": "^1.18.0",
|
||||
"assert": "^2.0.0",
|
||||
"borsh": "^0.4.0",
|
||||
"buffer": "^6.0.1",
|
||||
"@project-serum/borsh": "^0.2.2",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"@solana/web3.js": "^1.30.2",
|
||||
"bn.js": "^5.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"process": "^0.11.10"
|
||||
},
|
||||
"type": "module"
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.16.5",
|
||||
"@babel/preset-typescript": "^7.16.5",
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/jest": "^27.0.3",
|
||||
"jest": "^27.3.1",
|
||||
"prettier": "^2.2.1"
|
||||
},
|
||||
"jest": {
|
||||
"testRegex": ".*\\.test\\.ts$",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { Buffer } from "buffer";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { solToLamports } from "./utils";
|
||||
|
||||
export const TRANSIENT_STAKE_SEED_PREFIX = Buffer.from('transient');
|
||||
|
||||
export const STAKE_POOL_PROGRAM_ID = new PublicKey('SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy');
|
||||
|
||||
export const MIN_STAKE_BALANCE = solToLamports(0.001);
|
|
@ -0,0 +1,3 @@
|
|||
that files are copied from https://github.com/solana-labs/solana-web3.js
|
||||
|
||||
I think it would be a good idea to export that functionality
|
|
@ -0,0 +1,48 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import * as BufferLayout from '@solana/buffer-layout';
|
||||
|
||||
import * as Layout from './layout';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type InstructionType = {
|
||||
/** The Instruction index (from solana upstream program) */
|
||||
index: number;
|
||||
/** The BufferLayout to use to build data */
|
||||
layout: BufferLayout.Layout;
|
||||
};
|
||||
|
||||
/**
|
||||
* Populate a buffer of instruction data using an InstructionType
|
||||
* @internal
|
||||
*/
|
||||
export function encodeData(type: InstructionType, fields?: any): Buffer {
|
||||
const allocLength = type.layout.span >= 0 ? type.layout.span : Layout.getAlloc(type, fields);
|
||||
const data = Buffer.alloc(allocLength);
|
||||
const layoutFields = Object.assign({ instruction: type.index }, fields);
|
||||
type.layout.encode(layoutFields, data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode instruction data buffer using an InstructionType
|
||||
* @internal
|
||||
*/
|
||||
export function decodeData(type: InstructionType, buffer: Buffer): any {
|
||||
let data;
|
||||
try {
|
||||
data = type.layout.decode(buffer);
|
||||
} catch (err) {
|
||||
throw new Error('invalid instruction; ' + err);
|
||||
}
|
||||
|
||||
if (data.instruction !== type.index) {
|
||||
throw new Error(
|
||||
`invalid instruction; instruction index mismatch ${data.instruction} != ${type.index}`,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import {Buffer} from 'buffer';
|
||||
import * as BufferLayout from '@solana/buffer-layout';
|
||||
|
||||
/**
|
||||
* Layout for a public key
|
||||
*/
|
||||
export const publicKey = (
|
||||
property: string = 'publicKey',
|
||||
): BufferLayout.Layout => {
|
||||
return BufferLayout.blob(32, property);
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout for a 64bit unsigned value
|
||||
*/
|
||||
export const uint64 = (property: string = 'uint64'): BufferLayout.Layout => {
|
||||
return BufferLayout.blob(8, property);
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout for a Rust String type
|
||||
*/
|
||||
export const rustString = (property: string = 'string') => {
|
||||
const rsl = BufferLayout.struct(
|
||||
[
|
||||
BufferLayout.u32('length'),
|
||||
BufferLayout.u32('lengthPadding'),
|
||||
BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'),
|
||||
],
|
||||
property,
|
||||
);
|
||||
const _decode = rsl.decode.bind(rsl);
|
||||
const _encode = rsl.encode.bind(rsl);
|
||||
|
||||
rsl.decode = (buffer: any, offset: any) => {
|
||||
const data = _decode(buffer, offset);
|
||||
return data['chars'].toString('utf8');
|
||||
};
|
||||
|
||||
rsl.encode = (str: any, buffer: any, offset: any) => {
|
||||
const data = {
|
||||
chars: Buffer.from(str, 'utf8'),
|
||||
};
|
||||
return _encode(data, buffer, offset);
|
||||
};
|
||||
|
||||
(rsl as any).alloc = (str: any) => {
|
||||
return (
|
||||
BufferLayout.u32().span +
|
||||
BufferLayout.u32().span +
|
||||
Buffer.from(str, 'utf8').length
|
||||
);
|
||||
};
|
||||
|
||||
return rsl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout for an Authorized object
|
||||
*/
|
||||
export const authorized = (property: string = 'authorized') => {
|
||||
return BufferLayout.struct(
|
||||
[publicKey('staker'), publicKey('withdrawer')],
|
||||
property,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout for a Lockup object
|
||||
*/
|
||||
export const lockup = (property: string = 'lockup') => {
|
||||
return BufferLayout.struct(
|
||||
[
|
||||
BufferLayout.ns64('unixTimestamp'),
|
||||
BufferLayout.ns64('epoch'),
|
||||
publicKey('custodian'),
|
||||
],
|
||||
property,
|
||||
);
|
||||
};
|
||||
|
||||
export function getAlloc(type: any, fields: any): number {
|
||||
let alloc = 0;
|
||||
type.layout.fields.forEach((item: any) => {
|
||||
if (item.span >= 0) {
|
||||
alloc += item.span;
|
||||
} else if (typeof item.alloc === 'function') {
|
||||
alloc += item.alloc(fields[item.property]);
|
||||
}
|
||||
});
|
||||
return alloc;
|
||||
}
|
|
@ -1,24 +1,59 @@
|
|||
import * as schema from './schema.js';
|
||||
import solanaWeb3 from '@solana/web3.js';
|
||||
import assert from 'assert';
|
||||
import {
|
||||
AccountInfo,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
Signer, StakeProgram,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
} from '@solana/spl-token';
|
||||
import {
|
||||
addAssociatedTokenAccount,
|
||||
calcLamportsWithdrawAmount,
|
||||
findStakeProgramAddress,
|
||||
findWithdrawAuthorityProgramAddress,
|
||||
getTokenAccount,
|
||||
newStakeAccount,
|
||||
prepareWithdrawAccounts,
|
||||
lamportsToSol,
|
||||
solToLamports,
|
||||
} from './utils';
|
||||
import { StakePoolInstruction } from './instructions';
|
||||
import { StakePoolLayout, ValidatorListLayout, ValidatorList, StakePool } from './layouts';
|
||||
import { MIN_STAKE_BALANCE, STAKE_POOL_PROGRAM_ID } from "./constants";
|
||||
|
||||
export class StakePoolAccounts {
|
||||
/**
|
||||
* Wrapper class for a stake pool.
|
||||
* Each stake pool has a stake pool account and a validator list account.
|
||||
*/
|
||||
stakePool: StakePoolAccount;
|
||||
validatorList: ValidatorListAccount;
|
||||
export type { StakePool, AccountType, ValidatorList, ValidatorStakeInfo } from './layouts';
|
||||
export { STAKE_POOL_PROGRAM_ID } from './constants';
|
||||
export * from './instructions';
|
||||
|
||||
export interface ValidatorListAccount {
|
||||
pubkey: PublicKey;
|
||||
account: AccountInfo<ValidatorList>;
|
||||
}
|
||||
|
||||
export interface StakePoolAccount {
|
||||
pubkey: solanaWeb3.PublicKey;
|
||||
account: solanaWeb3.AccountInfo<schema.StakePool>;
|
||||
pubkey: PublicKey;
|
||||
account: AccountInfo<StakePool>;
|
||||
}
|
||||
|
||||
export interface ValidatorListAccount {
|
||||
pubkey: solanaWeb3.PublicKey;
|
||||
account: solanaWeb3.AccountInfo<schema.ValidatorList>;
|
||||
export interface WithdrawAccount {
|
||||
stakeAddress: PublicKey;
|
||||
voteAddress?: PublicKey;
|
||||
poolAmount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for a stake pool.
|
||||
* Each stake pool has a stake pool account and a validator list account.
|
||||
*/
|
||||
export interface StakePoolAccounts {
|
||||
stakePool: StakePoolAccount | undefined;
|
||||
validatorList: ValidatorListAccount | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,15 +62,19 @@ export interface ValidatorListAccount {
|
|||
* @param stakePoolPubKey: The public key (address) of the stake pool account.
|
||||
*/
|
||||
export async function getStakePoolAccount(
|
||||
connection: solanaWeb3.Connection,
|
||||
stakePoolPubKey: solanaWeb3.PublicKey,
|
||||
connection: Connection,
|
||||
stakePoolPubKey: PublicKey,
|
||||
): Promise<StakePoolAccount> {
|
||||
const account = await connection.getAccountInfo(stakePoolPubKey);
|
||||
|
||||
if (!account) {
|
||||
throw new Error('Invalid account');
|
||||
}
|
||||
|
||||
return {
|
||||
pubkey: stakePoolPubKey,
|
||||
account: {
|
||||
data: schema.StakePool.decode(account.data),
|
||||
data: StakePoolLayout.decode(account.data),
|
||||
executable: account.executable,
|
||||
lamports: account.lamports,
|
||||
owner: account.owner,
|
||||
|
@ -43,114 +82,393 @@ export async function getStakePoolAccount(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and deserializes a ValidatorList account using a web3js connection and the validator list address.
|
||||
* @param connection: An active web3js connection.
|
||||
* @param validatorListPubKey: The public key (address) of the validator list account.
|
||||
*/
|
||||
export async function getValidatorListAccount(
|
||||
connection: solanaWeb3.Connection,
|
||||
validatorListPubKey: solanaWeb3.PublicKey,
|
||||
): Promise<ValidatorListAccount> {
|
||||
try {
|
||||
const account = await connection.getAccountInfo(validatorListPubKey);
|
||||
|
||||
return {
|
||||
pubkey: validatorListPubKey,
|
||||
account: {
|
||||
data: schema.ValidatorList.decodeUnchecked(account.data),
|
||||
executable: account.executable,
|
||||
lamports: account.lamports,
|
||||
owner: account.owner,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all StakePool and ValidatorList accounts that are running a particular StakePool program.
|
||||
* @param connection: An active web3js connection.
|
||||
* @param stakePoolProgramAddress: The public key (address) of the StakePool program.
|
||||
*/
|
||||
export async function getStakePoolAccounts(
|
||||
connection: solanaWeb3.Connection,
|
||||
stakePoolProgramAddress: solanaWeb3.PublicKey,
|
||||
): Promise<(StakePoolAccount | ValidatorListAccount)[]> {
|
||||
try {
|
||||
let response = await connection.getProgramAccounts(stakePoolProgramAddress);
|
||||
connection: Connection,
|
||||
stakePoolProgramAddress: PublicKey,
|
||||
): Promise<(StakePoolAccount | ValidatorListAccount)[] | undefined> {
|
||||
const response = await connection.getProgramAccounts(stakePoolProgramAddress);
|
||||
|
||||
const stakePoolAccounts = response.map(a => {
|
||||
let decodedData;
|
||||
return response.map(a => {
|
||||
let decodedData;
|
||||
|
||||
if (a.account.data.readUInt8() === 1) {
|
||||
try {
|
||||
decodedData = schema.StakePool.decode(a.account.data);
|
||||
} catch (error) {
|
||||
console.log('Could not decode StakeAccount. Error:', error);
|
||||
decodedData = undefined;
|
||||
}
|
||||
} else if (a.account.data.readUInt8() === 2) {
|
||||
try {
|
||||
decodedData = schema.ValidatorList.decodeUnchecked(a.account.data);
|
||||
} catch (error) {
|
||||
console.log('Could not decode ValidatorList. Error:', error);
|
||||
decodedData = undefined;
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`Could not decode. StakePoolAccount Enum is ${a.account.data.readUInt8()}, expected 1 or 2!`,
|
||||
);
|
||||
if (a.account.data.readUInt8() === 1) {
|
||||
try {
|
||||
decodedData = StakePoolLayout.decode(a.account.data);
|
||||
} catch (error) {
|
||||
console.log('Could not decode StakeAccount. Error:', error);
|
||||
decodedData = undefined;
|
||||
}
|
||||
} else if (a.account.data.readUInt8() === 2) {
|
||||
try {
|
||||
decodedData = ValidatorListLayout.decode(a.account.data);
|
||||
} catch (error) {
|
||||
console.log('Could not decode ValidatorList. Error:', error);
|
||||
decodedData = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
pubkey: a.pubkey,
|
||||
account: {
|
||||
data: decodedData,
|
||||
executable: a.account.executable,
|
||||
lamports: a.account.lamports,
|
||||
owner: a.account.owner,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return stakePoolAccounts;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to pretty print a schema.PublicKey
|
||||
* Pretty prints a PublicKey in base58 format */
|
||||
export function prettyPrintPubKey(pubKey: solanaWeb3.PublicKey): string {
|
||||
return new solanaWeb3.PublicKey(
|
||||
new solanaWeb3.PublicKey(pubKey.toBuffer()).toBytes().reverse(),
|
||||
).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to pretty print a decoded account
|
||||
*/
|
||||
export function prettyPrintAccount(
|
||||
account: ValidatorListAccount | StakePoolAccount,
|
||||
): void {
|
||||
console.log('Address:', account.pubkey.toString());
|
||||
const sp = account.account.data;
|
||||
if (typeof sp === 'undefined') {
|
||||
console.log('Account could not be decoded');
|
||||
}
|
||||
|
||||
for (const val in sp) {
|
||||
if (sp[val] instanceof solanaWeb3.PublicKey) {
|
||||
console.log(val, prettyPrintPubKey(sp[val]));
|
||||
} else {
|
||||
console.log(val, sp[val]);
|
||||
console.error(
|
||||
`Could not decode. StakePoolAccount Enum is ${a.account.data.readUInt8()}, expected 1 or 2!`,
|
||||
);
|
||||
decodedData = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
pubkey: a.pubkey,
|
||||
account: {
|
||||
data: decodedData,
|
||||
executable: a.account.executable,
|
||||
lamports: a.account.lamports,
|
||||
owner: a.account.owner,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instructions required to deposit sol to stake pool.
|
||||
*/
|
||||
export async function depositSol(
|
||||
connection: Connection,
|
||||
stakePoolAddress: PublicKey,
|
||||
from: PublicKey,
|
||||
lamports: number,
|
||||
destinationTokenAccount?: PublicKey,
|
||||
referrerTokenAccount?: PublicKey,
|
||||
depositAuthority?: PublicKey
|
||||
) {
|
||||
const fromBalance = await connection.getBalance(from, 'confirmed');
|
||||
if (fromBalance < lamports) {
|
||||
throw new Error(
|
||||
`Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
|
||||
fromBalance,
|
||||
)} SOL.`,
|
||||
);
|
||||
}
|
||||
|
||||
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
|
||||
const stakePool = stakePoolAccount.account.data;
|
||||
|
||||
// Ephemeral SOL account just to do the transfer
|
||||
const userSolTransfer = new Keypair();
|
||||
const signers: Signer[] = [userSolTransfer];
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
|
||||
// Create the ephemeral SOL account
|
||||
instructions.push(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: from,
|
||||
toPubkey: userSolTransfer.publicKey,
|
||||
lamports,
|
||||
}),
|
||||
);
|
||||
|
||||
// Create token account if not specified
|
||||
if (!destinationTokenAccount) {
|
||||
destinationTokenAccount = await addAssociatedTokenAccount(
|
||||
connection,
|
||||
from,
|
||||
stakePool.poolMint,
|
||||
instructions,
|
||||
);
|
||||
}
|
||||
|
||||
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
||||
STAKE_POOL_PROGRAM_ID,
|
||||
stakePoolAddress,
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
StakePoolInstruction.depositSol({
|
||||
stakePool: stakePoolAddress,
|
||||
reserveStake: stakePool.reserveStake,
|
||||
fundingAccount: userSolTransfer.publicKey,
|
||||
destinationPoolAccount: destinationTokenAccount,
|
||||
managerFeeAccount: stakePool.managerFeeAccount,
|
||||
referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
|
||||
poolMint: stakePool.poolMint,
|
||||
lamports: lamports,
|
||||
withdrawAuthority: withdrawAuthority,
|
||||
depositAuthority: depositAuthority,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
instructions,
|
||||
signers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instructions required to withdraw stake from a stake pool.
|
||||
*/
|
||||
export async function withdrawStake(
|
||||
connection: Connection,
|
||||
stakePoolAddress: PublicKey,
|
||||
tokenOwner: PublicKey,
|
||||
amount: number,
|
||||
useReserve = false,
|
||||
voteAccountAddress?: PublicKey,
|
||||
stakeReceiver?: PublicKey,
|
||||
poolTokenAccount?: PublicKey,
|
||||
) {
|
||||
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
|
||||
const poolAmount = solToLamports(amount);
|
||||
|
||||
if (!poolTokenAccount) {
|
||||
poolTokenAccount = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
stakePool.account.data.poolMint,
|
||||
tokenOwner,
|
||||
);
|
||||
}
|
||||
|
||||
const tokenAccount = await getTokenAccount(
|
||||
connection,
|
||||
poolTokenAccount,
|
||||
stakePool.account.data.poolMint,
|
||||
);
|
||||
if (!tokenAccount) {
|
||||
throw new Error('Invalid token account');
|
||||
}
|
||||
|
||||
// Check withdrawFrom balance
|
||||
if (tokenAccount.amount.toNumber() < poolAmount) {
|
||||
throw new Error(
|
||||
`Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
|
||||
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount.toNumber())} pool tokens.`,
|
||||
);
|
||||
}
|
||||
|
||||
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
||||
STAKE_POOL_PROGRAM_ID,
|
||||
stakePoolAddress,
|
||||
);
|
||||
|
||||
const withdrawAccounts: WithdrawAccount[] = [];
|
||||
|
||||
if (useReserve) {
|
||||
withdrawAccounts.push({
|
||||
stakeAddress: stakePool.account.data.reserveStake,
|
||||
voteAddress: undefined,
|
||||
poolAmount,
|
||||
});
|
||||
} else if (voteAccountAddress) {
|
||||
const stakeAccountAddress = await findStakeProgramAddress(
|
||||
STAKE_POOL_PROGRAM_ID,
|
||||
voteAccountAddress,
|
||||
stakePoolAddress,
|
||||
);
|
||||
const stakeAccount = await connection.getAccountInfo(stakeAccountAddress);
|
||||
if (!stakeAccount) {
|
||||
throw new Error('Invalid Stake Account');
|
||||
}
|
||||
|
||||
const availableForWithdrawal = calcLamportsWithdrawAmount(
|
||||
stakePool.account.data,
|
||||
stakeAccount.lamports - MIN_STAKE_BALANCE,
|
||||
);
|
||||
|
||||
if (availableForWithdrawal < poolAmount) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(
|
||||
`Not enough lamports available for withdrawal from ${stakeAccountAddress},
|
||||
${poolAmount} asked, ${availableForWithdrawal} available.`,
|
||||
);
|
||||
}
|
||||
withdrawAccounts.push({
|
||||
stakeAddress: stakeAccountAddress,
|
||||
voteAddress: voteAccountAddress,
|
||||
poolAmount,
|
||||
});
|
||||
} else {
|
||||
// Get the list of accounts to withdraw from
|
||||
withdrawAccounts.push(
|
||||
...(await prepareWithdrawAccounts(
|
||||
connection,
|
||||
stakePool.account.data,
|
||||
stakePoolAddress,
|
||||
poolAmount,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
// Construct transaction to withdraw from withdrawAccounts account list
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
const userTransferAuthority = Keypair.generate();
|
||||
|
||||
const signers: Signer[] = [userTransferAuthority];
|
||||
|
||||
instructions.push(
|
||||
Token.createApproveInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
poolTokenAccount,
|
||||
userTransferAuthority.publicKey,
|
||||
tokenOwner,
|
||||
[],
|
||||
poolAmount,
|
||||
),
|
||||
);
|
||||
|
||||
let totalRentFreeBalances = 0;
|
||||
|
||||
// Max 5 accounts to prevent an error: "Transaction too large"
|
||||
const maxWithdrawAccounts = 5;
|
||||
let i = 0;
|
||||
|
||||
// Go through prepared accounts and withdraw/claim them
|
||||
for (const withdrawAccount of withdrawAccounts) {
|
||||
if (i > maxWithdrawAccounts) {
|
||||
break;
|
||||
}
|
||||
// Convert pool tokens amount to lamports
|
||||
const solWithdrawAmount = Math.ceil(
|
||||
calcLamportsWithdrawAmount(stakePool.account.data, withdrawAccount.poolAmount),
|
||||
);
|
||||
|
||||
let infoMsg = `Withdrawing ◎${solWithdrawAmount},
|
||||
from stake account ${withdrawAccount.stakeAddress?.toBase58()}`;
|
||||
|
||||
if (withdrawAccount.voteAddress) {
|
||||
infoMsg = `${infoMsg}, delegated to ${withdrawAccount.voteAddress?.toBase58()}`;
|
||||
}
|
||||
|
||||
console.info(infoMsg);
|
||||
|
||||
let stakeToReceive;
|
||||
|
||||
// Use separate mutable variable because withdraw might create a new account
|
||||
if (!stakeReceiver) {
|
||||
const stakeReceiverAccountBalance = await connection.getMinimumBalanceForRentExemption(
|
||||
StakeProgram.space,
|
||||
);
|
||||
const stakeKeypair = newStakeAccount(tokenOwner, instructions, stakeReceiverAccountBalance);
|
||||
signers.push(stakeKeypair);
|
||||
totalRentFreeBalances += stakeReceiverAccountBalance;
|
||||
stakeToReceive = stakeKeypair.publicKey;
|
||||
} else {
|
||||
stakeToReceive = stakeReceiver;
|
||||
}
|
||||
|
||||
instructions.push(
|
||||
StakePoolInstruction.withdrawStake({
|
||||
stakePool: stakePoolAddress,
|
||||
validatorList: stakePool.account.data.validatorList,
|
||||
validatorStake: withdrawAccount.stakeAddress,
|
||||
destinationStake: stakeToReceive,
|
||||
destinationStakeAuthority: tokenOwner,
|
||||
sourceTransferAuthority: userTransferAuthority.publicKey,
|
||||
sourcePoolAccount: poolTokenAccount,
|
||||
managerFeeAccount: stakePool.account.data.managerFeeAccount,
|
||||
poolMint: stakePool.account.data.poolMint,
|
||||
poolTokens: withdrawAccount.poolAmount,
|
||||
withdrawAuthority,
|
||||
})
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return {
|
||||
instructions,
|
||||
signers,
|
||||
stakeReceiver,
|
||||
totalRentFreeBalances,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instructions required to withdraw SOL directly from a stake pool.
|
||||
*/
|
||||
export async function withdrawSol(
|
||||
connection: Connection,
|
||||
stakePoolAddress: PublicKey,
|
||||
tokenOwner: PublicKey,
|
||||
solReceiver: PublicKey,
|
||||
amount: number,
|
||||
solWithdrawAuthority?: PublicKey,
|
||||
) {
|
||||
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
|
||||
const poolAmount = solToLamports(amount);
|
||||
|
||||
const poolTokenAccount = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
stakePool.account.data.poolMint,
|
||||
tokenOwner,
|
||||
);
|
||||
|
||||
const tokenAccount = await getTokenAccount(
|
||||
connection,
|
||||
poolTokenAccount,
|
||||
stakePool.account.data.poolMint,
|
||||
);
|
||||
if (!tokenAccount) {
|
||||
throw new Error('Invalid token account');
|
||||
}
|
||||
|
||||
// Check withdrawFrom balance
|
||||
if (tokenAccount.amount.toNumber() < poolAmount) {
|
||||
throw new Error(
|
||||
`Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
|
||||
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount.toNumber())} pool tokens.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Construct transaction to withdraw from withdrawAccounts account list
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
const userTransferAuthority = Keypair.generate();
|
||||
const signers: Signer[] = [userTransferAuthority];
|
||||
|
||||
instructions.push(
|
||||
Token.createApproveInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
poolTokenAccount,
|
||||
userTransferAuthority.publicKey,
|
||||
tokenOwner,
|
||||
[],
|
||||
poolAmount,
|
||||
),
|
||||
);
|
||||
|
||||
const poolWithdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
||||
STAKE_POOL_PROGRAM_ID,
|
||||
stakePoolAddress,
|
||||
);
|
||||
|
||||
if (solWithdrawAuthority) {
|
||||
const expectedSolWithdrawAuthority = stakePool.account.data.solWithdrawAuthority;
|
||||
if (!expectedSolWithdrawAuthority) {
|
||||
throw new Error('SOL withdraw authority specified in arguments but stake pool has none');
|
||||
}
|
||||
if (solWithdrawAuthority.toBase58() != expectedSolWithdrawAuthority.toBase58()) {
|
||||
throw new Error(
|
||||
`Invalid deposit withdraw specified, expected ${expectedSolWithdrawAuthority.toBase58()}, received ${solWithdrawAuthority.toBase58()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log('Executable?:', account.account.executable);
|
||||
console.log('Lamports:', account.account.lamports);
|
||||
console.log('Owner PubKey:', account.account.owner.toString());
|
||||
|
||||
const withdrawTransaction = StakePoolInstruction.withdrawSol({
|
||||
stakePool: stakePoolAddress,
|
||||
withdrawAuthority: poolWithdrawAuthority,
|
||||
reserveStake: stakePool.account.data.reserveStake,
|
||||
sourcePoolAccount: poolTokenAccount,
|
||||
sourceTransferAuthority: userTransferAuthority.publicKey,
|
||||
destinationSystemAccount: solReceiver,
|
||||
managerFeeAccount: stakePool.account.data.managerFeeAccount,
|
||||
poolMint: stakePool.account.data.poolMint,
|
||||
poolTokens: poolAmount,
|
||||
solWithdrawAuthority,
|
||||
});
|
||||
|
||||
instructions.push(withdrawTransaction);
|
||||
|
||||
return {
|
||||
instructions,
|
||||
signers,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,381 @@
|
|||
/**
|
||||
* Based on https://github.com/solana-labs/solana-web3.js/blob/master/src/stake-program.ts
|
||||
*/
|
||||
import { encodeData, decodeData, InstructionType } from './copied-from-solana-web3/instruction';
|
||||
import {
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
StakeProgram,
|
||||
SystemProgram,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
SYSVAR_STAKE_HISTORY_PUBKEY,
|
||||
} from '@solana/web3.js';
|
||||
import * as BufferLayout from '@solana/buffer-layout';
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
import { STAKE_POOL_PROGRAM_ID } from "./constants";
|
||||
|
||||
/**
|
||||
* An enumeration of valid StakePoolInstructionType's
|
||||
*/
|
||||
export type StakePoolInstructionType =
|
||||
| 'DepositStake'
|
||||
| 'DepositSol'
|
||||
| 'WithdrawStake'
|
||||
| 'WithdrawSol';
|
||||
|
||||
/**
|
||||
* An enumeration of valid stake InstructionType's
|
||||
* @internal
|
||||
*/
|
||||
export const STAKE_POOL_INSTRUCTION_LAYOUTS: {
|
||||
[type in StakePoolInstructionType]: InstructionType;
|
||||
} = Object.freeze({
|
||||
DepositStake: {
|
||||
index: 9,
|
||||
layout: BufferLayout.struct([BufferLayout.u8('instruction')]),
|
||||
},
|
||||
/// Withdraw the token from the pool at the current ratio.
|
||||
WithdrawStake: {
|
||||
index: 10,
|
||||
layout: BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.ns64('poolTokens')]),
|
||||
},
|
||||
/// Deposit SOL directly into the pool's reserve account. The output is a "pool" token
|
||||
/// representing ownership into the pool. Inputs are converted to the current ratio.
|
||||
DepositSol: {
|
||||
index: 14,
|
||||
layout: BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.ns64('lamports')]),
|
||||
},
|
||||
/// Withdraw SOL directly from the pool's reserve account. Fails if the
|
||||
/// reserve does not have enough SOL.
|
||||
WithdrawSol: {
|
||||
index: 16,
|
||||
layout: BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.ns64('poolTokens')]),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deposit stake pool instruction params
|
||||
*/
|
||||
export type DepositStakeParams = {
|
||||
stakePool: PublicKey;
|
||||
validatorList: PublicKey;
|
||||
depositAuthority: PublicKey;
|
||||
withdrawAuthority: PublicKey;
|
||||
depositStake: PublicKey;
|
||||
validatorStake: PublicKey;
|
||||
reserveStake: PublicKey;
|
||||
destinationPoolAccount: PublicKey;
|
||||
managerFeeAccount: PublicKey;
|
||||
referralPoolAccount: PublicKey;
|
||||
poolMint: PublicKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* Withdraw stake pool instruction params
|
||||
*/
|
||||
export type WithdrawStakeParams = {
|
||||
stakePool: PublicKey;
|
||||
validatorList: PublicKey;
|
||||
withdrawAuthority: PublicKey;
|
||||
validatorStake: PublicKey;
|
||||
destinationStake: PublicKey;
|
||||
destinationStakeAuthority: PublicKey;
|
||||
sourceTransferAuthority: PublicKey;
|
||||
sourcePoolAccount: PublicKey;
|
||||
managerFeeAccount: PublicKey;
|
||||
poolMint: PublicKey;
|
||||
poolTokens: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Withdraw sol instruction params
|
||||
*/
|
||||
export type WithdrawSolParams = {
|
||||
stakePool: PublicKey;
|
||||
sourcePoolAccount: PublicKey;
|
||||
withdrawAuthority: PublicKey;
|
||||
reserveStake: PublicKey;
|
||||
destinationSystemAccount: PublicKey;
|
||||
sourceTransferAuthority: PublicKey;
|
||||
solWithdrawAuthority?: PublicKey | undefined;
|
||||
managerFeeAccount: PublicKey;
|
||||
poolMint: PublicKey;
|
||||
poolTokens: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deposit sol instruction params
|
||||
*/
|
||||
export type DepositSolParams = {
|
||||
stakePool: PublicKey;
|
||||
depositAuthority?: PublicKey | undefined;
|
||||
withdrawAuthority: PublicKey;
|
||||
reserveStake: PublicKey;
|
||||
fundingAccount: PublicKey;
|
||||
destinationPoolAccount: PublicKey;
|
||||
managerFeeAccount: PublicKey;
|
||||
referralPoolAccount: PublicKey;
|
||||
poolMint: PublicKey;
|
||||
lamports: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stake Pool Instruction class
|
||||
*/
|
||||
export class StakePoolInstruction {
|
||||
/**
|
||||
* Creates a transaction instruction to deposit SOL into a stake pool.
|
||||
*/
|
||||
static depositStake(params: DepositStakeParams): TransactionInstruction {
|
||||
const {
|
||||
stakePool,
|
||||
validatorList,
|
||||
depositAuthority,
|
||||
withdrawAuthority,
|
||||
depositStake,
|
||||
validatorStake,
|
||||
reserveStake,
|
||||
destinationPoolAccount,
|
||||
managerFeeAccount,
|
||||
referralPoolAccount,
|
||||
poolMint,
|
||||
} = params;
|
||||
|
||||
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositStake;
|
||||
const data = encodeData(type);
|
||||
|
||||
const keys = [
|
||||
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
||||
{ pubkey: validatorList, isSigner: false, isWritable: true },
|
||||
{ pubkey: depositAuthority, isSigner: false, isWritable: false },
|
||||
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
||||
{ pubkey: depositStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: validatorStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: reserveStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: destinationPoolAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: managerFeeAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: referralPoolAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: poolMint, isSigner: false, isWritable: true },
|
||||
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
||||
{ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
|
||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||
{ pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
programId: STAKE_POOL_PROGRAM_ID,
|
||||
keys,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transaction instruction to withdraw SOL from a stake pool.
|
||||
*/
|
||||
static depositSol(params: DepositSolParams): TransactionInstruction {
|
||||
const {
|
||||
stakePool,
|
||||
withdrawAuthority,
|
||||
depositAuthority,
|
||||
reserveStake,
|
||||
fundingAccount,
|
||||
destinationPoolAccount,
|
||||
managerFeeAccount,
|
||||
referralPoolAccount,
|
||||
poolMint,
|
||||
lamports,
|
||||
} = params;
|
||||
|
||||
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol;
|
||||
const data = encodeData(type, { lamports });
|
||||
|
||||
const keys = [
|
||||
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
||||
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
||||
{ pubkey: reserveStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: fundingAccount, isSigner: true, isWritable: true },
|
||||
{ pubkey: destinationPoolAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: managerFeeAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: referralPoolAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: poolMint, isSigner: false, isWritable: true },
|
||||
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||
];
|
||||
|
||||
if (depositAuthority) {
|
||||
keys.push({
|
||||
pubkey: depositAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return new TransactionInstruction({
|
||||
programId: STAKE_POOL_PROGRAM_ID,
|
||||
keys,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transaction instruction to withdraw SOL from a stake pool.
|
||||
*/
|
||||
static withdrawStake(params: WithdrawStakeParams): TransactionInstruction {
|
||||
const {
|
||||
stakePool,
|
||||
validatorList,
|
||||
withdrawAuthority,
|
||||
validatorStake,
|
||||
destinationStake,
|
||||
destinationStakeAuthority,
|
||||
sourceTransferAuthority,
|
||||
sourcePoolAccount,
|
||||
managerFeeAccount,
|
||||
poolMint,
|
||||
poolTokens,
|
||||
} = params;
|
||||
|
||||
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStake;
|
||||
const data = encodeData(type, { poolTokens });
|
||||
|
||||
const keys = [
|
||||
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
||||
{ pubkey: validatorList, isSigner: false, isWritable: true },
|
||||
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
||||
{ pubkey: validatorStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: destinationStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: destinationStakeAuthority, isSigner: false, isWritable: false },
|
||||
{ pubkey: sourceTransferAuthority, isSigner: true, isWritable: false },
|
||||
{ pubkey: sourcePoolAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: managerFeeAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: poolMint, isSigner: false, isWritable: true },
|
||||
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||
{ pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
programId: STAKE_POOL_PROGRAM_ID,
|
||||
keys,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transaction instruction to withdraw SOL from a stake pool.
|
||||
*/
|
||||
static withdrawSol(params: WithdrawSolParams): TransactionInstruction {
|
||||
const {
|
||||
stakePool,
|
||||
withdrawAuthority,
|
||||
sourceTransferAuthority,
|
||||
sourcePoolAccount,
|
||||
reserveStake,
|
||||
destinationSystemAccount,
|
||||
managerFeeAccount,
|
||||
solWithdrawAuthority,
|
||||
poolMint,
|
||||
poolTokens,
|
||||
} = params;
|
||||
|
||||
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawSol;
|
||||
const data = encodeData(type, { poolTokens });
|
||||
|
||||
const keys = [
|
||||
{ pubkey: stakePool, isSigner: false, isWritable: true },
|
||||
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
|
||||
{ pubkey: sourceTransferAuthority, isSigner: true, isWritable: false },
|
||||
{ pubkey: sourcePoolAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: reserveStake, isSigner: false, isWritable: true },
|
||||
{ pubkey: destinationSystemAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: managerFeeAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: poolMint, isSigner: false, isWritable: true },
|
||||
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
|
||||
{ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
|
||||
{ pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
|
||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||
];
|
||||
|
||||
if (solWithdrawAuthority) {
|
||||
keys.push({
|
||||
pubkey: solWithdrawAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return new TransactionInstruction({
|
||||
programId: STAKE_POOL_PROGRAM_ID,
|
||||
keys,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a deposit stake pool instruction and retrieve the instruction params.
|
||||
*/
|
||||
static decodeDepositStake(instruction: TransactionInstruction): DepositStakeParams {
|
||||
this.checkProgramId(instruction.programId);
|
||||
this.checkKeyLength(instruction.keys, 11);
|
||||
|
||||
decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositStake, instruction.data);
|
||||
|
||||
return {
|
||||
stakePool: instruction.keys[0].pubkey,
|
||||
validatorList: instruction.keys[1].pubkey,
|
||||
depositAuthority: instruction.keys[2].pubkey,
|
||||
withdrawAuthority: instruction.keys[3].pubkey,
|
||||
depositStake: instruction.keys[4].pubkey,
|
||||
validatorStake: instruction.keys[5].pubkey,
|
||||
reserveStake: instruction.keys[6].pubkey,
|
||||
destinationPoolAccount: instruction.keys[7].pubkey,
|
||||
managerFeeAccount: instruction.keys[8].pubkey,
|
||||
referralPoolAccount: instruction.keys[9].pubkey,
|
||||
poolMint: instruction.keys[10].pubkey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a deposit sol instruction and retrieve the instruction params.
|
||||
*/
|
||||
static decodeDepositSol(instruction: TransactionInstruction): DepositSolParams {
|
||||
this.checkProgramId(instruction.programId);
|
||||
this.checkKeyLength(instruction.keys, 9);
|
||||
|
||||
const { amount } = decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol, instruction.data);
|
||||
|
||||
return {
|
||||
stakePool: instruction.keys[0].pubkey,
|
||||
depositAuthority: instruction.keys[1].pubkey,
|
||||
withdrawAuthority: instruction.keys[2].pubkey,
|
||||
reserveStake: instruction.keys[3].pubkey,
|
||||
fundingAccount: instruction.keys[4].pubkey,
|
||||
destinationPoolAccount: instruction.keys[5].pubkey,
|
||||
managerFeeAccount: instruction.keys[6].pubkey,
|
||||
referralPoolAccount: instruction.keys[7].pubkey,
|
||||
poolMint: instruction.keys[8].pubkey,
|
||||
lamports: amount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private static checkProgramId(programId: PublicKey) {
|
||||
if (!programId.equals(StakeProgram.programId)) {
|
||||
throw new Error('Invalid instruction; programId is not StakeProgram');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private static checkKeyLength(keys: Array<any>, expectedLength: number) {
|
||||
if (keys.length < expectedLength) {
|
||||
throw new Error(
|
||||
`Invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import * as index from './index.js';
|
||||
import * as schema from './schema.js';
|
||||
import BN from 'bn.js';
|
||||
import assert, {deepStrictEqual} from 'assert';
|
||||
import {SOLANA_SCHEMA, PublicKey, Connection} from '@solana/web3.js';
|
||||
|
||||
// First populate schema
|
||||
schema.addStakePoolSchema(SOLANA_SCHEMA);
|
||||
|
||||
describe('Integration test', () => {
|
||||
it('should successfully decode all validators from devnet', async () => {
|
||||
/**
|
||||
* Full integration test:
|
||||
* Makes a connection to devnet, gets all stake pool accounts there,
|
||||
* decodes them, and prints their details.
|
||||
*/
|
||||
const connection = new Connection(
|
||||
'https://api.devnet.solana.com/',
|
||||
'confirmed',
|
||||
);
|
||||
const STAKE_POOL_PROGRAM_ADDR = new PublicKey(
|
||||
'poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj',
|
||||
);
|
||||
|
||||
const accounts = await index.getStakePoolAccounts(
|
||||
connection,
|
||||
STAKE_POOL_PROGRAM_ADDR,
|
||||
);
|
||||
|
||||
console.log('Number of stake pool accounts in devnet: ', accounts.length);
|
||||
|
||||
accounts.map(account => {
|
||||
index.prettyPrintAccount(account);
|
||||
console.log('\n');
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully decode all validators from testnet', async () => {
|
||||
/**
|
||||
* Full integration test:
|
||||
* Makes a connection to testnet, gets all stake pool accounts there,
|
||||
* decodes them, and prints their details.
|
||||
* Testnet presents a greater challenge due to the presence of old stake pool program accounts
|
||||
*/
|
||||
const connection = new Connection(
|
||||
'https://api.testnet.solana.com/',
|
||||
'confirmed',
|
||||
);
|
||||
const STAKE_POOL_PROGRAM_ADDR = new PublicKey(
|
||||
'poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj',
|
||||
);
|
||||
|
||||
const accounts = await index.getStakePoolAccounts(
|
||||
connection,
|
||||
STAKE_POOL_PROGRAM_ADDR,
|
||||
);
|
||||
|
||||
console.log('Number of stake pool accounts in testnet: ', accounts.length);
|
||||
|
||||
accounts.map(account => {
|
||||
index.prettyPrintAccount(account);
|
||||
console.log('\n');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
import { publicKey, struct, u32, u64, u8, option, vec } from '@project-serum/borsh';
|
||||
import { Lockup, PublicKey } from '@solana/web3.js';
|
||||
import { AccountInfo } from "@solana/spl-token";
|
||||
import BN from 'bn.js';
|
||||
|
||||
export interface Fee {
|
||||
denominator: BN;
|
||||
numerator: BN;
|
||||
}
|
||||
|
||||
const feeFields = [u64('denominator'), u64('numerator')];
|
||||
|
||||
/**
|
||||
* AccountLayout.encode from "@solana/spl-token" doesn't work
|
||||
*/
|
||||
export const AccountLayout = struct<AccountInfo>([
|
||||
publicKey('mint'),
|
||||
publicKey('owner'),
|
||||
u64('amount'),
|
||||
u32('delegateOption'),
|
||||
publicKey('delegate'),
|
||||
u8('state'),
|
||||
u32('isNativeOption'),
|
||||
u64('isNative'),
|
||||
u64('delegatedAmount'),
|
||||
u32('closeAuthorityOption'),
|
||||
publicKey('closeAuthority'),
|
||||
]);
|
||||
|
||||
export enum AccountType {
|
||||
Uninitialized,
|
||||
StakePool,
|
||||
ValidatorList,
|
||||
}
|
||||
|
||||
export interface StakePool {
|
||||
accountType: AccountType;
|
||||
manager: PublicKey;
|
||||
staker: PublicKey;
|
||||
stakeDepositAuthority: PublicKey;
|
||||
stakeWithdrawBumpSeed: number;
|
||||
validatorList: PublicKey;
|
||||
reserveStake: PublicKey;
|
||||
poolMint: PublicKey;
|
||||
managerFeeAccount: PublicKey;
|
||||
tokenProgramId: PublicKey;
|
||||
totalLamports: BN;
|
||||
poolTokenSupply: BN;
|
||||
lastUpdateEpoch: BN;
|
||||
lockup: Lockup;
|
||||
epochFee: Fee;
|
||||
nextEpochFee?: Fee | undefined;
|
||||
preferredDepositValidatorVoteAddress?: PublicKey | undefined;
|
||||
preferredWithdrawValidatorVoteAddress?: PublicKey | undefined;
|
||||
stakeDepositFee: Fee;
|
||||
stakeWithdrawalFee: Fee;
|
||||
nextWithdrawalFee?: Fee | undefined;
|
||||
stakeReferralFee: number;
|
||||
solDepositAuthority?: PublicKey | undefined;
|
||||
solDepositFee: Fee;
|
||||
solReferralFee: number;
|
||||
solWithdrawAuthority?: PublicKey | undefined;
|
||||
solWithdrawalFee: Fee;
|
||||
nextSolWithdrawalFee?: Fee | undefined;
|
||||
lastEpochPoolTokenSupply: BN;
|
||||
lastEpochTotalLamports: BN;
|
||||
}
|
||||
|
||||
export const StakePoolLayout = struct<StakePool>([
|
||||
u8('accountType'),
|
||||
publicKey('manager'),
|
||||
publicKey('staker'),
|
||||
publicKey('stakeDepositAuthority'),
|
||||
u8('stakeWithdrawBumpSeed'),
|
||||
publicKey('validatorList'),
|
||||
publicKey('reserveStake'),
|
||||
publicKey('poolMint'),
|
||||
publicKey('managerFeeAccount'),
|
||||
publicKey('tokenProgramId'),
|
||||
u64('totalLamports'),
|
||||
u64('poolTokenSupply'),
|
||||
u64('lastUpdateEpoch'),
|
||||
struct([u64('unixTimestamp'), u64('epoch'), publicKey('custodian')], 'lockup'),
|
||||
struct(feeFields, 'epochFee'),
|
||||
option(struct(feeFields), 'nextEpochFee'),
|
||||
option(publicKey(), 'preferredDepositValidatorVoteAddress'),
|
||||
option(publicKey(), 'preferredWithdrawValidatorVoteAddress'),
|
||||
struct(feeFields, 'stakeDepositFee'),
|
||||
struct(feeFields, 'stakeWithdrawalFee'),
|
||||
option(struct(feeFields), 'nextWithdrawalFee'),
|
||||
u8('stakeReferralFee'),
|
||||
option(publicKey(), 'solDepositAuthority'),
|
||||
struct(feeFields, 'solDepositFee'),
|
||||
u8('solReferralFee'),
|
||||
option(publicKey(), 'solWithdrawAuthority'),
|
||||
struct(feeFields, 'solWithdrawalFee'),
|
||||
option(struct(feeFields), 'nextSolWithdrawalFee'),
|
||||
u64('lastEpochPoolTokenSupply'),
|
||||
u64('lastEpochTotalLamports'),
|
||||
]);
|
||||
|
||||
export enum ValidatorStakeInfoStatus {
|
||||
Active,
|
||||
DeactivatingTransient,
|
||||
ReadyForRemoval,
|
||||
}
|
||||
|
||||
export interface ValidatorStakeInfo {
|
||||
status: ValidatorStakeInfoStatus;
|
||||
voteAccountAddress: PublicKey;
|
||||
activeStakeLamports: BN;
|
||||
transientStakeLamports: BN;
|
||||
transientSeedSuffixStart: BN;
|
||||
transientSeedSuffixEnd: BN;
|
||||
lastUpdateEpoch: BN;
|
||||
}
|
||||
|
||||
export const ValidatorStakeInfoLayout = struct<ValidatorStakeInfo>([
|
||||
/// Amount of active stake delegated to this validator
|
||||
/// Note that if `last_update_epoch` does not match the current epoch then
|
||||
/// this field may not be accurate
|
||||
u64('activeStakeLamports'),
|
||||
/// Amount of transient stake delegated to this validator
|
||||
/// Note that if `last_update_epoch` does not match the current epoch then
|
||||
/// this field may not be accurate
|
||||
u64('transientStakeLamports'),
|
||||
/// Last epoch the active and transient stake lamports fields were updated
|
||||
u64('lastUpdateEpoch'),
|
||||
/// Start of the validator transient account seed suffixes
|
||||
u64('transientSeedSuffixStart'),
|
||||
/// End of the validator transient account seed suffixes
|
||||
u64('transientSeedSuffixEnd'),
|
||||
/// Status of the validator stake account
|
||||
u8('status'),
|
||||
/// Validator vote account address
|
||||
publicKey('voteAccountAddress'),
|
||||
]);
|
||||
|
||||
export interface ValidatorList {
|
||||
/// Account type, must be ValidatorList currently
|
||||
accountType: number;
|
||||
/// Maximum allowable number of validators
|
||||
maxValidators: number;
|
||||
/// List of stake info for each validator in the pool
|
||||
validators: ValidatorStakeInfo[];
|
||||
}
|
||||
|
||||
export const ValidatorListLayout = struct<ValidatorList>([
|
||||
u8('accountType'),
|
||||
u32('maxValidators'),
|
||||
vec(ValidatorStakeInfoLayout, 'validators'),
|
||||
]);
|
|
@ -1,139 +0,0 @@
|
|||
import {Schema, serialize, deserializeUnchecked} from 'borsh';
|
||||
import BN from 'bn.js';
|
||||
import {Struct, Enum, PublicKey} from '@solana/web3.js';
|
||||
|
||||
export class Fee extends Struct {
|
||||
denominator: BN;
|
||||
numerator: BN;
|
||||
}
|
||||
|
||||
export class AccountType extends Enum {}
|
||||
|
||||
export class AccountTypeEnum extends Struct {}
|
||||
|
||||
export enum AccountTypeKind {
|
||||
Uninitialized = 'Uninitialized',
|
||||
StakePool = 'StakePool',
|
||||
ValidatorList = 'ValidatorList',
|
||||
}
|
||||
|
||||
export class StakePool extends Struct {
|
||||
accountType: AccountType;
|
||||
manager: PublicKey;
|
||||
staker: PublicKey;
|
||||
depositAuthority: PublicKey;
|
||||
withdrawBumpSeed: number;
|
||||
validatorList: PublicKey;
|
||||
reserveStake: PublicKey;
|
||||
poolMint: PublicKey;
|
||||
managerFeeAccount: PublicKey;
|
||||
totalStakeLamports: BN;
|
||||
poolTokenSupply: BN;
|
||||
lastUpdateEpoch: BN;
|
||||
fee: Fee;
|
||||
}
|
||||
|
||||
export class ValidatorList extends Struct {
|
||||
accountType: AccountType;
|
||||
maxValidators: number;
|
||||
validators: [ValidatorStakeInfo];
|
||||
}
|
||||
export class ValidatorStakeInfo extends Struct {
|
||||
status: StakeStatus;
|
||||
voteAccountAddress: PublicKey;
|
||||
stakeLamports: BN;
|
||||
lastUpdateEpoch: BN;
|
||||
}
|
||||
export class StakeStatus extends Enum {}
|
||||
|
||||
export class StakeStatusEnum extends Struct {}
|
||||
|
||||
export enum StakeStatusKind {
|
||||
Active = 'Active',
|
||||
DeactivatingTransient = 'DeactivatingTransient',
|
||||
ReadyForRemoval = 'ReadyForRemoval',
|
||||
}
|
||||
|
||||
export function addStakePoolSchema(schema: Schema): void {
|
||||
/**
|
||||
* Borsh requires something called a Schema,
|
||||
* which is a Map (key-value pairs) that tell borsh how to deserialise the raw data
|
||||
* This function adds a new schema to an existing schema object.
|
||||
*/
|
||||
schema.set(PublicKey, {
|
||||
kind: 'struct',
|
||||
fields: [['_bn', 'u256']],
|
||||
});
|
||||
|
||||
schema.set(Fee, {
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['denominator', 'u64'],
|
||||
['numerator', 'u64'],
|
||||
],
|
||||
});
|
||||
|
||||
schema.set(AccountType, {
|
||||
kind: 'enum',
|
||||
field: 'enum',
|
||||
values: [
|
||||
// if the account has not been initialized, the enum will be 0
|
||||
[AccountTypeKind.Uninitialized, AccountTypeEnum],
|
||||
[AccountTypeKind.StakePool, AccountTypeEnum],
|
||||
[AccountTypeKind.ValidatorList, AccountTypeEnum],
|
||||
],
|
||||
});
|
||||
|
||||
schema.set(AccountTypeEnum, {kind: 'struct', fields: []});
|
||||
|
||||
schema.set(StakePool, {
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['accountType', AccountType],
|
||||
['manager', PublicKey],
|
||||
['staker', PublicKey],
|
||||
['depositAuthority', PublicKey],
|
||||
['withdrawBumpSeed', 'u8'],
|
||||
['validatorList', PublicKey],
|
||||
['reserveStake', PublicKey],
|
||||
['poolMint', PublicKey],
|
||||
['managerFeeAccount', PublicKey],
|
||||
['tokenProgramId', PublicKey],
|
||||
['totalStakeLamports', 'u64'],
|
||||
['poolTokenSupply', 'u64'],
|
||||
['lastUpdateEpoch', 'u64'],
|
||||
['fee', Fee],
|
||||
],
|
||||
});
|
||||
|
||||
schema.set(ValidatorList, {
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['accountType', AccountType],
|
||||
['maxValidators', 'u32'],
|
||||
['validators', [ValidatorStakeInfo]],
|
||||
],
|
||||
});
|
||||
|
||||
schema.set(StakeStatus, {
|
||||
kind: 'enum',
|
||||
field: 'enum',
|
||||
values: [
|
||||
[StakeStatusKind.Active, StakeStatusEnum],
|
||||
[StakeStatusKind.DeactivatingTransient, StakeStatusEnum],
|
||||
[StakeStatusKind.ReadyForRemoval, StakeStatusEnum],
|
||||
],
|
||||
});
|
||||
|
||||
schema.set(StakeStatusEnum, {kind: 'struct', fields: []});
|
||||
|
||||
schema.set(ValidatorStakeInfo, {
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['status', StakeStatus],
|
||||
['voteAccountAddress', PublicKey],
|
||||
['stakeLamports', 'u64'],
|
||||
['lastUpdateEpoch', 'u64'],
|
||||
],
|
||||
});
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
import * as index from './index.js';
|
||||
import * as schema from './schema.js';
|
||||
import BN from 'bn.js';
|
||||
import assert, {deepStrictEqual} from 'assert';
|
||||
import {SOLANA_SCHEMA, PublicKey, Connection} from '@solana/web3.js';
|
||||
|
||||
// First populate schema
|
||||
schema.addStakePoolSchema(SOLANA_SCHEMA);
|
||||
|
||||
function deepStrictEqualBN(decodedData: object, expectedData: object) {
|
||||
/**
|
||||
* Helper function to do deep equality check because BNs are not equal.
|
||||
* TODO: write this function recursively. For now, sufficient.
|
||||
*/
|
||||
for (const key in decodedData) {
|
||||
if (expectedData[key] instanceof BN) {
|
||||
assert.ok(expectedData[key].eq(decodedData[key]));
|
||||
} else {
|
||||
if (decodedData[key] instanceof Object) {
|
||||
for (const subkey in decodedData[key]) {
|
||||
if (decodedData[key][subkey] instanceof Object) {
|
||||
if (decodedData[key][subkey] instanceof BN) {
|
||||
assert.ok(decodedData[key][subkey].eq(expectedData[key][subkey]));
|
||||
} else {
|
||||
for (const subsubkey in decodedData[key][subkey]) {
|
||||
console.log(decodedData[key][subkey][subsubkey]);
|
||||
if (decodedData[key][subkey][subsubkey] instanceof BN) {
|
||||
assert.ok(
|
||||
decodedData[key][subkey][subsubkey].eq(
|
||||
expectedData[key][subkey][subsubkey],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
assert.deepStrictEqual(
|
||||
expectedData[key][subkey][subsubkey],
|
||||
decodedData[key][subkey][subsubkey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.strictEqual(
|
||||
decodedData[key][subkey],
|
||||
expectedData[key][subkey],
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.strictEqual(decodedData[key], expectedData[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('schema.decode', () => {
|
||||
describe('StakePoolAccount', () => {
|
||||
it('should successfully decode StakePoolAccount account data', () => {
|
||||
const expectedData = new schema.StakePool({
|
||||
accountType: new schema.AccountType({
|
||||
StakePool: new schema.AccountTypeEnum({}),
|
||||
}),
|
||||
manager: new PublicKey(
|
||||
new BN(
|
||||
'dc23cda2ad09ddec126f89ed7f67d06a4d167cca996503f1a1b3b5a13625964f',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
staker: new PublicKey(
|
||||
new BN(
|
||||
'dc23cda2ad09ddec126f89ed7f67d06a4d167cca996503f1a1b3b5a13625964f',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
depositAuthority: new PublicKey(
|
||||
new BN(
|
||||
new Buffer(
|
||||
'5911e7451a1a854fdc9e495081790f293eba623f8ec7e2b9d34a5fd25c7009bb',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
),
|
||||
withdrawBumpSeed: 255,
|
||||
validatorList: new PublicKey(
|
||||
new BN(
|
||||
'7103ba4895b8804263197364da9e791db96ec8f0c8ca184dd666e69013838610',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
reserveStake: new PublicKey(
|
||||
new BN(
|
||||
'74a5b1ab8442103baa8bd39ab8494eb034e96035ac664e1693bb3eef458761ee',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
poolMint: new PublicKey(
|
||||
new BN(
|
||||
'8722bf107b95d2620008d256b18c13fa3a46ab7f643c24cf7656f57267563e00',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
managerFeeAccount: new PublicKey(
|
||||
new BN(
|
||||
new Buffer(
|
||||
'b783b4dcd341cbca22e781bbd49b2d16908a844a21b98e26b69d44fc50e1db0f',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenProgramId: new PublicKey(
|
||||
new BN(
|
||||
'a900ff7e85f58c3a91375b5fed85b41cac79ebce46e1cbd993a165d7e1f6dd06',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
totalStakeLamports: new BN('0', 'hex'),
|
||||
poolTokenSupply: new BN('0', 'hex'),
|
||||
lastUpdateEpoch: new BN('7c', 'hex'),
|
||||
fee: new schema.Fee({
|
||||
denominator: new BN('3e8', 'hex'),
|
||||
numerator: new BN('38', 'hex'),
|
||||
}),
|
||||
});
|
||||
|
||||
const decodedData = schema.StakePool.decode(expectedData.encode());
|
||||
|
||||
deepStrictEqualBN(decodedData, expectedData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ValidatorListAccount', () => {
|
||||
it('should successfully decode ValidatorListAccount account data', () => {
|
||||
const expectedData = new schema.ValidatorList({
|
||||
accountType: new schema.AccountType({
|
||||
ValidatorList: new schema.AccountTypeEnum({}),
|
||||
}),
|
||||
maxValidators: 10,
|
||||
validators: [],
|
||||
});
|
||||
|
||||
const decodedData = schema.ValidatorList.decode(expectedData.encode());
|
||||
assert.deepStrictEqual(decodedData, expectedData);
|
||||
});
|
||||
|
||||
it('should successfully decode ValidatorListAccount with nonempty ValidatorInfo', () => {
|
||||
// TODO also test for decoding ValidatorListAccount with actual ValidatorInfo
|
||||
// Do this once we have a stake pool with validators deployed on testnet
|
||||
|
||||
const expectedData = new schema.ValidatorList({
|
||||
accountType: new schema.AccountType({
|
||||
ValidatorList: new schema.AccountTypeEnum({}),
|
||||
}),
|
||||
maxValidators: 100,
|
||||
validators: [
|
||||
new schema.ValidatorStakeInfo({
|
||||
status: new schema.StakeStatus({
|
||||
Active: new schema.StakeStatusEnum({}),
|
||||
}),
|
||||
voteAccountAddress: new PublicKey(
|
||||
new BN(
|
||||
'a9946a889af14fd3c9b33d5df309489d9699271a6b09ff3190fcb41cf21a2f8c',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
stakeLamports: new BN('0', 'hex'),
|
||||
lastUpdateEpoch: new BN('c3', 'hex'),
|
||||
}),
|
||||
new schema.ValidatorStakeInfo({
|
||||
status: new schema.StakeStatus({
|
||||
Active: new schema.StakeStatusEnum({}),
|
||||
}),
|
||||
voteAccountAddress: new PublicKey(
|
||||
new BN(
|
||||
'3796d40645ee07e3c64117e3f73430471d4c40465f696ebc9b034c1fc06a9f7d',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
stakeLamports: new BN('0', 'hex'),
|
||||
lastUpdateEpoch: new BN('c3', 'hex'),
|
||||
}),
|
||||
new schema.ValidatorStakeInfo({
|
||||
status: new schema.StakeStatus({
|
||||
Active: new schema.StakeStatusEnum({}),
|
||||
}),
|
||||
voteAccountAddress: new PublicKey(
|
||||
new BN(
|
||||
'e4e37d6f2e80c0bb0f3da8a06304e57be5cda6efa2825b86780aa320d9784cf8',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
stakeLamports: new BN('0', 'hex'),
|
||||
lastUpdateEpoch: new BN('c3', 'hex'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const decodedData = schema.ValidatorList.decode(expectedData.encode());
|
||||
deepStrictEqualBN(decodedData, expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('index.ts/PrettyPrintPubkey', () => {
|
||||
it('should successfully pretty print a pubkey', () => {
|
||||
assert.equal(
|
||||
index.prettyPrintPubKey(
|
||||
new PublicKey(
|
||||
new BN(
|
||||
'99572085579321386496717000324290408927851378839748241098946587626478579848783',
|
||||
),
|
||||
),
|
||||
),
|
||||
'6MfzrQUzB2mozveRWU9a77zMoQzSrYa4Gq46KswjupQB',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
export * from './math'
|
||||
export * from './program-address'
|
||||
export * from './stake'
|
||||
export * from './token'
|
|
@ -0,0 +1,24 @@
|
|||
import BN from "bn.js";
|
||||
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
|
||||
|
||||
export function solToLamports(amount: number): number {
|
||||
if (isNaN(amount)) return Number(0);
|
||||
return Number(amount * LAMPORTS_PER_SOL);
|
||||
}
|
||||
|
||||
export function lamportsToSol(lamports: number | BN): number {
|
||||
if (typeof lamports === 'number') {
|
||||
return Math.abs(lamports) / LAMPORTS_PER_SOL;
|
||||
}
|
||||
|
||||
let signMultiplier = 1;
|
||||
if (lamports.isNeg()) {
|
||||
signMultiplier = -1;
|
||||
}
|
||||
|
||||
const absLamports = lamports.abs();
|
||||
const lamportsString = absLamports.toString(10).padStart(10, '0');
|
||||
const splitIndex = lamportsString.length - 9;
|
||||
const solString = lamportsString.slice(0, splitIndex) + '.' + lamportsString.slice(splitIndex);
|
||||
return signMultiplier * parseFloat(solString);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
import { TRANSIENT_STAKE_SEED_PREFIX } from "../constants";
|
||||
|
||||
/**
|
||||
* Generates the withdraw authority program address for the stake pool
|
||||
*/
|
||||
export async function findWithdrawAuthorityProgramAddress(
|
||||
programId: PublicKey,
|
||||
stakePoolAddress: PublicKey,
|
||||
) {
|
||||
const [publicKey] = await PublicKey.findProgramAddress(
|
||||
[stakePoolAddress.toBuffer(), Buffer.from('withdraw')],
|
||||
programId,
|
||||
);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the stake program address for a validator's vote account
|
||||
*/
|
||||
export async function findStakeProgramAddress(
|
||||
programId: PublicKey,
|
||||
voteAccountAddress: PublicKey,
|
||||
stakePoolAddress: PublicKey,
|
||||
) {
|
||||
const [publicKey] = await PublicKey.findProgramAddress(
|
||||
[voteAccountAddress.toBuffer(), stakePoolAddress.toBuffer()],
|
||||
programId,
|
||||
);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the stake program address for a validator's vote account
|
||||
*/
|
||||
export async function findTransientStakeProgramAddress(
|
||||
programId: PublicKey,
|
||||
voteAccountAddress: PublicKey,
|
||||
stakePoolAddress: PublicKey,
|
||||
seed: BN,
|
||||
) {
|
||||
const [publicKey] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
TRANSIENT_STAKE_SEED_PREFIX,
|
||||
voteAccountAddress.toBuffer(),
|
||||
stakePoolAddress.toBuffer(),
|
||||
new Uint8Array(seed.toArray('le', 8)),
|
||||
],
|
||||
programId,
|
||||
);
|
||||
return publicKey;
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey, StakeProgram,
|
||||
SystemProgram,
|
||||
TransactionInstruction
|
||||
} from "@solana/web3.js";
|
||||
import { findStakeProgramAddress, findTransientStakeProgramAddress } from "./program-address";
|
||||
import BN from "bn.js";
|
||||
|
||||
import { lamportsToSol } from "./math";
|
||||
import { WithdrawAccount } from "../index";
|
||||
import {
|
||||
StakePool,
|
||||
ValidatorList,
|
||||
ValidatorListLayout,
|
||||
ValidatorStakeInfoStatus
|
||||
} from "../layouts";
|
||||
import { STAKE_POOL_PROGRAM_ID } from "../constants";
|
||||
|
||||
export async function prepareWithdrawAccounts(
|
||||
connection: Connection,
|
||||
stakePool: StakePool,
|
||||
stakePoolAddress: PublicKey,
|
||||
amount: number,
|
||||
): Promise<WithdrawAccount[]> {
|
||||
const validatorListAcc = await connection.getAccountInfo(stakePool.validatorList);
|
||||
const validatorList = ValidatorListLayout.decode(validatorListAcc!.data) as ValidatorList;
|
||||
|
||||
if (!validatorList?.validators || validatorList?.validators.length == 0) {
|
||||
throw new Error('No accounts found');
|
||||
}
|
||||
|
||||
let accounts = [] as Array<{
|
||||
type: 'preferred' | 'active' | 'transient' | 'reserve';
|
||||
voteAddress?: PublicKey | undefined;
|
||||
stakeAddress: PublicKey;
|
||||
lamports: number;
|
||||
}>;
|
||||
|
||||
// Prepare accounts
|
||||
for (const validator of validatorList.validators) {
|
||||
|
||||
if (validator.status !== ValidatorStakeInfoStatus.Active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stakeAccountAddress = await findStakeProgramAddress(
|
||||
STAKE_POOL_PROGRAM_ID,
|
||||
validator.voteAccountAddress,
|
||||
stakePoolAddress,
|
||||
);
|
||||
|
||||
if (!validator.activeStakeLamports.isZero()) {
|
||||
const isPreferred =
|
||||
stakePool.preferredWithdrawValidatorVoteAddress &&
|
||||
stakePool.preferredWithdrawValidatorVoteAddress!.toBase58() ==
|
||||
validator.voteAccountAddress.toBase58();
|
||||
accounts.push({
|
||||
type: isPreferred ? 'preferred' : 'active',
|
||||
voteAddress: validator.voteAccountAddress,
|
||||
stakeAddress: stakeAccountAddress,
|
||||
lamports: validator.activeStakeLamports.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
const transientStakeAccountAddress = await findTransientStakeProgramAddress(
|
||||
STAKE_POOL_PROGRAM_ID,
|
||||
validator.voteAccountAddress,
|
||||
stakePoolAddress,
|
||||
validator.transientSeedSuffixStart!,
|
||||
);
|
||||
|
||||
if (!validator.transientStakeLamports?.isZero()) {
|
||||
accounts.push({
|
||||
type: 'transient',
|
||||
voteAddress: validator.voteAccountAddress,
|
||||
stakeAddress: transientStakeAccountAddress,
|
||||
lamports: validator.transientStakeLamports!.toNumber(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort from highest to lowest balance
|
||||
accounts = accounts.sort((a, b) => b.lamports - a.lamports);
|
||||
|
||||
const reserveStake = await connection.getAccountInfo(stakePool.reserveStake);
|
||||
if (reserveStake && reserveStake.lamports > 0) {
|
||||
console.log('Reserve Stake: ', reserveStake.lamports);
|
||||
accounts.push({
|
||||
type: 'reserve',
|
||||
stakeAddress: stakePool.reserveStake,
|
||||
lamports: reserveStake?.lamports,
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare the list of accounts to withdraw from
|
||||
const withdrawFrom: WithdrawAccount[] = [];
|
||||
let remainingAmount = amount;
|
||||
|
||||
for (const type of ['preferred', 'active', 'transient', 'reserve']) {
|
||||
const filteredAccounts = accounts.filter(a => a.type == type);
|
||||
|
||||
for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
|
||||
let availableForWithdrawal = Math.floor(calcPoolTokensForDeposit(stakePool, lamports));
|
||||
if (!stakePool.stakeWithdrawalFee.denominator.isZero()) {
|
||||
availableForWithdrawal = divideBnToNumber(
|
||||
new BN(availableForWithdrawal).mul(stakePool.stakeWithdrawalFee.denominator),
|
||||
stakePool.stakeWithdrawalFee.denominator.sub(stakePool.stakeWithdrawalFee.numerator),
|
||||
);
|
||||
}
|
||||
|
||||
const poolAmount = Math.min(availableForWithdrawal, remainingAmount);
|
||||
if (poolAmount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Those accounts will be withdrawn completely with `claim` instruction
|
||||
withdrawFrom.push({ stakeAddress, voteAddress, poolAmount });
|
||||
remainingAmount -= poolAmount;
|
||||
if (remainingAmount == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remainingAmount == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Not enough stake to withdraw the specified amount
|
||||
if (remainingAmount > 0) {
|
||||
throw new Error(
|
||||
`No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(amount)} pool tokens.`
|
||||
);
|
||||
}
|
||||
|
||||
return withdrawFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the pool tokens that should be minted for a deposit of `stakeLamports`
|
||||
*/
|
||||
export function calcPoolTokensForDeposit(stakePool: StakePool, stakeLamports: number): number {
|
||||
if (stakePool.poolTokenSupply.isZero() || stakePool.totalLamports.isZero()) {
|
||||
return stakeLamports;
|
||||
}
|
||||
return divideBnToNumber(
|
||||
new BN(stakeLamports).mul(stakePool.poolTokenSupply),
|
||||
stakePool.totalLamports,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate lamports amount on withdrawal
|
||||
*/
|
||||
export function calcLamportsWithdrawAmount(stakePool: StakePool, poolTokens: number): number {
|
||||
const numerator = new BN(poolTokens).mul(stakePool.totalLamports);
|
||||
const denominator = stakePool.poolTokenSupply;
|
||||
if (numerator.lt(denominator)) {
|
||||
return 0;
|
||||
}
|
||||
return divideBnToNumber(numerator, denominator);
|
||||
}
|
||||
|
||||
export function divideBnToNumber(numerator: BN, denominator: BN): number {
|
||||
if (denominator.isZero()) {
|
||||
return 0;
|
||||
}
|
||||
const quotient = numerator.div(denominator).toNumber();
|
||||
const rem = numerator.umod(denominator);
|
||||
const gcd = rem.gcd(denominator);
|
||||
return quotient + rem.div(gcd).toNumber() / denominator.div(gcd).toNumber();
|
||||
}
|
||||
|
||||
export function newStakeAccount(
|
||||
feePayer: PublicKey,
|
||||
instructions: TransactionInstruction[],
|
||||
lamports: number,
|
||||
): Keypair {
|
||||
// Account for tokens not specified, creating one
|
||||
const stakeReceiverKeypair = Keypair.generate();
|
||||
console.log(`Creating account to receive stake ${stakeReceiverKeypair.publicKey}`);
|
||||
|
||||
instructions.push(
|
||||
// Creating new account
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: feePayer,
|
||||
newAccountPubkey: stakeReceiverKeypair.publicKey,
|
||||
lamports,
|
||||
space: StakeProgram.space,
|
||||
programId: StakeProgram.programId,
|
||||
}),
|
||||
);
|
||||
|
||||
return stakeReceiverKeypair;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js";
|
||||
import {
|
||||
AccountInfo,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID
|
||||
} from "@solana/spl-token";
|
||||
import { AccountLayout } from "../layouts";
|
||||
|
||||
const FAILED_TO_FIND_ACCOUNT = 'Failed to find account';
|
||||
const INVALID_ACCOUNT_OWNER = 'Invalid account owner';
|
||||
|
||||
/**
|
||||
* Retrieve the associated account or create one if not found.
|
||||
* This account may then be used as a `transfer()` or `approve()` destination
|
||||
*/
|
||||
export async function addAssociatedTokenAccount(
|
||||
connection: Connection,
|
||||
owner: PublicKey,
|
||||
mint: PublicKey,
|
||||
instructions: TransactionInstruction[],
|
||||
) {
|
||||
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mint,
|
||||
owner,
|
||||
);
|
||||
|
||||
// This is the optimum logic, considering TX fee, client-side computation,
|
||||
// RPC roundtrips and guaranteed idempotent.
|
||||
// Sadly we can't do this atomically;
|
||||
try {
|
||||
const account = await connection.getAccountInfo(associatedAddress);
|
||||
if (!account) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(FAILED_TO_FIND_ACCOUNT);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// INVALID_ACCOUNT_OWNER can be possible if the associatedAddress has
|
||||
// already been received some lamports (= became system accounts).
|
||||
// Assuming program derived addressing is safe, this is the only case
|
||||
// for the INVALID_ACCOUNT_OWNER in this code-path
|
||||
if (err.message === FAILED_TO_FIND_ACCOUNT || err.message === INVALID_ACCOUNT_OWNER) {
|
||||
instructions.push(
|
||||
Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
mint,
|
||||
associatedAddress,
|
||||
owner,
|
||||
owner,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
return associatedAddress;
|
||||
}
|
||||
|
||||
export async function getTokenAccount(
|
||||
connection: Connection,
|
||||
tokenAccountAddress: PublicKey,
|
||||
expectedTokenMint: PublicKey,
|
||||
): Promise<AccountInfo | void> {
|
||||
try {
|
||||
const account = await connection.getAccountInfo(tokenAccountAddress);
|
||||
if (!account) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(`Invalid account ${tokenAccountAddress.toBase58()}`);
|
||||
}
|
||||
const tokenAccount = AccountLayout.decode(account.data) as AccountInfo;
|
||||
if (tokenAccount.mint?.toBase58() != expectedTokenMint.toBase58()) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(
|
||||
`Invalid token mint for ${tokenAccountAddress}, expected mint is ${expectedTokenMint}`,
|
||||
);
|
||||
}
|
||||
return tokenAccount;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
import { StakePoolAccount, getStakePoolAccounts } from "../src";
|
||||
|
||||
export function isStakePoolAccount(account: any): account is StakePoolAccount {
|
||||
return (account !== undefined) &&
|
||||
(account.account !== undefined) &&
|
||||
(account.account.data !== undefined) &&
|
||||
('manager' in account.account.data);
|
||||
}
|
||||
|
||||
export async function getFirstStakePoolAccount(
|
||||
connection: Connection,
|
||||
stakePoolProgramAddress: PublicKey,
|
||||
): Promise<StakePoolAccount | undefined> {
|
||||
const accounts = await getStakePoolAccounts(connection, stakePoolProgramAddress);
|
||||
|
||||
return accounts!
|
||||
.filter(account => isStakePoolAccount(account))
|
||||
.pop() as StakePoolAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to do deep equality check because BNs are not equal.
|
||||
* TODO: write this function recursively. For now, sufficient.
|
||||
*/
|
||||
export function deepStrictEqualBN(a: any, b: any) {
|
||||
for (const key in a) {
|
||||
if (b[key] instanceof BN) {
|
||||
expect(b[key].toString()).toEqual(a[key].toString());
|
||||
} else {
|
||||
if (a[key] instanceof Object) {
|
||||
for (const subkey in a[key]) {
|
||||
if (a[key][subkey] instanceof Object) {
|
||||
if (a[key][subkey] instanceof BN) {
|
||||
expect(b[key][subkey].toString()).toEqual(a[key][subkey].toString());
|
||||
} else {
|
||||
for (const subsubkey in a[key][subkey]) {
|
||||
if (a[key][subkey][subsubkey] instanceof BN) {
|
||||
expect(b[key][subkey][subsubkey].toString()).toEqual(a[key][subkey][subsubkey].toString());
|
||||
} else {
|
||||
expect(b[key][subkey][subsubkey]).toStrictEqual(a[key][subkey][subsubkey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
expect(b[key][subkey]).toStrictEqual(a[key][subkey]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
expect(b[key]).toStrictEqual(a[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
import {
|
||||
PublicKey,
|
||||
Connection,
|
||||
Keypair,
|
||||
SystemProgram, AccountInfo, LAMPORTS_PER_SOL
|
||||
} from '@solana/web3.js';
|
||||
import { StakePoolLayout } from '../src/layouts';
|
||||
import { STAKE_POOL_PROGRAM_ID } from '../src/constants';
|
||||
import { decodeData } from '../src/copied-from-solana-web3/instruction';
|
||||
import {
|
||||
STAKE_POOL_INSTRUCTION_LAYOUTS,
|
||||
DepositSolParams,
|
||||
StakePoolInstruction,
|
||||
depositSol,
|
||||
withdrawSol,
|
||||
withdrawStake
|
||||
} from "../src";
|
||||
|
||||
import { mockTokenAccount, mockValidatorList, stakePoolMock } from "./mocks";
|
||||
|
||||
describe('StakePoolProgram', () => {
|
||||
|
||||
const connection = new Connection('http://127.0.0.1:8899');
|
||||
|
||||
connection.getMinimumBalanceForRentExemption = jest.fn(async () => 10000);
|
||||
|
||||
const stakePoolPubkey = new PublicKey(
|
||||
'SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy',
|
||||
);
|
||||
|
||||
const data = Buffer.alloc(1024);
|
||||
StakePoolLayout.encode(stakePoolMock, data)
|
||||
|
||||
const stakePoolAccount = <AccountInfo<any>>{
|
||||
executable: true,
|
||||
owner: stakePoolPubkey,
|
||||
lamports: 99999,
|
||||
data,
|
||||
};
|
||||
|
||||
it('StakePoolInstruction.depositSol', () => {
|
||||
|
||||
const payload: DepositSolParams = {
|
||||
stakePool: stakePoolPubkey,
|
||||
withdrawAuthority: Keypair.generate().publicKey,
|
||||
reserveStake: Keypair.generate().publicKey,
|
||||
fundingAccount: Keypair.generate().publicKey,
|
||||
destinationPoolAccount: Keypair.generate().publicKey,
|
||||
managerFeeAccount: Keypair.generate().publicKey,
|
||||
referralPoolAccount: Keypair.generate().publicKey,
|
||||
poolMint: Keypair.generate().publicKey,
|
||||
lamports: 99999,
|
||||
};
|
||||
|
||||
const instruction = StakePoolInstruction.depositSol(payload);
|
||||
|
||||
expect(instruction.keys).toHaveLength(10);
|
||||
expect(instruction.keys[0].pubkey.toBase58()).toEqual(payload.stakePool.toBase58());
|
||||
expect(instruction.keys[1].pubkey.toBase58()).toEqual(payload.withdrawAuthority.toBase58());
|
||||
expect(instruction.keys[3].pubkey.toBase58()).toEqual(payload.fundingAccount.toBase58());
|
||||
expect(instruction.keys[4].pubkey.toBase58()).toEqual(payload.destinationPoolAccount.toBase58());
|
||||
expect(instruction.keys[5].pubkey.toBase58()).toEqual(payload.managerFeeAccount.toBase58());
|
||||
expect(instruction.keys[6].pubkey.toBase58()).toEqual(payload.referralPoolAccount.toBase58());
|
||||
expect(instruction.keys[8].pubkey.toBase58()).toEqual(SystemProgram.programId.toBase58());
|
||||
expect(instruction.keys[9].pubkey.toBase58()).toEqual(STAKE_POOL_PROGRAM_ID.toBase58());
|
||||
|
||||
const decodedData = decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol, instruction.data);
|
||||
|
||||
expect(decodedData.instruction).toEqual(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol.index);
|
||||
expect(decodedData.lamports).toEqual(payload.lamports);
|
||||
|
||||
payload.depositAuthority = Keypair.generate().publicKey;
|
||||
|
||||
const instruction2 = StakePoolInstruction.depositSol(payload);
|
||||
|
||||
expect(instruction2.keys).toHaveLength(11);
|
||||
expect(instruction2.keys[10].pubkey.toBase58()).toEqual(payload.depositAuthority.toBase58());
|
||||
|
||||
});
|
||||
|
||||
describe('depositSol', () => {
|
||||
const from = Keypair.generate().publicKey;
|
||||
const balance = 10000;
|
||||
|
||||
connection.getBalance = jest.fn(async () => balance);
|
||||
|
||||
connection.getAccountInfo = jest.fn(async (pubKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
return <AccountInfo<any>>{
|
||||
executable: true,
|
||||
owner: from,
|
||||
lamports: balance,
|
||||
data: null,
|
||||
}
|
||||
});
|
||||
|
||||
it.only('should throw an error with invalid balance', async () => {
|
||||
await expect(
|
||||
depositSol(connection, stakePoolPubkey, from, balance + 1)
|
||||
).rejects.toThrow(Error('Not enough SOL to deposit into pool. Maximum deposit amount is 0.00001 SOL.'));
|
||||
});
|
||||
|
||||
it.only('should throw an error with invalid account', async () => {
|
||||
connection.getAccountInfo = jest.fn(async () => null);
|
||||
await expect(
|
||||
depositSol(connection, stakePoolPubkey, from, balance)
|
||||
).rejects.toThrow(Error('Invalid account'));
|
||||
});
|
||||
|
||||
it.only('should call successfully', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
return <AccountInfo<any>>{
|
||||
executable: true,
|
||||
owner: from,
|
||||
lamports: balance,
|
||||
data: null,
|
||||
}
|
||||
});
|
||||
|
||||
const res = await depositSol(connection, stakePoolPubkey, from, balance)
|
||||
|
||||
expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(2);
|
||||
expect(res.instructions).toHaveLength(2);
|
||||
expect(res.signers).toHaveLength(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('withdrawSol', () => {
|
||||
const tokenOwner = new PublicKey(0);
|
||||
const solReceiver = new PublicKey(1);
|
||||
|
||||
it.only('should throw an error with invalid stake pool account', async () => {
|
||||
connection.getAccountInfo = jest.fn(async () => null);
|
||||
await expect(
|
||||
withdrawSol(connection, stakePoolPubkey, tokenOwner, solReceiver, 1)
|
||||
).rejects.toThrowError('Invalid account');
|
||||
});
|
||||
|
||||
it.only('should throw an error with invalid token account', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
if (pubKey.toBase58() == '9q2rZU5RujvyD9dmYKhzJAZfG4aGBbvQ8rWY52jCNBai') {
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await expect(
|
||||
withdrawSol(connection, stakePoolPubkey, tokenOwner, solReceiver, 1)
|
||||
).rejects.toThrow(Error('Invalid token account'));
|
||||
});
|
||||
|
||||
it.only('should throw an error with invalid token account balance', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
if (pubKey.toBase58() == 'GQkqTamwqjaNDfsbNm7r3aXPJ4oTSqKC3d5t2PF9Smqd') {
|
||||
return mockTokenAccount(0);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await expect(
|
||||
withdrawSol(connection, stakePoolPubkey, tokenOwner, solReceiver, 1)
|
||||
).rejects.toThrow(Error('Not enough token balance to withdraw 1 pool tokens.\n Maximum withdraw amount is 0 pool tokens.'));
|
||||
});
|
||||
|
||||
it.only('should call successfully', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
if (pubKey.toBase58() == 'GQkqTamwqjaNDfsbNm7r3aXPJ4oTSqKC3d5t2PF9Smqd') {
|
||||
return mockTokenAccount(LAMPORTS_PER_SOL);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const res = await withdrawSol(connection, stakePoolPubkey, tokenOwner, solReceiver, 1)
|
||||
|
||||
expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(2);
|
||||
expect(res.instructions).toHaveLength(2);
|
||||
expect(res.signers).toHaveLength(1);
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
describe('withdrawStake', () => {
|
||||
|
||||
const tokenOwner = new PublicKey(0);
|
||||
|
||||
it.only('should throw an error with invalid token account', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await expect(
|
||||
withdrawStake(connection, stakePoolPubkey, tokenOwner, 1)
|
||||
).rejects.toThrow(Error('Invalid token account'));
|
||||
});
|
||||
|
||||
it.only('should throw an error with invalid token account balance', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
if (pubKey.toBase58() == 'GQkqTamwqjaNDfsbNm7r3aXPJ4oTSqKC3d5t2PF9Smqd') {
|
||||
return mockTokenAccount(0);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await expect(
|
||||
withdrawStake(connection, stakePoolPubkey, tokenOwner, 1)
|
||||
).rejects.toThrow(Error('Not enough token balance to withdraw 1 pool tokens.\n' +
|
||||
' Maximum withdraw amount is 0 pool tokens.'));
|
||||
});
|
||||
|
||||
it.only('should call successfully', async () => {
|
||||
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
|
||||
if (pubKey == stakePoolPubkey) {
|
||||
return stakePoolAccount
|
||||
}
|
||||
if (pubKey.toBase58() == 'GQkqTamwqjaNDfsbNm7r3aXPJ4oTSqKC3d5t2PF9Smqd') {
|
||||
return mockTokenAccount(LAMPORTS_PER_SOL * 2);
|
||||
}
|
||||
if (pubKey.toBase58() == stakePoolMock.validatorList.toBase58()) {
|
||||
return mockValidatorList();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const res = await withdrawStake(connection, stakePoolPubkey, tokenOwner, 1);
|
||||
|
||||
expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(4);
|
||||
expect(res.instructions).toHaveLength(3);
|
||||
expect(res.signers).toHaveLength(2);
|
||||
expect(res.stakeReceiver).toEqual(undefined);
|
||||
expect(res.totalRentFreeBalances).toEqual(10000);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* TODO: https://github.com/solana-labs/solana-program-library/pull/2604
|
||||
* @joncinque
|
||||
* These tests could be extremely flaky because of the devnet connection, so we could probably just remove them.
|
||||
* It doesn't need to be done in this PR, but eventually we should have tests that create a stake pool / deposit / withdraw,
|
||||
* all only accessing a local test validator. Same as with the token and token-swap js tests.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
describe('Integration test', () => {
|
||||
|
||||
it('should be implemented', () => {
|
||||
|
||||
})
|
||||
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
StakePoolLayout,
|
||||
ValidatorListLayout,
|
||||
ValidatorList,
|
||||
} from '../src/layouts';
|
||||
import { deepStrictEqualBN } from "./equal";
|
||||
import { stakePoolMock, validatorListMock } from "./mocks";
|
||||
|
||||
describe('layouts', () => {
|
||||
|
||||
describe('StakePoolAccount', () => {
|
||||
it('should successfully decode StakePoolAccount data', () => {
|
||||
const encodedData = Buffer.alloc(1024);
|
||||
StakePoolLayout.encode(stakePoolMock, encodedData);
|
||||
const decodedData = StakePoolLayout.decode(encodedData);
|
||||
deepStrictEqualBN(decodedData, stakePoolMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ValidatorListAccount', () => {
|
||||
it('should successfully decode ValidatorListAccount account data', () => {
|
||||
const expectedData: ValidatorList = {
|
||||
accountType: 0,
|
||||
maxValidators: 10,
|
||||
validators: [],
|
||||
};
|
||||
const encodedData = Buffer.alloc(64);
|
||||
ValidatorListLayout.encode(expectedData, encodedData);
|
||||
const decodedData = ValidatorListLayout.decode(encodedData);
|
||||
expect(decodedData).toEqual(expectedData);
|
||||
});
|
||||
|
||||
it('should successfully decode ValidatorListAccount with nonempty ValidatorInfo', () => {
|
||||
const encodedData = Buffer.alloc(1024);
|
||||
ValidatorListLayout.encode(validatorListMock, encodedData);
|
||||
const decodedData = ValidatorListLayout.decode(encodedData);
|
||||
deepStrictEqualBN(decodedData, validatorListMock);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,150 @@
|
|||
import { AccountInfo, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
import { ValidatorStakeInfo } from "../src";
|
||||
import { ValidatorStakeInfoStatus, AccountLayout, ValidatorListLayout } from "../src/layouts";
|
||||
|
||||
export const stakePoolMock = {
|
||||
accountType: 1,
|
||||
manager: new PublicKey(11),
|
||||
staker: new PublicKey(12),
|
||||
stakeDepositAuthority: new PublicKey(13),
|
||||
stakeWithdrawBumpSeed: 255,
|
||||
validatorList: new PublicKey(14),
|
||||
reserveStake: new PublicKey(15),
|
||||
poolMint: new PublicKey(16),
|
||||
managerFeeAccount: new PublicKey(17),
|
||||
tokenProgramId: new PublicKey(18),
|
||||
totalLamports: new BN(LAMPORTS_PER_SOL * 999),
|
||||
poolTokenSupply: new BN(LAMPORTS_PER_SOL * 100),
|
||||
lastUpdateEpoch: new BN('7c', 'hex'),
|
||||
lockup: {
|
||||
unixTimestamp: new BN(Date.now()),
|
||||
epoch: new BN(1),
|
||||
custodian: new PublicKey(0),
|
||||
},
|
||||
epochFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
nextEpochFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
preferredDepositValidatorVoteAddress: new PublicKey(1),
|
||||
preferredWithdrawValidatorVoteAddress: new PublicKey(2),
|
||||
stakeDepositFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
stakeWithdrawalFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
nextWithdrawalFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
stakeReferralFee: 0,
|
||||
solDepositAuthority: new PublicKey(0),
|
||||
solDepositFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
solReferralFee: 0,
|
||||
solWithdrawAuthority: new PublicKey(0),
|
||||
solWithdrawalFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
nextSolWithdrawalFee: {
|
||||
denominator: new BN(0),
|
||||
numerator: new BN(0),
|
||||
},
|
||||
lastEpochPoolTokenSupply: new BN(0),
|
||||
lastEpochTotalLamports: new BN(0),
|
||||
};
|
||||
|
||||
export const validatorListMock = {
|
||||
accountType: 0,
|
||||
maxValidators: 100,
|
||||
validators: <ValidatorStakeInfo[]>[
|
||||
{
|
||||
status: ValidatorStakeInfoStatus.ReadyForRemoval,
|
||||
voteAccountAddress: new PublicKey(
|
||||
new BN(
|
||||
'a9946a889af14fd3c9b33d5df309489d9699271a6b09ff3190fcb41cf21a2f8c',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
lastUpdateEpoch: new BN('c3', 'hex'),
|
||||
activeStakeLamports: new BN(123),
|
||||
transientStakeLamports: new BN(999),
|
||||
transientSeedSuffixStart: new BN(999),
|
||||
transientSeedSuffixEnd: new BN(999),
|
||||
},
|
||||
{
|
||||
status: ValidatorStakeInfoStatus.Active,
|
||||
voteAccountAddress: new PublicKey(
|
||||
new BN(
|
||||
'3796d40645ee07e3c64117e3f73430471d4c40465f696ebc9b034c1fc06a9f7d',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
lastUpdateEpoch: new BN('c3', 'hex'),
|
||||
activeStakeLamports: new BN(LAMPORTS_PER_SOL * 100),
|
||||
transientStakeLamports: new BN(22),
|
||||
transientSeedSuffixStart: new BN(0),
|
||||
transientSeedSuffixEnd: new BN(0),
|
||||
},
|
||||
{
|
||||
status: ValidatorStakeInfoStatus.Active,
|
||||
voteAccountAddress: new PublicKey(
|
||||
new BN(
|
||||
'e4e37d6f2e80c0bb0f3da8a06304e57be5cda6efa2825b86780aa320d9784cf8',
|
||||
'hex',
|
||||
),
|
||||
),
|
||||
lastUpdateEpoch: new BN('c3', 'hex'),
|
||||
activeStakeLamports: new BN(0),
|
||||
transientStakeLamports: new BN(0),
|
||||
transientSeedSuffixStart: new BN('a', 'hex'),
|
||||
transientSeedSuffixEnd: new BN('a', 'hex'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function mockTokenAccount(amount = 0) {
|
||||
const data = Buffer.alloc(1024);
|
||||
AccountLayout.encode({
|
||||
state: 0,
|
||||
mint: stakePoolMock.poolMint,
|
||||
owner: new PublicKey(0),
|
||||
amount: new BN(amount),
|
||||
// address: new PublicKey(0),
|
||||
// delegate: null,
|
||||
// delegatedAmount: new BN(0),
|
||||
// isInitialized: true,
|
||||
// isFrozen: false,
|
||||
// isNative: false,
|
||||
// rentExemptReserve: null,
|
||||
// closeAuthority: null,
|
||||
}, data)
|
||||
|
||||
return <AccountInfo<any>>{
|
||||
executable: true,
|
||||
owner: new PublicKey(0),
|
||||
lamports: amount,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
export function mockValidatorList() {
|
||||
const data = Buffer.alloc(1024);
|
||||
ValidatorListLayout.encode(validatorListMock, data)
|
||||
return <AccountInfo<any>>{
|
||||
executable: true,
|
||||
owner: new PublicKey(0),
|
||||
lamports: 0,
|
||||
data,
|
||||
}
|
||||
}
|
|
@ -1,14 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "ES6",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "declarations",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": "src",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true
|
||||
},
|
||||
"include": [
|
||||
"test",
|
||||
"src"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue