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,
|
||||
WRAPPED_SOL_MINT,
|
||||
} from '../utils/ids';
|
||||
import { deserializeBorsh } from './../utils/borsh';
|
||||
import { TokenAccount } from '../models/account';
|
||||
import { cache, TokenAccountParser } from '../contexts/accounts';
|
||||
// @ts-ignore
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
import { serialize, deserialize } from 'borsh';
|
||||
import { serialize, BinaryReader, Schema, BorshError } from 'borsh';
|
||||
|
||||
export function ensureSplAccount(
|
||||
instructions: TransactionInstruction[],
|
||||
|
@ -185,17 +184,74 @@ export function createAssociatedTokenAccountInstruction(
|
|||
class CreateMetadataArgs {
|
||||
instruction: number = 0;
|
||||
allow_duplicates: boolean = false;
|
||||
name: string = '';
|
||||
symbol: string = '';
|
||||
uri: string = '';
|
||||
name: string;
|
||||
symbol: string;
|
||||
uri: string;
|
||||
|
||||
constructor(name: string, symbol: string, uri: string) {
|
||||
this.name = name;
|
||||
this.symbol = symbol;
|
||||
this.uri = uri;
|
||||
constructor(args: { name: string; symbol: string; uri: string }) {
|
||||
this.name = args.name;
|
||||
this.symbol = args.symbol;
|
||||
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(
|
||||
instructions: TransactionInstruction[],
|
||||
payer: PublicKey,
|
||||
|
@ -261,23 +317,8 @@ export async function createMetadata(
|
|||
)
|
||||
)[0];
|
||||
|
||||
const schema = new Map([
|
||||
[
|
||||
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 value = new CreateMetadataArgs({ name, symbol, uri });
|
||||
const data = Buffer.from(serialize(SCHEMA, value));
|
||||
|
||||
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 './strings';
|
||||
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 { MarketsContextState } from './market';
|
||||
|
||||
export interface VinciAccountsContextState {
|
||||
|
||||
metaAccounts: Metadata[];
|
||||
}
|
||||
|
||||
const VinciAccountsContext = React.createContext<VinciAccountsContextState | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function VinciAccountsProvider({ children = null as any }) {
|
||||
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 (
|
||||
<VinciAccountsContext.Provider value={{ }}>
|
||||
<VinciAccountsContext.Provider value={{ metaAccounts }}>
|
||||
{children}
|
||||
</VinciAccountsContext.Provider>
|
||||
);
|
||||
|
@ -26,3 +74,15 @@ export const useCoingecko = () => {
|
|||
const context = useContext(VinciAccountsContext);
|
||||
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