solana/web3.js/test/connection.test.ts

5951 lines
185 KiB
TypeScript

import bs58 from 'bs58';
import {Buffer} from 'buffer';
import * as splToken from '@solana/spl-token';
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {Agent as HttpAgent} from 'http';
import {Agent as HttpsAgent} from 'https';
import {match, mock, spy, useFakeTimers, SinonFakeTimers} from 'sinon';
import sinonChai from 'sinon-chai';
import {fail} from 'assert';
import {
Authorized,
Connection,
EpochSchedule,
SystemProgram,
Transaction,
LAMPORTS_PER_SOL,
Lockup,
PublicKey,
StakeProgram,
sendAndConfirmTransaction,
Keypair,
Message,
AddressLookupTableProgram,
SYSTEM_INSTRUCTION_LAYOUTS,
NONCE_ACCOUNT_LENGTH,
MessageAddressTableLookup,
} from '../src';
import invariant from '../src/utils/assert';
import {toBuffer} from '../src/utils/to-buffer';
import {MOCK_PORT, url, Node14Controller, nodeVersion} from './url';
import {
AccountInfo,
BLOCKHASH_CACHE_TIMEOUT_MS,
BlockResponse,
BlockSignatures,
Commitment,
ConfirmedBlock,
Context,
EpochInfo,
InflationGovernor,
InflationRate,
Logs,
SignatureResult,
SlotInfo,
} from '../src/connection';
import {sleep} from '../src/utils/sleep';
import {
helpers,
mockErrorMessage,
mockErrorResponse,
mockRpcBatchResponse,
mockRpcResponse,
mockServer,
} from './mocks/rpc-http';
import {
stubRpcWebSocket,
restoreRpcWebSocket,
mockRpcMessage,
} from './mocks/rpc-websockets';
import {
NonceInformation,
TransactionInstruction,
TransactionSignature,
TransactionExpiredBlockheightExceededError,
TransactionExpiredNonceInvalidError,
TransactionExpiredTimeoutError,
TransactionMessage,
} from '../src/transaction';
import type {
SignatureStatus,
TransactionError,
KeyedAccountInfo,
} from '../src/connection';
import {VersionedTransaction} from '../src/transaction/versioned';
import {MessageV0} from '../src/message/v0';
import {encodeData} from '../src/instruction';
use(chaiAsPromised);
use(sinonChai);
async function mockNonceAccountResponse(
nonceAccountPubkey: string,
nonceValue: string,
nonceAuthority: string,
slot?: number,
) {
const mockNonceAccountData = Buffer.alloc(NONCE_ACCOUNT_LENGTH);
mockNonceAccountData.fill(0);
// Authority starts after 4 version bytes and 4 state bytes.
mockNonceAccountData.set(bs58.decode(nonceAuthority), 4 + 4);
// Nonce hash starts 32 bytes after the authority.
mockNonceAccountData.set(bs58.decode(nonceValue), 4 + 4 + 32);
await mockRpcResponse({
method: 'getAccountInfo',
params: [nonceAccountPubkey, {encoding: 'base64'}],
value: {
owner: SystemProgram.programId.toBase58(),
lamports: LAMPORTS_PER_SOL,
data: [mockNonceAccountData.toString('base64'), 'base64'],
executable: false,
rentEpoch: 20,
},
slot,
withContext: true,
});
}
const verifySignatureStatus = (
status: SignatureStatus | null,
err?: TransactionError,
): SignatureStatus => {
if (status === null) {
expect(status).not.to.be.null;
throw new Error(); // unreachable
}
const expectedErr = err || null;
expect(status.err).to.eql(expectedErr);
expect(status.slot).to.be.at.least(0);
if (expectedErr !== null) return status;
const confirmations = status.confirmations;
if (typeof confirmations === 'number') {
expect(confirmations).to.be.at.least(0);
} else {
expect(confirmations).to.be.null;
}
return status;
};
describe('Connection', function () {
let connection: Connection;
beforeEach(() => {
connection = new Connection(url);
});
if (mockServer) {
const server = mockServer;
beforeEach(() => {
server.start(MOCK_PORT);
stubRpcWebSocket(connection);
});
afterEach(() => {
server.stop();
restoreRpcWebSocket(connection);
});
}
if (mockServer) {
it('should pass HTTP headers to RPC', async () => {
const headers = {
Authorization: 'Bearer 123',
};
let connection = new Connection(url, {
httpHeaders: headers,
});
await mockRpcResponse({
method: 'getVersion',
params: [],
value: {'solana-core': '0.20.4'},
withHeaders: headers,
});
expect(await connection.getVersion()).to.be.not.null;
});
it('should allow middleware to augment request', async () => {
let connection = new Connection(url, {
fetchMiddleware: (url, options, fetch) => {
if (options) {
options.headers = Object.assign(options.headers!, {
Authorization: 'Bearer 123',
});
fetch(url, options);
} else {
fail('options must be defined!');
}
},
});
await mockRpcResponse({
method: 'getVersion',
params: [],
value: {'solana-core': '0.20.4'},
withHeaders: {
Authorization: 'Bearer 123',
},
});
expect(await connection.getVersion()).to.be.not.null;
});
}
describe('override HTTP agent', () => {
let previousBrowserEnv: string | undefined;
beforeEach(() => {
previousBrowserEnv = process.env.BROWSER;
delete process.env.BROWSER;
});
afterEach(() => {
process.env.BROWSER = previousBrowserEnv;
});
it('uses no agent with fetch when `overrideAgent` is `false`', () => {
const fetch = spy();
const c = new Connection(url, {httpAgent: false, fetch});
c.getBlock(0);
expect(fetch).to.have.been.calledWith(
match.any,
match({agent: undefined}),
);
});
it('uses the supplied `overrideAgent` with fetch', () => {
const fetch = spy();
const httpAgent = new HttpsAgent();
const c = new Connection('https://example.com', {httpAgent, fetch});
c.getBlock(0);
expect(fetch).to.have.been.calledWith(
match.any,
match({agent: httpAgent}),
);
});
it('throws when the supplied `overrideAgent` is http but the endpoint is https', () => {
expect(() => {
new Connection('https://example.com', {httpAgent: new HttpAgent()});
}).to.throw;
});
it('throws when the supplied `overrideAgent` is https but the endpoint is http', () => {
expect(() => {
new Connection('http://example.com', {httpAgent: new HttpsAgent()});
}).to.throw;
});
});
it('should attribute middleware fatals to the middleware', async () => {
let connection = new Connection(url, {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
fetchMiddleware: (_url, _options, _fetch) => {
throw new Error('This middleware experienced a fatal error');
},
});
const error = await expect(connection.getVersion()).to.be.rejectedWith(
'This middleware experienced a fatal error',
);
expect(error)
.to.be.an.instanceOf(Error)
.and.to.have.property('stack')
.that.include('fetchMiddleware');
});
it('should not attribute fetch errors to the middleware', async () => {
let connection = new Connection(url, {
fetchMiddleware: (url, _options, fetch) => {
fetch(url, {
body: 'An `Object` was expected here; this is a `TypeError`.',
});
},
});
const error = await expect(connection.getVersion()).to.be.rejected;
expect(error)
.to.be.an.instanceOf(Error)
.and.to.have.property('stack')
.that.does.not.include('fetchMiddleware');
});
it('get account info - not found', async () => {
const account = Keypair.generate();
await mockRpcResponse({
method: 'getAccountInfo',
params: [account.publicKey.toBase58(), {encoding: 'base64'}],
value: null,
withContext: true,
});
expect(await connection.getAccountInfo(account.publicKey)).to.be.null;
await mockRpcResponse({
method: 'getAccountInfo',
params: [account.publicKey.toBase58(), {encoding: 'jsonParsed'}],
value: null,
withContext: true,
});
expect((await connection.getParsedAccountInfo(account.publicKey)).value).to
.be.null;
});
it('get multiple accounts info', async () => {
const account1 = Keypair.generate();
const account2 = Keypair.generate();
{
await helpers.airdrop({
connection,
address: account1.publicKey,
amount: LAMPORTS_PER_SOL,
});
await helpers.airdrop({
connection,
address: account2.publicKey,
amount: LAMPORTS_PER_SOL,
});
}
const value = [
{
owner: '11111111111111111111111111111111',
lamports: LAMPORTS_PER_SOL,
data: ['', 'base64'],
executable: false,
rentEpoch: 0,
space: 0,
},
{
owner: '11111111111111111111111111111111',
lamports: LAMPORTS_PER_SOL,
data: ['', 'base64'],
executable: false,
rentEpoch: 0,
space: 0,
},
];
await mockRpcResponse({
method: 'getMultipleAccounts',
params: [
[account1.publicKey.toBase58(), account2.publicKey.toBase58()],
{encoding: 'base64'},
],
value: value,
withContext: true,
});
const res = await connection.getMultipleAccountsInfo(
[account1.publicKey, account2.publicKey],
'confirmed',
);
const expectedValue = [
{
owner: new PublicKey('11111111111111111111111111111111'),
lamports: LAMPORTS_PER_SOL,
data: Buffer.from([]),
executable: false,
rentEpoch: 0,
space: 0,
},
{
owner: new PublicKey('11111111111111111111111111111111'),
lamports: LAMPORTS_PER_SOL,
data: Buffer.from([]),
executable: false,
rentEpoch: 0,
space: 0,
},
];
expect(res).to.eql(expectedValue);
});
it('get program accounts', async () => {
const account0 = Keypair.generate();
const account1 = Keypair.generate();
const programId = Keypair.generate();
{
await helpers.airdrop({
connection,
address: account0.publicKey,
amount: LAMPORTS_PER_SOL,
});
const transaction = new Transaction().add(
SystemProgram.assign({
accountPubkey: account0.publicKey,
programId: programId.publicKey,
}),
);
await helpers.processTransaction({
connection,
transaction,
signers: [account0],
commitment: 'confirmed',
});
}
{
await helpers.airdrop({
connection,
address: account1.publicKey,
amount: 0.5 * LAMPORTS_PER_SOL,
});
const transaction = new Transaction().add(
SystemProgram.assign({
accountPubkey: account1.publicKey,
programId: programId.publicKey,
}),
);
await helpers.processTransaction({
connection,
transaction,
signers: [account1],
commitment: 'confirmed',
});
}
const feeCalculator = (await helpers.recentBlockhash({connection}))
.feeCalculator;
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{commitment: 'confirmed', encoding: 'base64'},
],
value: [
{
account: {
data: ['', 'base64'],
executable: false,
lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account0.publicKey.toBase58(),
},
{
account: {
data: ['', 'base64'],
executable: false,
lamports:
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account1.publicKey.toBase58(),
},
],
});
const programAccounts = await connection.getProgramAccounts(
programId.publicKey,
{
commitment: 'confirmed',
},
);
expect(programAccounts).to.have.length(2);
programAccounts.forEach(function (keyedAccount) {
if (keyedAccount.pubkey.equals(account0.publicKey)) {
expect(keyedAccount.account.lamports).to.eq(
LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else {
expect(keyedAccount.pubkey).to.eql(account1.publicKey);
expect(keyedAccount.account.lamports).to.eq(
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
}
});
}
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{commitment: 'confirmed', encoding: 'base64'},
],
value: [
{
account: {
data: ['', 'base64'],
executable: false,
lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account0.publicKey.toBase58(),
},
{
account: {
data: ['', 'base64'],
executable: false,
lamports:
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account1.publicKey.toBase58(),
},
],
});
const programAccounts = await connection.getProgramAccounts(
programId.publicKey,
'confirmed',
);
expect(programAccounts).to.have.length(2);
programAccounts.forEach(function (keyedAccount) {
if (keyedAccount.pubkey.equals(account0.publicKey)) {
expect(keyedAccount.account.lamports).to.eq(
LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else {
expect(keyedAccount.pubkey).to.eql(account1.publicKey);
expect(keyedAccount.account.lamports).to.eq(
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
}
});
}
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{
commitment: 'confirmed',
encoding: 'base64',
filters: [
{
dataSize: 0,
},
],
},
],
value: [
{
account: {
data: ['', 'base64'],
executable: false,
lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account0.publicKey.toBase58(),
},
{
account: {
data: ['', 'base64'],
executable: false,
lamports:
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account1.publicKey.toBase58(),
},
],
});
const programAccountsDoMatchFilter = await connection.getProgramAccounts(
programId.publicKey,
{
commitment: 'confirmed',
encoding: 'base64',
filters: [{dataSize: 0}],
},
);
expect(programAccountsDoMatchFilter).to.have.length(2);
}
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{
commitment: 'confirmed',
encoding: 'base64',
filters: [
{
memcmp: {
offset: 0,
bytes: 'XzdZ3w',
},
},
],
},
],
value: [],
});
const programAccountsDontMatchFilter =
await connection.getProgramAccounts(programId.publicKey, {
commitment: 'confirmed',
filters: [
{
memcmp: {
offset: 0,
bytes: 'XzdZ3w',
},
},
],
});
expect(programAccountsDontMatchFilter).to.have.length(0);
}
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{commitment: 'confirmed', encoding: 'jsonParsed'},
],
value: [
{
account: {
data: ['', 'base64'],
executable: false,
lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account0.publicKey.toBase58(),
},
{
account: {
data: ['', 'base64'],
executable: false,
lamports:
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account1.publicKey.toBase58(),
},
],
});
const programAccounts = await connection.getParsedProgramAccounts(
programId.publicKey,
{
commitment: 'confirmed',
},
);
expect(programAccounts).to.have.length(2);
programAccounts.forEach(function (element) {
if (element.pubkey.equals(account0.publicKey)) {
expect(element.account.lamports).to.eq(
LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else {
expect(element.pubkey).to.eql(account1.publicKey);
expect(element.account.lamports).to.eq(
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
}
});
}
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{
commitment: 'confirmed',
encoding: 'jsonParsed',
filters: [
{
dataSize: 2,
},
],
},
],
value: [],
});
const programAccountsDontMatchFilter =
await connection.getParsedProgramAccounts(programId.publicKey, {
commitment: 'confirmed',
filters: [{dataSize: 2}],
});
expect(programAccountsDontMatchFilter).to.have.length(0);
}
{
await mockRpcResponse({
method: 'getProgramAccounts',
params: [
programId.publicKey.toBase58(),
{
commitment: 'confirmed',
encoding: 'jsonParsed',
filters: [
{
memcmp: {
offset: 0,
bytes: '',
},
},
],
},
],
value: [
{
account: {
data: ['', 'base64'],
executable: false,
lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account0.publicKey.toBase58(),
},
{
account: {
data: ['', 'base64'],
executable: false,
lamports:
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
owner: programId.publicKey.toBase58(),
rentEpoch: 20,
},
pubkey: account1.publicKey.toBase58(),
},
],
});
const programAccountsDoMatchFilter =
await connection.getParsedProgramAccounts(programId.publicKey, {
commitment: 'confirmed',
filters: [
{
memcmp: {
offset: 0,
bytes: '',
},
},
],
});
expect(programAccountsDoMatchFilter).to.have.length(2);
}
}).timeout(30 * 1000);
it('get balance', async () => {
const account = Keypair.generate();
await mockRpcResponse({
method: 'getBalance',
params: [account.publicKey.toBase58()],
value: {
context: {
slot: 11,
},
value: 0,
},
});
const balance = await connection.getBalance(account.publicKey);
expect(balance).to.be.at.least(0);
});
it('get inflation', async () => {
await mockRpcResponse({
method: 'getInflationGovernor',
params: [],
value: {
foundation: 0.05,
foundationTerm: 7.0,
initial: 0.15,
taper: 0.15,
terminal: 0.015,
},
});
const inflation = await connection.getInflationGovernor();
const inflationKeys: (keyof InflationGovernor)[] = [
'initial',
'terminal',
'taper',
'foundation',
'foundationTerm',
];
for (const key of inflationKeys) {
expect(inflation).to.have.property(key);
expect(inflation[key]).to.be.greaterThan(0);
}
});
it('get inflation reward', async () => {
if (mockServer) {
await mockRpcResponse({
method: 'getInflationReward',
params: [
[
'7GHnTRB8Rz14qZQhDXf8ox1Kfu7mPcPLpKaBJJirmYj2',
'CrinLuHjVGDDcQfrEoCmM4k31Ni9sMoTCEEvNSUSh7Jg',
],
{
epoch: 0,
},
],
value: [
{
amount: 3646143,
effectiveSlot: 432000,
epoch: 0,
postBalance: 30504783,
commission: 0,
},
null,
],
});
const inflationReward = await connection.getInflationReward(
[
new PublicKey('7GHnTRB8Rz14qZQhDXf8ox1Kfu7mPcPLpKaBJJirmYj2'),
new PublicKey('CrinLuHjVGDDcQfrEoCmM4k31Ni9sMoTCEEvNSUSh7Jg'),
],
0,
);
expect(inflationReward).to.have.lengthOf(2);
}
});
it('get inflation rate', async () => {
await mockRpcResponse({
method: 'getInflationRate',
params: [],
value: {
total: 0.08,
validator: 0.076,
foundation: 0.004,
epoch: 1,
},
});
const inflation = await connection.getInflationRate();
const inflationKeys: (keyof InflationRate)[] = [
'total',
'validator',
'foundation',
'epoch',
];
for (const key of inflationKeys) {
expect(inflation).to.have.property(key);
if (mockServer) {
expect(inflation[key]).to.be.greaterThan(0);
} else {
expect(inflation[key]).to.be.at.least(0);
}
}
});
it('get epoch info', async () => {
await mockRpcResponse({
method: 'getEpochInfo',
params: [{commitment: 'confirmed'}],
value: {
epoch: 0,
slotIndex: 1,
slotsInEpoch: 8192,
absoluteSlot: 1,
blockHeight: 1,
},
});
const epochInfo = await connection.getEpochInfo('confirmed');
const epochInfoKeys: (keyof EpochInfo)[] = [
'epoch',
'slotIndex',
'slotsInEpoch',
'absoluteSlot',
'blockHeight',
];
for (const key of epochInfoKeys) {
expect(epochInfo).to.have.property(key);
expect(epochInfo[key]).to.be.at.least(0);
}
});
it('get epoch schedule', async () => {
await mockRpcResponse({
method: 'getEpochSchedule',
params: [],
value: {
firstNormalEpoch: 8,
firstNormalSlot: 8160,
leaderScheduleSlotOffset: 8192,
slotsPerEpoch: 8192,
warmup: true,
},
});
const epochSchedule = await connection.getEpochSchedule();
const epochScheduleKeys: (keyof EpochSchedule)[] = [
'firstNormalEpoch',
'firstNormalSlot',
'leaderScheduleSlotOffset',
'slotsPerEpoch',
];
for (const key of epochScheduleKeys) {
expect(epochSchedule).to.have.property('warmup');
expect(epochSchedule).to.have.property(key);
if (epochSchedule.warmup) {
expect(epochSchedule[key]).to.be.greaterThan(0);
}
}
});
it('get leader schedule', async () => {
await mockRpcResponse({
method: 'getLeaderSchedule',
params: [],
value: {
'123vij84ecQEKUvQ7gYMKxKwKF6PbYSzCzzURYA4xULY': [0, 1, 2, 3],
'8PTjAikKoAybKXcEPnDSoy8wSNNikUBJ1iKawJKQwXnB': [4, 5, 6, 7],
},
});
const leaderSchedule = await connection.getLeaderSchedule();
expect(Object.keys(leaderSchedule).length).to.be.at.least(1);
for (const key in leaderSchedule) {
const slots = leaderSchedule[key];
expect(Array.isArray(slots)).to.be.true;
expect(slots.length).to.be.at.least(4);
}
});
it('get slot', async () => {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 123,
});
const slot = await connection.getSlot();
if (mockServer) {
expect(slot).to.eq(123);
} else {
// No idea what the correct slot value should be on a live cluster, so
// just check the type
expect(typeof slot).to.eq('number');
}
});
it('get slot leader', async () => {
await mockRpcResponse({
method: 'getSlotLeader',
params: [],
value: '11111111111111111111111111111111',
});
const slotLeader = await connection.getSlotLeader();
if (mockServer) {
expect(slotLeader).to.eq('11111111111111111111111111111111');
} else {
// No idea what the correct slotLeader value should be on a live cluster, so
// just check the type
expect(typeof slotLeader).to.eq('string');
}
});
it('get slot leaders', async () => {
await mockRpcResponse({
method: 'getSlotLeaders',
params: [0, 1],
value: ['11111111111111111111111111111111'],
});
const slotLeaders = await connection.getSlotLeaders(0, 1);
expect(slotLeaders).to.have.length(1);
expect(slotLeaders[0]).to.be.instanceOf(PublicKey);
});
it('get cluster nodes', async () => {
await mockRpcResponse({
method: 'getClusterNodes',
params: [],
value: [
{
pubkey: '11111111111111111111111111111111',
gossip: '127.0.0.0:1234',
tpu: '127.0.0.0:1235',
rpc: null,
version: '1.1.10',
},
],
});
const clusterNodes = await connection.getClusterNodes();
if (mockServer) {
expect(clusterNodes).to.have.length(1);
expect(clusterNodes[0].pubkey).to.eq('11111111111111111111111111111111');
expect(typeof clusterNodes[0].gossip).to.eq('string');
expect(typeof clusterNodes[0].tpu).to.eq('string');
expect(clusterNodes[0].rpc).to.be.null;
} else {
// There should be at least one node (the node that we're talking to)
expect(clusterNodes.length).to.be.greaterThan(0);
}
});
if (process.env.TEST_LIVE) {
it('get vote accounts', async () => {
const voteAccounts = await connection.getVoteAccounts();
expect(
voteAccounts.current.concat(voteAccounts.delinquent).length,
).to.be.greaterThan(0);
});
}
if (process.env.TEST_LIVE) {
describe('transaction confirmation (live)', () => {
let connection: Connection;
beforeEach(() => {
connection = new Connection(url, 'confirmed');
});
describe('blockheight based transaction confirmation', () => {
let latestBlockhash: {blockhash: string; lastValidBlockHeight: number};
let signature: string;
beforeEach(async function () {
this.timeout(60 * 1000);
const keypair = Keypair.generate();
const [
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_,
blockhash,
] = await Promise.all([
connection.confirmTransaction(
await connection.requestAirdrop(
keypair.publicKey,
LAMPORTS_PER_SOL,
),
),
helpers.latestBlockhash({connection}),
]);
latestBlockhash = blockhash;
const ix = new TransactionInstruction({
keys: [
{
pubkey: keypair.publicKey,
isSigner: true,
isWritable: true,
},
],
programId: new PublicKey(
'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
),
data: Buffer.from('Hello world', 'utf8'),
});
const transaction = new Transaction({
...latestBlockhash,
});
transaction.add(ix);
transaction.sign(keypair);
signature = await connection.sendTransaction(transaction, [keypair]);
});
it('confirms transactions using the last valid blockheight strategy', async () => {
let result = await connection.confirmTransaction(
{
signature,
...latestBlockhash,
},
'processed',
);
expect(result.value).to.have.property('err', null);
}).timeout(60 * 1000);
it('throws when confirming using a blockhash whose last valid blockheight has passed', async () => {
const confirmationPromise = connection.confirmTransaction({
signature,
...latestBlockhash,
lastValidBlockHeight: (await connection.getBlockHeight()) - 1, // Simulate the blockheight having passed.
});
expect(confirmationPromise).to.eventually.be.rejectedWith(
TransactionExpiredBlockheightExceededError,
);
}).timeout(60 * 1000);
});
describe('nonce-based transaction confirmation', () => {
let keypair: Keypair;
let minContextSlot: number;
let nonceInfo: NonceInformation;
let nonceKeypair: Keypair;
let transaction: Transaction;
beforeEach(async function () {
this.timeout(60 * 1000);
keypair = Keypair.generate();
nonceKeypair = Keypair.generate();
const [
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_,
blockhash,
minimumNonceAccountRentLamports,
] = await Promise.all([
connection.confirmTransaction(
await connection.requestAirdrop(
keypair.publicKey,
LAMPORTS_PER_SOL,
),
),
helpers.latestBlockhash({connection}),
connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH),
]);
const createNonceAccountTransaction =
SystemProgram.createNonceAccount({
authorizedPubkey: keypair.publicKey,
fromPubkey: keypair.publicKey,
lamports: minimumNonceAccountRentLamports,
noncePubkey: nonceKeypair.publicKey,
});
createNonceAccountTransaction.recentBlockhash = blockhash.blockhash;
createNonceAccountTransaction.feePayer = keypair.publicKey;
const createNonceAccountTransactionSignature =
await connection.sendTransaction(createNonceAccountTransaction, [
keypair,
nonceKeypair,
]);
const {context} = await connection.confirmTransaction({
...blockhash,
signature: createNonceAccountTransactionSignature,
});
minContextSlot = context.slot;
const nonceAccount = await connection.getNonce(
nonceKeypair.publicKey,
{minContextSlot},
);
nonceInfo = {
nonce: nonceAccount!.nonce,
nonceInstruction: SystemProgram.nonceAdvance({
authorizedPubkey: keypair.publicKey,
noncePubkey: nonceKeypair.publicKey,
}),
};
invariant(
nonceAccount,
'Expected a nonce account to have been created in the test setup',
);
const ix = new TransactionInstruction({
keys: [
{
pubkey: keypair.publicKey,
isSigner: true,
isWritable: true,
},
],
programId: new PublicKey(
'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
),
data: Buffer.from('Hello world', 'utf8'),
});
transaction = new Transaction({minContextSlot, nonceInfo});
transaction.add(ix);
transaction.sign(keypair);
});
it('confirms transactions using the durable nonce strategy', async () => {
const signature = await connection.sendTransaction(transaction, [
keypair,
]);
const result = await connection.confirmTransaction(
{
minContextSlot,
nonceAccountPubkey: nonceKeypair.publicKey,
nonceValue: nonceInfo.nonce,
signature,
},
'processed',
);
expect(result.value).to.have.property('err', null);
}).timeout(60 * 1000);
it('throws when confirming using a nonce that is no longer valid', async () => {
// Advance the nonce.
const blockhash = await connection.getLatestBlockhash();
await sendAndConfirmTransaction(
connection,
new Transaction({feePayer: keypair.publicKey, ...blockhash}).add(
nonceInfo.nonceInstruction,
),
[keypair],
);
const [currentSlot, signature] = await Promise.all([
connection.getSlot(),
connection.sendTransaction(transaction, [keypair], {
skipPreflight: true,
}),
]);
const confirmationPromise = connection.confirmTransaction({
minContextSlot: currentSlot,
nonceAccountPubkey: nonceKeypair.publicKey,
nonceValue: nonceInfo.nonce, // The old nonce.
signature,
});
await expect(confirmationPromise).to.eventually.be.rejectedWith(
TransactionExpiredNonceInvalidError,
);
}).timeout(60 * 1000);
});
});
}
if (!process.env.TEST_LIVE) {
describe('transaction confirmation (mock)', () => {
let clock: SinonFakeTimers;
beforeEach(() => {
clock = useFakeTimers();
});
afterEach(() => {
clock.restore();
});
describe('timeout strategy (deprecated)', () => {
it('throws a `TransactionExpiredTimeoutError` when the timer elapses without a signature confirmation', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}),
});
const timeoutPromise = connection.confirmTransaction(mockSignature);
// Advance the clock past all waiting timers, notably the expiry timer.
clock.runAllAsync();
await expect(timeoutPromise).to.be.rejectedWith(
TransactionExpiredTimeoutError,
);
});
});
describe('block height strategy', () => {
it('rejects if called with an already-aborted `abortSignal`', () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
const abortController: any =
nodeVersion >= 16 ? new AbortController() : Node14Controller();
abortController.abort();
expect(
connection.confirmTransaction({
abortSignal: abortController.signal,
blockhash: 'sampleBlockhash',
lastValidBlockHeight: 1,
signature: mockSignature,
}),
).to.eventually.be.rejectedWith('AbortError');
});
it('rejects upon receiving an abort signal', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
const abortController: any =
nodeVersion >= 16 ? new AbortController() : Node14Controller();
// Keep the subscription from ever returning data.
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve.
});
clock.runAllAsync();
const confirmationPromise = connection.confirmTransaction({
abortSignal: abortController.signal,
blockhash: 'sampleBlockhash',
lastValidBlockHeight: 1,
signature: mockSignature,
});
clock.runAllAsync();
expect(confirmationPromise).not.to.have.been.rejected;
abortController.abort();
await expect(confirmationPromise).to.eventually.be.rejected;
});
it('throws a `TransactionExpiredBlockheightExceededError` when the block height advances past the last valid one for this transaction without a signature confirmation', async () => {
const mockSignature =
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve this = never get a response.
});
const lastValidBlockHeight = 3;
// Start the block height at `lastValidBlockHeight - 1`.
await mockRpcResponse({
method: 'getBlockHeight',
params: [],
value: lastValidBlockHeight - 1,
});
const confirmationPromise = connection.confirmTransaction({
signature: mockSignature,
blockhash: 'sampleBlockhash',
lastValidBlockHeight,
});
clock.runAllAsync();
// Advance the block height to the `lastValidBlockHeight`.
await mockRpcResponse({
method: 'getBlockHeight',
params: [],
value: lastValidBlockHeight,
});
clock.runAllAsync();
// Advance the block height to `lastValidBlockHeight + 1`,
// past the last valid blockheight for this transaction.
await mockRpcResponse({
method: 'getBlockHeight',
params: [],
value: lastValidBlockHeight + 1,
});
clock.runAllAsync();
await expect(confirmationPromise).to.be.rejectedWith(
TransactionExpiredBlockheightExceededError,
);
});
it('when the `getBlockHeight` method throws an error it does not timeout but rather keeps waiting for a confirmation', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
let resolveResultPromise = function (result: SignatureResult): any {
return result;
};
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise<SignatureResult>(resolve => {
resolveResultPromise = resolve;
}),
});
// Simulate a failure to fetch the block height.
let rejectBlockheightPromise = function (): void {};
await mockRpcResponse({
method: 'getBlockHeight',
params: [],
value: (() => {
const p = new Promise((_, reject) => {
rejectBlockheightPromise = reject;
});
p.catch(() => {});
return p;
})(),
});
const confirmationPromise = connection.confirmTransaction({
signature: mockSignature,
blockhash: 'sampleBlockhash',
lastValidBlockHeight: 3,
});
rejectBlockheightPromise();
clock.runToLastAsync();
resolveResultPromise({err: null});
clock.runToLastAsync();
expect(confirmationPromise).not.to.eventually.be.rejected;
});
it('confirms the transaction if the signature confirmation is received before the block height is exceeded', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
let resolveResultPromise = function (result: SignatureResult): any {
return result;
};
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise<SignatureResult>(resolve => {
resolveResultPromise = resolve;
}),
});
const lastValidBlockHeight = 3;
// Advance the block height to the `lastValidBlockHeight`.
await mockRpcResponse({
method: 'getBlockHeight',
params: [],
value: lastValidBlockHeight,
});
const confirmationPromise = connection.confirmTransaction({
signature: mockSignature,
blockhash: 'sampleBlockhash',
lastValidBlockHeight,
});
clock.runAllAsync();
// Return a signature result in the nick of time.
resolveResultPromise({err: null});
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
});
});
describe('nonce strategy', () => {
it('rejects if called with an already-aborted `abortSignal`', () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
const abortController: any =
nodeVersion >= 16 ? new AbortController() : Node14Controller();
abortController.abort();
expect(
connection.confirmTransaction({
abortSignal: abortController.signal,
minContextSlot: 1,
nonceAccountPubkey: new PublicKey(1),
nonceValue: 'fakenonce',
signature: mockSignature,
}),
).to.eventually.be.rejectedWith('AbortError');
});
it('rejects upon receiving an abort signal', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
const abortController: any =
nodeVersion >= 16 ? new AbortController() : Node14Controller();
// Keep the subscription from ever returning data.
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve.
});
clock.runAllAsync();
const confirmationPromise = connection.confirmTransaction({
abortSignal: abortController.signal,
minContextSlot: 1,
nonceAccountPubkey: new PublicKey(1),
nonceValue: 'fakenonce',
signature: mockSignature,
});
clock.runAllAsync();
expect(confirmationPromise).not.to.have.been.rejected;
abortController.abort();
await expect(confirmationPromise).to.eventually.be.rejected;
});
it('confirms the transaction if the signature confirmation is received before the nonce is advanced', async () => {
const mockSignature =
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG';
let resolveResultPromise = function (result: SignatureResult): any {
return result;
};
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise<SignatureResult>(resolve => {
resolveResultPromise = resolve;
}),
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
const authority = new PublicKey(3);
// Start with the nonce account matching the nonce used to sign the transaction.
await mockNonceAccountResponse(
nonceAccountPubkey.toBase58(),
nonceValue,
authority.toBase58(),
);
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 0,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
clock.runAllAsync();
// Respond, a second time, with the same nonce hash.
await mockNonceAccountResponse(
nonceAccountPubkey.toBase58(),
nonceValue,
authority.toBase58(),
);
clock.runAllAsync();
// Return a signature result in the nick of time.
resolveResultPromise({err: null});
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
});
it('succeeds if double-checking the signature after the nonce-advances demonstrates that the transaction is confirmed', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve this = never get a response.
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
const authority = new PublicKey(3);
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 0,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
// Simulate the nonce advancing but the double-check of the signature status succeeding.
await mockNonceAccountResponse(
nonceAccountPubkey.toBase58(),
new PublicKey(4).toBase58(), // A new nonce.
authority.toBase58(),
);
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
err: null,
confirmations: 0,
confirmationStatus: 'finalized', // Demonstrate that the transaction is, in fact, confirmed.
slot: 0,
},
],
withContext: true,
});
clock.runToLastAsync();
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
});
it('keeps double-checking the signature after the nonce-advances until a signature from the minimum allowable slot is obtained', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve this = never get a response.
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
const authority = new PublicKey(3);
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 11,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
// Simulate the nonce advancing but the double-check of the signature status succeeding.
await mockNonceAccountResponse(
nonceAccountPubkey.toBase58(),
new PublicKey(4).toBase58(), // A new nonce.
authority.toBase58(),
);
// Simulate getting a response from an old slot.
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
err: null,
confirmations: 0,
confirmationStatus: 'processed', // A non-finalized value from an old slot.
slot: 10,
},
],
slot: 10,
withContext: true,
});
// Then obtain a response from the minimum allowable slot.
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
err: null,
confirmations: 32,
confirmationStatus: 'finalized', // Demonstrate that the transaction is, in fact, confirmed.
slot: 11,
},
],
slot: 11,
withContext: true,
});
clock.runAllAsync();
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
});
it('throws a `TransactionExpiredNonceInvalidError` when the nonce is no longer the one with which this transaction was signed', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve this = never get a response.
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
const authority = new PublicKey(3);
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 0,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
// Simulate the nonce advancing but the double-check of the signature status succeeding.
await mockNonceAccountResponse(
nonceAccountPubkey.toBase58(),
new PublicKey(4).toBase58(), // A new nonce.
authority.toBase58(),
);
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
err: null,
confirmations: 0,
confirmationStatus: 'processed', // Demonstrate that the transaction is, in fact, not confirmed.
slot: 0,
},
],
withContext: true,
});
clock.runToLastAsync();
await expect(confirmationPromise).to.eventually.be.rejectedWith(
TransactionExpiredNonceInvalidError,
);
});
it('when fetching the nonce account throws an error it does not timeout but rather keeps waiting for a confirmation', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
let resolveResultPromise = function (result: SignatureResult): any {
return result;
};
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise<SignatureResult>(resolve => {
resolveResultPromise = resolve;
}),
});
// Simulate a failure to fetch the nonce account.
let rejectNonceAccountFetchPromise = function (): void {};
await mockRpcResponse({
method: 'getAccountInfo',
params: [],
value: (() => {
const p = new Promise((_, reject) => {
rejectNonceAccountFetchPromise = reject;
});
p.catch(() => {});
return p;
})(),
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 0,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
rejectNonceAccountFetchPromise();
clock.runToLastAsync();
resolveResultPromise({err: null});
clock.runToLastAsync();
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
});
it('throws `TransactionExpiredNonceInvalidError` when the nonce account does not exist', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve this = never get a response.
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 0,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
// Simulate a non-existent nonce account.
await mockRpcResponse({
method: 'getAccountInfo',
params: [],
value: null,
withContext: true,
});
clock.runToLastAsync();
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
err: null,
confirmations: 0,
confirmationStatus: 'processed', // Demonstrate that the transaction is, in fact, not confirmed.
slot: 0,
},
],
withContext: true,
});
clock.runToLastAsync();
await expect(confirmationPromise).to.eventually.be.rejectedWith(
TransactionExpiredNonceInvalidError,
);
});
it('when the nonce account data fails to deserialize', async () => {
const mockSignature =
'LPJ18iiyfz3G1LpNNbcBnBtaS4dVBdPHKrnELqikjER2DcvB4iyTgz43nKQJH3JQAJHuZdM1xVh5Cnc5Hc7LrqC';
let resolveResultPromise = function (result: SignatureResult): any {
return result;
};
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise<SignatureResult>(resolve => {
resolveResultPromise = resolve;
}),
});
const nonceAccountPubkey = new PublicKey(1);
const nonceValue = new PublicKey(2).toBase58();
// Simulate a failure to deserialize the nonce.
await mockRpcResponse({
method: 'getAccountInfo',
params: [nonceAccountPubkey.toBase58(), {encoding: 'base64'}],
value: {
owner: SystemProgram.programId.toBase58(),
lamports: LAMPORTS_PER_SOL,
data: ['JUNK_DATA', 'base64'],
executable: false,
rentEpoch: 20,
},
withContext: true,
});
const confirmationPromise = connection.confirmTransaction({
minContextSlot: 0,
nonceAccountPubkey,
nonceValue,
signature: mockSignature,
});
clock.runToLastAsync();
resolveResultPromise({err: null});
clock.runToLastAsync();
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
});
});
it('confirm transaction - does not check the signature status before the signature subscription comes alive', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: {err: null},
subscriptionEstablishmentPromise: new Promise(() => {}), // Never resolve.
});
const getSignatureStatusesExpectation = mock(connection)
.expects('getSignatureStatuses')
.never();
connection.confirmTransaction(mockSignature);
getSignatureStatusesExpectation.verify();
});
it('confirm transaction - checks the signature status once the signature subscription comes alive', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: {err: null},
});
const getSignatureStatusesExpectation = mock(connection)
.expects('getSignatureStatuses')
.once();
const confirmationPromise =
connection.confirmTransaction(mockSignature);
clock.runAllAsync();
await expect(confirmationPromise).to.eventually.deep.equal({
context: {slot: 11},
value: {err: null},
});
getSignatureStatusesExpectation.verify();
});
// FIXME: This test does not work.
// it('confirm transaction - confirms transaction when signature status check yields confirmation before signature subscription does', async () => {
// const mockSignature =
// 'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
// // Keep the subscription from ever returning data.
// await mockRpcMessage({
// method: 'signatureSubscribe',
// params: [mockSignature, {commitment: 'finalized'}],
// result: new Promise(() => {}), // Never resolve.
// });
// clock.runAllAsync();
// const confirmationPromise =
// connection.confirmTransaction(mockSignature);
// clock.runAllAsync();
// // Return a signature status through the RPC API.
// await mockRpcResponse({
// method: 'getSignatureStatuses',
// params: [[mockSignature]],
// value: [
// {
// slot: 0,
// confirmations: 11,
// status: {Ok: null},
// err: null,
// },
// ],
// });
// clock.runAllAsync();
// await expect(confirmationPromise).to.eventually.deep.equal({
// context: {slot: 11},
// value: {err: null},
// });
// });
it('confirm transaction - does not confirm the transaction when signature status check yields confirmation for a lower commitment before signature subscription confirms the transaction', async () => {
const mockSignature =
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt';
// Keep the subscription from ever returning data.
await mockRpcMessage({
method: 'signatureSubscribe',
params: [mockSignature, {commitment: 'finalized'}],
result: new Promise(() => {}), // Never resolve.
});
clock.runAllAsync();
const confirmationPromise =
connection.confirmTransaction(mockSignature);
clock.runAllAsync();
// Return a signature status with a lower finality through the RPC API.
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[mockSignature]],
value: [
{
slot: 0,
confirmations: null,
confirmationStatus: 'processed', // Lower than we expect
err: null,
},
],
});
clock.runAllAsync();
await expect(confirmationPromise).to.be.rejectedWith(
TransactionExpiredTimeoutError,
);
});
});
}
describe('transaction confirmation', () => {
it('confirm transaction - error', async () => {
const badTransactionSignature = 'bad transaction signature';
await expect(
connection.confirmTransaction({
blockhash: 'sampleBlockhash',
lastValidBlockHeight: 9999,
signature: badTransactionSignature,
}),
).to.be.rejectedWith('signature must be base58 encoded');
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[badTransactionSignature]],
error: mockErrorResponse,
});
await expect(
connection.getSignatureStatus(badTransactionSignature),
).to.be.rejectedWith(mockErrorMessage);
});
});
it('get transaction count', async () => {
await mockRpcResponse({
method: 'getTransactionCount',
params: [],
value: 1000000,
});
const count = await connection.getTransactionCount();
expect(count).to.be.at.least(0);
});
it('get total supply', async () => {
await mockRpcResponse({
method: 'getSupply',
params: [],
value: {
total: 1000000,
circulating: 100000,
nonCirculating: 900000,
nonCirculatingAccounts: [],
},
withContext: true,
});
const count = await connection.getTotalSupply();
expect(count).to.be.at.least(0);
});
it('get minimum balance for rent exemption', async () => {
await mockRpcResponse({
method: 'getMinimumBalanceForRentExemption',
params: [512],
value: 1000000,
});
const count = await connection.getMinimumBalanceForRentExemption(512);
expect(count).to.be.at.least(0);
});
it('get confirmed signatures for address', async () => {
const connection = new Connection(url);
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [1],
value: {
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let slot = await connection.getFirstAvailableBlock();
let address: PublicKey | undefined;
let expectedSignature: string | undefined;
while (!address || !expectedSignature) {
const block = await connection.getConfirmedBlock(slot);
if (block.transactions.length > 0) {
const {signature, publicKey} =
block.transactions[0].transaction.signatures[0];
if (signature) {
address = publicKey;
expectedSignature = bs58.encode(signature);
break;
}
}
slot++;
}
// getConfirmedSignaturesForAddress tests...
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
const mockSignature =
'5SHZ9NwpnS9zYnauN7pnuborKf39zGMr11XpMC59VvRSeDJNcnYLecmdxXCVuBFPNQLdCBBjyZiNCL4KoHKr3tvz';
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [slot, {transactionDetails: 'signatures', rewards: false}],
value: {
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 1,
signatures: [mockSignature],
},
});
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 123,
});
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [slot + 2, {transactionDetails: 'signatures', rewards: false}],
value: {
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 1,
signatures: [mockSignature],
},
});
await mockRpcResponse({
method: 'getConfirmedSignaturesForAddress2',
params: [address.toBase58(), {before: mockSignature}],
value: [
{
signature: expectedSignature,
slot,
err: null,
memo: null,
},
],
});
const confirmedSignatures =
await connection.getConfirmedSignaturesForAddress(
address,
slot,
slot + 1,
);
expect(confirmedSignatures.includes(expectedSignature)).to.be.true;
const badSlot = Number.MAX_SAFE_INTEGER - 1;
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [badSlot - 1, {transactionDetails: 'signatures', rewards: false}],
error: {message: 'Block not available for slot ' + badSlot},
});
expect(
connection.getConfirmedSignaturesForAddress(
address,
badSlot,
badSlot + 1,
),
).to.be.rejected;
// getConfirmedSignaturesForAddress2 tests...
await mockRpcResponse({
method: 'getConfirmedSignaturesForAddress2',
params: [address.toBase58(), {limit: 1}],
value: [
{
signature: expectedSignature,
slot,
err: null,
memo: null,
},
],
});
const confirmedSignatures2 =
await connection.getConfirmedSignaturesForAddress2(address, {limit: 1});
expect(confirmedSignatures2).to.have.length(1);
if (mockServer) {
expect(confirmedSignatures2[0].signature).to.eq(expectedSignature);
expect(confirmedSignatures2[0].slot).to.eq(slot);
expect(confirmedSignatures2[0].err).to.be.null;
expect(confirmedSignatures2[0].memo).to.be.null;
}
});
it('get signatures for address', async () => {
const connection = new Connection(url);
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [1],
value: {
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let slot = await connection.getFirstAvailableBlock();
let address: PublicKey | undefined;
let expectedSignature: string | undefined;
while (!address || !expectedSignature) {
const block = await connection.getConfirmedBlock(slot);
if (block.transactions.length > 0) {
const {signature, publicKey} =
block.transactions[0].transaction.signatures[0];
if (signature) {
address = publicKey;
expectedSignature = bs58.encode(signature);
break;
}
}
slot++;
}
// getSignaturesForAddress tests...
await mockRpcResponse({
method: 'getSignaturesForAddress',
params: [address.toBase58(), {limit: 1}],
value: [
{
signature: expectedSignature,
slot,
err: null,
memo: null,
},
],
});
const signatures = await connection.getSignaturesForAddress(address, {
limit: 1,
});
expect(signatures).to.have.length(1);
if (mockServer) {
expect(signatures[0].signature).to.eq(expectedSignature);
expect(signatures[0].slot).to.eq(slot);
expect(signatures[0].err).to.be.null;
expect(signatures[0].memo).to.be.null;
}
});
it('get parsed confirmed transactions', async () => {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [1],
value: {
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let slot = await connection.getFirstAvailableBlock();
let confirmedTransaction: string | undefined;
while (!confirmedTransaction) {
const block = await connection.getConfirmedBlock(slot);
for (const tx of block.transactions) {
if (tx.transaction.signature) {
confirmedTransaction = bs58.encode(tx.transaction.signature);
break;
}
}
slot++;
}
await mockRpcBatchResponse({
batch: [
{
methodName: 'getConfirmedTransaction',
args: [],
},
],
result: [
{
blockTime: 1616102519,
meta: {
err: null,
fee: 5000,
innerInstructions: [],
logMessages: [
'Program Vote111111111111111111111111111111111111111 invoke [1]',
'Program Vote111111111111111111111111111111111111111 success',
],
postBalances: [499999995000, 26858640, 1, 1, 1],
postTokenBalances: [],
preBalances: [500000000000, 26858640, 1, 1, 1],
preTokenBalances: [],
status: {
Ok: null,
},
},
slot: 2,
transaction: {
message: {
accountKeys: [
{
pubkey: 'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
signer: true,
writable: true,
},
{
pubkey: 'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
signer: false,
writable: true,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
writable: false,
},
],
instructions: [
{
parsed: {
info: {
clockSysvar:
'SysvarC1ock11111111111111111111111111111111',
slotHashesSysvar:
'SysvarS1otHashes111111111111111111111111111',
vote: {
hash: 'GuCya3AAGxn1qhoqxqy3WEdZdZUkXKpa9pthQ3tqvbpx',
slots: [1],
timestamp: 1616102669,
},
voteAccount:
'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
voteAuthority:
'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
},
type: 'vote',
},
program: 'vote',
programId: 'Vote111111111111111111111111111111111111111',
},
],
recentBlockhash: 'G9ywjV5CVgMtLXruXtrE7af4QgFKYNXgDTw4jp7SWcSo',
},
signatures: [
'4G4rTqnUdzrmBHsdKJSiMtonpQLWSw1avJ8YxWQ95jE6iFFHFsEkBnoYycxnkBS9xHWRc6EarDsrFG9USFBbjfjx',
],
},
},
{
blockTime: 1616102519,
meta: {
err: null,
fee: 5000,
innerInstructions: [],
logMessages: [
'Program Vote111111111111111111111111111111111111111 invoke [1]',
'Program Vote111111111111111111111111111111111111111 success',
],
postBalances: [499999995000, 26858640, 1, 1, 1],
postTokenBalances: [],
preBalances: [500000000000, 26858640, 1, 1, 1],
preTokenBalances: [],
status: {
Ok: null,
},
},
slot: 2,
transaction: {
message: {
accountKeys: [
{
pubkey: 'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
signer: true,
writable: true,
},
{
pubkey: 'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
signer: false,
writable: true,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
writable: false,
},
],
instructions: [
{
parsed: {
info: {
clockSysvar:
'SysvarC1ock11111111111111111111111111111111',
slotHashesSysvar:
'SysvarS1otHashes111111111111111111111111111',
vote: {
hash: 'GuCya3AAGxn1qhoqxqy3WEdZdZUkXKpa9pthQ3tqvbpx',
slots: [1],
timestamp: 1616102669,
},
voteAccount:
'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
voteAuthority:
'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
},
type: 'vote',
},
program: 'vote',
programId: 'Vote111111111111111111111111111111111111111',
},
],
recentBlockhash: 'G9ywjV5CVgMtLXruXtrE7af4QgFKYNXgDTw4jp7SWcSo',
},
signatures: [
'4G4rTqnUdzrmBHsdKJSiMtonpQLWSw1avJ8YxWQ95jE6iFFHFsEkBnoYycxnkBS9xHWRc6EarDsrFG9USFBbjfjx',
],
},
},
],
});
let result = await connection.getParsedConfirmedTransactions([
confirmedTransaction,
confirmedTransaction,
]);
if (!result) {
expect(result).to.be.ok;
return;
}
expect(result).to.be.length(2);
expect(result[0]).to.not.be.null;
expect(result[1]).to.not.be.null;
if (result[0] !== null) {
expect(result[0].transaction.signatures).not.to.be.null;
}
if (result[1] !== null) {
expect(result[1].transaction.signatures).not.to.be.null;
}
result = await connection.getParsedConfirmedTransactions([]);
if (!result) {
expect(result).to.be.ok;
return;
}
expect(result).to.be.empty;
});
it('get block height', async () => {
const commitment: Commitment = 'confirmed';
await mockRpcResponse({
method: 'getBlockHeight',
params: [{commitment: commitment}],
value: 10,
});
const blockHeight = await connection.getBlockHeight(commitment);
expect(blockHeight).to.be.a('number');
});
it('get block production', async () => {
const commitment: Commitment = 'processed';
// Find slot of the lowest confirmed block
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let firstSlot = await connection.getFirstAvailableBlock();
// Find current block height
await mockRpcResponse({
method: 'getBlockHeight',
params: [{commitment: commitment}],
value: 10,
});
let lastSlot = await connection.getBlockHeight(commitment);
const blockProductionConfig = {
commitment: commitment,
range: {
firstSlot,
lastSlot,
},
};
const blockProductionRet = {
byIdentity: {
'85iYT5RuzRTDgjyRa3cP8SYhM2j21fj7NhfJ3peu1DPr': [12, 10],
},
range: {
firstSlot,
lastSlot,
},
};
//mock RPC call with config specified
await mockRpcResponse({
method: 'getBlockProduction',
params: [blockProductionConfig],
value: blockProductionRet,
withContext: true,
});
//mock RPC call with commitment only
await mockRpcResponse({
method: 'getBlockProduction',
params: [{commitment: commitment}],
value: blockProductionRet,
withContext: true,
});
const result = await connection.getBlockProduction(blockProductionConfig);
if (!result) {
expect(result).to.be.ok;
return;
}
expect(result.context).to.be.ok;
expect(result.value).to.be.ok;
const resultContextSlot = result.context.slot;
expect(resultContextSlot).to.be.a('number');
const resultIdentityDictionary = result.value.byIdentity;
expect(resultIdentityDictionary).to.be.a('object');
for (var key in resultIdentityDictionary) {
expect(key).to.be.a('string');
expect(resultIdentityDictionary[key]).to.be.a('array');
expect(resultIdentityDictionary[key][0]).to.be.a('number');
expect(resultIdentityDictionary[key][1]).to.be.a('number');
}
const resultSlotRange = result.value.range;
expect(resultSlotRange.firstSlot).to.equal(firstSlot);
expect(resultSlotRange.lastSlot).to.equal(lastSlot);
const resultCommitmentOnly = await connection.getBlockProduction(
commitment,
);
if (!resultCommitmentOnly) {
expect(resultCommitmentOnly).to.be.ok;
return;
}
expect(resultCommitmentOnly.context).to.be.ok;
expect(resultCommitmentOnly.value).to.be.ok;
const resultCOContextSlot = result.context.slot;
expect(resultCOContextSlot).to.be.a('number');
const resultCOIdentityDictionary = result.value.byIdentity;
expect(resultCOIdentityDictionary).to.be.a('object');
for (var property in resultCOIdentityDictionary) {
expect(property).to.be.a('string');
expect(resultCOIdentityDictionary[property]).to.be.a('array');
expect(resultCOIdentityDictionary[property][0]).to.be.a('number');
expect(resultCOIdentityDictionary[property][1]).to.be.a('number');
}
const resultCOSlotRange = result.value.range;
expect(resultCOSlotRange.firstSlot).to.equal(firstSlot);
expect(resultCOSlotRange.lastSlot).to.equal(lastSlot);
});
it('get transaction', async () => {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getBlock',
params: [1],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let slot = await connection.getFirstAvailableBlock();
let transaction: string | undefined;
while (!transaction) {
const block = await connection.getBlock(slot);
if (block && block.transactions.length > 0) {
transaction = block.transactions[0].transaction.signatures[0];
continue;
}
slot++;
}
await mockRpcResponse({
method: 'getTransaction',
params: [transaction],
value: {
slot,
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
},
});
const result = await connection.getTransaction(transaction);
if (!result) {
expect(result).to.be.ok;
return;
}
const resultSignature = result.transaction.signatures[0];
expect(resultSignature).to.eq(transaction);
const newAddress = Keypair.generate().publicKey;
const recentSignature = await helpers.airdrop({
connection,
address: newAddress,
amount: 1,
});
await mockRpcResponse({
method: 'getTransaction',
params: [recentSignature],
value: null,
});
// Signature hasn't been finalized yet
const nullResponse = await connection.getTransaction(recentSignature);
expect(nullResponse).to.be.null;
});
it('get confirmed transaction', async () => {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [1],
value: {
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let slot = await connection.getFirstAvailableBlock();
let confirmedTransaction: string | undefined;
while (!confirmedTransaction) {
const block = await connection.getConfirmedBlock(slot);
for (const tx of block.transactions) {
if (tx.transaction.signature) {
confirmedTransaction = bs58.encode(tx.transaction.signature);
break;
}
}
slot++;
}
await mockRpcResponse({
method: 'getConfirmedTransaction',
params: [confirmedTransaction],
value: {
slot,
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
},
});
const result = await connection.getConfirmedTransaction(
confirmedTransaction,
);
if (!result) {
expect(result).to.be.ok;
return;
}
if (result.transaction.signature === null) {
expect(result.transaction.signature).not.to.be.null;
return;
}
const resultSignature = bs58.encode(result.transaction.signature);
expect(resultSignature).to.eq(confirmedTransaction);
const newAddress = Keypair.generate().publicKey;
const recentSignature = await helpers.airdrop({
connection,
address: newAddress,
amount: 1,
});
await mockRpcResponse({
method: 'getConfirmedTransaction',
params: [recentSignature],
value: null,
});
// Signature hasn't been finalized yet
const nullResponse = await connection.getConfirmedTransaction(
recentSignature,
);
expect(nullResponse).to.be.null;
});
it('get transactions', async function () {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getBlock',
params: [1],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
let slot = await connection.getFirstAvailableBlock();
let transaction: string | undefined;
while (!transaction) {
const block = await connection.getBlock(slot);
if (block && block.transactions.length > 0) {
transaction = block.transactions[0].transaction.signatures[0];
continue;
}
slot++;
}
await mockRpcBatchResponse({
batch: [
{
methodName: 'getTransaction',
args: [transaction],
},
],
result: [
{
slot,
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
},
],
});
const [firstResult] = await connection.getTransactions([transaction]);
if (firstResult == null) {
expect.fail('Expected `getTransactions()` to return one result');
}
expect(firstResult.transaction.message.isAccountSigner(0)).to.be.true;
});
if (mockServer) {
it('get parsed confirmed transaction coerces public keys of inner instructions', async () => {
const confirmedTransaction: TransactionSignature =
'4ADvAUQYxkh4qWKYE9QLW8gCLomGG94QchDLG4quvpBz1WqARYvzWQDDitKduAKspuy1DjcbnaDAnCAfnKpJYs48';
function getMockData(inner: any) {
return {
slot: 353050305,
transaction: {
message: {
accountKeys: [
{
pubkey: 'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
signer: true,
writable: true,
},
],
instructions: [
{
accounts: ['va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf'],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
},
],
recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
innerInstructions: [
{
index: 0,
instructions: [inner],
},
],
status: {Ok: null},
err: null,
},
};
}
await mockRpcResponse({
method: 'getConfirmedTransaction',
params: [confirmedTransaction, {encoding: 'jsonParsed'}],
value: getMockData({
parsed: {},
program: 'spl-token',
programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
}),
});
const result = await connection.getParsedConfirmedTransaction(
confirmedTransaction,
);
if (result && result.meta && result.meta.innerInstructions) {
const innerInstructions = result.meta.innerInstructions;
const firstIx = innerInstructions[0].instructions[0];
expect(firstIx.programId).to.be.instanceOf(PublicKey);
}
await mockRpcResponse({
method: 'getConfirmedTransaction',
params: [confirmedTransaction, {encoding: 'jsonParsed'}],
value: getMockData({
accounts: [
'EeJqWk5pczNjsqqY3jia9xfFNG1dD68te4s8gsdCuEk7',
'6tVrjJhFm5SAvvdh6tysjotQurCSELpxuW3JaAAYeC1m',
],
data: 'ai3535',
programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
}),
});
const result2 = await connection.getParsedConfirmedTransaction(
confirmedTransaction,
);
if (result2 && result2.meta && result2.meta.innerInstructions) {
const innerInstructions = result2.meta.innerInstructions;
const instruction = innerInstructions[0].instructions[0];
expect(instruction.programId).to.be.instanceOf(PublicKey);
if ('accounts' in instruction) {
expect(instruction.accounts[0]).to.be.instanceOf(PublicKey);
expect(instruction.accounts[1]).to.be.instanceOf(PublicKey);
} else {
expect('accounts' in instruction).to.be.true;
}
}
});
}
describe('get parsed block', function () {
it('can deserialize a response when `transactionDetails` is `full`', async () => {
// Mock block with transaction, fetched using `"transactionDetails": "full"`.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
encoding: 'jsonParsed',
maxSupportedTransactionVersion: 0,
transactionDetails: 'full',
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '49d2UbduiZWjtR3Wvfv2t2QxmXvtZNWSPFRZxEDYAvQN',
parentSlot: 0,
previousBlockhash: 'mDd5yMLfuroS1JVZMHo2VZLTgKXXNBXrzPR5UkzFD4X',
transactions: [
{
meta: {
err: null,
fee: 5000,
innerInstructions: [],
logMessages: [
'Program Vote111111111111111111111111111111111111111 invoke [1]',
'Program Vote111111111111111111111111111111111111111 success',
],
postBalances: [3712706991, 5765419239, 1169280, 143487360, 1],
postTokenBalances: [],
preBalances: [3712711991, 5765419239, 1169280, 143487360, 1],
preTokenBalances: [],
rewards: null,
status: {Ok: null},
},
transaction: {
message: {
accountKeys: [
{
pubkey: '7v5fMKBqC9PuwjSdS9k9JU7efEXmq3bHTMF5fuSHnqrm',
signer: true,
source: 'transaction',
writable: true,
},
{
pubkey: 'AhcvnNdppGEcgdpK5gfcaZnAWz4ct8V4n7De5QiLiuzG',
signer: false,
source: 'transaction',
writable: true,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
],
addressTableLookups: null,
instructions: [
{
parsed: {
info: {
clockSysvar:
'SysvarC1ock11111111111111111111111111111111',
slotHashesSysvar:
'SysvarS1otHashes111111111111111111111111111',
vote: {
hash: '2gmQ8xMjZaXn63kr8qzPAUjQAHi7xCDjSibPdJxhVYMm',
slots: [164153060, 164153061],
timestamp: 1669845645,
},
voteAccount:
'AhcvnNdppGEcgdpK5gfcaZnAWz4ct8V4n7De5QiLiuzG',
voteAuthority:
'7v5fMKBqC9PuwjSdS9k9JU7efEXmq3bHTMF5fuSHnqrm',
},
type: 'vote',
},
program: 'vote',
programId: 'Vote111111111111111111111111111111111111111',
},
],
recentBlockhash:
'GLqYrN6AQxCGtFTQywkPj2WN5tafC3KerBhW4QkmAyD4',
},
signatures: [
'5qDZ3nUUwp8VHFfAE5ydTQRULCoVLMGs16EprwdXsvyNCLe1NfckCkRE4BPi6wyEW9hXvG9iWU2prXfbM8SNPVEC',
],
},
version: 'legacy',
},
],
},
});
await expect(
connection.getParsedBlock(1, {
maxSupportedTransactionVersion: 0,
transactionDetails: 'full',
}),
).not.to.eventually.be.rejected;
});
it('can deserialize a response when `transactionDetails` is `none`', async () => {
// Mock block with transaction, fetched using `"transactionDetails": "none"`.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
encoding: 'jsonParsed',
maxSupportedTransactionVersion: 0,
transactionDetails: 'none',
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '49d2UbduiZWjtR3Wvfv2t2QxmXvtZNWSPFRZxEDYAvQN',
parentSlot: 0,
previousBlockhash: 'mDd5yMLfuroS1JVZMHo2VZLTgKXXNBXrzPR5UkzFD4X',
},
});
await expect(
connection.getParsedBlock(1, {
maxSupportedTransactionVersion: 0,
transactionDetails: 'none',
}),
).not.to.eventually.be.rejected;
});
it('can deserialize a response when `transactionDetails` is `accounts`', async () => {
// Mock block with transaction, fetched using `"transactionDetails": "accounts"`.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
encoding: 'jsonParsed',
maxSupportedTransactionVersion: 0,
transactionDetails: 'accounts',
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '49d2UbduiZWjtR3Wvfv2t2QxmXvtZNWSPFRZxEDYAvQN',
parentSlot: 0,
previousBlockhash: 'mDd5yMLfuroS1JVZMHo2VZLTgKXXNBXrzPR5UkzFD4X',
transactions: [
{
meta: {
err: null,
fee: 5000,
postBalances: [18237691394, 26858640, 1169280, 143487360, 1],
postTokenBalances: [],
preBalances: [18237696394, 26858640, 1169280, 143487360, 1],
preTokenBalances: [],
status: {Ok: null},
},
transaction: {
accountKeys: [
{
pubkey: '914RFshndUeZaNPjf8UWDCyo49ahQ1XQ2w9BnEMwpHKF',
signer: true,
source: 'transaction',
writable: true,
},
{
pubkey: '4cCd4SGrMswhqboYBJ5AcCVvCjh5NtaeZNwWFJzsnUWY',
signer: false,
source: 'transaction',
writable: true,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
],
signatures: [
'5ZDp1HfNZhNRHc75ncsiZ4sCq1fGJHMGf9u36M3foD5PMH4Xu5S4X2x7aryn4JinUdG11oSYCk7zxbNmLJzzqUft',
],
},
version: 'legacy',
},
],
},
});
await expect(
connection.getParsedBlock(1, {
maxSupportedTransactionVersion: 0,
transactionDetails: 'accounts',
}),
).not.to.eventually.be.rejected;
});
});
describe('get block', function () {
beforeEach(async function () {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
});
it('gets the genesis block', async function () {
await mockRpcResponse({
method: 'getBlock',
params: [0],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [],
},
});
let maybeBlock0: BlockResponse | null;
try {
maybeBlock0 = await connection.getBlock(0);
} catch (e) {
if (process.env.TEST_LIVE) {
console.warn(
'WARNING: We ran no assertions about the genesis block because block 0 ' +
'could not be found. See https://github.com/solana-labs/solana/issues/23853.',
);
this.skip();
} else {
throw e;
}
}
expect(maybeBlock0).not.to.be.null;
const block0 = maybeBlock0!;
// Block 0 never has any transactions in test validator
const blockhash0 = block0.blockhash;
expect(block0.transactions).to.have.length(0);
expect(blockhash0).not.to.be.null;
expect(block0.previousBlockhash).not.to.be.null;
expect(block0.parentSlot).to.eq(0);
});
it('gets a block having a parent', async function () {
// Mock parent of block with transaction.
await mockRpcResponse({
method: 'getBlock',
params: [0],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [],
},
});
// Mock block with transaction.
await mockRpcResponse({
method: 'getBlock',
params: [1],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash:
'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction *and* a parent.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
let candidateSlot = (await connection.getFirstAvailableBlock()) + 1;
let result:
| {
blockWithTransaction: BlockResponse;
parentBlock: BlockResponse;
}
| undefined;
while (!result) {
const candidateBlock = await connection.getBlock(candidateSlot);
if (candidateBlock && candidateBlock.transactions.length) {
const parentBlock = await connection.getBlock(candidateSlot - 1);
if (parentBlock) {
result = {blockWithTransaction: candidateBlock, parentBlock};
break;
}
}
candidateSlot++;
}
// Compare data with parent
expect(result.blockWithTransaction.previousBlockhash).to.eq(
result.parentBlock.blockhash,
);
expect(result.blockWithTransaction.blockhash).not.to.be.null;
expect(result.blockWithTransaction.transactions[0].transaction).not.to.be
.null;
await mockRpcResponse({
method: 'getBlock',
params: [Number.MAX_SAFE_INTEGER],
error: {
message: `Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
},
});
await expect(
connection.getBlock(Number.MAX_SAFE_INTEGER),
).to.be.rejectedWith(
`Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
);
});
it('can deserialize a response when `transactionDetails` is `full`', async () => {
// Mock block with transaction, fetched using `"transactionDetails": "full"`.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
maxSupportedTransactionVersion: 0,
transactionDetails: 'full',
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '49d2UbduiZWjtR3Wvfv2t2QxmXvtZNWSPFRZxEDYAvQN',
parentSlot: 0,
previousBlockhash: 'mDd5yMLfuroS1JVZMHo2VZLTgKXXNBXrzPR5UkzFD4X',
transactions: [
{
meta: {
err: null,
fee: 5000,
innerInstructions: [],
loadedAddresses: {readonly: [], writable: []},
logMessages: [
'Program Vote111111111111111111111111111111111111111 invoke [1]',
'Program Vote111111111111111111111111111111111111111 success',
],
postBalances: [12278161908, 39995373, 1169280, 143487360, 1],
postTokenBalances: [],
preBalances: [12278166908, 39995373, 1169280, 143487360, 1],
preTokenBalances: [],
rewards: null,
status: {Ok: null},
},
transaction: {
message: {
accountKeys: [
'FTWuJ2tqjecNizCSE66z4BD1tBHomG6DVffGUwRuWUkM',
'H2z3pBT62ByS4jpqsiEMtgN3NUFEuZHiTvoKCFjqCtD6',
'SysvarC1ock11111111111111111111111111111111',
'SysvarS1otHashes111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 1,
},
instructions: [
{
accounts: [1, 3, 2, 0],
data: '29z5mr1JoRmJYQ6zG7p2F3mu68pWTNw9q49Tu7KrSEgoS6Jh1LMPGUK3HXs1N3Dody3icCcXxu6xPYoXLWnUTafEGm3knK',
programIdIndex: 4,
},
],
recentBlockhash:
'GLqYrN6AQxCGtFTQywkPj2WN5tafC3KerBhW4QkmAyD4',
},
signatures: [
'4SZofEnXEVzCYvzk16z6ScR6F3iNtZ3FsCC1PEWegpzvGwTJR6x9cDi8VHRmCFGC5XFs2yEFms3j36Mj7XVyHXbb',
],
},
version: 'legacy',
},
],
},
});
await expect(
connection.getBlock(1, {
maxSupportedTransactionVersion: 0,
transactionDetails: 'full',
}),
).not.to.eventually.be.rejected;
});
it('can deserialize a response when `transactionDetails` is `none`', async () => {
// Mock block with transaction, fetched using `"transactionDetails": "none"`.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
maxSupportedTransactionVersion: 0,
transactionDetails: 'none',
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '49d2UbduiZWjtR3Wvfv2t2QxmXvtZNWSPFRZxEDYAvQN',
parentSlot: 0,
previousBlockhash: 'mDd5yMLfuroS1JVZMHo2VZLTgKXXNBXrzPR5UkzFD4X',
},
});
await expect(
connection.getBlock(1, {
maxSupportedTransactionVersion: 0,
transactionDetails: 'none',
}),
).not.to.eventually.be.rejected;
});
it('can deserialize a response when `transactionDetails` is `accounts`', async () => {
// Mock block with transaction, fetched using `"transactionDetails": "accounts"`.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
maxSupportedTransactionVersion: 0,
transactionDetails: 'accounts',
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: '49d2UbduiZWjtR3Wvfv2t2QxmXvtZNWSPFRZxEDYAvQN',
parentSlot: 0,
previousBlockhash: 'mDd5yMLfuroS1JVZMHo2VZLTgKXXNBXrzPR5UkzFD4X',
transactions: [
{
meta: {
err: null,
fee: 5000,
postBalances: [2751549948, 11751747405, 1169280, 143487360, 1],
postTokenBalances: [],
preBalances: [2751554948, 11751747405, 1169280, 143487360, 1],
preTokenBalances: [],
status: {Ok: null},
},
transaction: {
accountKeys: [
{
pubkey: 'D7hwgGRTr1vaCxzmfEKCaf56SPgBJmjHh6UXHG3p12bB',
signer: true,
source: 'transaction',
writable: true,
},
{
pubkey: '8iLE53Y9k4sccy4gxrT936BHbhYS6J13kQT5vRXhXFMX',
signer: false,
source: 'transaction',
writable: true,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
source: 'transaction',
writable: false,
},
],
signatures: [
'uNKj2ogn8ZRRjyVWXLC7sLRWpKQyMUomm66RXoDuWLXikPSJN8C7ZZK95j8S2bzcjwH6MvrXKSHtCWEURPpEXMB',
],
},
version: 'legacy',
},
],
},
});
await expect(
connection.getBlock(1, {
maxSupportedTransactionVersion: 0,
transactionDetails: 'accounts',
}),
).not.to.eventually.be.rejected;
});
});
describe('get confirmed block', function () {
beforeEach(async function () {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
});
it('gets the genesis block', async function () {
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [0],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [],
},
});
let block0: ConfirmedBlock;
try {
block0 = await connection.getConfirmedBlock(0);
} catch (e) {
if (process.env.TEST_LIVE) {
console.warn(
'WARNING: We ran no assertions about the genesis block because block 0 ' +
'could not be found. See https://github.com/solana-labs/solana/issues/23853.',
);
this.skip();
} else {
throw e;
}
}
// Block 0 never has any transactions in test validator
const blockhash0 = block0.blockhash;
expect(block0.transactions).to.have.length(0);
expect(blockhash0).not.to.be.null;
expect(block0.previousBlockhash).not.to.be.null;
expect(block0.parentSlot).to.eq(0);
});
it('gets a block having a parent', async function () {
// Mock parent of block with transaction.
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [0],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [],
},
});
// Mock block with transaction.
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [1],
value: {
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [
{
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
transaction: {
message: {
accountKeys: [
'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf',
'57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
'SysvarS1otHashes111111111111111111111111111',
'SysvarC1ock11111111111111111111111111111111',
'Vote111111111111111111111111111111111111111',
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7',
programIdIndex: 4,
},
],
recentBlockhash:
'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE',
},
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
},
],
},
});
// Find a block that has a transaction *and* a parent.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
let candidateSlot = (await connection.getFirstAvailableBlock()) + 1;
let result:
| {
blockWithTransaction: ConfirmedBlock;
parentBlock: ConfirmedBlock;
}
| undefined;
while (!result) {
const candidateBlock = await connection.getConfirmedBlock(
candidateSlot,
);
if (candidateBlock && candidateBlock.transactions.length) {
const parentBlock = await connection.getConfirmedBlock(
candidateSlot - 1,
);
if (parentBlock) {
result = {blockWithTransaction: candidateBlock, parentBlock};
break;
}
}
candidateSlot++;
}
// Compare data with parent
expect(result.blockWithTransaction.previousBlockhash).to.eq(
result.parentBlock.blockhash,
);
expect(result.blockWithTransaction.blockhash).not.to.be.null;
expect(result.blockWithTransaction.transactions[0].transaction).not.to.be
.null;
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [Number.MAX_SAFE_INTEGER],
error: {
message: `Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
},
});
await expect(
connection.getConfirmedBlock(Number.MAX_SAFE_INTEGER),
).to.be.rejectedWith(
`Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
);
});
});
it('get blocks between two slots', async () => {
await mockRpcResponse({
method: 'getBlocks',
params: [0, 9],
value: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
});
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 9,
});
while ((await connection.getSlot()) <= 1) {
continue;
}
const [startSlot, latestSlot] = await Promise.all([
connection.getFirstAvailableBlock(),
connection.getSlot(),
]);
const blocks = await connection.getBlocks(startSlot, latestSlot);
expect(blocks).to.have.length(latestSlot - startSlot + 1);
expect(blocks[0]).to.eq(startSlot);
expect(blocks).to.contain(latestSlot);
}).timeout(20 * 1000);
it('get blocks from starting slot', async () => {
await mockRpcResponse({
method: 'getBlocks',
params: [0],
value: [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42,
],
});
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 20,
});
while ((await connection.getSlot()) <= 1) {
continue;
}
const startSlot = await connection.getFirstAvailableBlock();
const [blocks, latestSlot] = await Promise.all([
connection.getBlocks(startSlot),
connection.getSlot(),
]);
if (mockServer) {
expect(blocks).to.have.length(43);
} else {
expect(blocks).to.have.length(latestSlot - startSlot + 1);
}
expect(blocks[0]).to.eq(startSlot);
expect(blocks).to.contain(latestSlot);
}).timeout(20 * 1000);
describe('get block signatures', function () {
beforeEach(async function () {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
});
it('gets the genesis block', async function () {
await mockRpcResponse({
method: 'getBlock',
params: [
0,
{
transactionDetails: 'signatures',
rewards: false,
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
signatures: [],
},
});
let block0: BlockSignatures;
try {
block0 = await connection.getBlockSignatures(0);
} catch (e) {
if (process.env.TEST_LIVE) {
console.warn(
'WARNING: We ran no assertions about the genesis block because block 0 ' +
'could not be found. See https://github.com/solana-labs/solana/issues/23853.',
);
this.skip();
} else {
throw e;
}
}
// Block 0 never has any transactions in test validator
const blockhash0 = block0.blockhash;
expect(block0.signatures).to.have.length(0);
expect(blockhash0).not.to.be.null;
expect(block0.previousBlockhash).not.to.be.null;
expect(block0.parentSlot).to.eq(0);
expect(block0).to.not.have.property('rewards');
});
it('gets a block having a parent', async function () {
// Mock parent of block with transaction.
await mockRpcResponse({
method: 'getBlock',
params: [
0,
{
transactionDetails: 'signatures',
rewards: false,
},
],
value: {
blockHeight: 0,
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
signatures: [],
},
});
// Mock block with transaction.
await mockRpcResponse({
method: 'getBlock',
params: [
1,
{
transactionDetails: 'signatures',
rewards: false,
},
],
value: {
blockHeight: 1,
blockTime: 1614281964,
blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
signatures: [
'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt',
'4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG',
],
},
});
// Find a block that has a transaction *and* a parent.
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
let candidateSlot = (await connection.getFirstAvailableBlock()) + 1;
let result:
| {
blockWithTransaction: BlockSignatures;
parentBlock: BlockSignatures;
}
| undefined;
while (!result) {
const candidateBlock = await connection.getBlockSignatures(
candidateSlot,
);
if (candidateBlock && candidateBlock.signatures.length) {
const parentBlock = await connection.getBlockSignatures(
candidateSlot - 1,
);
if (parentBlock) {
result = {blockWithTransaction: candidateBlock, parentBlock};
break;
}
}
candidateSlot++;
}
// Compare data with parent
expect(result.blockWithTransaction.previousBlockhash).to.eq(
result.parentBlock.blockhash,
);
expect(result.blockWithTransaction.blockhash).not.to.be.null;
expect(result.blockWithTransaction.signatures[0]).not.to.be.null;
expect(result.blockWithTransaction).to.not.have.property('rewards');
await mockRpcResponse({
method: 'getBlock',
params: [Number.MAX_SAFE_INTEGER],
error: {
message: `Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
},
});
await expect(
connection.getBlockSignatures(Number.MAX_SAFE_INTEGER),
).to.be.rejectedWith(
`Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
);
});
});
it('get recent blockhash', async () => {
const commitments: Commitment[] = ['processed', 'confirmed', 'finalized'];
for (const commitment of commitments) {
const {blockhash, feeCalculator} = await helpers.recentBlockhash({
connection,
commitment,
});
expect(bs58.decode(blockhash)).to.have.length(32);
expect(feeCalculator.lamportsPerSignature).to.be.at.least(0);
}
});
it('get latest blockhash', async () => {
const commitments: Commitment[] = ['processed', 'confirmed', 'finalized'];
for (const commitment of commitments) {
const {blockhash, lastValidBlockHeight} = await helpers.latestBlockhash({
connection,
commitment,
});
expect(bs58.decode(blockhash)).to.have.length(32);
expect(lastValidBlockHeight).to.be.at.least(0);
}
});
it('get fee calculator', async () => {
const {blockhash} = await helpers.recentBlockhash({connection});
await mockRpcResponse({
method: 'getFeeCalculatorForBlockhash',
params: [blockhash, {commitment: 'confirmed'}],
value: {
feeCalculator: {
lamportsPerSignature: 5000,
},
},
withContext: true,
});
const feeCalculator = (
await connection.getFeeCalculatorForBlockhash(blockhash, 'confirmed')
).value;
if (feeCalculator === null) {
expect(feeCalculator).not.to.be.null;
return;
}
expect(feeCalculator.lamportsPerSignature).to.eq(5000);
});
it('get fee for message (legacy)', async () => {
const accountFrom = Keypair.generate();
const accountTo = Keypair.generate();
const latestBlockhash = await helpers.latestBlockhash({connection});
const transaction = new Transaction({
feePayer: accountFrom.publicKey,
...latestBlockhash,
}).add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 10,
}),
);
const message = transaction.compileMessage();
await mockRpcResponse({
method: 'getFeeForMessage',
params: [
message.serialize().toString('base64'),
{commitment: 'confirmed'},
],
value: 5000,
withContext: true,
});
const fee = (await connection.getFeeForMessage(message, 'confirmed')).value;
expect(fee).to.eq(5000);
});
it('get fee for message (v0)', async () => {
const accountFrom = Keypair.generate();
const accountTo = Keypair.generate();
const recentBlockhash = (await helpers.latestBlockhash({connection}))
.blockhash;
const instructions = [
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 10,
}),
];
const messageV0 = new TransactionMessage({
payerKey: accountFrom.publicKey,
recentBlockhash,
instructions,
}).compileToV0Message();
await mockRpcResponse({
method: 'getFeeForMessage',
params: [
toBuffer(messageV0.serialize()).toString('base64'),
{commitment: 'confirmed'},
],
value: 5000,
withContext: true,
});
const fee = (await connection.getFeeForMessage(messageV0, 'confirmed'))
.value;
expect(fee).to.eq(5000);
});
it('get block time', async () => {
await mockRpcResponse({
method: 'getBlockTime',
params: [1],
value: 10000,
});
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 1,
});
const slot = await connection.getFirstAvailableBlock();
const blockTime = await connection.getBlockTime(slot);
if (blockTime === null) {
expect(blockTime).not.to.be.null;
} else {
expect(blockTime).to.be.greaterThan(0);
}
});
it('get minimum ledger slot', async () => {
await mockRpcResponse({
method: 'minimumLedgerSlot',
params: [],
value: 0,
});
const minimumLedgerSlot = await connection.getMinimumLedgerSlot();
expect(minimumLedgerSlot).to.be.at.least(0);
});
it('get first available block', async () => {
await mockRpcResponse({
method: 'getFirstAvailableBlock',
params: [],
value: 0,
});
const firstAvailableBlock = await connection.getFirstAvailableBlock();
expect(firstAvailableBlock).to.be.at.least(0);
});
it('get supply', async () => {
await mockRpcResponse({
method: 'getSupply',
params: [{commitment: 'finalized'}],
value: {
total: 1000,
circulating: 100,
nonCirculating: 900,
nonCirculatingAccounts: [Keypair.generate().publicKey.toBase58()],
},
withContext: true,
});
const supply = (await connection.getSupply('finalized')).value;
expect(supply.total).to.be.greaterThan(0);
expect(supply.circulating).to.be.greaterThan(0);
expect(supply.nonCirculating).to.be.at.least(0);
expect(supply.nonCirculatingAccounts.length).to.be.at.least(0);
});
it('get supply without accounts', async () => {
await mockRpcResponse({
method: 'getSupply',
params: [{commitment: 'finalized'}],
value: {
total: 1000,
circulating: 100,
nonCirculating: 900,
nonCirculatingAccounts: [],
},
withContext: true,
});
const supply = (
await connection.getSupply({
commitment: 'finalized',
excludeNonCirculatingAccountsList: true,
})
).value;
expect(supply.total).to.be.greaterThan(0);
expect(supply.circulating).to.be.greaterThan(0);
expect(supply.nonCirculating).to.be.at.least(0);
expect(supply.nonCirculatingAccounts.length).to.eq(0);
});
[undefined, 'confirmed' as Commitment].forEach(function (commitment) {
describe(
"when the connection's default commitment is `" + commitment + '`',
() => {
let connectionWithCommitment: Connection;
beforeEach(() => {
connectionWithCommitment = new Connection(url, commitment);
});
it('get performance samples', async () => {
await mockRpcResponse({
method: 'getRecentPerformanceSamples',
params: [],
value: [
{
slot: 1234,
numTransactions: 1000,
numSlots: 60,
samplePeriodSecs: 60,
},
],
});
const perfSamples =
await connectionWithCommitment.getRecentPerformanceSamples();
expect(Array.isArray(perfSamples)).to.be.true;
if (perfSamples.length > 0) {
expect(perfSamples[0].slot).to.be.greaterThan(0);
expect(perfSamples[0].numTransactions).to.be.greaterThan(0);
expect(perfSamples[0].numSlots).to.be.greaterThan(0);
expect(perfSamples[0].samplePeriodSecs).to.be.greaterThan(0);
}
});
},
);
});
it('get performance samples limit too high', async () => {
await mockRpcResponse({
method: 'getRecentPerformanceSamples',
params: [100000],
error: mockErrorResponse,
});
await expect(connection.getRecentPerformanceSamples(100000)).to.be.rejected;
});
const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
);
if (process.env.TEST_LIVE) {
describe('token methods', () => {
const connection = new Connection(url, 'confirmed');
const newAccount = PublicKey.unique();
let payerKeypair = new Keypair();
let testTokenMintPubkey: PublicKey;
let testOwnerKeypair: Keypair;
let testTokenAccountPubkey: PublicKey;
let testSignature: TransactionSignature;
// Setup token mints and accounts for token tests
before(async function () {
this.timeout(30 * 1000);
await connection.confirmTransaction(
await connection.requestAirdrop(payerKeypair.publicKey, 100000000000),
);
const mintOwnerKeypair = new Keypair();
const accountOwnerKeypair = new Keypair();
const mintPubkey = await splToken.createMint(
connection as any,
payerKeypair,
mintOwnerKeypair.publicKey,
null, // freeze authority
2, // decimals
);
const tokenAccountPubkey = await splToken.createAccount(
connection as any,
payerKeypair,
mintPubkey,
accountOwnerKeypair.publicKey,
);
await splToken.mintTo(
connection as any,
payerKeypair,
mintPubkey,
tokenAccountPubkey,
mintOwnerKeypair,
11111,
);
const mintPubkey2 = await splToken.createMint(
connection as any,
payerKeypair,
mintOwnerKeypair.publicKey,
null, // freeze authority
2, // decimals
);
const tokenAccountPubkey2 = await splToken.createAccount(
connection as any,
payerKeypair,
mintPubkey2,
accountOwnerKeypair.publicKey,
);
await splToken.mintTo(
connection as any,
payerKeypair,
mintPubkey2,
tokenAccountPubkey2,
mintOwnerKeypair,
100,
);
const tokenAccountDestPubkey = await splToken.createAccount(
connection as any,
payerKeypair,
mintPubkey,
accountOwnerKeypair.publicKey,
new Keypair() as any,
);
testSignature = await splToken.transfer(
connection as any,
payerKeypair,
tokenAccountPubkey,
tokenAccountDestPubkey,
accountOwnerKeypair,
1,
);
testTokenMintPubkey = mintPubkey as PublicKey;
testOwnerKeypair = accountOwnerKeypair;
testTokenAccountPubkey = tokenAccountPubkey as PublicKey;
});
it('get token supply', async () => {
const supply = (await connection.getTokenSupply(testTokenMintPubkey))
.value;
expect(supply.uiAmount).to.eq(111.11);
expect(supply.decimals).to.eq(2);
expect(supply.amount).to.eq('11111');
await expect(connection.getTokenSupply(newAccount)).to.be.rejected;
});
it('get token largest accounts', async () => {
const largestAccounts = (
await connection.getTokenLargestAccounts(testTokenMintPubkey)
).value;
expect(largestAccounts).to.have.length(2);
const largestAccount = largestAccounts[0];
expect(largestAccount.address.equals(testTokenAccountPubkey)).to.be
.true;
expect(largestAccount.amount).to.eq('11110');
expect(largestAccount.decimals).to.eq(2);
expect(largestAccount.uiAmount).to.eq(111.1);
await expect(connection.getTokenLargestAccounts(newAccount)).to.be
.rejected;
});
it('get confirmed token transaction', async () => {
const parsedTx = await connection.getParsedConfirmedTransaction(
testSignature,
);
if (parsedTx === null) {
expect(parsedTx).not.to.be.null;
return;
}
const {signatures, message} = parsedTx.transaction;
expect(signatures[0]).to.eq(testSignature);
const ix = message.instructions[0];
if ('parsed' in ix) {
expect(ix.program).to.eq('spl-token');
expect(ix.programId).to.eql(TOKEN_PROGRAM_ID);
} else {
expect('parsed' in ix).to.be.true;
}
const missingSignature =
'45pGoC4Rr3fJ1TKrsiRkhHRbdUeX7633XAGVec6XzVdpRbzQgHhe6ZC6Uq164MPWtiqMg7wCkC6Wy3jy2BqsDEKf';
const nullResponse = await connection.getParsedConfirmedTransaction(
missingSignature,
);
expect(nullResponse).to.be.null;
});
it('get token account balance', async () => {
const balance = (
await connection.getTokenAccountBalance(testTokenAccountPubkey)
).value;
expect(balance.amount).to.eq('11110');
expect(balance.decimals).to.eq(2);
expect(balance.uiAmount).to.eq(111.1);
await expect(connection.getTokenAccountBalance(newAccount)).to.be
.rejected;
});
it('get parsed token account info', async () => {
const accountInfo = (
await connection.getParsedAccountInfo(testTokenAccountPubkey)
).value;
if (accountInfo) {
const data = accountInfo.data;
if (Buffer.isBuffer(data)) {
expect(Buffer.isBuffer(data)).to.eq(false);
} else {
expect(data.program).to.eq('spl-token');
expect(data.parsed).to.be.ok;
}
}
});
it('get multiple parsed token accounts', async () => {
const accounts = (
await connection.getMultipleParsedAccounts([
testTokenAccountPubkey,
testTokenMintPubkey,
payerKeypair.publicKey,
newAccount,
])
).value;
expect(accounts.length).to.eq(4);
const parsedTokenAccount = accounts[0];
if (parsedTokenAccount) {
const data = parsedTokenAccount.data;
if (Buffer.isBuffer(data)) {
expect(Buffer.isBuffer(data)).to.eq(false);
} else {
expect(data.program).to.eq('spl-token');
expect(data.parsed).to.be.ok;
}
} else {
expect(parsedTokenAccount).to.be.ok;
}
const parsedTokenMint = accounts[1];
if (parsedTokenMint) {
const data = parsedTokenMint.data;
if (Buffer.isBuffer(data)) {
expect(Buffer.isBuffer(data)).to.eq(false);
} else {
expect(data.program).to.eq('spl-token');
expect(data.parsed).to.be.ok;
}
} else {
expect(parsedTokenMint).to.be.ok;
}
const unparsedPayerAccount = accounts[2];
if (unparsedPayerAccount) {
const data = unparsedPayerAccount.data;
expect(Buffer.isBuffer(data)).to.be.true;
} else {
expect(unparsedPayerAccount).to.be.ok;
}
const unknownAccount = accounts[3];
expect(unknownAccount).to.not.be.ok;
});
it('get parsed token program accounts', async () => {
const tokenAccounts = await connection.getParsedProgramAccounts(
TOKEN_PROGRAM_ID,
);
tokenAccounts.forEach(({account}) => {
expect(account.owner).to.eql(TOKEN_PROGRAM_ID);
const data = account.data;
if (Buffer.isBuffer(data)) {
expect(Buffer.isBuffer(data)).to.eq(false);
} else {
expect(data.parsed).to.be.ok;
expect(data.program).to.eq('spl-token');
}
});
});
it('get parsed token accounts by owner', async () => {
const tokenAccounts = (
await connection.getParsedTokenAccountsByOwner(
testOwnerKeypair.publicKey,
{
mint: testTokenMintPubkey,
},
)
).value;
tokenAccounts.forEach(({account}) => {
expect(account.owner).to.eql(TOKEN_PROGRAM_ID);
const data = account.data;
if (Buffer.isBuffer(data)) {
expect(Buffer.isBuffer(data)).to.eq(false);
} else {
expect(data.parsed).to.be.ok;
expect(data.program).to.eq('spl-token');
}
});
});
it('get token accounts by owner', async () => {
const accountsWithMintFilter = (
await connection.getTokenAccountsByOwner(testOwnerKeypair.publicKey, {
mint: testTokenMintPubkey,
})
).value;
expect(accountsWithMintFilter).to.have.length(2);
const accountsWithProgramFilter = (
await connection.getTokenAccountsByOwner(testOwnerKeypair.publicKey, {
programId: TOKEN_PROGRAM_ID,
})
).value;
expect(accountsWithProgramFilter).to.have.length(3);
const noAccounts = (
await connection.getTokenAccountsByOwner(newAccount, {
mint: testTokenMintPubkey,
})
).value;
expect(noAccounts).to.have.length(0);
await expect(
connection.getTokenAccountsByOwner(testOwnerKeypair.publicKey, {
mint: newAccount,
}),
).to.be.rejected;
await expect(
connection.getTokenAccountsByOwner(testOwnerKeypair.publicKey, {
programId: newAccount,
}),
).to.be.rejected;
});
});
it('consistent preflightCommitment', async () => {
const connection = new Connection(url, 'singleGossip');
const sender = Keypair.generate();
const recipient = Keypair.generate();
let signature = await connection.requestAirdrop(
sender.publicKey,
2 * LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(signature, 'singleGossip');
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient.publicKey,
lamports: LAMPORTS_PER_SOL,
}),
);
await sendAndConfirmTransaction(connection, transaction, [sender]);
});
}
it('get largest accounts', async () => {
await mockRpcResponse({
method: 'getLargestAccounts',
params: [],
value: new Array(20).fill(0).map(() => ({
address: Keypair.generate().publicKey.toBase58(),
lamports: 1000,
})),
withContext: true,
});
const largestAccounts = (await connection.getLargestAccounts()).value;
expect(largestAccounts).to.have.length(20);
});
it('stake activation should throw when called for not delegated account', async () => {
const publicKey = Keypair.generate().publicKey;
await mockRpcResponse({
method: 'getStakeActivation',
params: [publicKey.toBase58(), {}],
error: {message: 'account not delegated'},
});
await expect(connection.getStakeActivation(publicKey)).to.be.rejected;
});
if (process.env.TEST_LIVE) {
it('stake activation should return activating for new accounts', async () => {
// todo: use `Connection.getMinimumStakeDelegation` when implemented
const MIN_STAKE_DELEGATION = LAMPORTS_PER_SOL;
const STAKE_ACCOUNT_MIN_BALANCE =
await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
const voteAccounts = await connection.getVoteAccounts();
const voteAccount = voteAccounts.current.concat(
voteAccounts.delinquent,
)[0];
const votePubkey = new PublicKey(voteAccount.votePubkey);
const authorized = Keypair.generate();
let signature = await connection.requestAirdrop(
authorized.publicKey,
2 * LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(signature, 'confirmed');
const newStakeAccount = Keypair.generate();
let createAndInitialize = StakeProgram.createAccount({
fromPubkey: authorized.publicKey,
stakePubkey: newStakeAccount.publicKey,
authorized: new Authorized(authorized.publicKey, authorized.publicKey),
lockup: new Lockup(0, 0, new PublicKey(0)),
lamports: STAKE_ACCOUNT_MIN_BALANCE + MIN_STAKE_DELEGATION,
});
await sendAndConfirmTransaction(
connection,
createAndInitialize,
[authorized, newStakeAccount],
{
preflightCommitment: 'confirmed',
commitment: 'confirmed',
},
);
let delegation = StakeProgram.delegate({
stakePubkey: newStakeAccount.publicKey,
authorizedPubkey: authorized.publicKey,
votePubkey,
});
await sendAndConfirmTransaction(connection, delegation, [authorized], {
preflightCommitment: 'confirmed',
commitment: 'confirmed',
});
const LARGE_EPOCH = 4000;
await expect(
connection.getStakeActivation(
newStakeAccount.publicKey,
'confirmed',
LARGE_EPOCH,
),
).to.be.rejectedWith(
`failed to get Stake Activation ${newStakeAccount.publicKey.toBase58()}: Invalid param: epoch ${LARGE_EPOCH} has not yet started`,
);
const activationState = await connection.getStakeActivation(
newStakeAccount.publicKey,
'confirmed',
);
expect(activationState.state).to.eq('activating');
expect(activationState.inactive).to.eq(MIN_STAKE_DELEGATION);
expect(activationState.active).to.eq(0);
});
}
if (mockServer) {
it('stake activation should only accept state with valid string literals', async () => {
const publicKey = Keypair.generate().publicKey;
const addStakeActivationMock = async (state: any) => {
await mockRpcResponse({
method: 'getStakeActivation',
params: [publicKey.toBase58(), {}],
value: {
state: state,
active: 0,
inactive: 80,
},
});
};
await addStakeActivationMock('active');
let activation = await connection.getStakeActivation(
publicKey,
'confirmed',
);
expect(activation.state).to.eq('active');
expect(activation.active).to.eq(0);
expect(activation.inactive).to.eq(80);
await addStakeActivationMock('invalid');
await expect(connection.getStakeActivation(publicKey, 'confirmed')).to.be
.rejected;
});
}
it('getVersion', async () => {
await mockRpcResponse({
method: 'getVersion',
params: [],
value: {'solana-core': '0.20.4'},
});
const version = await connection.getVersion();
expect(version['solana-core']).to.be.ok;
});
it('getGenesisHash', async () => {
await mockRpcResponse({
method: 'getGenesisHash',
params: [],
value: 'GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC',
});
const genesisHash = await connection.getGenesisHash();
expect(genesisHash).not.to.be.empty;
});
it('request airdrop', async () => {
const account = Keypair.generate();
await helpers.airdrop({
connection,
address: account.publicKey,
amount: LAMPORTS_PER_SOL,
});
await mockRpcResponse({
method: 'getBalance',
params: [account.publicKey.toBase58(), {commitment: 'confirmed'}],
value: LAMPORTS_PER_SOL,
withContext: true,
});
const balance = await connection.getBalance(account.publicKey, 'confirmed');
expect(balance).to.eq(LAMPORTS_PER_SOL);
await mockRpcResponse({
method: 'getAccountInfo',
params: [
account.publicKey.toBase58(),
{commitment: 'confirmed', encoding: 'base64'},
],
value: {
owner: '11111111111111111111111111111111',
lamports: LAMPORTS_PER_SOL,
data: ['', 'base64'],
executable: false,
rentEpoch: 20,
},
withContext: true,
});
const accountInfo = await connection.getAccountInfo(
account.publicKey,
'confirmed',
);
if (accountInfo === null) {
expect(accountInfo).not.to.be.null;
return;
}
expect(accountInfo.lamports).to.eq(LAMPORTS_PER_SOL);
expect(accountInfo.data).to.have.length(0);
expect(accountInfo.owner).to.eql(SystemProgram.programId);
await mockRpcResponse({
method: 'getAccountInfo',
params: [
account.publicKey.toBase58(),
{commitment: 'confirmed', encoding: 'jsonParsed'},
],
value: {
owner: '11111111111111111111111111111111',
lamports: LAMPORTS_PER_SOL,
data: ['', 'base64'],
executable: false,
rentEpoch: 20,
},
withContext: true,
});
const parsedAccountInfo = (
await connection.getParsedAccountInfo(account.publicKey, 'confirmed')
).value;
if (parsedAccountInfo === null) {
expect(parsedAccountInfo).not.to.be.null;
return;
} else if ('parsed' in parsedAccountInfo.data) {
expect(parsedAccountInfo.data.parsed).not.to.be.ok;
return;
}
expect(parsedAccountInfo.lamports).to.eq(LAMPORTS_PER_SOL);
expect(parsedAccountInfo.data).to.have.length(0);
expect(parsedAccountInfo.owner).to.eql(SystemProgram.programId);
});
it('transaction failure', async () => {
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: LAMPORTS_PER_SOL,
});
const newAccount = Keypair.generate();
let transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: newAccount.publicKey,
lamports: LAMPORTS_PER_SOL / 2,
space: 0,
programId: SystemProgram.programId,
}),
);
await helpers.processTransaction({
connection,
transaction,
signers: [payer, newAccount],
commitment: 'confirmed',
});
// This should fail because the account is already created
const expectedErr = {InstructionError: [0, {Custom: 0}]};
const confirmResult = (
await helpers.processTransaction({
connection,
transaction,
signers: [payer, newAccount],
commitment: 'confirmed',
err: expectedErr,
})
).value;
expect(confirmResult.err).to.eql(expectedErr);
invariant(transaction.signature);
const signature = bs58.encode(transaction.signature);
await mockRpcResponse({
method: 'getSignatureStatuses',
params: [[signature]],
value: [
{
slot: 0,
confirmations: 11,
status: {Err: expectedErr},
err: expectedErr,
},
],
withContext: true,
});
const response = (await connection.getSignatureStatus(signature)).value;
verifySignatureStatus(response, expectedErr);
});
if (mockServer) {
it('returnData on simulateTransaction', async () => {
const tx = new Transaction();
tx.feePayer = Keypair.generate().publicKey;
const getLatestBlockhashResponse = {
method: 'getLatestBlockhash',
params: [],
value: {
blockhash: 'CSymwgTNX1j3E4qhKfJAUE41nBWEwXufoYryPbkde5RR',
feeCalculator: {
lamportsPerSignature: 5000,
},
lastValidBlockHeight: 51,
},
withContext: true,
};
const simulateTransactionResponse = {
method: 'simulateTransaction',
params: [],
value: {
err: null,
accounts: null,
logs: [
'Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri invoke [1]',
'Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri consumed 2366 of 1400000 compute units',
'Program return: 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri KgAAAAAAAAA=',
'Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success',
],
returnData: {
data: ['KgAAAAAAAAA==', 'base64'],
programId: '83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri',
},
unitsConsumed: 2366,
},
withContext: true,
};
await mockRpcResponse(getLatestBlockhashResponse);
await mockRpcResponse(simulateTransactionResponse);
const response = (await connection.simulateTransaction(tx)).value;
expect(response.returnData).to.eql({
data: ['KgAAAAAAAAA==', 'base64'],
programId: '83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri',
});
});
}
if (process.env.TEST_LIVE) {
it('getStakeMinimumDelegation', async () => {
const {value} = await connection.getStakeMinimumDelegation();
expect(value).to.be.a('number');
});
it('sendTransaction', async () => {
const connection = new Connection(url, 'confirmed');
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: LAMPORTS_PER_SOL,
});
const recentBlockhash = await (
await helpers.latestBlockhash({connection})
).blockhash;
const versionedTx = new VersionedTransaction(
new Message({
header: {
numRequiredSignatures: 1,
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 0,
},
recentBlockhash,
instructions: [],
accountKeys: [payer.publicKey.toBase58()],
}),
);
versionedTx.sign([payer]);
await connection.sendTransaction(versionedTx);
});
it('simulateTransaction', async () => {
const connection = new Connection(url, 'confirmed');
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: LAMPORTS_PER_SOL,
});
const recentBlockhash = await (
await helpers.latestBlockhash({connection})
).blockhash;
const versionedTx = new VersionedTransaction(
new Message({
header: {
numRequiredSignatures: 1,
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 0,
},
recentBlockhash,
instructions: [],
accountKeys: [payer.publicKey.toBase58()],
}),
);
const response = await connection.simulateTransaction(versionedTx, {
accounts: {
encoding: 'base64',
addresses: [payer.publicKey.toBase58()],
},
});
expect(response.value.err).to.be.null;
expect(response.value.accounts).to.eql([
{
data: ['', 'base64'],
executable: false,
lamports: LAMPORTS_PER_SOL - 5000,
owner: SystemProgram.programId.toBase58(),
rentEpoch: 0,
space: 0,
},
]);
});
it('simulate transaction with message', async () => {
connection._commitment = 'confirmed';
const account1 = Keypair.generate();
const account2 = Keypair.generate();
await helpers.airdrop({
connection,
address: account1.publicKey,
amount: LAMPORTS_PER_SOL,
});
await helpers.airdrop({
connection,
address: account2.publicKey,
amount: LAMPORTS_PER_SOL,
});
const recentBlockhash = await (
await helpers.latestBlockhash({connection})
).blockhash;
const message = new Message({
accountKeys: [
account1.publicKey.toString(),
account2.publicKey.toString(),
'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo',
],
header: {
numReadonlySignedAccounts: 1,
numReadonlyUnsignedAccounts: 2,
numRequiredSignatures: 1,
},
instructions: [
{
accounts: [0, 1],
data: bs58.encode(Buffer.alloc(5).fill(9)),
programIdIndex: 2,
},
],
recentBlockhash,
});
const results1 = await connection.simulateTransaction(
message,
[account1],
true,
);
expect(results1.value.accounts).lengthOf(2);
const results2 = await connection.simulateTransaction(
message,
[account1],
[
account1.publicKey,
new PublicKey('Missing111111111111111111111111111111111111'),
],
);
expect(results2.value.accounts).lengthOf(2);
if (results2.value.accounts) {
expect(results2.value.accounts[1]).to.be.null;
}
}).timeout(10000);
it('transaction', async () => {
connection._commitment = 'confirmed';
const accountFrom = Keypair.generate();
const accountTo = Keypair.generate();
const minimumAmount = await connection.getMinimumBalanceForRentExemption(
0,
);
await helpers.airdrop({
connection,
address: accountFrom.publicKey,
amount: minimumAmount + 100010,
});
await helpers.airdrop({
connection,
address: accountTo.publicKey,
amount: minimumAmount,
});
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 10,
}),
);
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[accountFrom],
{preflightCommitment: 'confirmed'},
);
// Send again and ensure that new blockhash is used
const lastFetch = Date.now();
const transaction2 = new Transaction().add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 10,
}),
);
const signature2 = await sendAndConfirmTransaction(
connection,
transaction2,
[accountFrom],
{preflightCommitment: 'confirmed'},
);
expect(signature).not.to.eq(signature2);
expect(transaction.recentBlockhash).not.to.eq(
transaction2.recentBlockhash,
);
// Send new transaction and ensure that same blockhash is used
const transaction3 = new Transaction().add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 9,
}),
);
await sendAndConfirmTransaction(connection, transaction3, [accountFrom], {
preflightCommitment: 'confirmed',
});
expect(transaction2.recentBlockhash).to.eq(transaction3.recentBlockhash);
// Sleep until blockhash cache times out
await sleep(
Math.max(
0,
1000 + BLOCKHASH_CACHE_TIMEOUT_MS - (Date.now() - lastFetch),
),
);
const transaction4 = new Transaction().add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 13,
}),
);
await sendAndConfirmTransaction(connection, transaction4, [accountFrom], {
preflightCommitment: 'confirmed',
});
expect(transaction4.recentBlockhash).not.to.eq(
transaction3.recentBlockhash,
);
// accountFrom may have less than 100000 due to transaction fees
const balance = await connection.getBalance(accountFrom.publicKey);
expect(balance).to.be.greaterThan(0);
expect(balance).to.be.at.most(minimumAmount + 100000);
expect(await connection.getBalance(accountTo.publicKey)).to.eq(
minimumAmount + 42,
);
}).timeout(45 * 1000); // waits 30s for cache timeout
it('multi-instruction transaction', async () => {
connection._commitment = 'confirmed';
const accountFrom = Keypair.generate();
const accountTo = Keypair.generate();
let signature = await connection.requestAirdrop(
accountFrom.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(signature);
expect(await connection.getBalance(accountFrom.publicKey)).to.eq(
LAMPORTS_PER_SOL,
);
const minimumAmount = await connection.getMinimumBalanceForRentExemption(
0,
);
signature = await connection.requestAirdrop(
accountTo.publicKey,
minimumAmount + 21,
);
await connection.confirmTransaction(signature);
expect(await connection.getBalance(accountTo.publicKey)).to.eq(
minimumAmount + 21,
);
// 1. Move(accountFrom, accountTo)
// 2. Move(accountTo, accountFrom)
const transaction = new Transaction()
.add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
toPubkey: accountTo.publicKey,
lamports: 100,
}),
)
.add(
SystemProgram.transfer({
fromPubkey: accountTo.publicKey,
toPubkey: accountFrom.publicKey,
lamports: 100,
}),
);
signature = await connection.sendTransaction(
transaction,
[accountFrom, accountTo],
{skipPreflight: true},
);
await connection.confirmTransaction({
blockhash: transaction.recentBlockhash!,
lastValidBlockHeight: transaction.lastValidBlockHeight!,
signature: signature,
});
const response = (await connection.getSignatureStatus(signature)).value;
if (response !== null) {
expect(typeof response.slot).to.eq('number');
expect(response.err).to.be.null;
} else {
expect(response).not.to.be.null;
}
// accountFrom may have less than LAMPORTS_PER_SOL due to transaction fees
expect(
await connection.getBalance(accountFrom.publicKey),
).to.be.greaterThan(0);
expect(await connection.getBalance(accountFrom.publicKey)).to.be.at.most(
LAMPORTS_PER_SOL,
);
expect(await connection.getBalance(accountTo.publicKey)).to.eq(
minimumAmount + 21,
);
});
describe('given an open websocket connection', () => {
beforeEach(async () => {
// Open the socket connection and wait for it to become pingable.
connection._rpcWebSocket.connect();
// eslint-disable-next-line no-constant-condition
while (true) {
try {
await connection._rpcWebSocket.notify('ping');
break;
// eslint-disable-next-line no-empty
} catch (_err) {}
await sleep(100);
}
});
it('account change notification', async () => {
const connection = new Connection(url, 'confirmed');
const owner = Keypair.generate();
let subscriptionId: number | undefined;
try {
const accountInfoPromise = new Promise<AccountInfo<Buffer>>(
resolve => {
subscriptionId = connection.onAccountChange(
owner.publicKey,
resolve,
'confirmed',
);
},
);
connection.requestAirdrop(owner.publicKey, LAMPORTS_PER_SOL);
const accountInfo = await accountInfoPromise;
expect(accountInfo.lamports).to.eq(LAMPORTS_PER_SOL);
expect(accountInfo.owner.equals(SystemProgram.programId)).to.be.true;
} finally {
if (subscriptionId != null) {
await connection.removeAccountChangeListener(subscriptionId);
}
}
});
it('program account change notification', async () => {
connection._commitment = 'confirmed';
const owner = Keypair.generate();
const programAccount = Keypair.generate();
const balanceNeeded =
await connection.getMinimumBalanceForRentExemption(0);
let subscriptionId: number | undefined;
try {
const keyedAccountInfoPromise = new Promise<KeyedAccountInfo>(
resolve => {
subscriptionId = connection.onProgramAccountChange(
SystemProgram.programId,
resolve,
);
},
);
await helpers.airdrop({
connection,
address: owner.publicKey,
amount: LAMPORTS_PER_SOL,
});
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: owner.publicKey,
toPubkey: programAccount.publicKey,
lamports: balanceNeeded,
}),
);
await sendAndConfirmTransaction(connection, transaction, [owner], {
commitment: 'confirmed',
});
const keyedAccountInfo = await keyedAccountInfoPromise;
if (keyedAccountInfo.accountId.equals(programAccount.publicKey)) {
expect(keyedAccountInfo.accountInfo.lamports).to.eq(balanceNeeded);
expect(
keyedAccountInfo.accountInfo.owner.equals(
SystemProgram.programId,
),
).to.be.true;
}
} finally {
if (subscriptionId != null) {
await connection.removeProgramAccountChangeListener(subscriptionId);
}
}
});
it('slot notification', async () => {
let subscriptionId: number | undefined;
try {
const notifiedSlotInfo = await new Promise<SlotInfo>(resolve => {
subscriptionId = connection.onSlotChange(resolve);
});
expect(notifiedSlotInfo.parent).to.be.at.least(0);
expect(notifiedSlotInfo.root).to.be.at.least(0);
expect(notifiedSlotInfo.slot).to.be.at.least(1);
} finally {
if (subscriptionId != null) {
await connection.removeSlotChangeListener(subscriptionId);
}
}
});
it('root notification', async () => {
let subscriptionId: number | undefined;
try {
const atLeastTwoRoots = await new Promise<number[]>(resolve => {
const roots: number[] = [];
subscriptionId = connection.onRootChange(root => {
if (roots.length === 2) {
return;
}
roots.push(root);
if (roots.length === 2) {
// Collect at least two, then resolve.
resolve(roots);
}
});
});
expect(atLeastTwoRoots[1]).to.be.greaterThan(atLeastTwoRoots[0]);
} finally {
if (subscriptionId != null) {
await connection.removeRootChangeListener(subscriptionId);
}
}
});
it('signature notification', async () => {
const owner = Keypair.generate();
const signature = await connection.requestAirdrop(
owner.publicKey,
LAMPORTS_PER_SOL,
);
const signatureResult = await new Promise<SignatureResult>(resolve => {
// NOTE: Signature subscriptions auto-remove themselves, so there's no
// need to track the subscription id and remove it when the test ends.
connection.onSignature(signature, resolve, 'processed');
});
expect(signatureResult.err).to.be.null;
});
it('logs notification', async () => {
let subscriptionId: number | undefined;
const owner = Keypair.generate();
try {
const logPromise = new Promise<[Logs, Context]>(resolve => {
subscriptionId = connection.onLogs(
owner.publicKey,
(logs, ctx) => {
if (!logs.err) {
resolve([logs, ctx]);
}
},
'processed',
);
});
// Execute a transaction so that we can pickup its logs.
await connection.requestAirdrop(owner.publicKey, LAMPORTS_PER_SOL);
const [logsRes, ctx] = await logPromise;
expect(ctx.slot).to.be.greaterThan(0);
expect(logsRes.logs.length).to.eq(2);
expect(logsRes.logs[0]).to.eq(
'Program 11111111111111111111111111111111 invoke [1]',
);
expect(logsRes.logs[1]).to.eq(
'Program 11111111111111111111111111111111 success',
);
} finally {
if (subscriptionId != null) {
await connection.removeOnLogsListener(subscriptionId);
}
}
});
});
it('https request', async () => {
const connection = new Connection('https://api.mainnet-beta.solana.com');
const version = await connection.getVersion();
expect(version['solana-core']).to.be.ok;
}).timeout(20 * 1000);
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();
before(async () => {
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: 10 * LAMPORTS_PER_SOL,
});
});
it('createLookupTable', async () => {
const recentSlot = await connection.getSlot('finalized');
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',
});
});
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',
});
});
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,
{
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.be.undefined;
expect(lookupTableAccount.state.addresses).to.eql(lookupTableAddresses);
});
});
describe('v0 transaction', () => {
const connection = new Connection(url);
const payer = Keypair.generate();
before(async () => {
await helpers.airdrop({
connection,
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;
return;
}
// eslint-disable-next-line no-constant-condition
while (true) {
const latestSlot = await connection.getSlot('confirmed');
if (latestSlot > lookupTableAccount.state.lastExtendedSlot) {
break;
} else {
console.log('Waiting for next slot...');
await sleep(500);
}
}
});
let signature: TransactionSignature;
let addressTableLookups: MessageAddressTableLookup[];
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: {
numRequiredSignatures: 1,
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 1,
},
staticAccountKeys: [payer.publicKey, SystemProgram.programId],
recentBlockhash: blockhash,
compiledInstructions: [
{
programIdIndex: 1,
accountKeyIndexes: [0, 2],
data: transferIxData,
},
],
addressTableLookups,
}),
);
transaction.sign([payer]);
signature = bs58.encode(transaction.signatures[0]);
const serializedTransaction = transaction.serialize();
await connection.sendRawTransaction(serializedTransaction, {
preflightCommitment: 'confirmed',
});
await connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
},
'confirmed',
);
const transferToKey = lookupTableAddresses[0];
const transferToAccount = await connection.getAccountInfo(
transferToKey,
{commitment: 'confirmed', dataSlice: {length: 0, offset: 0}},
);
expect(transferToAccount?.data.length).to.be.eq(0);
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: number;
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.meta?.computeUnitsConsumed).to.not.be
.undefined;
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);
// loaded addresses are not returned for parsed transactions
expect(parsedTransaction?.meta?.loadedAddresses).to.be.undefined;
expect(parsedTransaction?.meta?.computeUnitsConsumed).to.not.be
.undefined;
expect(
parsedTransaction?.transaction.message.addressTableLookups,
).to.eql(addressTableLookups);
expect(parsedTransaction?.transaction.message.accountKeys).to.eql([
{
pubkey: payer.publicKey,
signer: true,
writable: true,
source: 'transaction',
},
{
pubkey: SystemProgram.programId,
signer: false,
writable: false,
source: 'transaction',
},
{
pubkey: lookupTableAddresses[0],
signer: false,
writable: true,
source: 'lookupTable',
},
]);
});
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;
});
it('getParsedBlock', async () => {
const block = await connection.getParsedBlock(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);
}
});