feat: add stake program methods; refactor instruction type handling

This commit is contained in:
Tyera Eulberg 2019-12-23 11:46:49 -07:00 committed by Michael Vines
parent fc77e55920
commit 532b28e96e
16 changed files with 602 additions and 48 deletions

4
web3.js/flow-typed/hasha.js vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'hasha' {
// TODO: Fill in types
declare module.exports: any;
}

View File

@ -203,6 +203,62 @@ declare module '@solana/web3.js' {
validatorExit(): Promise<boolean>;
}
// === src/config-program.js ===
declare export var CONFIG_PROGRAM_ID;
// === src/stake-program.js ===
declare export class StakeProgram {
static programId: PublicKey;
static space: number;
static createAccount(
from: PublicKey,
stakeAccount: PublicKey,
authorized: Authorized,
lockup: Lockup,
lamports: number,
): Transaction;
static createAccountWithSeed(
from: PublicKey,
stakeAccount: PublicKey,
seed: string,
authorized: Authorized,
lockup: Lockup,
lamports: number,
): Transaction;
static delegate(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
votePubkey: PublicKey,
): Transaction;
static authorize(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
newAuthorized: PublicKey,
stakeAuthorizationType: StakeAuthorizationType,
): Transaction;
static redeemVoteCredits(
stakeAccount: PublicKey,
votePubkey: PublicKey,
): Transaction;
static split(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
lamports: number,
splitStakePubkey: PublicKey,
): Transaction;
static withdraw(
stakeAccount: PublicKey,
withdrawerPubkey: PublicKey,
to: PublicKey,
lamports: number,
): Transaction;
static deactivate(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
): Transaction;
}
// === src/system-program.js ===
declare export class SystemProgram {
static programId: PublicKey;
@ -248,11 +304,14 @@ declare module '@solana/web3.js' {
static fromConfigData(buffer: Buffer): ?ValidatorInfo;
}
// === src/sysvar-rent.js ===
// === src/sysvar.js ===
declare export var SYSVAR_CLOCK_PUBKEY;
declare export var SYSVAR_RENT_PUBKEY;
declare export var SYSVAR_REWARDS_PUBKEY;
declare export var SYSVAR_STAKE_HISTORY_PUBKEY;
// === src/vote-account.js ===
declare export var VOTE_ACCOUNT_KEY;
declare export var VOTE_PROGRAM_ID;
declare export type Lockout = {|
slot: number,
confirmationCount: number,
@ -277,6 +336,17 @@ declare module '@solana/web3.js' {
static fromAccountData(buffer: Buffer): VoteAccount;
}
// === src/instruction.js ===
declare export type InstructionType = {|
index: number,
layout: typeof BufferLayout,
|};
declare export function encodeData(
type: InstructionType,
fields: Object,
): Buffer;
// === src/transaction.js ===
declare export type TransactionSignature = string;

View File

@ -9692,6 +9692,27 @@
"minimalistic-assert": "^1.0.1"
}
},
"hasha": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz",
"integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==",
"requires": {
"is-stream": "^2.0.0",
"type-fest": "^0.8.0"
},
"dependencies": {
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
}
}
},
"hawk": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",

View File

@ -71,6 +71,7 @@
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"esdoc-inject-style-plugin": "^1.0.0",
"hasha": "^5.1.0",
"jayson": "^3.0.1",
"mz": "^2.7.0",
"node-fetch": "^2.2.0",

View File

@ -0,0 +1,6 @@
// @flow
import {PublicKey} from './publickey';
export const CONFIG_PROGRAM_ID = new PublicKey(
'Config1111111111111111111111111111111111111',
);

View File

@ -2,21 +2,26 @@
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 {StakeInstruction, StakeProgram} from './stake-program';
export {SystemInstruction, SystemProgram} from './system-program';
export {Transaction, TransactionInstruction} from './transaction';
export {VALIDATOR_INFO_KEY, ValidatorInfo} from './validator-info';
export {VOTE_ACCOUNT_KEY, VoteAccount} from './vote-account';
export {SYSVAR_RENT_PUBKEY} from './sysvar-rent';
export {VOTE_PROGRAM_ID, VoteAccount} from './vote-account';
export {
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
SYSVAR_REWARDS_PUBKEY,
SYSVAR_STAKE_HISTORY_PUBKEY,
} from './sysvar';
export {
sendAndConfirmTransaction,
sendAndConfirmRecentTransaction,
} from './util/send-and-confirm-transaction';
export {
sendAndConfirmRawTransaction,
} from './util/send-and-confirm-raw-transaction';
export {sendAndConfirmRawTransaction} from './util/send-and-confirm-raw-transaction';
export {testnetChannelEndpoint} from './util/testnet';
// There are 1-billion lamports in one SOL

