Update token balances via websocket

This commit is contained in:
armaniferrante 2021-05-17 19:09:37 -07:00
parent 7ab35ffa91
commit 2b361b2459
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
2 changed files with 68 additions and 29 deletions

View File

@ -1,4 +1,5 @@
import React, { useContext, useState, useEffect } from "react"; import React, { useContext, useState, useEffect } from "react";
import * as assert from "assert";
import { useAsync } from "react-async-hook"; import { useAsync } from "react-async-hook";
import { Provider } from "@project-serum/anchor"; import { Provider } from "@project-serum/anchor";
import { PublicKey, Account } from "@solana/web3.js"; import { PublicKey, Account } from "@solana/web3.js";
@ -8,35 +9,37 @@ import {
Token, Token,
TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID,
} from "@solana/spl-token"; } from "@solana/spl-token";
import { getOwnedTokenAccounts } from "../utils/tokens"; import { getOwnedTokenAccounts, parseTokenAccountData } from "../utils/tokens";
export type TokenContext = { export type TokenContext = {
provider: Provider; provider: Provider;
ownedTokenAccounts:
| { publicKey: PublicKey; account: TokenAccount }[]
| undefined;
}; };
const _TokenContext = React.createContext<TokenContext | null>(null); const _TokenContext = React.createContext<TokenContext | null>(null);
export function TokenContextProvider(props: any) { export function TokenContextProvider(props: any) {
const provider = props.provider; const provider = props.provider;
const [ownedTokenAccounts, setOwnedTokenAccounts] = useState(undefined); const [, setRefresh] = useState(0);
// Fetch all the owned token accounts for the wallet. // Fetch all the owned token accounts for the wallet.
useEffect(() => { useEffect(() => {
if (!provider.wallet.publicKey) { if (!provider.wallet.publicKey) {
setOwnedTokenAccounts(undefined); _OWNED_TOKEN_ACCOUNTS_CACHE.length = 0;
setRefresh((r) => r + 1);
return; return;
} }
getOwnedTokenAccounts(provider.connection, provider.wallet.publicKey).then( getOwnedTokenAccounts(provider.connection, provider.wallet.publicKey).then(
setOwnedTokenAccounts (accs) => {
if (accs) {
_OWNED_TOKEN_ACCOUNTS_CACHE.push(...accs);
setRefresh((r) => r + 1);
}
}
); );
}, [provider.wallet.publicKey, provider.connection]); }, [provider.wallet.publicKey, provider.connection]);
return ( return (
<_TokenContext.Provider <_TokenContext.Provider
value={{ value={{
ownedTokenAccounts,
provider, provider,
}} }}
> >
@ -58,21 +61,12 @@ function useTokenContext() {
export function useOwnedTokenAccount( export function useOwnedTokenAccount(
mint?: PublicKey mint?: PublicKey
): { publicKey: PublicKey; account: TokenAccount } | null | undefined { ): { publicKey: PublicKey; account: TokenAccount } | null | undefined {
const ctx = useTokenContext(); const { provider } = useTokenContext();
if (mint === undefined) { const [, setRefresh] = useState(0);
return mint; const tokenAccounts = _OWNED_TOKEN_ACCOUNTS_CACHE.filter(
} (account) => mint && account.account.mint.equals(mint)
if (ctx.ownedTokenAccounts === undefined) {
return undefined;
}
const tokenAccounts = ctx.ownedTokenAccounts.filter((account) =>
account.account.mint.equals(mint)
); );
if (tokenAccounts.length === 0) {
return null;
}
// Take the account with the most tokens in it. // Take the account with the most tokens in it.
tokenAccounts.sort((a, b) => tokenAccounts.sort((a, b) =>
a.account.amount < b.account.amount a.account.amount < b.account.amount
@ -81,11 +75,43 @@ export function useOwnedTokenAccount(
? 1 ? 1
: 0 : 0
); );
return tokenAccounts[0];
}
// Cache storing all previously fetched mint infos. const tokenAccount = tokenAccounts[0];
const _MINT_CACHE = new Map<string, MintInfo>();
// Stream updates when the balance changes.
useEffect(() => {
let listener: number;
if (tokenAccount) {
listener = provider.connection.onAccountChange(
tokenAccount.publicKey,
(info) => {
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);
}
}
);
}
return () => {
if (listener) {
provider.connection.removeAccountChangeListener(listener);
}
};
}, [provider.connection, tokenAccount]);
if (mint === undefined) {
return undefined;
}
if (tokenAccounts.length === 0) {
return null;
}
return tokenAccount;
}
export function useMint(mint?: PublicKey): MintInfo | undefined | null { export function useMint(mint?: PublicKey): MintInfo | undefined | null {
const { provider } = useTokenContext(); const { provider } = useTokenContext();
@ -114,3 +140,12 @@ export function useMint(mint?: PublicKey): MintInfo | undefined | null {
} }
return undefined; return undefined;
} }
// 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.
const _MINT_CACHE = new Map<string, MintInfo>();

View File

@ -3,7 +3,10 @@
import * as BufferLayout from "buffer-layout"; import * as BufferLayout from "buffer-layout";
import { BN } from "@project-serum/anchor"; import { BN } from "@project-serum/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; import {
TOKEN_PROGRAM_ID,
AccountInfo as TokenAccount,
} from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js"; import { Connection, PublicKey } from "@solana/web3.js";
import * as bs58 from "bs58"; import * as bs58 from "bs58";
@ -60,22 +63,23 @@ export async function getOwnedTokenAccounts(
}); });
} }
export const ACCOUNT_LAYOUT = BufferLayout.struct([ const ACCOUNT_LAYOUT = BufferLayout.struct([
BufferLayout.blob(32, "mint"), BufferLayout.blob(32, "mint"),
BufferLayout.blob(32, "owner"), BufferLayout.blob(32, "owner"),
BufferLayout.nu64("amount"), BufferLayout.nu64("amount"),
BufferLayout.blob(93), BufferLayout.blob(93),
]); ]);
export const MINT_LAYOUT = BufferLayout.struct([ const MINT_LAYOUT = BufferLayout.struct([
BufferLayout.blob(44), BufferLayout.blob(44),
BufferLayout.u8("decimals"), BufferLayout.u8("decimals"),
BufferLayout.blob(37), BufferLayout.blob(37),
]); ]);
export function parseTokenAccountData(data: Buffer) { export function parseTokenAccountData(data: Buffer): TokenAccount {
// @ts-ignore // @ts-ignore
let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data); let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
// @ts-ignore
return { return {
mint: new PublicKey(mint), mint: new PublicKey(mint),
owner: new PublicKey(owner), owner: new PublicKey(owner),