Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

# workspace package build artifacts
packages/*/dist

# Speedy Web Compiler
.swc/

Expand Down
2 changes: 1 addition & 1 deletion app/components/account/TokenHistoryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { ErrorCard } from '@components/common/ErrorCard';
import { LoadingCard } from '@components/common/LoadingCard';
import { Signature } from '@components/common/Signature';
import { Slot } from '@components/common/Slot';
import { isMangoInstruction, parseMangoInstructionTitle } from '@components/instruction/mango/types';
import { isSerumInstruction, parseSerumInstructionTitle } from '@components/instruction/serum/types';
import {
isTokenLendingInstruction,
parseTokenLendingInstructionTitle,
} from '@components/instruction/token-lending/types';
import { isTokenSwapInstruction, parseTokenSwapInstructionTitle } from '@components/instruction/token-swap/types';
import { isMangoInstruction, parseMangoInstructionTitle } from '@explorer/decoder-mango/detection';
import { isTokenProgramData } from '@providers/accounts';
import { useAccountHistories, useFetchAccountHistory } from '@providers/accounts/history';
import { isTokenProgramId, TokenInfoWithPubkey, useAccountOwnedTokens } from '@providers/accounts/tokens';
Expand Down
2 changes: 2 additions & 0 deletions app/features/instruction-program-mango/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Mango Markets v3 program ID on mainnet-beta */
export const MANGO_V3_PROGRAM_ID = 'mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68';
1 change: 1 addition & 0 deletions app/features/instruction-program-mango/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MangoDetailsCard } from './ui/MangoDetailsCard';
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
getPerpMarketFromPerpMarketConfig,
getSpotMarketFromSpotMarketConfig,
Market,
PerpMarket,
PerpMarketConfig,
SpotMarketConfig,
} from '@explorer/decoder-mango';
import { PublicKey } from '@solana/web3.js';
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { MANGO_V3_PROGRAM_ID } from '../../const';
import { useMangoPerpMarket, useMangoSpotMarket } from '../use-mango-market';

vi.mock('@providers/cluster', () => ({
useCluster: vi.fn(() => ({
url: 'https://mainnet.rpc.address',
})),
}));

vi.mock('@explorer/decoder-mango', () => ({
getPerpMarketFromPerpMarketConfig: vi.fn(),
getSpotMarketFromSpotMarketConfig: vi.fn(),
}));

const perpMarketConfig = { publicKey: PublicKey.default } as unknown as PerpMarketConfig;
const spotMarketConfig = { publicKey: PublicKey.default } as unknown as SpotMarketConfig;
const programId = new PublicKey(MANGO_V3_PROGRAM_ID);

describe('useMangoPerpMarket', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should return undefined when config is undefined', () => {
const { result } = renderHook(() => useMangoPerpMarket(undefined));

expect(result.current).toBeUndefined();
expect(getPerpMarketFromPerpMarketConfig).not.toHaveBeenCalled();
});

it('should return the resolved perp market when config is provided', async () => {
const resolved = { name: 'BTC-PERP' } as unknown as PerpMarket;
vi.mocked(getPerpMarketFromPerpMarketConfig).mockResolvedValueOnce(resolved);

const { result } = renderHook(() => useMangoPerpMarket(perpMarketConfig));

await waitFor(() => {
expect(result.current).toBe(resolved);
});
expect(getPerpMarketFromPerpMarketConfig).toHaveBeenCalledWith('https://mainnet.rpc.address', perpMarketConfig);
});

it('should return undefined when resolution throws', async () => {
vi.mocked(getPerpMarketFromPerpMarketConfig).mockRejectedValueOnce(new Error('rpc failed'));

const { result } = renderHook(() => useMangoPerpMarket(perpMarketConfig));

await waitFor(() => {
expect(getPerpMarketFromPerpMarketConfig).toHaveBeenCalled();
});
expect(result.current).toBeUndefined();
});

it('should reset to undefined when config transitions from defined to undefined', async () => {
const resolved = { name: 'BTC-PERP' } as unknown as PerpMarket;
vi.mocked(getPerpMarketFromPerpMarketConfig).mockResolvedValueOnce(resolved);

const { result, rerender } = renderHook(({ config }) => useMangoPerpMarket(config), {
initialProps: { config: perpMarketConfig as PerpMarketConfig | undefined },
});

await waitFor(() => {
expect(result.current).toBe(resolved);
});

rerender({ config: undefined });

await waitFor(() => {
expect(result.current).toBeUndefined();
});
});
});

describe('useMangoSpotMarket', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should return undefined when config is undefined', () => {
const { result } = renderHook(() => useMangoSpotMarket(programId, undefined));

expect(result.current).toBeUndefined();
expect(getSpotMarketFromSpotMarketConfig).not.toHaveBeenCalled();
});

