explorer: Parse Serum DEX and swap instructions for TokenHistory (#13320)
* map serum instructions on token history card * add token swap instruction parsing * refactor serum program and instruction data
This commit is contained in:
5485
explorer/package-lock.json
generated
5485
explorer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@project-serum/serum": "^0.13.9",
|
||||||
"@react-hook/debounce": "^3.0.0",
|
"@react-hook/debounce": "^3.0.0",
|
||||||
"@sentry/react": "^5.27.2",
|
"@sentry/react": "^5.27.2",
|
||||||
"@solana/web3.js": "^0.86.2",
|
"@solana/web3.js": "^0.86.2",
|
||||||
|
@ -31,6 +31,15 @@ import {
|
|||||||
IX_TITLES,
|
IX_TITLES,
|
||||||
} from "components/instruction/token/types";
|
} from "components/instruction/token/types";
|
||||||
import { reportError } from "utils/sentry";
|
import { reportError } from "utils/sentry";
|
||||||
|
import { intoTransactionInstruction } from "utils/tx";
|
||||||
|
import {
|
||||||
|
isTokenSwapInstruction,
|
||||||
|
parseTokenSwapInstructionTitle,
|
||||||
|
} from "components/instruction/token-swap/types";
|
||||||
|
import {
|
||||||
|
isSerumInstruction,
|
||||||
|
parseSerumInstructionTitle,
|
||||||
|
} from "components/instruction/serum/types";
|
||||||
|
|
||||||
export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
@ -301,13 +310,41 @@ const TokenTransactionRow = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const tokenInstructionNames = instructions
|
const tokenInstructionNames = instructions
|
||||||
.map((ix): string | undefined => {
|
.map((ix, index): string | undefined => {
|
||||||
|
let transactionInstruction;
|
||||||
|
if (details?.data?.transaction?.transaction) {
|
||||||
|
transactionInstruction = intoTransactionInstruction(
|
||||||
|
details.data.transaction.transaction,
|
||||||
|
index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ("parsed" in ix) {
|
if ("parsed" in ix) {
|
||||||
if (ix.program === "spl-token") {
|
if (ix.program === "spl-token") {
|
||||||
return instructionTypeName(ix, tx);
|
return instructionTypeName(ix, tx);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
transactionInstruction &&
|
||||||
|
isSerumInstruction(transactionInstruction)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return parseSerumInstructionTitle(transactionInstruction);
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, { signature: tx.signature });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
transactionInstruction &&
|
||||||
|
isTokenSwapInstruction(transactionInstruction)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return parseTokenSwapInstructionTitle(transactionInstruction);
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, { signature: tx.signature });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
ix.accounts.findIndex((account) =>
|
ix.accounts.findIndex((account) =>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
|
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
|
||||||
import { InstructionCard } from "./InstructionCard";
|
import { InstructionCard } from "./InstructionCard";
|
||||||
import { parseSerumInstructionTitle } from "utils/tx";
|
|
||||||
import { useCluster } from "providers/cluster";
|
import { useCluster } from "providers/cluster";
|
||||||
import { reportError } from "utils/sentry";
|
import { reportError } from "utils/sentry";
|
||||||
|
import { parseSerumInstructionTitle } from "./serum/types";
|
||||||
|
|
||||||
export function SerumDetailsCard({
|
export function SerumDetailsCard({
|
||||||
ix,
|
ix,
|
||||||
@ -33,7 +33,7 @@ export function SerumDetailsCard({
|
|||||||
ix={ix}
|
ix={ix}
|
||||||
index={index}
|
index={index}
|
||||||
result={result}
|
result={result}
|
||||||
title={title || "Unknown"}
|
title={`Serum: ${title || "Unknown"}`}
|
||||||
defaultRaw
|
defaultRaw
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
40
explorer/src/components/instruction/TokenSwapDetailsCard.tsx
Normal file
40
explorer/src/components/instruction/TokenSwapDetailsCard.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
|
||||||
|
import { InstructionCard } from "./InstructionCard";
|
||||||
|
import { useCluster } from "providers/cluster";
|
||||||
|
import { reportError } from "utils/sentry";
|
||||||
|
import { parseTokenSwapInstructionTitle } from "./token-swap/types";
|
||||||
|
|
||||||
|
export function TokenSwapDetailsCard({
|
||||||
|
ix,
|
||||||
|
index,
|
||||||
|
result,
|
||||||
|
signature,
|
||||||
|
}: {
|
||||||
|
ix: TransactionInstruction;
|
||||||
|
index: number;
|
||||||
|
result: SignatureResult;
|
||||||
|
signature: string;
|
||||||
|
}) {
|
||||||
|
const { url } = useCluster();
|
||||||
|
|
||||||
|
let title;
|
||||||
|
try {
|
||||||
|
title = parseTokenSwapInstructionTitle(ix);
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, {
|
||||||
|
url: url,
|
||||||
|
signature: signature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstructionCard
|
||||||
|
ix={ix}
|
||||||
|
index={index}
|
||||||
|
result={result}
|
||||||
|
title={`Token Swap: ${title || "Unknown"}`}
|
||||||
|
defaultRaw
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
38
explorer/src/components/instruction/serum/types.ts
Normal file
38
explorer/src/components/instruction/serum/types.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { MARKETS } from "@project-serum/serum";
|
||||||
|
import { TransactionInstruction } from "@solana/web3.js";
|
||||||
|
|
||||||
|
const SERUM_PROGRAM_ID = "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn";
|
||||||
|
|
||||||
|
export function isSerumInstruction(instruction: TransactionInstruction) {
|
||||||
|
return (
|
||||||
|
instruction.programId.toBase58() === SERUM_PROGRAM_ID ||
|
||||||
|
MARKETS.some(
|
||||||
|
(market) =>
|
||||||
|
market.programId && market.programId.equals(instruction.programId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERUM_CODE_LOOKUP: { [key: number]: string } = {
|
||||||
|
0: "Initialize Market",
|
||||||
|
1: "New Order",
|
||||||
|
2: "Match Orders",
|
||||||
|
3: "Consume Events",
|
||||||
|
4: "Cancel Order",
|
||||||
|
5: "Settle Funds",
|
||||||
|
6: "Cancel Order By Client Id",
|
||||||
|
7: "Disable Market",
|
||||||
|
8: "Sweep Fees",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseSerumInstructionTitle(
|
||||||
|
instruction: TransactionInstruction
|
||||||
|
): string {
|
||||||
|
const code = instruction.data.slice(1, 5).readUInt32LE(0);
|
||||||
|
|
||||||
|
if (!(code in SERUM_CODE_LOOKUP)) {
|
||||||
|
throw new Error(`Unrecognized Serum instruction code: ${code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SERUM_CODE_LOOKUP[code];
|
||||||
|
}
|
38
explorer/src/components/instruction/token-swap/types.ts
Normal file
38
explorer/src/components/instruction/token-swap/types.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { TransactionInstruction } from "@solana/web3.js";
|
||||||
|
|
||||||
|
export const PROGRAM_IDS: string[] = [
|
||||||
|
"9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL", // mainnet
|
||||||
|
"2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg", // testnet
|
||||||
|
"9tdctNJuFsYZ6VrKfKEuwwbPp4SFdFw3jYBZU8QUtzeX", // testnet - legacy
|
||||||
|
"CrRvVBS4Hmj47TPU3cMukurpmCUYUrdHYxTQBxncBGqw", // testnet - legacy
|
||||||
|
"BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ", // devnet
|
||||||
|
"H1E1G7eD5Rrcy43xvDxXCsjkRggz7MWNMLGJ8YNzJ8PM", // devnet - legacy
|
||||||
|
"CMoteLxSPVPoc7Drcggf3QPg3ue8WPpxYyZTg77UGqHo", // devnet - legacy
|
||||||
|
"EEuPz4iZA5reBUeZj6x1VzoiHfYeHMppSCnHZasRFhYo", // devnet - legacy
|
||||||
|
"5rdpyt5iGfr68qt28hkefcFyF4WtyhTwqKDmHSBG8GZx", // localnet
|
||||||
|
];
|
||||||
|
|
||||||
|
const INSTRUCTION_LOOKUP: { [key: number]: string } = {
|
||||||
|
0: "Initialize Swap",
|
||||||
|
1: "Swap",
|
||||||
|
2: "Deposit",
|
||||||
|
3: "Withdraw",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isTokenSwapInstruction(
|
||||||
|
instruction: TransactionInstruction
|
||||||
|
): boolean {
|
||||||
|
return PROGRAM_IDS.includes(instruction.programId.toBase58());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTokenSwapInstructionTitle(
|
||||||
|
instruction: TransactionInstruction
|
||||||
|
): string {
|
||||||
|
const code = instruction.data[0];
|
||||||
|
|
||||||
|
if (!(code in INSTRUCTION_LOOKUP)) {
|
||||||
|
throw new Error(`Unrecognized Token Swap instruction code: ${code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return INSTRUCTION_LOOKUP[code];
|
||||||
|
}
|
@ -24,11 +24,14 @@ import { displayTimestamp } from "utils/date";
|
|||||||
import { InfoTooltip } from "components/common/InfoTooltip";
|
import { InfoTooltip } from "components/common/InfoTooltip";
|
||||||
import { Address } from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
import { Signature } from "components/common/Signature";
|
import { Signature } from "components/common/Signature";
|
||||||
import { intoTransactionInstruction, isSerumInstruction } from "utils/tx";
|
import { intoTransactionInstruction } from "utils/tx";
|
||||||
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
|
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
|
||||||
import { FetchStatus } from "providers/cache";
|
import { FetchStatus } from "providers/cache";
|
||||||
import { SerumDetailsCard } from "components/instruction/SerumDetailsCard";
|
import { SerumDetailsCard } from "components/instruction/SerumDetailsCard";
|
||||||
import { Slot } from "components/common/Slot";
|
import { Slot } from "components/common/Slot";
|
||||||
|
import { isTokenSwapInstruction } from "components/instruction/token-swap/types";
|
||||||
|
import { TokenSwapDetailsCard } from "components/instruction/TokenSwapDetailsCard";
|
||||||
|
import { isSerumInstruction } from "components/instruction/serum/types";
|
||||||
|
|
||||||
const AUTO_REFRESH_INTERVAL = 2000;
|
const AUTO_REFRESH_INTERVAL = 2000;
|
||||||
const ZERO_CONFIRMATION_BAILOUT = 5;
|
const ZERO_CONFIRMATION_BAILOUT = 5;
|
||||||
@ -467,6 +470,8 @@ function InstructionsSection({ signature }: SignatureProps) {
|
|||||||
|
|
||||||
if (isSerumInstruction(ix)) {
|
if (isSerumInstruction(ix)) {
|
||||||
return <SerumDetailsCard key={index} {...props} />;
|
return <SerumDetailsCard key={index} {...props} />;
|
||||||
|
} else if (isTokenSwapInstruction(ix)) {
|
||||||
|
return <TokenSwapDetailsCard key={index} {...props} />;
|
||||||
} else {
|
} else {
|
||||||
return <UnknownDetailsCard key={index} {...props} />;
|
return <UnknownDetailsCard key={index} {...props} />;
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,6 @@ import { TokenRegistry } from "tokenRegistry";
|
|||||||
import { Cluster } from "providers/cluster";
|
import { Cluster } from "providers/cluster";
|
||||||
import { SerumMarketRegistry } from "serumMarketRegistry";
|
import { SerumMarketRegistry } from "serumMarketRegistry";
|
||||||
|
|
||||||
export const EXTERNAL_PROGRAMS: { [key: string]: string } = {
|
|
||||||
Serum: "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn",
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProgramName = typeof PROGRAM_IDS[keyof typeof PROGRAM_IDS];
|
export type ProgramName = typeof PROGRAM_IDS[keyof typeof PROGRAM_IDS];
|
||||||
|
|
||||||
export const PROGRAM_IDS = {
|
export const PROGRAM_IDS = {
|
||||||
@ -127,35 +123,3 @@ export function intoParsedTransaction(tx: Transaction): ParsedTransaction {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSerumInstruction(instruction: TransactionInstruction) {
|
|
||||||
return instruction.programId.toBase58() === EXTERNAL_PROGRAMS["Serum"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const SERUM_CODE_LOOKUP: { [key: number]: string } = {
|
|
||||||
0: "Initialize Market",
|
|
||||||
1: "New Order",
|
|
||||||
2: "Match Order",
|
|
||||||
3: "Consume Events",
|
|
||||||
4: "Cancel Order",
|
|
||||||
5: "Settle Funds",
|
|
||||||
6: "Cancel Order By Client Id",
|
|
||||||
7: "Disable Market",
|
|
||||||
8: "Sweep Fees",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function parseSerumInstructionTitle(
|
|
||||||
instruction: TransactionInstruction
|
|
||||||
): string {
|
|
||||||
try {
|
|
||||||
const code = instruction.data.slice(1, 5).readUInt32LE(0);
|
|
||||||
|
|
||||||
if (!(code in SERUM_CODE_LOOKUP)) {
|
|
||||||
throw new Error(`Unrecognized Serum instruction code: ${code}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SERUM_CODE_LOOKUP[code];
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user