diff --git a/web3.js/src/transaction/index.ts b/web3.js/src/transaction/index.ts index 2f5c19cb2510a5..88d1cb1700ea62 100644 --- a/web3.js/src/transaction/index.ts +++ b/web3.js/src/transaction/index.ts @@ -1,4 +1,5 @@ export * from './constants'; export * from './expiry-custom-errors'; export * from './legacy'; +export * from './message'; export * from './versioned'; diff --git a/web3.js/src/transaction/message.ts b/web3.js/src/transaction/message.ts new file mode 100644 index 00000000000000..f6b761d3c385f9 --- /dev/null +++ b/web3.js/src/transaction/message.ts @@ -0,0 +1,180 @@ +import {LoadedAddresses} from '../connection'; +import {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 {PublicKey} from '../publickey'; +import {AccountMeta, TransactionInstruction} from './legacy'; + +export type TransactionMessageArgs = { + payerKey: PublicKey; + instructions: Array; + recentBlockhash: Blockhash; +}; + +export type DecompileArgs = + | { + message: VersionedMessage; + } + | { + message: MessageV0; + loadedAddresses: LoadedAddresses; + } + | { + message: MessageV0; + addressLookupTableAccounts: AddressLookupTableAccount[]; + }; + +export class TransactionMessage { + payerKey: PublicKey; + instructions: Array; + recentBlockhash: Blockhash; + + constructor(args: TransactionMessageArgs) { + this.payerKey = args.payerKey; + this.instructions = args.instructions; + this.recentBlockhash = args.recentBlockhash; + } + + static decompile(args: DecompileArgs): TransactionMessage { + const message = args.message; + + let loadedAddresses: LoadedAddresses | undefined; + if ('loadedAddresses' in args) { + loadedAddresses = args.loadedAddresses; + } else if ('addressLookupTableAccounts' in args) { + loadedAddresses = { + writable: [], + readonly: [], + }; + + for (const tableLookup of message.addressTableLookups) { + const tableAccount = args.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) { + const address = tableAccount.state.addresses.at(index); + if (address === undefined) { + throw new Error( + `Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`, + ); + } + loadedAddresses.writable.push(address); + } + + for (const index of tableLookup.readonlyIndexes) { + const address = tableAccount.state.addresses.at(index); + if (address === undefined) { + throw new Error( + `Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`, + ); + } + loadedAddresses.readonly.push(address); + } + } + } + + const accountKeys = new MessageAccountKeys( + message.staticAccountKeys, + loadedAddresses, + ); + const {header, compiledInstructions, recentBlockhash} = message; + + const { + numRequiredSignatures, + numReadonlySignedAccounts, + numReadonlyUnsignedAccounts, + } = header; + + const numWritableSignedAccounts = + numRequiredSignatures - numReadonlySignedAccounts; + assert(numWritableSignedAccounts > 0, 'Message header is invalid'); + + const numWritableUnsignedAccounts = + accountKeys.staticAccountKeys.length - numReadonlyUnsignedAccounts; + assert(numWritableUnsignedAccounts >= 0, 'Message header is invalid'); + + 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 < + // loadedAddresses cannot be undefined because we already found a pubkey for this index above + accountKeys.loadedAddresses!.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({ + payerKey: accountKeys.staticAccountKeys[0], + instructions, + recentBlockhash, + }); + } + + compileToLegacyMessage(): Message { + return Message.compile({ + payerKey: this.payerKey, + recentBlockhash: this.recentBlockhash, + instructions: this.instructions, + }); + } + + compileToV0Message( + addressLookupTableAccounts?: AddressLookupTableAccount[], + ): MessageV0 { + return MessageV0.compile({ + payerKey: this.payerKey, + recentBlockhash: this.recentBlockhash, + instructions: this.instructions, + addressLookupTableAccounts, + }); + } +}