From 8e30e661f65d75abaa255eb39411665336803d49 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Thu, 25 Aug 2022 15:42:54 +0200 Subject: [PATCH] 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 --- src/layout.ts | 7 + src/message/index.ts | 25 ++- src/message/legacy.ts | 55 +++++- src/message/v0.ts | 324 ++++++++++++++++++++++++++++++++++ src/message/versioned.ts | 27 +++ src/transaction/constants.ts | 2 + src/transaction/index.ts | 1 + src/transaction/versioned.ts | 108 ++++++++++++ test/connection.test.ts | 120 +++++++++++++ test/message-tests/v0.test.ts | 56 ++++++ test/transaction.test.ts | 26 ++- 11 files changed, 742 insertions(+), 9 deletions(-) create mode 100644 src/message/v0.ts create mode 100644 src/message/versioned.ts create mode 100644 src/transaction/versioned.ts create mode 100644 test/message-tests/v0.test.ts diff --git a/src/layout.ts b/src/layout.ts index b0e2438b843c..8c35d589a48a 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -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 */ diff --git a/src/message/index.ts b/src/message/index.ts index 23a8ae60ad55..24f7a1dcb843 100644 --- a/src/message/index.ts +++ b/src/message/index.ts @@ -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; + readonlyIndexes: Array; +}; + /** * 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; }; diff --git a/src/message/legacy.ts b/src/message/legacy.ts index ff6ea9a00052..38faa6320a0d 100644 --- a/src/message/legacy.ts +++ b/src/message/legacy.ts @@ -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 { + return this.accountKeys; + } + + get compiledInstructions(): Array { + return this.instructions.map( + (ix): MessageCompiledInstruction => ({ + programIdIndex: ix.programIdIndex, + accountKeyIndexes: ix.accounts, + data: bs58.decode(ix.data), + }), + ); + } + + get addressTableLookups(): Array { + 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; diff --git a/src/message/v0.ts b/src/message/v0.ts new file mode 100644 index 000000000000..ea770df3a9b2 --- /dev/null +++ b/src/message/v0.ts @@ -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; + recentBlockhash: Blockhash; + compiledInstructions: Array; + addressTableLookups: Array; + + 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(); + shortvec.encodeLength( + encodedStaticAccountKeysLength, + this.staticAccountKeys.length, + ); + + const serializedInstructions = this.serializeInstructions(); + const encodedInstructionsLength = Array(); + shortvec.encodeLength( + encodedInstructionsLength, + this.compiledInstructions.length, + ); + + const serializedAddressTableLookups = this.serializeAddressTableLookups(); + const encodedAddressTableLookupsLength = Array(); + shortvec.encodeLength( + encodedAddressTableLookupsLength, + this.addressTableLookups.length, + ); + + const messageLayout = BufferLayout.struct<{ + prefix: number; + header: MessageHeader; + staticAccountKeysLength: Uint8Array; + staticAccountKeys: Array; + recentBlockhash: Uint8Array; + instructionsLength: Uint8Array; + serializedInstructions: Uint8Array; + addressTableLookupsLength: Uint8Array; + serializedAddressTableLookups: Uint8Array; + }>([ + BufferLayout.u8('prefix'), + BufferLayout.struct( + [ + 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(); + shortvec.encodeLength( + encodedAccountKeyIndexesLength, + instruction.accountKeyIndexes.length, + ); + + const encodedDataLength = Array(); + 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(); + shortvec.encodeLength( + encodedWritableIndexesLength, + lookup.writableIndexes.length, + ); + + const encodedReadonlyIndexesLength = Array(); + 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, + }); + } +} diff --git a/src/message/versioned.ts b/src/message/versioned.ts new file mode 100644 index 000000000000..042fc1c72fce --- /dev/null +++ b/src/message/versioned.ts @@ -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`, + ); + } + }, +}; diff --git a/src/transaction/constants.ts b/src/transaction/constants.ts index 591873f8b6bd..075337e8dce1 100644 --- a/src/transaction/constants.ts +++ b/src/transaction/constants.ts @@ -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; diff --git a/src/transaction/index.ts b/src/transaction/index.ts index ed913f5124db..2f5c19cb2510 100644 --- a/src/transaction/index.ts +++ b/src/transaction/index.ts @@ -1,3 +1,4 @@ export * from './constants'; export * from './expiry-custom-errors'; export * from './legacy'; +export * from './versioned'; diff --git a/src/transaction/versioned.ts b/src/transaction/versioned.ts new file mode 100644 index 000000000000..fa3930b34f46 --- /dev/null +++ b/src/transaction/versioned.ts @@ -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; + message: VersionedMessage; + + constructor(message: VersionedMessage, signatures?: Array) { + 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(); + shortvec.encodeLength(encodedSignaturesLength, this.signatures.length); + + const transactionLayout = BufferLayout.struct<{ + encodedSignaturesLength: Uint8Array; + signatures: Array; + 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) { + 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, + ); + } + } +} diff --git a/test/connection.test.ts b/test/connection.test.ts index 10b62f04dd7b..5cc8fc7d491c 100644 --- a/test/connection.test.ts +++ b/test/connection.test.ts @@ -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); + } + }); } }); diff --git a/test/message-tests/v0.test.ts b/test/message-tests/v0.test.ts new file mode 100644 index 000000000000..9cd3b189735b --- /dev/null +++ b/test/message-tests/v0.test.ts @@ -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', + ); + }); +}); diff --git a/test/transaction.test.ts b/test/transaction.test.ts index dd40d1ecf6c2..5d159f5e32f2 100644 --- a/test/transaction.test.ts +++ b/test/transaction.test.ts @@ -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();