feat: query accounts

This commit is contained in:
bartosz-lipinski 2021-04-04 23:21:36 -05:00
parent c307827dca
commit 2779947c83
4 changed files with 239 additions and 31 deletions

View File

@ -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 = [
{

View File

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

View File

@ -5,3 +5,4 @@ export * from './notifications';
export * from './utils';
export * from './strings';
export * as shortvec from './shortvec';
export * from './borsh';

View File

@ -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:";
}