Compare commits

...

2 Commits

Author SHA1 Message Date
Maximilian Schneider 5a0cfa413f add support for pyth oracle 2022-07-26 22:28:34 +02:00
Maximilian Schneider 1c7749bdfc format everything with prettier 2022-07-26 22:21:22 +02:00
11 changed files with 1560 additions and 923 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -244,4 +244,4 @@
"USDC": "9q4p8UFxphSipGL3TGku8byTijgk4koTMwhBMV4QKvjw"
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

23
tests/oracle.test.ts Normal file
View File

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

View File

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