323 lines
9.9 KiB
TypeScript
323 lines
9.9 KiB
TypeScript
import bs58 from 'bs58';
|
|
import {Buffer} from 'buffer';
|
|
import * as BufferLayout from '@solana/buffer-layout';
|
|
|
|
import {PublicKey, PUBLIC_KEY_LENGTH} from '../publickey';
|
|
import type {Blockhash} from '../blockhash';
|
|
import * as Layout from '../layout';
|
|
import {PACKET_DATA_SIZE, VERSION_PREFIX_MASK} from '../transaction/constants';
|
|
import * as shortvec from '../utils/shortvec-encoding';
|
|
import {toBuffer} from '../utils/to-buffer';
|
|
import {
|
|
MessageHeader,
|
|
MessageAddressTableLookup,
|
|
MessageCompiledInstruction,
|
|
} from './index';
|
|
import {TransactionInstruction} from '../transaction';
|
|
import {CompiledKeys} from './compiled-keys';
|
|
import {MessageAccountKeys} from './account-keys';
|
|
|
|
/**
|
|
* An instruction to execute by a program
|
|
*
|
|
* @property {number} programIdIndex
|
|
* @property {number[]} accounts
|
|
* @property {string} data
|
|
*/
|
|
export type CompiledInstruction = {
|
|
/** Index into the transaction keys array indicating the program account that executes this instruction */
|
|
programIdIndex: number;
|
|
/** Ordered indices into the transaction keys array indicating which accounts to pass to the program */
|
|
accounts: number[];
|
|
/** The program input data encoded as base 58 */
|
|
data: string;
|
|
};
|
|
|
|
/**
|
|
* Message constructor arguments
|
|
*/
|
|
export type MessageArgs = {
|
|
/** The message header, identifying signed and read-only `accountKeys` */
|
|
header: MessageHeader;
|
|
/** All the account keys used by this transaction */
|
|
accountKeys: string[] | PublicKey[];
|
|
/** The hash of a recent ledger block */
|
|
recentBlockhash: Blockhash;
|
|
/** Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. */
|
|
instructions: CompiledInstruction[];
|
|
};
|
|
|
|
export type CompileLegacyArgs = {
|
|
payerKey: PublicKey;
|
|
instructions: Array<TransactionInstruction>;
|
|
recentBlockhash: Blockhash;
|
|
};
|
|
|
|
/**
|
|
* List of instructions to be processed atomically
|
|
*/
|
|
export class Message {
|
|
header: MessageHeader;
|
|
accountKeys: PublicKey[];
|
|
recentBlockhash: Blockhash;
|
|
instructions: CompiledInstruction[];
|
|
|
|
private indexToProgramIds: Map<number, PublicKey> = new Map<
|
|
number,
|
|
PublicKey
|
|
>();
|
|
|
|
constructor(args: MessageArgs) {
|
|
this.header = args.header;
|
|
this.accountKeys = args.accountKeys.map(account => new PublicKey(account));
|
|
this.recentBlockhash = args.recentBlockhash;
|
|
this.instructions = args.instructions;
|
|
this.instructions.forEach(ix =>
|
|
this.indexToProgramIds.set(
|
|
ix.programIdIndex,
|
|
this.accountKeys[ix.programIdIndex],
|
|
),
|
|
);
|
|
}
|
|
|
|
get version(): 'legacy' {
|
|
return 'legacy';
|
|
}
|
|
|
|
get staticAccountKeys(): Array<PublicKey> {
|
|
return this.accountKeys;
|
|
}
|
|
|
|
get compiledInstructions(): Array<MessageCompiledInstruction> {
|
|
return this.instructions.map(
|
|
(ix): MessageCompiledInstruction => ({
|
|
programIdIndex: ix.programIdIndex,
|
|
accountKeyIndexes: ix.accounts,
|
|
data: bs58.decode(ix.data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
get addressTableLookups(): Array<MessageAddressTableLookup> {
|
|
return [];
|
|
}
|
|
|
|
getAccountKeys(): MessageAccountKeys {
|
|
return new MessageAccountKeys(this.staticAccountKeys);
|
|
}
|
|
|
|
static compile(args: CompileLegacyArgs): Message {
|
|
const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey);
|
|
const [header, staticAccountKeys] = compiledKeys.getMessageComponents();
|
|
const accountKeys = new MessageAccountKeys(staticAccountKeys);
|
|
const instructions = accountKeys.compileInstructions(args.instructions).map(
|
|
(ix: MessageCompiledInstruction): CompiledInstruction => ({
|
|
programIdIndex: ix.programIdIndex,
|
|
accounts: ix.accountKeyIndexes,
|
|
data: bs58.encode(ix.data),
|
|
}),
|
|
);
|
|
return new Message({
|
|
header,
|
|
accountKeys: staticAccountKeys,
|
|
recentBlockhash: args.recentBlockhash,
|
|
instructions,
|
|
});
|
|
}
|
|
|
|
isAccountSigner(index: number): boolean {
|
|
return index < this.header.numRequiredSignatures;
|
|
}
|
|
|
|
isAccountWritable(index: number): boolean {
|
|
return (
|
|
index <
|
|
this.header.numRequiredSignatures -
|
|
this.header.numReadonlySignedAccounts ||
|
|
(index >= this.header.numRequiredSignatures &&
|
|
index <
|
|
this.accountKeys.length - this.header.numReadonlyUnsignedAccounts)
|
|
);
|
|
}
|
|
|
|
isProgramId(index: number): boolean {
|
|
return this.indexToProgramIds.has(index);
|
|
}
|
|
|
|
programIds(): PublicKey[] {
|
|
return [...this.indexToProgramIds.values()];
|
|
}
|
|
|
|
nonProgramIds(): PublicKey[] {
|
|
return this.accountKeys.filter((_, index) => !this.isProgramId(index));
|
|
}
|
|
|
|
serialize(): Buffer {
|
|
const numKeys = this.accountKeys.length;
|
|
|
|
let keyCount: number[] = [];
|
|
shortvec.encodeLength(keyCount, numKeys);
|
|
|
|
const instructions = this.instructions.map(instruction => {
|
|
const {accounts, programIdIndex} = instruction;
|
|
const data = Array.from(bs58.decode(instruction.data));
|
|
|
|
let keyIndicesCount: number[] = [];
|
|
shortvec.encodeLength(keyIndicesCount, accounts.length);
|
|
|
|
let dataCount: number[] = [];
|
|
shortvec.encodeLength(dataCount, data.length);
|
|
|
|
return {
|
|
programIdIndex,
|
|
keyIndicesCount: Buffer.from(keyIndicesCount),
|
|
keyIndices: accounts,
|
|
dataLength: Buffer.from(dataCount),
|
|
data,
|
|
};
|
|
});
|
|
|
|
let instructionCount: number[] = [];
|
|
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<
|
|
Readonly<{
|
|
data: number[];
|
|
dataLength: Uint8Array;
|
|
keyIndices: number[];
|
|
keyIndicesCount: Uint8Array;
|
|
programIdIndex: number;
|
|
}>
|
|
>([
|
|
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<
|
|
Readonly<{
|
|
keyCount: Uint8Array;
|
|
keys: Uint8Array[];
|
|
numReadonlySignedAccounts: Uint8Array;
|
|
numReadonlyUnsignedAccounts: Uint8Array;
|
|
numRequiredSignatures: Uint8Array;
|
|
recentBlockhash: Uint8Array;
|
|
}>
|
|
>([
|
|
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 => toBuffer(key.toBytes())),
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Decode a compiled message into a Message object.
|
|
*/
|
|
static from(buffer: Buffer | Uint8Array | Array<number>): Message {
|
|
// Slice up wire data
|
|
let byteArray = [...buffer];
|
|
|
|
const numRequiredSignatures = byteArray.shift() as number;
|
|
if (
|
|
numRequiredSignatures !==
|
|
(numRequiredSignatures & VERSION_PREFIX_MASK)
|
|
) {
|
|
throw new Error(
|
|
'Versioned messages must be deserialized with VersionedMessage.deserialize()',
|
|
);
|
|
}
|
|
|
|
const numReadonlySignedAccounts = byteArray.shift() as number;
|
|
const numReadonlyUnsignedAccounts = byteArray.shift() as number;
|
|
|
|
const accountCount = shortvec.decodeLength(byteArray);
|
|
let accountKeys = [];
|
|
for (let i = 0; i < accountCount; i++) {
|
|
const account = byteArray.slice(0, PUBLIC_KEY_LENGTH);
|
|
byteArray = byteArray.slice(PUBLIC_KEY_LENGTH);
|
|
accountKeys.push(new PublicKey(Buffer.from(account)));
|
|
}
|
|
|
|
const recentBlockhash = byteArray.slice(0, PUBLIC_KEY_LENGTH);
|
|
byteArray = byteArray.slice(PUBLIC_KEY_LENGTH);
|
|
|
|
const instructionCount = shortvec.decodeLength(byteArray);
|
|
let instructions: CompiledInstruction[] = [];
|
|
for (let i = 0; i < instructionCount; i++) {
|
|
const programIdIndex = byteArray.shift() as number;
|
|
const accountCount = shortvec.decodeLength(byteArray);
|
|
const accounts = byteArray.slice(0, accountCount);
|
|
byteArray = byteArray.slice(accountCount);
|
|
const dataLength = shortvec.decodeLength(byteArray);
|
|
const dataSlice = byteArray.slice(0, dataLength);
|
|
const data = bs58.encode(Buffer.from(dataSlice));
|
|
byteArray = byteArray.slice(dataLength);
|
|
instructions.push({
|
|
programIdIndex,
|
|
accounts,
|
|
data,
|
|
});
|
|
}
|
|
|
|
const messageArgs = {
|
|
header: {
|
|
numRequiredSignatures,
|
|
numReadonlySignedAccounts,
|
|
numReadonlyUnsignedAccounts,
|
|
},
|
|
recentBlockhash: bs58.encode(Buffer.from(recentBlockhash)),
|
|
accountKeys,
|
|
instructions,
|
|
};
|
|
|
|
return new Message(messageArgs);
|
|
}
|
|
}
|