feat: expose transaction message publicly

This commit is contained in:
Justin Starry 2020-05-01 00:05:29 +08:00 committed by Michael Vines
parent cd439bed0d
commit 14a41bc47b
5 changed files with 362 additions and 185 deletions

46
web3.js/module.d.ts vendored
View File

@ -352,21 +352,54 @@ declare module '@solana/web3.js' {
fields: Record<string, object>,
): 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<AccountMeta>;
programId?: PublicKey;
data?: Buffer;
};
export class TransactionInstruction {
keys: Array<{
pubkey: PublicKey;
isSigner: boolean;
isWritable: boolean;
}>;
keys: Array<AccountMeta>;
programId: PublicKey;
data: Buffer;
@ -403,6 +436,7 @@ declare module '@solana/web3.js' {
Transaction | TransactionInstruction | TransactionInstructionCtorFields
>
): Transaction;
compileMessage(): Message;
serializeMessage(): Buffer;
sign(...signers: Array<Account>): void;
signPartial(...partialSigners: Array<PublicKey | Account>): void;

View File

@ -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<AccountMeta>,
programId?: PublicKey,
data?: Buffer,
|};
declare export class TransactionInstruction {
keys: Array<{pubkey: PublicKey, isSigner: boolean, isWritable: boolean}>;
keys: Array<AccountMeta>;
programId: PublicKey;
data: Buffer;
@ -411,6 +448,7 @@ declare module '@solana/web3.js' {
Transaction | TransactionInstruction | TransactionInstructionCtorFields,
>
): Transaction;
compileMessage(): Message;
serializeMessage(): Buffer;
sign(...signers: Array<Account>): void;
signPartial(...partialSigners: Array<PublicKey | Account>): void;

View File

@ -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 {

170
web3.js/src/message.js Normal file
View File

@ -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);
}
}

View File

@ -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<AccountMeta>,
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<AccountMeta> = [];
/**
* 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<SignaturePubkeyPair>} 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<string>,
accounts: Array<string>,
instructions: Array<any>,
recentBlockhash: Array<number>,
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<string>, 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;
}
}