Extending the client, make more use friendly, make serum3 place order work for example

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-04-08 12:30:21 +02:00
parent f7fb0bbec8
commit 89aa667985
7 changed files with 330 additions and 125 deletions

View File

@ -5,13 +5,54 @@ import {
TransactionSignature,
} from '@solana/web3.js';
import { MangoClient } from '../../client';
import {
DEVNET_MINTS_REVERSE,
DEVNET_SERUM3_MARKETS_REVERSE,
} from '../../constants';
import { Bank } from './bank';
import { Serum3Market } from './serum3';
export class Group {
static from(publicKey: PublicKey, obj: { admin: PublicKey }): Group {
return new Group(publicKey, obj.admin);
return new Group(publicKey, obj.admin, new Map(), new Map());
}
constructor(public publicKey: PublicKey, public admin: PublicKey) {}
constructor(
public publicKey: PublicKey,
public admin: PublicKey,
public banksMap: Map<string, Bank>,
public serum3MarketsMap: Map<string, Serum3Market>,
) {}
public findBank(tokenIndex: number): Bank | undefined {
return Array.from(this.banksMap.values()).find(
(bank) => bank.tokenIndex === tokenIndex,
);
}
public async reload(client: MangoClient) {
await this.reloadBanks(client);
await this.reloadSerum3Markets(client);
}
public async reloadBanks(client: MangoClient) {
const banks = await client.getBanksForGroup(this);
this.banksMap = new Map(
banks.map((bank) => [DEVNET_MINTS_REVERSE[bank.mint.toBase58()], bank]),
);
}
public async reloadSerum3Markets(client: MangoClient) {
const serum3Markets = await client.serum3GetMarket(this);
this.serum3MarketsMap = new Map(
serum3Markets.map((serum3Market) => [
DEVNET_SERUM3_MARKETS_REVERSE[
serum3Market.serumMarketExternal.toBase58()
],
serum3Market,
]),
);
}
}
/**

View File

@ -68,6 +68,10 @@ export class MangoAccount {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
}
findSerum3Account(marketIndex: number): Serum3Account | undefined {
return this.serum3.find((sa) => sa.marketIndex == marketIndex);
}
getNativeDeposit(bank: Bank): I80F48 {
const ta = this.findToken(bank.tokenIndex);
return bank.depositIndex.mul(ta?.indexedValue!);

View File

@ -1,7 +1,9 @@
import { BN, Program, Provider } from '@project-serum/anchor';
import { Market } from '@project-serum/serum';
import * as spl from '@solana/spl-token';
import {
AccountMeta,
MemcmpFilter,
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionSignature,
@ -12,7 +14,12 @@ import { Group } from './accounts/types/group';
import { I80F48 } from './accounts/types/I80F48';
import { MangoAccount } from './accounts/types/mangoAccount';
import { StubOracle } from './accounts/types/oracle';
import { Serum3Market } from './accounts/types/serum3';
import {
Serum3Market,
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from './accounts/types/serum3';
import { IDL, MangoV4 } from './mango_v4';
export const MANGO_V4_ID = new PublicKey(
@ -48,6 +55,7 @@ export class MangoClient {
},
])
).map((tuple) => Group.from(tuple.publicKey, tuple.account));
await groups[0].reload(this);
return groups[0];
}
@ -151,6 +159,19 @@ export class MangoClient {
// MangoAccount
public async getOrCreateMangoAccount(
group: Group,
ownerPk: PublicKey,
accountNumber?: number,
): Promise<MangoAccount> {
let mangoAccounts = await this.getMangoAccountForOwner(group, ownerPk);
if (mangoAccounts.length === 0) {
await this.createMangoAccount(group, accountNumber ?? 0);
}
mangoAccounts = await this.getMangoAccountForOwner(group, ownerPk);
return mangoAccounts[0];
}
public async createMangoAccount(
group: Group,
accountNumber: number,
@ -165,7 +186,14 @@ export class MangoClient {
.rpc();
}
public async getMangoAccount(
public async getMangoAccount(mangoAccount: MangoAccount) {
return MangoAccount.from(
mangoAccount.publicKey,
await this.program.account.mangoAccount.fetch(mangoAccount.publicKey),
);
}
public async getMangoAccountForOwner(
group: Group,
ownerPk: PublicKey,
): Promise<MangoAccount[]> {
@ -192,9 +220,11 @@ export class MangoClient {
public async deposit(
group: Group,
mangoAccount: MangoAccount,
bank: Bank,
tokenName: string,
amount: number,
) {
const bank = group.banksMap.get(tokenName)!;
const tokenAccountPk = await spl.getAssociatedTokenAddress(
bank.mint,
mangoAccount.owner,
@ -225,10 +255,12 @@ export class MangoClient {
public async withdraw(
group: Group,
mangoAccount: MangoAccount,
bank: Bank,
tokenName: string,
amount: number,
allowBorrow: boolean,
) {
const bank = group.banksMap.get(tokenName)!;
const tokenAccountPk = await spl.getAssociatedTokenAddress(
bank.mint,
mangoAccount.owner,
@ -280,49 +312,57 @@ export class MangoClient {
.rpc();
}
public async serum3GetMarketForBaseAndQuote(
public async serum3GetMarket(
group: Group,
baseTokenIndex: number,
quoteTokenIndex: number,
baseTokenIndex?: number,
quoteTokenIndex?: number,
): Promise<Serum3Market[]> {
const bbuf = Buffer.alloc(2);
bbuf.writeUInt16LE(baseTokenIndex);
const qbuf = Buffer.alloc(2);
qbuf.writeUInt16LE(quoteTokenIndex);
const bumpfbuf = Buffer.alloc(1);
bumpfbuf.writeUInt8(255);
return (
await this.program.account.serum3Market.all([
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
},
const filters: MemcmpFilter[] = [
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
},
{
memcmp: {
bytes: bs58.encode(bbuf),
offset: 106,
},
},
];
if (baseTokenIndex) {
const bbuf = Buffer.alloc(2);
bbuf.writeUInt16LE(baseTokenIndex);
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 106,
},
{
memcmp: {
bytes: bs58.encode(qbuf),
offset: 108,
},
});
}
if (quoteTokenIndex) {
const qbuf = Buffer.alloc(2);
qbuf.writeUInt16LE(quoteTokenIndex);
filters.push({
memcmp: {
bytes: bs58.encode(qbuf),
offset: 106,
},
])
).map((tuple) => Serum3Market.from(tuple.publicKey, tuple.account));
});
}
return (await this.program.account.serum3Market.all(filters)).map((tuple) =>
Serum3Market.from(tuple.publicKey, tuple.account),
);
}
public async serum3CreateOpenOrders(
group: Group,
mangoAccount: MangoAccount,
serum3Market: Serum3Market,
marketName: string,
): Promise<TransactionSignature> {
const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!;
return await this.program.methods
.serum3CreateOpenOrders()
.accounts({
@ -337,6 +377,92 @@ export class MangoClient {
.rpc();
}
public async serum3PlaceOrder(
group: Group,
mangoAccount: MangoAccount,
serum3ProgramId: PublicKey,
serum3MarketName: string,
side: Serum3Side,
limitPrice: number,
maxBaseQty: number,
maxNativeQuoteQtyIncludingFees: number,
selfTradeBehavior: Serum3SelfTradeBehavior,
orderType: Serum3OrderType,
clientOrderId: number,
limit: number,
) {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC');
mangoAccount = await this.getMangoAccount(mangoAccount);
}
const serum3MarketExternal = await Market.load(
this.program.provider.connection,
serum3Market.serumMarketExternal,
{ commitment: this.program.provider.connection.commitment },
serum3ProgramId,
);
const serum3MarketExternalVaultSigner =
await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
serum3ProgramId,
);
const healthRemainingAccounts: PublicKey[] =
await this.buildHealthRemainingAccounts(group, mangoAccount);
return await this.program.methods
.serum3PlaceOrder(
side,
new BN(limitPrice),
new BN(maxBaseQty),
new BN(maxNativeQuoteQtyIncludingFees),
selfTradeBehavior,
orderType,
new BN(clientOrderId),
limit,
)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: this.program.provider.wallet.publicKey,
openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex)
?.openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: serum3ProgramId,
serumMarketExternal: serum3Market.serumMarketExternal,
marketBids: serum3MarketExternal.bidsAddress,
marketAsks: serum3MarketExternal.asksAddress,
marketEventQueue: serum3MarketExternal.decoded.eventQueue,
marketRequestQueue: serum3MarketExternal.decoded.requestQueue,
marketBaseVault: serum3MarketExternal.decoded.baseVault,
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
marketVaultSigner: serum3MarketExternalVaultSigner,
quoteBank: group.findBank(serum3Market.quoteTokenIndex)?.publicKey,
quoteVault: group.findBank(serum3Market.quoteTokenIndex)?.vault,
baseBank: group.findBank(serum3Market.baseTokenIndex)?.publicKey,
baseVault: group.findBank(serum3Market.baseTokenIndex)?.vault,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
),
)
.rpc();
}
/// static
static async connect(
@ -383,40 +509,43 @@ export class MangoClient {
private async buildHealthRemainingAccounts(
group: Group,
mangoAccount: MangoAccount,
bank: Bank,
bank?: Bank /** TODO for serum3PlaceOrde we are just ingoring this atm */,
) {
const healthRemainingAccounts: PublicKey[] = [];
{
const tokenIndices = mangoAccount.tokens
.filter((token) => token.tokenIndex !== 65535)
.map((token) => token.tokenIndex);
tokenIndices.push(bank.tokenIndex);
const mintInfos = await Promise.all(
[...new Set(tokenIndices)].map(async (tokenIndex) =>
getMintInfoForTokenIndex(this, group.publicKey, tokenIndex),
),
);
healthRemainingAccounts.push(
...mintInfos.flatMap((mintinfos) => {
return mintinfos.flatMap((mintinfo) => {
return mintinfo.bank;
});
}),
);
healthRemainingAccounts.push(
...mintInfos.flatMap((mintinfos) => {
return mintinfos.flatMap((mintinfo) => {
return mintinfo.oracle;
});
}),
);
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
const tokenIndices = mangoAccount.tokens
.filter((token) => token.tokenIndex !== 65535)
.map((token) => token.tokenIndex);
if (bank) {
tokenIndices.push(bank.tokenIndex);
}
const mintInfos = await Promise.all(
[...new Set(tokenIndices)].map(async (tokenIndex) =>
getMintInfoForTokenIndex(this, group.publicKey, tokenIndex),
),
);
healthRemainingAccounts.push(
...mintInfos.flatMap((mintinfos) => {
return mintinfos.flatMap((mintinfo) => {
return mintinfo.bank;
});
}),
);
healthRemainingAccounts.push(
...mintInfos.flatMap((mintinfos) => {
return mintinfos.flatMap((mintinfo) => {
return mintinfo.oracle;
});
}),
);
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
return healthRemainingAccounts;
}
}

