From d491cd3a55e760fa15bba0159822575e0845df51 Mon Sep 17 00:00:00 2001 From: vic-en Date: Tue, 16 Feb 2021 09:09:59 +0100 Subject: [PATCH 1/8] (feat) add Perpetual finance --- package.json | 4 +- src/app.js | 2 + src/routes/perpetual_finance.route.js | 513 ++++++++++++++++++++++++++ src/services/perpetual_finance.js | 261 +++++++++++++ 4 files changed, 779 insertions(+), 1 deletion(-) create mode 100644 src/routes/perpetual_finance.route.js create mode 100644 src/services/perpetual_finance.js diff --git a/package.json b/package.json index fd26718..5dd0a91 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "@perp/contract": "^1.0.6", "@balancer-labs/sor": "^0.3.3", "@terra-money/terra.js": "^0.5.8", "@uniswap/sdk": "^3.0.3", @@ -30,7 +31,8 @@ "moment": "^2.29.1", "util": "^0.12.3", "winston": "^3.3.3", - "winston-daily-rotate-file": "^4.5.0" + "winston-daily-rotate-file": "^4.5.0", + "cross-fetch": "^3.0.6" }, "devDependencies": { "@babel/core": "^7.11.6", diff --git a/src/app.js b/src/app.js index 76371c7..6b2e7a0 100644 --- a/src/app.js +++ b/src/app.js @@ -14,6 +14,7 @@ import balancerRoutes from './routes/balancer.route' import ethRoutes from './routes/eth.route' import terraRoutes from './routes/terra.route' import uniswapRoutes from './routes/uniswap.route' +import perpFiRoutes from './routes/perpetual_finance.route' // terminate if environment not found const result = dotenv.config(); @@ -46,6 +47,7 @@ app.use('/eth', ethRoutes); // app.use('/celo', celoRoutes); app.use('/terra', terraRoutes); app.use('/balancer', balancerRoutes); +app.use('/perpfi', perpFiRoutes); app.get('/', (req, res, next) => { res.send('ok') diff --git a/src/routes/perpetual_finance.route.js b/src/routes/perpetual_finance.route.js new file mode 100644 index 0000000..03427a6 --- /dev/null +++ b/src/routes/perpetual_finance.route.js @@ -0,0 +1,513 @@ +import { ethers, BigNumber } from 'ethers'; +import express from 'express'; + +import { getParamData, latency, statusMessages } from '../services/utils'; +import { logger } from '../services/logger'; +import PerpetualFinance from '../services/perpetual_finance'; + +require('dotenv').config() + +const router = express.Router() +const perpFi = new PerpetualFinance(process.env.ETHEREUM_CHAIN) +// perpFi.generate_tokens() +// setTimeout(perpFi.update_pairs.bind(perpFi), 2000) + +const getErrorMessage = (err) => { + /* + [WIP] Custom error message based-on string match + */ + let message = err + return message +} + +router.get('/', async (req, res) => { + /* + GET / + */ + res.status(200).json({ + network: perpFi.network, + provider: perpFi.provider.connection.url, + loadedMetadata: perpFi.loadedMetadata, + connection: true, + timestamp: Date.now(), + }) +}) + +router.get('/load-metadata', async (req, res) => { + /* + GET / + */ + const loadedMetadata = await perpFi.load_metadata() + res.status(200).json({ + network: perpFi.network, + provider: perpFi.provider.connection.url, + loadedMetadata: loadedMetadata, + connection: true, + timestamp: Date.now(), + }) +}) + +router.post('/balances', async (req, res) => { + /* + POST: /balances + x-www-form-urlencoded: { + privateKey:{{privateKey}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + const balances = {} + balances["XDAI"] = await perpFi.getXdaiBalance(wallet) + balances["USDC"] = await perpFi.getUSDCBalance(wallet) + try { + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + balances: balances + }) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/allowances', async (req, res) => { + /* + POST: /allowances + x-www-form-urlencoded: { + privateKey:{{privateKey}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + const approvals = {} + approvals["USDC"] = await perpFi.getAllowance(wallet) + try { + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + approvals: approvals + }) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/approve', async (req, res) => { + /* + POST: /approve + x-www-form-urlencoded: { + privateKey:{{privateKey}} + amount:{{amount}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + let amount + paramData.amount ? amount = paramData.amount + : amount = '1000000000' + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + try { + // call approve function + const approval = await perpFi.approve(wallet, amount) + logger.info('perpFi.route - Approving allowance') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + amount: amount, + approval: approval + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/open', async (req, res) => { + /* + POST: /open + x-www-form-urlencoded: { + side:{{side}} + pair:{{pair}} + margin:{{margin}} + leverage:{{leverage}} + privateKey:{{privateKey}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const side = paramData.side + const pair = paramData.pair + const margin = paramData.margin + const leverage = paramData.leverage + const privateKey = paramData.privateKey + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + try { + // call openPosition function + const tx = await perpFi.openPosition(side, margin, leverage, pair, wallet) + logger.info('perpFi.route - Opening position') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + margin: margin, + side: side, + leverage: leverage, + txHash: tx.hash + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/close', async (req, res) => { + /* + POST: /close + x-www-form-urlencoded: { + privateKey:{{privateKey}} + pair:{{pair}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + const pair = paramData.pair + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + try { + // call closePosition function + const tx = await perpFi.closePosition(wallet, pair) + logger.info('perpFi.route - Closing position') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + txHash: tx.hash + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + + +router.post('/position', async (req, res) => { + /* + POST: /position + x-www-form-urlencoded: { + privateKey:{{privateKey}} + pair:{{pair}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + const pair = paramData.pair + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + try { + // call getPosition function + const position = await perpFi.getPosition(wallet, pair) + logger.info('perpFi.route - getting active position') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + position: position + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/margin', async (req, res) => { + /* + POST: /margin + x-www-form-urlencoded: { + privateKey:{{privateKey}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + let wallet + try { + wallet = new ethers.Wallet(privateKey, perpFi.provider) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + + try { + // call getAllBalances function + const allBalances = await perpFi.getActiveMargin(wallet) + logger.info('perpFi.route - Getting all balances') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + margin: allBalances + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/receipt', async (req, res) => { + /* + POST: /receipt + x-www-form-urlencoded: { + txHash:{{txHash}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const txHash = paramData.txHash + const txReceipt = await perpFi.provider.getTransactionReceipt(txHash) + const receipt = {} + const confirmed = txReceipt && txReceipt.blockNumber ? true : false + if (txReceipt.blockNumber) { + receipt.gasUsed = ethers.utils.formatEther(txReceipt.gasUsed) + receipt.blockNumber = txReceipt.blockNumber + receipt.confirmations = txReceipt.confirmations + receipt.status = txReceipt.status + } + logger.info(`eth.route - Get TX Receipt: ${txHash}`, { message: JSON.stringify(receipt) }) + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + txHash: txHash, + confirmed: confirmed, + receipt: receipt, + }) + return txReceipt +}) + +router.post('/price', async (req, res) => { + /* + POST: /price + x-www-form-urlencoded: { + side:{{side}} + pair:{{pair}} + amount:{{amount}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const side = paramData.side + const pair = paramData.pair + const amount = paramData.amount + + try { + // call getPrice function + const price = await perpFi.getPrice(side, amount, pair) + logger.info('perpFi.route - Getting price') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + side: side, + price: price + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + + +router.get('/pairs', async (req, res) => { + /* + GET + */ + const initTime = Date.now() + + try { + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + pairs: Object.keys(perpFi.amm) + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/funding', async (req, res) => { + /* + POST: /funding + x-www-form-urlencoded: { + pair:{{pair}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const pair = paramData.pair + + try { + // call getFee function + const fr = await perpFi.getFundingRate(pair) + logger.info('perpFi.route - Getting funding info') + // submit response + res.status(200).json({ + network: perpFi.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + fr: fr + }) + } catch (err) { + logger.error(req.originalUrl, { message: err }) + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +export default router; diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js new file mode 100644 index 0000000..77bf0dd --- /dev/null +++ b/src/services/perpetual_finance.js @@ -0,0 +1,261 @@ +import { logger } from './logger'; + +const fetch = require('cross-fetch'); + +const Ethers = require('ethers') +const AmmArtifact = require("@perp/contract/build/contracts/Amm.json") +const ClearingHouseArtifact = require("@perp/contract/build/contracts/ClearingHouse.json") +const RootBridgeArtifact = require("@perp/contract/build/contracts/RootBridge.json") +const ClientBridgeArtifact = require("@perp/contract/build/contracts/ClientBridge.json") +const ClearingHouseViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.json") +const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken.json") + +const GAS_LIMIT = 150688; +const DEFAULT_DECIMALS = 18 +const CONTRACT_ADDRESSES = "https://metadata.perp.exchange/" +const XDAI_PROVIDER = "https://rpc.xdaichain.com" +const PNL_OPTION_SPOT_PRICE = 0 + +const sleep = (milliseconds) => { + return new Promise(resolve => setTimeout(resolve, milliseconds)) +} + + +export default class PerpetualFinance { + constructor (network = 'mainnet') { + this.providerUrl = XDAI_PROVIDER + this.network = process.env.ETHEREUM_CHAIN + this.provider = new Ethers.providers.JsonRpcProvider(this.providerUrl) + this.gasLimit = GAS_LIMIT + this.contractAddressesUrl = CONTRACT_ADDRESSES + this.amm = {} + + + switch (network) { + case 'mainnet': + this.contractAddressesUrl += 'production.json'; + break; + case 'kovan': + this.contractAddressesUrl += 'staging.json'; + break; + default: + const err = `Invalid network ${network}` + logger.error(err) + throw Error(err) + } + + this.loadedMetadata = this.load_metadata() + + } + + async load_metadata() { + try{ + const metadata = await fetch(this.contractAddressesUrl).then(res => res.json()) + const layer2 = Object.keys(metadata.layers.layer2.contracts) + + for (var key of layer2){ + if (metadata.layers.layer2.contracts[key].name === "Amm") { + this.amm[key] = metadata.layers.layer2.contracts[key].address; + } else{ + this[key] = metadata.layers.layer2.contracts[key].address; + } + } + + this.layer2AmbAddr = metadata.layers.layer2.externalContracts.ambBridgeOnXDai + this.xUsdcAddr = metadata.layers.layer2.externalContracts.usdc + this.loadedMetadata = true + return true + } catch(err) { + return false + } + + } + + // get XDai balance + async getXdaiBalance (wallet) { + try { + const xDaiBalance = await wallet.getBalance() + return Ethers.utils.formatEther(xDaiBalance) + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error xDai balance lookup' + return reason + } + } + + // get XDai USDC balance + async getUSDCBalance (wallet) { + try { + const layer2Usdc = new Ethers.Contract(this.xUsdcAddr, TetherTokenArtifact.abi, wallet) + let layer2UsdcBalance = await layer2Usdc.balanceOf(wallet.address) + const layer2UsdcDecimals = await layer2Usdc.decimals() + return Ethers.utils.formatUnits(layer2UsdcBalance, layer2UsdcDecimals) + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error balance lookup' + return reason + } + } + + // get allowance + async getAllowance (wallet) { + // instantiate a contract and pass in provider for read-only access + const layer2Usdc = new Ethers.Contract(this.xUsdcAddr, TetherTokenArtifact.abi, wallet) + + try { + const allowanceForClearingHouse = await layer2Usdc.allowance( + wallet.address, + this.ClearingHouse + ) + + return Ethers.utils.formatUnits(allowanceForClearingHouse, DEFAULT_DECIMALS) + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error allowance lookup' + return reason + } + } + + // approve + async approve (wallet, amount) { + try { + // instantiate a contract and pass in wallet + const layer2Usdc = new Ethers.Contract(this.xUsdcAddr, TetherTokenArtifact.abi, wallet) + const tx = await layer2Usdc.approve(this.ClearingHouse, Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS)) + // TO-DO: We may want to supply custom gasLimit value above + return tx.hash + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error approval' + return reason + } + } + + //open Position + async openPosition(side, margin, levrg, pair, wallet) { + try { + const quoteAssetAmount = { d: Ethers.utils.parseUnits(margin, DEFAULT_DECIMALS) } + const leverage = { d: Ethers.utils.parseUnits(levrg, DEFAULT_DECIMALS) } + const minBaseAssetAmount = { d: "0" } // "0" can be automatically converted + const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) + const tx = await clearingHouse.openPosition( + this.amm[pair], + side, + quoteAssetAmount, + leverage, + minBaseAssetAmount + ) + return tx + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error opening position' + return reason + } + } + + //close Position + async closePosition(wallet, pair) { + try { + const minimalQuoteAsset = {d: "0"} + const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) + const tx = await clearingHouse.closePosition(this.amm[pair], minimalQuoteAsset) + return tx + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error closing position' + return reason + } + } + + //get active position + async getPosition(wallet, pair) { + try { + const positionValues = {} + const clearingHouseViewer = new Ethers.Contract(this.ClearingHouseViewer, ClearingHouseViewerArtifact.abi, wallet) + await Promise.allSettled([clearingHouseViewer.getPersonalPositionWithFundingPayment(this.amm[pair], + wallet.address), + clearingHouseViewer.getUnrealizedPnl(this.amm[pair], + wallet.address, + Ethers.BigNumber.from(PNL_OPTION_SPOT_PRICE))]) + .then(values => {positionValues.openNotional = Ethers.utils.formatUnits(values[0].value.openNotional.d, DEFAULT_DECIMALS); + positionValues.size = Ethers.utils.formatUnits(values[0].value.size.d, DEFAULT_DECIMALS); + positionValues.pnl = Ethers.utils.formatUnits(values[1].value.d);}) + + positionValues.entryPrice = Math.abs(positionValues.openNotional / positionValues.size) + return positionValues + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error getting active position' + return reason + } + } + + //get active margin + async getActiveMargin(wallet) { + try { + const clearingHouseViewer = new Ethers.Contract(this.ClearingHouseViewer, ClearingHouseViewerArtifact.abi, wallet) + const activeMargin = await clearingHouseViewer.getPersonalBalanceWithFundingPayment( + this.xUsdcAddr, + wallet.address) + return activeMargin / 1e18.toString() + } catch (err) { + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error getting active position' + return reason + } + } + + // get Price + async getPrice(side, amount, pair) { + try { + let price + const amm = new Ethers.Contract(this.amm[pair], AmmArtifact.abi, this.provider) + if (side === "buy") { + price = await amm.getInputPrice(0, {d: Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS) }) + price = amount / Ethers.utils.formatUnits(price.d) + } else { + price = await amm.getOutputPrice(0, {d: Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS) }) + price = Ethers.utils.formatUnits(price.d) / amount + } + return price + } catch (err) { + console.log(err) + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error getting Price' + return reason + } + } + + // get getFundingRate + async getFundingRate(pair) { + try { + let funding = {} + const amm = new Ethers.Contract(this.amm[pair], AmmArtifact.abi, this.provider) + await Promise.allSettled([amm.getUnderlyingTwapPrice(3600), + amm.getTwapPrice(3600), + amm.nextFundingTime()]) + .then(values => {funding.indexPrice = Ethers.utils.formatUnits(values[0].value.d); + funding.markPrice = Ethers.utils.formatUnits(values[1].value.d); + funding.nextFundingTime = values[2].value.toString();}) + + funding.rate = ((funding.markPrice - funding.indexPrice) / 24) / funding.indexPrice + return funding + } catch (err) { + console.log(err) + logger.error(err)() + let reason + err.reason ? reason = err.reason : reason = 'error getting fee' + return reason + } + } + +} From 281d0dc4e6d3786fa0523ce7256855b3014496dd Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 22 Feb 2021 13:33:52 +0100 Subject: [PATCH 2/8] (fix) fix some minor errors due to type conversion --- package-lock.json | 13 +++++++++++++ src/routes/perpetual_finance.route.js | 2 +- src/services/perpetual_finance.js | 18 ++++++++++-------- yarn.lock | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8a94b52 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "gateway-api", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "yarn": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.10.tgz", + "integrity": "sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA==" + } + } +} diff --git a/src/routes/perpetual_finance.route.js b/src/routes/perpetual_finance.route.js index 03427a6..e8b5394 100644 --- a/src/routes/perpetual_finance.route.js +++ b/src/routes/perpetual_finance.route.js @@ -396,7 +396,7 @@ router.post('/receipt', async (req, res) => { const txReceipt = await perpFi.provider.getTransactionReceipt(txHash) const receipt = {} const confirmed = txReceipt && txReceipt.blockNumber ? true : false - if (txReceipt.blockNumber) { + if (txReceipt !== null) { receipt.gasUsed = ethers.utils.formatEther(txReceipt.gasUsed) receipt.blockNumber = txReceipt.blockNumber receipt.confirmations = txReceipt.confirmations diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index 77bf0dd..62ec64e 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -12,8 +12,8 @@ const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken. const GAS_LIMIT = 150688; const DEFAULT_DECIMALS = 18 -const CONTRACT_ADDRESSES = "https://metadata.perp.exchange/" -const XDAI_PROVIDER = "https://rpc.xdaichain.com" +const CONTRACT_ADDRESSES = 'https://metadata.perp.exchange/' +const XDAI_PROVIDER = 'https://dai.poa.network' const PNL_OPTION_SPOT_PRICE = 0 const sleep = (milliseconds) => { @@ -24,7 +24,7 @@ const sleep = (milliseconds) => { export default class PerpetualFinance { constructor (network = 'mainnet') { this.providerUrl = XDAI_PROVIDER - this.network = process.env.ETHEREUM_CHAIN + this.network = network this.provider = new Ethers.providers.JsonRpcProvider(this.providerUrl) this.gasLimit = GAS_LIMIT this.contractAddressesUrl = CONTRACT_ADDRESSES @@ -140,7 +140,7 @@ export default class PerpetualFinance { try { const quoteAssetAmount = { d: Ethers.utils.parseUnits(margin, DEFAULT_DECIMALS) } const leverage = { d: Ethers.utils.parseUnits(levrg, DEFAULT_DECIMALS) } - const minBaseAssetAmount = { d: "0" } // "0" can be automatically converted + const minBaseAssetAmount = { d: Ethers.utils.parseUnits("0", DEFAULT_DECIMALS) } const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) const tx = await clearingHouse.openPosition( this.amm[pair], @@ -149,6 +149,7 @@ export default class PerpetualFinance { leverage, minBaseAssetAmount ) + console.log(tx) return tx } catch (err) { logger.error(err) @@ -161,8 +162,9 @@ export default class PerpetualFinance { //close Position async closePosition(wallet, pair) { try { - const minimalQuoteAsset = {d: "0"} + const minimalQuoteAsset = { d: Ethers.utils.parseUnits("0", DEFAULT_DECIMALS) } const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) + console.log(this.amm[pair]) const tx = await clearingHouse.closePosition(this.amm[pair], minimalQuoteAsset) return tx } catch (err) { @@ -243,9 +245,9 @@ export default class PerpetualFinance { await Promise.allSettled([amm.getUnderlyingTwapPrice(3600), amm.getTwapPrice(3600), amm.nextFundingTime()]) - .then(values => {funding.indexPrice = Ethers.utils.formatUnits(values[0].value.d); - funding.markPrice = Ethers.utils.formatUnits(values[1].value.d); - funding.nextFundingTime = values[2].value.toString();}) + .then(values => {funding.indexPrice = parseFloat(Ethers.utils.formatUnits(values[0].value.d)); + funding.markPrice = parseFloat(Ethers.utils.formatUnits(values[1].value.d)); + funding.nextFundingTime = parseInt(values[2].value.toString());}) funding.rate = ((funding.markPrice - funding.indexPrice) / 24) / funding.indexPrice return funding diff --git a/yarn.lock b/yarn.lock index 6a19a4e..1a4e3d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1219,6 +1219,11 @@ "@ethersproject/properties" "^5.0.3" "@ethersproject/strings" "^5.0.4" +"@perp/contract@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@perp/contract/-/contract-1.0.6.tgz#b423738d095a15fccd17de7bc46a531482a45d18" + integrity sha512-5exstpCstXpXSLaxY/hTVT3BtRF8UVJ2JgGN8abiFSKBg+aaQdBZ4qvsOzr4HQ0fkZSOlURL/JnOUDV04UrD5A== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -1859,6 +1864,13 @@ create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" + integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3284,6 +3296,11 @@ node-environment-flags@^1.0.5: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" +node-fetch@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" From 2dc5cec78ba8f0b71771ec147b89a87c0331cbd1 Mon Sep 17 00:00:00 2001 From: vic-en Date: Tue, 23 Feb 2021 13:33:40 +0100 Subject: [PATCH 3/8] (feat) add PERPFI postman collection for testing purpose --- test/postman/PERPFI.postman_collection.json | 454 ++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 test/postman/PERPFI.postman_collection.json diff --git a/test/postman/PERPFI.postman_collection.json b/test/postman/PERPFI.postman_collection.json new file mode 100644 index 0000000..513a083 --- /dev/null +++ b/test/postman/PERPFI.postman_collection.json @@ -0,0 +1,454 @@ +{ + "info": { + "_postman_id": "08bf4528-5e70-42cd-bf71-b97f81088b9c", + "name": "PERPFI", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "default", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/perpfi/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/load-metadata", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/perpfi/load-metadata", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "load-metadata" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/pairs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/perpfi/pairs", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "pairs" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/balances", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/balances", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "balances" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/allowances", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/allowances", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "allowances" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/approve", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "amount", + "value": "10", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/approve", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "approve" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/open", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "margin", + "value": "15", + "type": "text" + }, + { + "key": "leverage", + "value": "2", + "type": "text" + }, + { + "key": "side", + "value": "{{SHORT}}", + "type": "text" + }, + { + "key": "pair", + "value": "SNXUSDC", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/open", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "open" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/close", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "pair", + "value": "SNXUSDC", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/close", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "close" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/receipt", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "txHash", + "value": "0xd29120d947319c880f68a44b897c733c07ec313d670a7df55e523b501d7a03a2", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/receipt", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "receipt" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/position", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "pair", + "value": "SNXUSDC", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/position", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "position" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/margin", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/margin", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "margin" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/pnl", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "pair", + "value": "SNXUSDC", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/pnl", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "pnl" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/funding", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "amount", + "value": "1", + "type": "text" + }, + { + "key": "pair", + "value": "SNXUSDC", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/funding", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "funding" + ] + } + }, + "response": [] + }, + { + "name": "perpfi/price", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "side", + "value": "buy", + "type": "text" + }, + { + "key": "pair", + "value": "SNXUSDC", + "type": "text" + }, + { + "key": "amount", + "value": "1", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/perpfi/price", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "perpfi", + "price" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file From f2a297fc8e532cdee3c71e0188f3bed7fd5cfdb5 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 24 Feb 2021 20:12:38 +0100 Subject: [PATCH 4/8] (feat) add funding payment and slippage parameters to open and close functions --- src/routes/perpetual_finance.route.js | 13 +++++++--- src/services/perpetual_finance.js | 35 +++++++++++++++------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/routes/perpetual_finance.route.js b/src/routes/perpetual_finance.route.js index e8b5394..f953b9e 100644 --- a/src/routes/perpetual_finance.route.js +++ b/src/routes/perpetual_finance.route.js @@ -191,6 +191,7 @@ router.post('/open', async (req, res) => { pair:{{pair}} margin:{{margin}} leverage:{{leverage}} + minBaseAssetAmount:{{minBaseAssetAmount}} privateKey:{{privateKey}} } */ @@ -200,6 +201,8 @@ router.post('/open', async (req, res) => { const pair = paramData.pair const margin = paramData.margin const leverage = paramData.leverage + const minBaseAssetAmount = paramData.minBaseAssetAmount + console.log(minBaseAssetAmount) const privateKey = paramData.privateKey let wallet try { @@ -217,7 +220,7 @@ router.post('/open', async (req, res) => { try { // call openPosition function - const tx = await perpFi.openPosition(side, margin, leverage, pair, wallet) + const tx = await perpFi.openPosition(side, margin, leverage, pair, minBaseAssetAmount, wallet) logger.info('perpFi.route - Opening position') // submit response res.status(200).json({ @@ -227,6 +230,7 @@ router.post('/open', async (req, res) => { margin: margin, side: side, leverage: leverage, + minBaseAssetAmount: minBaseAssetAmount, txHash: tx.hash }) } catch (err) { @@ -244,12 +248,14 @@ router.post('/close', async (req, res) => { /* POST: /close x-www-form-urlencoded: { + minimalQuoteAsset:{{minimalQuoteAsset}} privateKey:{{privateKey}} pair:{{pair}} } */ const initTime = Date.now() const paramData = getParamData(req.body) + const minimalQuoteAsset = paramData.minimalQuoteAsset const privateKey = paramData.privateKey const pair = paramData.pair let wallet @@ -268,13 +274,14 @@ router.post('/close', async (req, res) => { try { // call closePosition function - const tx = await perpFi.closePosition(wallet, pair) + const tx = await perpFi.closePosition(wallet, pair, minimalQuoteAsset) logger.info('perpFi.route - Closing position') // submit response res.status(200).json({ network: perpFi.network, timestamp: initTime, latency: latency(initTime, Date.now()), + minimalQuoteAsset: minimalQuoteAsset, txHash: tx.hash }) } catch (err) { @@ -489,7 +496,7 @@ router.post('/funding', async (req, res) => { const pair = paramData.pair try { - // call getFee function + // call getFundingRate function const fr = await perpFi.getFundingRate(pair) logger.info('perpFi.route - Getting funding info') // submit response diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index 62ec64e..8b75017 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -136,11 +136,11 @@ export default class PerpetualFinance { } //open Position - async openPosition(side, margin, levrg, pair, wallet) { + async openPosition(side, margin, levrg, pair, minBaseAmount, wallet) { try { const quoteAssetAmount = { d: Ethers.utils.parseUnits(margin, DEFAULT_DECIMALS) } const leverage = { d: Ethers.utils.parseUnits(levrg, DEFAULT_DECIMALS) } - const minBaseAssetAmount = { d: Ethers.utils.parseUnits("0", DEFAULT_DECIMALS) } + const minBaseAssetAmount = { d: Ethers.utils.parseUnits(minBaseAmount, DEFAULT_DECIMALS) } const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) const tx = await clearingHouse.openPosition( this.amm[pair], @@ -149,7 +149,6 @@ export default class PerpetualFinance { leverage, minBaseAssetAmount ) - console.log(tx) return tx } catch (err) { logger.error(err) @@ -160,11 +159,10 @@ export default class PerpetualFinance { } //close Position - async closePosition(wallet, pair) { + async closePosition(wallet, pair, minimalQuote) { try { - const minimalQuoteAsset = { d: Ethers.utils.parseUnits("0", DEFAULT_DECIMALS) } + const minimalQuoteAsset = { d: Ethers.utils.parseUnits(minimalQuote, DEFAULT_DECIMALS) } const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) - console.log(this.amm[pair]) const tx = await clearingHouse.closePosition(this.amm[pair], minimalQuoteAsset) return tx } catch (err) { @@ -179,17 +177,24 @@ export default class PerpetualFinance { async getPosition(wallet, pair) { try { const positionValues = {} - const clearingHouseViewer = new Ethers.Contract(this.ClearingHouseViewer, ClearingHouseViewerArtifact.abi, wallet) - await Promise.allSettled([clearingHouseViewer.getPersonalPositionWithFundingPayment(this.amm[pair], - wallet.address), - clearingHouseViewer.getUnrealizedPnl(this.amm[pair], - wallet.address, - Ethers.BigNumber.from(PNL_OPTION_SPOT_PRICE))]) - .then(values => {positionValues.openNotional = Ethers.utils.formatUnits(values[0].value.openNotional.d, DEFAULT_DECIMALS); - positionValues.size = Ethers.utils.formatUnits(values[0].value.size.d, DEFAULT_DECIMALS); - positionValues.pnl = Ethers.utils.formatUnits(values[1].value.d);}) + const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) + let premIndex = 0 + await Promise.allSettled([clearingHouse.getPosition(this.amm[pair], + wallet.address), + clearingHouse.getLatestCumulativePremiumFraction(this.amm[pair]), + clearingHouse.getPositionNotionalAndUnrealizedPnl(this.amm[pair], + wallet.address, + Ethers.BigNumber.from(PNL_OPTION_SPOT_PRICE))]) + .then(values => {positionValues.openNotional = Ethers.utils.formatUnits(values[0].value.openNotional.d, DEFAULT_DECIMALS); + positionValues.size = Ethers.utils.formatUnits(values[0].value.size.d, DEFAULT_DECIMALS); + positionValues.margin = Ethers.utils.formatUnits(values[0].value.margin.d, DEFAULT_DECIMALS); + positionValues.cumulativePremiumFraction = Ethers.utils.formatUnits(values[0].value.lastUpdatedCumulativePremiumFraction.d, DEFAULT_DECIMALS); + premIndex = Ethers.utils.formatUnits(values[1].value.d, DEFAULT_DECIMALS); + positionValues.pnl = Ethers.utils.formatUnits(values[2].value.unrealizedPnl.d, DEFAULT_DECIMALS); + positionValues.positionNotional = Ethers.utils.formatUnits(values[2].value.positionNotional.d, DEFAULT_DECIMALS);}) positionValues.entryPrice = Math.abs(positionValues.openNotional / positionValues.size) + positionValues.fundingPayment = (premIndex - positionValues.cumulativePremiumFraction) * positionValues.size * -1 return positionValues } catch (err) { logger.error(err) From e8bb4ab009273c2f4e1d2d22d0a08657b8a54d1e Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 1 Mar 2021 19:28:06 +0100 Subject: [PATCH 5/8] (feat) add cacheing for price and fix negation of funding payment --- src/routes/perpetual_finance.route.js | 3 +- src/services/perpetual_finance.js | 61 ++++++++++++++++++++------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/routes/perpetual_finance.route.js b/src/routes/perpetual_finance.route.js index f953b9e..f16b30e 100644 --- a/src/routes/perpetual_finance.route.js +++ b/src/routes/perpetual_finance.route.js @@ -9,8 +9,7 @@ require('dotenv').config() const router = express.Router() const perpFi = new PerpetualFinance(process.env.ETHEREUM_CHAIN) -// perpFi.generate_tokens() -// setTimeout(perpFi.update_pairs.bind(perpFi), 2000) +setTimeout(perpFi.update_price_loop.bind(perpFi), 2000) const getErrorMessage = (err) => { /* diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index 8b75017..2759ea9 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -10,15 +10,12 @@ const ClientBridgeArtifact = require("@perp/contract/build/contracts/ClientBridg const ClearingHouseViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.json") const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken.json") -const GAS_LIMIT = 150688; -const DEFAULT_DECIMALS = 18 -const CONTRACT_ADDRESSES = 'https://metadata.perp.exchange/' -const XDAI_PROVIDER = 'https://dai.poa.network' -const PNL_OPTION_SPOT_PRICE = 0 - -const sleep = (milliseconds) => { - return new Promise(resolve => setTimeout(resolve, milliseconds)) -} +const GAS_LIMIT = 150688; // 1,147,912 +const DEFAULT_DECIMALS = 18; +const CONTRACT_ADDRESSES = 'https://metadata.perp.exchange/'; +const XDAI_PROVIDER = 'https://dai.poa.network'; +const PNL_OPTION_SPOT_PRICE = 0; +const UPDATE_PERIOD = 60000; // stop updating prices after 30 secs from last request export default class PerpetualFinance { @@ -29,6 +26,9 @@ export default class PerpetualFinance { this.gasLimit = GAS_LIMIT this.contractAddressesUrl = CONTRACT_ADDRESSES this.amm = {} + this.priceCache = {} + this.cacheExpirary = {} + this.pairAmountCache = {} switch (network) { @@ -71,6 +71,27 @@ export default class PerpetualFinance { } + async update_price_loop() { + if (Object.keys(this.cacheExpirary).length > 0) { + for (let pair in this.cacheExpirary){ + if (this.cacheExpirary[pair] <= Date.now()) { + delete this.cacheExpirary[pair]; + delete this.priceCache[pair]; + } + } + + for (let pair in this.cacheExpirary){ + let amm = new Ethers.Contract(this.amm[pair], AmmArtifact.abi, this.provider) + await Promise.allSettled([amm.getInputPrice(0, {d: Ethers.utils.parseUnits(this.pairAmountCache[pair], DEFAULT_DECIMALS) }), + amm.getOutputPrice(0, {d: Ethers.utils.parseUnits(this.pairAmountCache[pair], DEFAULT_DECIMALS) })]) + .then(values => {if (!this.priceCache.hasOwnProperty(pair)) { this.priceCache[pair] = [] }; + this.priceCache[pair][0] = this.pairAmountCache[pair] / Ethers.utils.formatUnits(values[0].value.d); + this.priceCache[pair][1] = Ethers.utils.formatUnits(values[1].value.d) / this.pairAmountCache[pair];})} + + } + setTimeout(this.update_price_loop.bind(this), 2000); // update every 2 seconds + } + // get XDai balance async getXdaiBalance (wallet) { try { @@ -194,7 +215,7 @@ export default class PerpetualFinance { positionValues.positionNotional = Ethers.utils.formatUnits(values[2].value.positionNotional.d, DEFAULT_DECIMALS);}) positionValues.entryPrice = Math.abs(positionValues.openNotional / positionValues.size) - positionValues.fundingPayment = (premIndex - positionValues.cumulativePremiumFraction) * positionValues.size * -1 + positionValues.fundingPayment = (premIndex - positionValues.cumulativePremiumFraction) * positionValues.size // * -1 return positionValues } catch (err) { logger.error(err) @@ -224,13 +245,21 @@ export default class PerpetualFinance { async getPrice(side, amount, pair) { try { let price - const amm = new Ethers.Contract(this.amm[pair], AmmArtifact.abi, this.provider) - if (side === "buy") { - price = await amm.getInputPrice(0, {d: Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS) }) - price = amount / Ethers.utils.formatUnits(price.d) + this.cacheExpirary[pair] = Date.now() + UPDATE_PERIOD + this.pairAmountCache[pair] = amount + if (!this.priceCache.hasOwnProperty(pair)){ + const amm = new Ethers.Contract(this.amm[pair], AmmArtifact.abi, this.provider) + if (side === "buy") { + price = await amm.getInputPrice(0, {d: Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS) }) + price = amount / Ethers.utils.formatUnits(price.d) + } else { + price = await amm.getOutputPrice(0, {d: Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS) }) + price = Ethers.utils.formatUnits(price.d) / amount + } } else { - price = await amm.getOutputPrice(0, {d: Ethers.utils.parseUnits(amount, DEFAULT_DECIMALS) }) - price = Ethers.utils.formatUnits(price.d) / amount + if (side === "buy") { + price = this.priceCache[pair][0] + } else { price = this.priceCache[pair][1] } } return price } catch (err) { From e8e93d177da6aa86711532bf9f42640d0edde228 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 3 Mar 2021 02:31:17 +0100 Subject: [PATCH 6/8] (feat) add custom gas limit --- src/services/perpetual_finance.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index 2759ea9..ceb2c56 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -10,7 +10,7 @@ const ClientBridgeArtifact = require("@perp/contract/build/contracts/ClientBridg const ClearingHouseViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.json") const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken.json") -const GAS_LIMIT = 150688; // 1,147,912 +const GAS_LIMIT = 2000000; // 1,147,912 const DEFAULT_DECIMALS = 18; const CONTRACT_ADDRESSES = 'https://metadata.perp.exchange/'; const XDAI_PROVIDER = 'https://dai.poa.network'; @@ -168,7 +168,8 @@ export default class PerpetualFinance { side, quoteAssetAmount, leverage, - minBaseAssetAmount + minBaseAssetAmount, + { gasLimit: this.gasLimit } ) return tx } catch (err) { @@ -184,7 +185,7 @@ export default class PerpetualFinance { try { const minimalQuoteAsset = { d: Ethers.utils.parseUnits(minimalQuote, DEFAULT_DECIMALS) } const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) - const tx = await clearingHouse.closePosition(this.amm[pair], minimalQuoteAsset) + const tx = await clearingHouse.closePosition(this.amm[pair], minimalQuoteAsset, { gasLimit: this.gasLimit } ) return tx } catch (err) { logger.error(err) From c9fd55cfb87e360b6ae6773698483c3a6c65b032 Mon Sep 17 00:00:00 2001 From: vic-en Date: Fri, 5 Mar 2021 03:12:28 +0100 Subject: [PATCH 7/8] (feat) increase price polling interval --- src/services/perpetual_finance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index ceb2c56..79a7f68 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -89,7 +89,7 @@ export default class PerpetualFinance { this.priceCache[pair][1] = Ethers.utils.formatUnits(values[1].value.d) / this.pairAmountCache[pair];})} } - setTimeout(this.update_price_loop.bind(this), 2000); // update every 2 seconds + setTimeout(this.update_price_loop.bind(this), 10000); // update every 10 seconds } // get XDai balance From c046065027aed152026dc3565ae88c4063c536c9 Mon Sep 17 00:00:00 2001 From: vic-en Date: Sat, 6 Mar 2021 11:23:28 +0100 Subject: [PATCH 8/8] (feat) change perpfi gas limit --- src/services/perpetual_finance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index 79a7f68..7707cec 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -10,7 +10,7 @@ const ClientBridgeArtifact = require("@perp/contract/build/contracts/ClientBridg const ClearingHouseViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.json") const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken.json") -const GAS_LIMIT = 2000000; // 1,147,912 +const GAS_LIMIT = 2123456; const DEFAULT_DECIMALS = 18; const CONTRACT_ADDRESSES = 'https://metadata.perp.exchange/'; const XDAI_PROVIDER = 'https://dai.poa.network';