mango-client-ts/src/utils.ts

494 lines
13 KiB
TypeScript

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<RpcResponseAndContext<SimulatedTransactionResponse>> {
// @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<number> {
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<T>(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<Buffer> }[]> {
// @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<undefined> {
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<Buffer>;
}[]
> {
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<Buffer>,
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<number> {
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);
}
}
}