feat: add support for creating version 0 transactions (#27142)

* feat: add support for version 0 transactions

* chore: feedback

* chore: update VersionedMessage type

* chore: use literals for version getter

* chore: fix lint error

* chore: switch to VersionedMessage.deserialize
This commit is contained in:
Justin Starry 2022-08-25 15:42:54 +02:00 committed by GitHub
parent 4c944931c5
commit 1ccfc65a52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 742 additions and 9 deletions

View File

@ -8,6 +8,13 @@ export const publicKey = (property: string = 'publicKey') => {
return BufferLayout.blob(32, property);
};
/**
* Layout for a signature
*/
export const signature = (property: string = 'signature') => {
return BufferLayout.blob(64, property);
};
/**
* Layout for a 64bit unsigned value
*/

View File

@ -1,4 +1,8 @@
import {PublicKey} from '../publickey';
export * from './legacy';
export * from './versioned';
export * from './v0';
/**
* The message header, identifying signed and read-only account
@ -15,18 +19,27 @@ export type MessageHeader = {
numReadonlyUnsignedAccounts: number;
};
/**
* An address table lookup used to load additional accounts
*/
export type MessageAddressTableLookup = {
accountKey: PublicKey;
writableIndexes: Array<number>;
readonlyIndexes: Array<number>;
};
/**
* An instruction to execute by a program
*
* @property {number} programIdIndex
* @property {number[]} accounts
* @property {string} data
* @property {number[]} accountKeyIndexes
* @property {Uint8Array} data
*/
export type CompiledInstruction = {
export type MessageCompiledInstruction = {
/** Index into the transaction keys array indicating the program account that executes this instruction */
programIdIndex: number;
/** Ordered indices into the transaction keys array indicating which accounts to pass to the program */
accounts: number[];
/** The program input data encoded as base 58 */
data: string;
accountKeyIndexes: number[];
/** The program input data */
data: Uint8Array;
};

View File

@ -5,10 +5,30 @@ import * as BufferLayout from '@solana/buffer-layout';
import {PublicKey, PUBLIC_KEY_LENGTH} from '../publickey';
import type {Blockhash} from '../blockhash';
import * as Layout from '../layout';
import {PACKET_DATA_SIZE} from '../transaction/constants';
import {PACKET_DATA_SIZE, VERSION_PREFIX_MASK} from '../transaction/constants';
import * as shortvec from '../utils/shortvec-encoding';
import {toBuffer} from '../utils/to-buffer';
import {CompiledInstruction, MessageHeader} from './index';
import {
MessageHeader,
MessageAddressTableLookup,
MessageCompiledInstruction,
} from './index';
/**
* An instruction to execute by a program
*
* @property {number} programIdIndex
* @property {number[]} accounts
* @property {string} data
*/
export type CompiledInstruction = {
/** Index into the transaction keys array indicating the program account that executes this instruction */
programIdIndex: number;
/** Ordered indices into the transaction keys array indicating which accounts to pass to the program */
accounts: number[];
/** The program input data encoded as base 58 */
data: string;
};
/**
* Message constructor arguments
@ -51,6 +71,28 @@ export class Message {
);
}
get version(): 'legacy' {
return 'legacy';
}
get staticAccountKeys(): Array<PublicKey> {
return this.accountKeys;
}
get compiledInstructions(): Array<MessageCompiledInstruction> {
return this.instructions.map(
(ix): MessageCompiledInstruction => ({
programIdIndex: ix.programIdIndex,
accountKeyIndexes: ix.accounts,
data: bs58.decode(ix.data),
}),
);
}
get addressTableLookups(): Array<MessageAddressTableLookup> {
return [];
}
isAccountSigner(index: number): boolean {
return index < this.header.numRequiredSignatures;
}
@ -191,6 +233,15 @@ export class Message {
let byteArray = [...buffer];
const numRequiredSignatures = byteArray.shift() as number;
if (
numRequiredSignatures !==
(numRequiredSignatures & VERSION_PREFIX_MASK)
) {
throw new Error(
'Versioned messages must be deserialized with VersionedMessage.deserialize()',
);
}
const numReadonlySignedAccounts = byteArray.shift() as number;
const numReadonlyUnsignedAccounts = byteArray.shift() as number;

324
web3.js/src/message/v0.ts Normal file
View File

@ -0,0 +1,324 @@
import bs58 from 'bs58';
import * as BufferLayout from '@solana/buffer-layout';
import * as Layout from '../layout';
import {Blockhash} from '../blockhash';
import {
MessageHeader,
MessageAddressTableLookup,
MessageCompiledInstruction,
} from './index';
import {PublicKey, PUBLIC_KEY_LENGTH} from '../publickey';
import * as shortvec from '../utils/shortvec-encoding';
import assert from '../utils/assert';
import {PACKET_DATA_SIZE, VERSION_PREFIX_MASK} from '../transaction/constants';
/**
* Message constructor arguments
*/
export type MessageV0Args = {
/** The message header, identifying signed and read-only `accountKeys` */
header: MessageHeader;
/** The static account keys used by this transaction */
staticAccountKeys: 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. */
compiledInstructions: MessageCompiledInstruction[];
/** Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. */
addressTableLookups: MessageAddressTableLookup[];
};
export class MessageV0 {
header: MessageHeader;
staticAccountKeys: Array<PublicKey>;
recentBlockhash: Blockhash;
compiledInstructions: Array<MessageCompiledInstruction>;
addressTableLookups: Array<MessageAddressTableLookup>;
constructor(args: MessageV0Args) {
this.header = args.header;
this.staticAccountKeys = args.staticAccountKeys;
this.recentBlockhash = args.recentBlockhash;
this.compiledInstructions = args.compiledInstructions;
this.addressTableLookups = args.addressTableLookups;
}
get version(): 0 {
return 0;
}
serialize(): Uint8Array {
const encodedStaticAccountKeysLength = Array<number>();
shortvec.encodeLength(
encodedStaticAccountKeysLength,
this.staticAccountKeys.length,
);
const serializedInstructions = this.serializeInstructions();
const encodedInstructionsLength = Array<number>();
shortvec.encodeLength(
encodedInstructionsLength,
this.compiledInstructions.length,
);
const serializedAddressTableLookups = this.serializeAddressTableLookups();
const encodedAddressTableLookupsLength = Array<number>();
shortvec.encodeLength(
encodedAddressTableLookupsLength,
this.addressTableLookups.length,
);
const messageLayout = BufferLayout.struct<{
prefix: number;
header: MessageHeader;
staticAccountKeysLength: Uint8Array;
staticAccountKeys: Array<Uint8Array>;
recentBlockhash: Uint8Array;
instructionsLength: Uint8Array;
serializedInstructions: Uint8Array;
addressTableLookupsLength: Uint8Array;
serializedAddressTableLookups: Uint8Array;
}>([
BufferLayout.u8('prefix'),
BufferLayout.struct<MessageHeader>(
[
BufferLayout.u8('numRequiredSignatures'),
BufferLayout.u8('numReadonlySignedAccounts'),
BufferLayout.u8('numReadonlyUnsignedAccounts'),
],
'header',
),
BufferLayout.blob(
encodedStaticAccountKeysLength.length,
'staticAccountKeysLength',
),
BufferLayout.seq(
Layout.publicKey(),
this.staticAccountKeys.length,
'staticAccountKeys',
),
Layout.publicKey('recentBlockhash'),
BufferLayout.blob(encodedInstructionsLength.length, 'instructionsLength'),
BufferLayout.blob(
serializedInstructions.length,
'serializedInstructions',
),
BufferLayout.blob(
encodedAddressTableLookupsLength.length,
'addressTableLookupsLength',
),
BufferLayout.blob(
serializedAddressTableLookups.length,
'serializedAddressTableLookups',
),
]);
const serializedMessage = new Uint8Array(PACKET_DATA_SIZE);
const MESSAGE_VERSION_0_PREFIX = 1 << 7;
const serializedMessageLength = messageLayout.encode(
{
prefix: MESSAGE_VERSION_0_PREFIX,
header: this.header,
staticAccountKeysLength: new Uint8Array(encodedStaticAccountKeysLength),
staticAccountKeys: this.staticAccountKeys.map(key => key.toBytes()),
recentBlockhash: bs58.decode(this.recentBlockhash),
instructionsLength: new Uint8Array(encodedInstructionsLength),
serializedInstructions,
addressTableLookupsLength: new Uint8Array(
encodedAddressTableLookupsLength,
),
serializedAddressTableLookups,
},
serializedMessage,
);
return serializedMessage.slice(0, serializedMessageLength);
}
private serializeInstructions(): Uint8Array {
let serializedLength = 0;
const serializedInstructions = new Uint8Array(PACKET_DATA_SIZE);
for (const instruction of this.compiledInstructions) {
const encodedAccountKeyIndexesLength = Array<number>();
shortvec.encodeLength(
encodedAccountKeyIndexesLength,
instruction.accountKeyIndexes.length,
);
const encodedDataLength = Array<number>();
shortvec.encodeLength(encodedDataLength, instruction.data.length);
const instructionLayout = BufferLayout.struct<{
programIdIndex: number;
encodedAccountKeyIndexesLength: Uint8Array;
accountKeyIndexes: number[];
encodedDataLength: Uint8Array;
data: Uint8Array;
}>([
BufferLayout.u8('programIdIndex'),
BufferLayout.blob(
encodedAccountKeyIndexesLength.length,
'encodedAccountKeyIndexesLength',
),
BufferLayout.seq(
BufferLayout.u8(),
instruction.accountKeyIndexes.length,
'accountKeyIndexes',
),
BufferLayout.blob(encodedDataLength.length, 'encodedDataLength'),
BufferLayout.blob(instruction.data.length, 'data'),
]);
serializedLength += instructionLayout.encode(
{
programIdIndex: instruction.programIdIndex,
encodedAccountKeyIndexesLength: new Uint8Array(
encodedAccountKeyIndexesLength,
),
accountKeyIndexes: instruction.accountKeyIndexes,
encodedDataLength: new Uint8Array(encodedDataLength),
data: instruction.data,
},
serializedInstructions,
serializedLength,
);
}
return serializedInstructions.slice(0, serializedLength);
}
private serializeAddressTableLookups(): Uint8Array {
let serializedLength = 0;
const serializedAddressTableLookups = new Uint8Array(PACKET_DATA_SIZE);
for (const lookup of this.addressTableLookups) {
const encodedWritableIndexesLength = Array<number>();
shortvec.encodeLength(
encodedWritableIndexesLength,
lookup.writableIndexes.length,
);
const encodedReadonlyIndexesLength = Array<number>();
shortvec.encodeLength(
encodedReadonlyIndexesLength,
lookup.readonlyIndexes.length,
);
const addressTableLookupLayout = BufferLayout.struct<{
accountKey: Uint8Array;
encodedWritableIndexesLength: Uint8Array;
writableIndexes: number[];
encodedReadonlyIndexesLength: Uint8Array;
readonlyIndexes: number[];
}>([
Layout.publicKey('accountKey'),
BufferLayout.blob(
encodedWritableIndexesLength.length,
'encodedWritableIndexesLength',
),
BufferLayout.seq(
BufferLayout.u8(),
lookup.writableIndexes.length,
'writableIndexes',
),
BufferLayout.blob(
encodedReadonlyIndexesLength.length,
'encodedReadonlyIndexesLength',
),
BufferLayout.seq(
BufferLayout.u8(),
lookup.readonlyIndexes.length,
'readonlyIndexes',
),
]);
serializedLength += addressTableLookupLayout.encode(
{
accountKey: lookup.accountKey.toBytes(),
encodedWritableIndexesLength: new Uint8Array(
encodedWritableIndexesLength,
),
writableIndexes: lookup.writableIndexes,
encodedReadonlyIndexesLength: new Uint8Array(
encodedReadonlyIndexesLength,
),
readonlyIndexes: lookup.readonlyIndexes,
},
serializedAddressTableLookups,
serializedLength,
);
}
return serializedAddressTableLookups.slice(0, serializedLength);
}
static deserialize(serializedMessage: Uint8Array): MessageV0 {
let byteArray = [...serializedMessage];
const prefix = byteArray.shift() as number;
const maskedPrefix = prefix & VERSION_PREFIX_MASK;
assert(
prefix !== maskedPrefix,
`Expected versioned message but received legacy message`,
);
const version = maskedPrefix;
assert(
version === 0,
`Expected versioned message with version 0 but found version ${version}`,
);
const header: MessageHeader = {
numRequiredSignatures: byteArray.shift() as number,
numReadonlySignedAccounts: byteArray.shift() as number,
numReadonlyUnsignedAccounts: byteArray.shift() as number,
};
const staticAccountKeys = [];
const staticAccountKeysLength = shortvec.decodeLength(byteArray);
for (let i = 0; i < staticAccountKeysLength; i++) {
staticAccountKeys.push(
new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH)),
);
}
const recentBlockhash = bs58.encode(byteArray.splice(0, PUBLIC_KEY_LENGTH));
const instructionCount = shortvec.decodeLength(byteArray);
const compiledInstructions: MessageCompiledInstruction[] = [];
for (let i = 0; i < instructionCount; i++) {
const programIdIndex = byteArray.shift() as number;
const accountKeyIndexesLength = shortvec.decodeLength(byteArray);
const accountKeyIndexes = byteArray.splice(0, accountKeyIndexesLength);
const dataLength = shortvec.decodeLength(byteArray);
const data = new Uint8Array(byteArray.splice(0, dataLength));
compiledInstructions.push({
programIdIndex,
accountKeyIndexes,
data,
});
}
const addressTableLookupsCount = shortvec.decodeLength(byteArray);
const addressTableLookups: MessageAddressTableLookup[] = [];
for (let i = 0; i < addressTableLookupsCount; i++) {
const accountKey = new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH));
const writableIndexesLength = shortvec.decodeLength(byteArray);
const writableIndexes = byteArray.splice(0, writableIndexesLength);
const readonlyIndexesLength = shortvec.decodeLength(byteArray);
const readonlyIndexes = byteArray.splice(0, readonlyIndexesLength);
addressTableLookups.push({
accountKey,
writableIndexes,
readonlyIndexes,
});
}
return new MessageV0({
header,
staticAccountKeys,
recentBlockhash,
compiledInstructions,
addressTableLookups,
});
}
}

