Skip to content

Commit

Permalink
feat: add TransactionMessage class (#27526)
Browse files Browse the repository at this point in the history
feat: implement message v0 decompilation
  • Loading branch information
jstarry committed Sep 7, 2022
1 parent e7ceb19 commit f255bc5
Show file tree
Hide file tree
Showing 7 changed files with 593 additions and 3 deletions.
36 changes: 34 additions & 2 deletions src/message/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
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
Expand All @@ -37,13 +40,19 @@ export type MessageArgs = {
/** The message header, identifying signed and read-only `accountKeys` */
header: MessageHeader;
/** All the account keys used by this transaction */
accountKeys: string[];
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
*/
Expand Down Expand Up @@ -93,6 +102,29 @@ export class Message {
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;
}
Expand Down Expand Up @@ -250,7 +282,7 @@ export class Message {
for (let i = 0; i < accountCount; i++) {
const account = byteArray.slice(0, PUBLIC_KEY_LENGTH);
byteArray = byteArray.slice(PUBLIC_KEY_LENGTH);
accountKeys.push(bs58.encode(Buffer.from(account)));
accountKeys.push(new PublicKey(Buffer.from(account)));
}

const recentBlockhash = byteArray.slice(0, PUBLIC_KEY_LENGTH);
Expand Down
90 changes: 90 additions & 0 deletions src/message/v0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export type CompileV0Args = {
addressLookupTableAccounts?: Array<AddressLookupTableAccount>;
};

export type GetAccountKeysArgs =
| {
accountKeysFromLookups: AccountKeysFromLookups;
}
| {
addressLookupTableAccounts: AddressLookupTableAccount[];
};

export class MessageV0 {
header: MessageHeader;
staticAccountKeys: Array<PublicKey>;
Expand All @@ -59,6 +67,88 @@ export class MessageV0 {
return 0;
}

get numAccountKeysFromLookups(): number {
let count = 0;
for (const lookup of this.addressTableLookups) {
count += lookup.readonlyIndexes.length + lookup.writableIndexes.length;
}
return count;
}

getAccountKeys(args?: GetAccountKeysArgs): MessageAccountKeys {
let accountKeysFromLookups: AccountKeysFromLookups | undefined;
if (args && 'accountKeysFromLookups' in args) {
if (
this.numAccountKeysFromLookups !=
args.accountKeysFromLookups.writable.length +
args.accountKeysFromLookups.readonly.length
) {
throw new Error(
'Failed to get account keys because of a mismatch in the number of account keys from lookups',
);
}
accountKeysFromLookups = args.accountKeysFromLookups;
} else if (args && 'addressLookupTableAccounts' in args) {
accountKeysFromLookups = this.resolveAddressTableLookups(
args.addressLookupTableAccounts,
);
} else if (this.addressTableLookups.length > 0) {
throw new Error(
'Failed to get account keys because address table lookups were not resolved',
);
}
return new MessageAccountKeys(
this.staticAccountKeys,
accountKeysFromLookups,
);
}

resolveAddressTableLookups(
addressLookupTableAccounts: AddressLookupTableAccount[],
): AccountKeysFromLookups {
const accountKeysFromLookups: AccountKeysFromLookups = {
writable: [],
readonly: [],
};

for (const tableLookup of this.addressTableLookups) {
const tableAccount = addressLookupTableAccounts.find(account =>
account.key.equals(tableLookup.accountKey),
);
if (!tableAccount) {
throw new Error(
`Failed to find address lookup table account for table key ${tableLookup.accountKey.toBase58()}`,
);
}

for (const index of tableLookup.writableIndexes) {
if (index < tableAccount.state.addresses.length) {
accountKeysFromLookups.writable.push(
tableAccount.state.addresses[index],
);
} else {
throw new Error(
`Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`,
);
}
}

for (const index of tableLookup.readonlyIndexes) {
if (index < tableAccount.state.addresses.length) {
accountKeysFromLookups.readonly.push(
tableAccount.state.addresses[index],
);
} else {
throw new Error(
`Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`,
);
}
}
}

return accountKeysFromLookups;
}

static compile(args: CompileV0Args): MessageV0 {
const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey);

Expand Down
1 change: 1 addition & 0 deletions src/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './constants';
export * from './expiry-custom-errors';
export * from './legacy';
export * from './message';
export * from './versioned';
147 changes: 147 additions & 0 deletions src/transaction/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
AccountKeysFromLookups,
MessageAccountKeys,
} from '../message/account-keys';
import assert from '../utils/assert';
import {toBuffer} from '../utils/to-buffer';
import {Blockhash} from '../blockhash';
import {Message, MessageV0, VersionedMessage} from '../message';
import {AddressLookupTableAccount} from '../programs';
import {AccountMeta, TransactionInstruction} from './legacy';

export type TransactionMessageArgs = {
accountKeys: MessageAccountKeys;
instructions: Array<TransactionInstruction>;
recentBlockhash: Blockhash;
};

export type DecompileArgs =
| {
accountKeysFromLookups: AccountKeysFromLookups;
}
| {
addressLookupTableAccounts: AddressLookupTableAccount[];
};

export class TransactionMessage {
accountKeys: MessageAccountKeys;
instructions: Array<TransactionInstruction>;
recentBlockhash: Blockhash;

constructor(args: TransactionMessageArgs) {
this.accountKeys = args.accountKeys;
this.instructions = args.instructions;
this.recentBlockhash = args.recentBlockhash;
}

static decompile(
message: VersionedMessage,
args?: DecompileArgs,
): TransactionMessage {
const {header, compiledInstructions, recentBlockhash} = message;

const {
numRequiredSignatures,
numReadonlySignedAccounts,
numReadonlyUnsignedAccounts,
} = header;

const numWritableSignedAccounts =
numRequiredSignatures - numReadonlySignedAccounts;
assert(numWritableSignedAccounts > 0, 'Message header is invalid');

const numWritableUnsignedAccounts =
message.staticAccountKeys.length - numReadonlyUnsignedAccounts;
assert(numWritableUnsignedAccounts >= 0, 'Message header is invalid');

const accountKeys = message.getAccountKeys(args);
const instructions: TransactionInstruction[] = [];
for (const compiledIx of compiledInstructions) {
const keys: AccountMeta[] = [];

for (const keyIndex of compiledIx.accountKeyIndexes) {
const pubkey = accountKeys.get(keyIndex);
if (pubkey === undefined) {
throw new Error(
`Failed to find key for account key index ${keyIndex}`,
);
}

const isSigner = keyIndex < numRequiredSignatures;

let isWritable;
if (isSigner) {
isWritable = keyIndex < numWritableSignedAccounts;
} else if (keyIndex < accountKeys.staticAccountKeys.length) {
isWritable =
keyIndex - numRequiredSignatures < numWritableUnsignedAccounts;
} else {
isWritable =
keyIndex - accountKeys.staticAccountKeys.length <
// accountKeysFromLookups cannot be undefined because we already found a pubkey for this index above
accountKeys.accountKeysFromLookups!.writable.length;
}

keys.push({
pubkey,
isSigner: keyIndex < header.numRequiredSignatures,
isWritable,
});
}

const programId = accountKeys.get(compiledIx.programIdIndex);
if (programId === undefined) {
throw new Error(
`Failed to find program id for program id index ${compiledIx.programIdIndex}`,
);
}

instructions.push(
new TransactionInstruction({
programId,
data: toBuffer(compiledIx.data),
keys,
}),
);
}

return new TransactionMessage({
accountKeys,
instructions,
recentBlockhash,
});
}

compileToLegacyMessage(): Message {
const payerKey = this.accountKeys.get(0);
if (payerKey === undefined) {
throw new Error(
'Failed to compile message because no account keys were found',
);
}

return Message.compile({
payerKey,
recentBlockhash: this.recentBlockhash,
instructions: this.instructions,
});
}

compileToV0Message(
addressLookupTableAccounts?: AddressLookupTableAccount[],
): MessageV0 {
const payerKey = this.accountKeys.get(0);
if (payerKey === undefined) {
throw new Error(
'Failed to compile message because no account keys were found',
);
}

return MessageV0.compile({
payerKey,
recentBlockhash: this.recentBlockhash,
instructions: this.instructions,
addressLookupTableAccounts,
});
}
}

0 comments on commit f255bc5

Please sign in to comment.