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/package.json b/package.json index 6997fab..e1efda5 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", @@ -31,7 +32,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..f16b30e --- /dev/null +++ b/src/routes/perpetual_finance.route.js @@ -0,0 +1,519 @@ +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) +setTimeout(perpFi.update_price_loop.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}} + minBaseAssetAmount:{{minBaseAssetAmount}} + 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 minBaseAssetAmount = paramData.minBaseAssetAmount + console.log(minBaseAssetAmount) + 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, minBaseAssetAmount, 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, + minBaseAssetAmount: minBaseAssetAmount, + 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: { + 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 + 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, 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) { + 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 !== null) { + 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 getFundingRate 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..7707cec --- /dev/null +++ b/src/services/perpetual_finance.js @@ -0,0 +1,298 @@ +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 = 2123456; +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 { + constructor (network = 'mainnet') { + this.providerUrl = XDAI_PROVIDER + this.network = network + this.provider = new Ethers.providers.JsonRpcProvider(this.providerUrl) + this.gasLimit = GAS_LIMIT + this.contractAddressesUrl = CONTRACT_ADDRESSES + this.amm = {} + this.priceCache = {} + this.cacheExpirary = {} + this.pairAmountCache = {} + + + 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 + } + + } + + 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), 10000); // update every 10 seconds + } + + // 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, 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(minBaseAmount, DEFAULT_DECIMALS) } + const clearingHouse = new Ethers.Contract(this.ClearingHouse, ClearingHouseArtifact.abi, wallet) + const tx = await clearingHouse.openPosition( + this.amm[pair], + side, + quoteAssetAmount, + leverage, + minBaseAssetAmount, + { gasLimit: this.gasLimit } + ) + 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, minimalQuote) { + 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, { gasLimit: this.gasLimit } ) + 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 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) + 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 + 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 { + if (side === "buy") { + price = this.priceCache[pair][0] + } else { price = this.priceCache[pair][1] } + } + 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 = 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 + } catch (err) { + console.log(err) + logger.error(err)() + let reason + err.reason ? reason = err.reason : reason = 'error getting fee' + return reason + } + } + +} 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 diff --git a/yarn.lock b/yarn.lock index 5962676..b12ecfa 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" @@ -1866,6 +1871,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" @@ -3291,6 +3303,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"