View File

@ -0,0 +1,27 @@
import {VERSION_PREFIX_MASK} from '../transaction/constants';
import {Message} from './legacy';
import {MessageV0} from './v0';
export type VersionedMessage = Message | MessageV0;
// eslint-disable-next-line no-redeclare
export const VersionedMessage = {
deserialize: (serializedMessage: Uint8Array): VersionedMessage => {
const prefix = serializedMessage[0];
const maskedPrefix = prefix & VERSION_PREFIX_MASK;
// if the highest bit of the prefix is not set, the message is not versioned
if (maskedPrefix === prefix) {
return Message.from(serializedMessage);
}
// the lower 7 bits of the prefix indicate the message version
const version = maskedPrefix;
if (version === 0) {
return MessageV0.deserialize(serializedMessage);
} else {
throw new Error(
`Transaction message version ${version} deserialization is not supported`,
);
}
},
};

View File

@ -7,4 +7,6 @@
*/
export const PACKET_DATA_SIZE = 1280 - 40 - 8;
export const VERSION_PREFIX_MASK = 0x7f;
export const SIGNATURE_LENGTH_IN_BYTES = 64;

View File

@ -1,3 +1,4 @@
export * from './constants';
export * from './expiry-custom-errors';
export * from './legacy';
export * from './versioned';

