diff --git a/web3.js/src/index.js b/web3.js/src/index.js index d642ed2827..b413599938 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -5,7 +5,7 @@ export {BudgetProgram} from './budget-program'; export {Connection} from './connection'; export {Loader} from './loader'; export {PublicKey} from './publickey'; -export {SystemProgram} from './system-program'; +export {SystemInstruction, SystemProgram} from './system-program'; export {Token, TokenAmount} from './token-program'; export {Transaction, TransactionInstruction} from './transaction'; export {VALIDATOR_INFO_KEY, ValidatorInfo} from './validator-info'; diff --git a/web3.js/src/system-program.js b/web3.js/src/system-program.js index 5ac67ff63c..3e1203c96b 100644 --- a/web3.js/src/system-program.js +++ b/web3.js/src/system-program.js @@ -2,9 +2,154 @@ import * as BufferLayout from 'buffer-layout'; -import {Transaction} from './transaction'; +import {Transaction, TransactionInstruction} from './transaction'; import {PublicKey} from './publickey'; import * as Layout from './layout'; +import type {TransactionInstructionCtorFields} from './transaction'; + +/** + * System Instruction class + */ +export class SystemInstruction extends TransactionInstruction { + /** + * Type of SystemInstruction + */ + type: SystemInstructionType; + + constructor( + opts?: TransactionInstructionCtorFields, + type?: SystemInstructionType, + ) { + if ( + opts && + opts.programId && + !opts.programId.equals(SystemProgram.programId) + ) { + throw new Error('programId incorrect; not a SystemInstruction'); + } + super(opts); + if (type) { + this.type = type; + } + } + + static from(instruction: TransactionInstruction): SystemInstruction { + if (!instruction.programId.equals(SystemProgram.programId)) { + throw new Error('programId incorrect; not SystemProgram'); + } + + const instructionTypeLayout = BufferLayout.u32('instruction'); + const typeIndex = instructionTypeLayout.decode(instruction.data); + let type; + for (const t in SystemInstructionEnum) { + if (SystemInstructionEnum[t].index == typeIndex) { + type = SystemInstructionEnum[t]; + } + } + if (!type) { + throw new Error('Instruction type incorrect; not a SystemInstruction'); + } + return new SystemInstruction( + { + keys: instruction.keys, + programId: instruction.programId, + data: instruction.data, + }, + type, + ); + } + + /** + * The `from` public key of the instruction; + * returns null if SystemInstructionType does not support this field + */ + get From(): PublicKey | null { + if ( + this.type == SystemInstructionEnum.CREATE || + this.type == SystemInstructionEnum.TRANSFER + ) { + return this.keys[0].pubkey; + } + return null; + } + + /** + * The `to` public key of the instruction; + * returns null if SystemInstructionType does not support this field + */ + get To(): PublicKey | null { + if ( + this.type == SystemInstructionEnum.CREATE || + this.type == SystemInstructionEnum.TRANSFER + ) { + return this.keys[1].pubkey; + } + return null; + } + + /** + * The `amount` or `lamports` of the instruction; + * returns null if SystemInstructionType does not support this field + */ + get Amount(): number | null { + const data = this.type.layout.decode(this.data); + if (this.type == SystemInstructionEnum.TRANSFER) { + return data.amount; + } else if (this.type == SystemInstructionEnum.CREATE) { + return data.lamports; + } + return null; + } +} + +/** + * @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 + */ +const SystemInstructionEnum = Object.freeze({ + CREATE: { + index: 0, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.ns64('lamports'), + BufferLayout.ns64('space'), + Layout.publicKey('programId'), + ]), + }, + ASSIGN: { + index: 1, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + Layout.publicKey('programId'), + ]), + }, + TRANSFER: { + index: 2, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.ns64('amount'), + ]), + }, +}); + +/** + * Populate a buffer of instruction data using the SystemInstructionType + */ +function encodeData(type: SystemInstructionType, fields: Object): Buffer { + const data = Buffer.alloc(type.layout.span); + 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 @@ -29,23 +174,12 @@ export class SystemProgram { space: number, programId: PublicKey, ): Transaction { - const dataLayout = BufferLayout.struct([ - BufferLayout.u32('instruction'), - BufferLayout.ns64('lamports'), - BufferLayout.ns64('space'), - Layout.publicKey('programId'), - ]); - - const data = Buffer.alloc(dataLayout.span); - dataLayout.encode( - { - instruction: 0, // Create Account instruction - lamports, - space, - programId: programId.toBuffer(), - }, - data, - ); + const type = SystemInstructionEnum.CREATE; + const data = encodeData(type, { + lamports, + space, + programId: programId.toBuffer(), + }); return new Transaction().add({ keys: [ @@ -61,19 +195,8 @@ export class SystemProgram { * Generate a Transaction that transfers lamports from one account to another */ static transfer(from: PublicKey, to: PublicKey, amount: number): Transaction { - const dataLayout = BufferLayout.struct([ - BufferLayout.u32('instruction'), - BufferLayout.ns64('amount'), - ]); - - const data = Buffer.alloc(dataLayout.span); - dataLayout.encode( - { - instruction: 2, // Move instruction - amount, - }, - data, - ); + const type = SystemInstructionEnum.TRANSFER; + const data = encodeData(type, {amount}); return new Transaction().add({ keys: [ @@ -89,19 +212,8 @@ export class SystemProgram { * Generate a Transaction that assigns an account to a program */ static assign(from: PublicKey, programId: PublicKey): Transaction { - const dataLayout = BufferLayout.struct([ - BufferLayout.u32('instruction'), - Layout.publicKey('programId'), - ]); - - const data = Buffer.alloc(dataLayout.span); - dataLayout.encode( - { - instruction: 1, // Assign instruction - programId: programId.toBuffer(), - }, - data, - ); + const type = SystemInstructionEnum.ASSIGN; + const data = encodeData(type, {programId: programId.toBuffer()}); return new Transaction().add({ keys: [{pubkey: from, isSigner: true, isDebitable: true}], diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 510814a5dc..738c652cc2 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -40,7 +40,7 @@ export const PACKET_DATA_SIZE = 1280 - 40 - 8; * @property {?PublicKey} programId * @property {?Buffer} data */ -type TransactionInstructionCtorFields = {| +export type TransactionInstructionCtorFields = {| keys?: Array<{pubkey: PublicKey, isSigner: boolean, isDebitable: boolean}>, programId?: PublicKey, data?: Buffer, diff --git a/web3.js/test/system-program.test.js b/web3.js/test/system-program.test.js index 2cd55cbc75..692b26e220 100644 --- a/web3.js/test/system-program.test.js +++ b/web3.js/test/system-program.test.js @@ -1,6 +1,12 @@ // @flow -import {Account, BudgetProgram, SystemProgram} from '../src'; +import { + Account, + BudgetProgram, + SystemInstruction, + SystemProgram, + Transaction, +} from '../src'; test('createAccount', () => { const from = new Account(); @@ -43,3 +49,95 @@ test('assign', () => { expect(transaction.programId).toEqual(SystemProgram.programId); // TODO: Validate transaction contents more }); + +test('SystemInstruction create', () => { + const from = new Account(); + const to = new Account(); + const program = new Account(); + const amount = 42; + const space = 100; + const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash + const create = SystemProgram.createAccount( + from.publicKey, + to.publicKey, + amount, + space, + program.publicKey, + ); + const transaction = new Transaction({recentBlockhash}).add(create); + + const systemInstruction = SystemInstruction.from(transaction.instructions[0]); + expect(systemInstruction.From).toEqual(from.publicKey); + expect(systemInstruction.To).toEqual(to.publicKey); + expect(systemInstruction.Amount).toEqual(amount); + expect(systemInstruction.programId).toEqual(SystemProgram.programId); +}); + +test('SystemInstruction transfer', () => { + const from = new Account(); + const to = new Account(); + const amount = 42; + const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash + const transfer = SystemProgram.transfer(from.publicKey, to.publicKey, amount); + const transaction = new Transaction({recentBlockhash}).add(transfer); + transaction.sign(from); + + const systemInstruction = SystemInstruction.from(transaction.instructions[0]); + expect(systemInstruction.From).toEqual(from.publicKey); + expect(systemInstruction.To).toEqual(to.publicKey); + expect(systemInstruction.Amount).toEqual(amount); + expect(systemInstruction.programId).toEqual(SystemProgram.programId); +}); + +test('SystemInstruction assign', () => { + const from = new Account(); + const program = new Account(); + const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash + const assign = SystemProgram.assign(from.publicKey, program.publicKey); + const transaction = new Transaction({recentBlockhash}).add(assign); + transaction.sign(from); + + const systemInstruction = SystemInstruction.from(transaction.instructions[0]); + expect(systemInstruction.From).toBeNull(); + expect(systemInstruction.To).toBeNull(); + expect(systemInstruction.Amount).toBeNull(); + expect(systemInstruction.programId).toEqual(SystemProgram.programId); +}); + +test('non-SystemInstruction error', () => { + const from = new Account(); + const program = new Account(); + const to = new Account(); + + const badProgramId = { + keys: [ + {pubkey: from.publicKey, isSigner: true, isDebitable: true}, + {pubkey: to.publicKey, isSigner: false, isDebitable: false}, + ], + programId: BudgetProgram.programId, + data: Buffer.from([2, 0, 0, 0]), + }; + expect(() => { + new SystemInstruction(badProgramId); + }).toThrow(); + + const amount = 123; + const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash + const budgetPay = BudgetProgram.pay( + from.publicKey, + program.publicKey, + to.publicKey, + amount, + ); + const transaction = new Transaction({recentBlockhash}).add(budgetPay); + transaction.sign(from); + + expect(() => { + SystemInstruction.from(transaction.instructions[1]); + }).toThrow(); + + transaction.instructions[0].data[0] = 4; + expect(() => { + SystemInstruction.from(transaction.instructions[0]); + }).toThrow(); +});