Merge branch 'master' into feat/update-market-info-over-ws

This commit is contained in:
taureau75
2022-02-05 20:48:29 -08:00
committed by GitHub
3 changed files with 175 additions and 52 deletions

View File

@@ -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. 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`. 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: To run the marketmaker:

View File

@@ -1,6 +1,10 @@
{ {
"cryptowatchApiKey": "aaaaxxx", "cryptowatchApiKey": "aaaaxxx",
"ethPrivKey": "ccccxxxxx", "ethPrivKey": "",
"ethPrivKeys": [
"",
""
],
"zigzagChainId": 1, "zigzagChainId": 1,
"zigzagWsUrl": "wss://zigzag-exchange.herokuapp.com", "zigzagWsUrl": "wss://zigzag-exchange.herokuapp.com",
"pairs": { "pairs": {

View File

@@ -11,8 +11,7 @@ dotenv.config();
const PRICE_FEEDS = {}; const PRICE_FEEDS = {};
const OPEN_ORDERS = {}; const OPEN_ORDERS = {};
const NONCES = {}; const NONCES = {};
let ACCOUNT_STATE = null; const WALLETS = {};
let ORDER_BROADCASTING = false;
const FILL_QUEUE = []; const FILL_QUEUE = [];
const MARKETS = {}; const MARKETS = {};
@@ -37,26 +36,43 @@ console.log("ACTIVE PAIRS", activePairs);
// Start price feeds // Start price feeds
cryptowatchWsSetup(); cryptowatchWsSetup();
// Initiate fill loop
setTimeout(processFillQueue, 1000);
// Connect to zksync // Connect to zksync
const CHAIN_ID = parseInt(MM_CONFIG.zigzagChainId); const CHAIN_ID = parseInt(MM_CONFIG.zigzagChainId);
const ETH_NETWORK = (CHAIN_ID === 1) ? "mainnet" : "rinkeby"; const ETH_NETWORK = (CHAIN_ID === 1) ? "mainnet" : "rinkeby";
let syncWallet, ethersProvider, syncProvider, ethWallet, let ethersProvider, syncProvider, fillOrdersInterval, indicateLiquidityInterval;
fillOrdersInterval, indicateLiquidityInterval;
ethersProvider = ethers.getDefaultProvider(ETH_NETWORK); ethersProvider = ethers.getDefaultProvider(ETH_NETWORK);
try { try {
syncProvider = await zksync.getDefaultProvider(ETH_NETWORK); syncProvider = await zksync.getDefaultProvider(ETH_NETWORK);
ethWallet = new ethers.Wallet(process.env.ETH_PRIVKEY || MM_CONFIG.ethPrivKey); const keys = [];
syncWallet = await zksync.Wallet.fromEthSigner(ethWallet, syncProvider); const ethPrivKey = (process.env.ETH_PRIVKEY || MM_CONFIG.ethPrivKey);
if (!(await syncWallet.isSigningKeySet())) { if(ethPrivKey && ethPrivKey != "") { keys.push(ethPrivKey); }
console.log("setting sign key"); const ethPrivKeys = (process.env.ETH_PRIVKEYS || MM_CONFIG.ethPrivKeys);
const signKeyResult = await syncWallet.setSigningKey({ if(ethPrivKeys && ethPrivKeys.length > 0) {
feeToken: "ETH", ethPrivKeys.forEach( key => {
ethAuthType: "ECDSA", if(key != "" && !keys.includes(key)) {
keys.push(key);
}
}); });
console.log(signKeyResult); }
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({
feeToken: "ETH",
ethAuthType: "ECDSA",
});
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) { } catch (e) {
console.log(e); console.log(e);
@@ -64,9 +80,33 @@ try {
} }
// Update account state loop // Update account state loop
await updateAccountState();
setInterval(updateAccountState, 30000); 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); let zigzagws = new WebSocket(MM_CONFIG.zigzagWsUrl);
zigzagws.on('open', onWsOpen); zigzagws.on('open', onWsOpen);
zigzagws.on('error', console.error); zigzagws.on('error', console.error);
@@ -86,7 +126,9 @@ function onWsOpen() {
function onWsClose () { function onWsClose () {
console.log("Websocket closed. Restarting"); console.log("Websocket closed. Restarting");
ORDER_BROADCASTING = false; Object.keys(WALLETS).forEach(accountId => {
WALLETS[accountId]['ORDER_BROADCASTING'] = false;
});
setTimeout(() => { setTimeout(() => {
clearInterval(fillOrdersInterval) clearInterval(fillOrdersInterval)
clearInterval(indicateLiquidityInterval) clearInterval(indicateLiquidityInterval)
@@ -101,7 +143,9 @@ async function handleMessage(json) {
if (!(["lastprice", "liquidity2", "fillstatus"]).includes(msg.op)) console.log(json.toString()); if (!(["lastprice", "liquidity2", "fillstatus"]).includes(msg.op)) console.log(json.toString());
switch(msg.op) { switch(msg.op) {
case 'error': case 'error':
ORDER_BROADCASTING = false; Object.keys(WALLETS).forEach(accountId => {
WALLETS[accountId]['ORDER_BROADCASTING'] = false;
});
break; break;
case 'orders': case 'orders':
const orders = msg.args[0]; const orders = msg.args[0];
@@ -110,7 +154,7 @@ async function handleMessage(json) {
const fillable = isOrderFillable(order); const fillable = isOrderFillable(order);
console.log(fillable); console.log(fillable);
if (fillable.fillable) { if (fillable.fillable) {
FILL_QUEUE.push(order); FILL_QUEUE.push({ order: order, wallets: fillable.wallets});
} }
else if (fillable.reason === "badprice") { else if (fillable.reason === "badprice") {
OPEN_ORDERS[orderid] = order; OPEN_ORDERS[orderid] = order;
@@ -120,12 +164,19 @@ async function handleMessage(json) {
case "userordermatch": case "userordermatch":
const chainid = msg.args[0]; const chainid = msg.args[0];
const orderid = msg.args[1]; const orderid = msg.args[1];
try { const fillOrder = msg.args[3];
await broadcastfill(chainid, orderid, msg.args[2], msg.args[3]); const wallet = WALLETS[fillOrder.accountId];
} catch (e) { if(!wallet) {
console.error(e); console.error("No wallet with this accountId: "+fillOrder.accountId);
break
} else {
try {
await broadcastfill(chainid, orderid, msg.args[2], fillOrder, wallet);
} catch (e) {
console.error(e);
}
wallet['ORDER_BROADCASTING'] = false;
} }
ORDER_BROADCASTING = false;
break break
case "marketinfo": case "marketinfo":
const market_info = msg.args[0]; const market_info = msg.args[0];
@@ -154,7 +205,13 @@ function isOrderFillable(order) {
const sellCurrency = (side === 's') ? market.quoteAsset.symbol : market.baseAsset.symbol; const sellCurrency = (side === 's') ? market.quoteAsset.symbol : market.baseAsset.symbol;
const sellDecimals = (side === 's') ? market.quoteAsset.decimals : market.baseAsset.decimals; const sellDecimals = (side === 's') ? market.quoteAsset.decimals : market.baseAsset.decimals;
const sellQuantity = (side === 's') ? quoteQuantity : baseQuantity; 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; const now = Date.now() / 1000 | 0;
if (now > expires) { if (now > expires) {
@@ -172,7 +229,7 @@ function isOrderFillable(order) {
return { fillable: false, reason: "badsize" }; return { fillable: false, reason: "badsize" };
} }
if (balance < sellQuantity) { if (goodWallets.length === 0) {
return { fillable: false, reason: "badbalance" }; return { fillable: false, reason: "badbalance" };
} }
@@ -190,7 +247,7 @@ function isOrderFillable(order) {
return { fillable: false, reason: "badprice" }; return { fillable: false, reason: "badprice" };
} }
return { fillable: true, reason: null }; return { fillable: true, reason: null, wallets: goodWallets};
} }
function genquote(chainid, market_id, side, baseQuantity) { function genquote(chainid, market_id, side, baseQuantity) {
@@ -240,6 +297,7 @@ function validatePriceFeed(market_id) {
const primaryPrice = PRICE_FEEDS[primaryPriceFeedId]; const primaryPrice = PRICE_FEEDS[primaryPriceFeedId];
if (!primaryPrice) throw new Error("Primary price feed unavailable"); if (!primaryPrice) throw new Error("Primary price feed unavailable");
// If there is no secondary price feed, the price auto-validates // If there is no secondary price feed, the price auto-validates
if (!secondaryPriceFeedId) return true; if (!secondaryPriceFeedId) return true;
@@ -256,7 +314,7 @@ function validatePriceFeed(market_id) {
return true; return true;
} }
async function sendfillrequest(orderreceipt) { async function sendfillrequest(orderreceipt, accountId) {
const chainId = orderreceipt[0]; const chainId = orderreceipt[0];
const orderId = orderreceipt[1]; const orderId = orderreceipt[1];
const market_id = orderreceipt[2]; const market_id = orderreceipt[2];
@@ -297,16 +355,16 @@ async function sendfillrequest(orderreceipt) {
ratio: zksync.utils.tokenRatio(tokenRatio), ratio: zksync.utils.tokenRatio(tokenRatio),
validUntil: one_min_expiry validUntil: one_min_expiry
} }
const fillOrder = await syncWallet.getOrder(orderDetails); const fillOrder = await WALLETS[accountId].syncWallet.getOrder(orderDetails);
// Set global flag // Set wallet flag
ORDER_BROADCASTING = true; WALLETS[accountId]['ORDER_BROADCASTING'] = true;
const resp = { op: "fillrequest", args: [chainId, orderId, fillOrder] }; const resp = { op: "fillrequest", args: [chainId, orderId, fillOrder] };
zigzagws.send(JSON.stringify(resp)); zigzagws.send(JSON.stringify(resp));
} }
async function broadcastfill(chainid, orderid, swapOffer, fillOrder) { async function broadcastfill(chainid, orderid, swapOffer, fillOrder, wallet) {
// Nonce check // Nonce check
const nonce = swapOffer.nonce; const nonce = swapOffer.nonce;
const userNonce = NONCES[swapOffer.accountId]; const userNonce = NONCES[swapOffer.accountId];
@@ -315,7 +373,7 @@ async function broadcastfill(chainid, orderid, swapOffer, fillOrder) {
} }
const randint = (Math.random()*1000).toFixed(0); const randint = (Math.random()*1000).toFixed(0);
console.time('syncswap' + randint); console.time('syncswap' + randint);
const swap = await syncWallet.syncSwap({ const swap = await wallet['syncWallet'].syncSwap({
orders: [swapOffer, fillOrder], orders: [swapOffer, fillOrder],
feeToken: "ETH", feeToken: "ETH",
nonce: fillOrder.nonce nonce: fillOrder.nonce
@@ -351,7 +409,7 @@ async function fillOpenOrders() {
const order = OPEN_ORDERS[orderid]; const order = OPEN_ORDERS[orderid];
const fillable = isOrderFillable(order); const fillable = isOrderFillable(order);
if (fillable.fillable) { if (fillable.fillable) {
FILL_QUEUE.push(order); FILL_QUEUE.push({ order: order, wallets: fillable.wallets});
delete OPEN_ORDERS[orderid]; delete OPEN_ORDERS[orderid];
} }
else if (fillable.reason !== "badprice") { else if (fillable.reason !== "badprice") {
@@ -361,22 +419,33 @@ async function fillOpenOrders() {
} }
async function processFillQueue() { async function processFillQueue() {
if (ORDER_BROADCASTING) {
setTimeout(processFillQueue, 100);
return false;
}
if (FILL_QUEUE.length === 0) { if (FILL_QUEUE.length === 0) {
setTimeout(processFillQueue, 100); setTimeout(processFillQueue, 100);
return false; return;
} }
const order = FILL_QUEUE.shift(); await Promise.all(Object.keys(WALLETS).map(async accountId => {
try { const wallet = WALLETS[accountId];
await sendfillrequest(order); if (wallet['ORDER_BROADCASTING']) {
} catch (e) { return;
console.error(e); }
ORDER_BROADCASTING = false; let index = 0;
} for(;index<FILL_QUEUE.length; index++) {
setTimeout(processFillQueue, 50); if(FILL_QUEUE[index].wallets.includes(accountId)) {
break;
}
}
if (index < FILL_QUEUE.length) {
const selectedOrder = FILL_QUEUE.splice(index, 1);
try {
await sendfillrequest(selectedOrder[0].order, accountId);
return;
} catch (e) {
console.error(e);
wallet['ORDER_BROADCASTING'] = false;
}
}
}));
setTimeout(processFillQueue, 100);
} }
async function cryptowatchWsSetup() { async function cryptowatchWsSetup() {
@@ -457,8 +526,16 @@ function indicateLiquidity (market_id) {
const expires = (Date.now() / 1000 | 0) + 10; // 10s expiry const expires = (Date.now() / 1000 | 0) + 10; // 10s expiry
const side = mmConfig.side || 'd'; 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 maxSellSize = Math.min(baseBalance, mmConfig.maxSize);
const maxBuySize = Math.min(quoteBalance / midPrice, mmConfig.maxSize); const maxBuySize = Math.min(quoteBalance / midPrice, mmConfig.maxSize);
@@ -492,5 +569,45 @@ function getMidPrice (market_id) {
} }
async function updateAccountState() { 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
}
} }