diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index c915c387b0..7367686c8d 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -203,9 +203,6 @@ declare module '@solana/web3.js' { validatorExit(): Promise; } - // === src/config-program.js === - declare export var CONFIG_PROGRAM_ID; - // === src/stake-program.js === declare export class StakeProgram { static programId: PublicKey; diff --git a/web3.js/src/config-program.js b/web3.js/src/config-program.js deleted file mode 100644 index 54029afe0f..0000000000 --- a/web3.js/src/config-program.js +++ /dev/null @@ -1,6 +0,0 @@ -// @flow -import {PublicKey} from './publickey'; - -export const CONFIG_PROGRAM_ID = new PublicKey( - 'Config1111111111111111111111111111111111111', -); diff --git a/web3.js/src/index.js b/web3.js/src/index.js index 088562c1d0..0700703883 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -2,11 +2,11 @@ export {Account} from './account'; export {BpfLoader} from './bpf-loader'; export {BudgetProgram} from './budget-program'; -export {CONFIG_PROGRAM_ID} from './config-program'; export {Connection} from './connection'; export {Loader} from './loader'; export {PublicKey} from './publickey'; export { + STAKE_CONFIG_ID, Authorized, Lockup, StakeAuthorizationLayout, diff --git a/web3.js/src/layout.js b/web3.js/src/layout.js index 82c317c90d..56e94e2f59 100644 --- a/web3.js/src/layout.js +++ b/web3.js/src/layout.js @@ -69,7 +69,11 @@ export const authorized = (property: string = 'authorized') => { */ export const lockup = (property: string = 'lockup') => { return BufferLayout.struct( - [BufferLayout.ns64('epoch'), publicKey('custodian')], + [ + BufferLayout.ns64('unixTimestamp'), + BufferLayout.ns64('epoch'), + publicKey('custodian'), + ], property, ); }; diff --git a/web3.js/src/stake-program.js b/web3.js/src/stake-program.js index be27160458..183c65d03f 100644 --- a/web3.js/src/stake-program.js +++ b/web3.js/src/stake-program.js @@ -3,7 +3,6 @@ import * as BufferLayout from 'buffer-layout'; import hasha from 'hasha'; -import {CONFIG_PROGRAM_ID} from './config-program'; import {encodeData} from './instruction'; import type {InstructionType} from './instruction'; import * as Layout from './layout'; @@ -18,6 +17,10 @@ import { import {Transaction, TransactionInstruction} from './transaction'; import type {TransactionInstructionCtorFields} from './transaction'; +export const STAKE_CONFIG_ID = new PublicKey( + 'StakeConfig11111111111111111111111111111111', +); + export class Authorized { staker: PublicKey; withdrawer: PublicKey; @@ -32,13 +35,15 @@ export class Authorized { } export class Lockup { + unixTimestamp: number; epoch: number; custodian: PublicKey; /** - * Create a new Authorized object + * Create a new Lockup object */ - constructor(epoch: number, custodian: PublicKey) { + constructor(unixTimestamp: number, epoch: number, custodian: PublicKey) { + this.unixTimestamp = unixTimestamp; this.epoch = epoch; this.custodian = custodian; } @@ -126,14 +131,14 @@ export const StakeInstructionLayout = Object.freeze({ index: 4, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), - BufferLayout.ns64('amount'), + BufferLayout.ns64('lamports'), ]), }, Withdraw: { index: 5, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), - BufferLayout.ns64('amount'), + BufferLayout.ns64('lamports'), ]), }, Deactivate: { @@ -197,7 +202,7 @@ export class StakeProgram { * Max space of a Stake account */ static get space(): number { - return 2000; + return 2008; } /** @@ -215,6 +220,7 @@ export class StakeProgram { withdrawer: authorized.withdrawer.toBuffer(), }, lockup: { + unixTimestamp: lockup.unixTimestamp, epoch: lockup.epoch, custodian: lockup.custodian.toBuffer(), }, @@ -293,7 +299,7 @@ export class StakeProgram { {pubkey: stakeAccount, isSigner: false, isWritable: true}, {pubkey: votePubkey, isSigner: false, isWritable: false}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, - {pubkey: CONFIG_PROGRAM_ID, isSigner: false, isWritable: false}, + {pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ], programId: this.programId, diff --git a/web3.js/test/stake-program.test.js b/web3.js/test/stake-program.test.js index eb87c3bff0..09d9cf8186 100644 --- a/web3.js/test/stake-program.test.js +++ b/web3.js/test/stake-program.test.js @@ -3,8 +3,11 @@ import { Account, Authorized, + Connection, Lockup, PublicKey, + sendAndConfirmRecentTransaction, + LAMPORTS_PER_SOL, StakeAuthorizationLayout, StakeInstruction, StakeInstructionLayout, @@ -13,6 +16,13 @@ import { SystemProgram, Transaction, } from '../src'; +import {mockRpcEnabled} from './__mocks__/node-fetch'; +import {url} from './url'; + +if (!mockRpcEnabled) { + // Testing max commitment level takes around 20s to complete + jest.setTimeout(30000); +} test('createAccountWithSeed', () => { const from = new Account(); @@ -30,7 +40,7 @@ test('createAccountWithSeed', () => { newAccountPubkey, seed, new Authorized(authorized.publicKey, authorized.publicKey), - new Lockup(0, from.publicKey), + new Lockup(0, 0, from.publicKey), 123, ); @@ -52,7 +62,7 @@ test('createAccount', () => { from.publicKey, newAccount.publicKey, new Authorized(authorized.publicKey, authorized.publicKey), - new Lockup(0, from.publicKey), + new Lockup(0, 0, from.publicKey), 123, ); @@ -179,7 +189,7 @@ test('StakeInstructions', () => { newAccountPubkey, seed, new Authorized(authorized.publicKey, authorized.publicKey), - new Lockup(0, from.publicKey), + new Lockup(0, 0, from.publicKey), amount, ); const createWithSeedTransaction = new Transaction({recentBlockhash}).add( @@ -220,3 +230,121 @@ test('StakeInstructions', () => { StakeInstructionLayout.DelegateStake, ); }); + +test('live staking actions', async () => { + if (mockRpcEnabled) { + console.log('non-live test skipped'); + return; + } + + const connection = new Connection(url, 'recent'); + const voteAccounts = await connection.getVoteAccounts(); + const voteAccount = voteAccounts.current.concat(voteAccounts.delinquent)[0]; + const votePubkey = new PublicKey(voteAccount.votePubkey); + + const from = new Account(); + const authorized = new Account(); + await connection.requestAirdrop(from.publicKey, LAMPORTS_PER_SOL); + await connection.requestAirdrop(authorized.publicKey, LAMPORTS_PER_SOL); + + const minimumAmount = await connection.getMinimumBalanceForRentExemption( + StakeProgram.space, + 'recent', + ); + + // Create Stake account with seed + const seed = 'test string'; + const newAccountPubkey = PublicKey.createWithSeed( + from.publicKey, + seed, + StakeProgram.programId, + ); + + let createAndInitializeWithSeed = StakeProgram.createAccountWithSeed( + from.publicKey, + newAccountPubkey, + seed, + new Authorized(authorized.publicKey, authorized.publicKey), + new Lockup(0, 0, new PublicKey('0x00')), + 2 * minimumAmount + 42, + ); + + await sendAndConfirmRecentTransaction( + connection, + createAndInitializeWithSeed, + from, + ); + let originalStakeBalance = await connection.getBalance(newAccountPubkey); + expect(originalStakeBalance).toEqual(2 * minimumAmount + 42); + + let delegation = StakeProgram.delegate( + newAccountPubkey, + authorized.publicKey, + votePubkey, + ); + await sendAndConfirmRecentTransaction(connection, delegation, authorized); + + // Test that withdraw fails before deactivation + const recipient = new Account(); + let withdraw = StakeProgram.withdraw( + newAccountPubkey, + authorized.publicKey, + recipient.publicKey, + 1000, + ); + await expect( + sendAndConfirmRecentTransaction(connection, withdraw, authorized), + ).rejects.toThrow(); + + // Authorize to new account + const newAuthorized = new Account(); + await connection.requestAirdrop(newAuthorized.publicKey, LAMPORTS_PER_SOL); + + let authorize = StakeProgram.authorize( + newAccountPubkey, + authorized.publicKey, + newAuthorized.publicKey, + StakeAuthorizationLayout.Withdrawer, + ); + await sendAndConfirmRecentTransaction(connection, authorize, authorized); + authorize = StakeProgram.authorize( + newAccountPubkey, + authorized.publicKey, + newAuthorized.publicKey, + StakeAuthorizationLayout.Staker, + ); + await sendAndConfirmRecentTransaction(connection, authorize, authorized); + + // Test old authorized can't deactivate + let deactivateNotAuthorized = StakeProgram.deactivate( + newAccountPubkey, + authorized.publicKey, + ); + await expect( + sendAndConfirmRecentTransaction( + connection, + deactivateNotAuthorized, + authorized, + ), + ).rejects.toThrow(); + + // Deactivate stake + let deactivate = StakeProgram.deactivate( + newAccountPubkey, + newAuthorized.publicKey, + ); + await sendAndConfirmRecentTransaction(connection, deactivate, newAuthorized); + + // Test that withdraw succeeds after deactivation + withdraw = StakeProgram.withdraw( + newAccountPubkey, + newAuthorized.publicKey, + recipient.publicKey, + minimumAmount + 20, + ); + await sendAndConfirmRecentTransaction(connection, withdraw, newAuthorized); + const balance = await connection.getBalance(newAccountPubkey); + expect(balance).toEqual(minimumAmount + 22); + const recipientBalance = await connection.getBalance(recipient.publicKey); + expect(recipientBalance).toEqual(minimumAmount + 20); +});