solana/web3.js/test/program-tests/stake.test.ts

705 lines
23 KiB
TypeScript

import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {
Keypair,
Authorized,
Connection,
Lockup,
PublicKey,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
StakeAuthorizationLayout,
StakeInstruction,
StakeProgram,
SystemInstruction,
Transaction,
} from '../../src';
import {helpers} from '../mocks/rpc-http';
import {url} from '../url';
use(chaiAsPromised);
describe('StakeProgram', () => {
it('createAccountWithSeed', async () => {
const fromPubkey = Keypair.generate().publicKey;
const seed = 'test string';
const newAccountPubkey = await PublicKey.createWithSeed(
fromPubkey,
seed,
StakeProgram.programId,
);
const authorizedPubkey = Keypair.generate().publicKey;
const authorized = new Authorized(authorizedPubkey, authorizedPubkey);
const lockup = new Lockup(0, 0, fromPubkey);
const lamports = 123;
const transaction = StakeProgram.createAccountWithSeed({
fromPubkey,
stakePubkey: newAccountPubkey,
basePubkey: fromPubkey,
seed,
authorized,
lockup,
lamports,
});
expect(transaction.instructions).to.have.length(2);
const [systemInstruction, stakeInstruction] = transaction.instructions;
const systemParams = {
fromPubkey,
newAccountPubkey,
basePubkey: fromPubkey,
seed,
lamports,
space: StakeProgram.space,
programId: StakeProgram.programId,
};
expect(systemParams).to.eql(
SystemInstruction.decodeCreateWithSeed(systemInstruction),
);
const initParams = {stakePubkey: newAccountPubkey, authorized, lockup};
expect(initParams).to.eql(
StakeInstruction.decodeInitialize(stakeInstruction),
);
});
it('createAccount', () => {
const fromPubkey = Keypair.generate().publicKey;
const newAccountPubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const authorized = new Authorized(authorizedPubkey, authorizedPubkey);
const lockup = new Lockup(0, 0, fromPubkey);
const lamports = 123;
const transaction = StakeProgram.createAccount({
fromPubkey,
stakePubkey: newAccountPubkey,
authorized,
lockup,
lamports,
});
expect(transaction.instructions).to.have.length(2);
const [systemInstruction, stakeInstruction] = transaction.instructions;
const systemParams = {
fromPubkey,
newAccountPubkey,
lamports,
space: StakeProgram.space,
programId: StakeProgram.programId,
};
expect(systemParams).to.eql(
SystemInstruction.decodeCreateAccount(systemInstruction),
);
const initParams = {stakePubkey: newAccountPubkey, authorized, lockup};
expect(initParams).to.eql(
StakeInstruction.decodeInitialize(stakeInstruction),
);
});
it('delegate', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const votePubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
authorizedPubkey,
votePubkey,
};
const transaction = StakeProgram.delegate(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeDelegate(stakeInstruction));
});
it('authorize', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const newAuthorizedPubkey = Keypair.generate().publicKey;
const stakeAuthorizationType = StakeAuthorizationLayout.Staker;
const params = {
stakePubkey,
authorizedPubkey,
newAuthorizedPubkey,
stakeAuthorizationType,
};
const transaction = StakeProgram.authorize(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeAuthorize(stakeInstruction));
});
it('authorize with custodian', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const newAuthorizedPubkey = Keypair.generate().publicKey;
const stakeAuthorizationType = StakeAuthorizationLayout.Withdrawer;
const custodianPubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
authorizedPubkey,
newAuthorizedPubkey,
stakeAuthorizationType,
custodianPubkey,
};
const transaction = StakeProgram.authorize(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeAuthorize(stakeInstruction));
});
it('authorizeWithSeed', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorityBase = Keypair.generate().publicKey;
const authoritySeed = 'test string';
const authorityOwner = Keypair.generate().publicKey;
const newAuthorizedPubkey = Keypair.generate().publicKey;
const stakeAuthorizationType = StakeAuthorizationLayout.Staker;
const params = {
stakePubkey,
authorityBase,
authoritySeed,
authorityOwner,
newAuthorizedPubkey,
stakeAuthorizationType,
};
const transaction = StakeProgram.authorizeWithSeed(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(
StakeInstruction.decodeAuthorizeWithSeed(stakeInstruction),
);
});
it('authorizeWithSeed with custodian', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorityBase = Keypair.generate().publicKey;
const authoritySeed = 'test string';
const authorityOwner = Keypair.generate().publicKey;
const newAuthorizedPubkey = Keypair.generate().publicKey;
const stakeAuthorizationType = StakeAuthorizationLayout.Staker;
const custodianPubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
authorityBase,
authoritySeed,
authorityOwner,
newAuthorizedPubkey,
stakeAuthorizationType,
custodianPubkey,
};
const transaction = StakeProgram.authorizeWithSeed(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(
StakeInstruction.decodeAuthorizeWithSeed(stakeInstruction),
);
});
it('split', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const splitStakePubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
authorizedPubkey,
splitStakePubkey,
lamports: 123,
};
const transaction = StakeProgram.split(params);
expect(transaction.instructions).to.have.length(2);
const [systemInstruction, stakeInstruction] = transaction.instructions;
const systemParams = {
fromPubkey: authorizedPubkey,
newAccountPubkey: splitStakePubkey,
lamports: 0,
space: StakeProgram.space,
programId: StakeProgram.programId,
};
expect(systemParams).to.eql(
SystemInstruction.decodeCreateAccount(systemInstruction),
);
expect(params).to.eql(StakeInstruction.decodeSplit(stakeInstruction));
});
it('splitWithSeed', async () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const lamports = 123;
const seed = 'test string';
const basePubkey = Keypair.generate().publicKey;
const splitStakePubkey = await PublicKey.createWithSeed(
basePubkey,
seed,
StakeProgram.programId,
);
const transaction = StakeProgram.splitWithSeed({
stakePubkey,
authorizedPubkey,
lamports,
splitStakePubkey,
basePubkey,
seed,
});
expect(transaction.instructions).to.have.length(2);
const [systemInstruction, stakeInstruction] = transaction.instructions;
const systemParams = {
accountPubkey: splitStakePubkey,
basePubkey,
seed,
space: StakeProgram.space,
programId: StakeProgram.programId,
};
expect(systemParams).to.eql(
SystemInstruction.decodeAllocateWithSeed(systemInstruction),
);
const splitParams = {
stakePubkey,
authorizedPubkey,
splitStakePubkey,
lamports,
};
expect(splitParams).to.eql(StakeInstruction.decodeSplit(stakeInstruction));
});
it('merge', () => {
const stakePubkey = Keypair.generate().publicKey;
const sourceStakePubKey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
sourceStakePubKey,
authorizedPubkey,
};
const transaction = StakeProgram.merge(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeMerge(stakeInstruction));
});
it('withdraw', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const toPubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
authorizedPubkey,
toPubkey,
lamports: 123,
};
const transaction = StakeProgram.withdraw(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeWithdraw(stakeInstruction));
});
it('withdraw with custodian', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const toPubkey = Keypair.generate().publicKey;
const custodianPubkey = Keypair.generate().publicKey;
const params = {
stakePubkey,
authorizedPubkey,
toPubkey,
lamports: 123,
custodianPubkey,
};
const transaction = StakeProgram.withdraw(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeWithdraw(stakeInstruction));
});
it('deactivate', () => {
const stakePubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const params = {stakePubkey, authorizedPubkey};
const transaction = StakeProgram.deactivate(params);
expect(transaction.instructions).to.have.length(1);
const [stakeInstruction] = transaction.instructions;
expect(params).to.eql(StakeInstruction.decodeDeactivate(stakeInstruction));
});
it('StakeInstructions', async () => {
const from = Keypair.generate();
const seed = 'test string';
const newAccountPubkey = await PublicKey.createWithSeed(
from.publicKey,
seed,
StakeProgram.programId,
);
const authorized = Keypair.generate();
const amount = 123;
const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash
const createWithSeed = StakeProgram.createAccountWithSeed({
fromPubkey: from.publicKey,
stakePubkey: newAccountPubkey,
basePubkey: from.publicKey,
seed,
authorized: new Authorized(authorized.publicKey, authorized.publicKey),
lockup: new Lockup(0, 0, from.publicKey),
lamports: amount,
});
const createWithSeedTransaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add(createWithSeed);
expect(createWithSeedTransaction.instructions).to.have.length(2);
const systemInstructionType = SystemInstruction.decodeInstructionType(
createWithSeedTransaction.instructions[0],
);
expect(systemInstructionType).to.eq('CreateWithSeed');
const stakeInstructionType = StakeInstruction.decodeInstructionType(
createWithSeedTransaction.instructions[1],
);
expect(stakeInstructionType).to.eq('Initialize');
expect(() => {
StakeInstruction.decodeInstructionType(
createWithSeedTransaction.instructions[0],
);
}).to.throw();
const stake = Keypair.generate();
const vote = Keypair.generate();
const delegate = StakeProgram.delegate({
stakePubkey: stake.publicKey,
authorizedPubkey: authorized.publicKey,
votePubkey: vote.publicKey,
});
const delegateTransaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add(delegate);
const anotherStakeInstructionType = StakeInstruction.decodeInstructionType(
delegateTransaction.instructions[0],
);
expect(anotherStakeInstructionType).to.eq('Delegate');
});
if (process.env.TEST_LIVE) {
it('live staking actions', async () => {
const connection = new Connection(url, 'confirmed');
const [
SYSTEM_ACCOUNT_MIN_BALANCE,
STAKE_ACCOUNT_MIN_BALANCE,
{value: MIN_STAKE_DELEGATION},
] = await Promise.all([
connection.getMinimumBalanceForRentExemption(0),
connection.getMinimumBalanceForRentExemption(StakeProgram.space),
connection.getStakeMinimumDelegation(),
]);
const voteAccounts = await connection.getVoteAccounts();
const voteAccount = voteAccounts.current.concat(
voteAccounts.delinquent,
)[0];
const votePubkey = new PublicKey(voteAccount.votePubkey);
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: 10 * LAMPORTS_PER_SOL,
});
const authorized = Keypair.generate();
await helpers.airdrop({
connection,
address: authorized.publicKey,
amount: 2 * LAMPORTS_PER_SOL,
});
const recipient = Keypair.generate();
await helpers.airdrop({
connection,
address: recipient.publicKey,
amount: SYSTEM_ACCOUNT_MIN_BALANCE,
});
{
// Create Stake account without seed
const newStakeAccount = Keypair.generate();
let createAndInitialize = StakeProgram.createAccount({
fromPubkey: payer.publicKey,
stakePubkey: newStakeAccount.publicKey,
authorized: new Authorized(
authorized.publicKey,
authorized.publicKey,
),
lamports: STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
});
await sendAndConfirmTransaction(
connection,
createAndInitialize,
[payer, newStakeAccount],
{preflightCommitment: 'confirmed'},
);
expect(await connection.getBalance(newStakeAccount.publicKey)).to.eq(
STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
);
const delegation = StakeProgram.delegate({
stakePubkey: newStakeAccount.publicKey,
authorizedPubkey: authorized.publicKey,
votePubkey,
});
await sendAndConfirmTransaction(connection, delegation, [authorized], {
commitment: 'confirmed',
});
}
// Create Stake account with seed
const seed = 'test string';
const newAccountPubkey = await PublicKey.createWithSeed(
payer.publicKey,
seed,
StakeProgram.programId,
);
const WITHDRAW_AMOUNT = 1;
const INITIAL_STAKE_DELEGATION = 5 * LAMPORTS_PER_SOL;
let createAndInitializeWithSeed = StakeProgram.createAccountWithSeed({
fromPubkey: payer.publicKey,
stakePubkey: newAccountPubkey,
basePubkey: payer.publicKey,
seed,
authorized: new Authorized(authorized.publicKey, authorized.publicKey),
lockup: new Lockup(0, 0, new PublicKey(0)),
lamports: STAKE_ACCOUNT_MIN_BALANCE + INITIAL_STAKE_DELEGATION,
});
await sendAndConfirmTransaction(
connection,
createAndInitializeWithSeed,
[payer],
{preflightCommitment: 'confirmed'},
);
let originalStakeBalance = await connection.getBalance(newAccountPubkey);
expect(originalStakeBalance).to.eq(
STAKE_ACCOUNT_MIN_BALANCE + INITIAL_STAKE_DELEGATION,
);
let delegation = StakeProgram.delegate({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
votePubkey,
});
await sendAndConfirmTransaction(connection, delegation, [authorized], {
preflightCommitment: 'confirmed',
});
// Test that withdraw fails before deactivation
let withdraw = StakeProgram.withdraw({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
toPubkey: recipient.publicKey,
lamports: WITHDRAW_AMOUNT,
});
await expect(
sendAndConfirmTransaction(connection, withdraw, [authorized], {
preflightCommitment: 'confirmed',
}),
).to.be.rejected;
// Deactivate stake
let deactivate = StakeProgram.deactivate({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
});
await sendAndConfirmTransaction(connection, deactivate, [authorized], {
preflightCommitment: 'confirmed',
});
let stakeActivationState;
do {
stakeActivationState = await connection.getStakeActivation(
newAccountPubkey,
);
} while (stakeActivationState.state != 'inactive');
// Test that withdraw succeeds after deactivation
withdraw = StakeProgram.withdraw({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
toPubkey: recipient.publicKey,
lamports: WITHDRAW_AMOUNT,
});
await sendAndConfirmTransaction(connection, withdraw, [authorized], {
preflightCommitment: 'confirmed',
});
const recipientBalance = await connection.getBalance(recipient.publicKey);
expect(recipientBalance).to.eq(
SYSTEM_ACCOUNT_MIN_BALANCE + WITHDRAW_AMOUNT,
);
// Split stake
const newStake = Keypair.generate();
let split = StakeProgram.split({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
splitStakePubkey: newStake.publicKey,
lamports: STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
});
await sendAndConfirmTransaction(
connection,
split,
[authorized, newStake],
{
preflightCommitment: 'confirmed',
},
);
const balance = await connection.getBalance(newStake.publicKey);
expect(balance).to.eq(STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION);
// Split stake with seed
const seed2 = 'test string 2';
const newStake2 = await PublicKey.createWithSeed(
payer.publicKey,
seed2,
StakeProgram.programId,
);
let splitWithSeed = StakeProgram.splitWithSeed({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
lamports: STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
splitStakePubkey: newStake2,
basePubkey: payer.publicKey,
seed: seed2,
});
await sendAndConfirmTransaction(
connection,
splitWithSeed,
[payer, authorized],
{
preflightCommitment: 'confirmed',
},
);
expect(await connection.getBalance(newStake2)).to.eq(
STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
);
// Merge stake
const preMergeBalance = await connection.getBalance(newAccountPubkey);
let merge = StakeProgram.merge({
stakePubkey: newAccountPubkey,
sourceStakePubKey: newStake.publicKey,
authorizedPubkey: authorized.publicKey,
});
await sendAndConfirmTransaction(connection, merge, [authorized], {
preflightCommitment: 'confirmed',
});
const postMergeBalance = await connection.getBalance(newAccountPubkey);
expect(postMergeBalance - preMergeBalance).to.eq(
STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
);
// Resplit
split = StakeProgram.split({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
splitStakePubkey: newStake.publicKey,
// use a different amount than the first split so that this
// transaction is different and won't require a fresh blockhash
lamports: STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
});
await sendAndConfirmTransaction(
connection,
split,
[authorized, newStake],
{
preflightCommitment: 'confirmed',
},
);
// Authorize to new account
const newAuthorized = Keypair.generate();
await connection.requestAirdrop(
newAuthorized.publicKey,
LAMPORTS_PER_SOL,
);
let authorize = StakeProgram.authorize({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
newAuthorizedPubkey: newAuthorized.publicKey,
stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
});
await sendAndConfirmTransaction(connection, authorize, [authorized], {
preflightCommitment: 'confirmed',
});
authorize = StakeProgram.authorize({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
newAuthorizedPubkey: newAuthorized.publicKey,
stakeAuthorizationType: StakeAuthorizationLayout.Staker,
});
await sendAndConfirmTransaction(connection, authorize, [authorized], {
preflightCommitment: 'confirmed',
});
// Test old authorized can't delegate
let delegateNotAuthorized = StakeProgram.delegate({
stakePubkey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
votePubkey,
});
await expect(
sendAndConfirmTransaction(
connection,
delegateNotAuthorized,
[authorized],
{
preflightCommitment: 'confirmed',
},
),
).to.be.rejected;
// Test accounts with different authorities can't be merged
let mergeNotAuthorized = StakeProgram.merge({
stakePubkey: newStake.publicKey,
sourceStakePubKey: newAccountPubkey,
authorizedPubkey: authorized.publicKey,
});
await expect(
sendAndConfirmTransaction(
connection,
mergeNotAuthorized,
[authorized],
{
preflightCommitment: 'confirmed',
},
),
).to.be.rejected;
// Authorize a derived address
authorize = StakeProgram.authorize({
stakePubkey: newAccountPubkey,
authorizedPubkey: newAuthorized.publicKey,
newAuthorizedPubkey: newAccountPubkey,
stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
});
await sendAndConfirmTransaction(connection, authorize, [newAuthorized], {
preflightCommitment: 'confirmed',
});
// Restore the previous authority using a derived address
authorize = StakeProgram.authorizeWithSeed({
stakePubkey: newAccountPubkey,
authorityBase: payer.publicKey,
authoritySeed: seed,
authorityOwner: StakeProgram.programId,
newAuthorizedPubkey: newAuthorized.publicKey,
stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
});
await sendAndConfirmTransaction(connection, authorize, [payer], {
preflightCommitment: 'confirmed',
});
}).timeout(10 * 1000);
}
});