feat: update `Connection` to support versioned transactions (#27068)

feat: update Connection to support versioned transactions
This commit is contained in:
Justin Starry 2022-08-31 13:46:24 +01:00 committed by GitHub
parent 0eec25be1a
commit 292b2a1bfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 403 additions and 106 deletions

View File

@ -32,8 +32,12 @@ import {NonceAccount} from './nonce-account';
import {PublicKey} from './publickey';
import {Signer} from './keypair';
import {MS_PER_SLOT} from './timing';
import {Transaction, TransactionStatus} from './transaction';
import {Message} from './message';
import {
Transaction,
TransactionStatus,
TransactionVersion,
} from './transaction';
import {Message, MessageHeader, MessageV0, VersionedMessage} from './message';
import {AddressLookupTableAccount} from './programs/address-lookup-table/state';
import assert from './utils/assert';
import {sleep} from './utils/sleep';
@ -395,6 +399,32 @@ function notificationResultAndContext<T, U>(value: Struct<T, U>) {
});
}
/**
* @internal
*/
function versionedMessageFromResponse(
version: TransactionVersion | undefined,
response: MessageResponse,
): VersionedMessage {
if (version === 0) {
return new MessageV0({
header: response.header,
staticAccountKeys: response.accountKeys.map(
accountKey => new PublicKey(accountKey),
),
recentBlockhash: response.recentBlockhash,
compiledInstructions: response.instructions.map(ix => ({
programIdIndex: ix.programIdIndex,
accountKeyIndexes: ix.accounts,
data: bs58.decode(ix.data),
})),
addressTableLookups: response.addressTableLookups!,
});
} else {
return new Message(response);
}
}
/**
* The level of commitment desired when querying state
* <pre>
@ -457,6 +487,14 @@ export type GetBalanceConfig = {
export type GetBlockConfig = {
/** The level of finality desired */
commitment?: Finality;
};
/**
* Configuration object for changing `getBlock` query behavior
*/
export type GetVersionedBlockConfig = {
/** The level of finality desired */
commitment?: Finality;
/** The max transaction version to return in responses. If the requested transaction is a higher version, an error will be returned */
maxSupportedTransactionVersion?: number;
};
@ -537,6 +575,14 @@ export type GetSlotLeaderConfig = {
export type GetTransactionConfig = {
/** The level of finality desired */
commitment?: Finality;
};
/**
* Configuration object for changing `getTransaction` query behavior
*/
export type GetVersionedTransactionConfig = {
/** The level of finality desired */
commitment?: Finality;
/** The max transaction version to return in responses. If the requested transaction is a higher version, an error will be returned */
maxSupportedTransactionVersion?: number;
};
@ -869,6 +915,8 @@ export type ConfirmedTransactionMeta = {
postTokenBalances?: Array<TokenBalance> | null;
/** The error result of transaction processing */
err: TransactionError | null;
/** The collection of addresses loaded using address lookup tables */
loadedAddresses?: LoadedAddresses;
};
/**
@ -890,6 +938,38 @@ export type TransactionResponse = {
blockTime?: number | null;
};
/**
* A processed transaction from the RPC API
*/
export type VersionedTransactionResponse = {
/** The slot during which the transaction was processed */
slot: number;
/** The transaction */
transaction: {
/** The transaction message */
message: VersionedMessage;
/** The transaction signatures */
signatures: string[];
};
/** Metadata produced from the transaction */
meta: ConfirmedTransactionMeta | null;
/** The unix timestamp of when the transaction was processed */
blockTime?: number | null;
/** The transaction version */
version?: TransactionVersion;
};
/**
* A processed transaction message from the RPC API
*/
type MessageResponse = {
accountKeys: string[];
header: MessageHeader;
instructions: CompiledInstruction[];
recentBlockhash: string;
addressTableLookups?: ParsedAddressTableLookup[];
};
/**
* A confirmed transaction on the ledger
*
@ -942,6 +1022,18 @@ export type ParsedInstruction = {
parsed: any;
};
/**
* A parsed address table lookup
*/
export type ParsedAddressTableLookup = {
/** Address lookup table account key */
accountKey: PublicKey;
/** Parsed instruction info */
writableIndexes: number[];
/** Parsed instruction info */
readonlyIndexes: number[];
};
/**
* A parsed transaction message
*/
@ -952,6 +1044,8 @@ export type ParsedMessage = {
instructions: (ParsedInstruction | PartiallyDecodedInstruction)[];
/** Recent blockhash */
recentBlockhash: string;
/** Address table lookups used to load additional accounts */
addressTableLookups?: ParsedAddressTableLookup[] | null;
};
/**
@ -983,6 +1077,8 @@ export type ParsedTransactionWithMeta = {
meta: ParsedTransactionMeta | null;
/** The unix timestamp of when the transaction was processed */
blockTime?: number | null;
/** The version of the transaction message */
version?: TransactionVersion;
};
/**
@ -1006,6 +1102,47 @@ export type BlockResponse = {
};
/** Metadata produced from the transaction */
meta: ConfirmedTransactionMeta | null;
/** The transaction version */
version?: TransactionVersion;
}>;
/** Vector of block rewards */
rewards?: Array<{
/** Public key of reward recipient */
pubkey: string;
/** Reward value in lamports */
lamports: number;
/** Account balance after reward is applied */
postBalance: number | null;
/** Type of reward received */
rewardType: string | null;
}>;
/** The unix timestamp of when the block was processed */
blockTime: number | null;
};
/**
* A processed block fetched from the RPC API
*/
export type VersionedBlockResponse = {
/** Blockhash of this block */
blockhash: Blockhash;
/** Blockhash of this block's parent */
previousBlockhash: Blockhash;
/** Slot index of this block's parent */
parentSlot: number;
/** Vector of transactions with status meta and original message */
transactions: Array<{
/** The transaction */
transaction: {
/** The transaction message */
message: VersionedMessage;
/** The transaction signatures */
signatures: string[];
};
/** Metadata produced from the transaction */
meta: ConfirmedTransactionMeta | null;
/** The transaction version */
version?: TransactionVersion;
}>;
/** Vector of block rewards */
rewards?: Array<{
@ -1728,6 +1865,12 @@ const GetSignatureStatusesRpcResult = jsonRpcResultAndContext(
*/
const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult(number());
const AddressTableLookupStruct = pick({
accountKey: PublicKeyFromString,
writableIndexes: array(number()),
readonlyIndexes: array(number()),
});
const ConfirmedTransactionResult = pick({
signatures: array(string()),
message: pick({
@ -1745,6 +1888,7 @@ const ConfirmedTransactionResult = pick({
}),
),
recentBlockhash: string(),
addressTableLookups: optional(array(AddressTableLookupStruct)),
}),
});
@ -1805,6 +1949,7 @@ const ParsedConfirmedTransactionResult = pick({
),
instructions: array(ParsedOrRawInstruction),
recentBlockhash: string(),
addressTableLookups: optional(nullable(array(AddressTableLookupStruct))),
}),
});
@ -1874,6 +2019,8 @@ const ParsedConfirmedTransactionMetaResult = pick({
loadedAddresses: optional(LoadedAddressesResult),
});
const TransactionVersionStruct = union([literal(0), literal('legacy')]);
/**
* Expected JSON RPC response for the "getBlock" message
*/
@ -1887,6 +2034,7 @@ const GetBlockRpcResult = jsonRpcResult(
pick({
transaction: ConfirmedTransactionResult,
meta: nullable(ConfirmedTransactionMetaResult),
version: optional(TransactionVersionStruct),
}),
),
rewards: optional(
@ -1962,6 +2110,7 @@ const GetTransactionRpcResult = jsonRpcResult(
meta: ConfirmedTransactionMetaResult,
blockTime: optional(nullable(number())),
transaction: ConfirmedTransactionResult,
version: optional(TransactionVersionStruct),
}),
),
);
@ -1976,6 +2125,7 @@ const GetParsedTransactionRpcResult = jsonRpcResult(
transaction: ParsedConfirmedTransactionResult,
meta: nullable(ParsedConfirmedTransactionMetaResult),
blockTime: optional(nullable(number())),
version: optional(TransactionVersionStruct),
}),
),
);
@ -3644,11 +3794,32 @@ export class Connection {
/**
* Fetch a processed block from the cluster.
*
* @deprecated Instead, call `getBlock` using a `GetVersionedBlockConfig` by
* setting the `maxSupportedTransactionVersion` property.
*/
async getBlock(
slot: number,
rawConfig?: GetBlockConfig,
): Promise<BlockResponse | null> {
): Promise<BlockResponse | null>;
/**
* Fetch a processed block from the cluster.
*/
// eslint-disable-next-line no-dupe-class-members
async getBlock(
slot: number,
rawConfig?: GetVersionedBlockConfig,
): Promise<VersionedBlockResponse | null>;
/**
* Fetch a processed block from the cluster.
*/
// eslint-disable-next-line no-dupe-class-members
async getBlock(
slot: number,
rawConfig?: GetVersionedBlockConfig,
): Promise<VersionedBlockResponse | null> {
const {commitment, config} = extractCommitmentFromConfig(rawConfig);
const args = this._buildArgsAtLeastConfirmed(
[slot],
@ -3668,16 +3839,14 @@ export class Connection {
return {
...result,
transactions: result.transactions.map(({transaction, meta}) => {
const message = new Message(transaction.message);
return {
meta,
transaction: {
...transaction,
message,
},
};
}),
transactions: result.transactions.map(({transaction, meta, version}) => ({
meta,
transaction: {
...transaction,
message: versionedMessageFromResponse(version, transaction.message),
},
version,
})),
};
}
@ -3739,11 +3908,33 @@ export class Connection {
/**
* Fetch a confirmed or finalized transaction from the cluster.
*
* @deprecated Instead, call `getTransaction` using a
* `GetVersionedTransactionConfig` by setting the
* `maxSupportedTransactionVersion` property.
*/
async getTransaction(
signature: string,
rawConfig?: GetTransactionConfig,
): Promise<TransactionResponse | null> {
): Promise<TransactionResponse | null>;
/**
* Fetch a confirmed or finalized transaction from the cluster.
*/
// eslint-disable-next-line no-dupe-class-members
async getTransaction(
signature: string,
rawConfig: GetVersionedTransactionConfig,
): Promise<VersionedTransactionResponse | null>;
/**
* Fetch a confirmed or finalized transaction from the cluster.
*/
// eslint-disable-next-line no-dupe-class-members
async getTransaction(
signature: string,
rawConfig?: GetVersionedTransactionConfig,
): Promise<VersionedTransactionResponse | null> {
const {commitment, config} = extractCommitmentFromConfig(rawConfig);
const args = this._buildArgsAtLeastConfirmed(
[signature],
@ -3764,7 +3955,10 @@ export class Connection {
...result,
transaction: {
...result.transaction,
message: new Message(result.transaction.message),
message: versionedMessageFromResponse(
result.version,
result.transaction.message,
),
},
};
}
@ -3774,8 +3968,8 @@ export class Connection {
*/
async getParsedTransaction(
signature: TransactionSignature,
commitmentOrConfig?: GetTransactionConfig | Finality,
): Promise<ParsedConfirmedTransaction | null> {
commitmentOrConfig?: GetVersionedTransactionConfig | Finality,
): Promise<ParsedTransactionWithMeta | null> {
const {commitment, config} =
extractCommitmentFromConfig(commitmentOrConfig);
const args = this._buildArgsAtLeastConfirmed(
@ -3797,8 +3991,8 @@ export class Connection {
*/
async getParsedTransactions(
signatures: TransactionSignature[],
commitmentOrConfig?: GetTransactionConfig | Finality,
): Promise<(ParsedConfirmedTransaction | null)[]> {
commitmentOrConfig?: GetVersionedTransactionConfig | Finality,
): Promise<(ParsedTransactionWithMeta | null)[]> {
const {commitment, config} =
extractCommitmentFromConfig(commitmentOrConfig);
const batch = signatures.map(signature => {
@ -3829,11 +4023,37 @@ export class Connection {
/**
* Fetch transaction details for a batch of confirmed transactions.
* Similar to {@link getParsedTransactions} but returns a {@link TransactionResponse}.
*
* @deprecated Instead, call `getTransactions` using a
* `GetVersionedTransactionConfig` by setting the
* `maxSupportedTransactionVersion` property.
*/
async getTransactions(
signatures: TransactionSignature[],
commitmentOrConfig?: GetTransactionConfig | Finality,
): Promise<(TransactionResponse | null)[]> {
): Promise<(TransactionResponse | null)[]>;
/**
* Fetch transaction details for a batch of confirmed transactions.
* Similar to {@link getParsedTransactions} but returns a {@link
* VersionedTransactionResponse}.
*/
// eslint-disable-next-line no-dupe-class-members
async getTransactions(
signatures: TransactionSignature[],
commitmentOrConfig: GetVersionedTransactionConfig | Finality,
): Promise<(VersionedTransactionResponse | null)[]>;
/**
* Fetch transaction details for a batch of confirmed transactions.
* Similar to {@link getParsedTransactions} but returns a {@link
* VersionedTransactionResponse}.
*/
// eslint-disable-next-line no-dupe-class-members
async getTransactions(
signatures: TransactionSignature[],
commitmentOrConfig: GetVersionedTransactionConfig | Finality,
): Promise<(VersionedTransactionResponse | null)[]> {
const {commitment, config} =
extractCommitmentFromConfig(commitmentOrConfig);
const batch = signatures.map(signature => {
@ -3862,7 +4082,10 @@ export class Connection {
...result,
transaction: {
...result.transaction,
message: new Message(result.transaction.message),
message: versionedMessageFromResponse(
result.version,
result.transaction.message,
),
},
};
});

View File

@ -3256,7 +3256,6 @@ describe('Connection', function () {
11111,
);
console.log('create mint');
const mintPubkey2 = await splToken.createMint(
connection as any,
payerKeypair,
@ -4249,30 +4248,44 @@ describe('Connection', function () {
expect(version['solana-core']).to.be.ok;
}).timeout(20 * 1000);
it('getAddressLookupTable', async () => {
let lookupTableKey: PublicKey;
const lookupTableAddresses = new Array(10)
.fill(0)
.map(() => Keypair.generate().publicKey);
describe('address lookup table program', () => {
const connection = new Connection(url);
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: LAMPORTS_PER_SOL,
before(async () => {
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: 10 * LAMPORTS_PER_SOL,
});
});
const lookupTableAddresses = new Array(10)
.fill(0)
.map(() => Keypair.generate().publicKey);
it('createLookupTable', async () => {
const recentSlot = await connection.getSlot('finalized');
const recentSlot = await connection.getSlot('finalized');
const [createIx, lookupTableKey] =
AddressLookupTableProgram.createLookupTable({
recentSlot,
payer: payer.publicKey,
authority: payer.publicKey,
let createIx: TransactionInstruction;
[createIx, lookupTableKey] =
AddressLookupTableProgram.createLookupTable({
recentSlot,
payer: payer.publicKey,
authority: payer.publicKey,
});
await helpers.processTransaction({
connection,
transaction: new Transaction().add(createIx),
signers: [payer],
commitment: 'processed',
});
});
// create, extend, and fetch
{
const transaction = new Transaction().add(createIx).add(
it('extendLookupTable', async () => {
const transaction = new Transaction().add(
AddressLookupTableProgram.extendLookupTable({
lookupTable: lookupTableKey,
addresses: lookupTableAddresses,
@ -4280,44 +4293,32 @@ describe('Connection', function () {
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;
}
expect(lookupTableAccount.isActive()).to.be.true;
expect(lookupTableAccount.state.authority).to.eql(payer.publicKey);
expect(lookupTableAccount.state.addresses).to.eql(lookupTableAddresses);
}
// freeze and fetch
{
it('freezeLookupTable', async () => {
const transaction = new Transaction().add(
AddressLookupTableProgram.freezeLookupTable({
lookupTable: lookupTableKey,
authority: payer.publicKey,
}),
);
await helpers.processTransaction({
connection,
transaction,
signers: [payer],
commitment: 'processed',
});
});
it('getAddressLookupTable', async () => {
const lookupTableResponse = await connection.getAddressLookupTable(
lookupTableKey,
{
@ -4331,50 +4332,31 @@ describe('Connection', function () {
}
expect(lookupTableAccount.isActive()).to.be.true;
expect(lookupTableAccount.state.authority).to.be.undefined;
}
expect(lookupTableAccount.state.addresses).to.eql(lookupTableAddresses);
});
});
it('sendRawTransaction with v0 transaction', async () => {
describe('v0 transaction', () => {
const connection = new Connection(url);
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: 10 * LAMPORTS_PER_SOL,
before(async () => {
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',
});
// wait for lookup table to be usable
before(async () => {
const lookupTableResponse = await connection.getAddressLookupTable(
lookupTableKey,
{
commitment: 'processed',
},
);
const lookupTableAccount = lookupTableResponse.value;
if (!lookupTableAccount) {
expect(lookupTableAccount).to.be.ok;
@ -4383,7 +4365,7 @@ describe('Connection', function () {
// eslint-disable-next-line no-constant-condition
while (true) {
const latestSlot = await connection.getSlot('processed');
const latestSlot = await connection.getSlot('confirmed');
if (latestSlot > lookupTableAccount.state.lastExtendedSlot) {
break;
} else {
@ -4391,15 +4373,23 @@ describe('Connection', function () {
await sleep(500);
}
}
}
});
// create, serialize, send and confirm versioned transaction
{
let signature;
let addressTableLookups;
it('send and confirm', async () => {
const {blockhash, lastValidBlockHeight} =
await connection.getLatestBlockhash();
const transferIxData = encodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, {
lamports: BigInt(LAMPORTS_PER_SOL),
});
addressTableLookups = [
{
accountKey: lookupTableKey,
writableIndexes: [0],
readonlyIndexes: [],
},
];
const transaction = new VersionedTransaction(
new MessageV0({
header: {
@ -4416,20 +4406,14 @@ describe('Connection', function () {
data: transferIxData,
},
],
addressTableLookups: [
{
accountKey: lookupTableKey,
writableIndexes: [0],
readonlyIndexes: [],
},
],
addressTableLookups,
}),
);
transaction.sign([payer]);
const signature = bs58.encode(transaction.signatures[0]);
signature = bs58.encode(transaction.signatures[0]);
const serializedTransaction = transaction.serialize();
await connection.sendRawTransaction(serializedTransaction, {
preflightCommitment: 'processed',
preflightCommitment: 'confirmed',
});
await connection.confirmTransaction(
@ -4438,16 +4422,106 @@ describe('Connection', function () {
blockhash,
lastValidBlockHeight,
},
'processed',
'confirmed',
);
const transferToKey = lookupTableAddresses[0];
const transferToAccount = await connection.getAccountInfo(
transferToKey,
'processed',
'confirmed',
);
expect(transferToAccount?.lamports).to.be.eq(LAMPORTS_PER_SOL);
}
});
});
it('getTransaction (failure)', async () => {
await expect(
connection.getTransaction(signature, {
commitment: 'confirmed',
}),
).to.be.rejectedWith(
'failed to get transaction: Transaction version (0) is not supported',
);
});
let transactionSlot;
it('getTransaction', async () => {
// fetch v0 transaction
const fetchedTransaction = await connection.getTransaction(signature, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
});
if (fetchedTransaction === null) {
expect(fetchedTransaction).to.not.be.null;
return;
}
transactionSlot = fetchedTransaction.slot;
expect(fetchedTransaction.version).to.eq(0);
expect(fetchedTransaction.meta?.loadedAddresses).to.eql({
readonly: [],
writable: [lookupTableAddresses[0]],
});
expect(
fetchedTransaction.transaction.message.addressTableLookups,
).to.eql(addressTableLookups);
});
it('getParsedTransaction (failure)', async () => {
await expect(
connection.getParsedTransaction(signature, {
commitment: 'confirmed',
}),
).to.be.rejectedWith(
'failed to get transaction: Transaction version (0) is not supported',
);
});
it('getParsedTransaction', async () => {
const parsedTransaction = await connection.getParsedTransaction(
signature,
{
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
},
);
expect(parsedTransaction).to.not.be.null;
expect(parsedTransaction?.version).to.eq(0);
expect(parsedTransaction?.meta?.loadedAddresses).to.eql({
readonly: [],
writable: [lookupTableAddresses[0]],
});
expect(
parsedTransaction?.transaction.message.addressTableLookups,
).to.eql(addressTableLookups);
});
it('getBlock (failure)', async () => {
await expect(
connection.getBlock(transactionSlot, {
maxSupportedTransactionVersion: undefined,
commitment: 'confirmed',
}),
).to.be.rejectedWith(
'failed to get confirmed block: Transaction version (0) is not supported',
);
});
it('getBlock', async () => {
const block = await connection.getBlock(transactionSlot, {
maxSupportedTransactionVersion: 0,
commitment: 'confirmed',
});
expect(block).to.not.be.null;
if (block === null) throw new Error(); // unreachable
let foundTx = false;
for (const tx of block.transactions) {
if (tx.transaction.signatures[0] === signature) {
foundTx = true;
expect(tx.version).to.eq(0);
}
}
expect(foundTx).to.be.true;
});
}).timeout(5 * 1000);
}
});