move fns from @nfteyez/sol-rayz into app

This commit is contained in:
tjs 2022-05-17 13:07:33 -04:00
parent a53753a706
commit f2967dfc6b
7 changed files with 438 additions and 2234 deletions

View File

@ -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

View File

@ -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",

View File

@ -40,8 +40,8 @@ import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp'
import { decodeBook } from '../hooks/useHydrateStore'
import { IOrderLineAdapter } from '../public/charting_library/charting_library'
import { Wallet } from '@solana/wallet-adapter-react'
import { getParsedNftAccountsByOwner } from '@nfteyez/sol-rayz'
import { getTokenAccountsByMint } from 'utils/tokens'
import { getParsedNftAccountsByOwner } from 'utils/getParsedNftAccountsByOwner'
export const ENDPOINTS: EndpointInfo[] = [
{

View File

@ -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
}

View File

@ -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
}
}

124
utils/metaplex.ts Normal file
View File

@ -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 }

2274
yarn.lock

File diff suppressed because it is too large Load Diff