From cef0d5879fb78bdeccf28a620e7040beca131a22 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 5 Nov 2020 13:19:02 -0800 Subject: [PATCH] explorer: Serum DEX instruction full decoding and instruction cards (#13330) * map serum instructions in tokenhistory card * add token swap instruction parsing * add serum instruction builders * add new serum instruction detail cards * fix decode bug on cancel order by client id * avoid parsing unsupported instructions --- .../instruction/SerumDetailsCard.tsx | 71 +++- .../serum/CancelOrderByClientIdDetails.tsx | 56 ++++ .../instruction/serum/CancelOrderDetails.tsx | 73 ++++ .../serum/ConsumeEventsDetails.tsx | 58 ++++ .../serum/InitializeMarketDetailsCard.tsx | 120 +++++++ .../serum/MatchOrdersDetailsCard.tsx | 84 +++++ .../instruction/serum/NewOrderDetailsCard.tsx | 104 ++++++ .../serum/SettleFundsDetailsCard.tsx | 95 ++++++ .../src/components/instruction/serum/types.ts | 313 +++++++++++++++++- explorer/src/validators/bignum.ts | 2 +- 10 files changed, 965 insertions(+), 11 deletions(-) create mode 100644 explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx create mode 100644 explorer/src/components/instruction/serum/CancelOrderDetails.tsx create mode 100644 explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx create mode 100644 explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx create mode 100644 explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx create mode 100644 explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx create mode 100644 explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx diff --git a/explorer/src/components/instruction/SerumDetailsCard.tsx b/explorer/src/components/instruction/SerumDetailsCard.tsx index 29cdf2265e..cb5e2706b0 100644 --- a/explorer/src/components/instruction/SerumDetailsCard.tsx +++ b/explorer/src/components/instruction/SerumDetailsCard.tsx @@ -3,24 +3,81 @@ import { TransactionInstruction, SignatureResult } from "@solana/web3.js"; import { InstructionCard } from "./InstructionCard"; import { useCluster } from "providers/cluster"; import { reportError } from "utils/sentry"; -import { parseSerumInstructionTitle } from "./serum/types"; +import { + decodeCancelOrder, + decodeCancelOrderByClientId, + decodeConsumeEvents, + decodeInitializeMarket, + decodeMatchOrders, + decodeNewOrder, + decodeSettleFunds, + parseSerumInstructionCode, + parseSerumInstructionKey, + parseSerumInstructionTitle, + SERUM_DECODED_MAX, +} from "./serum/types"; +import { NewOrderDetailsCard } from "./serum/NewOrderDetailsCard"; +import { MatchOrdersDetailsCard } from "./serum/MatchOrdersDetailsCard"; +import { InitializeMarketDetailsCard } from "./serum/InitializeMarketDetailsCard"; +import { ConsumeEventsDetailsCard } from "./serum/ConsumeEventsDetails"; +import { CancelOrderDetailsCard } from "./serum/CancelOrderDetails"; +import { CancelOrderByClientIdDetailsCard } from "./serum/CancelOrderByClientIdDetails"; +import { SettleFundsDetailsCard } from "./serum/SettleFundsDetailsCard"; -export function SerumDetailsCard({ - ix, - index, - result, - signature, -}: { +export function SerumDetailsCard(props: { ix: TransactionInstruction; index: number; result: SignatureResult; signature: string; }) { + const { ix, index, result, signature } = props; + const { url } = useCluster(); let title; try { title = parseSerumInstructionTitle(ix); + const code = parseSerumInstructionCode(ix); + + if (code <= SERUM_DECODED_MAX) { + switch (parseSerumInstructionKey(ix)) { + case "initializeMarket": + return ( + + ); + case "newOrder": + return ; + case "matchOrders": + return ( + + ); + case "consumeEvents": + return ( + + ); + case "cancelOrder": + return ( + + ); + case "cancelOrderByClientId": + return ( + + ); + case "settleFunds": + return ( + + ); + } + } } catch (error) { reportError(error, { url: url, diff --git a/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx b/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx new file mode 100644 index 0000000000..de4d9f97f6 --- /dev/null +++ b/explorer/src/components/instruction/serum/CancelOrderByClientIdDetails.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { CancelOrderByClientId } from "./types"; + +export function CancelOrderByClientIdDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: CancelOrderByClientId; +}) { + const { ix, index, result, info } = props; + + return ( + + + Market + +
+ + + + + Open Orders + +
+ + + + + Request Queue + +
+ + + + + Owner + +
+ + + + + Client Id + {info.clientId.toString(10)} + + + ); +} diff --git a/explorer/src/components/instruction/serum/CancelOrderDetails.tsx b/explorer/src/components/instruction/serum/CancelOrderDetails.tsx new file mode 100644 index 0000000000..33ac8372d7 --- /dev/null +++ b/explorer/src/components/instruction/serum/CancelOrderDetails.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { CancelOrder } from "./types"; + +export function CancelOrderDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: CancelOrder; +}) { + const { ix, index, result, info } = props; + + return ( + + + Program + +
+ + + + + Market + +
+ + + + + Open Orders + +
+ + + + + Request Queue + +
+ + + + + Owner + +
+ + + + + Side + {info.side} + + + + Open Orders Slot + {info.openOrdersSlot} + + + + Order Id + {info.orderId.toString(10)} + + + ); +} diff --git a/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx b/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx new file mode 100644 index 0000000000..c7831579e4 --- /dev/null +++ b/explorer/src/components/instruction/serum/ConsumeEventsDetails.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { ConsumeEvents } from "./types"; + +export function ConsumeEventsDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: ConsumeEvents; +}) { + const { ix, index, result, info } = props; + + return ( + + + Program + +
+ + + + + Market + +
+ + + + + Event Queue + +
+ + + + + Open Orders Accounts + + {info.openOrdersAccounts.map((account, index) => { + return
; + })} + + + + + Limit + {info.limit} + + + ); +} diff --git a/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx b/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx new file mode 100644 index 0000000000..b76d9b178c --- /dev/null +++ b/explorer/src/components/instruction/serum/InitializeMarketDetailsCard.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { InitializeMarket } from "./types"; + +export function InitializeMarketDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: InitializeMarket; +}) { + const { ix, index, result, info } = props; + + return ( + + + Program + +
+ + + + + Market + +
+ + + + + Request Queue + +
+ + + + + Event Queue + +
+ + + + + Bids + +
+ + + + + Asks + +
+ + + + + Base Vault + +
+ + + + + Quote Vault + +
+ + + + + Base Mint + +
+ + + + + Quote Mint + +
+ + + + + Base Lot Size + {info.baseLotSize.toString(10)} + + + + Quote Lot Size + {info.quoteLotSize.toString(10)} + + + + Fee Rate Bps + {info.feeRateBps} + + + + Quote Dust THreshold + + {info.quoteDustThreshold.toString(10)} + + + + + Vault Signer Nonce + {info.vaultSignerNonce.toString(10)} + + + ); +} diff --git a/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx b/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx new file mode 100644 index 0000000000..1c83c91676 --- /dev/null +++ b/explorer/src/components/instruction/serum/MatchOrdersDetailsCard.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { MatchOrders } from "./types"; + +export function MatchOrdersDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: MatchOrders; +}) { + const { ix, index, result, info } = props; + + return ( + + + Program + +
+ + + + + Market + +
+ + + + + Request Queue + +
+ + + + + Event Queue + +
+ + + + + Bids + +
+ + + + + Asks + +
+ + + + + Base Vault + +
+ + + + + Quote Vault + +
+ + + + + Limit + {info.limit} + + + ); +} diff --git a/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx b/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx new file mode 100644 index 0000000000..aa005aaec1 --- /dev/null +++ b/explorer/src/components/instruction/serum/NewOrderDetailsCard.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { NewOrder } from "./types"; + +export function NewOrderDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: NewOrder; +}) { + const { ix, index, result, info } = props; + + return ( + + + Program + +
+ + + + + Market + +
+ + + + + Open Orders + +
+ + + + + Request Queue + +
+ + + + + Payer + +
+ + + + + Owner + +
+ + + + + Base Vault + +
+ + + + + Quote Vault + +
+ + + + + Side + {info.side} + + + + Order Type + {info.orderType} + + + + Limit Price + {info.limitPrice.toString(10)} + + + + Max Quantity + {info.maxQuantity.toString(10)} + + + + Client Id + {info.clientId.toString(10)} + + + ); +} diff --git a/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx b/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx new file mode 100644 index 0000000000..a2c685425e --- /dev/null +++ b/explorer/src/components/instruction/serum/SettleFundsDetailsCard.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; +import { InstructionCard } from "../InstructionCard"; +import { Address } from "components/common/Address"; +import { SettleFunds } from "./types"; + +export function SettleFundsDetailsCard(props: { + ix: TransactionInstruction; + index: number; + result: SignatureResult; + info: SettleFunds; +}) { + const { ix, index, result, info } = props; + + return ( + + + Program + +
+ + + + + Market + +
+ + + + + Open Orders + +
+ + + + + Owner + +
+ + + + + Base Vault + +
+ + + + + Quote Vault + +
+ + + + + Base Wallet + +
+ + + + + Quote Wallet + +
+ + + + + Vault Signer + +
+ + + + {info.referrerQuoteWallet && ( + + Referrer Quote Wallet + +
+ + + )} + + ); +} diff --git a/explorer/src/components/instruction/serum/types.ts b/explorer/src/components/instruction/serum/types.ts index 7199985c9f..dae467b1a7 100644 --- a/explorer/src/components/instruction/serum/types.ts +++ b/explorer/src/components/instruction/serum/types.ts @@ -1,8 +1,298 @@ -import { MARKETS } from "@project-serum/serum"; -import { TransactionInstruction } from "@solana/web3.js"; +/* eslint-disable @typescript-eslint/no-redeclare */ + +import { decodeInstruction, MARKETS } from "@project-serum/serum"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import BN from "bn.js"; +import { coerce, enums, number, optional, pick, StructType } from "superstruct"; +import { BigNumValue } from "validators/bignum"; +import { Pubkey } from "validators/pubkey"; const SERUM_PROGRAM_ID = "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn"; +export const SERUM_DECODED_MAX = 6; + +export type Side = StructType; +export const Side = enums(["buy", "sell"]); + +export type OrderType = StructType; +export const OrderType = enums(["limit", "ioc", "postOnly"]); + +export type InitializeMarket = { + market: PublicKey; + requestQueue: PublicKey; + eventQueue: PublicKey; + bids: PublicKey; + asks: PublicKey; + baseVault: PublicKey; + quoteVault: PublicKey; + baseMint: PublicKey; + quoteMint: PublicKey; + baseLotSize: BN; + quoteLotSize: BN; + feeRateBps: number; + vaultSignerNonce: BN; + quoteDustThreshold: BN; + programId: PublicKey; +}; + +export const InitializeMarketDecode = pick({ + baseLotSize: BigNumValue, + quoteLotSize: BigNumValue, + feeRateBps: number(), + quoteDustThreshold: BigNumValue, + vaultSignerNonce: BigNumValue, +}); + +export function decodeInitializeMarket( + ix: TransactionInstruction +): InitializeMarket { + const decoded = coerce( + decodeInstruction(ix.data).initializeMarket, + InitializeMarketDecode + ); + + let initializeMarket: InitializeMarket = { + market: ix.keys[0].pubkey, + requestQueue: ix.keys[1].pubkey, + eventQueue: ix.keys[2].pubkey, + bids: ix.keys[3].pubkey, + asks: ix.keys[4].pubkey, + baseVault: ix.keys[5].pubkey, + quoteVault: ix.keys[6].pubkey, + baseMint: ix.keys[7].pubkey, + quoteMint: ix.keys[8].pubkey, + programId: ix.programId, + baseLotSize: decoded.baseLotSize as BN, + quoteLotSize: decoded.quoteLotSize as BN, + feeRateBps: decoded.feeRateBps, + quoteDustThreshold: decoded.quoteDustThreshold as BN, + vaultSignerNonce: decoded.vaultSignerNonce as BN, + }; + + return initializeMarket; +} + +export type NewOrder = { + market: PublicKey; + openOrders: PublicKey; + requestQueue: PublicKey; + payer: PublicKey; + owner: PublicKey; + baseVault: PublicKey; + quoteVault: PublicKey; + programId: PublicKey; + feeDiscountPubkey?: PublicKey; + side: Side; + limitPrice: BN; + maxQuantity: BN; + orderType: OrderType; + clientId: BN; +}; + +export const NewOrderDecode = pick({ + side: Side, + limitPrice: BigNumValue, + maxQuantity: BigNumValue, + orderType: OrderType, + clientId: BigNumValue, + feeDiscountPubkey: optional(Pubkey), +}); + +export function decodeNewOrder(ix: TransactionInstruction): NewOrder { + const decoded = coerce(decodeInstruction(ix.data).newOrder, NewOrderDecode); + + let newOrder: NewOrder = { + market: ix.keys[0].pubkey, + openOrders: ix.keys[1].pubkey, + requestQueue: ix.keys[2].pubkey, + payer: ix.keys[3].pubkey, + owner: ix.keys[4].pubkey, + baseVault: ix.keys[5].pubkey, + quoteVault: ix.keys[6].pubkey, + programId: ix.programId, + side: decoded.side as Side, + limitPrice: decoded.limitPrice as BN, + maxQuantity: decoded.maxQuantity as BN, + orderType: decoded.orderType as OrderType, + clientId: decoded.clientId as BN, + }; + + if (decoded.feeDiscountPubkey) { + newOrder.feeDiscountPubkey = decoded.feeDiscountPubkey; + } + + return newOrder; +} + +export type MatchOrders = { + market: PublicKey; + requestQueue: PublicKey; + eventQueue: PublicKey; + bids: PublicKey; + asks: PublicKey; + baseVault: PublicKey; + quoteVault: PublicKey; + limit: number; + programId: PublicKey; +}; + +export const MatchOrdersDecode = pick({ + limit: number(), +}); + +export function decodeMatchOrders(ix: TransactionInstruction): MatchOrders { + const decoded = coerce( + decodeInstruction(ix.data).matchOrders, + MatchOrdersDecode + ); + + const matchOrders: MatchOrders = { + market: ix.keys[0].pubkey, + requestQueue: ix.keys[1].pubkey, + eventQueue: ix.keys[2].pubkey, + bids: ix.keys[3].pubkey, + asks: ix.keys[4].pubkey, + baseVault: ix.keys[5].pubkey, + quoteVault: ix.keys[6].pubkey, + programId: ix.programId, + limit: decoded.limit, + }; + + return matchOrders; +} + +export type ConsumeEvents = { + market: PublicKey; + eventQueue: PublicKey; + openOrdersAccounts: PublicKey[]; + limit: number; + programId: PublicKey; +}; + +export const ConsumeEventsDecode = pick({ + limit: number(), +}); + +export function decodeConsumeEvents(ix: TransactionInstruction): ConsumeEvents { + const decoded = coerce( + decodeInstruction(ix.data).consumeEvents, + ConsumeEventsDecode + ); + + const consumeEvents: ConsumeEvents = { + openOrdersAccounts: ix.keys.slice(0, -2).map((k) => k.pubkey), + market: ix.keys[ix.keys.length - 3].pubkey, + eventQueue: ix.keys[ix.keys.length - 2].pubkey, + programId: ix.programId, + limit: decoded.limit, + }; + + return consumeEvents; +} + +export type CancelOrder = { + market: PublicKey; + openOrders: PublicKey; + owner: PublicKey; + requestQueue: PublicKey; + side: "buy" | "sell"; + orderId: BN; + openOrdersSlot: number; + programId: PublicKey; +}; + +export const CancelOrderDecode = pick({ + side: Side, + orderId: BigNumValue, + openOrdersSlot: number(), +}); + +export function decodeCancelOrder(ix: TransactionInstruction): CancelOrder { + const decoded = coerce( + decodeInstruction(ix.data).cancelOrder, + CancelOrderDecode + ); + + const cancelOrder: CancelOrder = { + market: ix.keys[0].pubkey, + openOrders: ix.keys[1].pubkey, + requestQueue: ix.keys[2].pubkey, + owner: ix.keys[3].pubkey, + programId: ix.programId, + openOrdersSlot: decoded.openOrdersSlot, + orderId: decoded.orderId as BN, + side: decoded.side, + }; + + return cancelOrder; +} + +export type CancelOrderByClientId = { + market: PublicKey; + openOrders: PublicKey; + owner: PublicKey; + requestQueue: PublicKey; + clientId: BN; + programId: PublicKey; +}; + +export const CancelOrderByClientIdDecode = pick({ + clientId: BigNumValue, +}); + +export function decodeCancelOrderByClientId( + ix: TransactionInstruction +): CancelOrderByClientId { + const decoded = coerce( + decodeInstruction(ix.data).cancelOrderByClientId, + CancelOrderByClientIdDecode + ); + + const cancelOrderByClientId: CancelOrderByClientId = { + market: ix.keys[0].pubkey, + openOrders: ix.keys[1].pubkey, + requestQueue: ix.keys[2].pubkey, + owner: ix.keys[3].pubkey, + programId: ix.programId, + clientId: decoded.clientId as BN, + }; + + return cancelOrderByClientId; +} + +export type SettleFunds = { + market: PublicKey; + openOrders: PublicKey; + owner: PublicKey; + baseVault: PublicKey; + quoteVault: PublicKey; + baseWallet: PublicKey; + quoteWallet: PublicKey; + vaultSigner: PublicKey; + programId: PublicKey; + referrerQuoteWallet?: PublicKey; +}; + +export function decodeSettleFunds(ix: TransactionInstruction): SettleFunds { + let settleFunds: SettleFunds = { + market: ix.keys[0].pubkey, + openOrders: ix.keys[1].pubkey, + owner: ix.keys[2].pubkey, + baseVault: ix.keys[3].pubkey, + quoteVault: ix.keys[4].pubkey, + baseWallet: ix.keys[5].pubkey, + quoteWallet: ix.keys[6].pubkey, + vaultSigner: ix.keys[7].pubkey, + programId: ix.programId, + }; + + if (ix.keys.length > 9) { + settleFunds.referrerQuoteWallet = ix.keys[9].pubkey; + } + + return settleFunds; +} + export function isSerumInstruction(instruction: TransactionInstruction) { return ( instruction.programId.toBase58() === SERUM_PROGRAM_ID || @@ -13,6 +303,19 @@ export function isSerumInstruction(instruction: TransactionInstruction) { ); } +export function parseSerumInstructionKey( + instruction: TransactionInstruction +): string { + const decoded = decodeInstruction(instruction.data); + const keys = Object.keys(decoded); + + if (keys.length < 1) { + throw new Error("Serum instruction key not decoded"); + } + + return keys[0]; +} + const SERUM_CODE_LOOKUP: { [key: number]: string } = { 0: "Initialize Market", 1: "New Order", @@ -26,10 +329,14 @@ const SERUM_CODE_LOOKUP: { [key: number]: string } = { 9: "New Order", }; +export function parseSerumInstructionCode(instruction: TransactionInstruction) { + return instruction.data.slice(1, 5).readUInt32LE(0); +} + export function parseSerumInstructionTitle( instruction: TransactionInstruction ): string { - const code = instruction.data.slice(1, 5).readUInt32LE(0); + const code = parseSerumInstructionCode(instruction); if (!(code in SERUM_CODE_LOOKUP)) { throw new Error(`Unrecognized Serum instruction code: ${code}`); diff --git a/explorer/src/validators/bignum.ts b/explorer/src/validators/bignum.ts index 14d201d041..b7ce9a4e77 100644 --- a/explorer/src/validators/bignum.ts +++ b/explorer/src/validators/bignum.ts @@ -1,7 +1,7 @@ import { coercion, struct, Struct } from "superstruct"; import BN from "bn.js"; -const BigNumValue = struct("BigNum", (value) => value instanceof BN); +export const BigNumValue = struct("BigNum", (value) => value instanceof BN); export const BigNum: Struct = coercion(BigNumValue, (value) => { if (typeof value === "string") return new BN(value, 10); throw new Error("invalid big num");