View File

@ -0,0 +1,108 @@
import nacl from 'tweetnacl';
import * as BufferLayout from '@solana/buffer-layout';
import {Signer} from '../keypair';
import assert from '../utils/assert';
import {VersionedMessage} from '../message/versioned';
import {SIGNATURE_LENGTH_IN_BYTES} from './constants';
import * as shortvec from '../utils/shortvec-encoding';
import * as Layout from '../layout';
export type TransactionVersion = 'legacy' | 0;
/**
* Versioned transaction class
*/
export class VersionedTransaction {
signatures: Array<Uint8Array>;
message: VersionedMessage;
constructor(message: VersionedMessage, signatures?: Array<Uint8Array>) {
if (signatures !== undefined) {
assert(
signatures.length === message.header.numRequiredSignatures,
'Expected signatures length to be equal to the number of required signatures',
);
this.signatures = signatures;
} else {
const defaultSignatures = [];
for (let i = 0; i < message.header.numRequiredSignatures; i++) {
defaultSignatures.push(new Uint8Array(SIGNATURE_LENGTH_IN_BYTES));
}
this.signatures = defaultSignatures;
}
this.message = message;
}
serialize(): Uint8Array {
const serializedMessage = this.message.serialize();
const encodedSignaturesLength = Array<number>();
shortvec.encodeLength(encodedSignaturesLength, this.signatures.length);
const transactionLayout = BufferLayout.struct<{
encodedSignaturesLength: Uint8Array;
signatures: Array<Uint8Array>;
serializedMessage: Uint8Array;
}>([
BufferLayout.blob(
encodedSignaturesLength.length,
'encodedSignaturesLength',
),
BufferLayout.seq(
Layout.signature(),
this.signatures.length,
'signatures',
),
BufferLayout.blob(serializedMessage.length, 'serializedMessage'),
]);
const serializedTransaction = new Uint8Array(2048);
const serializedTransactionLength = transactionLayout.encode(
{
encodedSignaturesLength: new Uint8Array(encodedSignaturesLength),
signatures: this.signatures,
serializedMessage,
},
serializedTransaction,
);
return serializedTransaction.slice(0, serializedTransactionLength);
}
static deserialize(serializedTransaction: Uint8Array): VersionedTransaction {
let byteArray = [...serializedTransaction];
const signatures = [];
const signaturesLength = shortvec.decodeLength(byteArray);
for (let i = 0; i < signaturesLength; i++) {
signatures.push(
new Uint8Array(byteArray.splice(0, SIGNATURE_LENGTH_IN_BYTES)),
);
}
const message = VersionedMessage.deserialize(new Uint8Array(byteArray));
return new VersionedTransaction(message, signatures);
}
sign(signers: Array<Signer>) {
const messageData = this.message.serialize();
const signerPubkeys = this.message.staticAccountKeys.slice(
0,
this.message.header.numRequiredSignatures,
);
for (const signer of signers) {
const signerIndex = signerPubkeys.findIndex(pubkey =>
pubkey.equals(signer.publicKey),
);
assert(
signerIndex >= 0,
`Cannot sign with non signer key ${signer.publicKey.toBase58()}`,
);
this.signatures[signerIndex] = nacl.sign.detached(
messageData,
signer.secretKey,
);
}
}
}