it('should return the resolved spot market when config is provided', async () => {
const resolved = { address: 'spot' } as unknown as Market;
vi.mocked(getSpotMarketFromSpotMarketConfig).mockResolvedValueOnce(resolved);

const { result } = renderHook(() => useMangoSpotMarket(programId, spotMarketConfig));

await waitFor(() => {
expect(result.current).toBe(resolved);
});
expect(getSpotMarketFromSpotMarketConfig).toHaveBeenCalledWith(
programId,
'https://mainnet.rpc.address',
spotMarketConfig,
);
});

it('should return undefined when resolver yields undefined', async () => {
vi.mocked(getSpotMarketFromSpotMarketConfig).mockResolvedValueOnce(undefined);

const { result } = renderHook(() => useMangoSpotMarket(programId, spotMarketConfig));

await waitFor(() => {
expect(getSpotMarketFromSpotMarketConfig).toHaveBeenCalled();
});
expect(result.current).toBeUndefined();
});

it('should return undefined when resolution throws', async () => {
vi.mocked(getSpotMarketFromSpotMarketConfig).mockRejectedValueOnce(new Error('rpc failed'));

const { result } = renderHook(() => useMangoSpotMarket(programId, spotMarketConfig));

await waitFor(() => {
expect(getSpotMarketFromSpotMarketConfig).toHaveBeenCalled();
});
expect(result.current).toBeUndefined();
});
});
58 changes: 58 additions & 0 deletions app/features/instruction-program-mango/model/use-mango-market.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
getPerpMarketFromPerpMarketConfig,
getSpotMarketFromSpotMarketConfig,
Market,
PerpMarket,
PerpMarketConfig,
SpotMarketConfig,
} from '@explorer/decoder-mango';
import { useCluster } from '@providers/cluster';
import { PublicKey } from '@solana/web3.js';
import { useState } from 'react';
import useAsyncEffect from 'use-async-effect';

export function useMangoPerpMarket(config: PerpMarketConfig | undefined): PerpMarket | undefined {
const { url } = useCluster();
const [market, setMarket] = useState<PerpMarket | undefined>(undefined);

useAsyncEffect(
async isMounted => {
if (config === undefined) {
if (isMounted()) setMarket(undefined);
return;
}
try {
const resolved = await getPerpMarketFromPerpMarketConfig(url, config);
if (isMounted()) setMarket(resolved);
} catch {
if (isMounted()) setMarket(undefined);
}
},
[url, config],
);

return market;
}

