Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: solana-labs/solana-web3.js
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.55.0
Choose a base ref
...
head repository: solana-labs/solana-web3.js
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.56.0
Choose a head ref
  • 1 commit
  • 2 files changed
  • 1 contributor

Commits on Aug 31, 2022

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

    feat: update Connection to support versioned transactions
    jstarry authored Aug 31, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    KinectTheUnknown David-Joseph Xayavong
    Copy the full SHA
    0f27496 View commit details
Showing with 402 additions and 105 deletions.
  1. +244 −21 src/connection.ts
  2. +158 −84 test/connection.test.ts
265 changes: 244 additions & 21 deletions src/connection.ts
Original file line number Diff line number Diff line change
@@ -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,
),
},
};
});
242 changes: 158 additions & 84 deletions test/connection.test.ts
Original file line number Diff line number Diff line change
@@ -3256,7 +3256,6 @@ describe('Connection', function () {
11111,
);

console.log('create mint');
const mintPubkey2 = await splToken.createMint(
connection as any,
payerKeypair,
@@ -4249,75 +4248,77 @@ 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');

let createIx: TransactionInstruction;
[createIx, lookupTableKey] =
AddressLookupTableProgram.createLookupTable({
recentSlot,
payer: payer.publicKey,
authority: payer.publicKey,
});

const recentSlot = await connection.getSlot('finalized');
const [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,
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;
}
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,
});

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({
before(async () => {
await helpers.airdrop({
connection,
transaction,
signers: [payer],
commitment: 'processed',
address: payer.publicKey,
amount: 10 * LAMPORTS_PER_SOL,
});
});

// 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,23 +4365,31 @@ 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 {
console.log('Waiting for next slot...');
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);
}
});