View File

@ -19,6 +19,7 @@ import {
Keypair,
Message,
AddressLookupTableProgram,
SYSTEM_INSTRUCTION_LAYOUTS,
} from '../src';
import invariant from '../src/utils/assert';
import {MOCK_PORT, url} from './url';
@ -61,6 +62,9 @@ import type {
TransactionError,
KeyedAccountInfo,
} from '../src/connection';
import {VersionedTransaction} from '../src/transaction/versioned';
import {MessageV0} from '../src/message/v0';
import {encodeData} from '../src/instruction';
use(chaiAsPromised);
@ -4329,5 +4333,121 @@ describe('Connection', function () {
expect(lookupTableAccount.state.authority).to.be.undefined;
}
});
it('sendRawTransaction with v0 transaction', async () => {
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: 10 * LAMPORTS_PER_SOL,
});
const lookupTableAddresses = [Keypair.generate().publicKey];
const recentSlot = await connection.getSlot('finalized');
const [createIx, lookupTableKey] =
AddressLookupTableProgram.createLookupTable({
recentSlot,
payer: payer.publicKey,
authority: payer.publicKey,
});
// create, extend, and fetch lookup table
{
const transaction = new Transaction().add(createIx).add(
AddressLookupTableProgram.extendLookupTable({
lookupTable: lookupTableKey,
addresses: lookupTableAddresses,
authority: payer.publicKey,
payer: payer.publicKey,
}),
);
await helpers.processTransaction({
connection,
transaction,
signers: [payer],
commitment: 'processed',
});
const lookupTableResponse = await connection.getAddressLookupTable(
lookupTableKey,
{
commitment: 'processed',
},
);
const lookupTableAccount = lookupTableResponse.value;
if (!lookupTableAccount) {
expect(lookupTableAccount).to.be.ok;
return;
}
// eslint-disable-next-line no-constant-condition
while (true) {
const latestSlot = await connection.getSlot('processed');
if (latestSlot > lookupTableAccount.state.lastExtendedSlot) {
break;
} else {
console.log('Waiting for next slot...');
await sleep(500);
}
}
}
// create, serialize, send and confirm versioned transaction
{
const {blockhash, lastValidBlockHeight} =
await connection.getLatestBlockhash();
const transferIxData = encodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, {
lamports: BigInt(LAMPORTS_PER_SOL),
});
const transaction = new VersionedTransaction(
new MessageV0({
header: {
numRequiredSignatures: 1,
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 1,
},
staticAccountKeys: [payer.publicKey, SystemProgram.programId],
recentBlockhash: blockhash,
compiledInstructions: [
{
programIdIndex: 1,
accountKeyIndexes: [0, 2],
data: transferIxData,
},
],
addressTableLookups: [
{
accountKey: lookupTableKey,
writableIndexes: [0],
readonlyIndexes: [],
},
],
}),
);
transaction.sign([payer]);
const signature = bs58.encode(transaction.signatures[0]);
const serializedTransaction = transaction.serialize();
await connection.sendRawTransaction(serializedTransaction, {
preflightCommitment: 'processed',
});
await connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
},
'processed',
);
const transferToKey = lookupTableAddresses[0];
const transferToAccount = await connection.getAccountInfo(
transferToKey,
'processed',
);
expect(transferToAccount?.lamports).to.be.eq(LAMPORTS_PER_SOL);
}
});
}
});

