[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
|
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
|
> 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",
|
"name": "@solana/spl-stake-pool",
|
||||||
"version": "0.1.0",
|
"version": "0.2.1",
|
||||||
"description": "SPL Stake Pool Program JS API",
|
"description": "SPL Stake Pool Program JS API",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "npx prettier src/*.ts -w",
|
"lint": "npx prettier src/*.ts -w",
|
||||||
"build": "tsc",
|
"test": "jest",
|
||||||
"test": "./node_modules/mocha/bin/mocha -p ./dist"
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Lieu Zheng Hong",
|
"authors": [
|
||||||
|
"Lieu Zheng Hong",
|
||||||
|
"mFactory Team (https://mfactory.ch)"
|
||||||
|
],
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
|
||||||
"@types/mocha": "^8.2.2",
|
|
||||||
"mocha": "^8.4.0",
|
|
||||||
"prettier": "^2.2.1",
|
|
||||||
"typescript": "^4.2.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solana/web3.js": "^1.18.0",
|
"@project-serum/borsh": "^0.2.2",
|
||||||
"assert": "^2.0.0",
|
"@solana/spl-token": "^0.1.8",
|
||||||
"borsh": "^0.4.0",
|
"@solana/web3.js": "^1.30.2",
|
||||||
"buffer": "^6.0.1",
|
"bn.js": "^5.2.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"process": "^0.11.10"
|
"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 {
|
||||||
import solanaWeb3 from '@solana/web3.js';
|
AccountInfo,
|
||||||
import assert from 'assert';
|
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 {
|
export type { StakePool, AccountType, ValidatorList, ValidatorStakeInfo } from './layouts';
|
||||||
/**
|
export { STAKE_POOL_PROGRAM_ID } from './constants';
|
||||||
* Wrapper class for a stake pool.
|
export * from './instructions';
|
||||||
* Each stake pool has a stake pool account and a validator list account.
|
|
||||||
*/
|
export interface ValidatorListAccount {
|
||||||
stakePool: StakePoolAccount;
|
pubkey: PublicKey;
|
||||||
validatorList: ValidatorListAccount;
|
account: AccountInfo<ValidatorList>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StakePoolAccount {
|
export interface StakePoolAccount {
|
||||||
pubkey: solanaWeb3.PublicKey;
|
pubkey: PublicKey;
|
||||||
account: solanaWeb3.AccountInfo<schema.StakePool>;
|
account: AccountInfo<StakePool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidatorListAccount {
|
export interface WithdrawAccount {
|
||||||
pubkey: solanaWeb3.PublicKey;
|
stakeAddress: PublicKey;
|
||||||
account: solanaWeb3.AccountInfo<schema.ValidatorList>;
|
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.
|
* @param stakePoolPubKey: The public key (address) of the stake pool account.
|
||||||
*/
|
*/
|
||||||
export async function getStakePoolAccount(
|
export async function getStakePoolAccount(
|
||||||
connection: solanaWeb3.Connection,
|
connection: Connection,
|
||||||
stakePoolPubKey: solanaWeb3.PublicKey,
|
stakePoolPubKey: PublicKey,
|
||||||
): Promise<StakePoolAccount> {
|
): Promise<StakePoolAccount> {
|
||||||
const account = await connection.getAccountInfo(stakePoolPubKey);
|
const account = await connection.getAccountInfo(stakePoolPubKey);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
throw new Error('Invalid account');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pubkey: stakePoolPubKey,
|
pubkey: stakePoolPubKey,
|
||||||
account: {
|
account: {
|
||||||
data: schema.StakePool.decode(account.data),
|
data: StakePoolLayout.decode(account.data),
|
||||||
executable: account.executable,
|
executable: account.executable,
|
||||||
lamports: account.lamports,
|
lamports: account.lamports,
|
||||||
owner: account.owner,
|
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.
|
* Retrieves all StakePool and ValidatorList accounts that are running a particular StakePool program.
|
||||||
* @param connection: An active web3js connection.
|
* @param connection: An active web3js connection.
|
||||||
* @param stakePoolProgramAddress: The public key (address) of the StakePool program.
|
* @param stakePoolProgramAddress: The public key (address) of the StakePool program.
|
||||||
*/
|
*/
|
||||||
export async function getStakePoolAccounts(
|
export async function getStakePoolAccounts(
|
||||||
connection: solanaWeb3.Connection,
|
connection: Connection,
|
||||||
stakePoolProgramAddress: solanaWeb3.PublicKey,
|
stakePoolProgramAddress: PublicKey,
|
||||||
): Promise<(StakePoolAccount | ValidatorListAccount)[]> {
|
): Promise<(StakePoolAccount | ValidatorListAccount)[] | undefined> {
|
||||||
try {
|
const response = await connection.getProgramAccounts(stakePoolProgramAddress);
|
||||||
let response = await connection.getProgramAccounts(stakePoolProgramAddress);
|
|
||||||
|
|
||||||
const stakePoolAccounts = response.map(a => {
|
return response.map(a => {
|
||||||
let decodedData;
|
let decodedData;
|
||||||
|
|
||||||
if (a.account.data.readUInt8() === 1) {
|
if (a.account.data.readUInt8() === 1) {
|
||||||
try {
|
try {
|
||||||
decodedData = schema.StakePool.decode(a.account.data);
|
decodedData = StakePoolLayout.decode(a.account.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Could not decode StakeAccount. Error:', error);
|
console.log('Could not decode StakeAccount. Error:', error);
|
||||||
decodedData = undefined;
|
decodedData = undefined;
|
||||||
}
|
}
|
||||||
} else if (a.account.data.readUInt8() === 2) {
|
} else if (a.account.data.readUInt8() === 2) {
|
||||||
try {
|
try {
|
||||||
decodedData = schema.ValidatorList.decodeUnchecked(a.account.data);
|
decodedData = ValidatorListLayout.decode(a.account.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Could not decode ValidatorList. Error:', 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!`,
|
|
||||||
);
|
|
||||||
decodedData = undefined;
|
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 {
|
} 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);
|
const withdrawTransaction = StakePoolInstruction.withdrawSol({
|
||||||
console.log('Owner PubKey:', account.account.owner.toString());
|
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": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "es2019",
|
||||||
"module": "ES6",
|
"allowJs": true,
|
||||||
"esModuleInterop": true,
|
"declaration": true,
|
||||||
"moduleResolution": "Node",
|
"declarationDir": "declarations",
|
||||||
"outDir": "dist",
|
"esModuleInterop": true,
|
||||||
"declaration": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"declarationMap": true,
|
"strict": true,
|
||||||
},
|
"forceConsistentCasingInFileNames": true,
|
||||||
"include": [
|
"module": "esnext",
|
||||||
"src/**/*.ts"
|
"moduleResolution": "node",
|
||||||
]
|
"resolveJsonModule": true,
|
||||||
}
|
"isolatedModules": true,
|
||||||
|
"baseUrl": "src",
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitReturns": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"test",
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue