import { Account, AccountInfo, Commitment, Connection, PublicKey, RpcResponseAndContext, SimulatedTransactionResponse, SystemProgram, Transaction, TransactionConfirmationStatus, TransactionInstruction, TransactionSignature, } from '@solana/web3.js'; import BN from 'bn.js'; import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'; import { parseBaseData, parsePriceData, AccountType, } from '@pythnetwork/client'; import { bits, blob, struct, u8, u32, nu64 } from 'buffer-layout'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { AccountLayout } from './layout'; import { accountFlagsLayout, publicKeyLayout, u128, u64, zeros, } from '@project-serum/serum/lib/layout'; import { Aggregator } from './schema'; import Big from 'big.js'; import SwitchboardProgram from '@switchboard-xyz/sbv2-lite'; export const zeroKey = new PublicKey(new Uint8Array(32)); export async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } export function getDecimalCount(value: number): number { if ( !isNaN(value) && Math.floor(value) !== value && value.toString().includes('.') ) return value.toString().split('.')[1].length || 0; if ( !isNaN(value) && Math.floor(value) !== value && value.toString().includes('e') ) return parseInt(value.toString().split('e-')[1] || '0'); return 0; } export async function simulateTransaction( connection: Connection, transaction: Transaction, commitment: Commitment, ): Promise> { // @ts-ignore transaction.recentBlockhash = await connection._recentBlockhash( // @ts-ignore connection._disableBlockhashCaching, ); const signData = transaction.serializeMessage(); // @ts-ignore const wireTransaction = transaction._serialize(signData); const encodedTransaction = wireTransaction.toString('base64'); const config: any = { encoding: 'base64', commitment }; const args = [encodedTransaction, config]; // @ts-ignore const res = await connection._rpcRequest('simulateTransaction', args); if (res.error) { throw new Error('failed to simulate transaction: ' + res.error.message); } return res.result; } export async function awaitTransactionSignatureConfirmation( txid: TransactionSignature, timeout: number, connection: Connection, confirmLevel: TransactionConfirmationStatus, ) { let done = false; const confirmLevels: (TransactionConfirmationStatus | undefined)[] = [ 'finalized', ]; if (confirmLevel === 'confirmed') { confirmLevels.push('confirmed'); } else if (confirmLevel === 'processed') { confirmLevels.push('confirmed'); confirmLevels.push('processed'); } const result = await new Promise((resolve, reject) => { (async () => { setTimeout(() => { if (done) { return; } done = true; console.log('Timed out for txid', txid); reject({ timeout: true }); }, timeout); try { connection.onSignature( txid, (result) => { // console.log('WS confirmed', txid, result); done = true; if (result.err) { reject(result.err); } else { resolve(result); } }, 'singleGossip', ); // console.log('Set up WS connection', txid); } catch (e) { done = true; console.log('WS error in setup', txid, e); } while (!done) { // eslint-disable-next-line no-loop-func (async () => { try { const signatureStatuses = await connection.getSignatureStatuses([ txid, ]); const result = signatureStatuses && signatureStatuses.value[0]; if (!done) { if (!result) { // console.log('REST null result for', txid, result); } else if (result.err) { console.log('REST error for', txid, result); done = true; reject(result.err); } else if ( !( result.confirmations || confirmLevels.includes(result.confirmationStatus) ) ) { console.log('REST not confirmed', txid, result); } else { console.log('REST confirmed', txid, result); done = true; resolve(result); } } } catch (e) { if (!done) { console.log('REST connection error: txid', txid, e); } } })(); await sleep(300); } })(); }); done = true; return result; } export async function createAccountInstruction( connection: Connection, payer: PublicKey, space: number, owner: PublicKey, lamports?: number, ): Promise<{ account: Account; instruction: TransactionInstruction }> { const account = new Account(); const instruction = SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: account.publicKey, lamports: lamports ? lamports : await connection.getMinimumBalanceForRentExemption(space), space, programId: owner, }); return { account, instruction }; } const MINT_LAYOUT = struct([blob(44), u8('decimals'), blob(37)]); export async function getMintDecimals( connection: Connection, mint: PublicKey, ): Promise { if (mint.equals(WRAPPED_SOL_MINT)) { return 9; } const { data } = throwIfNull( await connection.getAccountInfo(mint), 'mint not found', ); const { decimals } = MINT_LAYOUT.decode(data); return decimals; } function throwIfNull(value: T | null, message = 'account not found'): T { if (value === null) { throw new Error(message); } return value; } export function uiToNative(amount: number, decimals: number): BN { return new BN(Math.round(amount * Math.pow(10, decimals))); } export function nativeToUi(amount: number, decimals: number): number { return amount / Math.pow(10, decimals); } export async function getFilteredProgramAccounts( connection: Connection, programId: PublicKey, filters, ): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo }[]> { // @ts-ignore const resp = await connection._rpcRequest('getProgramAccounts', [ programId.toBase58(), { commitment: connection.commitment, filters, encoding: 'base64', }, ]); if (resp.error) { throw new Error(resp.error.message); } return resp.result.map( ({ pubkey, account: { data, executable, owner, lamports } }) => ({ publicKey: new PublicKey(pubkey), accountInfo: { data: Buffer.from(data[0], 'base64'), executable, owner: new PublicKey(owner), lamports, }, }), ); } export async function promiseUndef(): Promise { return undefined; } export const getUnixTs = () => { return new Date().getTime() / 1000; }; export const ACCOUNT_LAYOUT = struct([ blob(32, 'mint'), blob(32, 'owner'), nu64('amount'), blob(93), ]); export function parseTokenAccountData(data: Buffer): { mint: PublicKey; owner: PublicKey; amount: number; } { let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data); return { mint: new PublicKey(mint), owner: new PublicKey(owner), amount, }; } export function parseTokenAccount(data: Buffer): { mint: PublicKey; owner: PublicKey; amount: BN; } { const decoded = AccountLayout.decode(data); return { mint: decoded.mint, owner: decoded.owner, amount: decoded.amount, }; } export async function getMultipleAccounts( connection: Connection, publicKeys: PublicKey[], commitment?: Commitment, ): Promise< { publicKey: PublicKey; context: { slot: number }; accountInfo: AccountInfo; }[] > { const len = publicKeys.length; if (len > 100) { const mid = Math.floor(publicKeys.length / 2); return Promise.all([ getMultipleAccounts(connection, publicKeys.slice(0, mid), commitment), getMultipleAccounts(connection, publicKeys.slice(mid, len), commitment), ]).then((a) => a[0].concat(a[1])); } const publicKeyStrs = publicKeys.map((pk) => pk.toBase58()); // load connection commitment as a default commitment ||= connection.commitment; const args = commitment ? [publicKeyStrs, { commitment }] : [publicKeyStrs]; // @ts-ignore const resp = await connection._rpcRequest('getMultipleAccounts', args); if (resp.error) { throw new Error(resp.error.message); } return resp.result.value.map( ({ data, executable, lamports, owner }, i: number) => ({ publicKey: publicKeys[i], context: resp.result.context, accountInfo: { data: Buffer.from(data[0], 'base64'), executable, owner: new PublicKey(owner), lamports, }, }), ); } export async function findLargestTokenAccountForOwner( connection: Connection, owner: PublicKey, mint: PublicKey, ): Promise<{ publicKey: PublicKey; tokenAccount: { mint: PublicKey; owner: PublicKey; amount: number }; }> { const response = await connection.getTokenAccountsByOwner( owner, { mint, programId: TOKEN_PROGRAM_ID }, connection.commitment, ); let max = -1; let maxTokenAccount: null | { mint: PublicKey; owner: PublicKey; amount: number; } = null; let maxPubkey: null | PublicKey = null; for (const { pubkey, account } of response.value) { const tokenAccount = parseTokenAccountData(account.data); if (tokenAccount.amount > max) { maxTokenAccount = tokenAccount; max = tokenAccount.amount; maxPubkey = pubkey; } } if (maxPubkey && maxTokenAccount) { return { publicKey: maxPubkey, tokenAccount: maxTokenAccount }; } else { throw new Error('No accounts for this token'); } } const EVENT_QUEUE_HEADER = struct([ blob(5), accountFlagsLayout('accountFlags'), u32('head'), zeros(4), u32('count'), zeros(4), u32('seqNum'), zeros(4), ]); const EVENT_FLAGS = bits(u8(), false, 'eventFlags'); EVENT_FLAGS.addBoolean('fill'); EVENT_FLAGS.addBoolean('out'); EVENT_FLAGS.addBoolean('bid'); EVENT_FLAGS.addBoolean('maker'); const EVENT = struct([ EVENT_FLAGS, u8('openOrdersSlot'), u8('feeTier'), blob(5), u64('nativeQuantityReleased'), // Amount the user received u64('nativeQuantityPaid'), // Amount the user paid u64('nativeFeeOrRebate'), u128('orderId'), publicKeyLayout('openOrders'), u64('clientOrderId'), ]); export function decodeRecentEvents(buffer: Buffer, lastSeenSeqNum?: number) { const header = EVENT_QUEUE_HEADER.decode(buffer); const nodes: any[] = []; if (lastSeenSeqNum !== undefined) { const allocLen = Math.floor( (buffer.length - EVENT_QUEUE_HEADER.span) / EVENT.span, ); const newEventsCount = header.seqNum - lastSeenSeqNum; for (let i = newEventsCount; i > 0; --i) { const nodeIndex = (header.head + header.count + allocLen - i) % allocLen; const decodedItem = EVENT.decode( buffer, EVENT_QUEUE_HEADER.span + nodeIndex * EVENT.span, ); nodes.push(decodedItem); } } return { header, nodes }; } const PYTH_MAGIC = Buffer.from([0xa1, 0xb2, 0xc3, 0xd4]); export function switchboardDecimalToBig(sbDecimal: { mantissa: BN; scale: number; }): Big { const mantissa = new Big(sbDecimal.mantissa.toString()); const scale = sbDecimal.scale; const oldDp = Big.DP; Big.DP = 20; const result: Big = mantissa.div(new Big(10).pow(scale)); Big.DP = oldDp; return result; } let sbv2MainnetProgram; export async function parseSwitchboardOracleV2( accountInfo: AccountInfo, connection: Connection, ): Promise<{ price: number; lastUpdatedSlot: number; uiDeviation: number }> { if (!sbv2MainnetProgram) { sbv2MainnetProgram = await SwitchboardProgram.loadMainnet(connection); } const price = sbv2MainnetProgram .decodeLatestAggregatorValue(accountInfo)! .toNumber(); const lastUpdatedSlot = sbv2MainnetProgram .decodeAggregator(accountInfo) .latestConfirmedRound!.roundOpenSlot!.toNumber(); const stdDeviation = switchboardDecimalToBig( sbv2MainnetProgram.decodeAggregator(accountInfo).latestConfirmedRound .stdDeviation, ); if (!price || !lastUpdatedSlot) throw new Error('Unable to parse Switchboard Oracle V2'); return { price, lastUpdatedSlot, uiDeviation: stdDeviation.toNumber() }; } export async function getOraclePrice( connection: Connection, oracle: PublicKey, ): Promise { const info = await connection.getAccountInfo(oracle); if (!info || !info.data.length) { throw new Error('account does not exist'); } if (info.data.length == 3851) { const { price } = await parseSwitchboardOracleV2(info, connection); return price; } else { const pythBase = parseBaseData(info.data); if (pythBase?.type == AccountType.Price) { const price = parsePriceData(info.data); return price.aggregate.price; } else { const agg = Aggregator.deserialize(info.data); return agg.answer.median.toNumber() / Math.pow(10, agg.config.decimals); } } }