diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index ca11f303cd..3669d10459 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -142,6 +142,22 @@ declare module '@solana/web3.js' { static assign(from: PublicKey, programId: PublicKey): Transaction; } + // === src/validator-info.js === + declare export type Info = {| + name: string, + website?: string, + details?: string, + keybaseId?: string, + |}; + + declare export class ValidatorInfo { + key: PublicKey; + info: Info; + + constructor(key: PublicKey, info: Info): ValidatorInfo; + static fromConfigData(buffer: Buffer): ?ValidatorInfo; + } + // === src/transaction.js === declare export type TransactionSignature = string; diff --git a/web3.js/src/index.js b/web3.js/src/index.js index dcf627ef3d..379a7031eb 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -9,6 +9,7 @@ export {PublicKey} from './publickey'; export {SystemProgram} from './system-program'; export {Token, TokenAmount} from './token-program'; export {Transaction, TransactionInstruction} from './transaction'; +export {ValidatorInfo} from './validator-info'; export {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; export { sendAndConfirmRawTransaction, diff --git a/web3.js/src/validator-info.js b/web3.js/src/validator-info.js new file mode 100644 index 0000000000..18709a8b3f --- /dev/null +++ b/web3.js/src/validator-info.js @@ -0,0 +1,101 @@ +// @flow + +import {struct} from 'superstruct'; + +import * as Layout from './layout'; +import * as shortvec from './util/shortvec-encoding'; +import {PublicKey} from './publickey'; + +const VALIDATOR_INFO_KEY = new PublicKey( + 'Va1idator1nfo111111111111111111111111111111', +); + +/** + * @private + */ +type ConfigKey = {| + publicKey: PublicKey, + isSigner: boolean, +|}; + +/** + * Info used to identity validators. + * + * @typedef {Object} Info + * @property {string} name validator name + * @property {?string} website optional, validator website + * @property {?string} details optional, extra information the validator chose to share + * @property {?string} keybaseId optional, used to identify validators on keybase.io + */ +export type Info = {| + name: string, + website?: string, + details?: string, + keybaseId?: string, +|}; + +const InfoString = struct({ + name: 'string', + website: 'string?', + details: 'string?', + keybaseId: 'string?', +}); + +/** + * ValidatorInfo class + */ +export class ValidatorInfo { + /** + * validator public key + */ + key: PublicKey; + /** + * validator information + */ + info: Info; + + /** + * Construct a valid ValidatorInfo + * + * @param key validator public key + * @param info validator information + */ + constructor(key: PublicKey, info: Info) { + this.key = key; + this.info = info; + } + + /** + * Deserialize ValidatorInfo from the config account data. Exactly two config + * keys are required in the data. + * + * @param buffer config account data + * @return null if info was not found + */ + static fromConfigData(buffer: Buffer): ?ValidatorInfo { + const PUBKEY_LENGTH = 32; + + let byteArray = [...buffer]; + const configKeyCount = shortvec.decodeLength(byteArray); + if (configKeyCount !== 2) return null; + + const configKeys: Array = []; + for (let i = 0; i < 2; i++) { + const publicKey = new PublicKey(byteArray.slice(0, PUBKEY_LENGTH)); + byteArray = byteArray.slice(PUBKEY_LENGTH); + const isSigner = byteArray.slice(0, 1)[0] === 1; + byteArray = byteArray.slice(1); + configKeys.push({publicKey, isSigner}); + } + + if (configKeys[0].publicKey.equals(VALIDATOR_INFO_KEY)) { + if (configKeys[1].isSigner) { + const rawInfo = Layout.rustString().decode(Buffer.from(byteArray)); + const info = InfoString(JSON.parse(rawInfo)); + return new ValidatorInfo(configKeys[1].publicKey, info); + } + } + + return null; + } +} diff --git a/web3.js/test/validator-info.test.js b/web3.js/test/validator-info.test.js new file mode 100644 index 0000000000..a48694991c --- /dev/null +++ b/web3.js/test/validator-info.test.js @@ -0,0 +1,36 @@ +// @flow +import nacl from 'tweetnacl'; + +import {PublicKey} from '../src/publickey'; +import {ValidatorInfo} from '../src/validator-info'; + +test('from config account data', () => { + const keypair = nacl.sign.keyPair.fromSeed( + Uint8Array.from(Array(32).fill(8)), + ); + + const expectedValidatorInfo = new ValidatorInfo( + new PublicKey(keypair.publicKey), + { + name: 'Validator', + keybaseId: 'validator_id', + }, + ); + + // Config data string steps: + // 1) Generate a keypair + // 2) Airdrop lamports to the account + // 3) Modify the `solana-validator-info` tool + // a) Remove the keybase id verification step + // b) Print base64 account data in the `get --all` codepath + // c) Add `println!("Account data: {:?}", base64::encode(&account.data));` + // 4) Use modified `solana-validator-info` tool to publish validator info + // 5) And then use it again to fetch the data! (feel free to trim some A's) + const configData = Buffer.from( + 'AgdRlwF0SPKsXcI8nrx6x4wKJyV6xhRFjeCk8W+AAAAAABOY9ixtGkV8UbpqS189vS9p/KkyFiGNyJl+QWvRfZPKAS8AAAAAAAAAeyJrZXliYXNlSWQiOiJ2YWxpZGF0b3JfaWQiLCJuYW1lIjoiVmFsaWRhdG9yIn0', + 'base64', + ); + const info = ValidatorInfo.fromConfigData(configData); + + expect(info).toEqual(expectedValidatorInfo); +});