diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 94f17cd9f..1b38e85b4 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -197,6 +197,12 @@ declare module '@solana/web3.js' { space: number; }; + export type StakeActivationData = { + state: 'active' | 'inactive' | 'activating' | 'deactivating'; + active: number; + inactive: number; + }; + export type KeyedAccountInfo = { accountId: PublicKey; accountInfo: AccountInfo; @@ -314,6 +320,11 @@ declare module '@solana/web3.js' { ): Promise< RpcResponseAndContext | null> >; + getStakeActivation( + publicKey: PublicKey, + commitment?: Commitment, + epoch?: number, + ): Promise; getProgramAccounts( programId: PublicKey, commitment?: Commitment, diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 1e4686102..4e443dfc0 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -178,6 +178,12 @@ declare module '@solana/web3.js' { space: number, }; + declare export type StakeActivationData = { + state: 'active' | 'inactive' | 'activating' | 'deactivating', + active: number, + inactive: number, + }; + declare export type ParsedMessageAccount = { pubkey: PublicKey, signer: boolean, @@ -328,6 +334,11 @@ declare module '@solana/web3.js' { ): Promise< RpcResponseAndContext | null>, >; + getStakeActivation( + publicKey: PublicKey, + commitment?: Commitment, + epoch?: number, + ): Promise; getProgramAccounts( programId: PublicKey, commitment: ?Commitment, diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 5efd1e857..d946ba9e0 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -806,6 +806,20 @@ const ParsedAccountInfoResult = struct.object({ rentEpoch: 'number?', }); +/** + * @private + */ +const StakeActivationResult = struct.object({ + state: struct.union([ + struct.literal('active'), + struct.literal('inactive'), + struct.literal('activating'), + struct.literal('deactivating'), + ]), + active: 'number', + inactive: 'number', +}); + /** * Expected JSON RPC response for the "getAccountInfo" message */ @@ -820,6 +834,11 @@ const GetParsedAccountInfoResult = jsonRpcResultAndContext( struct.union(['null', ParsedAccountInfoResult]), ); +/** + * Expected JSON RPC response for the "getStakeActivation" message with jsonParsed param + */ +const GetStakeActivationResult = jsonRpcResult(StakeActivationResult); + /** * Expected JSON RPC response for the "getConfirmedSignaturesForAddress" message */ @@ -1201,6 +1220,20 @@ type ParsedAccountData = { space: number, }; +/** + * Stake Activation data + * + * @typedef {Object} StakeActivationData + * @property {string} state: { + const args = this._buildArgs( + [publicKey.toBase58()], + commitment, + undefined, + epoch !== undefined ? {epoch} : undefined, + ); + + const unsafeRes = await this._rpcRequest('getStakeActivation', args); + const res = GetStakeActivationResult(unsafeRes); + if (res.error) { + throw new Error( + `failed to get Stake Activation ${publicKey.toBase58()}: ${ + res.error.message + }`, + ); + } + assert(typeof res.result !== 'undefined'); + + const {state, active, inactive} = res.result; + return {state, active, inactive}; + } + /** * Fetch all the accounts owned by the specified program id * @@ -3093,9 +3156,10 @@ export class Connection { args: Array, override: ?Commitment, encoding?: 'jsonParsed' | 'base64', + extra?: any, ): Array { const commitment = override || this._commitment; - if (commitment || encoding) { + if (commitment || encoding || extra) { let options: any = {}; if (encoding) { options.encoding = encoding; @@ -3103,6 +3167,9 @@ export class Connection { if (commitment) { options.commitment = commitment; } + if (extra) { + options = Object.assign(options, extra); + } args.push(options); } return args; diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 32af6eab9..6081024f4 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -4,12 +4,15 @@ import {Token, u64} from '@solana/spl-token'; import { Account, + Authorized, Connection, SystemProgram, Transaction, sendAndConfirmTransaction, LAMPORTS_PER_SOL, + Lockup, PublicKey, + StakeProgram, } from '../src'; import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing'; import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch'; @@ -1521,6 +1524,125 @@ test('get largest accounts', async () => { expect(largestAccounts.length).toEqual(20); }); +test('stake activation should throw when called for not delegated account', async () => { + const connection = new Connection(url); + + const publicKey = new Account().publicKey; + mockRpc.push([ + url, + { + method: 'getStakeActivation', + params: [publicKey.toBase58(), {}], + }, + { + error: {message: 'account not delegated'}, + result: undefined, + }, + ]); + + await expect(connection.getStakeActivation(publicKey)).rejects.toThrow(); +}); + +test('stake activation should return activating for new accounts', 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 authorized = new Account(); + await connection.requestAirdrop(authorized.publicKey, 2 * LAMPORTS_PER_SOL); + + const minimumAmount = await connection.getMinimumBalanceForRentExemption( + StakeProgram.space, + 'recent', + ); + + const newStakeAccount = new Account(); + let createAndInitialize = StakeProgram.createAccount({ + fromPubkey: authorized.publicKey, + stakePubkey: newStakeAccount.publicKey, + authorized: new Authorized(authorized.publicKey, authorized.publicKey), + lockup: new Lockup(0, 0, new PublicKey(0)), + lamports: minimumAmount + 42, + }); + + await sendAndConfirmTransaction( + connection, + createAndInitialize, + [authorized, newStakeAccount], + {commitment: 'single', skipPreflight: true}, + ); + let delegation = StakeProgram.delegate({ + stakePubkey: newStakeAccount.publicKey, + authorizedPubkey: authorized.publicKey, + votePubkey, + }); + await sendAndConfirmTransaction(connection, delegation, [authorized], { + commitment: 'single', + skipPreflight: true, + }); + + const LARGE_EPOCH = 4000; + await expect( + connection.getStakeActivation( + newStakeAccount.publicKey, + 'recent', + LARGE_EPOCH, + ), + ).rejects.toThrow( + `failed to get Stake Activation ${newStakeAccount.publicKey.toBase58()}: Invalid param: epoch ${LARGE_EPOCH} has not yet started`, + ); + + const activationState = await connection.getStakeActivation( + newStakeAccount.publicKey, + ); + expect(activationState.state).toBe('activating'); + expect(activationState.inactive).toBe(42); + expect(activationState.active).toBe(0); +}); + +test('stake activation should only accept state with valid string literals', async () => { + if (!mockRpcEnabled) { + console.log('live test skipped'); + return; + } + + const connection = new Connection(url, 'recent'); + const publicKey = new Account().publicKey; + + const addStakeActivationMock = state => { + mockRpc.push([ + url, + { + method: 'getStakeActivation', + params: [publicKey.toBase58(), {}], + }, + { + error: undefined, + result: { + state: state, + active: 0, + inactive: 80, + }, + }, + ]); + }; + + addStakeActivationMock('active'); + let activation = await connection.getStakeActivation(publicKey); + expect(activation.state).toBe('active'); + expect(activation.active).toBe(0); + expect(activation.inactive).toBe(80); + + addStakeActivationMock('invalid'); + await expect(connection.getStakeActivation(publicKey)).rejects.toThrow(); +}); + test('getVersion', async () => { const connection = new Connection(url);