export function useMangoSpotMarket(programId: PublicKey, config: SpotMarketConfig | undefined): Market | undefined {
const { url } = useCluster();
const [market, setMarket] = useState<Market | undefined>(undefined);

useAsyncEffect(
async isMounted => {
if (config === undefined) {
if (isMounted()) setMarket(undefined);
return;
}
try {
const resolved = await getSpotMarketFromSpotMarketConfig(programId, url, config);
if (isMounted()) setMarket(resolved ?? undefined);
} catch {
if (isMounted()) setMarket(undefined);
}
},
[url, programId, config],
);

return market;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { InstructionCard } from '@components/instruction/InstructionCard';
import { SignatureResult, TransactionInstruction } from '@solana/web3.js';

import { InstructionCard } from '../InstructionCard';

export function AddOracleDetailsCard(props: {
ix: TransactionInstruction;
index: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { InstructionCard } from '@components/instruction/InstructionCard';
import { AddPerpMarket } from '@explorer/decoder-mango';
import { SignatureResult, TransactionInstruction } from '@solana/web3.js';
import { formatDuration } from '@utils/date';

import { BaseTable } from '@/app/shared/ui/Table';

import { InstructionCard } from '../InstructionCard';
import { AddPerpMarket } from './types';

export function AddPerpMarketDetailsCard(props: {
ix: TransactionInstruction;
index: number;
Expand All @@ -25,10 +24,12 @@ export function AddPerpMarketDetailsCard(props: {
innerCards={innerCards}
childIndex={childIndex}
>
<BaseTable.Row>
<BaseTable.Cell>Market index</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{info.marketIndex}</BaseTable.Cell>
</BaseTable.Row>
{info.marketIndex !== undefined && (
<BaseTable.Row>
<BaseTable.Cell>Market index</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{info.marketIndex}</BaseTable.Cell>
</BaseTable.Row>
)}
<BaseTable.Row>
<BaseTable.Cell>Maintenance leverage</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{info.maintLeverage}</BaseTable.Cell>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { InstructionCard } from '@components/instruction/InstructionCard';
import { AddSpotMarket, spotMarketFromIndex } from '@explorer/decoder-mango';
import { SignatureResult, TransactionInstruction } from '@solana/web3.js';

import { BaseTable } from '@/app/shared/ui/Table';

import { InstructionCard } from '../InstructionCard';
import { AddSpotMarket, spotMarketFromIndex } from './types';

export function AddSpotMarketDetailsCard(props: {
ix: TransactionInstruction;
index: number;
Expand All @@ -15,6 +14,8 @@ export function AddSpotMarketDetailsCard(props: {
}) {
const { ix, index, result, info, innerCards, childIndex } = props;

const spotMarket = info.marketIndex !== undefined ? spotMarketFromIndex(ix, info.marketIndex) : undefined;

return (
<InstructionCard
ix={ix}
Expand All @@ -24,18 +25,18 @@ export function AddSpotMarketDetailsCard(props: {
innerCards={innerCards}
childIndex={childIndex}
>
{spotMarketFromIndex(ix, info.marketIndex) !== 'UNKNOWN' && (
{spotMarket !== undefined && spotMarket !== 'UNKNOWN' && (
<BaseTable.Row>
<BaseTable.Cell>Market</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">
{spotMarketFromIndex(ix, info.marketIndex)}
</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{spotMarket}</BaseTable.Cell>
</BaseTable.Row>
)}
{info.marketIndex !== undefined && (
<BaseTable.Row>
<BaseTable.Cell>Market index</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{info.marketIndex}</BaseTable.Cell>
</BaseTable.Row>
)}
<BaseTable.Row>
<BaseTable.Cell>Market index</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{info.marketIndex}</BaseTable.Cell>
</BaseTable.Row>
<BaseTable.Row>
<BaseTable.Cell>Maint leverage</BaseTable.Cell>
<BaseTable.Cell className="e-text-right">{info.maintLeverage}</BaseTable.Cell>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Address } from '@components/common/Address';
import { InstructionCard } from '@components/instruction/InstructionCard';
import { CancelPerpOrder, getPerpMarketFromInstruction } from '@explorer/decoder-mango';
import { SignatureResult, TransactionInstruction } from '@solana/web3.js';

import { BaseTable } from '@/app/shared/ui/Table';

import { InstructionCard } from '../InstructionCard';
import { CancelPerpOrder, getPerpMarketFromInstruction } from './types';

export function CancelPerpOrderDetailsCard(props: {
ix: TransactionInstruction;
index: number;
Expand All @@ -15,9 +14,7 @@ export function CancelPerpOrderDetailsCard(props: {
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);
const mangoPerpMarketConfig = getPerpMarketFromInstruction(ix, info.perpMarket);

return (
<InstructionCard
Expand All @@ -31,7 +28,7 @@ export function CancelPerpOrderDetailsCard(props: {
<BaseTable.Row>
<BaseTable.Cell>Mango account</BaseTable.Cell>
<BaseTable.Cell>
<Address pubkey={mangoAccount.pubkey} alignRight link />
<Address pubkey={info.mangoAccount.pubkey} alignRight link />
</BaseTable.Cell>
</BaseTable.Row>

Expand All @@ -45,7 +42,7 @@ export function CancelPerpOrderDetailsCard(props: {
<BaseTable.Row>
<BaseTable.Cell>Perp market address</BaseTable.Cell>
<BaseTable.Cell>
<Address pubkey={perpMarketAccountMeta.pubkey} alignRight link />
<Address pubkey={info.perpMarket.pubkey} alignRight link />
</BaseTable.Cell>
</BaseTable.Row>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Address } from '@components/common/Address';
import { InstructionCard } from '@components/instruction/InstructionCard';
import { CancelSpotOrder, getSpotMarketFromInstruction } from '@explorer/decoder-mango';
import { SignatureResult, TransactionInstruction } from '@solana/web3.js';

import { BaseTable } from '@/app/shared/ui/Table';

import { InstructionCard } from '../InstructionCard';
import { CancelSpotOrder, getSpotMarketFromInstruction } from './types';

export function CancelSpotOrderDetailsCard(props: {
ix: TransactionInstruction;
index: number;
Expand All @@ -15,9 +14,7 @@ export function CancelSpotOrderDetailsCard(props: {
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);
const mangoSpotMarketConfig = getSpotMarketFromInstruction(ix, info.spotMarket);

return (
<InstructionCard
Expand All @@ -31,7 +28,7 @@ export function CancelSpotOrderDetailsCard(props: {
<BaseTable.Row>
<BaseTable.Cell>Mango account</BaseTable.Cell>
<BaseTable.Cell>
<Address pubkey={mangoAccount.pubkey} alignRight link />
<Address pubkey={info.mangoAccount.pubkey} alignRight link />
</BaseTable.Cell>
</BaseTable.Row>

Expand All @@ -45,7 +42,7 @@ export function CancelSpotOrderDetailsCard(props: {
<BaseTable.Row>
<BaseTable.Cell>Spot market address</BaseTable.Cell>
<BaseTable.Cell>
<Address pubkey={spotMarketAccountMeta.pubkey} alignRight link />
<Address pubkey={info.spotMarket.pubkey} alignRight link />
</BaseTable.Cell>
</BaseTable.Row>

Expand Down
Loading
Loading