mango-client-ts/src/utils.ts

404 lines
11 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 { 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';
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 | null)[] = ['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 };
}