View File

@ -0,0 +1,56 @@
import {expect} from 'chai';
import {MessageV0} from '../../src/message';
import {PublicKey} from '../../src/publickey';
describe('MessageV0', () => {
it('serialize and deserialize', () => {
const messageV0 = new MessageV0({
header: {
numRequiredSignatures: 1,
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 1,
},
staticAccountKeys: [new PublicKey(1), new PublicKey(2)],
compiledInstructions: [
{
programIdIndex: 1,
accountKeyIndexes: [2, 3],
data: new Uint8Array(10),
},
],
recentBlockhash: new PublicKey(0).toString(),
addressTableLookups: [
{
accountKey: new PublicKey(3),
writableIndexes: [1],
readonlyIndexes: [],
},
{
accountKey: new PublicKey(4),
writableIndexes: [],
readonlyIndexes: [2],
},
],
});
const serializedMessage = messageV0.serialize();
const deserializedMessage = MessageV0.deserialize(serializedMessage);
expect(JSON.stringify(messageV0)).to.eql(
JSON.stringify(deserializedMessage),
);
});
it('deserialize failures', () => {
const bufferWithLegacyPrefix = new Uint8Array([1]);
expect(() => {
MessageV0.deserialize(bufferWithLegacyPrefix);
}).to.throw('Expected versioned message but received legacy message');
const bufferWithV1Prefix = new Uint8Array([(1 << 7) + 1]);
expect(() => {
MessageV0.deserialize(bufferWithV1Prefix);
}).to.throw(
'Expected versioned message with version 0 but found version 1',
);
});
});

