serum-dex-ui/src/utils/tokens.tsx

184 lines
4.9 KiB
TypeScript

import * as BufferLayout from 'buffer-layout';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
import { TokenAccount } from './types';
import { TOKEN_MINTS } from '@project-serum/serum';
import { useAllMarkets, useCustomMarkets, useTokenAccounts } from './markets';
import { getMultipleSolanaAccounts } from './send';
import { useConnection } from './connection';
import { useAsyncData } from './fetch-loop';
import tuple from 'immutable-tuple';
import BN from 'bn.js';
import { useMemo } from 'react';
export const ACCOUNT_LAYOUT = BufferLayout.struct([
BufferLayout.blob(32, 'mint'),
BufferLayout.blob(32, 'owner'),
BufferLayout.nu64('amount'),
BufferLayout.blob(93),
]);
export const MINT_LAYOUT = BufferLayout.struct([
BufferLayout.blob(36),
BufferLayout.blob(8, 'supply'),
BufferLayout.u8('decimals'),
BufferLayout.u8('initialized'),
BufferLayout.blob(36),
]);
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 interface MintInfo {
decimals: number;
initialized: boolean;
supply: BN;
}
export function parseTokenMintData(data): MintInfo {
let { decimals, initialized, supply } = MINT_LAYOUT.decode(data);
return {
decimals,
initialized: !!initialized,
supply: new BN(supply, 10, 'le'),
};
}
export function getOwnedAccountsFilters(publicKey: PublicKey) {
return [
{
memcmp: {
offset: ACCOUNT_LAYOUT.offsetOf('owner'),
bytes: publicKey.toBase58(),
},
},
{
dataSize: ACCOUNT_LAYOUT.span,
},
];
}
export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
);
export async function getOwnedTokenAccounts(
connection: Connection,
publicKey: PublicKey,
): Promise<Array<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }>> {
let filters = getOwnedAccountsFilters(publicKey);
let resp = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, {
filters,
});
return resp.map(
({ pubkey, account: { data, executable, owner, lamports } }) => ({
publicKey: new PublicKey(pubkey),
accountInfo: {
data,
executable,
owner: new PublicKey(owner),
lamports,
},
}),
);
}
export async function getTokenAccountInfo(
connection: Connection,
ownerAddress: PublicKey,
) {
let [splAccounts, account] = await Promise.all([
getOwnedTokenAccounts(connection, ownerAddress),
connection.getAccountInfo(ownerAddress),
]);
const parsedSplAccounts: TokenAccount[] = splAccounts.map(
({ publicKey, accountInfo }) => {
return {
pubkey: publicKey,
account: accountInfo,
effectiveMint: parseTokenAccountData(accountInfo.data).mint,
};
},
);
return parsedSplAccounts.concat({
pubkey: ownerAddress,
account,
effectiveMint: WRAPPED_SOL_MINT,
});
}
// todo: use this to map custom mints to custom tickers. Add functionality once custom markets store mints
export function useMintToTickers(): { [mint: string]: string } {
const { customMarkets } = useCustomMarkets();
return useMemo(() => {
return Object.fromEntries(
TOKEN_MINTS.map((mint) => [mint.address.toBase58(), mint.name]),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [customMarkets.length]);
}
const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
// todo: move this to using mints stored in static market infos once custom markets support that.
export function useMintInfos(): [
(
| {
[mintAddress: string]: {
decimals: number;
initialized: boolean;
} | null;
}
| null
| undefined
),
boolean,
] {
const connection = useConnection();
const [tokenAccounts] = useTokenAccounts();
const [allMarkets] = useAllMarkets();
const allMints = (tokenAccounts || [])
.map((account) => account.effectiveMint)
.concat(
(allMarkets || []).map((marketInfo) => marketInfo.market.baseMintAddress),
)
.concat(
(allMarkets || []).map(
(marketInfo) => marketInfo.market.quoteMintAddress,
),
);
const uniqueMints = [...new Set(allMints.map((mint) => mint.toBase58()))].map(
(stringMint) => new PublicKey(stringMint),
);
const getAllMintInfo = async () => {
const mintInfos = await getMultipleSolanaAccounts(connection, uniqueMints);
return Object.fromEntries(
Object.entries(mintInfos.value).map(([key, accountInfo]) => [
key,
accountInfo && parseTokenMintData(accountInfo.data),
]),
);
};
return useAsyncData(
getAllMintInfo,
tuple(
'getAllMintInfo',
connection,
(tokenAccounts || []).length,
(allMarkets || []).length,
),
{ refreshInterval: _VERY_SLOW_REFRESH_INTERVAL },
);
}