View File

@ -0,0 +1,27 @@
// @flow
import * as BufferLayout from 'buffer-layout';
import * as Layout from './layout';
/**
* @typedef {Object} InstructionType
* @property (index} The Instruction index (from solana upstream program)
* @property (BufferLayout} The BufferLayout to use to build data
*/
export type InstructionType = {|
index: number,
layout: typeof BufferLayout,
|};
/**
* Populate a buffer of instruction data using an InstructionType
*/
export function encodeData(type: InstructionType, fields: Object): 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;
}

View File

@ -54,6 +54,26 @@ export const rustString = (property: string = 'string') => {
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('epoch'), publicKey('custodian')],
property,
);
};
export function getAlloc(type: Object, fields: Object): number {
let alloc = 0;
type.layout.fields.forEach(item => {

View File

@ -6,7 +6,7 @@ import {Account} from './account';
import {PublicKey} from './publickey';
import {NUM_TICKS_PER_SECOND} from './timing';
import {Transaction, PACKET_DATA_SIZE} from './transaction';
import {SYSVAR_RENT_PUBKEY} from './sysvar-rent';
import {SYSVAR_RENT_PUBKEY} from './sysvar';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
import {sleep} from './util/sleep';
import type {Connection} from './connection';

View File

@ -80,8 +80,16 @@ export class PublicKey {
/**
* Derive a public key from another key, a seed, and a programId.
*/
static createWithSeed(fromPublicKey: PublicKey, seed: string, programId: PublicKey): PublicKey {
const buffer = Buffer.concat([fromPublicKey.toBuffer(), Buffer.from(seed), programId.toBuffer()])
static createWithSeed(
fromPublicKey: PublicKey,
seed: string,
programId: PublicKey,
): PublicKey {
const buffer = Buffer.concat([
fromPublicKey.toBuffer(),
Buffer.from(seed),
programId.toBuffer(),
]);
const hash = hasha(buffer, {algorithm: 'sha256'});
return new PublicKey('0x' + hash);
}

View File

@ -0,0 +1,395 @@
// @flow
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';
import {PublicKey} from './publickey';
import {SystemProgram} from './system-program';
import {
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
SYSVAR_REWARDS_PUBKEY,
SYSVAR_STAKE_HISTORY_PUBKEY,
} from './sysvar';
import {Transaction, TransactionInstruction} from './transaction';
import type {TransactionInstructionCtorFields} from './transaction';
export class Authorized {
staker: PublicKey;
withdrawer: PublicKey;
/**
* Create a new Authorized object
*/
constructor(staker: PublicKey, withdrawer: PublicKey) {
this.staker = staker;
this.withdrawer = withdrawer;
}
}
export class Lockup {
epoch: number;
custodian: PublicKey;
/**
* Create a new Authorized object
*/
constructor(epoch: number, custodian: PublicKey) {
this.epoch = epoch;
this.custodian = custodian;
}
}
/**
* Stake Instruction class
*/
export class StakeInstruction extends TransactionInstruction {
/**
* Type of StakeInstruction
*/
type: InstructionType;
}
/**
* An enumeration of valid StakeInstructionTypes
*/
const StakeInstructionLayout = Object.freeze({
Initialize: {
index: 0,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
Layout.authorized(),
Layout.lockup(),
]),
},
Authorize: {
index: 1,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
Layout.publicKey('newAuthorized'),
BufferLayout.u32('stakeAuthorizationType'),
]),
},
DelegateStake: {
index: 2,
layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
},
RedeemVoteCredits: {
index: 3,
layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
},
Split: {
index: 4,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
BufferLayout.ns64('amount'),
]),
},
Withdraw: {
index: 5,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
BufferLayout.ns64('amount'),
]),
},
Deactivate: {
index: 6,
layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
},
});
/**
* @typedef {Object} StakeAuthorizationType
* @property (index} The Stake Authorization index (from solana-stake-program)
*/
export type StakeAuthorizationType = {|
index: number,
|};
/**
* An enumeration of valid StakeInstructionTypes
*/
export const StakeAuthorizationLayout = Object.freeze({
Staker: {
index: 0,
},
Withdrawer: {
index: 1,
},
});
class RewardsPoolPublicKey extends PublicKey {
static get rewardsPoolBaseId(): PublicKey {
return new PublicKey('StakeRewards1111111111111111111111111111111');
}
/**
* Generate Derive a public key from another key, a seed, and a programId.
*/
static randomId(): PublicKey {
const randomInt = Math.floor(Math.random() * (256 - 1));
let pubkey = this.rewardsPoolBaseId;
for (let i = 0; i < randomInt; i++) {
const buffer = pubkey.toBuffer();
const hash = hasha(buffer, {algorithm: 'sha256'});
return new PublicKey('0x' + hash);
}
return pubkey;
}
}
/**
* Factory class for transactions to interact with the Stake program
*/
export class StakeProgram {
/**
* Public key that identifies the Stake program
*/
static get programId(): PublicKey {
return new PublicKey('Stake11111111111111111111111111111111111111');
}
/**
* Max space of a Stake account
*/
static get space(): number {
return 2000;
}
/**
* Generate an Initialize instruction to add to a Stake Create transaction
*/
static initialize(
stakeAccount: PublicKey,
authorized: Authorized,
lockup: Lockup,
): TransactionInstruction {
const type = StakeInstructionLayout.Initialize;
const data = encodeData(type, {
authorized,
lockup,
});
const instructionData = {
keys: [
{pubkey: stakeAccount, isSigner: false, isWritable: true},
{pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
],
programId: this.programId,
data,
};
return new TransactionInstruction(instructionData);
}
/**
* Generate a Transaction that creates a new Stake account at
* an address generated with `from`, a seed, and the Stake programId
*/
static createAccountWithSeed(
from: PublicKey,
stakeAccount: PublicKey,
seed: string,
authorized: Authorized,
lockup: Lockup,
lamports: number,
): Transaction {
let transaction = SystemProgram.createAccountWithSeed(
from,
stakeAccount,
seed,
lamports,
this.space,
this.programId,
);
return transaction.add(this.initialize(stakeAccount, authorized, lockup));
}
/**
* Generate a Transaction that creates a new Stake account
*/
static createAccount(
from: PublicKey,
stakeAccount: PublicKey,
authorized: Authorized,
lockup: Lockup,
lamports: number,
): Transaction {
let transaction = SystemProgram.createAccount(
from,
stakeAccount,
lamports,
this.space,
this.programId,
);
return transaction.add(this.initialize(stakeAccount, authorized, lockup));
}
/**
* Generate a Transaction that delegates Stake tokens to a validator
* Vote PublicKey. This transaction can also be used to redelegate Stake
* to a new validator Vote PublicKey.
*/
static delegate(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
votePubkey: PublicKey,
): Transaction {
const type = StakeInstructionLayout.DelegateStake;
const data = encodeData(type);
return new Transaction().add({
keys: [
{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: authorizedPubkey, isSigner: true, isWritable: false},
],
programId: this.programId,
data,
});
}
/**
* Generate a Transaction that authorizes a new PublicKey as Staker
* or Withdrawer on the Stake account.
*/
static authorize(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
newAuthorized: PublicKey,
stakeAuthorizationType: StakeAuthorizationType,
): Transaction {
const type = StakeInstructionLayout.Authorize;
const data = encodeData(type, {
newAuthorized,
stakeAuthorizationType: stakeAuthorizationType.index,
});
return new Transaction().add({
keys: [
{pubkey: stakeAccount, isSigner: false, isWritable: true},
{pubkey: authorizedPubkey, isSigner: true, isWritable: false},
],
programId: this.programId,
data,
});
}
/**
* Generate a Transaction that authorizes a new PublicKey as Staker
* or Withdrawer on the Stake account.
*/
static redeemVoteCredits(
stakeAccount: PublicKey,
votePubkey: PublicKey,
): Transaction {
const type = StakeInstructionLayout.RedeemVoteCredits;
const data = encodeData(type);
return new Transaction().add({
keys: [
{pubkey: stakeAccount, isSigner: false, isWritable: true},
{pubkey: votePubkey, isSigner: false, isWritable: true},
{
pubkey: RewardsPoolPublicKey.randomId(),
isSigner: false,
isWritable: true,
},
{pubkey: SYSVAR_REWARDS_PUBKEY, isSigner: false, isWritable: false},
{
pubkey: SYSVAR_STAKE_HISTORY_PUBKEY,
isSigner: false,
isWritable: false,
},
],
programId: this.programId,
data,
});
}
/**
* Generate a Transaction that splits Stake tokens into another stake account
*/
static split(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
lamports: number,
splitStakePubkey: PublicKey,
): Transaction {
let transaction = SystemProgram.createAccount(
stakeAccount,
splitStakePubkey,
0,
this.space,
this.programId,
);
const type = StakeInstructionLayout.Split;
const data = encodeData(type, {lamports});
return transaction.add({
keys: [
{pubkey: stakeAccount, isSigner: false, isWritable: true},
{pubkey: splitStakePubkey, isSigner: false, isWritable: true},
{pubkey: authorizedPubkey, isSigner: true, isWritable: false},
],
programId: this.programId,
data,
});
}
/**
* Generate a Transaction that withdraws deactivated Stake tokens.
*/
static withdraw(
stakeAccount: PublicKey,
withdrawerPubkey: PublicKey,
to: PublicKey,
lamports: number,
): Transaction {
const type = StakeInstructionLayout.Withdraw;
const data = encodeData(type, {lamports});
return new Transaction().add({
keys: [
{pubkey: stakeAccount, isSigner: false, isWritable: true},
{pubkey: to, isSigner: false, isWritable: true},
{pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false},
{
pubkey: SYSVAR_STAKE_HISTORY_PUBKEY,
isSigner: false,
isWritable: false,
},
{pubkey: withdrawerPubkey, isSigner: true, isWritable: false},
],
programId: this.programId,
data,
});
}
/**
* Generate a Transaction that deactivates Stake tokens.
*/
static deactivate(
stakeAccount: PublicKey,
authorizedPubkey: PublicKey,
): Transaction {
const type = StakeInstructionLayout.Deactivate;
const data = encodeData(type);
return new Transaction().add({
keys: [
{pubkey: stakeAccount, isSigner: false, isWritable: true},
{pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false},
{pubkey: authorizedPubkey, isSigner: true, isWritable: false},
],
programId: this.programId,
data,
});
}
}

View File

@ -2,9 +2,11 @@
import * as BufferLayout from 'buffer-layout';
import {Transaction, TransactionInstruction} from './transaction';
import {PublicKey} from './publickey';
import {encodeData} from './instruction';
import type {InstructionType} from './instruction';
import * as Layout from './layout';
import {PublicKey} from './publickey';
import {Transaction, TransactionInstruction} from './transaction';
import type {TransactionInstructionCtorFields} from './transaction';
/**
@ -14,12 +16,9 @@ export class SystemInstruction extends TransactionInstruction {
/**
* Type of SystemInstruction
*/
type: SystemInstructionType;
type: InstructionType;
constructor(
opts?: TransactionInstructionCtorFields,
type?: SystemInstructionType,
) {
constructor(opts?: TransactionInstructionCtorFields, type?: InstructionType) {
if (
opts &&
opts.programId &&
@ -107,16 +106,6 @@ export class SystemInstruction extends TransactionInstruction {
}
}
/**
* @typedef {Object} SystemInstructionType
* @property (index} The System Instruction index (from solana-sdk)
* @property (BufferLayout} The BufferLayout to use to build data
*/
type SystemInstructionType = {|
index: number,
layout: typeof BufferLayout,
|};
/**
* An enumeration of valid SystemInstructionTypes
*/
@ -157,18 +146,6 @@ const SystemInstructionLayout = Object.freeze({
},
});
/**
* Populate a buffer of instruction data using the SystemInstructionType
*/
function encodeData(type: SystemInstructionType, fields: Object): 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;
}
/**
* Factory class for transactions to interact with the System program
*/

View File

@ -1,6 +0,0 @@
// @flow
import {PublicKey} from './publickey';
export const SYSVAR_RENT_PUBKEY = new PublicKey(
'SysvarRent111111111111111111111111111111111',
);

18
web3.js/src/sysvar.js Normal file
View File

@ -0,0 +1,18 @@
// @flow
import {PublicKey} from './publickey';
export const SYSVAR_CLOCK_PUBKEY = new PublicKey(
'SysvarC1ock11111111111111111111111111111111',
);
export const SYSVAR_RENT_PUBKEY = new PublicKey(
'SysvarRent111111111111111111111111111111111',
);
export const SYSVAR_REWARDS_PUBKEY = new PublicKey(
'SysvarRewards111111111111111111111111111111',
);
export const SYSVAR_STAKE_HISTORY_PUBKEY = new PublicKey(
'SysvarStakeHistory1111111111111111111111111',
);

View File

@ -4,7 +4,7 @@ import * as BufferLayout from 'buffer-layout';
import * as Layout from './layout';
import {PublicKey} from './publickey';
export const VOTE_ACCOUNT_KEY = new PublicKey(
export const VOTE_PROGRAM_ID = new PublicKey(
'Vote111111111111111111111111111111111111111',
);

View File

@ -259,7 +259,15 @@ test('createWithSeed', () => {
0,
0,
]);
const derivedKey = PublicKey.createWithSeed(defaultPublicKey, 'limber chicken: 4/45', defaultPublicKey);
const derivedKey = PublicKey.createWithSeed(
defaultPublicKey,
'limber chicken: 4/45',
defaultPublicKey,
);
expect(derivedKey.equals(new PublicKey('9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq'))).toBe(true);
expect(
derivedKey.equals(
new PublicKey('9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq'),
),
).toBe(true);
});