Mango v3 integration (#19437)

* mango v3 integration

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* more instructions InitMangoAccount, Deposit, Withdraw, InitSpotOpenOrders, ConsumeEvents

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* add support for addPerpMarket

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* make group config handling generic

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* fix issue where rpc calls would be done infinitely - basically react useeffect (when to change) was not narrow enough + added some caching

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1
2021-09-04 00:01:29 +02:00
committed by GitHub
parent 1828579580
commit f7a5c0301a
19 changed files with 2034 additions and 6 deletions

View File

@@ -51,6 +51,7 @@ import { useQuery } from "utils/url";
import { TokenInfoMap } from "@solana/spl-token-registry";
import { useTokenRegistry } from "providers/mints/token-registry";
import { getTokenProgramInstructionName } from "utils/instruction";
import { isMangoInstruction, parseMangoInstructionTitle } from "components/instruction/mango/types";
const TRUNCATE_TOKEN_LENGTH = 10;
const ALL_TOKENS = "";
@@ -502,6 +503,16 @@ const TokenTransactionRow = React.memo(
reportError(error, { signature: tx.signature });
return undefined;
}
} else if (
transactionInstruction &&
isMangoInstruction(transactionInstruction)
) {
try {
name = parseMangoInstructionTitle(transactionInstruction);
} catch (error) {
reportError(error, { signature: tx.signature });
return undefined;
}
} else {
if (
ix.accounts.findIndex((account) =>

View File

@@ -0,0 +1,161 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { useCluster } from "providers/cluster";
import { reportError } from "utils/sentry";
import { InstructionCard } from "./InstructionCard";
import { AddOracleDetailsCard } from "./mango/AddOracleDetailsCard";
import { AddPerpMarketDetailsCard } from "./mango/AddPerpMarketDetailsCard";
import { AddSpotMarketDetailsCard } from "./mango/AddSpotMarketDetailsCard";
import { CancelPerpOrderDetailsCard } from "./mango/CancelPerpOrderDetailsCard";
import { CancelSpotOrderDetailsCard } from "./mango/CancelSpotOrderDetailsCard";
import { ChangePerpMarketParamsDetailsCard } from "./mango/ChangePerpMarketParamsDetailsCard";
import { ConsumeEventsDetailsCard } from "./mango/ConsumeEventsDetailsCard";
import { GenericMngoAccountDetailsCard } from "./mango/GenericMngoAccountDetailsCard";
import { GenericPerpMngoDetailsCard } from "./mango/GenericPerpMngoDetailsCard";
import { GenericSpotMngoDetailsCard } from "./mango/GenericSpotMngoDetailsCard";
import { PlacePerpOrderDetailsCard } from "./mango/PlacePerpOrderDetailsCard";
import { PlaceSpotOrderDetailsCard } from "./mango/PlaceSpotOrderDetailsCard";
import {
decodeAddPerpMarket,
decodeAddSpotMarket,
decodeCancelPerpOrder,
decodeCancelSpotOrder,
decodeChangePerpMarketParams,
decodePlacePerpOrder,
decodePlaceSpotOrder,
parseMangoInstructionTitle,
} from "./mango/types";
export function MangoDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
signature: string;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, signature, innerCards, childIndex } = props;
const { url } = useCluster();
let title;
try {
title = parseMangoInstructionTitle(ix);
switch (title) {
case "InitMangoAccount":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "Deposit":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "Withdraw":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "InitSpotOpenOrders":
return (
<GenericMngoAccountDetailsCard
mangoAccountKeyLocation={1}
title={title}
{...props}
/>
);
case "PlaceSpotOrder":
return (
<PlaceSpotOrderDetailsCard
info={decodePlaceSpotOrder(ix)}
{...props}
/>
);
case "CancelSpotOrder":
return (
<CancelSpotOrderDetailsCard
info={decodeCancelSpotOrder(ix)}
{...props}
/>
);
case "AddPerpMarket":
return (
<AddPerpMarketDetailsCard info={decodeAddPerpMarket(ix)} {...props} />
);
case "PlacePerpOrder":
return (
<PlacePerpOrderDetailsCard
info={decodePlacePerpOrder(ix)}
{...props}
/>
);
case "ConsumeEvents":
return <ConsumeEventsDetailsCard {...props} />;
case "CancelPerpOrder":
return (
<CancelPerpOrderDetailsCard
info={decodeCancelPerpOrder(ix)}
{...props}
/>
);
case "SettleFunds":
return (
<GenericSpotMngoDetailsCard
accountKeyLocation={2}
spotMarketkeyLocation={5}
title={title}
{...props}
/>
);
case "RedeemMngo":
return (
<GenericPerpMngoDetailsCard
mangoAccountKeyLocation={3}
perpMarketKeyLocation={4}
title={title}
{...props}
/>
);
case "ChangePerpMarketParams":
return (
<ChangePerpMarketParamsDetailsCard
info={decodeChangePerpMarketParams(ix)}
{...props}
/>
);
case "AddOracle":
return <AddOracleDetailsCard {...props} />;
case "AddSpotMarket":
return (
<AddSpotMarketDetailsCard info={decodeAddSpotMarket(ix)} {...props} />
);
}
} catch (error) {
reportError(error, {
url: url,
signature: signature,
});
}
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={`Mango: ${title || "Unknown"}`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}

View File

@@ -0,0 +1,23 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
export function AddOracleDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, innerCards, childIndex } = props;
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: AddOracle"
innerCards={innerCards}
childIndex={childIndex}
></InstructionCard>
);
}

