This repository has been archived by the owner on Jan 13, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Show token transaction history in explorer (#11473)
- Loading branch information
Showing
7 changed files
with
319 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
import React from "react"; | ||
import { | ||
PublicKey, | ||
ConfirmedSignatureInfo, | ||
ParsedInstruction, | ||
} from "@solana/web3.js"; | ||
import { FetchStatus } from "providers/accounts"; | ||
import { | ||
useAccountHistories, | ||
useFetchAccountHistory, | ||
} from "providers/accounts/history"; | ||
import { | ||
useAccountOwnedTokens, | ||
TokenAccountData, | ||
} from "providers/accounts/tokens"; | ||
import { ErrorCard } from "components/common/ErrorCard"; | ||
import { LoadingCard } from "components/common/LoadingCard"; | ||
import { Signature } from "components/common/Signature"; | ||
import { Address } from "components/common/Address"; | ||
import { useTransactionDetails } from "providers/transactions"; | ||
import { useFetchTransactionDetails } from "providers/transactions/details"; | ||
import { coerce } from "superstruct"; | ||
import { ParsedInfo } from "validators"; | ||
import { | ||
TokenInstructionType, | ||
IX_TITLES, | ||
} from "components/instruction/token/types"; | ||
|
||
export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) { | ||
const address = pubkey.toBase58(); | ||
const ownedTokens = useAccountOwnedTokens(address); | ||
|
||
if (ownedTokens === undefined) { | ||
return null; | ||
} | ||
|
||
const { tokens } = ownedTokens; | ||
if (tokens === undefined || tokens.length === 0) return null; | ||
|
||
return <TokenHistoryTable tokens={tokens} />; | ||
} | ||
|
||
function TokenHistoryTable({ tokens }: { tokens: TokenAccountData[] }) { | ||
const accountHistories = useAccountHistories(); | ||
const fetchAccountHistory = useFetchAccountHistory(); | ||
|
||
const fetchHistories = (refresh?: boolean) => { | ||
tokens.forEach((token) => { | ||
fetchAccountHistory(token.pubkey, refresh); | ||
}); | ||
}; | ||
|
||
// Fetch histories on load | ||
React.useEffect(() => { | ||
tokens.forEach((token) => { | ||
const address = token.pubkey.toBase58(); | ||
if (!accountHistories[address]) { | ||
fetchAccountHistory(token.pubkey, true); | ||
} | ||
}); | ||
}, []); // eslint-disable-line react-hooks/exhaustive-deps | ||
|
||
const fetchedFullHistory = tokens.every((token) => { | ||
const history = accountHistories[token.pubkey.toBase58()]; | ||
return history && history.foundOldest === true; | ||
}); | ||
|
||
const fetching = tokens.some((token) => { | ||
const history = accountHistories[token.pubkey.toBase58()]; | ||
return history && history.status === FetchStatus.Fetching; | ||
}); | ||
|
||
const failed = tokens.some((token) => { | ||
const history = accountHistories[token.pubkey.toBase58()]; | ||
return history && history.status === FetchStatus.FetchFailed; | ||
}); | ||
|
||
const mintAndTxs = tokens | ||
.map((token) => ({ | ||
mint: token.mint, | ||
history: accountHistories[token.pubkey.toBase58()], | ||
})) | ||
.filter(({ history }) => { | ||
return ( | ||
history !== undefined && history.fetched && history.fetched.length > 0 | ||
); | ||
}) | ||
.flatMap(({ mint, history }) => | ||
(history.fetched as ConfirmedSignatureInfo[]).map((tx) => ({ mint, tx })) | ||
); | ||
|
||
if (mintAndTxs.length === 0) { | ||
if (fetching) { | ||
return <LoadingCard message="Loading history" />; | ||
} else if (failed) { | ||
return ( | ||
<ErrorCard | ||
retry={() => fetchHistories(true)} | ||
text="Failed to fetch transaction history" | ||
/> | ||
); | ||
} | ||
return ( | ||
<ErrorCard | ||
retry={() => fetchHistories(true)} | ||
retryText="Try again" | ||
text="No transaction history found" | ||
/> | ||
); | ||
} | ||
|
||
mintAndTxs.sort((a, b) => { | ||
if (a.tx.slot > b.tx.slot) return -1; | ||
if (a.tx.slot < b.tx.slot) return 1; | ||
return 0; | ||
}); | ||
|
||
return ( | ||
<div className="card"> | ||
<div className="card-header align-items-center"> | ||
<h3 className="card-header-title">Token History</h3> | ||
<button | ||
className="btn btn-white btn-sm" | ||
disabled={fetching} | ||
onClick={() => fetchHistories(true)} | ||
> | ||
{fetching ? ( | ||
<> | ||
<span className="spinner-grow spinner-grow-sm mr-2"></span> | ||
Loading | ||
</> | ||
) : ( | ||
<> | ||
<span className="fe fe-refresh-cw mr-2"></span> | ||
Refresh | ||
</> | ||
)} | ||
</button> | ||
</div> | ||
|
||
<div className="table-responsive mb-0"> | ||
<table className="table table-sm table-nowrap card-table"> | ||
<thead> | ||
<tr> | ||
<th className="text-muted w-1">Slot</th> | ||
<th className="text-muted">Result</th> | ||
<th className="text-muted">Token</th> | ||
<th className="text-muted">Instruction Type</th> | ||
<th className="text-muted">Transaction Signature</th> | ||
</tr> | ||
</thead> | ||
<tbody className="list"> | ||
{mintAndTxs.map(({ mint, tx }) => ( | ||
<TokenTransactionRow key={tx.signature} mint={mint} tx={tx} /> | ||
))} | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
<div className="card-footer"> | ||
{fetchedFullHistory ? ( | ||
<div className="text-muted text-center">Fetched full history</div> | ||
) : ( | ||
<button | ||
className="btn btn-primary w-100" | ||
onClick={() => fetchHistories()} | ||
disabled={fetching} | ||
> | ||
{fetching ? ( | ||
<> | ||
<span className="spinner-grow spinner-grow-sm mr-2"></span> | ||
Loading | ||
</> | ||
) : ( | ||
"Load More" | ||
)} | ||
</button> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
function TokenTransactionRow({ | ||
mint, | ||
tx, | ||
}: { | ||
mint: PublicKey; | ||
tx: ConfirmedSignatureInfo; | ||
}) { | ||
const details = useTransactionDetails(tx.signature); | ||
const fetchDetails = useFetchTransactionDetails(); | ||
|
||
// Fetch details on load | ||
React.useEffect(() => { | ||
if (!details) fetchDetails(tx.signature); | ||
}, []); // eslint-disable-line react-hooks/exhaustive-deps | ||
|
||
const instructions = details?.transaction?.transaction.message.instructions; | ||
if (instructions) { | ||
const tokenInstructions = instructions.filter( | ||
(ix) => "parsed" in ix && ix.program === "spl-token" | ||
) as ParsedInstruction[]; | ||
if (tokenInstructions.length > 0) { | ||
return ( | ||
<> | ||
{tokenInstructions.map((ix, index) => { | ||
const parsed = coerce(ix.parsed, ParsedInfo); | ||
const { type: rawType } = parsed; | ||
const type = coerce(rawType, TokenInstructionType); | ||
const typeName = IX_TITLES[type]; | ||
|
||
let statusText; | ||
let statusClass; | ||
if (tx.err) { | ||
statusClass = "warning"; | ||
statusText = "Failed"; | ||
} else { | ||
statusClass = "success"; | ||
statusText = "Success"; | ||
} | ||
|
||
return ( | ||
<tr key={index}> | ||
<td className="w-1">{tx.slot}</td> | ||
|
||
<td> | ||
<span className={`badge badge-soft-${statusClass}`}> | ||
{statusText} | ||
</span> | ||
</td> | ||
|
||
<td> | ||
<Address pubkey={mint} link /> | ||
</td> | ||
|
||
<td>{typeName}</td> | ||
|
||
<td> | ||
<Signature signature={tx.signature} link /> | ||
</td> | ||
</tr> | ||
); | ||
})} | ||
</> | ||
); | ||
} | ||
} | ||
|
||
let statusText; | ||
let statusClass; | ||
if (tx.err) { | ||
statusClass = "warning"; | ||
statusText = "Failed"; | ||
} else { | ||
statusClass = "success"; | ||
statusText = "Success"; | ||
} | ||
|
||
return ( | ||
<tr key={tx.signature}> | ||
<td className="w-1">{tx.slot}</td> | ||
|
||
<td> | ||
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span> | ||
</td> | ||
|
||
<td> | ||
<span className="spinner-grow spinner-grow-sm mr-2"></span> | ||
Loading | ||
</td> | ||
|
||
<td> | ||
<Address pubkey={mint} link /> | ||
</td> | ||
|
||
<td> | ||
<Signature signature={tx.signature} link /> | ||
</td> | ||
</tr> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters