feat: add `TransactionMessage` class (#27526)
feat: implement message v0 decompilation
This commit is contained in:
parent
3374f41201
commit
9f81d27db8
|
@ -13,6 +13,9 @@ import {
|
||||||
MessageAddressTableLookup,
|
MessageAddressTableLookup,
|
||||||
MessageCompiledInstruction,
|
MessageCompiledInstruction,
|
||||||
} from './index';
|
} from './index';
|
||||||
|
import {TransactionInstruction} from '../transaction';
|
||||||
|
import {CompiledKeys} from './compiled-keys';
|
||||||
|
import {MessageAccountKeys} from './account-keys';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An instruction to execute by a program
|
* An instruction to execute by a program
|
||||||
|
@ -37,13 +40,19 @@ export type MessageArgs = {
|
||||||
/** The message header, identifying signed and read-only `accountKeys` */
|
/** The message header, identifying signed and read-only `accountKeys` */
|
||||||
header: MessageHeader;
|
header: MessageHeader;
|
||||||
/** All the account keys used by this transaction */
|
/** All the account keys used by this transaction */
|
||||||
accountKeys: string[];
|
accountKeys: string[] | PublicKey[];
|
||||||
/** The hash of a recent ledger block */
|
/** The hash of a recent ledger block */
|
||||||
recentBlockhash: Blockhash;
|
recentBlockhash: Blockhash;
|
||||||
/** Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. */
|
/** Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. */
|
||||||
instructions: CompiledInstruction[];
|
instructions: CompiledInstruction[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CompileLegacyArgs = {
|
||||||
|
payerKey: PublicKey;
|
||||||
|
instructions: Array<TransactionInstruction>;
|
||||||
|
recentBlockhash: Blockhash;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of instructions to be processed atomically
|
* List of instructions to be processed atomically
|
||||||
*/
|
*/
|
||||||
|
@ -93,6 +102,29 @@ export class Message {
|
||||||
return [];
|
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 {
|
isAccountSigner(index: number): boolean {
|
||||||
return index < this.header.numRequiredSignatures;
|
return index < this.header.numRequiredSignatures;
|
||||||
}
|
}
|
||||||
|
@ -250,7 +282,7 @@ export class Message {
|
||||||
for (let i = 0; i < accountCount; i++) {
|
for (let i = 0; i < accountCount; i++) {
|
||||||
const account = byteArray.slice(0, PUBLIC_KEY_LENGTH);
|
const account = byteArray.slice(0, PUBLIC_KEY_LENGTH);
|
||||||
byteArray = byteArray.slice(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);
|
const recentBlockhash = byteArray.slice(0, PUBLIC_KEY_LENGTH);
|
||||||
|
|
|
@ -40,6 +40,14 @@ export type CompileV0Args = {
|
||||||
addressLookupTableAccounts?: Array<AddressLookupTableAccount>;
|
addressLookupTableAccounts?: Array<AddressLookupTableAccount>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetAccountKeysArgs =
|
||||||
|
| {
|
||||||
|
accountKeysFromLookups: AccountKeysFromLookups;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
addressLookupTableAccounts: AddressLookupTableAccount[];
|
||||||
|
};
|
||||||
|
|
||||||
export class MessageV0 {
|
export class MessageV0 {
|
||||||
header: MessageHeader;
|
header: MessageHeader;
|
||||||
staticAccountKeys: Array<PublicKey>;
|
staticAccountKeys: Array<PublicKey>;
|
||||||
|
@ -59,6 +67,88 @@ export class MessageV0 {
|
||||||
return 0;
|
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 {
|
static compile(args: CompileV0Args): MessageV0 {
|
||||||
const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey);
|
const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './expiry-custom-errors';
|
export * from './expiry-custom-errors';
|
||||||
export * from './legacy';
|
export * from './legacy';
|
||||||
|
export * from './message';
|
||||||
export * from './versioned';
|
export * from './versioned';
|
||||||
|
|
|
@ -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,0 +1,91 @@
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {sha256} from '@noble/hashes/sha256';
|
||||||
|
|
||||||
|
import {Message} from '../../src/message';
|
||||||
|
import {TransactionInstruction} from '../../src/transaction';
|
||||||
|
import {PublicKey} from '../../src/publickey';
|
||||||
|
|
||||||
|
function createTestKeys(count: number): Array<PublicKey> {
|
||||||
|
return new Array(count).fill(0).map(() => PublicKey.unique());
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Message', () => {
|
||||||
|
it('compile', () => {
|
||||||
|
const keys = createTestKeys(5);
|
||||||
|
const recentBlockhash = bs58.encode(sha256('test'));
|
||||||
|
const payerKey = keys[0];
|
||||||
|
const instructions = [
|
||||||
|
new TransactionInstruction({
|
||||||
|
programId: keys[4],
|
||||||
|
keys: [
|
||||||
|
{pubkey: keys[1], isSigner: true, isWritable: true},
|
||||||
|
{pubkey: keys[2], isSigner: false, isWritable: false},
|
||||||
|
{pubkey: keys[3], isSigner: false, isWritable: false},
|
||||||
|
],
|
||||||
|
data: Buffer.alloc(1),
|
||||||
|
}),
|
||||||
|
new TransactionInstruction({
|
||||||
|
programId: keys[1],
|
||||||
|
keys: [
|
||||||
|
{pubkey: keys[2], isSigner: true, isWritable: false},
|
||||||
|
{pubkey: keys[3], isSigner: false, isWritable: true},
|
||||||
|
],
|
||||||
|
data: Buffer.alloc(2),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const message = Message.compile({
|
||||||
|
payerKey,
|
||||||
|
recentBlockhash,
|
||||||
|
instructions,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.accountKeys).to.eql([
|
||||||
|
payerKey, // payer is first
|
||||||
|
keys[1], // other writable signer
|
||||||
|
keys[2], // sole readonly signer
|
||||||
|
keys[3], // sole writable non-signer
|
||||||
|
keys[4], // sole readonly non-signer
|
||||||
|
]);
|
||||||
|
expect(message.header).to.eql({
|
||||||
|
numRequiredSignatures: 3,
|
||||||
|
numReadonlySignedAccounts: 1,
|
||||||
|
numReadonlyUnsignedAccounts: 1,
|
||||||
|
});
|
||||||
|
expect(message.addressTableLookups.length).to.eq(0);
|
||||||
|
expect(message.instructions).to.eql([
|
||||||
|
{
|
||||||
|
programIdIndex: 4,
|
||||||
|
accounts: [1, 2, 3],
|
||||||
|
data: bs58.encode(Buffer.alloc(1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
programIdIndex: 1,
|
||||||
|
accounts: [2, 3],
|
||||||
|
data: bs58.encode(Buffer.alloc(2)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(message.recentBlockhash).to.eq(recentBlockhash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('compile without instructions', () => {
|
||||||
|
const payerKey = PublicKey.unique();
|
||||||
|
const recentBlockhash = bs58.encode(sha256('test'));
|
||||||
|
const message = Message.compile({
|
||||||
|
payerKey,
|
||||||
|
instructions: [],
|
||||||
|
recentBlockhash,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.accountKeys).to.eql([payerKey]);
|
||||||
|
expect(message.header).to.eql({
|
||||||
|
numRequiredSignatures: 1,
|
||||||
|
numReadonlySignedAccounts: 0,
|
||||||
|
numReadonlyUnsignedAccounts: 0,
|
||||||
|
});
|
||||||
|
expect(message.addressTableLookups.length).to.eq(0);
|
||||||
|
expect(message.instructions.length).to.eq(0);
|
||||||
|
expect(message.recentBlockhash).to.eq(recentBlockhash);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,7 +2,11 @@ import bs58 from 'bs58';
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {sha256} from '@noble/hashes/sha256';
|
import {sha256} from '@noble/hashes/sha256';
|
||||||
|
|
||||||
import {MessageV0} from '../../src/message';
|
import {
|
||||||
|
MessageAccountKeys,
|
||||||
|
MessageAddressTableLookup,
|
||||||
|
MessageV0,
|
||||||
|
} from '../../src/message';
|
||||||
import {TransactionInstruction} from '../../src/transaction';
|
import {TransactionInstruction} from '../../src/transaction';
|
||||||
import {PublicKey} from '../../src/publickey';
|
import {PublicKey} from '../../src/publickey';
|
||||||
import {AddressLookupTableAccount} from '../../src/programs';
|
import {AddressLookupTableAccount} from '../../src/programs';
|
||||||
|
@ -28,6 +32,142 @@ function createTestLookupTable(
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('MessageV0', () => {
|
describe('MessageV0', () => {
|
||||||
|
it('numAccountKeysFromLookups', () => {
|
||||||
|
const message = MessageV0.compile({
|
||||||
|
payerKey: PublicKey.unique(),
|
||||||
|
recentBlockhash: '',
|
||||||
|
instructions: [],
|
||||||
|
});
|
||||||
|
expect(message.numAccountKeysFromLookups).to.eq(0);
|
||||||
|
|
||||||
|
message.addressTableLookups = [
|
||||||
|
{
|
||||||
|
accountKey: PublicKey.unique(),
|
||||||
|
writableIndexes: [0],
|
||||||
|
readonlyIndexes: [1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountKey: PublicKey.unique(),
|
||||||
|
writableIndexes: [0, 2],
|
||||||
|
readonlyIndexes: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(message.numAccountKeysFromLookups).to.eq(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAccountKeys', () => {
|
||||||
|
const staticAccountKeys = createTestKeys(3);
|
||||||
|
const lookupTable = createTestLookupTable(createTestKeys(2));
|
||||||
|
const message = new MessageV0({
|
||||||
|
header: {
|
||||||
|
numRequiredSignatures: 1,
|
||||||
|
numReadonlySignedAccounts: 0,
|
||||||
|
numReadonlyUnsignedAccounts: 0,
|
||||||
|
},
|
||||||
|
recentBlockhash: 'test',
|
||||||
|
staticAccountKeys,
|
||||||
|
compiledInstructions: [],
|
||||||
|
addressTableLookups: [
|
||||||
|
{
|
||||||
|
accountKey: lookupTable.key,
|
||||||
|
writableIndexes: [0],
|
||||||
|
readonlyIndexes: [1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => message.getAccountKeys()).to.throw(
|
||||||
|
'Failed to get account keys because address table lookups were not resolved',
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
message.getAccountKeys({
|
||||||
|
accountKeysFromLookups: {writable: [PublicKey.unique()], readonly: []},
|
||||||
|
}),
|
||||||
|
).to.throw(
|
||||||
|
'Failed to get account keys because of a mismatch in the number of account keys from lookups',
|
||||||
|
);
|
||||||
|
|
||||||
|
const accountKeysFromLookups = message.resolveAddressTableLookups([
|
||||||
|
lookupTable,
|
||||||
|
]);
|
||||||
|
const expectedAccountKeys = new MessageAccountKeys(
|
||||||
|
staticAccountKeys,
|
||||||
|
accountKeysFromLookups,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
message.getAccountKeys({
|
||||||
|
accountKeysFromLookups,
|
||||||
|
}),
|
||||||
|
).to.eql(expectedAccountKeys);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
message.getAccountKeys({
|
||||||
|
addressLookupTableAccounts: [lookupTable],
|
||||||
|
}),
|
||||||
|
).to.eql(expectedAccountKeys);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveAddressTableLookups', () => {
|
||||||
|
const keys = createTestKeys(7);
|
||||||
|
const lookupTable = createTestLookupTable(keys);
|
||||||
|
const createTestMessage = (
|
||||||
|
addressTableLookups: MessageAddressTableLookup[],
|
||||||
|
): MessageV0 => {
|
||||||
|
return new MessageV0({
|
||||||
|
header: {
|
||||||
|
numRequiredSignatures: 1,
|
||||||
|
numReadonlySignedAccounts: 0,
|
||||||
|
numReadonlyUnsignedAccounts: 0,
|
||||||
|
},
|
||||||
|
recentBlockhash: 'test',
|
||||||
|
staticAccountKeys: [],
|
||||||
|
compiledInstructions: [],
|
||||||
|
addressTableLookups,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
createTestMessage([]).resolveAddressTableLookups([lookupTable]),
|
||||||
|
).to.eql({
|
||||||
|
writable: [],
|
||||||
|
readonly: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
createTestMessage([
|
||||||
|
{
|
||||||
|
accountKey: PublicKey.unique(),
|
||||||
|
writableIndexes: [1, 3, 5],
|
||||||
|
readonlyIndexes: [0, 2, 4],
|
||||||
|
},
|
||||||
|
]).resolveAddressTableLookups([lookupTable]),
|
||||||
|
).to.throw('Failed to find address lookup table account for table key');
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
createTestMessage([
|
||||||
|
{
|
||||||
|
accountKey: lookupTable.key,
|
||||||
|
writableIndexes: [10],
|
||||||
|
readonlyIndexes: [],
|
||||||
|
},
|
||||||
|
]).resolveAddressTableLookups([lookupTable]),
|
||||||
|
).to.throw('Failed to find address for index');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
createTestMessage([
|
||||||
|
{
|
||||||
|
accountKey: lookupTable.key,
|
||||||
|
writableIndexes: [1, 3, 5],
|
||||||
|
readonlyIndexes: [0, 2, 4],
|
||||||
|
},
|
||||||
|
]).resolveAddressTableLookups([lookupTable]),
|
||||||
|
).to.eql({
|
||||||
|
writable: [keys[1], keys[3], keys[5]],
|
||||||
|
readonly: [keys[0], keys[2], keys[4]],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('compile', () => {
|
it('compile', () => {
|
||||||
const keys = createTestKeys(7);
|
const keys = createTestKeys(7);
|
||||||
const recentBlockhash = bs58.encode(sha256('test'));
|
const recentBlockhash = bs58.encode(sha256('test'));
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {sha256} from '@noble/hashes/sha256';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TransactionInstruction,
|
||||||
|
TransactionMessage,
|
||||||
|
} from '../../src/transaction';
|
||||||
|
import {PublicKey} from '../../src/publickey';
|
||||||
|
import {AddressLookupTableAccount} from '../../src/programs';
|
||||||
|
import {MessageV0} from '../../src/message';
|
||||||
|
|
||||||
|
function createTestKeys(count: number): Array<PublicKey> {
|
||||||
|
return new Array(count).fill(0).map(() => PublicKey.unique());
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestLookupTable(
|
||||||
|
addresses: Array<PublicKey>,
|
||||||
|
): AddressLookupTableAccount {
|
||||||
|
const U64_MAX = 2n ** 64n - 1n;
|
||||||
|
return new AddressLookupTableAccount({
|
||||||
|
key: PublicKey.unique(),
|
||||||
|
state: {
|
||||||
|
lastExtendedSlot: 0,
|
||||||
|
lastExtendedSlotStartIndex: 0,
|
||||||
|
deactivationSlot: U64_MAX,
|
||||||
|
authority: PublicKey.unique(),
|
||||||
|
addresses,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TransactionMessage', () => {
|
||||||
|
it('decompile', () => {
|
||||||
|
const keys = createTestKeys(7);
|
||||||
|
const recentBlockhash = bs58.encode(sha256('test'));
|
||||||
|
const payerKey = keys[0];
|
||||||
|
const instructions = [
|
||||||
|
new TransactionInstruction({
|
||||||
|
programId: keys[4],
|
||||||
|
keys: [
|
||||||
|
{pubkey: keys[1], isSigner: true, isWritable: true},
|
||||||
|
{pubkey: keys[2], isSigner: true, isWritable: false},
|
||||||
|
{pubkey: keys[3], isSigner: false, isWritable: true},
|
||||||
|
{pubkey: keys[5], isSigner: false, isWritable: true},
|
||||||
|
{pubkey: keys[6], isSigner: false, isWritable: false},
|
||||||
|
],
|
||||||
|
data: Buffer.alloc(1),
|
||||||
|
}),
|
||||||
|
new TransactionInstruction({
|
||||||
|
programId: keys[1],
|
||||||
|
keys: [],
|
||||||
|
data: Buffer.alloc(2),
|
||||||
|
}),
|
||||||
|
new TransactionInstruction({
|
||||||
|
programId: keys[3],
|
||||||
|
keys: [],
|
||||||
|
data: Buffer.alloc(3),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const addressLookupTableAccounts = [createTestLookupTable(keys)];
|
||||||
|
const message = MessageV0.compile({
|
||||||
|
payerKey,
|
||||||
|
recentBlockhash,
|
||||||
|
instructions,
|
||||||
|
addressLookupTableAccounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => TransactionMessage.decompile(message)).to.throw(
|
||||||
|
'Failed to get account keys because address table lookups were not resolved',
|
||||||
|
);
|
||||||
|
|
||||||
|
const accountKeys = message.getAccountKeys({addressLookupTableAccounts});
|
||||||
|
const decompiledMessage = TransactionMessage.decompile(message, {
|
||||||
|
addressLookupTableAccounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decompiledMessage.accountKeys).to.eql(accountKeys);
|
||||||
|
expect(decompiledMessage.recentBlockhash).to.eq(recentBlockhash);
|
||||||
|
expect(decompiledMessage.instructions).to.eql(instructions);
|
||||||
|
|
||||||
|
expect(decompiledMessage).to.eql(
|
||||||
|
TransactionMessage.decompile(message, {
|
||||||
|
accountKeysFromLookups: accountKeys.accountKeysFromLookups!,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue