diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 63b4d966b1..56ef20ab9b 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -352,21 +352,54 @@ declare module '@solana/web3.js' { fields: Record, ): Buffer; + // === src/message.js === + export type MessageHeader = { + numRequiredSignatures: number; + numReadonlySignedAccounts: number; + numReadonlyUnsignedAccounts: number; + }; + + export type CompiledInstruction = { + programIdIndex: number; + accounts: number[]; + data: string; + }; + + export type MessageArgs = { + header: MessageHeader; + accountKeys: PublicKey[]; + recentBlockhash: Blockhash; + instructions: CompiledInstruction[]; + }; + + export class Message { + header: MessageHeader; + accountKeys: PublicKey[]; + recentBlockhash: Blockhash; + instructions: CompiledInstruction[]; + + constructor(args: MessageArgs); + isAccountWritable(account: PublicKey): boolean; + serialize(): Buffer; + } + // === src/transaction.js === export type TransactionSignature = string; + export type AccountMeta = { + pubkey: PublicKey; + isSigner: boolean; + isWritable: boolean; + }; + export type TransactionInstructionCtorFields = { - keys?: Array<{pubkey: PublicKey; isSigner: boolean; isWritable: boolean}>; + keys?: Array; programId?: PublicKey; data?: Buffer; }; export class TransactionInstruction { - keys: Array<{ - pubkey: PublicKey; - isSigner: boolean; - isWritable: boolean; - }>; + keys: Array; programId: PublicKey; data: Buffer; @@ -403,6 +436,7 @@ declare module '@solana/web3.js' { Transaction | TransactionInstruction | TransactionInstructionCtorFields > ): Transaction; + compileMessage(): Message; serializeMessage(): Buffer; sign(...signers: Array): void; signPartial(...partialSigners: Array): void; diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index c9021da70c..156abcbbc9 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -362,17 +362,54 @@ declare module '@solana/web3.js' { declare export function encodeData(type: InstructionType, fields: {}): Buffer; + // === src/message.js === + declare export type MessageHeader = { + numRequiredSignatures: number, + numReadonlySignedAccounts: number, + numReadonlyUnsignedAccounts: number, + }; + + declare export type CompiledInstruction = { + programIdIndex: number, + accounts: number[], + data: string, + }; + + declare export type MessageArgs = { + header: MessageHeader, + accountKeys: PublicKey[], + recentBlockhash: Blockhash, + instructions: CompiledInstruction[], + }; + + declare export class Message { + header: MessageHeader; + accountKeys: PublicKey[]; + recentBlockhash: Blockhash; + instructions: CompiledInstruction[]; + + constructor(args: MessageArgs): Message; + isAccountWritable(account: PublicKey): boolean; + serialize(): Buffer; + } + // === src/transaction.js === declare export type TransactionSignature = string; + declare export type AccountMeta = { + pubkey: PublicKey, + isSigner: boolean, + isWritable: boolean, + }; + declare type TransactionInstructionCtorFields = {| - keys: ?Array<{pubkey: PublicKey, isSigner: boolean, isWritable: boolean}>, + keys: ?Array, programId?: PublicKey, data?: Buffer, |}; declare export class TransactionInstruction { - keys: Array<{pubkey: PublicKey, isSigner: boolean, isWritable: boolean}>; + keys: Array; programId: PublicKey; data: Buffer; @@ -411,6 +448,7 @@ declare module '@solana/web3.js' { Transaction | TransactionInstruction | TransactionInstructionCtorFields, > ): Transaction; + compileMessage(): Message; serializeMessage(): Buffer; sign(...signers: Array): void; signPartial(...partialSigners: Array): void; diff --git a/web3.js/src/index.js b/web3.js/src/index.js index 2f8353c33b..90a9e763cd 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -4,6 +4,7 @@ export {BpfLoader} from './bpf-loader'; export {BudgetProgram} from './budget-program'; export {Connection} from './connection'; export {Loader} from './loader'; +export {Message} from './message'; export {NonceAccount, NONCE_ACCOUNT_LENGTH} from './nonce-account'; export {PublicKey} from './publickey'; export { diff --git a/web3.js/src/message.js b/web3.js/src/message.js new file mode 100644 index 0000000000..8c0fe7d7d7 --- /dev/null +++ b/web3.js/src/message.js @@ -0,0 +1,170 @@ +// @flow + +import bs58 from 'bs58'; +import * as BufferLayout from 'buffer-layout'; + +import {PublicKey} from './publickey'; +import type {Blockhash} from './blockhash'; +import * as Layout from './layout'; +import {PACKET_DATA_SIZE} from './transaction'; +import * as shortvec from './util/shortvec-encoding'; + +/** + * The message header, identifying signed and read-only account + * + * @typedef {Object} MessageHeader + * @property {number} numRequiredSignatures The number of signatures required for this message to be considered valid + * @property {number} numReadonlySignedAccounts: The last `numReadonlySignedAccounts` of the signed keys are read-only accounts + * @property {number} numReadonlyUnsignedAccounts The last `numReadonlySignedAccounts` of the unsigned keys are read-only accounts + */ +export type MessageHeader = { + numRequiredSignatures: number, + numReadonlySignedAccounts: number, + numReadonlyUnsignedAccounts: number, +}; + +/** + * An instruction to execute by a program + * + * @typedef {Object} CompiledInstruction + * @property {number} programIdIndex Index into the transaction keys array indicating the program account that executes this instruction + * @property {number[]} accounts Ordered indices into the transaction keys array indicating which accounts to pass to the program + * @property {string} data The program input data encoded as base 58 + */ +export type CompiledInstruction = { + programIdIndex: number, + accounts: number[], + data: string, +}; + +/** + * Message constructor arguments + * + * @typedef {Object} MessageArgs + * @property {MessageHeader} header The message header, identifying signed and read-only `accountKeys` + * @property {PublicKey[]} accounts All the account keys used by this transaction + * @property {Blockhash} recentBlockhash The hash of a recent ledger block + * @property {CompiledInstruction[]} instructions Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. + */ +type MessageArgs = { + header: MessageHeader, + accountKeys: PublicKey[], + recentBlockhash: Blockhash, + instructions: CompiledInstruction[], +}; + +/** + * List of instructions to be processed atomically + */ +export class Message { + header: MessageHeader; + accountKeys: PublicKey[]; + recentBlockhash: Blockhash; + instructions: CompiledInstruction[]; + + constructor(args: MessageArgs) { + this.header = args.header; + this.accountKeys = args.accountKeys; + this.recentBlockhash = args.recentBlockhash; + this.instructions = args.instructions; + } + + isAccountWritable(index: number): boolean { + return ( + index < + this.header.numRequiredSignatures - + this.header.numReadonlySignedAccounts || + (index >= this.header.numRequiredSignatures && + index < + this.accountKeys.length - this.header.numReadonlyUnsignedAccounts) + ); + } + + serialize(): Buffer { + const numKeys = this.accountKeys.length; + + let keyCount = []; + shortvec.encodeLength(keyCount, numKeys); + + const instructions = this.instructions.map(instruction => { + const {accounts, programIdIndex} = instruction; + const data = bs58.decode(instruction.data); + + let keyIndicesCount = []; + shortvec.encodeLength(keyIndicesCount, accounts.length); + + let dataCount = []; + shortvec.encodeLength(dataCount, data.length); + + return { + programIdIndex, + keyIndicesCount: Buffer.from(keyIndicesCount), + keyIndices: Buffer.from(accounts), + dataLength: Buffer.from(dataCount), + data, + }; + }); + + let instructionCount = []; + shortvec.encodeLength(instructionCount, instructions.length); + let instructionBuffer = Buffer.alloc(PACKET_DATA_SIZE); + Buffer.from(instructionCount).copy(instructionBuffer); + let instructionBufferLength = instructionCount.length; + + instructions.forEach(instruction => { + const instructionLayout = BufferLayout.struct([ + BufferLayout.u8('programIdIndex'), + + BufferLayout.blob( + instruction.keyIndicesCount.length, + 'keyIndicesCount', + ), + BufferLayout.seq( + BufferLayout.u8('keyIndex'), + instruction.keyIndices.length, + 'keyIndices', + ), + BufferLayout.blob(instruction.dataLength.length, 'dataLength'), + BufferLayout.seq( + BufferLayout.u8('userdatum'), + instruction.data.length, + 'data', + ), + ]); + const length = instructionLayout.encode( + instruction, + instructionBuffer, + instructionBufferLength, + ); + instructionBufferLength += length; + }); + instructionBuffer = instructionBuffer.slice(0, instructionBufferLength); + + const signDataLayout = BufferLayout.struct([ + BufferLayout.blob(1, 'numRequiredSignatures'), + BufferLayout.blob(1, 'numReadonlySignedAccounts'), + BufferLayout.blob(1, 'numReadonlyUnsignedAccounts'), + BufferLayout.blob(keyCount.length, 'keyCount'), + BufferLayout.seq(Layout.publicKey('key'), numKeys, 'keys'), + Layout.publicKey('recentBlockhash'), + ]); + + const transaction = { + numRequiredSignatures: Buffer.from([this.header.numRequiredSignatures]), + numReadonlySignedAccounts: Buffer.from([ + this.header.numReadonlySignedAccounts, + ]), + numReadonlyUnsignedAccounts: Buffer.from([ + this.header.numReadonlyUnsignedAccounts, + ]), + keyCount: Buffer.from(keyCount), + keys: this.accountKeys.map(key => key.toBuffer()), + recentBlockhash: bs58.decode(this.recentBlockhash), + }; + + let signData = Buffer.alloc(2048); + const length = signDataLayout.encode(transaction, signData); + instructionBuffer.copy(signData, length); + return signData.slice(0, length + instructionBuffer.length); + } +} diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 4762ff6412..dd12e5f222 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -1,11 +1,11 @@ // @flow import invariant from 'assert'; -import * as BufferLayout from 'buffer-layout'; import nacl from 'tweetnacl'; import bs58 from 'bs58'; -import * as Layout from './layout'; +import type {CompiledInstruction} from './message'; +import {Message} from './message'; import {PublicKey} from './publickey'; import {Account} from './account'; import * as shortvec from './util/shortvec-encoding'; @@ -35,6 +35,20 @@ export const PACKET_DATA_SIZE = 1280 - 40 - 8; const PUBKEY_LENGTH = 32; const SIGNATURE_LENGTH = 64; +/** + * Account metadata used to define instructions + * + * @typedef {Object} AccountMeta + * @property {PublicKey} pubkey An account's public key + * @property {boolean} isSigner True if an instruction requires a transaction signature matching `pubkey` + * @property {boolean} isWritable True if the `pubkey` can be loaded as a read-write account. + */ +export type AccountMeta = { + pubkey: PublicKey, + isSigner: boolean, + isWritable: boolean, +}; + /** * List of TransactionInstruction object fields that may be initialized at construction * @@ -44,7 +58,7 @@ const SIGNATURE_LENGTH = 64; * @property {?Buffer} data */ export type TransactionInstructionCtorFields = {| - keys?: Array<{pubkey: PublicKey, isSigner: boolean, isWritable: boolean}>, + keys?: Array, programId?: PublicKey, data?: Buffer, |}; @@ -57,11 +71,7 @@ export class TransactionInstruction { * Public keys to include in this transaction * Boolean represents whether this pubkey needs to sign the transaction */ - keys: Array<{ - pubkey: PublicKey, - isSigner: boolean, - isWritable: boolean, - }> = []; + keys: Array = []; /** * Program Id to execute @@ -90,8 +100,8 @@ type SignaturePubkeyPair = {| * List of Transaction object fields that may be initialized at construction * * @typedef {Object} TransactionCtorFields - * @property (?recentBlockhash} A recent block hash - * @property (?signatures} One or more signatures + * @property {?Blockhash} recentBlockhash A recent blockhash + * @property {?Array} signatures One or more signatures * */ type TransactionCtorFields = {| @@ -104,8 +114,8 @@ type TransactionCtorFields = {| * NonceInformation to be used to build a Transaction. * * @typedef {Object} NonceInformation - * @property {nonce} The current Nonce blockhash - * @property {nonceInstruction} The AdvanceNonceAccount Instruction + * @property {Blockhash} nonce The current Nonce blockhash + * @property {TransactionInstruction} nonceInstruction AdvanceNonceAccount Instruction */ type NonceInformation = {| nonce: Blockhash, @@ -180,9 +190,9 @@ export class Transaction { } /** - * Get a buffer of the Transaction data that need to be covered by signatures + * Compile transaction data */ - serializeMessage(): Buffer { + compileMessage(): Message { const {nonceInfo} = this; if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) { this.recentBlockhash = nonceInfo.nonce; @@ -197,16 +207,15 @@ export class Transaction { throw new Error('No instructions provided'); } - const keys = this.signatures.map(({publicKey}) => publicKey.toString()); let numReadonlySignedAccounts = 0; let numReadonlyUnsignedAccounts = 0; - const programIds = []; - - const allKeys = []; + const keys = this.signatures.map(({publicKey}) => publicKey.toString()); + const programIds: string[] = []; + const accountMetas: AccountMeta[] = []; this.instructions.forEach(instruction => { - instruction.keys.forEach(keySignerPair => { - allKeys.push(keySignerPair); + instruction.keys.forEach(accountMeta => { + accountMetas.push(accountMeta); }); const programId = instruction.programId.toString(); @@ -215,30 +224,28 @@ export class Transaction { } }); - allKeys.sort(function (x, y) { + accountMetas.sort(function (x, y) { const checkSigner = x.isSigner === y.isSigner ? 0 : x.isSigner ? -1 : 1; const checkWritable = x.isWritable === y.isWritable ? 0 : x.isWritable ? -1 : 1; return checkSigner || checkWritable; }); - allKeys.forEach(keySignerPair => { - const keyStr = keySignerPair.pubkey.toString(); + accountMetas.forEach(({pubkey, isSigner, isWritable}) => { + const keyStr = pubkey.toString(); if (!keys.includes(keyStr)) { - if (keySignerPair.isSigner) { + keys.push(keyStr); + if (isSigner) { this.signatures.push({ signature: null, - publicKey: keySignerPair.pubkey, + publicKey: pubkey, }); - if (!keySignerPair.isWritable) { + if (!isWritable) { numReadonlySignedAccounts += 1; } - } else { - if (!keySignerPair.isWritable) { - numReadonlyUnsignedAccounts += 1; - } + } else if (!isWritable) { + numReadonlyUnsignedAccounts += 1; } - keys.push(keyStr); } }); @@ -249,92 +256,41 @@ export class Transaction { } }); - let keyCount = []; - shortvec.encodeLength(keyCount, keys.length); - - const instructions = this.instructions.map(instruction => { - const {data, programId} = instruction; - let keyIndicesCount = []; - shortvec.encodeLength(keyIndicesCount, instruction.keys.length); - let dataCount = []; - shortvec.encodeLength(dataCount, instruction.data.length); - return { - programIdIndex: keys.indexOf(programId.toString()), - keyIndicesCount: Buffer.from(keyIndicesCount), - keyIndices: Buffer.from( - instruction.keys.map(keyObj => + const instructions: CompiledInstruction[] = this.instructions.map( + instruction => { + const {data, programId} = instruction; + return { + programIdIndex: keys.indexOf(programId.toString()), + accounts: instruction.keys.map(keyObj => keys.indexOf(keyObj.pubkey.toString()), ), - ), - dataLength: Buffer.from(dataCount), - data, - }; - }); + data: bs58.encode(data), + }; + }, + ); instructions.forEach(instruction => { invariant(instruction.programIdIndex >= 0); - instruction.keyIndices.forEach(keyIndex => invariant(keyIndex >= 0)); + instruction.accounts.forEach(keyIndex => invariant(keyIndex >= 0)); }); - let instructionCount = []; - shortvec.encodeLength(instructionCount, instructions.length); - let instructionBuffer = Buffer.alloc(PACKET_DATA_SIZE); - Buffer.from(instructionCount).copy(instructionBuffer); - let instructionBufferLength = instructionCount.length; - - instructions.forEach(instruction => { - const instructionLayout = BufferLayout.struct([ - BufferLayout.u8('programIdIndex'), - - BufferLayout.blob( - instruction.keyIndicesCount.length, - 'keyIndicesCount', - ), - BufferLayout.seq( - BufferLayout.u8('keyIndex'), - instruction.keyIndices.length, - 'keyIndices', - ), - BufferLayout.blob(instruction.dataLength.length, 'dataLength'), - BufferLayout.seq( - BufferLayout.u8('userdatum'), - instruction.data.length, - 'data', - ), - ]); - const length = instructionLayout.encode( - instruction, - instructionBuffer, - instructionBufferLength, - ); - instructionBufferLength += length; + return new Message({ + header: { + numRequiredSignatures: this.signatures.length, + numReadonlySignedAccounts, + numReadonlyUnsignedAccounts, + }, + accountKeys: keys.map(k => new PublicKey(k)), + recentBlockhash, + instructions, }); - instructionBuffer = instructionBuffer.slice(0, instructionBufferLength); + } - const signDataLayout = BufferLayout.struct([ - BufferLayout.blob(1, 'numRequiredSignatures'), - BufferLayout.blob(1, 'numReadonlySignedAccounts'), - BufferLayout.blob(1, 'numReadonlyUnsignedAccounts'), - BufferLayout.blob(keyCount.length, 'keyCount'), - BufferLayout.seq(Layout.publicKey('key'), keys.length, 'keys'), - Layout.publicKey('recentBlockhash'), - ]); - - const transaction = { - numRequiredSignatures: Buffer.from([this.signatures.length]), - numReadonlySignedAccounts: Buffer.from([numReadonlySignedAccounts]), - numReadonlyUnsignedAccounts: Buffer.from([numReadonlyUnsignedAccounts]), - keyCount: Buffer.from(keyCount), - keys: keys.map(key => new PublicKey(key).toBuffer()), - recentBlockhash: Buffer.from(bs58.decode(recentBlockhash)), - }; - - let signData = Buffer.alloc(2048); - const length = signDataLayout.encode(transaction, signData); - instructionBuffer.copy(signData, length); - signData = signData.slice(0, length + instructionBuffer.length); - - return signData; + /** + * Get a buffer of the Transaction data that need to be covered by signatures + */ + serializeMessage(): Buffer { + return this.compileMessage().serialize(); } /** @@ -517,11 +473,8 @@ export class Transaction { } const numRequiredSignatures = byteArray.shift(); - // byteArray = byteArray.slice(1); // Skip numRequiredSignatures byte const numReadonlySignedAccounts = byteArray.shift(); - // byteArray = byteArray.slice(1); // Skip numReadonlySignedAccounts byte const numReadonlyUnsignedAccounts = byteArray.shift(); - // byteArray = byteArray.slice(1); // Skip numReadonlyUnsignedAccounts byte const accountCount = shortvec.decodeLength(byteArray); let accounts = []; @@ -549,15 +502,18 @@ export class Transaction { instructions.push(instruction); } - return Transaction._populate( - signatures, - accounts, + const message = { + header: { + numRequiredSignatures, + numReadonlySignedAccounts, + numReadonlyUnsignedAccounts, + }, + recentBlockhash: bs58.encode(Buffer.from(recentBlockhash)), + accountKeys: accounts.map(account => new PublicKey(account)), instructions, - recentBlockhash, - numRequiredSignatures, - numReadonlySignedAccounts, - numReadonlyUnsignedAccounts, - ); + }; + + return Transaction._populate(signatures, new Message(message)); } /** @@ -574,82 +530,60 @@ export class Transaction { rpcResult.message.header.numReadonlySignedAccounts; const numReadonlyUnsignedAccounts = rpcResult.message.header.numReadonlyUnsignedAccounts; - return Transaction._populate( - signatures, - accounts, + + const message = { + header: { + numRequiredSignatures, + numReadonlySignedAccounts, + numReadonlyUnsignedAccounts, + }, + recentBlockhash: bs58.encode(Buffer.from(recentBlockhash)), + accountKeys: accounts.map(account => new PublicKey(account)), instructions, - recentBlockhash, - numRequiredSignatures, - numReadonlySignedAccounts, - numReadonlyUnsignedAccounts, - ); + }; + + return Transaction._populate(signatures, new Message(message)); } /** * Populate Transaction object * @private */ - static _populate( - signatures: Array, - accounts: Array, - instructions: Array, - recentBlockhash: Array, - numRequiredSignatures: number, - numReadonlySignedAccounts: number, - numReadonlyUnsignedAccounts: number, - ): Transaction { - function isWritable( - i: number, - numRequiredSignatures: number, - numReadonlySignedAccounts: number, - numReadonlyUnsignedAccounts: number, - numKeys: number, - ): boolean { - return ( - i < numRequiredSignatures - numReadonlySignedAccounts || - (i >= numRequiredSignatures && - i < numKeys - numReadonlyUnsignedAccounts) - ); - } - + static _populate(signatures: Array, message: Message): Transaction { const transaction = new Transaction(); - transaction.recentBlockhash = new PublicKey(recentBlockhash).toBase58(); - for (let i = 0; i < signatures.length; i++) { + transaction.recentBlockhash = message.recentBlockhash; + signatures.forEach((signature, index) => { const sigPubkeyPair = { signature: - signatures[i] == bs58.encode(DEFAULT_SIGNATURE) + signature == bs58.encode(DEFAULT_SIGNATURE) ? null - : bs58.decode(signatures[i]), - publicKey: new PublicKey(accounts[i]), + : bs58.decode(signature), + publicKey: message.accountKeys[index], }; transaction.signatures.push(sigPubkeyPair); - } - for (let i = 0; i < instructions.length; i++) { - let instructionData = { - keys: [], - programId: new PublicKey(accounts[instructions[i].programIdIndex]), - data: bs58.decode(instructions[i].data), - }; - for (let j = 0; j < instructions[i].accounts.length; j++) { - const pubkey = new PublicKey(accounts[instructions[i].accounts[j]]); + }); - instructionData.keys.push({ + message.instructions.forEach(instruction => { + const keys = instruction.accounts.map(account => { + const pubkey = message.accountKeys[account]; + return { pubkey, isSigner: transaction.signatures.some( keyObj => keyObj.publicKey.toString() === pubkey.toString(), ), - isWritable: isWritable( - j, - numRequiredSignatures, - numReadonlySignedAccounts, - numReadonlyUnsignedAccounts, - accounts.length, - ), - }); - } - let instruction = new TransactionInstruction(instructionData); - transaction.instructions.push(instruction); - } + isWritable: message.isAccountWritable(account), + }; + }); + + transaction.instructions.push( + new TransactionInstruction({ + keys, + programId: message.accountKeys[instruction.programIdIndex], + data: bs58.decode(instruction.data), + }), + ); + }); + return transaction; } }