Merge branch 'main' into mobile-tweaks
This commit is contained in:
commit
f31425582e
|
@ -21,14 +21,6 @@ export interface MarketInfo {
|
|||
baseLabel?: string
|
||||
}
|
||||
|
||||
export interface CustomMarketInfo {
|
||||
address: string
|
||||
name: string
|
||||
programId: string
|
||||
quoteLabel?: string
|
||||
baseLabel?: string
|
||||
}
|
||||
|
||||
export interface TokenAccount {
|
||||
pubkey: PublicKey
|
||||
account: AccountInfo<Buffer> | null
|
||||
|
@ -132,3 +124,19 @@ export interface PerpTriggerOrder {
|
|||
triggerCondition: 'above' | 'below'
|
||||
triggerPrice: number
|
||||
}
|
||||
|
||||
export type StringPublicKey = string
|
||||
|
||||
export interface PromiseFulfilledResult<T> {
|
||||
status: 'fulfilled'
|
||||
value: T
|
||||
}
|
||||
|
||||
export interface PromiseRejectedResult {
|
||||
status: 'rejected'
|
||||
reason: any
|
||||
}
|
||||
|
||||
export type PromiseSettledResult<T> =
|
||||
| PromiseFulfilledResult<T>
|
||||
| PromiseRejectedResult
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
TrBody,
|
||||
Td,
|
||||
TableDateDisplay,
|
||||
Row,
|
||||
} from '../TableElements'
|
||||
import { LinkButton } from '../Button'
|
||||
import { useSortableData } from '../../hooks/useSortableData'
|
||||
|
@ -28,6 +29,9 @@ import Tooltip from '../Tooltip'
|
|||
import { exportDataToCSV } from '../../utils/export'
|
||||
import { notify } from '../../utils/notifications'
|
||||
import Button from '../Button'
|
||||
import { useViewport } from '../../hooks/useViewport'
|
||||
import { breakpoints } from '.././TradePageGrid'
|
||||
import MobileTableHeader from 'components/mobile/MobileTableHeader'
|
||||
|
||||
const historyViews = [
|
||||
{ label: 'Trades', key: 'Trades' },
|
||||
|
@ -429,6 +433,8 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
const HistoryTable = ({ history, view }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
const filteredHistory = useMemo(() => {
|
||||
return history?.length
|
||||
? history
|
||||
|
@ -513,7 +519,7 @@ const HistoryTable = ({ history, view }) => {
|
|||
</Button>
|
||||
</div>
|
||||
{items.length ? (
|
||||
<>
|
||||
!isMobile ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
|
@ -592,12 +598,17 @@ const HistoryTable = ({ history, view }) => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{items.map((activity_details: any) => {
|
||||
const {
|
||||
signature,
|
||||
block_datetime,
|
||||
symbol,
|
||||
quantity,
|
||||
usd_equivalent,
|
||||
} = activity_details
|
||||
return (
|
||||
<TrBody key={activity_details.signature}>
|
||||
<TrBody key={signature}>
|
||||
<Td>
|
||||
<TableDateDisplay
|
||||
date={activity_details.block_datetime}
|
||||
/>
|
||||
<TableDateDisplay date={block_datetime} />
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
|
@ -605,18 +616,18 @@ const HistoryTable = ({ history, view }) => {
|
|||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${activity_details.symbol.toLowerCase()}.svg`}
|
||||
src={`/assets/icons/${symbol.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{activity_details.symbol}
|
||||
{symbol}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{activity_details.quantity.toLocaleString()}</Td>
|
||||
<Td>{formatUsdValue(activity_details.usd_equivalent)}</Td>
|
||||
<Td>{quantity.toLocaleString()}</Td>
|
||||
<Td>{formatUsdValue(usd_equivalent)}</Td>
|
||||
<Td>
|
||||
<a
|
||||
className="default-transition flex items-center justify-end text-th-fgd-2"
|
||||
href={`https://explorer.solana.com/tx/${activity_details.signature}`}
|
||||
href={`https://explorer.solana.com/tx/${signature}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
@ -629,7 +640,50 @@ const HistoryTable = ({ history, view }) => {
|
|||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
) : (
|
||||
<div className="mb-4 border-b border-th-bkg-4">
|
||||
<MobileTableHeader
|
||||
colOneHeader={t('date')}
|
||||
colTwoHeader={t('asset')}
|
||||
/>
|
||||
{items.map((activity_details: any) => {
|
||||
const {
|
||||
signature,
|
||||
block_datetime,
|
||||
symbol,
|
||||
quantity,
|
||||
usd_equivalent,
|
||||
} = activity_details
|
||||
return (
|
||||
<Row key={signature}>
|
||||
<div className="flex w-full items-center justify-between text-th-fgd-1">
|
||||
<div className="text-left">
|
||||
<TableDateDisplay date={block_datetime} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="text-right">
|
||||
<p className="mb-0 text-th-fgd-2">
|
||||
{`${quantity.toLocaleString()} ${symbol}`}
|
||||
</p>
|
||||
<p className="mb-0 text-xs text-th-fgd-3">
|
||||
{formatUsdValue(usd_equivalent)}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
className="default-transition flex items-center justify-end text-th-fgd-2"
|
||||
href={`https://explorer.solana.com/tx/${signature}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLinkIcon className={`ml-3.5 h-5 w-5`} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
|
||||
{t('history-empty')}
|
||||
|
|
10
package.json
10
package.json
|
@ -21,16 +21,12 @@
|
|||
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
|
||||
"@heroicons/react": "^1.0.0",
|
||||
"@jup-ag/react-hook": "^1.0.0-beta.22",
|
||||
"@nfteyez/sol-rayz": "^0.10.2",
|
||||
"@project-serum/serum": "0.13.55",
|
||||
"@project-serum/sol-wallet-adapter": "0.2.0",
|
||||
"@sentry/react": "^6.19.2",
|
||||
"@sentry/tracing": "^6.19.2",
|
||||
"@solana/wallet-adapter-base": "^0.9.5",
|
||||
"@solana/wallet-adapter-huobi": "^0.1.0",
|
||||
"@solana/wallet-adapter-react": "^0.15.4",
|
||||
"@solana/wallet-adapter-wallets": "^0.16.1",
|
||||
"@solana/web3.js": "^1.36.0",
|
||||
"@solflare-wallet/pfp": "^0.0.6",
|
||||
"@tippyjs/react": "^4.2.5",
|
||||
"big.js": "^6.1.1",
|
||||
|
@ -63,6 +59,11 @@
|
|||
"recharts": "^2.1.9",
|
||||
"zustand": "^3.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@project-serum/serum": ">=0.13.55",
|
||||
"@solana/web3.js": "^1.36.0",
|
||||
"borsh": "^0.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^12.1.0",
|
||||
"@svgr/webpack": "^6.1.2",
|
||||
|
@ -78,7 +79,6 @@
|
|||
"eslint-plugin-react": "^7.26.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^7.0.4",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"lint-staged": "^12.3.6",
|
||||
"postcss": "^8.4.7",
|
||||
"prettier": "^2.0.2",
|
||||
|
|
|
@ -41,8 +41,8 @@ import { decodeBook } from '../hooks/useHydrateStore'
|
|||
import { IOrderLineAdapter } from '../public/charting_library/charting_library'
|
||||
import { Wallet } from '@solana/wallet-adapter-react'
|
||||
import { coingeckoIds } from 'utils/tokens'
|
||||
import { getParsedNftAccountsByOwner } from '@nfteyez/sol-rayz'
|
||||
import { getTokenAccountsByMint } from 'utils/tokens'
|
||||
import { getParsedNftAccountsByOwner } from 'utils/getParsedNftAccountsByOwner'
|
||||
|
||||
export const ENDPOINTS: EndpointInfo[] = [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
AccountInfo,
|
||||
ParsedAccountData,
|
||||
} from '@solana/web3.js'
|
||||
import chunks from 'lodash/chunk'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import {
|
||||
StringPublicKey,
|
||||
PromiseSettledResult,
|
||||
PromiseFulfilledResult,
|
||||
} from '../@types/types'
|
||||
import { TOKEN_PROGRAM_ID } from './tokens'
|
||||
import { Metadata, METADATA_SCHEMA } from './metaplex'
|
||||
import { isValidSolanaAddress } from 'utils'
|
||||
import { deserializeUnchecked } from 'borsh'
|
||||
|
||||
const METADATA_PREFIX = 'metadata'
|
||||
const METADATA_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
|
||||
|
||||
const metaProgamPublicKey = new PublicKey(METADATA_PROGRAM)
|
||||
const metaProgamPublicKeyBuffer = metaProgamPublicKey.toBuffer()
|
||||
// Create UTF-8 bytes Buffer from string
|
||||
// similar to Buffer.from(METADATA_PREFIX) but should work by default in node.js/browser
|
||||
const metaProgamPrefixBuffer = new TextEncoder().encode(METADATA_PREFIX)
|
||||
|
||||
export const decodeTokenMetadata = async (buffer: Buffer) =>
|
||||
deserializeUnchecked(METADATA_SCHEMA, Metadata, buffer)
|
||||
|
||||
/**
|
||||
* Get Addresses of Metadata account assosiated with Mint Token
|
||||
*/
|
||||
export async function getSolanaMetadataAddress(tokenMint: PublicKey) {
|
||||
const metaProgamPublicKey = new PublicKey(METADATA_PROGRAM)
|
||||
return (
|
||||
await PublicKey.findProgramAddress(
|
||||
[metaProgamPrefixBuffer, metaProgamPublicKeyBuffer, tokenMint.toBuffer()],
|
||||
metaProgamPublicKey
|
||||
)
|
||||
)[0]
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
* Wallet public address
|
||||
*/
|
||||
publicAddress: StringPublicKey
|
||||
/**
|
||||
* Optionally provide your own connection object.
|
||||
* Otherwise createConnectionConfig() will be used
|
||||
*/
|
||||
connection?: Connection
|
||||
/**
|
||||
* Remove possible rust's empty string symbols `\x00` from the values,
|
||||
* which is very common issue.
|
||||
* Default is true
|
||||
*/
|
||||
sanitize?: boolean
|
||||
/**
|
||||
* Convert all PublicKey objects to string versions.
|
||||
* Default is true
|
||||
*/
|
||||
stringifyPubKeys?: boolean
|
||||
/**
|
||||
* Sort tokens by Update Authority (read by Collection)
|
||||
* Default is true
|
||||
*/
|
||||
sort?: boolean
|
||||
/**
|
||||
* Limit response by this number
|
||||
* by default response limited by 5000 NFTs.
|
||||
*/
|
||||
limit?: number
|
||||
}
|
||||
|
||||
enum sortKeys {
|
||||
updateAuthority = 'updateAuthority',
|
||||
}
|
||||
|
||||
export const getParsedNftAccountsByOwner = async ({
|
||||
publicAddress,
|
||||
connection,
|
||||
sanitize = true,
|
||||
stringifyPubKeys = true,
|
||||
sort = true,
|
||||
limit = 5000,
|
||||
}: Options) => {
|
||||
const isValidAddress = isValidSolanaAddress(publicAddress)
|
||||
if (!isValidAddress || !connection) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get all accounts owned by user
|
||||
// and created by SPL Token Program
|
||||
// this will include all NFTs, Coins, Tokens, etc.
|
||||
const { value: splAccounts } = await connection.getParsedTokenAccountsByOwner(
|
||||
new PublicKey(publicAddress),
|
||||
{
|
||||
programId: new PublicKey(TOKEN_PROGRAM_ID),
|
||||
}
|
||||
)
|
||||
|
||||
// We assume NFT is SPL token with decimals === 0 and amount at least 1
|
||||
// At this point we filter out other SPL tokens, like coins e.g.
|
||||
// Unfortunately, this method will also remove NFTы created before Metaplex NFT Standard
|
||||
// like Solarians e.g., so you need to check wallets for them in separate call if you wish
|
||||
const nftAccounts = splAccounts
|
||||
.filter((t) => {
|
||||
const amount = t.account?.data?.parsed?.info?.tokenAmount?.uiAmount
|
||||
const decimals = t.account?.data?.parsed?.info?.tokenAmount?.decimals
|
||||
return decimals === 0 && amount >= 1
|
||||
})
|
||||
.map((t) => {
|
||||
const address = t.account?.data?.parsed?.info?.mint
|
||||
return new PublicKey(address)
|
||||
})
|
||||
|
||||
// if user have tons of NFTs return first N
|
||||
const accountsSlice = nftAccounts?.slice(0, limit)
|
||||
|
||||
// Get Addresses of Metadata Account assosiated with Mint Token
|
||||
// This info can be deterministically calculated by Associated Token Program
|
||||
// available in @solana/web3.js
|
||||
const metadataAcountsAddressPromises = await Promise.allSettled(
|
||||
accountsSlice.map(getSolanaMetadataAddress)
|
||||
)
|
||||
|
||||
const metadataAccounts = metadataAcountsAddressPromises
|
||||
.filter(onlySuccessfullPromises)
|
||||
.map((p) => (p as PromiseFulfilledResult<PublicKey>).value)
|
||||
|
||||
// Fetch Found Metadata Account data by chunks
|
||||
const metaAccountsRawPromises: PromiseSettledResult<
|
||||
(AccountInfo<Buffer | ParsedAccountData> | null)[]
|
||||
>[] = await Promise.allSettled(
|
||||
chunks(metadataAccounts, 99).map((chunk) =>
|
||||
connection.getMultipleAccountsInfo(chunk as PublicKey[])
|
||||
)
|
||||
)
|
||||
|
||||
const accountsRawMeta = metaAccountsRawPromises
|
||||
.filter(({ status }) => status === 'fulfilled')
|
||||
.flatMap((p) => (p as PromiseFulfilledResult<unknown>).value)
|
||||
|
||||
// There is no reason to continue processing
|
||||
// if Mints doesn't have associated metadata account. just return []
|
||||
if (!accountsRawMeta?.length || accountsRawMeta?.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Decode data from Buffer to readable objects
|
||||
const accountsDecodedMeta = await Promise.allSettled(
|
||||
accountsRawMeta.map((accountInfo) =>
|
||||
decodeTokenMetadata((accountInfo as AccountInfo<Buffer>)?.data)
|
||||
)
|
||||
)
|
||||
|
||||
const accountsFiltered = accountsDecodedMeta
|
||||
.filter(onlySuccessfullPromises)
|
||||
.filter(onlyNftsWithMetadata)
|
||||
.map((p) => {
|
||||
const { value } = p as PromiseFulfilledResult<Metadata>
|
||||
return sanitize ? sanitizeTokenMeta(value) : value
|
||||
})
|
||||
.map((token) => (stringifyPubKeys ? publicKeyToString(token) : token))
|
||||
|
||||
// sort accounts if sort is true & updateAuthority stringified
|
||||
if (stringifyPubKeys && sort) {
|
||||
const accountsSorted = orderBy(
|
||||
accountsFiltered,
|
||||
[sortKeys.updateAuthority],
|
||||
['asc']
|
||||
)
|
||||
|
||||
return accountsSorted
|
||||
}
|
||||
// otherwise return unsorted
|
||||
return accountsFiltered
|
||||
}
|
||||
|
||||
const sanitizeTokenMeta = (tokenData: Metadata) => ({
|
||||
...tokenData,
|
||||
data: {
|
||||
...tokenData?.data,
|
||||
name: sanitizeMetaStrings(tokenData?.data?.name),
|
||||
symbol: sanitizeMetaStrings(tokenData?.data?.symbol),
|
||||
uri: sanitizeMetaStrings(tokenData?.data?.uri),
|
||||
},
|
||||
})
|
||||
|
||||
// Convert all PublicKey to string
|
||||
const publicKeyToString = (tokenData: Metadata) => ({
|
||||
...tokenData,
|
||||
mint: tokenData?.mint?.toString?.(),
|
||||
updateAuthority: tokenData?.updateAuthority?.toString?.(),
|
||||
data: {
|
||||
...tokenData?.data,
|
||||
creators: tokenData?.data?.creators?.map((c: any) => ({
|
||||
...c,
|
||||
address: new PublicKey(c?.address)?.toString?.(),
|
||||
})),
|
||||
},
|
||||
})
|
||||
|
||||
// Remove all empty space, new line, etc. symbols
|
||||
// In some reason such symbols parsed back from Buffer looks weird
|
||||
// like "\x0000" instead of usual spaces.
|
||||
export const sanitizeMetaStrings = (metaString: string) =>
|
||||
metaString.replace(/\0/g, '')
|
||||
|
||||
const onlySuccessfullPromises = (
|
||||
result: PromiseSettledResult<unknown>
|
||||
): boolean => result && result.status === 'fulfilled'
|
||||
|
||||
// Remove any NFT Metadata Account which doesn't have uri field
|
||||
// We can assume such NFTs are broken or invalid.
|
||||
const onlyNftsWithMetadata = (t: PromiseSettledResult<Metadata>) => {
|
||||
const uri = (
|
||||
t as PromiseFulfilledResult<Metadata>
|
||||
).value.data?.uri?.replace?.(/\0/g, '')
|
||||
return uri !== '' && uri !== undefined
|
||||
}
|
|
@ -331,3 +331,18 @@ export function patchInternalMarketName(marketName: string) {
|
|||
export function roundPerpSize(size: number, symbol: string) {
|
||||
return new BigNumber(size).abs().toFormat(perpContractPrecision[symbol])
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed address is Solana address
|
||||
*/
|
||||
export const isValidSolanaAddress = (address: string) => {
|
||||
try {
|
||||
// this fn accepts Base58 character
|
||||
// and if it pass we suppose Solana address is valid
|
||||
new PublicKey(address)
|
||||
return true
|
||||
} catch (error) {
|
||||
// Non-base58 character or can't be used as Solana address
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import { StringPublicKey } from '../@types/types'
|
||||
|
||||
const METADATA_PREFIX = 'metadata'
|
||||
const METADATA_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
|
||||
|
||||
export enum MetadataKey {
|
||||
Uninitialized = 0,
|
||||
MetadataV1 = 4,
|
||||
EditionV1 = 1,
|
||||
MasterEditionV1 = 2,
|
||||
MasterEditionV2 = 6,
|
||||
EditionMarker = 7,
|
||||
}
|
||||
|
||||
export class Creator {
|
||||
address: StringPublicKey
|
||||
verified: boolean
|
||||
share: number
|
||||
|
||||
constructor(args: {
|
||||
address: StringPublicKey
|
||||
verified: boolean
|
||||
share: number
|
||||
}) {
|
||||
this.address = args.address
|
||||
this.verified = args.verified
|
||||
this.share = args.share
|
||||
}
|
||||
}
|
||||
|
||||
class Metadata {
|
||||
key: MetadataKey
|
||||
updateAuthority: StringPublicKey
|
||||
mint: StringPublicKey
|
||||
data: Data
|
||||
primarySaleHappened: boolean
|
||||
isMutable: boolean
|
||||
editionNonce: number | null
|
||||
|
||||
// set lazy
|
||||
masterEdition?: StringPublicKey
|
||||
edition?: StringPublicKey
|
||||
|
||||
constructor(args: {
|
||||
updateAuthority: StringPublicKey
|
||||
mint: StringPublicKey
|
||||
data: Data
|
||||
primarySaleHappened: boolean
|
||||
isMutable: boolean
|
||||
editionNonce: number | null
|
||||
}) {
|
||||
this.key = MetadataKey.MetadataV1
|
||||
this.updateAuthority = args.updateAuthority
|
||||
this.mint = args.mint
|
||||
this.data = args.data
|
||||
this.primarySaleHappened = args.primarySaleHappened
|
||||
this.isMutable = args.isMutable
|
||||
this.editionNonce = args.editionNonce ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export class Data {
|
||||
name: string
|
||||
symbol: string
|
||||
uri: string
|
||||
sellerFeeBasisPoints: number
|
||||
creators: Creator[] | null
|
||||
constructor(args: {
|
||||
name: string
|
||||
symbol: string
|
||||
uri: string
|
||||
sellerFeeBasisPoints: number
|
||||
creators: Creator[] | null
|
||||
}) {
|
||||
this.name = args.name
|
||||
this.symbol = args.symbol
|
||||
this.uri = args.uri
|
||||
this.sellerFeeBasisPoints = args.sellerFeeBasisPoints
|
||||
this.creators = args.creators
|
||||
}
|
||||
}
|
||||
|
||||
const METADATA_SCHEMA = new Map<any, any>([
|
||||
[
|
||||
Data,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['name', 'string'],
|
||||
['symbol', 'string'],
|
||||
['uri', 'string'],
|
||||
['sellerFeeBasisPoints', 'u16'],
|
||||
['creators', { kind: 'option', type: [Creator] }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Metadata,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['key', 'u8'],
|
||||
['updateAuthority', 'pubkey'],
|
||||
['mint', 'pubkey'],
|
||||
['data', Data],
|
||||
['primarySaleHappened', 'u8'], // bool
|
||||
['isMutable', 'u8'], // bool
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Creator,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['address', 'pubkey'],
|
||||
['verified', 'u8'],
|
||||
['share', 'u8'],
|
||||
],
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
export { METADATA_SCHEMA, METADATA_PREFIX, METADATA_PROGRAM, Metadata }
|
Loading…
Reference in New Issue