35
ts/constants.ts Normal file
View File

@ -0,0 +1,35 @@
import { PublicKey } from '@solana/web3.js';
export const DEVNET_GROUP = '6ACH752p6FsdLzuociVkmDwc3wJW8pcCoxZKfXJKfKcD';
export const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'],
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
]);
export const DEVNET_MINTS_REVERSE = Array.from(DEVNET_MINTS.entries()).reduce(
function (map, obj) {
map[obj[1]] = obj[0];
return map;
},
{},
);
export const DEVNET_ORACLES = new Map([
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
]);
export const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
]);
export const DEVNET_SERUM3_MARKETS_REVERSE = Array.from(
DEVNET_SERUM3_MARKETS.entries(),
).reduce(function (map, obj) {
map[obj[1]] = obj[0];
return map;
}, {});
export const DEVNET_SERUM3_PROGRAM_ID = new PublicKey(
'DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY',
);

View File

@ -35,6 +35,11 @@ import {
Serum3Side,
} from './accounts/types/serum3';
import { MangoClient } from './client';
import {
DEVNET_MINTS,
DEVNET_ORACLES,
DEVNET_SERUM3_PROGRAM_ID,
} from './constants';
import { findOrCreate } from './utils';
//
@ -86,9 +91,7 @@ async function main() {
//
// Find existing or register new oracles
//
const usdcDevnetMint = new PublicKey(
'8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN',
);
const usdcDevnetMint = new PublicKey(DEVNET_MINTS['USDC']);
const usdcDevnetStubOracle = await findOrCreate<StubOracle>(
'stubOracle',
getStubOracleForGroupAndMint,
@ -99,12 +102,8 @@ async function main() {
console.log(
`usdcDevnetStubOracle ${usdcDevnetStubOracle.publicKey.toBase58()}`,
);
const btcDevnetMint = new PublicKey(
'3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU',
);
const btcDevnetOracle = new PublicKey(
'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J',
);
const btcDevnetMint = new PublicKey(DEVNET_MINTS['BTC']);
const btcDevnetOracle = new PublicKey(DEVNET_ORACLES['BTC']);
//
// Find existing or register new tokens
@ -185,9 +184,7 @@ async function main() {
//
// Find existing or register a new serum3 market
//
const serumProgramId = new web3.PublicKey(
'DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY',
);
const serumMarketExternalPk = new web3.PublicKey(
'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB',
);
@ -200,7 +197,7 @@ async function main() {
adminClient,
group.publicKey,
admin.publicKey,
serumProgramId,
DEVNET_SERUM3_PROGRAM_ID,
serumMarketExternalPk,
usdcBank.publicKey,
btcBank.publicKey,
@ -219,7 +216,7 @@ async function main() {
group.publicKey,
mangoAccount.publicKey,
serum3Market.publicKey,
serumProgramId,
DEVNET_SERUM3_PROGRAM_ID,
serumMarketExternalPk,
user.publicKey,
);
@ -317,7 +314,7 @@ async function main() {
userClient.program.provider.connection,
serumMarketExternalPk,
{ commitment: userClient.program.provider.connection.commitment },
serumProgramId,
DEVNET_SERUM3_PROGRAM_ID,
);
const serum3MarketExternalVaultSigner = await PublicKey.createProgramAddress(
[
@ -328,7 +325,7 @@ async function main() {
8,
),
],
serumProgramId,
DEVNET_SERUM3_PROGRAM_ID,
);
const clientOrderId = Date.now();
await serum3PlaceOrder(
@ -338,7 +335,7 @@ async function main() {
user.publicKey,
mangoAccount.serum3[0].openOrders,
serum3Market.publicKey,
serumProgramId,
DEVNET_SERUM3_PROGRAM_ID,
serumMarketExternalPk,
serum3MarketExternal.bidsAddress,
serum3MarketExternal.asksAddress,

View File

@ -2,6 +2,12 @@ import { Provider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from './client';
import {
DEVNET_MINTS,
DEVNET_ORACLES,
DEVNET_SERUM3_MARKETS,
DEVNET_SERUM3_PROGRAM_ID,
} from './constants';
//
// An example for admins based on high level api i.e. the client
@ -31,20 +37,14 @@ async function main() {
console.log(`Group ${group.publicKey}`);
// register token 0
const btcDevnetMint = new PublicKey(
'3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU',
);
const btcDevnetOracle = new PublicKey(
'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J',
);
const btcDevnetMint = new PublicKey(DEVNET_MINTS['BTC']);
const btcDevnetOracle = new PublicKey(DEVNET_ORACLES['BTC']);
try {
await client.registerToken(group, btcDevnetMint, btcDevnetOracle, 0);
} catch (error) {}
// stub oracle + register token 1
const usdcDevnetMint = new PublicKey(
'8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN',
);
const usdcDevnetMint = new PublicKey(DEVNET_MINTS['USDC']);
try {
await client.createStubOracle(group, usdcDevnetMint, 1.0);
} catch (error) {}
@ -67,24 +67,21 @@ async function main() {
}
// register serum market
const serumProgramId = new PublicKey(
'DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY',
);
const serumMarketExternalPk = new PublicKey(
'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB',
DEVNET_SERUM3_MARKETS['BTC/USDC'],
);
try {
} catch (error) {
await client.serum3RegisterMarket(
group,
serumProgramId,
DEVNET_SERUM3_PROGRAM_ID,
serumMarketExternalPk,
banks[0],
banks[1],
0,
);
}
const markets = await client.serum3GetMarketForBaseAndQuote(
const markets = await client.serum3GetMarket(
group,
banks[0].tokenIndex,
banks[1].tokenIndex,

View File

@ -1,8 +1,13 @@
import { Provider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoAccount } from './accounts/types/mangoAccount';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from './accounts/types/serum3';
import { MangoClient } from './client';
import { DEVNET_GROUP, DEVNET_SERUM3_PROGRAM_ID } from './constants';
//
// An example for users based on high level api i.e. the client
@ -25,42 +30,39 @@ async function main() {
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroup(
new PublicKey('6ACH752p6FsdLzuociVkmDwc3wJW8pcCoxZKfXJKfKcD'),
);
console.log(`Group ${group.publicKey}`);
// fetch banks
const banks = await client.getBanksForGroup(group);
for (const bank of banks) {
console.log(`Bank ${bank.tokenIndex} ${bank.publicKey}`);
}
const group = await client.getGroup(new PublicKey(DEVNET_GROUP));
console.log(`Group ${group.publicKey.toBase58()}`);
// create + fetch account
let mangoAccounts: MangoAccount[] = [];
let mangoAccount: MangoAccount;
mangoAccounts = await client.getMangoAccount(group, user.publicKey);
if (mangoAccounts.length === 0) {
await client.createMangoAccount(group, 0);
mangoAccounts = await client.getMangoAccount(group, user.publicKey);
}
mangoAccount = mangoAccounts[0];
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
0,
);
console.log(`MangoAccount ${mangoAccount.publicKey}`);
// deposit and withdraw
console.log(`Depositing...1000`);
await client.deposit(group, mangoAccount, banks[0], 1000);
console.log(`Withdrawing...500`);
await client.withdraw(group, mangoAccount, banks[0], 500, false);
console.log(`Depositing...1000000`);
await client.deposit(group, mangoAccount, 'USDC', 1000000);
console.log(`Withdrawing...500000`);
await client.withdraw(group, mangoAccount, 'USDC', 500000, false);
// serum3
const markets = await client.serum3GetMarketForBaseAndQuote(
console.log(`Placing serum3 order`);
await client.serum3PlaceOrder(
group,
banks[1].tokenIndex,
banks[0].tokenIndex,
mangoAccount,
DEVNET_SERUM3_PROGRAM_ID,
'BTC/USDC',
Serum3Side.bid,
40000,
1,
1000000,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
10,
);
console.log(markets);
await client.serum3CreateOpenOrders(group, mangoAccount, markets[0]);
process.exit();
}