View File

@@ -0,0 +1,76 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import moment from "moment";
import { InstructionCard } from "../InstructionCard";
import { AddPerpMarket } from "./types";
export function AddPerpMarketDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: AddPerpMarket;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: AddPerpMarket"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Market index</td>
<td className="text-lg-right">{info.marketIndex}</td>
</tr>
<tr>
<td>Maintenance leverage</td>
<td className="text-lg-right">{info.maintLeverage}</td>
</tr>
<tr>
<td>Initial leverage</td>
<td className="text-lg-right">{info.initLeverage}</td>
</tr>
<tr>
<td>Liquidation fee</td>
<td className="text-lg-right">{info.liquidationFee}</td>
</tr>
<tr>
<td>Maker fee</td>
<td className="text-lg-right">{info.makerFee}</td>
</tr>
<tr>
<td>Taker fee</td>
<td className="text-lg-right">{info.takerFee}</td>
</tr>
<tr>
<td>Base lot size</td>
<td className="text-lg-right">{info.baseLotSize}</td>
</tr>
<tr>
<td>Quote lot size</td>
<td className="text-lg-right">{info.quoteLotSize}</td>
</tr>
<tr>
<td>Rate</td>
<td className="text-lg-right">{info.rate}</td>
</tr>
<tr>
<td>Max depth bps</td>
<td className="text-lg-right">{info.maxDepthBps}</td>
</tr>
<tr>
<td>
MNGO per{" "}
{moment.duration(info.targetPeriodLength, "seconds").humanize()}
</td>
<td className="text-lg-right">
{info.mngoPerPeriod} {}
</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,62 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { AddSpotMarket, spotMarketFromIndex } from "./types";
export function AddSpotMarketDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: AddSpotMarket;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: AddSpotMarket"
innerCards={innerCards}
childIndex={childIndex}
>
{spotMarketFromIndex(ix, info.marketIndex) !== "UNKNOWN" && (
<tr>
<td>Market</td>
<td className="text-lg-right">
{spotMarketFromIndex(ix, info.marketIndex)}
</td>
</tr>
)}
<tr>
<td>Market index</td>
<td className="text-lg-right">{info.marketIndex}</td>
</tr>
<tr>
<td>Maint leverage</td>
<td className="text-lg-right">{info.maintLeverage}</td>
</tr>
<tr>
<td>Init leverage</td>
<td className="text-lg-right">{info.initLeverage}</td>
</tr>
<tr>
<td>Liquidation fee</td>
<td className="text-lg-right">{info.liquidationFee}</td>
</tr>
<tr>
<td>Optimal util</td>
<td className="text-lg-right">{info.optimalUtil}</td>
</tr>
<tr>
<td>Optimal rate</td>
<td className="text-lg-right">{info.optimalRate}</td>
</tr>
<tr>
<td>Max rate</td>
<td className="text-lg-right">{info.maxRate}</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,58 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { InstructionCard } from "../InstructionCard";
import { CancelPerpOrder, getPerpMarketFromInstruction } from "./types";
export function CancelPerpOrderDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: CancelPerpOrder;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
const mangoAccount = ix.keys[1];
const perpMarketAccountMeta = ix.keys[3];
const mangoPerpMarketConfig = getPerpMarketFromInstruction(
ix,
perpMarketAccountMeta
);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: CancelPerpOrder"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
{mangoPerpMarketConfig !== undefined && (
<tr>
<td>Perp market</td>
<td className="text-lg-right">{mangoPerpMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Perp market address</td>
<td>
<Address pubkey={perpMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
<tr>
<td>Order Id</td>
<td className="text-lg-right">{info.orderId}</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,58 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { InstructionCard } from "../InstructionCard";
import { CancelSpotOrder, getSpotMarketFromInstruction } from "./types";
export function CancelSpotOrderDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: CancelSpotOrder;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
const mangoAccount = ix.keys[2];
const spotMarketAccountMeta = ix.keys[4];
const mangoSpotMarketConfig = getSpotMarketFromInstruction(
ix,
spotMarketAccountMeta
);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: CancelSpotOrder"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
{mangoSpotMarketConfig !== undefined && (
<tr>
<td>Spot market</td>
<td className="text-lg-right">{mangoSpotMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Spot market address</td>
<td>
<Address pubkey={spotMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
<tr>
<td>Order Id</td>
<td className="text-lg-right">{info.orderId}</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,122 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import moment from "moment";
import { useCluster } from "providers/cluster";
import { useEffect, useState } from "react";
import { InstructionCard } from "../InstructionCard";
import {
ChangePerpMarketParams,
getPerpMarketFromInstruction,
getPerpMarketFromPerpMarketConfig,
} from "./types";
export function ChangePerpMarketParamsDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: ChangePerpMarketParams;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
const perpMarketAccountMeta = ix.keys[1];
const mangoPerpMarketConfig = getPerpMarketFromInstruction(
ix,
perpMarketAccountMeta
);
const cluster = useCluster();
const [targetPeriodLength, setTargetPeriodLength] = useState<number | null>(
null
);
useEffect(() => {
async function getTargetPeriodLength() {
if (mangoPerpMarketConfig === undefined) {
return;
}
const mangoPerpMarket = await getPerpMarketFromPerpMarketConfig(
cluster.url,
mangoPerpMarketConfig
);
setTargetPeriodLength(
mangoPerpMarket.liquidityMiningInfo.targetPeriodLength.toNumber()
);
}
getTargetPeriodLength();
}, [cluster.url, mangoPerpMarketConfig]);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: ChangePerpMarketParams"
innerCards={innerCards}
childIndex={childIndex}
>
{info.initLeverageOption && (
<tr>
<td>Init leverage</td>
<td className="text-lg-right">{info.initLeverage}</td>
</tr>
)}
{info.liquidationFeeOption && (
<tr>
<td>Liquidation fee</td>
<td className="text-lg-right">{info.liquidationFee}</td>
</tr>
)}
{info.maintLeverageOption && (
<tr>
<td>Maint leverage</td>
<td className="text-lg-right">{info.maintLeverage}</td>
</tr>
)}
{info.makerFeeOption && (
<tr>
<td>Maker fee</td>
<td className="text-lg-right">{info.makerFee}</td>
</tr>
)}
{info.mngoPerPeriodOption && (
<tr>
<td>
MNGO per{" "}
{targetPeriodLength !== null &&
moment.duration(targetPeriodLength, "seconds").humanize()}
</td>
<td className="text-lg-right">
{info.mngoPerPeriod} {}
</td>
</tr>
)}
{info.maxDepthBpsOption && (
<tr>
<td>Max depth bps</td>
<td className="text-lg-right">{info.maxDepthBps}</td>
</tr>
)}
{info.rateOption && (
<tr>
<td>Rate</td>
<td className="text-lg-right">{info.rate}</td>
</tr>
)}
{info.takerFeeOption && (
<tr>
<td>Taker fee</td>
<td className="text-lg-right">{info.takerFee}</td>
</tr>
)}
{info.targetPeriodLengthOption && (
<tr>
<td>Target period length</td>
<td className="text-lg-right">{info.targetPeriodLength}</td>
</tr>
)}
</InstructionCard>
);
}

View File

@@ -0,0 +1,45 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { InstructionCard } from "../InstructionCard";
import { getPerpMarketFromInstruction } from "./types";
export function ConsumeEventsDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, innerCards, childIndex } = props;
const perpMarketAccountMeta = ix.keys[2];
const mangoPerpMarketConfig = getPerpMarketFromInstruction(
ix,
perpMarketAccountMeta
);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={"Mango: ConsumeEvents"}
innerCards={innerCards}
childIndex={childIndex}
>
{mangoPerpMarketConfig !== undefined && (
<tr>
<td>Perp market</td>
<td className="text-lg-right">{mangoPerpMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Perp market address</td>
<td>
<Address pubkey={perpMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,42 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { InstructionCard } from "../InstructionCard";
export function GenericMngoAccountDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
mangoAccountKeyLocation: number;
title: String;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const {
ix,
index,
result,
mangoAccountKeyLocation,
title,
innerCards,
childIndex,
} = props;
const mangoAccount = ix.keys[mangoAccountKeyLocation];
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={"Mango: " + title}
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,64 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { InstructionCard } from "../InstructionCard";
import { getPerpMarketFromInstruction } from "./types";
export function GenericPerpMngoDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
mangoAccountKeyLocation: number;
perpMarketKeyLocation: number;
title: String;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const {
ix,
index,
result,
mangoAccountKeyLocation,
perpMarketKeyLocation,
title,
innerCards,
childIndex,
} = props;
const mangoAccount = ix.keys[mangoAccountKeyLocation];
const perpMarketAccountMeta = ix.keys[perpMarketKeyLocation];
const mangoPerpMarketConfig = getPerpMarketFromInstruction(
ix,
perpMarketAccountMeta
);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={"Mango: " + title}
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
{mangoPerpMarketConfig !== undefined && (
<tr>
<td>Perp market</td>
<td className="text-lg-right">{mangoPerpMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Perp market address</td>
<td>
<Address pubkey={perpMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,64 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { InstructionCard } from "../InstructionCard";
import { getSpotMarketFromInstruction } from "./types";
export function GenericSpotMngoDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
accountKeyLocation: number;
spotMarketkeyLocation: number;
title: String;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const {
ix,
index,
result,
accountKeyLocation,
spotMarketkeyLocation,
title,
innerCards,
childIndex,
} = props;
const mangoAccount = ix.keys[accountKeyLocation];
const spotMarketAccountMeta = ix.keys[spotMarketkeyLocation];
const mangoSpotMarketConfig = getSpotMarketFromInstruction(
ix,
spotMarketAccountMeta
);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title={"Mango: " + title}
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
{mangoSpotMarketConfig !== undefined && (
<tr>
<td>Spot market</td>
<td className="text-lg-right">{mangoSpotMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Spot market address</td>
<td>
<Address pubkey={spotMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
</InstructionCard>
);
}

View File

@@ -0,0 +1,118 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import BN from "bn.js";
import { Address } from "components/common/Address";
import { useCluster } from "providers/cluster";
import { useEffect, useState } from "react";
import { InstructionCard } from "../InstructionCard";
import {
getPerpMarketFromInstruction,
getPerpMarketFromPerpMarketConfig,
OrderLotDetails,
PlacePerpOrder,
} from "./types";
export function PlacePerpOrderDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: PlacePerpOrder;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
const mangoAccount = ix.keys[1];
const perpMarketAccountMeta = ix.keys[4];
const mangoPerpMarketConfig = getPerpMarketFromInstruction(
ix,
perpMarketAccountMeta
);
const cluster = useCluster();
const [orderLotDetails, setOrderLotDetails] =
useState<OrderLotDetails | null>(null);
useEffect(() => {
async function getOrderLotDetails() {
if (mangoPerpMarketConfig === undefined) {
return;
}
const mangoPerpMarket = await getPerpMarketFromPerpMarketConfig(
cluster.url,
mangoPerpMarketConfig
);
const maxBaseQuantity = mangoPerpMarket.baseLotsToNumber(
new BN(info.quantity.toString())
);
const limitPrice = mangoPerpMarket.priceLotsToNumber(
new BN(info.price.toString())
);
setOrderLotDetails({
price: limitPrice,
size: maxBaseQuantity,
} as OrderLotDetails);
}
getOrderLotDetails();
}, [cluster.url, info.quantity, info.price, mangoPerpMarketConfig]);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: PlacePerpOrder"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
{" "}
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
{mangoPerpMarketConfig !== undefined && (
<tr>
<td>Perp market</td>
<td className="text-lg-right">{mangoPerpMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Perp market address</td>
<td>
<Address pubkey={perpMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
{info.clientOrderId !== "0" && (
<tr>
<td>Client order Id</td>
<td className="text-lg-right">{info.clientOrderId}</td>
</tr>
)}
<tr>
<td>Order type</td>
<td className="text-lg-right">{info.orderType}</td>
</tr>
<tr>
<td>side</td>
<td className="text-lg-right">{info.side}</td>
</tr>
{orderLotDetails !== null && (
<tr>
<td>price</td>
<td className="text-lg-right">{orderLotDetails?.price} USDC</td>
</tr>
)}
{orderLotDetails !== null && (
<tr>
<td>quantity</td>
<td className="text-lg-right">{orderLotDetails?.size}</td>
</tr>
)}
</InstructionCard>
);
}

View File

@@ -0,0 +1,130 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import BN from "bn.js";
import { Address } from "components/common/Address";
import { useCluster } from "providers/cluster";
import { useEffect, useState } from "react";
import { InstructionCard } from "../InstructionCard";
import {
getSpotMarketFromInstruction,
getSpotMarketFromSpotMarketConfig,
OrderLotDetails,
PlaceSpotOrder,
} from "./types";
export function PlaceSpotOrderDetailsCard(props: {
ix: TransactionInstruction;
index: number;
result: SignatureResult;
info: PlaceSpotOrder;
innerCards?: JSX.Element[];
childIndex?: number;
}) {
const { ix, index, result, info, innerCards, childIndex } = props;
const mangoAccount = ix.keys[1];
const spotMarketAccountMeta = ix.keys[5];
const mangoSpotMarketConfig = getSpotMarketFromInstruction(
ix,
spotMarketAccountMeta
);
const cluster = useCluster();
const [orderLotDetails, setOrderLotDetails] =
useState<OrderLotDetails | null>(null);
useEffect(() => {
async function getOrderLotDetails() {
if (mangoSpotMarketConfig === undefined) {
return;
}
const mangoSpotMarket = await getSpotMarketFromSpotMarketConfig(
ix.programId,
cluster.url,
mangoSpotMarketConfig
);
if (mangoSpotMarket === undefined) {
return;
}
const maxBaseQuantity = mangoSpotMarket.baseSizeLotsToNumber(
new BN(info.maxBaseQuantity.toString())
);
const limitPrice = mangoSpotMarket.priceLotsToNumber(
new BN(info.limitPrice.toString())
);
setOrderLotDetails({
price: limitPrice,
size: maxBaseQuantity,
} as OrderLotDetails);
}
getOrderLotDetails();
}, [
cluster.url,
info.maxBaseQuantity,
info.limitPrice,
ix.programId,
mangoSpotMarketConfig,
]);
return (
<InstructionCard
ix={ix}
index={index}
result={result}
title="Mango: PlaceSpotOrder"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Mango account</td>
<td>
{" "}
<Address pubkey={mangoAccount.pubkey} alignRight link />
</td>
</tr>
{mangoSpotMarketConfig !== undefined && (
<tr>
<td>Spot market</td>
<td className="text-lg-right">{mangoSpotMarketConfig.name}</td>
</tr>
)}
<tr>
<td>Spot market address</td>
<td>
<Address pubkey={spotMarketAccountMeta.pubkey} alignRight link />
</td>
</tr>
<tr>
<td>Order type</td>
<td className="text-lg-right">{info.orderType}</td>
</tr>
{info.clientId !== "0" && (
<tr>
<td>Client Id</td>
<td className="text-lg-right">{info.clientId}</td>
</tr>
)}
<tr>
<td>Side</td>
<td className="text-lg-right">{info.side}</td>
</tr>
{orderLotDetails !== null && (
<tr>
<td>Limit price</td>
{/* todo fix price */}
<td className="text-lg-right">{orderLotDetails?.price} USDC</td>
</tr>
)}
{orderLotDetails !== null && (
<tr>
<td>Size</td>
<td className="text-lg-right">{orderLotDetails?.size}</td>
</tr>
)}
</InstructionCard>
);
}

View File

@@ -0,0 +1,430 @@
import {
Config,
GroupConfig,
MangoInstructionLayout,
PerpMarket,
PerpMarketConfig,
PerpMarketLayout,
SpotMarketConfig,
} from "@blockworks-foundation/mango-client";
import { Market } from "@project-serum/serum";
import {
AccountInfo,
AccountMeta,
Connection,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
// note: mainnet.1 suffices since its a superset of mainnet.0
const mangoGroups = Config.ids().groups.filter(
(group) => group.name !== "mainnet.0"
);
// caching of account info's by public keys
let accountInfoCache: Record<string, Promise<AccountInfo<Buffer> | null>> = {};
function getAccountInfo(
clusterUrl: string,
publicKey: PublicKey
): Promise<AccountInfo<Buffer> | null> {
if (publicKey.toBase58() in accountInfoCache) {
return accountInfoCache[publicKey.toBase58()];
}
const connection = new Connection(clusterUrl);
const accountInfoPromise = connection.getAccountInfo(publicKey);
accountInfoCache[publicKey.toBase58()] = accountInfoPromise;
return accountInfoPromise;
}
function findGroupConfig(programId: PublicKey): GroupConfig | undefined {
const filtered = mangoGroups.filter((group) =>
group.mangoProgramId.equals(programId)
);
if (filtered.length) {
return filtered[0];
}
}
export const isMangoInstruction = (instruction: TransactionInstruction) => {
return mangoGroups
.map((group) => group.mangoProgramId.toBase58())
.includes(instruction.programId.toBase58());
};
export const INSTRUCTION_LOOKUP: { [key: number]: string } = {
0: "InitMangoGroup",
1: "InitMangoAccount",
2: "Deposit",
3: "Withdraw",
4: "AddSpotMarket",
5: "AddToBasket",
6: "Borrow",
7: "CachePrices",
8: "CacheRootBanks",
9: "PlaceSpotOrder",
10: "AddOracle",
11: "AddPerpMarket",
12: "PlacePerpOrder",
13: "CancelPerpOrderByClientId",
14: "CancelPerpOrder",
15: "ConsumeEvents",
16: "CachePerpMarkets",
17: "UpdateFunding",
18: "SetOracle",
19: "SettleFunds",
20: "CancelSpotOrder",
21: "UpdateRootBank",
22: "SettlePnl",
23: "SettleBorrow",
24: "ForceCancelSpotOrders",
25: "ForceCancelPerpOrders",
26: "LiquidateTokenAndToken",
27: "LiquidateTokenAndPerp",
28: "LiquidatePerpMarket",
29: "SettleFees",
30: "ResolvePerpBankruptcy",
31: "ResolveTokenBankruptcy",
32: "InitSpotOpenOrders",
33: "RedeemMngo",
34: "AddMangoAccountInfo",
35: "DepositMsrm",
36: "WithdrawMsrm",
37: "ChangePerpMarketParams",
};
export const parseMangoInstructionTitle = (
instruction: TransactionInstruction
): string => {
const code = instruction.data[0];
if (!(code in INSTRUCTION_LOOKUP)) {
throw new Error(`Unrecognized Mango instruction code: ${code}`);
}
return INSTRUCTION_LOOKUP[code];
};
export type Deposit = {
quantity: number;
};
export const decodeDeposit = (ix: TransactionInstruction): Deposit => {
const decoded = MangoInstructionLayout.decode(ix.data);
const deposit: Deposit = {
quantity: decoded.Deposit.quantity.toNumber(),
};
return deposit;
};
export type AddToBasket = {
marketIndex: number;
};
export const decodeAddToBasket = (ix: TransactionInstruction): AddToBasket => {
const decoded = MangoInstructionLayout.decode(ix.data);
const addToBasket: AddToBasket = {
marketIndex: decoded.AddToBasket.marketIndex.toNumber(),
};
return addToBasket;
};
export type Withdraw = {
quantity: number;
allowBorrow: String;
};
export const decodeWithdraw = (ix: TransactionInstruction): Withdraw => {
const decoded = MangoInstructionLayout.decode(ix.data);
const withdraw: Withdraw = {
quantity: decoded.Withdraw.quantity.toNumber(),
allowBorrow: decoded.Withdraw.allowBorrow.toString(),
};
return withdraw;
};
export type PlaceSpotOrder = {
side: String;
limitPrice: number;
maxBaseQuantity: number;
maxQuoteQuantity: number;
selfTradeBehavior: String;
orderType: String;
clientId: String;
limit: String;
};
export const decodePlaceSpotOrder = (
ix: TransactionInstruction
): PlaceSpotOrder => {
const decoded = MangoInstructionLayout.decode(ix.data);
const placeSpotOrder: PlaceSpotOrder = {
side: decoded.PlaceSpotOrder.side.toString(),
limitPrice: decoded.PlaceSpotOrder.limitPrice.toNumber(),
maxBaseQuantity: decoded.PlaceSpotOrder.maxBaseQuantity.toNumber(),
maxQuoteQuantity: decoded.PlaceSpotOrder.maxQuoteQuantity.toNumber(),
selfTradeBehavior: decoded.PlaceSpotOrder.selfTradeBehavior,
orderType: decoded.PlaceSpotOrder.orderType.toString(),
clientId: decoded.PlaceSpotOrder.clientId.toString(),
limit: decoded.PlaceSpotOrder.limit.toString(),
};
return placeSpotOrder;
};
export type CancelSpotOrder = {
orderId: String;
side: String;
};
export const decodeCancelSpotOrder = (
ix: TransactionInstruction
): CancelSpotOrder => {
const decoded = MangoInstructionLayout.decode(ix.data);
const cancelSpotOrder: CancelSpotOrder = {
orderId: decoded.CancelSpotOrder.orderId.toString(),
side: decoded.CancelSpotOrder.side.toString(),
};
return cancelSpotOrder;
};
export type PlacePerpOrder = {
price: number;
quantity: number;
clientOrderId: String;
side: String;
orderType: String;
};
export const decodePlacePerpOrder = (
ix: TransactionInstruction
): PlacePerpOrder => {
const decoded = MangoInstructionLayout.decode(ix.data);
const placePerpOrder: PlacePerpOrder = {
price: decoded.PlacePerpOrder.price.toNumber(),
quantity: decoded.PlacePerpOrder.quantity.toNumber(),
clientOrderId: decoded.PlacePerpOrder.clientOrderId.toString(),
side: decoded.PlacePerpOrder.side.toString(),
orderType: decoded.PlacePerpOrder.orderType.toString(),
};
return placePerpOrder;
};
export type CancelPerpOrder = {
orderId: String;
invalidIdOk: String;
};
export const decodeCancelPerpOrder = (
ix: TransactionInstruction
): CancelPerpOrder => {
const decoded = MangoInstructionLayout.decode(ix.data);
const cancelPerpOrder: CancelPerpOrder = {
orderId: decoded.CancelPerpOrder.orderId.toString(),
invalidIdOk: decoded.CancelPerpOrder.invalidIdOk.toString(),
};
return cancelPerpOrder;
};
export type ChangePerpMarketParams = {
maintLeverageOption: Boolean;
maintLeverage: number;
initLeverageOption: Boolean;
initLeverage: number;
liquidationFeeOption: Boolean;
liquidationFee: number;
makerFeeOption: Boolean;
makerFee: number;
takerFeeOption: Boolean;
takerFee: number;
rateOption: Boolean;
rate: number;
maxDepthBpsOption: Boolean;
maxDepthBps: number;
targetPeriodLengthOption: Boolean;
targetPeriodLength: number;
mngoPerPeriodOption: Boolean;
mngoPerPeriod: number;
};
export const decodeChangePerpMarketParams = (
ix: TransactionInstruction
): ChangePerpMarketParams => {
const decoded = MangoInstructionLayout.decode(ix.data);
const changePerpMarketParams: ChangePerpMarketParams = {
maintLeverageOption: decoded.ChangePerpMarketParams.maintLeverageOption,
maintLeverage: decoded.ChangePerpMarketParams.maintLeverage.toString(),
initLeverageOption: decoded.ChangePerpMarketParams.initLeverageOption,
initLeverage: decoded.ChangePerpMarketParams.initLeverage.toString(),
liquidationFeeOption: decoded.ChangePerpMarketParams.liquidationFeeOption,
liquidationFee: decoded.ChangePerpMarketParams.liquidationFee.toString(),
makerFeeOption: decoded.ChangePerpMarketParams.makerFeeOption,
makerFee: decoded.ChangePerpMarketParams.makerFee.toString(),
takerFeeOption: decoded.ChangePerpMarketParams.takerFeeOption,
takerFee: decoded.ChangePerpMarketParams.takerFee.toString(),
rateOption: decoded.ChangePerpMarketParams.rateOption,
rate: decoded.ChangePerpMarketParams.rate.toString(),
maxDepthBpsOption: decoded.ChangePerpMarketParams.maxDepthBpsOption,
maxDepthBps: decoded.ChangePerpMarketParams.maxDepthBps.toString(),
targetPeriodLengthOption:
decoded.ChangePerpMarketParams.targetPeriodLengthOption,
targetPeriodLength:
decoded.ChangePerpMarketParams.targetPeriodLength.toString(),
mngoPerPeriodOption: decoded.ChangePerpMarketParams.mngoPerPeriodOption,
mngoPerPeriod: decoded.ChangePerpMarketParams.mngoPerPeriod.toString(),
};
return changePerpMarketParams;
};
export type AddSpotMarket = {
marketIndex: number;
maintLeverage: number;
initLeverage: number;
liquidationFee: number;
optimalUtil: number;
optimalRate: number;
maxRate: number;
};
export const decodeAddSpotMarket = (
ix: TransactionInstruction
): AddSpotMarket => {
const decoded = MangoInstructionLayout.decode(ix.data);
const addSpotMarket: AddSpotMarket = {
marketIndex: decoded.AddSpotMarket.marketIndex.toNumber(),
maintLeverage: decoded.AddSpotMarket.maintLeverage.toNumber(),
initLeverage: decoded.AddSpotMarket.initLeverage.toNumber(),
liquidationFee: decoded.AddSpotMarket.liquidationFee.toNumber(),
optimalUtil: decoded.AddSpotMarket.optimalUtil.toNumber(),
optimalRate: decoded.AddSpotMarket.optimalRate.toNumber(),
maxRate: decoded.AddSpotMarket.maxRate.toNumber(),
};
return addSpotMarket;
};
export type AddPerpMarket = {
marketIndex: number;
maintLeverage: number;
initLeverage: number;
liquidationFee: number;
makerFee: number;
takerFee: number;
baseLotSize: number;
quoteLotSize: number;
rate: number;
maxDepthBps: number;
targetPeriodLength: number;
mngoPerPeriod: number;
};
export const decodeAddPerpMarket = (
ix: TransactionInstruction
): AddPerpMarket => {
const decoded = MangoInstructionLayout.decode(ix.data);
const addPerpMarket: AddPerpMarket = {
marketIndex: decoded.AddPerpMarket.marketIndex.toNumber(),
maintLeverage: decoded.AddPerpMarket.maintLeverage.toNumber(),
initLeverage: decoded.AddPerpMarket.initLeverage.toNumber(),
liquidationFee: decoded.AddPerpMarket.liquidationFee.toNumber(),
makerFee: decoded.AddPerpMarket.makerFee.toNumber(),
takerFee: decoded.AddPerpMarket.takerFee.toNumber(),
baseLotSize: decoded.AddPerpMarket.baseLotSize.toNumber(),
quoteLotSize: decoded.AddPerpMarket.quoteLotSize.toNumber(),
rate: decoded.AddPerpMarket.rate.toNumber(),
maxDepthBps: decoded.AddPerpMarket.maxDepthBps.toNumber(),
targetPeriodLength: decoded.AddPerpMarket.targetPeriodLength.toNumber(),
mngoPerPeriod: decoded.AddPerpMarket.mngoPerPeriod.toNumber(),
};
return addPerpMarket;
};
export type OrderLotDetails = {
price: number;
size: number;
};
////
export function logAllKeys(keys: AccountMeta[]) {
keys.map((key) => console.log(key.pubkey.toBase58()));
}
export function getSpotMarketFromInstruction(
ix: TransactionInstruction,
spotMarket: AccountMeta
): SpotMarketConfig | undefined {
const groupConfig = findGroupConfig(ix.programId);
if (groupConfig === undefined) {
return;
}
const spotMarketConfigs = groupConfig.spotMarkets.filter((mangoSpotMarket) =>
spotMarket.pubkey.equals(mangoSpotMarket.publicKey)
);
if (spotMarketConfigs.length) {
return spotMarketConfigs[0];
}
}
export async function getSpotMarketFromSpotMarketConfig(
programId: PublicKey,
clusterUrl: string,
mangoSpotMarketConfig: SpotMarketConfig
): Promise<Market | undefined> {
const connection = new Connection(clusterUrl);
const groupConfig = findGroupConfig(programId);
if (groupConfig === undefined) {
return;
}
return await Market.load(
connection,
mangoSpotMarketConfig.publicKey,
undefined,
groupConfig.serumProgramId
);
}
export function getPerpMarketFromInstruction(
ix: TransactionInstruction,
perpMarket: AccountMeta
): PerpMarketConfig | undefined {
const groupConfig = findGroupConfig(ix.programId);
if (groupConfig === undefined) {
return;
}
const perpMarketConfigs = groupConfig.perpMarkets.filter((mangoPerpMarket) =>
perpMarket.pubkey.equals(mangoPerpMarket.publicKey)
);
if (perpMarketConfigs.length) {
return perpMarketConfigs[0];
}
}
export async function getPerpMarketFromPerpMarketConfig(
clusterUrl: string,
mangoPerpMarketConfig: PerpMarketConfig
): Promise<PerpMarket> {
const acc = await getAccountInfo(clusterUrl, mangoPerpMarketConfig.publicKey);
const decoded = PerpMarketLayout.decode(acc?.data);
return new PerpMarket(
mangoPerpMarketConfig.publicKey,
mangoPerpMarketConfig.baseDecimals,
mangoPerpMarketConfig.quoteDecimals,
decoded
);
}
export function spotMarketFromIndex(
ix: TransactionInstruction,
marketIndex: number
): String | undefined {
const groupConfig = findGroupConfig(ix.programId);
if (groupConfig === undefined) {
return;
}
const spotMarketConfigs = groupConfig.spotMarkets.filter(
(spotMarketConfig) => spotMarketConfig.marketIndex === marketIndex
);
if (!spotMarketConfigs.length) {
return "UNKNOWN";
}
return spotMarketConfigs[0].name;
}

View File

@@ -39,6 +39,8 @@ import { BpfUpgradeableLoaderDetailsCard } from "components/instruction/bpf-upgr
import { VoteDetailsCard } from "components/instruction/vote/VoteDetailsCard";
import { isWormholeInstruction } from "components/instruction/wormhole/types";
import { AssociatedTokenDetailsCard } from "components/instruction/AssociatedTokenDetailsCard";
import { isMangoInstruction } from "components/instruction/mango/types";
import { MangoDetailsCard } from "components/instruction/MangoDetails";
export type InstructionDetailsProps = {
tx: ParsedTransaction;
@@ -208,6 +210,8 @@ function renderInstructionCard({
if (isBonfidaBotInstruction(transactionIx)) {
return <BonfidaBotDetailsCard key={key} {...props} />;
} else if (isMangoInstruction(transactionIx)) {
return <MangoDetailsCard key={key} {...props} />;
} else if (isSerumInstruction(transactionIx)) {
return <SerumDetailsCard key={key} {...props} />;
} else if (isTokenSwapInstruction(transactionIx)) {

View File

@@ -50,6 +50,7 @@ export enum PROGRAM_NAMES {
SERUM_1 = "Serum Program v1",
SERUM_2 = "Serum Program v2",
SERUM_3 = "Serum Program v3",
MANGO_3 = "Mango Program v3",
}
const ALL_CLUSTERS = [
@@ -90,6 +91,7 @@ export const PROGRAM_DEPLOYMENTS = {
[PROGRAM_NAMES.SERUM_1]: MAINNET_ONLY,
[PROGRAM_NAMES.SERUM_2]: MAINNET_ONLY,
[PROGRAM_NAMES.SERUM_3]: MAINNET_ONLY,
[PROGRAM_NAMES.MANGO_3]: MAINNET_ONLY,
} as const;
export const PROGRAM_NAME_BY_ID = {
@@ -121,6 +123,7 @@ export const PROGRAM_NAME_BY_ID = {
BJ3jrUzddfuSrZHXSCxMUUQsjKEyLmuuyZebkcaFp2fg: PROGRAM_NAMES.SERUM_1,
EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o: PROGRAM_NAMES.SERUM_2,
"9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin": PROGRAM_NAMES.SERUM_3,
mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68: PROGRAM_NAMES.MANGO_3,
} as const;
export type LoaderName = typeof LOADER_IDS[keyof typeof LOADER_IDS];