⚙️Cranking
Common crank instructions that keep our exchange moving
Github repository: https://github.com/zetamarkets/sdk/tree/main/examples/cranking
There are multiple permissionless instructions that "crank", keeping the zeta platform in an up to date state. This example runs all the necessary instructions.
Crank event queue: Process maker fill events to ensure that user margin accounts are in the correct state after trades occur.
Update pricing: Recalculate mark prices and greeks for our products on an expiry basis. This will also update perp funding rates.
Rebalance insurance vault: Moves funds collected from fees between margin accounts and the insurance vault (which is used to ensure platform security).
Apply perp funding: Periodically apply any unpaid funding to all margin accounts holding perp positions.
Prune expired TIF orders: When an order of TIF expires it must be pruned to get deleted. This happens automatically if a counterparty tries to trade against it however.
Code
require("dotenv").config();
import {
Wallet,
Exchange,
Network,
utils,
programTypes,
constants,
Market,
assets,
types,
} from "@zetamarkets/sdk";
import { PublicKey, Connection, Keypair } from "@solana/web3.js";
let network: Network;
switch (process.env["network"]) {
case "localnet":
network = Network.LOCALNET;
break;
case "devnet":
network = Network.DEVNET;
break;
case "mainnet":
network = Network.MAINNET;
break;
default:
throw Error("Unsupported network type!");
}
const NETWORK_URL = process.env["network_url"]!;
const MAX_ACCOUNTS_TO_FETCH = 99;
let crankingMarkets = new Array(constants.ACTIVE_MARKETS - 1).fill(false);
console.log(NETWORK_URL);
const assetList = [constants.Asset.BTC];
export async function sleep(ms: number) {
await new Promise((resolve) => setTimeout(resolve, ms, undefined));
}
async function main() {
// Generate a new keypair for wallet otherwise load from a private key.
const userKey = Keypair.generate();
const wallet = new Wallet(userKey);
// Load from private_key stored in .env file.
// const privateKey = Keypair.fromSecretKey(
// new Uint8Array(JSON.parse(Buffer.from(process.env.private_key).toString()))
// );
// const wallet = new Wallet(privateKey);
// Create a solana web3 connection to devnet.
const connection = new Connection(NETWORK_URL, "confirmed");
// Airdropping SOL.
// Only able to be done on localnet/devnet.
if (network != Network.MAINNET) {
await connection.requestAirdrop(wallet.publicKey, 100000000);
}
const loadExchangeConfig = types.defaultLoadExchangeConfig(
network,
connection,
utils.defaultCommitment(),
0, // ThrottleMs - increase if you are running into rate limit issues on startup.
true
);
// We load the exchange with a valid wallet containing SOL to call permissionless zeta functions.
await Exchange.load(loadExchangeConfig, wallet);
// Display state of zeta markets
setInterval(
async function () {
utils.displayState();
},
process.env.DISPLAY_STATE_INTERVAL
? parseInt(process.env.DISPLAY_STATE_INTERVAL)
: 30000
);
/**
* Cranks the serum dex event queue for each zeta market. This will process trades that consist of maker fills.
* All other events are atomically processed at time of call such as taker fills and cancels.
* Functionality here will keep track of markets that are currently being cranked, markets that have empty event queues
* as well as allowing specification of whether only live markets are being cranked.
* This will flush event queues completely upon call.
* This function will poll all market event queues asynchronously so is quite expensive in terms of RPC requests per second.
* Use crankExchangeThrottle if you are running into rate limit issues.
*/
setInterval(
async function () {
assetList.map(async (asset) => {
await utils.crankMarket(asset);
});
},
process.env.CRANK_EXCHANGE_INTERVAL
? parseInt(process.env.CRANK_EXCHANGE_INTERVAL)
: 5000
);
// Update pricing on live markets
setInterval(
async function () {
await updatePricing();
},
process.env.UPDATE_PRICING_INTERVAL
? parseInt(process.env.UPDATE_PRICING_INTERVAL)
: 5000
);
// Apply funding to any users holding perp positions
setInterval(
async function () {
await applyFunding();
}.bind(this),
process.env.APPLY_FUNDING_INTERVAL
? parseInt(process.env.APPLY_FUNDING_INTERVAL)
: 120_000
);
setInterval(
async function () {
await pruneExpiredTIFOrders();
}.bind(this),
process.env.PRUNE_EXPIRED_TIF_INTERVAL
? parseInt(process.env.PRUNE_EXPIRED_TIF_INTERVAL)
: 60000
);
// Rebalance zeta vault and insurance fund
setInterval(
async function () {
await rebalanceInsuranceVault();
}.bind(this),
process.env.REBALANCE_INSURANCE_VAULT_INTERVAL
? parseInt(process.env.REBALANCE_INSURANCE_VAULT_INTERVAL)
: 20000
);
}
/**
* This calls zeta's permissionless update pricing function through the Exchange object.
* Cranks zeta's on-chain pricing ensuring all our greeks and theos are up-to-date.
*/
async function updatePricing() {
// Get relevant expiry indices.
await Promise.all(
assetList.map(async (asset) => {
try {
console.log(`[${assets.assetToName(asset)}] Update pricing`);
await Exchange.updatePricing(asset);
} catch (e) {
console.error(
`[${assets.assetToName(asset)}] Update pricing failed. ${e}`
);
}
})
);
}
async function applyFunding() {
let marginAccPubkeys = [];
try {
marginAccPubkeys = await utils.getAllProgramAccountAddresses(
types.ProgramAccountType.MarginAccount
);
} catch (e) {
throw Error("Account address fetch error on applyFunding()!");
}
for (
let i = 0;
i < marginAccPubkeys.length;
i += constants.MAX_FUNDING_ACCOUNTS
) {
// Grab set of margin accounts
let marginAccounts = [];
try {
marginAccounts =
await Exchange.program.account.marginAccount.fetchMultiple(
marginAccPubkeys.slice(i, i + constants.MAX_FUNDING_ACCOUNTS)
);
} catch (e) {
throw Error("Account data fetch error on applyFunding()!");
}
// In that set: Check if any have non-zero perp positions
// If they do, apply funding on them
let fundingAccounts = new Map<constants.Asset, PublicKey[]>();
for (var asset of Exchange.assets) {
fundingAccounts.set(asset, []);
}
for (let j = 0; j < marginAccounts.length; j++) {
if (marginAccounts[j].perpProductLedger.position.size != 0) {
fundingAccounts
.get(assets.fromProgramAsset(marginAccounts[j].asset))
.push(marginAccPubkeys[i + j]);
}
}
// This will automatically break into multiple txs if there are too many
for (var asset of Exchange.assets) {
await utils.applyPerpFunding(asset, fundingAccounts.get(asset));
}
}
}
async function pruneExpiredTIFOrders() {
let subExchanges = Exchange.getAllSubExchanges();
try {
await Promise.all(
subExchanges.map((se) => {
return utils.pruneExpiredTIFOrders(se.asset);
})
);
console.log("Pruned expired TIF orders.");
} catch (e) {
console.log("Failed to prune expired TIF orders.", `Error=${e}`);
}
}
/**
* Rebalances the zeta vault and the insurance vault to ensure consistent platform security.
* Checks all margin accounts for non-zero rebalance amounts and rebalances them all.
*/
async function rebalanceInsuranceVault() {
let marginAccPubkeys: PublicKey[];
try {
marginAccPubkeys = await utils.getAllProgramAccountAddresses(
types.ProgramAccountType.MarginAccount
);
} catch (e) {
console.log(
"rebalanceInsuranceVault account address fetch error",
`Error=${e}`
);
}
for (let i = 0; i < marginAccPubkeys.length; i += MAX_ACCOUNTS_TO_FETCH) {
let marginAccs: any[];
try {
marginAccs = await Exchange.program.account.marginAccount.fetchMultiple(
marginAccPubkeys.slice(i, i + MAX_ACCOUNTS_TO_FETCH)
);
} catch (e) {
console.log(
"rebalanceInsuranceVault margin account fetch error",
`Error=${e}`
);
}
let remainingAccounts: any[] = new Array();
for (let j = 0; j < marginAccs.length; j++) {
if (marginAccs[j].rebalanceAmount.toNumber() != 0) {
remainingAccounts.push({
pubkey: marginAccPubkeys[i + j],
isSigner: false,
isWritable: true,
});
}
}
console.log(
`[REBALANCE INSURANCE VAULT] for ${remainingAccounts.length} accounts.`
);
try {
await Exchange.rebalanceInsuranceVault(remainingAccounts);
} catch (e) {
console.log(
"rebalanceInsuranceVault vault error on transaction",
`Error=${e}`
);
}
}
}
main().catch(console.error.bind(console));
Last updated