UNPKG

quasdeserunt

Version:
1,844 lines (1,689 loc) 91.1 kB
// @flow import assert from 'assert'; import bs58 from 'bs58'; import {Buffer} from 'buffer'; import {parse as urlParse, format as urlFormat} from 'url'; import fetch from 'node-fetch'; import jayson from 'jayson/lib/client/browser'; import { type as pick, number, string, array, boolean, literal, record, union, optional, nullable, coerce, instance, create, tuple, unknown, } from 'superstruct'; import type {Struct} from 'superstruct'; import {Client as RpcWebSocketClient} from 'rpc-websockets'; import {AgentManager} from './agent-manager'; import {NonceAccount} from './nonce-account'; import {PublicKey} from './publickey'; import {MS_PER_SLOT} from './timing'; import {Transaction} from './transaction'; import {Message} from './message'; import {sleep} from './util/sleep'; import {promiseTimeout} from './util/promise-timeout'; import {toBuffer} from './util/to-buffer'; import type {Blockhash} from './blockhash'; import type {FeeCalculator} from './fee-calculator'; import type {Account} from './account'; import type {TransactionSignature} from './transaction'; import type {CompiledInstruction} from './message'; const PublicKeyFromString = coerce( instance(PublicKey), string(), value => new PublicKey(value), ); const RawAccountDataResult = tuple([string(), literal('base64')]); const BufferFromRawAccountData = coerce( instance(Buffer), RawAccountDataResult, value => Buffer.from(value[0], 'base64'), ); export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000; type RpcRequest = (methodName: string, args: Array<any>) => any; type TokenAccountsFilter = | {| mint: PublicKey, |} | {| programId: PublicKey, |}; /** * Extra contextual information for RPC responses * * @typedef {Object} Context * @property {number} slot */ type Context = { slot: number, }; /** * Options for sending transactions * * @typedef {Object} SendOptions * @property {boolean | undefined} skipPreflight disable transaction verification step * @property {Commitment | undefined} preflightCommitment preflight commitment level */ export type SendOptions = { skipPreflight?: boolean, preflightCommitment?: Commitment, }; /** * Options for confirming transactions * * @typedef {Object} ConfirmOptions * @property {boolean | undefined} skipPreflight disable transaction verification step * @property {Commitment | undefined} commitment desired commitment level * @property {Commitment | undefined} preflightCommitment preflight commitment level */ export type ConfirmOptions = { skipPreflight?: boolean, commitment?: Commitment, preflightCommitment?: Commitment, }; /** * Options for getConfirmedSignaturesForAddress2 * * @typedef {Object} ConfirmedSignaturesForAddress2Options * @property {TransactionSignature | undefined} before start searching backwards from this transaction signature. * If not provided the search starts from the highest max confirmed block. * @property {number | undefined} limit maximum transaction signatures to return (between 1 and 1,000, default: 1,000). * */ export type ConfirmedSignaturesForAddress2Options = { before?: TransactionSignature, limit?: number, }; /** * RPC Response with extra contextual information * * @typedef {Object} RpcResponseAndContext * @property {Context} context * @property {T} value response */ type RpcResponseAndContext<T> = { context: Context, value: T, }; /** * @private */ function createRpcResult<T, U>(result: Struct<T, U>) { return union([ pick({ jsonrpc: literal('2.0'), id: string(), result, }), pick({ jsonrpc: literal('2.0'), id: string(), error: pick({ code: unknown(), message: string(), data: optional(unknown()), }), }), ]); } const UnknownRpcResult = createRpcResult(unknown()); /** * @private */ function jsonRpcResult<T, U>(schema: Struct<T, U>) { return coerce(createRpcResult(schema), UnknownRpcResult, value => { if ('error' in value) { return value; } else { return { ...value, result: create(value.result, schema), }; } }); } /** * @private */ function jsonRpcResultAndContext<T, U>(value: Struct<T, U>) { return jsonRpcResult( pick({ context: pick({ slot: number(), }), value, }), ); } /** * @private */ function notificationResultAndContext<T, U>(value: Struct<T, U>) { return pick({ context: pick({ slot: number(), }), value, }); } /** * The level of commitment desired when querying state * <pre> * 'processed': Query the most recent block which has reached 1 confirmation by the connected node * 'confirmed': Query the most recent block which has reached 1 confirmation by the cluster * 'finalized': Query the most recent block which has been finalized by the cluster * </pre> * * @typedef {'processed' | 'confirmed' | 'finalized'} Commitment */ export type Commitment = | 'processed' | 'confirmed' | 'finalized' | 'recent' // Deprecated as of v1.5.5 | 'single' // Deprecated as of v1.5.5 | 'singleGossip' // Deprecated as of v1.5.5 | 'root' // Deprecated as of v1.5.5 | 'max'; // Deprecated as of v1.5.5 /** * Filter for largest accounts query * <pre> * 'circulating': Return the largest accounts that are part of the circulating supply * 'nonCirculating': Return the largest accounts that are not part of the circulating supply * </pre> * * @typedef {'circulating' | 'nonCirculating'} LargestAccountsFilter */ export type LargestAccountsFilter = 'circulating' | 'nonCirculating'; /** * Configuration object for changing `getLargestAccounts` query behavior * * @typedef {Object} GetLargestAccountsConfig * @property {Commitment|undefined} commitment The level of commitment desired * @property {LargestAccountsFilter|undefined} filter Filter largest accounts by whether they are part of the circulating supply */ type GetLargestAccountsConfig = { commitment: ?Commitment, filter: ?LargestAccountsFilter, }; /** * Configuration object for changing query behavior * * @typedef {Object} SignatureStatusConfig * @property {boolean} searchTransactionHistory enable searching status history, not needed for recent transactions */ export type SignatureStatusConfig = { searchTransactionHistory: boolean, }; /** * Information describing a cluster node * * @typedef {Object} ContactInfo * @property {string} pubkey Identity public key of the node * @property {string|null} gossip Gossip network address for the node * @property {string|null} tpu TPU network address for the node (null if not available) * @property {string|null} rpc JSON RPC network address for the node (null if not available) * @property {string|null} version Software version of the node (null if not available) */ type ContactInfo = { pubkey: string, gossip: string | null, tpu: string | null, rpc: string | null, version: string | null, }; /** * Information describing a vote account * * @typedef {Object} VoteAccountInfo * @property {string} votePubkey Public key of the vote account * @property {string} nodePubkey Identity public key of the node voting with this account * @property {number} activatedStake The stake, in lamports, delegated to this vote account and activated * @property {boolean} epochVoteAccount Whether the vote account is staked for this epoch * @property {Array<Array<number>>} epochCredits Recent epoch voting credit history for this voter * @property {number} commission A percentage (0-100) of rewards payout owed to the voter * @property {number} lastVote Most recent slot voted on by this vote account */ type VoteAccountInfo = { votePubkey: string, nodePubkey: string, activatedStake: number, epochVoteAccount: boolean, epochCredits: Array<[number, number, number]>, commission: number, lastVote: number, }; /** * A collection of cluster vote accounts * * @typedef {Object} VoteAccountStatus * @property {Array<VoteAccountInfo>} current Active vote accounts * @property {Array<VoteAccountInfo>} delinquent Inactive vote accounts */ type VoteAccountStatus = { current: Array<VoteAccountInfo>, delinquent: Array<VoteAccountInfo>, }; /** * Network Inflation * (see https://docs.solana.com/implemented-proposals/ed_overview) * * @typedef {Object} InflationGovernor * @property {number} foundation * @property {number} foundation_term * @property {number} initial * @property {number} taper * @property {number} terminal */ type InflationGovernor = { foundation: number, foundationTerm: number, initial: number, taper: number, terminal: number, }; const GetInflationGovernorResult = pick({ foundation: number(), foundationTerm: number(), initial: number(), taper: number(), terminal: number(), }); /** * Information about the current epoch * * @typedef {Object} EpochInfo * @property {number} epoch * @property {number} slotIndex * @property {number} slotsInEpoch * @property {number} absoluteSlot * @property {number} blockHeight * @property {number} transactionCount */ type EpochInfo = { epoch: number, slotIndex: number, slotsInEpoch: number, absoluteSlot: number, blockHeight: number | null, transactionCount: number | null, }; const GetEpochInfoResult = pick({ epoch: number(), slotIndex: number(), slotsInEpoch: number(), absoluteSlot: number(), blockHeight: optional(number()), transactionCount: optional(number()), }); /** * Epoch schedule * (see https://docs.solana.com/terminology#epoch) * * @typedef {Object} EpochSchedule * @property {number} slotsPerEpoch The maximum number of slots in each epoch * @property {number} leaderScheduleSlotOffset The number of slots before beginning of an epoch to calculate a leader schedule for that epoch * @property {boolean} warmup Indicates whether epochs start short and grow * @property {number} firstNormalEpoch The first epoch with `slotsPerEpoch` slots * @property {number} firstNormalSlot The first slot of `firstNormalEpoch` */ type EpochSchedule = { slotsPerEpoch: number, leaderScheduleSlotOffset: number, warmup: boolean, firstNormalEpoch: number, firstNormalSlot: number, }; const GetEpochScheduleResult = pick({ slotsPerEpoch: number(), leaderScheduleSlotOffset: number(), warmup: boolean(), firstNormalEpoch: number(), firstNormalSlot: number(), }); /** * Leader schedule * (see https://docs.solana.com/terminology#leader-schedule) * * @typedef {Object} LeaderSchedule */ type LeaderSchedule = { [address: string]: number[], }; // TODO: check if validating array(number()) is still extremely slow const GetLeaderScheduleResult = record(string(), unknown()); /** * Transaction error or null */ const TransactionErrorResult = nullable(pick({})); /** * Signature status for a transaction */ const SignatureStatusResult = pick({ err: TransactionErrorResult, }); /** * Version info for a node * * @typedef {Object} Version * @property {string} solana-core Version of solana-core */ const Version = pick({ 'solana-core': string(), 'feature-set': optional(nullable(number())), }); type SimulatedTransactionResponse = { err: TransactionError | string | null, logs: Array<string> | null, }; const SimulatedTransactionResponseStruct = jsonRpcResultAndContext( pick({ err: nullable(union([pick({}), string()])), logs: nullable(array(string())), }), ); type ParsedInnerInstruction = { index: number, instructions: (ParsedInstruction | PartiallyDecodedInstruction)[], }; type TokenBalance = { accountIndex: number, mint: string, uiTokenAmount: TokenAmount, }; /** * Metadata for a parsed confirmed transaction on the ledger * * @typedef {Object} ParsedConfirmedTransactionMeta * @property {number} fee The fee charged for processing the transaction * @property {Array<ParsedInnerInstruction>} innerInstructions An array of cross program invoked parsed instructions * @property {Array<number>} preBalances The balances of the transaction accounts before processing * @property {Array<number>} postBalances The balances of the transaction accounts after processing * @property {Array<string>} logMessages An array of program log messages emitted during a transaction * @property {Array<TokenBalance>} preTokenBalances The token balances of the transaction accounts before processing * @property {Array<TokenBalance>} postTokenBalances The token balances of the transaction accounts after processing * @property {object|null} err The error result of transaction processing */ type ParsedConfirmedTransactionMeta = { fee: number, innerInstructions?: ParsedInnerInstruction[], preBalances: Array<number>, postBalances: Array<number>, logMessages?: Array<string>, preTokenBalances?: Array<TokenBalance>, postTokenBalances?: Array<TokenBalance>, err: TransactionError | null, }; type CompiledInnerInstruction = { index: number, instructions: CompiledInstruction[], }; /** * Metadata for a confirmed transaction on the ledger * * @typedef {Object} ConfirmedTransactionMeta * @property {number} fee The fee charged for processing the transaction * @property {Array<CompiledInnerInstruction>} innerInstructions An array of cross program invoked instructions * @property {Array<number>} preBalances The balances of the transaction accounts before processing * @property {Array<number>} postBalances The balances of the transaction accounts after processing * @property {Array<string>} logMessages An array of program log messages emitted during a transaction * @property {Array<TokenBalance>} preTokenBalances The token balances of the transaction accounts before processing * @property {Array<TokenBalance>} postTokenBalances The token balances of the transaction accounts after processing * @property {object|null} err The error result of transaction processing */ type ConfirmedTransactionMeta = { fee: number, innerInstructions?: CompiledInnerInstruction[], preBalances: Array<number>, postBalances: Array<number>, logMessages?: Array<string>, preTokenBalances?: Array<TokenBalance>, postTokenBalances?: Array<TokenBalance>, err: TransactionError | null, }; /** * A confirmed transaction on the ledger * * @typedef {Object} ConfirmedTransaction * @property {number} slot The slot during which the transaction was processed * @property {Transaction} transaction The details of the transaction * @property {ConfirmedTransactionMeta|null} meta Metadata produced from the transaction * @property {number|null|undefined} blockTime The unix timestamp of when the transaction was processed */ type ConfirmedTransaction = { slot: number, transaction: Transaction, meta: ConfirmedTransactionMeta | null, blockTime?: number | null, }; /** * A partially decoded transaction instruction * * @typedef {Object} ParsedMessageAccount * @property {PublicKey} pubkey Public key of the account * @property {PublicKey} accounts Indicates if the account signed the transaction * @property {string} data Raw base-58 instruction data */ type PartiallyDecodedInstruction = {| programId: PublicKey, accounts: Array<PublicKey>, data: string, |}; /** * A parsed transaction message account * * @typedef {Object} ParsedMessageAccount * @property {PublicKey} pubkey Public key of the account * @property {boolean} signer Indicates if the account signed the transaction * @property {boolean} writable Indicates if the account is writable for this transaction */ type ParsedMessageAccount = { pubkey: PublicKey, signer: boolean, writable: boolean, }; /** * A parsed transaction instruction * * @typedef {Object} ParsedInstruction * @property {string} program Name of the program for this instruction * @property {PublicKey} programId ID of the program for this instruction * @property {any} parsed Parsed instruction info */ type ParsedInstruction = {| program: string, programId: PublicKey, parsed: any, |}; /** * A parsed transaction message * * @typedef {Object} ParsedMessage * @property {Array<ParsedMessageAccount>} accountKeys Accounts used in the instructions * @property {Array<ParsedInstruction | PartiallyDecodedInstruction>} instructions The atomically executed instructions for the transaction * @property {string} recentBlockhash Recent blockhash */ type ParsedMessage = { accountKeys: ParsedMessageAccount[], instructions: (ParsedInstruction | PartiallyDecodedInstruction)[], recentBlockhash: string, }; /** * A parsed transaction * * @typedef {Object} ParsedTransaction * @property {Array<string>} signatures Signatures for the transaction * @property {ParsedMessage} message Message of the transaction */ type ParsedTransaction = { signatures: Array<string>, message: ParsedMessage, }; /** * A parsed and confirmed transaction on the ledger * * @typedef {Object} ParsedConfirmedTransaction * @property {number} slot The slot during which the transaction was processed * @property {ParsedTransaction} transaction The details of the transaction * @property {ConfirmedTransactionMeta|null} meta Metadata produced from the transaction * @property {number|null|undefined} blockTime The unix timestamp of when the transaction was processed */ type ParsedConfirmedTransaction = { slot: number, transaction: ParsedTransaction, meta: ParsedConfirmedTransactionMeta | null, blockTime?: number | null, }; /** * A ConfirmedBlock on the ledger * * @typedef {Object} ConfirmedBlock * @property {Blockhash} blockhash Blockhash of this block * @property {Blockhash} previousBlockhash Blockhash of this block's parent * @property {number} parentSlot Slot index of this block's parent * @property {Array<object>} transactions Vector of transactions and status metas * @property {Array<object>} rewards Vector of block rewards */ type ConfirmedBlock = { blockhash: Blockhash, previousBlockhash: Blockhash, parentSlot: number, transactions: Array<{ transaction: Transaction, meta: ConfirmedTransactionMeta | null, }>, rewards: Array<{ pubkey: string, lamports: number, postBalance: number | null, rewardType: string | null, }>, }; /** * A performance sample * * @typedef {Object} PerfSample * @property {number} slot Slot number of sample * @property {number} numTransactions Number of transactions in a sample window * @property {number} numSlots Number of slots in a sample window * @property {number} samplePeriodSecs Sample window in seconds */ type PerfSample = { slot: number, numTransactions: number, numSlots: number, samplePeriodSecs: number, }; function createRpcRequest(url: string, useHttps: boolean): RpcRequest { let agentManager; if (!process.env.BROWSER) { agentManager = new AgentManager(useHttps); } const server = jayson(async (request, callback) => { const agent = agentManager ? agentManager.requestStart() : undefined; const options = { method: 'POST', body: request, agent, headers: { 'Content-Type': 'application/json', }, }; try { let too_many_requests_retries = 5; let res = {}; let waitTime = 500; for (;;) { res = await fetch(url, options); if (res.status !== 429 /* Too many requests */) { break; } too_many_requests_retries -= 1; if (too_many_requests_retries === 0) { break; } console.log( `Server responded with ${res.status} ${res.statusText}. Retrying after ${waitTime}ms delay...`, ); await sleep(waitTime); waitTime *= 2; } const text = await res.text(); if (res.ok) { callback(null, text); } else { callback(new Error(`${res.status} ${res.statusText}: ${text}`)); } } catch (err) { callback(err); } finally { agentManager && agentManager.requestEnd(); } }); return (method, args) => { return new Promise((resolve, reject) => { server.request(method, args, (err, response) => { if (err) { reject(err); return; } resolve(response); }); }); }; } /** * Expected JSON RPC response for the "getInflationGovernor" message */ const GetInflationGovernorRpcResult = jsonRpcResult(GetInflationGovernorResult); /** * Expected JSON RPC response for the "getEpochInfo" message */ const GetEpochInfoRpcResult = jsonRpcResult(GetEpochInfoResult); /** * Expected JSON RPC response for the "getEpochSchedule" message */ const GetEpochScheduleRpcResult = jsonRpcResult(GetEpochScheduleResult); /** * Expected JSON RPC response for the "getLeaderSchedule" message */ const GetLeaderScheduleRpcResult = jsonRpcResult(GetLeaderScheduleResult); /** * Expected JSON RPC response for the "minimumLedgerSlot" and "getFirstAvailableBlock" messages */ const SlotRpcResult = jsonRpcResult(number()); /** * Supply * * @typedef {Object} Supply * @property {number} total Total supply in lamports * @property {number} circulating Circulating supply in lamports * @property {number} nonCirculating Non-circulating supply in lamports * @property {Array<PublicKey>} nonCirculatingAccounts List of non-circulating account addresses */ type Supply = { total: number, circulating: number, nonCirculating: number, nonCirculatingAccounts: Array<PublicKey>, }; /** * Expected JSON RPC response for the "getSupply" message */ const GetSupplyRpcResult = jsonRpcResultAndContext( pick({ total: number(), circulating: number(), nonCirculating: number(), nonCirculatingAccounts: array(PublicKeyFromString), }), ); /** * Token amount object which returns a token amount in different formats * for various client use cases. * * @typedef {Object} TokenAmount * @property {string} amount Raw amount of tokens as string ignoring decimals * @property {number} decimals Number of decimals configured for token's mint * @property {number} uiAmount Token account as float, accounts for decimals */ type TokenAmount = { amount: string, decimals: number, uiAmount: number, }; /** * Expected JSON RPC structure for token amounts */ const TokenAmountResult = pick({ amount: string(), uiAmount: number(), decimals: number(), }); /** * Token address and balance. * * @typedef {Object} TokenAccountBalancePair * @property {PublicKey} address Address of the token account * @property {string} amount Raw amount of tokens as string ignoring decimals * @property {number} decimals Number of decimals configured for token's mint * @property {number} uiAmount Token account as float, accounts for decimals */ type TokenAccountBalancePair = { address: PublicKey, amount: string, decimals: number, uiAmount: number, }; /** * Expected JSON RPC response for the "getTokenLargestAccounts" message */ const GetTokenLargestAccountsResult = jsonRpcResultAndContext( array( pick({ address: PublicKeyFromString, amount: string(), uiAmount: number(), decimals: number(), }), ), ); /** * Expected JSON RPC response for the "getTokenAccountsByOwner" message */ const GetTokenAccountsByOwner = jsonRpcResultAndContext( array( pick({ pubkey: PublicKeyFromString, account: pick({ executable: boolean(), owner: PublicKeyFromString, lamports: number(), data: BufferFromRawAccountData, rentEpoch: number(), }), }), ), ); const ParsedAccountDataResult = pick({ program: string(), parsed: unknown(), space: number(), }); /** * Expected JSON RPC response for the "getTokenAccountsByOwner" message with parsed data */ const GetParsedTokenAccountsByOwner = jsonRpcResultAndContext( array( pick({ pubkey: PublicKeyFromString, account: pick({ executable: boolean(), owner: PublicKeyFromString, lamports: number(), data: ParsedAccountDataResult, rentEpoch: number(), }), }), ), ); /** * Pair of an account address and its balance * * @typedef {Object} AccountBalancePair * @property {PublicKey} address * @property {number} lamports */ type AccountBalancePair = { address: PublicKey, lamports: number, }; /** * Expected JSON RPC response for the "getLargestAccounts" message */ const GetLargestAccountsRpcResult = jsonRpcResultAndContext( array( pick({ lamports: number(), address: PublicKeyFromString, }), ), ); /** * @private */ const AccountInfoResult = pick({ executable: boolean(), owner: PublicKeyFromString, lamports: number(), data: BufferFromRawAccountData, rentEpoch: number(), }); /** * @private */ const KeyedAccountInfoResult = pick({ pubkey: PublicKeyFromString, account: AccountInfoResult, }); const ParsedOrRawAccountData = coerce( union([instance(Buffer), ParsedAccountDataResult]), union([RawAccountDataResult, ParsedAccountDataResult]), value => { if (Array.isArray(value)) { return create(value, BufferFromRawAccountData); } else { return value; } }, ); /** * @private */ const ParsedAccountInfoResult = pick({ executable: boolean(), owner: PublicKeyFromString, lamports: number(), data: ParsedOrRawAccountData, rentEpoch: number(), }); const KeyedParsedAccountInfoResult = pick({ pubkey: PublicKeyFromString, account: ParsedAccountInfoResult, }); /** * @private */ const StakeActivationResult = pick({ state: union([ literal('active'), literal('inactive'), literal('activating'), literal('deactivating'), ]), active: number(), inactive: number(), }); /** * Expected JSON RPC response for the "getConfirmedSignaturesForAddress" message */ const GetConfirmedSignaturesForAddressRpcResult = jsonRpcResult( array(string()), ); /** * Expected JSON RPC response for the "getConfirmedSignaturesForAddress2" message */ const GetConfirmedSignaturesForAddress2RpcResult = jsonRpcResult( array( pick({ signature: string(), slot: number(), err: TransactionErrorResult, memo: nullable(string()), blockTime: optional(nullable(number())), }), ), ); /*** * Expected JSON RPC response for the "accountNotification" message */ const AccountNotificationResult = pick({ subscription: number(), result: notificationResultAndContext(AccountInfoResult), }); /** * @private */ const ProgramAccountInfoResult = pick({ pubkey: PublicKeyFromString, account: AccountInfoResult, }); /*** * Expected JSON RPC response for the "programNotification" message */ const ProgramAccountNotificationResult = pick({ subscription: number(), result: notificationResultAndContext(ProgramAccountInfoResult), }); /** * @private */ const SlotInfoResult = pick({ parent: number(), slot: number(), root: number(), }); /** * Expected JSON RPC response for the "slotNotification" message */ const SlotNotificationResult = pick({ subscription: number(), result: SlotInfoResult, }); /** * Expected JSON RPC response for the "signatureNotification" message */ const SignatureNotificationResult = pick({ subscription: number(), result: notificationResultAndContext(SignatureStatusResult), }); /** * Expected JSON RPC response for the "rootNotification" message */ const RootNotificationResult = pick({ subscription: number(), result: number(), }); const ContactInfoResult = pick({ pubkey: string(), gossip: nullable(string()), tpu: nullable(string()), rpc: nullable(string()), version: nullable(string()), }); const VoteAccountInfoResult = pick({ votePubkey: string(), nodePubkey: string(), activatedStake: number(), epochVoteAccount: boolean(), epochCredits: array(tuple([number(), number(), number()])), commission: number(), lastVote: number(), rootSlot: nullable(number()), }); /** * Expected JSON RPC response for the "getVoteAccounts" message */ const GetVoteAccounts = jsonRpcResult( pick({ current: array(VoteAccountInfoResult), delinquent: array(VoteAccountInfoResult), }), ); const SignatureStatusResponse = pick({ slot: number(), confirmations: nullable(number()), err: TransactionErrorResult, confirmationStatus: optional(nullable(string())), }); /** * Expected JSON RPC response for the "getSignatureStatuses" message */ const GetSignatureStatusesRpcResult = jsonRpcResultAndContext( array(nullable(SignatureStatusResponse)), ); /** * Expected JSON RPC response for the "getMinimumBalanceForRentExemption" message */ const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult(number()); /** * @private */ const ConfirmedTransactionResult = pick({ signatures: array(string()), message: pick({ accountKeys: array(string()), header: pick({ numRequiredSignatures: number(), numReadonlySignedAccounts: number(), numReadonlyUnsignedAccounts: number(), }), instructions: array( pick({ accounts: array(number()), data: string(), programIdIndex: number(), }), ), recentBlockhash: string(), }), }); const TransactionFromConfirmed = coerce( instance(Transaction), ConfirmedTransactionResult, result => { const {message, signatures} = result; return Transaction.populate(new Message(message), signatures); }, ); const ParsedInstructionResult = pick({ parsed: unknown(), program: string(), programId: PublicKeyFromString, }); const RawInstructionResult = pick({ accounts: array(PublicKeyFromString), data: string(), programId: PublicKeyFromString, }); const InstructionResult = union([ RawInstructionResult, ParsedInstructionResult, ]); const UnknownInstructionResult = union([ pick({ parsed: unknown(), program: string(), programId: string(), }), pick({ accounts: array(string()), data: string(), programId: string(), }), ]); const ParsedOrRawInstruction = coerce( InstructionResult, UnknownInstructionResult, value => { if ('accounts' in value) { return create(value, RawInstructionResult); } else { return create(value, ParsedInstructionResult); } }, ); /** * @private */ const ParsedConfirmedTransactionResult = pick({ signatures: array(string()), message: pick({ accountKeys: array( pick({ pubkey: PublicKeyFromString, signer: boolean(), writable: boolean(), }), ), instructions: array(ParsedOrRawInstruction), recentBlockhash: string(), }), }); const TokenBalanceResult = pick({ accountIndex: number(), mint: string(), uiTokenAmount: TokenAmountResult, }); /** * @private */ const ConfirmedTransactionMetaResult = pick({ err: TransactionErrorResult, fee: number(), innerInstructions: optional( nullable( array( pick({ index: number(), instructions: array( pick({ accounts: array(number()), data: string(), programIdIndex: number(), }), ), }), ), ), ), preBalances: array(number()), postBalances: array(number()), logMessages: optional(nullable(array(string()))), preTokenBalances: optional(nullable(array(TokenBalanceResult))), postTokenBalances: optional(nullable(array(TokenBalanceResult))), }); /** * @private */ const ParsedConfirmedTransactionMetaResult = pick({ err: TransactionErrorResult, fee: number(), innerInstructions: optional( nullable( array( pick({ index: number(), instructions: array(ParsedOrRawInstruction), }), ), ), ), preBalances: array(number()), postBalances: array(number()), logMessages: optional(nullable(array(string()))), preTokenBalances: optional(nullable(array(TokenBalanceResult))), postTokenBalances: optional(nullable(array(TokenBalanceResult))), }); /** * Expected JSON RPC response for the "getConfirmedBlock" message */ export const GetConfirmedBlockRpcResult = jsonRpcResult( nullable( pick({ blockhash: string(), previousBlockhash: string(), parentSlot: number(), transactions: array( pick({ transaction: TransactionFromConfirmed, meta: nullable(ConfirmedTransactionMetaResult), }), ), rewards: optional( array( pick({ pubkey: string(), lamports: number(), postBalance: nullable(number()), rewardType: nullable(string()), }), ), ), }), ), ); /** * Expected JSON RPC response for the "getConfirmedTransaction" message */ const GetConfirmedTransactionRpcResult = jsonRpcResult( nullable( pick({ slot: number(), transaction: TransactionFromConfirmed, meta: ConfirmedTransactionMetaResult, blockTime: optional(nullable(number())), }), ), ); /** * Expected JSON RPC response for the "getConfirmedTransaction" message */ const GetParsedConfirmedTransactionRpcResult = jsonRpcResult( nullable( pick({ slot: number(), transaction: ParsedConfirmedTransactionResult, meta: nullable(ParsedConfirmedTransactionMetaResult), blockTime: optional(nullable(number())), }), ), ); /** * Expected JSON RPC response for the "getRecentBlockhash" message */ const GetRecentBlockhashAndContextRpcResult = jsonRpcResultAndContext( pick({ blockhash: string(), feeCalculator: pick({ lamportsPerSignature: number(), }), }), ); const PerfSampleResult = pick({ slot: number(), numTransactions: number(), numSlots: number(), samplePeriodSecs: number(), }); /* * Expected JSON RPC response for "getRecentPerformanceSamples" message */ const GetRecentPerformanceSamplesRpcResult = jsonRpcResult( array(PerfSampleResult), ); /** * Expected JSON RPC response for the "getFeeCalculatorForBlockhash" message */ const GetFeeCalculatorRpcResult = jsonRpcResultAndContext( nullable( pick({ feeCalculator: pick({ lamportsPerSignature: number(), }), }), ), ); /** * Expected JSON RPC response for the "requestAirdrop" message */ const RequestAirdropRpcResult = jsonRpcResult(string()); /** * Expected JSON RPC response for the "sendTransaction" message */ const SendTransactionRpcResult = jsonRpcResult(string()); /** * Information about the latest slot being processed by a node * * @typedef {Object} SlotInfo * @property {number} slot Currently processing slot * @property {number} parent Parent of the current slot * @property {number} root The root block of the current slot's fork */ export type SlotInfo = { slot: number, parent: number, root: number, }; /** * Parsed account data * * @typedef {Object} ParsedAccountData * @property {string} program Name of the program that owns this account * @property {any} parsed Parsed account data * @property {number} space Space used by account data */ type ParsedAccountData = { program: string, parsed: any, space: number, }; /** * Stake Activation data * * @typedef {Object} StakeActivationData * @property {string} state: <string - the stake account's activation state, one of: active, inactive, activating, deactivating * @property {number} active: stake active during the epoch * @property {number} inactive: stake inactive during the epoch */ type StakeActivationData = { state: 'active' | 'inactive' | 'activating' | 'deactivating', active: number, inactive: number, }; /** * Information describing an account * * @typedef {Object} AccountInfo * @property {number} lamports Number of lamports assigned to the account * @property {PublicKey} owner Identifier of the program that owns the account * @property {T} data Optional data assigned to the account * @property {boolean} executable `true` if this account's data contains a loaded program */ type AccountInfo<T> = { executable: boolean, owner: PublicKey, lamports: number, data: T, }; /** * Account information identified by pubkey * * @typedef {Object} KeyedAccountInfo * @property {PublicKey} accountId * @property {AccountInfo<Buffer>} accountInfo */ export type KeyedAccountInfo = { accountId: PublicKey, accountInfo: AccountInfo<Buffer>, }; /** * Callback function for account change notifications */ export type AccountChangeCallback = ( accountInfo: AccountInfo<Buffer>, context: Context, ) => void; /** * @private */ type SubscriptionId = 'subscribing' | number; /** * @private */ type AccountSubscriptionInfo = { publicKey: string, // PublicKey of the account as a base 58 string callback: AccountChangeCallback, commitment: ?Commitment, subscriptionId: ?SubscriptionId, // null when there's no current server subscription id }; /** * Callback function for program account change notifications */ export type ProgramAccountChangeCallback = ( keyedAccountInfo: KeyedAccountInfo, context: Context, ) => void; /** * @private */ type ProgramAccountSubscriptionInfo = { programId: string, // PublicKey of the program as a base 58 string callback: ProgramAccountChangeCallback, commitment: ?Commitment, subscriptionId: ?SubscriptionId, // null when there's no current server subscription id }; /** * Callback function for slot change notifications */ export type SlotChangeCallback = (slotInfo: SlotInfo) => void; /** * @private */ type SlotSubscriptionInfo = { callback: SlotChangeCallback, subscriptionId: ?SubscriptionId, // null when there's no current server subscription id }; /** * Callback function for signature notifications */ export type SignatureResultCallback = ( signatureResult: SignatureResult, context: Context, ) => void; /** * @private */ type SignatureSubscriptionInfo = { signature: TransactionSignature, // TransactionSignature as a base 58 string callback: SignatureResultCallback, commitment: ?Commitment, subscriptionId: ?SubscriptionId, // null when there's no current server subscription id }; /** * Callback function for root change notifications */ export type RootChangeCallback = (root: number) => void; /** * @private */ type RootSubscriptionInfo = { callback: RootChangeCallback, subscriptionId: ?SubscriptionId, // null when there's no current server subscription id }; /** * Signature result * * @typedef {Object} SignatureResult */ export type SignatureResult = {| err: TransactionError | null, |}; /** * Transaction error * * @typedef {Object} TransactionError */ export type TransactionError = {}; /** * Signature status * * @typedef {Object} SignatureStatus * @property {number} slot when the transaction was processed * @property {number | null} confirmations the number of blocks that have been confirmed and voted on in the fork containing `slot` (TODO) * @property {TransactionError | null} err error, if any * @property {string | null} confirmationStatus the transaction's cluster confirmation status, if data available. Possible non-null responses: `processed`, `confirmed`, `finalized` */ export type SignatureStatus = { slot: number, confirmations: number | null, err: TransactionError | null, confirmationStatus: string | null, }; /** * A confirmed signature with its status * * @typedef {Object} ConfirmedSignatureInfo * @property {string} signature the transaction signature * @property {number} slot when the transaction was processed * @property {TransactionError | null} err error, if any * @property {string | null} memo memo associated with the transaction, if any * @property {number | null | undefined} blockTime The unix timestamp of when the transaction was processed */ export type ConfirmedSignatureInfo = { signature: string, slot: number, err: TransactionError | null, memo: string | null, blockTime?: number | null, }; /** * A connection to a fullnode JSON RPC endpoint */ export class Connection { _rpcEndpoint: string; _rpcRequest: RpcRequest; _rpcWebSocket: RpcWebSocketClient; _rpcWebSocketConnected: boolean = false; _rpcWebSocketHeartbeat: IntervalID | null = null; _rpcWebSocketIdleTimeout: TimeoutID | null = null; _commitment: ?Commitment; _blockhashInfo: { recentBlockhash: Blockhash | null, lastFetch: Date, simulatedSignatures: Array<string>, transactionSignatures: Array<string>, }; _disableBlockhashCaching: boolean = false; _pollingBlockhash: boolean = false; _accountChangeSubscriptions: {[number]: AccountSubscriptionInfo} = {}; _accountChangeSubscriptionCounter: number = 0; _programAccountChangeSubscriptions: { [number]: ProgramAccountSubscriptionInfo, } = {}; _programAccountChangeSubscriptionCounter: number = 0; _slotSubscriptions: { [number]: SlotSubscriptionInfo, } = {}; _slotSubscriptionCounter: number = 0; _signatureSubscriptions: { [number]: SignatureSubscriptionInfo, } = {}; _signatureSubscriptionCounter: number = 0; _rootSubscriptions: { [number]: RootSubscriptionInfo, } = {}; _rootSubscriptionCounter: number = 0; /** * Establish a JSON RPC connection * * @param endpoint URL to the fullnode JSON RPC endpoint * @param commitment optional default commitment level */ constructor(endpoint: string, commitment: ?Commitment) { this._rpcEndpoint = endpoint; let url = urlParse(endpoint); const useHttps = url.protocol === 'https:'; this._rpcRequest = createRpcRequest(url.href, useHttps); this._commitment = commitment; this._blockhashInfo = { recentBlockhash: null, lastFetch: new Date(0), transactionSignatures: [], simulatedSignatures: [], }; url.protocol = useHttps ? 'wss:' : 'ws:'; url.host = ''; // Only shift the port by +1 as a convention for ws(s) only if given endpoint // is explictly specifying the endpoint port (HTTP-based RPC), assuming // we're directly trying to connect to solana-validator's ws listening port. // When the endpoint omits the port, we're connecting to the protocol // default ports: http(80) or https(443) and it's assumed we're behind a reverse // proxy which manages WebSocket upgrade and backend port redirection. if (url.port !== null) { url.port = String(Number(url.port) + 1); } this._rpcWebSocket = new RpcWebSocketClient(urlFormat(url), { autoconnect: false, max_reconnects: Infinity, }); this._rpcWebSocket.on('open', this._wsOnOpen.bind(this)); this._rpcWebSocket.on('error', this._wsOnError.bind(this)); this._rpcWebSocket.on('close', this._wsOnClose.bind(this)); this._rpcWebSocket.on( 'accountNotification', this._wsOnAccountNotification.bind(this), ); this._rpcWebSocket.on( 'programNotification', this._wsOnProgramAccountNotification.bind(this), ); this._rpcWebSocket.on( 'slotNotification', this._wsOnSlotNotification.bind(this), ); this._rpcWebSocket.on( 'signatureNotification', this._wsOnSignatureNotification.bind(this), ); this._rpcWebSocket.on( 'rootNotification', this._wsOnRootNotification.bind(this), ); } /** * The default commitment used for requests */ get commitment(): ?Commitment { return this._commitment; } /** * Fetch the balance for the specified public key, return with context */ async getBalanceAndContext( publicKey: PublicKey, commitment: ?Commitment, ): Promise<RpcResponseAndContext<number>> { const args = this._buildArgs([publicKey.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getBalance', args); const res = create(unsafeRes, jsonRpcResultAndContext(number())); if (res.error) { throw new Error( 'failed to get balance for ' + publicKey.toBase58() + ': ' + res.error.message, ); } return res.result; } /** * Fetch the balance for the specified public key */ async getBalance( publicKey: PublicKey, commitment: ?Commitment, ): Promise<number> { return await this.getBalanceAndContext(publicKey, commitment) .then(x => x.value) .catch(e => { throw new Error( 'failed to get balance of account ' + publicKey.toBase58() + ': ' + e, ); }); } /** * Fetch the estimated production time of a block */ async getBlockTime(slot: number): Promise<number | null> { const unsafeRes = await this._rpcRequest('getBlockTime', [slot]); const res = create(unsafeRes, jsonRpcResult(nullable(number()))); if (res.error) { throw new Error( 'failed to get block time for slot ' + slot + ': ' + res.error.message, ); } return res.result; } /** * Fetch the lowest slot that the node has information about in its ledger. * This value may increase over time if the node is configured to purge older ledger data */ async getMinimumLedgerSlot(): Promise<number> { const unsafeRes = await this._rpcRequest('minimumLedgerSlot', []); const res = create(unsafeRes, jsonRpcResult(number())); if (res.error) { throw new Error( 'failed to get minimum ledger slot: ' + res.error.message, ); } return res.result; } /** * Fetch the slot of the lowest confirmed block that has not been purged from the ledger */ async getFirstAvailableBlock(): Promise<number> { const unsafeRes = await this._rpcRequest('getFirstAvailableBlock', []); const res = create(unsafeRes, SlotRpcResult); if (res.error) { throw new Error( 'failed to get first available block: ' + res.error.message, ); } return res.result; } /** * Fetch information about the current supply */ async getSupply( commitment: ?Commitment, ): Promise<RpcResponseAndContext<Supply>> { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getSupply', args); const res = create(unsafeRes, GetSupplyRpcResult); if (res.error) { throw new Error('failed to get supply: ' + res.error.message); } return res.result; } /** * Fetch the current supply of a token mint */ async getTokenSupply( tokenMintAddress: PublicKey, commitment: ?Commitment, ): Promise<RpcResponseAndContext<TokenAmount>> { const args = this._buildArgs([tokenMintAddress.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getTokenSupply', args); const res = create(unsafeRes, jsonRpcResultAndContext(TokenAmountResult)); if (res.error) { throw new Error('failed to get token supply: ' + res.error.message); } return res.result; } /** * Fetch the current balance of a token account */ async getTokenAccountBalance( tokenAddress: PublicKey, commitment: ?Commitment, ): Promise<RpcResponseAndContext<TokenAmount>> { const args = this._buildArgs([tokenAddress.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getTokenAccountBalance', args); const res = create(unsafeRes, jsonRpcResultAndContext(TokenAmountResult)); if (res.error) { throw new Error( 'failed to get token account balance: ' + res.error.message, ); } return res.result; } /** * Fetch all the token accounts owned by the specified account * * @return {Promise<RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>>>} */ async getTokenAccountsByOwner( ownerAddress: PublicKey, filter: TokenAccountsFilter, commitment: ?Commitment, ): Promise< RpcResponseAndContext< Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>, >, > { let _args = [ownerAddress.toBase58()]; if (filter.mint) { _args.push({mint: filter.mint.toBase58()}); } else { _args.push({programId: filter.programId.toBase58()}); } const args = this._buildArgs(_args, commitment, 'base64'); const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args); const res = create(unsafeRes, GetTokenAccountsByOwner); if (res.error) { throw new Error( 'failed to get token accounts owned by account ' + ownerAddress.toBase58() + ': ' + res.error.message, ); } return res.result; } /** * Fetch parsed token accounts owned by the specified account * * @return {Promise<RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo<ParsedAccountData>}>>>} */ async getParsedTokenAccountsByOwner( ownerAddress: PublicKey, filter: TokenAccountsFilter, commitment: ?Commitment, ): Promise< RpcResponseAndContext< Array<{pubkey: PublicKey, account: AccountInfo<ParsedAccountData>}>, >, > { let _args = [ownerAddress.toBase58()]; if (filter.mint) { _args.push({mint: filter.mint.toBase58()}); } else { _args.push({programId: filter.programId.toBase58()}); } const args = this._buildArgs(_args, commitment, 'jsonParsed'); const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args); const res = create(unsafeRes, GetParsedTokenAccountsByOwner); if (res.error) { throw new Error( 'failed to get token accounts owned by account ' + ownerAddress.toBase58() + ': ' + res.error.message, ); } return res.result; } /** * Fetch the 20 largest accounts with their current balances */ async getLargestAccounts( config: ?GetLargestAccountsConfig, ): Promise<RpcResponseAndContext<Array<AccountBalancePair>>> { const arg = { ...config, commitment: (config && config.commitment) || this.commitment, }; const args = arg.filter || arg.commitment ? [arg] : []; const unsafeRes = await this._rpcRequest('getLargestAccounts', args); const res = create(unsafeRes, GetLargestAccoun