mirror of https://github.com/certusone/oyster.git
feat: query accounts
This commit is contained in:
parent
c307827dca
commit
2779947c83
|
@ -12,11 +12,10 @@ import {
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
WRAPPED_SOL_MINT,
|
WRAPPED_SOL_MINT,
|
||||||
} from '../utils/ids';
|
} from '../utils/ids';
|
||||||
|
import { deserializeBorsh } from './../utils/borsh';
|
||||||
import { TokenAccount } from '../models/account';
|
import { TokenAccount } from '../models/account';
|
||||||
import { cache, TokenAccountParser } from '../contexts/accounts';
|
import { cache, TokenAccountParser } from '../contexts/accounts';
|
||||||
// @ts-ignore
|
import { serialize, BinaryReader, Schema, BorshError } from 'borsh';
|
||||||
import * as BufferLayout from 'buffer-layout';
|
|
||||||
import { serialize, deserialize } from 'borsh';
|
|
||||||
|
|
||||||
export function ensureSplAccount(
|
export function ensureSplAccount(
|
||||||
instructions: TransactionInstruction[],
|
instructions: TransactionInstruction[],
|
||||||
|
@ -185,17 +184,74 @@ export function createAssociatedTokenAccountInstruction(
|
||||||
class CreateMetadataArgs {
|
class CreateMetadataArgs {
|
||||||
instruction: number = 0;
|
instruction: number = 0;
|
||||||
allow_duplicates: boolean = false;
|
allow_duplicates: boolean = false;
|
||||||
name: string = '';
|
name: string;
|
||||||
symbol: string = '';
|
symbol: string;
|
||||||
uri: string = '';
|
uri: string;
|
||||||
|
|
||||||
constructor(name: string, symbol: string, uri: string) {
|
constructor(args: { name: string; symbol: string; uri: string }) {
|
||||||
this.name = name;
|
this.name = args.name;
|
||||||
this.symbol = symbol;
|
this.symbol = args.symbol;
|
||||||
this.uri = uri;
|
this.uri = args.uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Metadata {
|
||||||
|
updateAuthority?: PublicKey;
|
||||||
|
mint: PublicKey;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
uri: string;
|
||||||
|
extended?: any;
|
||||||
|
|
||||||
|
constructor(args: {
|
||||||
|
updateAuthority?: Buffer;
|
||||||
|
mint: Buffer;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
uri: string;
|
||||||
|
}) {
|
||||||
|
this.updateAuthority =
|
||||||
|
args.updateAuthority && new PublicKey(args.updateAuthority);
|
||||||
|
this.mint = new PublicKey(args.mint);
|
||||||
|
this.name = args.name;
|
||||||
|
this.symbol = args.symbol;
|
||||||
|
this.uri = args.uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SCHEMA = new Map<any, any>([
|
||||||
|
[
|
||||||
|
CreateMetadataArgs,
|
||||||
|
{
|
||||||
|
kind: 'struct',
|
||||||
|
fields: [
|
||||||
|
['instruction', 'u8'],
|
||||||
|
['allow_duplicates', 'u8'],
|
||||||
|
['name', 'string'],
|
||||||
|
['symbol', 'string'],
|
||||||
|
['uri', 'string'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Metadata,
|
||||||
|
{
|
||||||
|
kind: 'struct',
|
||||||
|
fields: [
|
||||||
|
['allow_duplicates', { kind: 'option', type: 'u8' }],
|
||||||
|
['mint', [32]],
|
||||||
|
['name', 'string'],
|
||||||
|
['symbol', 'string'],
|
||||||
|
['uri', 'string'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const decodeMetadata = (buffer: Buffer) => {
|
||||||
|
return deserializeBorsh(SCHEMA, Metadata, buffer) as Metadata;
|
||||||
|
};
|
||||||
|
|
||||||
export function createMint(
|
export function createMint(
|
||||||
instructions: TransactionInstruction[],
|
instructions: TransactionInstruction[],
|
||||||
payer: PublicKey,
|
payer: PublicKey,
|
||||||
|
@ -261,23 +317,8 @@ export async function createMetadata(
|
||||||
)
|
)
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
const schema = new Map([
|
const value = new CreateMetadataArgs({ name, symbol, uri });
|
||||||
[
|
const data = Buffer.from(serialize(SCHEMA, value));
|
||||||
CreateMetadataArgs,
|
|
||||||
{
|
|
||||||
kind: 'struct',
|
|
||||||
fields: [
|
|
||||||
['instruction', 'u8'],
|
|
||||||
['allow_duplicates', 'u8'],
|
|
||||||
['name', 'string'],
|
|
||||||
['symbol', 'string'],
|
|
||||||
['uri', 'string'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
const value = new CreateMetadataArgs(name, symbol, uri);
|
|
||||||
const data = Buffer.from(serialize(schema, value));
|
|
||||||
|
|
||||||
const keys = [
|
const keys = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { serialize, BinaryReader, Schema, BorshError } from 'borsh';
|
||||||
|
|
||||||
|
function capitalizeFirstLetter(string: string): string {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserializeField(
|
||||||
|
schema: Schema,
|
||||||
|
fieldName: string,
|
||||||
|
fieldType: any,
|
||||||
|
reader: BinaryReader,
|
||||||
|
optional: boolean,
|
||||||
|
): any {
|
||||||
|
try {
|
||||||
|
if (typeof fieldType === 'string') {
|
||||||
|
return (reader as any)[`read${capitalizeFirstLetter(fieldType)}`]();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldType instanceof Array) {
|
||||||
|
if (typeof fieldType[0] === 'number') {
|
||||||
|
return reader.readFixedArray(fieldType[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.readArray(() =>
|
||||||
|
deserializeField(schema, fieldName, fieldType[0], reader, false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldType.kind === 'option') {
|
||||||
|
const option = reader.readU8();
|
||||||
|
if (option) {
|
||||||
|
return deserializeField(
|
||||||
|
schema,
|
||||||
|
fieldName,
|
||||||
|
fieldType.type,
|
||||||
|
reader,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserializeStruct(schema, fieldType, reader);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BorshError) {
|
||||||
|
error.addToFieldPath(fieldName);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserializeStruct(
|
||||||
|
schema: Schema,
|
||||||
|
classType: any,
|
||||||
|
reader: BinaryReader,
|
||||||
|
) {
|
||||||
|
const structSchema = schema.get(classType);
|
||||||
|
if (!structSchema) {
|
||||||
|
throw new BorshError(`Class ${classType.name} is missing in schema`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structSchema.kind === 'struct') {
|
||||||
|
const result: any = {};
|
||||||
|
for (const [fieldName, fieldType] of schema.get(classType).fields) {
|
||||||
|
result[fieldName] = deserializeField(
|
||||||
|
schema,
|
||||||
|
fieldName,
|
||||||
|
fieldType,
|
||||||
|
reader,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new classType(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structSchema.kind === 'enum') {
|
||||||
|
const idx = reader.readU8();
|
||||||
|
if (idx >= structSchema.values.length) {
|
||||||
|
throw new BorshError(`Enum index: ${idx} is out of range`);
|
||||||
|
}
|
||||||
|
const [fieldName, fieldType] = structSchema.values[idx];
|
||||||
|
const fieldValue = deserializeField(
|
||||||
|
schema,
|
||||||
|
fieldName,
|
||||||
|
fieldType,
|
||||||
|
reader,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return new classType({ [fieldName]: fieldValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BorshError(
|
||||||
|
`Unexpected schema kind: ${structSchema.kind} for ${classType.constructor.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes object from bytes using schema.
|
||||||
|
export function deserializeBorsh(
|
||||||
|
schema: Schema,
|
||||||
|
classType: any,
|
||||||
|
buffer: Buffer,
|
||||||
|
): any {
|
||||||
|
const reader = new BinaryReader(buffer);
|
||||||
|
return deserializeStruct(schema, classType, reader);
|
||||||
|
}
|
|
@ -5,3 +5,4 @@ export * from './notifications';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './strings';
|
export * from './strings';
|
||||||
export * as shortvec from './shortvec';
|
export * as shortvec from './shortvec';
|
||||||
|
export * from './borsh';
|
||||||
|
|
|
@ -1,22 +1,70 @@
|
||||||
import { EventEmitter, useConnection } from '@oyster/common';
|
import { EventEmitter, programIds, useConnection, decodeMetadata, Metadata, getMultipleAccounts, cache, MintParser, ParsedAccount } from '@oyster/common';
|
||||||
|
import { MintInfo } from '@solana/spl-token';
|
||||||
|
import BN from 'bn.js';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { MarketsContextState } from './market';
|
import { MarketsContextState } from './market';
|
||||||
|
|
||||||
export interface VinciAccountsContextState {
|
export interface VinciAccountsContextState {
|
||||||
|
metaAccounts: Metadata[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const VinciAccountsContext = React.createContext<VinciAccountsContextState | null>(
|
const VinciAccountsContext = React.createContext<VinciAccountsContextState | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
export function VinciAccountsProvider({ children = null as any }) {
|
export function VinciAccountsProvider({ children = null as any }) {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
|
const [metaAccounts, setMetaAccounts] = useState<Metadata[]>([]);
|
||||||
|
|
||||||
// TODO: query for metadata accounts and associated jsons
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const metadataAccounts = await connection.getProgramAccounts(programIds().metadata);
|
||||||
|
|
||||||
|
const mintToMetadata = new Map<string, Metadata>();
|
||||||
|
const extendedMetadataFetch = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
metadataAccounts.forEach(meta => {
|
||||||
|
try{
|
||||||
|
const metadata = decodeMetadata(meta.account.data);
|
||||||
|
if(isValidHttpUrl(metadata.uri)) {
|
||||||
|
mintToMetadata.set(metadata.mint.toBase58(), metadata);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
// add type as first byte for easier deserialization
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mints = await getMultipleAccounts(connection, [...mintToMetadata.keys()], 'single');
|
||||||
|
mints.keys.forEach((key, index) => {
|
||||||
|
const mintAccount = mints.array[index];
|
||||||
|
const mint = cache.add(key, mintAccount, MintParser) as ParsedAccount<MintInfo>;
|
||||||
|
if(mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) {
|
||||||
|
// naive not NFT check
|
||||||
|
mintToMetadata.delete(key);
|
||||||
|
} else {
|
||||||
|
const metadata = mintToMetadata.get(key);
|
||||||
|
if(metadata && metadata.uri) {
|
||||||
|
extendedMetadataFetch.set(key, fetch(metadata.uri).catch(() => {
|
||||||
|
mintToMetadata.delete(key);
|
||||||
|
return undefined;
|
||||||
|
}).then(_ => {
|
||||||
|
metadata.extended = _;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([...extendedMetadataFetch.values()]);
|
||||||
|
|
||||||
|
setMetaAccounts([...mintToMetadata.values()]);
|
||||||
|
|
||||||
|
console.log([...mintToMetadata.values()]);
|
||||||
|
})();
|
||||||
|
}, [connection, setMetaAccounts])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VinciAccountsContext.Provider value={{ }}>
|
<VinciAccountsContext.Provider value={{ metaAccounts }}>
|
||||||
{children}
|
{children}
|
||||||
</VinciAccountsContext.Provider>
|
</VinciAccountsContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -26,3 +74,15 @@ export const useCoingecko = () => {
|
||||||
const context = useContext(VinciAccountsContext);
|
const context = useContext(VinciAccountsContext);
|
||||||
return context as VinciAccountsContextState;
|
return context as VinciAccountsContextState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isValidHttpUrl(text: string) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(text);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue