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",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@project-serum/serum": "^0.13.9",
|
||||
"@react-hook/debounce": "^3.0.0",
|
||||
"@sentry/react": "^5.27.2",
|
||||
"@solana/web3.js": "^0.86.2",
|
||||
|
@ -31,6 +31,15 @@ import {
|
||||
IX_TITLES,
|
||||
} from "components/instruction/token/types";
|
||||
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 }) {
|
||||
const address = pubkey.toBase58();
|
||||
@ -301,13 +310,41 @@ const TokenTransactionRow = React.memo(
|
||||
);
|
||||
|
||||
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 (ix.program === "spl-token") {
|
||||
return instructionTypeName(ix, tx);
|
||||
} else {
|
||||
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 {
|
||||
if (
|
||||
ix.accounts.findIndex((account) =>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { TransactionInstruction, SignatureResult } from "@solana/web3.js";
|
||||
import { InstructionCard } from "./InstructionCard";
|
||||
import { parseSerumInstructionTitle } from "utils/tx";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { reportError } from "utils/sentry";
|
||||
import { parseSerumInstructionTitle } from "./serum/types";
|
||||
|
||||
export function SerumDetailsCard({
|
||||
ix,
|
||||
@ -33,7 +33,7 @@ export function SerumDetailsCard({
|
||||
ix={ix}
|
||||
index={index}
|
||||
result={result}
|
||||
title={title || "Unknown"}
|
||||
title={`Serum: ${title || "Unknown"}`}
|
||||
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 { Address } from "components/common/Address";
|
||||
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 { FetchStatus } from "providers/cache";
|
||||
import { SerumDetailsCard } from "components/instruction/SerumDetailsCard";
|
||||
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 ZERO_CONFIRMATION_BAILOUT = 5;
|
||||
@ -467,6 +470,8 @@ function InstructionsSection({ signature }: SignatureProps) {
|
||||
|
||||
if (isSerumInstruction(ix)) {
|
||||
return <SerumDetailsCard key={index} {...props} />;
|
||||
} else if (isTokenSwapInstruction(ix)) {
|
||||
return <TokenSwapDetailsCard key={index} {...props} />;
|
||||
} else {
|
||||
return <UnknownDetailsCard key={index} {...props} />;
|
||||
}
|
||||
|
@ -17,10 +17,6 @@ import { TokenRegistry } from "tokenRegistry";
|
||||
import { Cluster } from "providers/cluster";
|
||||
import { SerumMarketRegistry } from "serumMarketRegistry";
|
||||
|
||||
export const EXTERNAL_PROGRAMS: { [key: string]: string } = {
|
||||
Serum: "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn",
|
||||
};
|
||||
|
||||
export type ProgramName = typeof PROGRAM_IDS[keyof typeof 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