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.
|
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:
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
191
marketmaker.js
191
marketmaker.js
@@ -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,19 +36,27 @@ 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(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())) {
|
if (!(await syncWallet.isSigningKeySet())) {
|
||||||
console.log("setting sign key");
|
console.log("setting sign key");
|
||||||
const signKeyResult = await syncWallet.setSigningKey({
|
const signKeyResult = await syncWallet.setSigningKey({
|
||||||
@@ -58,15 +65,48 @@ try {
|
|||||||
});
|
});
|
||||||
console.log(signKeyResult);
|
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);
|
||||||
throw new Error("Could not connect to zksync API");
|
throw new Error("Could not connect to zksync API");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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];
|
||||||
|
const fillOrder = msg.args[3];
|
||||||
|
const wallet = WALLETS[fillOrder.accountId];
|
||||||
|
if(!wallet) {
|
||||||
|
console.error("No wallet with this accountId: "+fillOrder.accountId);
|
||||||
|
break
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
await broadcastfill(chainid, orderid, msg.args[2], msg.args[3]);
|
await broadcastfill(chainid, orderid, msg.args[2], fillOrder, wallet);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
ORDER_BROADCASTING = false;
|
wallet['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 => {
|
||||||
|
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 {
|
try {
|
||||||
await sendfillrequest(order);
|
await sendfillrequest(selectedOrder[0].order, accountId);
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ORDER_BROADCASTING = false;
|
wallet['ORDER_BROADCASTING'] = false;
|
||||||
}
|
}
|
||||||
setTimeout(processFillQueue, 50);
|
}
|
||||||
|
}));
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user