View File

@ -6,7 +6,11 @@ import {expect} from 'chai';
import {Connection} from '../src/connection';
import {Keypair} from '../src/keypair';
import {PublicKey} from '../src/publickey';
import {Transaction, TransactionInstruction} from '../src/transaction';
import {
Transaction,
TransactionInstruction,
VersionedTransaction,
} from '../src/transaction';
import {StakeProgram, SystemProgram} from '../src/programs';
import {Message} from '../src/message';
import invariant from '../src/utils/assert';
@ -856,6 +860,26 @@ describe('Transaction', () => {
expect(tx.verifySignatures()).to.be.true;
});
it('deserializes versioned transactions', () => {
const serializedVersionedTx = Buffer.from(
'AdTIDASR42TgVuXKkd7mJKk373J3LPVp85eyKMVcrboo9KTY8/vm6N/Cv0NiHqk2I8iYw6VX5ZaBKG8z' +
'9l1XjwiAAQACA+6qNbqfjaIENwt9GzEK/ENiB/ijGwluzBUmQ9xlTAMcCaS0ctnyxTcXXlJr7u2qtnaM' +
'gIAO2/c7RBD0ipHWUcEDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAJbI7VNs6MzREUlnzRaJ' +
'pBKP8QQoDn2dWQvD0KIgHFDiAwIACQAgoQcAAAAAAAIABQEAAAQAATYPBwAKBDIBAyQWIw0oCxIdCA4i' +
'JzQRKwUZHxceHCohMBUJJiwpMxAaGC0TLhQxGyAMBiU2NS8VDgAAAADuAgAAAAAAAAIAAAAAAAAAAdGCT' +
'Qiq5yw3+3m1sPoRNj0GtUNNs0FIMocxzt3zuoSZHQABAwQFBwgLDA8RFBcYGhwdHh8iIyUnKiwtLi8yF' +
'wIGCQoNDhASExUWGRsgISQmKCkrMDEz',
'base64',
);
expect(() => Transaction.from(serializedVersionedTx)).to.throw(
'Versioned messages must be deserialized with VersionedMessage.deserialize()',
);
const versionedTx = VersionedTransaction.deserialize(serializedVersionedTx);
expect(versionedTx.message.version).to.eq(0);
});
it('can serialize, deserialize, and reserialize with a partial signer', () => {
const signer = Keypair.generate();
const acc0Writable = Keypair.generate();