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:
parent
4c944931c5
commit
1ccfc65a52
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './constants';
|
||||
export * from './expiry-custom-errors';
|
||||
export * from './legacy';
|
||||
export * from './versioned';
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue