208 lines
5.6 KiB
TypeScript
208 lines
5.6 KiB
TypeScript
import React, { useContext, useState, useEffect } from "react";
|
|
import * as assert from "assert";
|
|
import { useAsync } from "react-async-hook";
|
|
import { Provider, BN } from "@project-serum/anchor";
|
|
import { PublicKey, Account } from "@solana/web3.js";
|
|
import {
|
|
MintInfo,
|
|
AccountInfo as TokenAccount,
|
|
Token,
|
|
TOKEN_PROGRAM_ID,
|
|
} from "@solana/spl-token";
|
|
import {
|
|
getOwnedAssociatedTokenAccounts,
|
|
parseTokenAccountData,
|
|
} from "../utils/tokens";
|
|
import { SOL_MINT } from "../utils/pubkeys";
|
|
|
|
export type TokenContext = {
|
|
provider: Provider;
|
|
};
|
|
const _TokenContext = React.createContext<TokenContext | null>(null);
|
|
|
|
export function TokenContextProvider(props: any) {
|
|
const provider = props.provider;
|
|
const [, setRefresh] = useState(0);
|
|
|
|
// Fetch all the owned token accounts for the wallet.
|
|
useEffect(() => {
|
|
if (!provider.wallet.publicKey) {
|
|
_OWNED_TOKEN_ACCOUNTS_CACHE.length = 0;
|
|
setRefresh((r) => r + 1);
|
|
return;
|
|
}
|
|
// Fetch SPL tokens.
|
|
getOwnedAssociatedTokenAccounts(
|
|
provider.connection,
|
|
provider.wallet.publicKey
|
|
).then((accs) => {
|
|
if (accs) {
|
|
// @ts-ignore
|
|
_OWNED_TOKEN_ACCOUNTS_CACHE.push(...accs);
|
|
setRefresh((r) => r + 1);
|
|
}
|
|
});
|
|
// Fetch SOL balance.
|
|
provider.connection
|
|
.getAccountInfo(provider.wallet.publicKey)
|
|
.then((acc: { lamports: number }) => {
|
|
if (acc) {
|
|
_OWNED_TOKEN_ACCOUNTS_CACHE.push({
|
|
publicKey: provider.wallet.publicKey,
|
|
// @ts-ignore
|
|
account: {
|
|
amount: new BN(acc.lamports),
|
|
mint: SOL_MINT,
|
|
},
|
|
});
|
|
setRefresh((r) => r + 1);
|
|
}
|
|
});
|
|
}, [provider.wallet.publicKey, provider.connection]);
|
|
|
|
return (
|
|
<_TokenContext.Provider
|
|
value={{
|
|
provider,
|
|
}}
|
|
>
|
|
{props.children}
|
|
</_TokenContext.Provider>
|
|
);
|
|
}
|
|
|
|
function useTokenContext() {
|
|
const ctx = useContext(_TokenContext);
|
|
if (ctx === null) {
|
|
throw new Error("Context not available");
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
// Null => none exists.
|
|
// Undefined => loading.
|
|
export function useOwnedTokenAccount(
|
|
mint?: PublicKey
|
|
): { publicKey: PublicKey; account: TokenAccount } | null | undefined {
|
|
const { provider } = useTokenContext();
|
|
const [, setRefresh] = useState(0);
|
|
const tokenAccounts = _OWNED_TOKEN_ACCOUNTS_CACHE.filter(
|
|
(account) => mint && account.account.mint.equals(mint)
|
|
);
|
|
|
|
// Take the account with the most tokens in it.
|
|
tokenAccounts.sort((a, b) =>
|
|
a.account.amount > b.account.amount
|
|
? -1
|
|
: a.account.amount < b.account.amount
|
|
? 1
|
|
: 0
|
|
);
|
|
|
|
let tokenAccount = tokenAccounts[0];
|
|
const isSol = mint?.equals(SOL_MINT);
|
|
|
|
// Stream updates when the balance changes.
|
|
useEffect(() => {
|
|
let listener: number;
|
|
// SOL is special cased since it's not an SPL token.
|
|
if (tokenAccount && isSol) {
|
|
listener = provider.connection.onAccountChange(
|
|
provider.wallet.publicKey,
|
|
(info: { lamports: number }) => {
|
|
const token = {
|
|
amount: new BN(info.lamports),
|
|
mint: SOL_MINT,
|
|
} as TokenAccount;
|
|
if (token.amount !== tokenAccount.account.amount) {
|
|
const index = _OWNED_TOKEN_ACCOUNTS_CACHE.indexOf(tokenAccount);
|
|
assert.ok(index >= 0);
|
|
_OWNED_TOKEN_ACCOUNTS_CACHE[index].account = token;
|
|
setRefresh((r) => r + 1);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
// SPL tokens.
|
|
else if (tokenAccount) {
|
|
listener = provider.connection.onAccountChange(
|
|
tokenAccount.publicKey,
|
|
(info) => {
|
|
if (info.data.length !== 0) {
|
|
try {
|
|
const token = parseTokenAccountData(info.data);
|
|
if (token.amount !== tokenAccount.account.amount) {
|
|
const index = _OWNED_TOKEN_ACCOUNTS_CACHE.indexOf(tokenAccount);
|
|
assert.ok(index >= 0);
|
|
_OWNED_TOKEN_ACCOUNTS_CACHE[index].account = token;
|
|
setRefresh((r) => r + 1);
|
|
}
|
|
} catch (error) {
|
|
console.log("Failed to decode token AccountInfo");
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
return () => {
|
|
if (listener) {
|
|
provider.connection.removeAccountChangeListener(listener);
|
|
}
|
|
};
|
|
}, [provider.connection, tokenAccount]);
|
|
|
|
if (mint === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!isSol && tokenAccounts.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return tokenAccount;
|
|
}
|
|
|
|
export function useMint(mint?: PublicKey): MintInfo | undefined | null {
|
|
const { provider } = useTokenContext();
|
|
// Lazy load the mint account if needeed.
|
|
const asyncMintInfo = useAsync(async () => {
|
|
if (!mint) {
|
|
return undefined;
|
|
}
|
|
if (_MINT_CACHE.get(mint.toString())) {
|
|
return _MINT_CACHE.get(mint.toString());
|
|
}
|
|
|
|
const mintClient = new Token(
|
|
provider.connection,
|
|
mint,
|
|
TOKEN_PROGRAM_ID,
|
|
new Account()
|
|
);
|
|
const mintInfo = mintClient.getMintInfo();
|
|
_MINT_CACHE.set(mint.toString(), mintInfo);
|
|
return mintInfo;
|
|
}, [provider.connection, mint]);
|
|
|
|
if (asyncMintInfo.result) {
|
|
return asyncMintInfo.result;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function setMintCache(pk: PublicKey, account: MintInfo) {
|
|
_MINT_CACHE.set(pk.toString(), new Promise((resolve) => resolve(account)));
|
|
}
|
|
|
|
// Cache storing all token accounts for the connected wallet provider.
|
|
const _OWNED_TOKEN_ACCOUNTS_CACHE: Array<{
|
|
publicKey: PublicKey;
|
|
account: TokenAccount;
|
|
}> = [];
|
|
|
|
// Cache storing all previously fetched mint infos.
|
|
// @ts-ignore
|
|
const _MINT_CACHE = new Map<string, Promise<MintInfo>>([
|
|
[SOL_MINT.toString(), { decimals: 9 }],
|
|
]);
|