Compare commits
2 Commits
de5e78f75f
...
5a0cfa413f
Author | SHA1 | Date |
---|---|---|
Maximilian Schneider | 5a0cfa413f | |
Maximilian Schneider | 1c7749bdfc |
|
@ -59,6 +59,7 @@
|
|||
"@project-serum/common": "^0.0.1-beta.3",
|
||||
"@project-serum/serum": "^0.13.20",
|
||||
"@project-serum/sol-wallet-adapter": "^0.1.4",
|
||||
"@pythnetwork/client": "^2.7.2",
|
||||
"@solana/spl-token": "0.0.13",
|
||||
"@solana/web3.js": "^0.95.0",
|
||||
"bn.js": "^5.1.2",
|
||||
|
|
1541
src/client.ts
1541
src/client.ts
File diff suppressed because it is too large
Load Diff
|
@ -244,4 +244,4 @@
|
|||
"USDC": "9q4p8UFxphSipGL3TGku8byTijgk4koTMwhBMV4QKvjw"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
16
src/index.ts
16
src/index.ts
|
@ -1,7 +1,15 @@
|
|||
import IDS from './ids.json';
|
||||
export { IDS }
|
||||
export { MangoClient, MangoGroup, MarginAccount, tokenToDecimals } from './client';
|
||||
export { MangoIndexLayout, MarginAccountLayout, MangoGroupLayout } from './layout';
|
||||
export { IDS };
|
||||
export {
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
MarginAccount,
|
||||
tokenToDecimals,
|
||||
} from './client';
|
||||
export {
|
||||
MangoIndexLayout,
|
||||
MarginAccountLayout,
|
||||
MangoGroupLayout,
|
||||
} from './layout';
|
||||
export * from './layout';
|
||||
export * from './utils';
|
||||
|
||||
|
|
|
@ -313,7 +313,7 @@ export function makeAddMarginAccountInfoInstruction(
|
|||
mangoGroup: PublicKey,
|
||||
marginAccount: PublicKey,
|
||||
owner: PublicKey,
|
||||
info: string
|
||||
info: string,
|
||||
): TransactionInstruction {
|
||||
const keys = [
|
||||
{ isSigner: false, isWritable: true, pubkey: mangoGroup },
|
||||
|
@ -321,12 +321,16 @@ export function makeAddMarginAccountInfoInstruction(
|
|||
{ isSigner: true, isWritable: false, pubkey: owner },
|
||||
];
|
||||
// TODO convert info into a 32 byte utf encoded byte array
|
||||
const encoded = Buffer.from(info)
|
||||
const encoded = Buffer.from(info);
|
||||
if (encoded.length > INFO_LEN) {
|
||||
throw new Error("info string too long. Must be less than or equal to 32 bytes")
|
||||
throw new Error(
|
||||
'info string too long. Must be less than or equal to 32 bytes',
|
||||
);
|
||||
}
|
||||
const infoArray = new Uint8Array(encoded, 0, INFO_LEN)
|
||||
const data = encodeMangoInstruction({ AddMarginAccountInfo: { info: infoArray } });
|
||||
const infoArray = new Uint8Array(encoded, 0, INFO_LEN);
|
||||
const data = encodeMangoInstruction({
|
||||
AddMarginAccountInfo: { info: infoArray },
|
||||
});
|
||||
|
||||
return new TransactionInstruction({ keys, data, programId });
|
||||
}
|
||||
}
|
||||
|
|
205
src/layout.ts
205
src/layout.ts
|
@ -1,14 +1,26 @@
|
|||
import { bits, BitStructure, Blob, Layout, seq, struct, u32, u8, u16, UInt, union } from 'buffer-layout';
|
||||
import {
|
||||
bits,
|
||||
BitStructure,
|
||||
Blob,
|
||||
Layout,
|
||||
seq,
|
||||
struct,
|
||||
u32,
|
||||
u8,
|
||||
u16,
|
||||
UInt,
|
||||
union,
|
||||
} from 'buffer-layout';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
|
||||
export const NUM_TOKENS = 5;
|
||||
export const NUM_MARKETS = NUM_TOKENS - 1;
|
||||
export const MANGO_GROUP_PADDING = 8 - (NUM_TOKENS + NUM_MARKETS) % 8;
|
||||
export const MAX_RATE = 1.5
|
||||
export const OPTIMAL_UTIL = 0.7
|
||||
export const OPTIMAL_RATE = 0.06
|
||||
export const INFO_LEN = 32
|
||||
export const MANGO_GROUP_PADDING = 8 - ((NUM_TOKENS + NUM_MARKETS) % 8);
|
||||
export const MAX_RATE = 1.5;
|
||||
export const OPTIMAL_UTIL = 0.7;
|
||||
export const OPTIMAL_RATE = 0.06;
|
||||
export const INFO_LEN = 32;
|
||||
|
||||
class PublicKeyLayout extends Blob {
|
||||
constructor(property) {
|
||||
|
@ -24,7 +36,7 @@ class PublicKeyLayout extends Blob {
|
|||
}
|
||||
}
|
||||
|
||||
export function publicKeyLayout(property = "") {
|
||||
export function publicKeyLayout(property = '') {
|
||||
return new PublicKeyLayout(property);
|
||||
}
|
||||
|
||||
|
@ -32,7 +44,7 @@ class BNLayout extends Blob {
|
|||
constructor(number: number, property) {
|
||||
super(number, property);
|
||||
// restore prototype chain
|
||||
Object.setPrototypeOf(this, new.target.prototype)
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
|
||||
decode(b, offset) {
|
||||
|
@ -44,15 +56,14 @@ class BNLayout extends Blob {
|
|||
}
|
||||
}
|
||||
|
||||
export function u64(property = "") {
|
||||
export function u64(property = '') {
|
||||
return new BNLayout(8, property);
|
||||
}
|
||||
|
||||
export function u128(property = "") {
|
||||
export function u128(property = '') {
|
||||
return new BNLayout(16, property);
|
||||
}
|
||||
|
||||
|
||||
class U64F64Layout extends Blob {
|
||||
constructor(property: string) {
|
||||
super(16, property);
|
||||
|
@ -70,8 +81,8 @@ class U64F64Layout extends Blob {
|
|||
}
|
||||
}
|
||||
|
||||
export function U64F64(property = "") {
|
||||
return new U64F64Layout(property)
|
||||
export function U64F64(property = '') {
|
||||
return new U64F64Layout(property);
|
||||
}
|
||||
|
||||
export class WideBits extends Layout {
|
||||
|
@ -115,13 +126,13 @@ ACCOUNT_FLAGS_LAYOUT.addBoolean('MarginAccount');
|
|||
ACCOUNT_FLAGS_LAYOUT.addBoolean('MangoSrmAccount');
|
||||
|
||||
export function accountFlagsLayout(property = 'accountFlags') {
|
||||
return ACCOUNT_FLAGS_LAYOUT.replicate(property); // TODO: when ts check is on, replicate throws error, doesn't compile
|
||||
return ACCOUNT_FLAGS_LAYOUT.replicate(property); // TODO: when ts check is on, replicate throws error, doesn't compile
|
||||
}
|
||||
|
||||
export const MangoIndexLayout = struct([
|
||||
u64('lastUpdate'),
|
||||
U64F64('borrow'), // U64F64
|
||||
U64F64('deposit') // U64F64
|
||||
U64F64('deposit'), // U64F64
|
||||
]);
|
||||
|
||||
export const MangoGroupLayout = struct([
|
||||
|
@ -144,10 +155,9 @@ export const MangoGroupLayout = struct([
|
|||
seq(u64(), NUM_TOKENS, 'borrowLimits'),
|
||||
seq(u8(), NUM_TOKENS, 'mintDecimals'),
|
||||
seq(u8(), NUM_MARKETS, 'oracleDecimals'),
|
||||
seq(u8(), MANGO_GROUP_PADDING, 'padding')
|
||||
seq(u8(), MANGO_GROUP_PADDING, 'padding'),
|
||||
]);
|
||||
|
||||
|
||||
export const MarginAccountLayout = struct([
|
||||
accountFlagsLayout('accountFlags'),
|
||||
publicKeyLayout('mangoGroup'),
|
||||
|
@ -159,14 +169,14 @@ export const MarginAccountLayout = struct([
|
|||
u8('beingLiquidated'),
|
||||
u8('hasBorrows'),
|
||||
seq(u8(), 32, 'info'),
|
||||
seq(u8(), 38, 'padding')
|
||||
seq(u8(), 38, 'padding'),
|
||||
]);
|
||||
|
||||
export const MangoSrmAccountLayout = struct([
|
||||
accountFlagsLayout('accountFlags'),
|
||||
publicKeyLayout('mangoGroup'),
|
||||
publicKeyLayout('owner'),
|
||||
u64('amount')
|
||||
u64('amount'),
|
||||
]);
|
||||
|
||||
export const AccountLayout = struct([
|
||||
|
@ -180,14 +190,14 @@ export const AccountLayout = struct([
|
|||
u64('isNative'),
|
||||
u64('delegatedAmount'),
|
||||
u32('closeAuthorityOption'),
|
||||
publicKeyLayout('closeAuthority')
|
||||
publicKeyLayout('closeAuthority'),
|
||||
]);
|
||||
|
||||
class EnumLayout extends UInt {
|
||||
values: any;
|
||||
constructor(values, span, property) {
|
||||
super(span, property);
|
||||
this.values = values
|
||||
this.values = values;
|
||||
}
|
||||
encode(src, b, offset) {
|
||||
if (this.values[src] !== undefined) {
|
||||
|
@ -217,71 +227,106 @@ export function orderTypeLayout(property) {
|
|||
}
|
||||
|
||||
export function selfTradeBehaviorLayout(property) {
|
||||
return new EnumLayout({ decrementTake: 0, cancelProvide: 1, abortTransaction: 2 }, 4, property);
|
||||
return new EnumLayout(
|
||||
{ decrementTake: 0, cancelProvide: 1, abortTransaction: 2 },
|
||||
4,
|
||||
property,
|
||||
);
|
||||
}
|
||||
|
||||
export const MangoInstructionLayout = union(u32('instruction'))
|
||||
export const MangoInstructionLayout = union(u32('instruction'));
|
||||
|
||||
MangoInstructionLayout.addVariant(0, struct([]), 'InitMangoGroup') // TODO this is unimplemented
|
||||
MangoInstructionLayout.addVariant(0, struct([]), 'InitMangoGroup'); // TODO this is unimplemented
|
||||
|
||||
MangoInstructionLayout.addVariant(1, struct([]), 'InitMarginAccount')
|
||||
MangoInstructionLayout.addVariant(2, struct([u64('quantity')]), 'Deposit')
|
||||
MangoInstructionLayout.addVariant(3, struct([u64('quantity')]), 'Withdraw')
|
||||
MangoInstructionLayout.addVariant(4, struct([u64('tokenIndex'), u64('quantity')]), 'Borrow')
|
||||
MangoInstructionLayout.addVariant(5, struct([u64('tokenIndex'), u64('quantity')]), 'SettleBorrow')
|
||||
MangoInstructionLayout.addVariant(6, struct([seq(u64(), NUM_TOKENS, 'depositQuantities')]), 'Liquidate')
|
||||
MangoInstructionLayout.addVariant(7, struct([u64('quantity')]), 'DepositSrm')
|
||||
MangoInstructionLayout.addVariant(8, struct([u64('quantity')]), 'WithdrawSrm')
|
||||
MangoInstructionLayout.addVariant(1, struct([]), 'InitMarginAccount');
|
||||
MangoInstructionLayout.addVariant(2, struct([u64('quantity')]), 'Deposit');
|
||||
MangoInstructionLayout.addVariant(3, struct([u64('quantity')]), 'Withdraw');
|
||||
MangoInstructionLayout.addVariant(
|
||||
4,
|
||||
struct([u64('tokenIndex'), u64('quantity')]),
|
||||
'Borrow',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
5,
|
||||
struct([u64('tokenIndex'), u64('quantity')]),
|
||||
'SettleBorrow',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
6,
|
||||
struct([seq(u64(), NUM_TOKENS, 'depositQuantities')]),
|
||||
'Liquidate',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(7, struct([u64('quantity')]), 'DepositSrm');
|
||||
MangoInstructionLayout.addVariant(8, struct([u64('quantity')]), 'WithdrawSrm');
|
||||
|
||||
MangoInstructionLayout.addVariant(9,
|
||||
struct(
|
||||
[
|
||||
sideLayout('side'),
|
||||
u64('limitPrice'),
|
||||
u64('maxBaseQuantity'),
|
||||
u64('maxQuoteQuantity'),
|
||||
selfTradeBehaviorLayout('selfTradeBehavior'),
|
||||
orderTypeLayout('orderType'),
|
||||
u64('clientId'),
|
||||
u16('limit'),
|
||||
]
|
||||
MangoInstructionLayout.addVariant(
|
||||
9,
|
||||
struct([
|
||||
sideLayout('side'),
|
||||
u64('limitPrice'),
|
||||
u64('maxBaseQuantity'),
|
||||
u64('maxQuoteQuantity'),
|
||||
selfTradeBehaviorLayout('selfTradeBehavior'),
|
||||
orderTypeLayout('orderType'),
|
||||
u64('clientId'),
|
||||
u16('limit'),
|
||||
]),
|
||||
'PlaceOrder',
|
||||
);
|
||||
|
||||
MangoInstructionLayout.addVariant(10, struct([]), 'SettleFunds');
|
||||
MangoInstructionLayout.addVariant(
|
||||
11,
|
||||
struct([sideLayout('side'), u128('orderId')]),
|
||||
'CancelOrder',
|
||||
);
|
||||
|
||||
MangoInstructionLayout.addVariant(
|
||||
12,
|
||||
struct([u64('clientId')]),
|
||||
'CancelOrderByClientId',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
13,
|
||||
struct([u64('tokenIndex'), u64('borrowLimit')]),
|
||||
'ChangeBorrowLimit',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
14,
|
||||
struct([
|
||||
sideLayout('side'),
|
||||
u64('limitPrice'),
|
||||
u64('maxBaseQuantity'),
|
||||
u64('maxQuoteQuantity'),
|
||||
selfTradeBehaviorLayout('selfTradeBehavior'),
|
||||
orderTypeLayout('orderType'),
|
||||
u64('clientId'),
|
||||
u16('limit'),
|
||||
]),
|
||||
'PlaceAndSettle',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
15,
|
||||
struct([u8('limit')]),
|
||||
'ForceCancelOrders',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
16,
|
||||
struct([u64('maxDeposit')]),
|
||||
'PartialLiquidate',
|
||||
);
|
||||
MangoInstructionLayout.addVariant(
|
||||
17,
|
||||
struct([seq(u8(), INFO_LEN, 'info')]),
|
||||
'AddMarginAccountInfo',
|
||||
);
|
||||
const instructionMaxSpan = Math.max(
|
||||
...Object.values(MangoInstructionLayout.registry).map(
|
||||
(r) =>
|
||||
// @ts-ignore
|
||||
r.span,
|
||||
),
|
||||
'PlaceOrder'
|
||||
)
|
||||
|
||||
MangoInstructionLayout.addVariant(10, struct([]), 'SettleFunds')
|
||||
MangoInstructionLayout.addVariant(11,
|
||||
struct(
|
||||
[
|
||||
sideLayout('side'),
|
||||
u128('orderId')
|
||||
]
|
||||
),
|
||||
'CancelOrder'
|
||||
)
|
||||
|
||||
MangoInstructionLayout.addVariant(12, struct([u64('clientId')]), 'CancelOrderByClientId')
|
||||
MangoInstructionLayout.addVariant(13, struct([u64('tokenIndex'), u64('borrowLimit')]), 'ChangeBorrowLimit')
|
||||
MangoInstructionLayout.addVariant(14,
|
||||
struct(
|
||||
[
|
||||
sideLayout('side'),
|
||||
u64('limitPrice'),
|
||||
u64('maxBaseQuantity'),
|
||||
u64('maxQuoteQuantity'),
|
||||
selfTradeBehaviorLayout('selfTradeBehavior'),
|
||||
orderTypeLayout('orderType'),
|
||||
u64('clientId'),
|
||||
u16('limit'),
|
||||
]
|
||||
),
|
||||
'PlaceAndSettle'
|
||||
)
|
||||
MangoInstructionLayout.addVariant(15, struct([u8('limit')]), 'ForceCancelOrders')
|
||||
MangoInstructionLayout.addVariant(16, struct([u64('maxDeposit')]), 'PartialLiquidate')
|
||||
MangoInstructionLayout.addVariant(17, struct([seq(u8(), INFO_LEN, 'info')]), 'AddMarginAccountInfo')
|
||||
// @ts-ignore
|
||||
const instructionMaxSpan = Math.max(...Object.values(MangoInstructionLayout.registry).map((r) => r.span));
|
||||
);
|
||||
export function encodeMangoInstruction(data) {
|
||||
const b = Buffer.alloc(instructionMaxSpan);
|
||||
const span = MangoInstructionLayout.encode(data, b);
|
||||
|
|
305
src/schema.ts
305
src/schema.ts
|
@ -1,16 +1,15 @@
|
|||
import BN from "bn.js"
|
||||
import { deserialize, serialize } from "borsh"
|
||||
import BN from 'bn.js';
|
||||
import { deserialize, serialize } from 'borsh';
|
||||
import { Connection, PublicKey } from '@solana/web3.js';
|
||||
|
||||
|
||||
// const conn = new Connection("https://devnet.solana.com", 'singleGossip')
|
||||
|
||||
const MAX_ORACLES = 13
|
||||
const MAX_ORACLES = 13;
|
||||
|
||||
const boolMapper = {
|
||||
encode: boolToInt,
|
||||
decode: intToBool,
|
||||
}
|
||||
};
|
||||
|
||||
const pubkeyMapper = {
|
||||
encode: (key: PublicKey) => {
|
||||
|
@ -20,48 +19,48 @@ const pubkeyMapper = {
|
|||
// key
|
||||
// }
|
||||
// TODO: support either account or public key
|
||||
return key.toBuffer()
|
||||
return key.toBuffer();
|
||||
},
|
||||
|
||||
decode: (buf: Uint8Array) => {
|
||||
return new PublicKey(buf)
|
||||
return new PublicKey(buf);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// support strings that can be contained in at most 32 bytes
|
||||
const str32Mapper = {
|
||||
encode: (str: string) => {
|
||||
str = str.substr(0, 32).padEnd(32)
|
||||
return Buffer.from(str, "utf8").slice(0, 32) // truncate at 32 bytes
|
||||
str = str.substr(0, 32).padEnd(32);
|
||||
return Buffer.from(str, 'utf8').slice(0, 32); // truncate at 32 bytes
|
||||
},
|
||||
|
||||
decode: (bytes: Uint8Array) => {
|
||||
return Buffer.from(bytes).toString("utf8").trim()
|
||||
return Buffer.from(bytes).toString('utf8').trim();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const u64Date = {
|
||||
encode: (date: Date) => {
|
||||
return new BN(Math.floor(date.getTime() / 1000))
|
||||
return new BN(Math.floor(date.getTime() / 1000));
|
||||
},
|
||||
|
||||
decode: (unixtime: BN) => {
|
||||
return new Date(unixtime.toNumber() * 1000)
|
||||
return new Date(unixtime.toNumber() * 1000);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export abstract class Serialization {
|
||||
public static async loadWithConnection<T>(
|
||||
this: { new (data: any): T },
|
||||
key: PublicKey,
|
||||
connection: Connection
|
||||
connection: Connection,
|
||||
): Promise<T> {
|
||||
const info = await connection.getAccountInfo(key)
|
||||
const info = await connection.getAccountInfo(key);
|
||||
if (!info) {
|
||||
throw new Error("account does not exist")
|
||||
throw new Error('account does not exist');
|
||||
}
|
||||
|
||||
return deserialize(schema, this, info.data)
|
||||
return deserialize(schema, this, info.data);
|
||||
}
|
||||
// public static async load<T>(
|
||||
// this: { new (data: any): T },
|
||||
|
@ -76,22 +75,22 @@ export abstract class Serialization {
|
|||
// }
|
||||
|
||||
public static deserialize<T>(this: { new (data: any): T }, data: Buffer): T {
|
||||
return deserialize(schema, this, data)
|
||||
return deserialize(schema, this, data);
|
||||
}
|
||||
|
||||
public static serialize<T extends Serialization>(
|
||||
this: { new (data: any): T },
|
||||
data: object
|
||||
data: object,
|
||||
): Buffer {
|
||||
return new this(data).serialize()
|
||||
return new this(data).serialize();
|
||||
}
|
||||
|
||||
public serialize(): Buffer {
|
||||
let buf = Buffer.from(serialize(schema, this))
|
||||
let buf = Buffer.from(serialize(schema, this));
|
||||
if (buf.length == 0) {
|
||||
throw new Error("serialized buffer is 0. something wrong with schema")
|
||||
throw new Error('serialized buffer is 0. something wrong with schema');
|
||||
}
|
||||
return buf
|
||||
return buf;
|
||||
}
|
||||
|
||||
// public toJSON(pretty = true) {
|
||||
|
@ -106,157 +105,156 @@ export abstract class Serialization {
|
|||
|
||||
constructor(data) {
|
||||
// this[Serialization.DATA_KEY] = data
|
||||
Object.assign(this, data)
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
class Submission {
|
||||
public updatedAt!: BN
|
||||
public value!: BN
|
||||
public oracle!: PublicKey
|
||||
public updatedAt!: BN;
|
||||
public value!: BN;
|
||||
public oracle!: PublicKey;
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["updatedAt", "u64"],
|
||||
["value", "u64"],
|
||||
["oracle", [32], pubkeyMapper],
|
||||
['updatedAt', 'u64'],
|
||||
['value', 'u64'],
|
||||
['oracle', [32], pubkeyMapper],
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
constructor(data: any) {
|
||||
Object.assign(this, data)
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAggregatorConfig {
|
||||
decimals: number
|
||||
description: string
|
||||
restartDelay: number
|
||||
rewardAmount: number
|
||||
maxSubmissions: number
|
||||
minSubmissions: number
|
||||
rewardTokenAccount: PublicKey
|
||||
decimals: number;
|
||||
description: string;
|
||||
restartDelay: number;
|
||||
rewardAmount: number;
|
||||
maxSubmissions: number;
|
||||
minSubmissions: number;
|
||||
rewardTokenAccount: PublicKey;
|
||||
}
|
||||
|
||||
export class AggregatorConfig
|
||||
extends Serialization
|
||||
implements IAggregatorConfig {
|
||||
public decimals!: number
|
||||
public description!: string
|
||||
public restartDelay!: number
|
||||
public rewardAmount!: number
|
||||
public maxSubmissions!: number
|
||||
public minSubmissions!: number
|
||||
public rewardTokenAccount!: PublicKey
|
||||
implements IAggregatorConfig
|
||||
{
|
||||
public decimals!: number;
|
||||
public description!: string;
|
||||
public restartDelay!: number;
|
||||
public rewardAmount!: number;
|
||||
public maxSubmissions!: number;
|
||||
public minSubmissions!: number;
|
||||
public rewardTokenAccount!: PublicKey;
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["description", [32], str32Mapper],
|
||||
["decimals", "u8"],
|
||||
["restartDelay", "u8"],
|
||||
["maxSubmissions", "u8"],
|
||||
["minSubmissions", "u8"],
|
||||
["rewardAmount", "u64"],
|
||||
["rewardTokenAccount", [32], pubkeyMapper],
|
||||
['description', [32], str32Mapper],
|
||||
['decimals', 'u8'],
|
||||
['restartDelay', 'u8'],
|
||||
['maxSubmissions', 'u8'],
|
||||
['minSubmissions', 'u8'],
|
||||
['rewardAmount', 'u64'],
|
||||
['rewardTokenAccount', [32], pubkeyMapper],
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Submissions extends Serialization {
|
||||
public isInitialized!: boolean
|
||||
public submissions!: Submission[]
|
||||
public isInitialized!: boolean;
|
||||
public submissions!: Submission[];
|
||||
|
||||
public static size = 625
|
||||
public static size = 625;
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["isInitialized", "u8", boolMapper],
|
||||
["submissions", [Submission, MAX_ORACLES]],
|
||||
['isInitialized', 'u8', boolMapper],
|
||||
['submissions', [Submission, MAX_ORACLES]],
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
// if not already submitted, and has empty spot
|
||||
public canSubmit(pk: PublicKey, cfg: AggregatorConfig): boolean {
|
||||
if (this.hadSubmitted(pk)) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
let emptyIndex = this.submissions.findIndex((s) => {
|
||||
return s.updatedAt.isZero()
|
||||
})
|
||||
return s.updatedAt.isZero();
|
||||
});
|
||||
|
||||
return emptyIndex > 0 && emptyIndex < cfg.maxSubmissions
|
||||
return emptyIndex > 0 && emptyIndex < cfg.maxSubmissions;
|
||||
}
|
||||
|
||||
public hadSubmitted(pk: PublicKey): boolean {
|
||||
return !!this.submissions.find((s) => {
|
||||
return s.oracle.equals(pk)
|
||||
})
|
||||
return s.oracle.equals(pk);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Round extends Serialization {
|
||||
public id!: BN
|
||||
public createdAt!: BN
|
||||
public updatedAt!: BN
|
||||
public id!: BN;
|
||||
public createdAt!: BN;
|
||||
public updatedAt!: BN;
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["id", "u64"],
|
||||
["createdAt", "u64"],
|
||||
["updatedAt", "u64"],
|
||||
['id', 'u64'],
|
||||
['createdAt', 'u64'],
|
||||
['updatedAt', 'u64'],
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Answer extends Serialization {
|
||||
public roundID!: BN
|
||||
public median!: BN
|
||||
public createdAt!: BN
|
||||
public updatedAt!: BN
|
||||
public roundID!: BN;
|
||||
public median!: BN;
|
||||
public createdAt!: BN;
|
||||
public updatedAt!: BN;
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["roundID", "u64"],
|
||||
["median", "u64"],
|
||||
["createdAt", "u64"],
|
||||
["updatedAt", "u64"],
|
||||
['roundID', 'u64'],
|
||||
['median', 'u64'],
|
||||
['createdAt', 'u64'],
|
||||
['updatedAt', 'u64'],
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Aggregator extends Serialization {
|
||||
public static size = 229
|
||||
public static size = 229;
|
||||
|
||||
public config!: AggregatorConfig
|
||||
public roundSubmissions!: PublicKey
|
||||
public answerSubmissions!: PublicKey
|
||||
public answer!: Answer
|
||||
public round!: Round
|
||||
public config!: AggregatorConfig;
|
||||
public roundSubmissions!: PublicKey;
|
||||
public answerSubmissions!: PublicKey;
|
||||
public answer!: Answer;
|
||||
public round!: Round;
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["config", AggregatorConfig],
|
||||
["isInitialized", "u8", boolMapper],
|
||||
["owner", [32], pubkeyMapper],
|
||||
["round", Round],
|
||||
["roundSubmissions", [32], pubkeyMapper],
|
||||
["answer", Answer],
|
||||
["answerSubmissions", [32], pubkeyMapper],
|
||||
['config', AggregatorConfig],
|
||||
['isInitialized', 'u8', boolMapper],
|
||||
['owner', [32], pubkeyMapper],
|
||||
['round', Round],
|
||||
['roundSubmissions', [32], pubkeyMapper],
|
||||
['answer', Answer],
|
||||
['answerSubmissions', [32], pubkeyMapper],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
abstract class InstructionSerialization extends Serialization {
|
||||
public serialize(): Buffer {
|
||||
return new Instruction({ [this.constructor.name]: this }).serialize()
|
||||
return new Instruction({ [this.constructor.name]: this }).serialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,55 +267,55 @@ export class Initialize extends InstructionSerialization {
|
|||
// public description!: string
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
fields: [["config", AggregatorConfig]],
|
||||
}
|
||||
kind: 'struct',
|
||||
fields: [['config', AggregatorConfig]],
|
||||
};
|
||||
}
|
||||
|
||||
export class Configure extends InstructionSerialization {
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
fields: [["config", AggregatorConfig]],
|
||||
}
|
||||
kind: 'struct',
|
||||
fields: [['config', AggregatorConfig]],
|
||||
};
|
||||
}
|
||||
|
||||
export class AddOracle extends InstructionSerialization {
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
fields: [["description", [32], str32Mapper]],
|
||||
}
|
||||
kind: 'struct',
|
||||
fields: [['description', [32], str32Mapper]],
|
||||
};
|
||||
}
|
||||
|
||||
export class RemoveOracle extends InstructionSerialization {
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Withdraw extends InstructionSerialization {
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
fields: [["faucetOwnerSeed", ["u8"]]],
|
||||
}
|
||||
kind: 'struct',
|
||||
fields: [['faucetOwnerSeed', ['u8']]],
|
||||
};
|
||||
}
|
||||
|
||||
export class Submit extends InstructionSerialization {
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["round_id", "u64"],
|
||||
["value", "u64"],
|
||||
['round_id', 'u64'],
|
||||
['value', 'u64'],
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Instruction extends Serialization {
|
||||
public enum!: string
|
||||
public enum!: string;
|
||||
|
||||
public static schema = {
|
||||
kind: "enum",
|
||||
field: "enum",
|
||||
kind: 'enum',
|
||||
field: 'enum',
|
||||
values: [
|
||||
[Initialize.name, Initialize],
|
||||
[Configure.name, Configure],
|
||||
|
@ -325,63 +323,63 @@ export class Instruction extends Serialization {
|
|||
[RemoveOracle.name, RemoveOracle],
|
||||
[Submit.name, Submit],
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
public constructor(prop: { [key: string]: any }) {
|
||||
super({})
|
||||
super({});
|
||||
// deserializer calls the construction with `{ [enum]: value }`, so we need
|
||||
// to figure out the enum type
|
||||
//
|
||||
// expect only one key-value (what a retarded interface)
|
||||
for (let key of Object.keys(prop)) {
|
||||
this.enum = key
|
||||
this[key] = prop[key]
|
||||
return
|
||||
this.enum = key;
|
||||
this[key] = prop[key];
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("not an expected enum object")
|
||||
throw new Error('not an expected enum object');
|
||||
}
|
||||
|
||||
public get value() {
|
||||
return this[this.enum]
|
||||
return this[this.enum];
|
||||
}
|
||||
}
|
||||
|
||||
function intToBool(i: number) {
|
||||
if (i == 0) {
|
||||
return false
|
||||
return false;
|
||||
} else {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function boolToInt(t: boolean) {
|
||||
if (t) {
|
||||
return 1
|
||||
return 1;
|
||||
} else {
|
||||
return 0
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class Oracle extends Serialization {
|
||||
public static size = 113
|
||||
public allowStartRound!: BN
|
||||
public withdrawable!: BN
|
||||
public static size = 113;
|
||||
public allowStartRound!: BN;
|
||||
public withdrawable!: BN;
|
||||
|
||||
public static schema = {
|
||||
kind: "struct",
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
["description", [32], str32Mapper],
|
||||
["isInitialized", "u8", boolMapper],
|
||||
["withdrawable", "u64"],
|
||||
["allowStartRound", "u64"],
|
||||
["aggregator", [32], pubkeyMapper],
|
||||
["owner", [32], pubkeyMapper],
|
||||
['description', [32], str32Mapper],
|
||||
['isInitialized', 'u8', boolMapper],
|
||||
['withdrawable', 'u64'],
|
||||
['allowStartRound', 'u64'],
|
||||
['aggregator', [32], pubkeyMapper],
|
||||
['owner', [32], pubkeyMapper],
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
public canStartNewRound(round: BN): boolean {
|
||||
return this.allowStartRound.lte(round)
|
||||
return this.allowStartRound.lte(round);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -402,5 +400,4 @@ export const schema = new Map([
|
|||
[Initialize, Initialize.schema],
|
||||
[AddOracle, AddOracle.schema],
|
||||
[Submit, Submit.schema],
|
||||
|
||||
] as any) as any
|
||||
] as any) as any;
|
||||
|
|
207
src/tests.ts
207
src/tests.ts
|
@ -11,21 +11,27 @@ import {
|
|||
getMultipleAccounts,
|
||||
nativeToUi,
|
||||
parseTokenAccountData,
|
||||
sleep
|
||||
} from './utils'
|
||||
sleep,
|
||||
} from './utils';
|
||||
import { NUM_MARKETS, NUM_TOKENS } from './layout';
|
||||
|
||||
async function tests() {
|
||||
const cluster = "mainnet-beta";
|
||||
const cluster = 'mainnet-beta';
|
||||
const client = new MangoClient();
|
||||
const clusterIds = IDS[cluster]
|
||||
const clusterIds = IDS[cluster];
|
||||
|
||||
const connection = new Connection(IDS.cluster_urls[cluster], 'processed' as Commitment)
|
||||
const mangoGroupPk = new PublicKey(clusterIds.mango_groups['BTC_ETH_SOL_SRM_USDC'].mango_group_pk);
|
||||
const connection = new Connection(
|
||||
IDS.cluster_urls[cluster],
|
||||
'processed' as Commitment,
|
||||
);
|
||||
const mangoGroupPk = new PublicKey(
|
||||
clusterIds.mango_groups['BTC_ETH_SOL_SRM_USDC'].mango_group_pk,
|
||||
);
|
||||
const mangoProgramId = new PublicKey(clusterIds.mango_program_id);
|
||||
|
||||
const keyPairPath = process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json'
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
|
||||
const keyPairPath =
|
||||
process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json';
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')));
|
||||
|
||||
/*
|
||||
async function testSolink() {
|
||||
|
@ -55,118 +61,169 @@ async function tests() {
|
|||
|
||||
async function getMarginAccountDetails() {
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
|
||||
const marginAccountPk = new PublicKey("BZXTBk41pBuwgw4RL3hUxRXqSdiA7VQiaxr4ueEMZrsH")
|
||||
const marginAccount = await client.getMarginAccount(connection, marginAccountPk, mangoGroup.dexProgramId)
|
||||
const prices = await mangoGroup.getPrices(connection)
|
||||
const marginAccountPk = new PublicKey(
|
||||
'BZXTBk41pBuwgw4RL3hUxRXqSdiA7VQiaxr4ueEMZrsH',
|
||||
);
|
||||
const marginAccount = await client.getMarginAccount(
|
||||
connection,
|
||||
marginAccountPk,
|
||||
mangoGroup.dexProgramId,
|
||||
);
|
||||
const prices = await mangoGroup.getPrices(connection);
|
||||
|
||||
console.log(marginAccount.toPrettyString(mangoGroup, prices))
|
||||
console.log(marginAccount.beingLiquidated)
|
||||
console.log(marginAccount.getCollateralRatio(mangoGroup, prices))
|
||||
console.log(marginAccount.toPrettyString(mangoGroup, prices));
|
||||
console.log(marginAccount.beingLiquidated);
|
||||
console.log(marginAccount.getCollateralRatio(mangoGroup, prices));
|
||||
|
||||
for (let i = 0; i < NUM_TOKENS; i++) {
|
||||
console.log(marginAccount.getUiDeposit(mangoGroup, i), marginAccount.getUiBorrow(mangoGroup, i))
|
||||
console.log(
|
||||
marginAccount.getUiDeposit(mangoGroup, i),
|
||||
marginAccount.getUiBorrow(mangoGroup, i),
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < NUM_MARKETS; i++) {
|
||||
let openOrdersAccount = marginAccount.openOrdersAccounts[i]
|
||||
let openOrdersAccount = marginAccount.openOrdersAccounts[i];
|
||||
if (openOrdersAccount === undefined) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log('referrer rebates', i, openOrdersAccount['referrerRebatesAccrued'].toNumber())
|
||||
console.log(i,
|
||||
nativeToUi(openOrdersAccount.quoteTokenTotal.toNumber() + openOrdersAccount['referrerRebatesAccrued'].toNumber(), mangoGroup.mintDecimals[NUM_MARKETS]),
|
||||
nativeToUi(openOrdersAccount.quoteTokenFree.toNumber(), mangoGroup.mintDecimals[NUM_MARKETS]),
|
||||
console.log(
|
||||
'referrer rebates',
|
||||
i,
|
||||
openOrdersAccount['referrerRebatesAccrued'].toNumber(),
|
||||
);
|
||||
console.log(
|
||||
i,
|
||||
nativeToUi(
|
||||
openOrdersAccount.quoteTokenTotal.toNumber() +
|
||||
openOrdersAccount['referrerRebatesAccrued'].toNumber(),
|
||||
mangoGroup.mintDecimals[NUM_MARKETS],
|
||||
),
|
||||
nativeToUi(
|
||||
openOrdersAccount.quoteTokenFree.toNumber(),
|
||||
mangoGroup.mintDecimals[NUM_MARKETS],
|
||||
),
|
||||
|
||||
nativeToUi(openOrdersAccount.baseTokenTotal.toNumber(), mangoGroup.mintDecimals[i]),
|
||||
nativeToUi(openOrdersAccount.baseTokenFree.toNumber(), mangoGroup.mintDecimals[i])
|
||||
|
||||
)
|
||||
nativeToUi(
|
||||
openOrdersAccount.baseTokenTotal.toNumber(),
|
||||
mangoGroup.mintDecimals[i],
|
||||
),
|
||||
nativeToUi(
|
||||
openOrdersAccount.baseTokenFree.toNumber(),
|
||||
mangoGroup.mintDecimals[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function testMarketOrderDex() {
|
||||
const NUM_MARKETS = 2;
|
||||
const dexProgramId = new PublicKey(clusterIds.dex_program_id);
|
||||
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
|
||||
|
||||
// load largest wallet account for each token
|
||||
const tokenWallets = (await Promise.all(
|
||||
mangoGroup.tokens.map(
|
||||
(mint) => findLargestTokenAccountForOwner(connection, payer.publicKey, mint).then(
|
||||
(response) => response.publicKey
|
||||
)
|
||||
)
|
||||
))
|
||||
console.log({tokenWallets: tokenWallets.map(w => w.toString())})
|
||||
const tokenWallets = await Promise.all(
|
||||
mangoGroup.tokens.map((mint) =>
|
||||
findLargestTokenAccountForOwner(connection, payer.publicKey, mint).then(
|
||||
(response) => response.publicKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
console.log({ tokenWallets: tokenWallets.map((w) => w.toString()) });
|
||||
|
||||
// load all markets
|
||||
const markets = await Promise.all(mangoGroup.spotMarkets.map(
|
||||
(pk) => Market.load(connection, pk, {skipPreflight: true, commitment: 'singleGossip'}, dexProgramId)
|
||||
))
|
||||
console.log({markets})
|
||||
const markets = await Promise.all(
|
||||
mangoGroup.spotMarkets.map((pk) =>
|
||||
Market.load(
|
||||
connection,
|
||||
pk,
|
||||
{ skipPreflight: true, commitment: 'singleGossip' },
|
||||
dexProgramId,
|
||||
),
|
||||
),
|
||||
);
|
||||
console.log({ markets });
|
||||
|
||||
// load open orders
|
||||
const liqorOpenOrdersKeys: PublicKey[] = []
|
||||
const liqorOpenOrdersKeys: PublicKey[] = [];
|
||||
for (let i = 0; i < NUM_MARKETS; i++) {
|
||||
let openOrdersAccounts: OpenOrders[] = await markets[i].findOpenOrdersAccountsForOwner(connection, payer.publicKey)
|
||||
if(openOrdersAccounts.length) {
|
||||
liqorOpenOrdersKeys.push(openOrdersAccounts[0].publicKey)
|
||||
let openOrdersAccounts: OpenOrders[] = await markets[
|
||||
i
|
||||
].findOpenOrdersAccountsForOwner(connection, payer.publicKey);
|
||||
if (openOrdersAccounts.length) {
|
||||
liqorOpenOrdersKeys.push(openOrdersAccounts[0].publicKey);
|
||||
} else {
|
||||
console.log(`No OpenOrders account found for market ${markets[i].publicKey.toBase58()}`)
|
||||
console.log(
|
||||
`No OpenOrders account found for market ${markets[
|
||||
i
|
||||
].publicKey.toBase58()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log({liqorOpenOrdersKeys: liqorOpenOrdersKeys.map(k => k.toString())})
|
||||
console.log({
|
||||
liqorOpenOrdersKeys: liqorOpenOrdersKeys.map((k) => k.toString()),
|
||||
});
|
||||
|
||||
const marketIndex = 1;
|
||||
const market = markets[marketIndex]; // ETH/USDT
|
||||
const price = 4000;
|
||||
const size = 0.001;
|
||||
|
||||
const txid = await market.placeOrder(
|
||||
connection,
|
||||
{
|
||||
owner: payer,
|
||||
payer: tokenWallets[marketIndex],
|
||||
side: 'sell',
|
||||
price,
|
||||
size,
|
||||
orderType: 'ioc',
|
||||
openOrdersAddressKey: liqorOpenOrdersKeys[marketIndex],
|
||||
feeDiscountPubkey: null // TODO find liqor's SRM fee account
|
||||
}
|
||||
)
|
||||
console.log({txid})
|
||||
const txid = await market.placeOrder(connection, {
|
||||
owner: payer,
|
||||
payer: tokenWallets[marketIndex],
|
||||
side: 'sell',
|
||||
price,
|
||||
size,
|
||||
orderType: 'ioc',
|
||||
openOrdersAddressKey: liqorOpenOrdersKeys[marketIndex],
|
||||
feeDiscountPubkey: null, // TODO find liqor's SRM fee account
|
||||
});
|
||||
console.log({ txid });
|
||||
|
||||
var lastSeenSeqNum = undefined
|
||||
var lastSeenSeqNum = undefined;
|
||||
|
||||
for (let i = 0; i < 50; ++i) {
|
||||
const status = await connection.getSignatureStatus(txid);
|
||||
console.log({status: status!.value!.confirmations})
|
||||
console.log({ status: status!.value!.confirmations });
|
||||
|
||||
let orders = await market.loadOrdersForOwner(connection, payer.publicKey);
|
||||
console.log({orders});
|
||||
console.log({ orders });
|
||||
|
||||
const info = await connection.getAccountInfo(market['_decoded'].eventQueue)
|
||||
const { header, nodes } = decodeRecentEvents(info!.data, lastSeenSeqNum)
|
||||
console.log({ header, nodes: nodes.map(n => [n.nativeQuantityPaid.toNumber(),
|
||||
n.nativeQuantityReleased.toNumber()]) })
|
||||
lastSeenSeqNum = header.seqNum
|
||||
const info = await connection.getAccountInfo(
|
||||
market['_decoded'].eventQueue,
|
||||
);
|
||||
const { header, nodes } = decodeRecentEvents(info!.data, lastSeenSeqNum);
|
||||
console.log({
|
||||
header,
|
||||
nodes: nodes.map((n) => [
|
||||
n.nativeQuantityPaid.toNumber(),
|
||||
n.nativeQuantityReleased.toNumber(),
|
||||
]),
|
||||
});
|
||||
lastSeenSeqNum = header.seqNum;
|
||||
|
||||
const liqorWalletAccounts = await getMultipleAccounts(connection, tokenWallets, 'processed' as Commitment)
|
||||
const liqorValuesUi = liqorWalletAccounts.map(
|
||||
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
|
||||
)
|
||||
console.log({liqorValuesUi})
|
||||
await sleep(500)
|
||||
const liqorWalletAccounts = await getMultipleAccounts(
|
||||
connection,
|
||||
tokenWallets,
|
||||
'processed' as Commitment,
|
||||
);
|
||||
const liqorValuesUi = liqorWalletAccounts.map((a, i) =>
|
||||
nativeToUi(
|
||||
parseTokenAccountData(a.accountInfo.data).amount,
|
||||
mangoGroup.mintDecimals[i],
|
||||
),
|
||||
);
|
||||
console.log({ liqorValuesUi });
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
await getMarginAccountDetails()
|
||||
await getMarginAccountDetails();
|
||||
// await testSolink()
|
||||
// await testDepositSrm()
|
||||
// await testMarketOrderDex()
|
||||
}
|
||||
|
||||
tests()
|
||||
tests();
|
||||
|
|
160
src/utils.ts
160
src/utils.ts
|
@ -1,14 +1,24 @@
|
|||
import {
|
||||
Account,
|
||||
AccountInfo, Commitment,
|
||||
AccountInfo,
|
||||
Commitment,
|
||||
Connection,
|
||||
PublicKey, RpcResponseAndContext, SimulatedTransactionResponse,
|
||||
SystemProgram, Transaction, TransactionConfirmationStatus,
|
||||
PublicKey,
|
||||
RpcResponseAndContext,
|
||||
SimulatedTransactionResponse,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
TransactionConfirmationStatus,
|
||||
TransactionInstruction,
|
||||
TransactionSignature,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
|
||||
import {
|
||||
parseBaseData,
|
||||
parsePriceData,
|
||||
AccountType,
|
||||
} from '@pythnetwork/client';
|
||||
import { bits, blob, struct, u8, u32, nu64 } from 'buffer-layout';
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
import { AccountLayout } from './layout';
|
||||
|
@ -20,8 +30,9 @@ import {
|
|||
u64,
|
||||
zeros,
|
||||
} from '@project-serum/serum/lib/layout';
|
||||
import { Aggregator } from './schema';
|
||||
|
||||
export const zeroKey = new PublicKey(new Uint8Array(32))
|
||||
export const zeroKey = new PublicKey(new Uint8Array(32));
|
||||
|
||||
export async function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
@ -73,16 +84,16 @@ export async function awaitTransactionSignatureConfirmation(
|
|||
txid: TransactionSignature,
|
||||
timeout: number,
|
||||
connection: Connection,
|
||||
confirmLevel: TransactionConfirmationStatus
|
||||
confirmLevel: TransactionConfirmationStatus,
|
||||
) {
|
||||
let done = false;
|
||||
|
||||
const confirmLevels: (TransactionConfirmationStatus | null)[] = ['finalized']
|
||||
const confirmLevels: (TransactionConfirmationStatus | null)[] = ['finalized'];
|
||||
if (confirmLevel === 'confirmed') {
|
||||
confirmLevels.push('confirmed')
|
||||
confirmLevels.push('confirmed');
|
||||
} else if (confirmLevel === 'processed') {
|
||||
confirmLevels.push('confirmed')
|
||||
confirmLevels.push('processed')
|
||||
confirmLevels.push('confirmed');
|
||||
confirmLevels.push('processed');
|
||||
}
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
|
@ -129,7 +140,12 @@ export async function awaitTransactionSignatureConfirmation(
|
|||
console.log('REST error for', txid, result);
|
||||
done = true;
|
||||
reject(result.err);
|
||||
} else if (!(result.confirmations || confirmLevels.includes(result.confirmationStatus))) {
|
||||
} else if (
|
||||
!(
|
||||
result.confirmations ||
|
||||
confirmLevels.includes(result.confirmationStatus)
|
||||
)
|
||||
) {
|
||||
console.log('REST not confirmed', txid, result);
|
||||
} else {
|
||||
console.log('REST confirmed', txid, result);
|
||||
|
@ -151,27 +167,27 @@ export async function awaitTransactionSignatureConfirmation(
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
export async function createAccountInstruction(
|
||||
connection: Connection,
|
||||
payer: PublicKey,
|
||||
space: number,
|
||||
owner: PublicKey,
|
||||
lamports?: number
|
||||
): Promise<{ account: Account, instruction: TransactionInstruction }> {
|
||||
lamports?: number,
|
||||
): Promise<{ account: Account; instruction: TransactionInstruction }> {
|
||||
const account = new Account();
|
||||
const instruction = SystemProgram.createAccount({
|
||||
fromPubkey: payer,
|
||||
newAccountPubkey: account.publicKey,
|
||||
lamports: lamports ? lamports : await connection.getMinimumBalanceForRentExemption(space),
|
||||
lamports: lamports
|
||||
? lamports
|
||||
: await connection.getMinimumBalanceForRentExemption(space),
|
||||
space,
|
||||
programId: owner
|
||||
})
|
||||
programId: owner,
|
||||
});
|
||||
|
||||
return { account, instruction };
|
||||
}
|
||||
|
||||
|
||||
const MINT_LAYOUT = struct([blob(44), u8('decimals'), blob(37)]);
|
||||
|
||||
export async function getMintDecimals(
|
||||
|
@ -196,16 +212,14 @@ function throwIfNull<T>(value: T | null, message = 'account not found'): T {
|
|||
return value;
|
||||
}
|
||||
|
||||
|
||||
export function uiToNative(amount: number, decimals: number): BN {
|
||||
return new BN(Math.round(amount * Math.pow(10, decimals)))
|
||||
return new BN(Math.round(amount * Math.pow(10, decimals)));
|
||||
}
|
||||
|
||||
export function nativeToUi(amount: number, decimals: number): number {
|
||||
return amount / Math.pow(10, decimals)
|
||||
return amount / Math.pow(10, decimals);
|
||||
}
|
||||
|
||||
|
||||
export async function getFilteredProgramAccounts(
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
|
@ -237,24 +251,25 @@ export async function getFilteredProgramAccounts(
|
|||
}
|
||||
|
||||
export async function promiseUndef(): Promise<undefined> {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const getUnixTs = () => {
|
||||
return new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const ACCOUNT_LAYOUT = struct([
|
||||
blob(32, 'mint'),
|
||||
blob(32, 'owner'),
|
||||
nu64('amount'),
|
||||
blob(93)
|
||||
blob(93),
|
||||
]);
|
||||
|
||||
export function parseTokenAccountData(
|
||||
data: Buffer,
|
||||
): { mint: PublicKey; owner: PublicKey; amount: number } {
|
||||
export function parseTokenAccountData(data: Buffer): {
|
||||
mint: PublicKey;
|
||||
owner: PublicKey;
|
||||
amount: number;
|
||||
} {
|
||||
let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
|
||||
return {
|
||||
mint: new PublicKey(mint),
|
||||
|
@ -263,16 +278,17 @@ export function parseTokenAccountData(
|
|||
};
|
||||
}
|
||||
|
||||
export function parseTokenAccount(
|
||||
data: Buffer
|
||||
): { mint: PublicKey; owner: PublicKey; amount: BN } {
|
||||
|
||||
const decoded = AccountLayout.decode(data)
|
||||
export function parseTokenAccount(data: Buffer): {
|
||||
mint: PublicKey;
|
||||
owner: PublicKey;
|
||||
amount: BN;
|
||||
} {
|
||||
const decoded = AccountLayout.decode(data);
|
||||
return {
|
||||
mint: decoded.mint,
|
||||
owner: decoded.owner,
|
||||
amount: decoded.amount
|
||||
}
|
||||
amount: decoded.amount,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMultipleAccounts(
|
||||
|
@ -318,31 +334,39 @@ export async function getMultipleAccounts(
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export async function findLargestTokenAccountForOwner(
|
||||
connection: Connection,
|
||||
owner: PublicKey,
|
||||
mint: PublicKey
|
||||
): Promise<{ publicKey: PublicKey; tokenAccount: { mint: PublicKey; owner: PublicKey; amount: number} }> {
|
||||
|
||||
const response = await connection.getTokenAccountsByOwner(owner, {mint, programId: TOKEN_PROGRAM_ID}, connection.commitment)
|
||||
mint: PublicKey,
|
||||
): Promise<{
|
||||
publicKey: PublicKey;
|
||||
tokenAccount: { mint: PublicKey; owner: PublicKey; amount: number };
|
||||
}> {
|
||||
const response = await connection.getTokenAccountsByOwner(
|
||||
owner,
|
||||
{ mint, programId: TOKEN_PROGRAM_ID },
|
||||
connection.commitment,
|
||||
);
|
||||
let max = -1;
|
||||
let maxTokenAccount: null | { mint: PublicKey; owner: PublicKey; amount: number} = null
|
||||
let maxPubkey: null | PublicKey = null
|
||||
let maxTokenAccount: null | {
|
||||
mint: PublicKey;
|
||||
owner: PublicKey;
|
||||
amount: number;
|
||||
} = null;
|
||||
let maxPubkey: null | PublicKey = null;
|
||||
for (const { pubkey, account } of response.value) {
|
||||
|
||||
const tokenAccount = parseTokenAccountData(account.data)
|
||||
const tokenAccount = parseTokenAccountData(account.data);
|
||||
if (tokenAccount.amount > max) {
|
||||
maxTokenAccount = tokenAccount
|
||||
max = tokenAccount.amount
|
||||
maxPubkey = pubkey
|
||||
maxTokenAccount = tokenAccount;
|
||||
max = tokenAccount.amount;
|
||||
maxPubkey = pubkey;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxPubkey && maxTokenAccount) {
|
||||
return {publicKey: maxPubkey, tokenAccount: maxTokenAccount}
|
||||
return { publicKey: maxPubkey, tokenAccount: maxTokenAccount };
|
||||
} else {
|
||||
throw new Error("No accounts for this token")
|
||||
throw new Error('No accounts for this token');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,11 +401,7 @@ const EVENT = struct([
|
|||
u64('clientOrderId'),
|
||||
]);
|
||||
|
||||
|
||||
export function decodeRecentEvents(
|
||||
buffer: Buffer,
|
||||
lastSeenSeqNum?: number,
|
||||
) {
|
||||
export function decodeRecentEvents(buffer: Buffer, lastSeenSeqNum?: number) {
|
||||
const header = EVENT_QUEUE_HEADER.decode(buffer);
|
||||
const nodes: any[] = [];
|
||||
|
||||
|
@ -390,14 +410,38 @@ export function decodeRecentEvents(
|
|||
(buffer.length - EVENT_QUEUE_HEADER.span) / EVENT.span,
|
||||
);
|
||||
|
||||
const newEventsCount = header.seqNum - lastSeenSeqNum
|
||||
const newEventsCount = header.seqNum - lastSeenSeqNum;
|
||||
|
||||
for (let i = newEventsCount; i > 0; --i) {
|
||||
const nodeIndex = (header.head + header.count + allocLen - i) % allocLen
|
||||
const decodedItem = EVENT.decode(buffer, EVENT_QUEUE_HEADER.span + nodeIndex * EVENT.span)
|
||||
nodes.push(decodedItem)
|
||||
const nodeIndex = (header.head + header.count + allocLen - i) % allocLen;
|
||||
const decodedItem = EVENT.decode(
|
||||
buffer,
|
||||
EVENT_QUEUE_HEADER.span + nodeIndex * EVENT.span,
|
||||
);
|
||||
nodes.push(decodedItem);
|
||||
}
|
||||
}
|
||||
|
||||
return { header, nodes };
|
||||
}
|
||||
|
||||
const PYTH_MAGIC = Buffer.from([0xa1, 0xb2, 0xc3, 0xd4]);
|
||||
|
||||
export async function getOraclePrice(
|
||||
connection: Connection,
|
||||
oracle: PublicKey,
|
||||
): Promise<number> {
|
||||
const info = await connection.getAccountInfo(oracle);
|
||||
if (!info || !info.data.length) {
|
||||
throw new Error('account does not exist');
|
||||
}
|
||||
|
||||
const pythBase = parseBaseData(info.data);
|
||||
if (pythBase?.type == AccountType.Price) {
|
||||
const price = parsePriceData(info.data);
|
||||
return price.aggregate.price;
|
||||
} else {
|
||||
const agg = Aggregator.deserialize(info.data);
|
||||
return agg.answer.median.toNumber() / Math.pow(10, agg.config.decimals);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { Connection, PublicKey } from '@solana/web3.js';
|
||||
import { expect } from 'chai';
|
||||
import { getOraclePrice } from '../src/utils';
|
||||
|
||||
const conn = new Connection('https://api.mainnet-beta.solana.com/');
|
||||
|
||||
describe('getOraclePrice', async () => {
|
||||
it('should parse flux aggregator', async () => {
|
||||
const p = await getOraclePrice(
|
||||
conn,
|
||||
new PublicKey('HxrRDnjj2Ltj9LMmtcN6PDuFqnDe3FqXDHPvs2pwmtYF'),
|
||||
);
|
||||
expect(p).to.be.within(5000, 80000);
|
||||
});
|
||||
|
||||
it('should parse pyth', async () => {
|
||||
const p = await getOraclePrice(
|
||||
conn,
|
||||
new PublicKey('GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'),
|
||||
);
|
||||
expect(p).to.be.within(5000, 80000);
|
||||
});
|
||||
});
|
|
@ -583,6 +583,13 @@
|
|||
bs58 "^4.0.1"
|
||||
eventemitter3 "^4.0.4"
|
||||
|
||||
"@pythnetwork/client@^2.7.2":
|
||||
version "2.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@pythnetwork/client/-/client-2.7.2.tgz#eca3a59e8f222aa1b67c8e4653e2484079f5fb9a"
|
||||
integrity sha512-Hx/GLaZH0evm0tT0gsO1S7r3liiDzUeqDfMaV1HH7a5yuQGH9zgUrmdEMEtkgRceLa2nGhEGnRtISqY/X96XtA==
|
||||
dependencies:
|
||||
buffer "^6.0.1"
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
|
||||
|
|
Loading…
Reference in New Issue