-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement message decompilation
- Loading branch information
Showing
2 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TransactionInstruction>; | ||
recentBlockhash: Blockhash; | ||
}; | ||
|
||
export type DecompileArgs = | ||
| { | ||
message: VersionedMessage; | ||
} | ||
| { | ||
message: MessageV0; | ||
loadedAddresses: LoadedAddresses; | ||
} | ||
| { | ||
message: MessageV0; | ||
addressLookupTableAccounts: AddressLookupTableAccount[]; | ||
}; | ||
|
||
export class TransactionMessage { | ||
payerKey: PublicKey; | ||
instructions: Array<TransactionInstruction>; | ||
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, | ||
}); | ||
} | ||
} |