mirror of
https://github.com/ZigZagExchange/zksync-lite-market-maker.git
synced 2025-12-19 08:04:27 +01:00
Merge branch 'master' into feat/update-market-info-over-ws
This commit is contained in:
@@ -22,6 +22,8 @@ Copy the `config.json.EXAMPLE` file to `config.json` to get started.
|
||||
|
||||
Set your `eth_privkey` to be able to relay transactions. The ETH address with that private key should be loaded up with adequate funds for market making.
|
||||
|
||||
Currently zkSync needs around 5 seconds to process a single swap and generate the receipt. So there is a upper limit of 12 swaps per wallet per minute. To circumvent this, there is also the option to use the `eth_privkeys` array. Here you can add any number of private keys. Each should be loaded up with adequate funds for market making. The founds will be handled separately, therefor each additional wallet has the opportunity to process (at least) 12 more swaps per minute.
|
||||
|
||||
For now, you need a Cryptowatch API key to use the market maker. Once you obtain one, you can set the `cryptowatchApiKey` field in `config.json`.
|
||||
|
||||
To run the marketmaker:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"cryptowatchApiKey": "aaaaxxx",
|
||||
"ethPrivKey": "ccccxxxxx",
|
||||
"ethPrivKey": "",
|
||||
"ethPrivKeys": [
|
||||
"",
|
||||
""
|
||||
],
|
||||
"zigzagChainId": 1,
|
||||
"zigzagWsUrl": "wss://zigzag-exchange.herokuapp.com",
|
||||
"pairs": {
|
||||
|
||||
191
marketmaker.js
191
marketmaker.js
@@ -11,8 +11,7 @@ dotenv.config();
|
||||
const PRICE_FEEDS = {};
|
||||
const OPEN_ORDERS = {};
|
||||
const NONCES = {};
|
||||
let ACCOUNT_STATE = null;
|
||||
let ORDER_BROADCASTING = false;
|
||||
const WALLETS = {};
|
||||
const FILL_QUEUE = [];
|
||||
const MARKETS = {};
|
||||
|
||||
@@ -37,19 +36,27 @@ console.log("ACTIVE PAIRS", activePairs);
|
||||
// Start price feeds
|
||||
cryptowatchWsSetup();
|
||||
|
||||
// Initiate fill loop
|
||||
setTimeout(processFillQueue, 1000);
|
||||
|
||||
// Connect to zksync
|
||||
const CHAIN_ID = parseInt(MM_CONFIG.zigzagChainId);
|
||||
const ETH_NETWORK = (CHAIN_ID === 1) ? "mainnet" : "rinkeby";
|
||||
let syncWallet, ethersProvider, syncProvider, ethWallet,
|
||||
fillOrdersInterval, indicateLiquidityInterval;
|
||||
let ethersProvider, syncProvider, fillOrdersInterval, indicateLiquidityInterval;
|
||||
ethersProvider = ethers.getDefaultProvider(ETH_NETWORK);
|
||||
try {
|
||||
syncProvider = await zksync.getDefaultProvider(ETH_NETWORK);
|
||||
ethWallet = new ethers.Wallet(process.env.ETH_PRIVKEY || MM_CONFIG.ethPrivKey);
|
||||
syncWallet = await zksync.Wallet.fromEthSigner(ethWallet, syncProvider);
|
||||
const keys = [];
|
||||
const ethPrivKey = (process.env.ETH_PRIVKEY || MM_CONFIG.ethPrivKey);
|
||||
if(ethPrivKey && ethPrivKey != "") { keys.push(ethPrivKey); }
|
||||
const ethPrivKeys = (process.env.ETH_PRIVKEYS || MM_CONFIG.ethPrivKeys);
|
||||
if(ethPrivKeys && ethPrivKeys.length > 0) {
|
||||
ethPrivKeys.forEach( key => {
|
||||
if(key != "" && !keys.includes(key)) {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
for(let i=0; i<keys.length; i++) {
|
||||
let ethWallet = new ethers.Wallet(keys[i]);
|
||||
let syncWallet = await zksync.Wallet.fromEthSigner(ethWallet, syncProvider);
|
||||
if (!(await syncWallet.isSigningKeySet())) {
|
||||
console.log("setting sign key");
|
||||
const signKeyResult = await syncWallet.setSigningKey({
|
||||
@@ -58,15 +65,48 @@ try {
|
||||
});
|
||||
console.log(signKeyResult);
|
||||
}
|
||||
let accountId = await syncWallet.getAccountId();
|
||||
let account_state = await syncWallet.getAccountState();
|
||||
WALLETS[accountId] = {
|
||||
'ethWallet': ethWallet,
|
||||
'syncWallet': syncWallet,
|
||||
'account_state': account_state,
|
||||
'ORDER_BROADCASTING': false,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new Error("Could not connect to zksync API");
|
||||
}
|
||||
|
||||
// Update account state loop
|
||||
await updateAccountState();
|
||||
setInterval(updateAccountState, 30000);
|
||||
|
||||
|
||||
// Log mm balance over all accounts
|
||||
logBalance();
|
||||
setInterval(logBalance, 3 * 60 * 60 * 1000); // 3h
|
||||
|
||||
// Initiate fill loop
|
||||
setTimeout(processFillQueue, 1000);
|
||||
|
||||
// Get markets info
|
||||
const activePairsText = activePairs.join(',');
|
||||
const markets_url = `https://zigzag-markets.herokuapp.com/markets?chainid=${CHAIN_ID}&id=${activePairsText}`
|
||||
const markets = await fetch(markets_url).then(r => r.json());
|
||||
if (markets.error) {
|
||||
console.error(markets);
|
||||
throw new Error(markets.error);
|
||||
}
|
||||
const MARKETS = {};
|
||||
for (let i in markets) {
|
||||
const market = markets[i];
|
||||
MARKETS[market.id] = market;
|
||||
if (market.alias) {
|
||||
MARKETS[market.alias] = market;
|
||||
}
|
||||
}
|
||||
|
||||
let zigzagws = new WebSocket(MM_CONFIG.zigzagWsUrl);
|
||||
zigzagws.on('open', onWsOpen);
|
||||
zigzagws.on('error', console.error);
|
||||
@@ -86,7 +126,9 @@ function onWsOpen() {
|
||||
|
||||
function onWsClose () {
|
||||
console.log("Websocket closed. Restarting");
|
||||
ORDER_BROADCASTING = false;
|
||||
Object.keys(WALLETS).forEach(accountId => {
|
||||
WALLETS[accountId]['ORDER_BROADCASTING'] = false;
|
||||
});
|
||||
setTimeout(() => {
|
||||
clearInterval(fillOrdersInterval)
|
||||
clearInterval(indicateLiquidityInterval)
|
||||
@@ -101,7 +143,9 @@ async function handleMessage(json) {
|
||||
if (!(["lastprice", "liquidity2", "fillstatus"]).includes(msg.op)) console.log(json.toString());
|
||||
switch(msg.op) {
|
||||
case 'error':
|
||||
ORDER_BROADCASTING = false;
|
||||
Object.keys(WALLETS).forEach(accountId => {
|
||||
WALLETS[accountId]['ORDER_BROADCASTING'] = false;
|
||||
});
|
||||
break;
|
||||
case 'orders':
|
||||
const orders = msg.args[0];
|
||||
@@ -110,7 +154,7 @@ async function handleMessage(json) {
|
||||
const fillable = isOrderFillable(order);
|
||||
console.log(fillable);
|
||||
if (fillable.fillable) {
|
||||
FILL_QUEUE.push(order);
|
||||
FILL_QUEUE.push({ order: order, wallets: fillable.wallets});
|
||||
}
|
||||
else if (fillable.reason === "badprice") {
|
||||
OPEN_ORDERS[orderid] = order;
|
||||
@@ -120,12 +164,19 @@ async function handleMessage(json) {
|
||||
case "userordermatch":
|
||||
const chainid = msg.args[0];
|
||||
const orderid = msg.args[1];
|
||||
const fillOrder = msg.args[3];
|
||||
const wallet = WALLETS[fillOrder.accountId];
|
||||
if(!wallet) {
|
||||
console.error("No wallet with this accountId: "+fillOrder.accountId);
|
||||
break
|
||||
} else {
|
||||
try {
|
||||
await broadcastfill(chainid, orderid, msg.args[2], msg.args[3]);
|
||||
await broadcastfill(chainid, orderid, msg.args[2], fillOrder, wallet);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
ORDER_BROADCASTING = false;
|
||||
wallet['ORDER_BROADCASTING'] = false;
|
||||
}
|
||||
break
|
||||
case "marketinfo":
|
||||
const market_info = msg.args[0];
|
||||
@@ -154,7 +205,13 @@ function isOrderFillable(order) {
|
||||
const sellCurrency = (side === 's') ? market.quoteAsset.symbol : market.baseAsset.symbol;
|
||||
const sellDecimals = (side === 's') ? market.quoteAsset.decimals : market.baseAsset.decimals;
|
||||
const sellQuantity = (side === 's') ? quoteQuantity : baseQuantity;
|
||||
const balance = ACCOUNT_STATE.committed.balances[sellCurrency] / 10**sellDecimals;
|
||||
const neededBalanceBN = sellQuantity * 10**sellDecimals;
|
||||
const goodWallets = [];
|
||||
Object.keys(WALLETS).forEach(accountId => {
|
||||
if (WALLETS[accountId]['account_state'].committed.balances[sellCurrency] > (neededBalanceBN * 1.05)) {
|
||||
goodWallets.push(accountId);
|
||||
}
|
||||
});
|
||||
const now = Date.now() / 1000 | 0;
|
||||
|
||||
if (now > expires) {
|
||||
@@ -172,7 +229,7 @@ function isOrderFillable(order) {
|
||||
return { fillable: false, reason: "badsize" };
|
||||
}
|
||||
|
||||
if (balance < sellQuantity) {
|
||||
if (goodWallets.length === 0) {
|
||||
return { fillable: false, reason: "badbalance" };
|
||||
}
|
||||
|
||||
@@ -190,7 +247,7 @@ function isOrderFillable(order) {
|
||||
return { fillable: false, reason: "badprice" };
|
||||
}
|
||||
|
||||
return { fillable: true, reason: null };
|
||||
return { fillable: true, reason: null, wallets: goodWallets};
|
||||
}
|
||||
|
||||
function genquote(chainid, market_id, side, baseQuantity) {
|
||||
@@ -240,6 +297,7 @@ function validatePriceFeed(market_id) {
|
||||
const primaryPrice = PRICE_FEEDS[primaryPriceFeedId];
|
||||
if (!primaryPrice) throw new Error("Primary price feed unavailable");
|
||||
|
||||
|
||||
// If there is no secondary price feed, the price auto-validates
|
||||
if (!secondaryPriceFeedId) return true;
|
||||
|
||||
@@ -256,7 +314,7 @@ function validatePriceFeed(market_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function sendfillrequest(orderreceipt) {
|
||||
async function sendfillrequest(orderreceipt, accountId) {
|
||||
const chainId = orderreceipt[0];
|
||||
const orderId = orderreceipt[1];
|
||||
const market_id = orderreceipt[2];
|
||||
@@ -297,16 +355,16 @@ async function sendfillrequest(orderreceipt) {
|
||||
ratio: zksync.utils.tokenRatio(tokenRatio),
|
||||
validUntil: one_min_expiry
|
||||
}
|
||||
const fillOrder = await syncWallet.getOrder(orderDetails);
|
||||
const fillOrder = await WALLETS[accountId].syncWallet.getOrder(orderDetails);
|
||||
|
||||
// Set global flag
|
||||
ORDER_BROADCASTING = true;
|
||||
// Set wallet flag
|
||||
WALLETS[accountId]['ORDER_BROADCASTING'] = true;
|
||||
|
||||
const resp = { op: "fillrequest", args: [chainId, orderId, fillOrder] };
|
||||
zigzagws.send(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
async function broadcastfill(chainid, orderid, swapOffer, fillOrder) {
|
||||
async function broadcastfill(chainid, orderid, swapOffer, fillOrder, wallet) {
|
||||
// Nonce check
|
||||
const nonce = swapOffer.nonce;
|
||||
const userNonce = NONCES[swapOffer.accountId];
|
||||
@@ -315,7 +373,7 @@ async function broadcastfill(chainid, orderid, swapOffer, fillOrder) {
|
||||
}
|
||||
const randint = (Math.random()*1000).toFixed(0);
|
||||
console.time('syncswap' + randint);
|
||||
const swap = await syncWallet.syncSwap({
|
||||
const swap = await wallet['syncWallet'].syncSwap({
|
||||
orders: [swapOffer, fillOrder],
|
||||
feeToken: "ETH",
|
||||
nonce: fillOrder.nonce
|
||||
@@ -351,7 +409,7 @@ async function fillOpenOrders() {
|
||||
const order = OPEN_ORDERS[orderid];
|
||||
const fillable = isOrderFillable(order);
|
||||
if (fillable.fillable) {
|
||||
FILL_QUEUE.push(order);
|
||||
FILL_QUEUE.push({ order: order, wallets: fillable.wallets});
|
||||
delete OPEN_ORDERS[orderid];
|
||||
}
|
||||
else if (fillable.reason !== "badprice") {
|
||||
@@ -361,22 +419,33 @@ async function fillOpenOrders() {
|
||||
}
|
||||
|
||||
async function processFillQueue() {
|
||||
if (ORDER_BROADCASTING) {
|
||||
setTimeout(processFillQueue, 100);
|
||||
return false;
|
||||
}
|
||||
if (FILL_QUEUE.length === 0) {
|
||||
setTimeout(processFillQueue, 100);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
const order = FILL_QUEUE.shift();
|
||||
await Promise.all(Object.keys(WALLETS).map(async accountId => {
|
||||
const wallet = WALLETS[accountId];
|
||||
if (wallet['ORDER_BROADCASTING']) {
|
||||
return;
|
||||
}
|
||||
let index = 0;
|
||||
for(;index<FILL_QUEUE.length; index++) {
|
||||
if(FILL_QUEUE[index].wallets.includes(accountId)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index < FILL_QUEUE.length) {
|
||||
const selectedOrder = FILL_QUEUE.splice(index, 1);
|
||||
try {
|
||||
await sendfillrequest(order);
|
||||
await sendfillrequest(selectedOrder[0].order, accountId);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ORDER_BROADCASTING = false;
|
||||
wallet['ORDER_BROADCASTING'] = false;
|
||||
}
|
||||
setTimeout(processFillQueue, 50);
|
||||
}
|
||||
}));
|
||||
setTimeout(processFillQueue, 100);
|
||||
}
|
||||
|
||||
async function cryptowatchWsSetup() {
|
||||
@@ -457,8 +526,16 @@ function indicateLiquidity (market_id) {
|
||||
|
||||
const expires = (Date.now() / 1000 | 0) + 10; // 10s expiry
|
||||
const side = mmConfig.side || 'd';
|
||||
const baseBalance = ACCOUNT_STATE.committed.balances[marketInfo.baseAsset.symbol] / 10**marketInfo.baseAsset.decimals;
|
||||
const quoteBalance = ACCOUNT_STATE.committed.balances[marketInfo.quoteAsset.symbol] / 10**marketInfo.quoteAsset.decimals;
|
||||
|
||||
let baseBN = 0, quoteBN = 0;
|
||||
Object.keys(WALLETS).forEach(accountId => {
|
||||
const thisBase = WALLETS[accountId]['account_state'].committed.balances[marketInfo.baseAsset.symbol];
|
||||
const thisQuote = WALLETS[accountId]['account_state'].committed.balances[marketInfo.quoteAsset.symbol];
|
||||
baseBN = (baseBN < thisBase) ? thisBase : baseBN;
|
||||
quoteBN = (quoteBN < thisQuote) ? thisQuote : quoteBN;
|
||||
});
|
||||
const baseBalance = baseBN / 10**marketInfo.baseAsset.decimals;
|
||||
const quoteBalance = quoteBN / 10**marketInfo.quoteAsset.decimals;
|
||||
const maxSellSize = Math.min(baseBalance, mmConfig.maxSize);
|
||||
const maxBuySize = Math.min(quoteBalance / midPrice, mmConfig.maxSize);
|
||||
|
||||
@@ -492,5 +569,45 @@ function getMidPrice (market_id) {
|
||||
}
|
||||
|
||||
async function updateAccountState() {
|
||||
ACCOUNT_STATE = await syncWallet.getAccountState();
|
||||
try {
|
||||
Object.keys(WALLETS).forEach(accountId => {
|
||||
(WALLETS[accountId]['syncWallet']).getAccountState().then((state) => {
|
||||
WALLETS[accountId]['account_state'] = state;
|
||||
})
|
||||
});
|
||||
} catch(err) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
async function logBalance() {
|
||||
try {
|
||||
await updateAccountState();
|
||||
// fetch all balances over all wallets per token
|
||||
const balance = {};
|
||||
Object.keys(WALLETS).forEach(accountId => {
|
||||
const committedBalaces = WALLETS[accountId]['account_state'].committed.balances;
|
||||
Object.keys(committedBalaces).forEach(token => {
|
||||
if(balance[token]) {
|
||||
balance[token] = balance[token] + parseInt(committedBalaces[token]);
|
||||
} else {
|
||||
balance[token] = parseInt(committedBalaces[token]);
|
||||
}
|
||||
});
|
||||
});
|
||||
// get token price and total in USD
|
||||
let sum = 0;
|
||||
await Promise.all(Object.keys(balance).map(async token => {
|
||||
const price = await syncProvider.getTokenPrice(token.toString());
|
||||
const tokenNumber = await syncProvider.tokenSet.formatToken(token, balance[token].toString())
|
||||
sum = sum + price * tokenNumber;
|
||||
}));
|
||||
|
||||
// log to CVS
|
||||
const date = new Date().toISOString();
|
||||
const content = date + ";" + sum.toFixed(2) + "\n";
|
||||
fs.writeFile('price_csv.txt', content, { flag: 'a+' }, err => {});
|
||||
} catch(err) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user