Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

feat: add TransactionMessage class #27526

Merged
merged 1 commit into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions web3.js/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 web3.js/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 web3.js/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 web3.js/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,
});
}
}
Loading