import { Exchange } from "../types/ethers/Exchange" import { ExchangeReader} from "../types/ethers/ExchangeReader" import { SakePerpState } from "../types/ethers/SakePerpState" import { SakePerp } from "../types/ethers/SakePerp" import { SakePerpViewer } from "../types/ethers/SakePerpViewer" import { SakePerpVault } from "../types/ethers/SakePerpVault" import { SystemSettings } from "../types/ethers/SystemSettings" import { BigNumber } from "@ethersproject/bignumber" import { ethers, Wallet } from "ethers" import { EthMetadata, SystemMetadataFactory } from "./SystemMetadataFactory" import { EthService } from "./EthService" import { formatEther, parseEther } from "@ethersproject/units" import { Log } from "./Log" import { Overrides } from "@ethersproject/contracts" import { ServerProfile } from "./ServerProfile" import { Service } from "typedi" import Big from "big.js" import { TransactionResponse } from "@ethersproject/abstract-provider" import ExchangeArtifact from "@sakeperp/artifact/src/Exchange.json" import ExchangeReaderArtifact from "@sakeperp/artifact/src/ExchangeReader.json" import SakePerpArtifact from "@sakeperp/artifact/src/SakePerp.json" import SakePerpStateArtifact from "@sakeperp/artifact/src/SakePerpState.json" import SakePerpViewerArtifact from "@sakeperp/artifact/src/SakePerpViewer.json" import SakePerpVaultArtifact from "@sakeperp/artifact/src/SakePerpVault.json" import SystemSettingsArtifact from "@sakeperp/artifact/src/SystemSettings.json" export enum Side { BUY, SELL, } export enum PnlCalcOption { SPOT_PRICE, TWAP, } export interface Decimal { d: BigNumber } export interface ExchangeProps { priceFeedKey: string quoteAssetSymbol: string baseAssetSymbol: string baseAssetReserve: Big quoteAssetReserve: Big } export interface Position { size: Big margin: Big openNotional: Big lastUpdatedCumulativePremiumFraction: Big } export interface PositionCost { side: Side size: Big baseAssetReserve: Big quoteAssetReserve: Big } @Service() export class PerpService { private readonly log = Log.getLogger(PerpService.name) constructor( readonly ethService: EthService, readonly systemMetadataFactory: SystemMetadataFactory, readonly serverProfile: ServerProfile, ) {} private async createSystemSettings(): Promise<SystemSettings> { return await this.createContract<SystemSettings>( ethMetadata => ethMetadata.systemSettingsAddr, SystemSettingsArtifact, ) } private createExchange(exchangeAddr: string): Exchange { return this.ethService.createContract<Exchange>(exchangeAddr, ExchangeArtifact) } private async createExchangeReader(): Promise<ExchangeReader> { return this.createContract<ExchangeReader>(systemMetadata => systemMetadata.exchangeReaderAddr, ExchangeReaderArtifact) } private async createSakePerp(signer?: ethers.Signer): Promise<SakePerp> { return this.createContract<SakePerp>( systemMetadata => systemMetadata.sakePerpAddr, SakePerpArtifact, signer, ) } private async createSakePerpVault(signer?: ethers.Signer): Promise<SakePerpVault> { return this.createContract<SakePerpVault>( systemMetadata => systemMetadata.sakePerpVault, SakePerpVaultArtifact, signer, ) } private async createSakePerpViewer(signer?: ethers.Signer): Promise<SakePerpViewer> { return this.createContract<SakePerpViewer>( systemMetadata => systemMetadata.sakePerpViewerAddr, SakePerpViewerArtifact, signer, ) } private async createSakePerpState(signer?: ethers.Signer): Promise<SakePerpState> { return this.createContract<SakePerpState>( systemMetadata => systemMetadata.sakePerpStateAddr, SakePerpStateArtifact, signer, ) } async checkWaitingPeriod(trader: Wallet, exchangeAddr: string, traderAddr: string, side: Side ): Promise<boolean> { const sakePerpState = await this.createSakePerpState(trader) const whiteList = await sakePerpState.functions.waitingWhitelist(traderAddr) const isWhiteAddr = whiteList[0] if (isWhiteAddr){ return true } const result = await sakePerpState.functions.tradingState(exchangeAddr, traderAddr) const lastestLongTime = result[0].toNumber() const lastestShortTime = result[1].toNumber() const nowTime = Math.floor(Date.now() / 1000) const waitingPeriodSecs = 360; if (side == Side.BUY) { this.log.jinfo({ event: "CheckWaitingPeriod", params: { side: "buy", lastestShortTime: lastestShortTime, nowTime: nowTime, canOpen: !(lastestShortTime + waitingPeriodSecs > nowTime) }, }) if (lastestShortTime + waitingPeriodSecs > nowTime) { return false } }else{ this.log.jinfo({ event: "CheckWaitingPeriod", params: { side: "sell", lastestLongTime: lastestLongTime, nowTime: nowTime, canOpen: !(lastestLongTime + waitingPeriodSecs > nowTime) }, }) if (lastestLongTime + waitingPeriodSecs > nowTime) { return false } } return true } private async createContract<T>( addressGetter: (systemMetadata: EthMetadata) => string, abi: ethers.ContractInterface, signer?: ethers.Signer, ): Promise<T> { const systemMetadata = await this.systemMetadataFactory.fetch() return this.ethService.createContract<T>(addressGetter(systemMetadata), abi, signer) } async getAllOpenExchanges(): Promise<Exchange[]> { const exchanges: Exchange[] = [] const systemSettings = await this.createSystemSettings() const allExchanges = await systemSettings.functions.getAllExchanges() for (const exchangeAddr of allExchanges[0]) { const exchange = this.createExchange(exchangeAddr) if (await exchange.open()) { exchanges.push(exchange) } } // this.log.info( // JSON.stringify({ // event: "GetAllOpenExchanges", // params: { // exchangeAddrs: exchanges.map(exchange => exchange.address), // }, // }), // ) return exchanges } async getExchangeStates(exchangeAddr: string): Promise<ExchangeProps> { const exchangeReader = await this.createExchangeReader() const props = (await exchangeReader.functions.getExchangeStates(exchangeAddr))[0] return { priceFeedKey: props.priceFeedKey, quoteAssetSymbol: props.quoteAssetSymbol, baseAssetSymbol: props.baseAssetSymbol, baseAssetReserve: PerpService.fromWei(props.baseAssetReserve), quoteAssetReserve: PerpService.fromWei(props.quoteAssetReserve), } } async getPosition(exchangeAddr: string, traderAddr: string): Promise<Position> { const sakePerp = await this.createSakePerp() const position = (await sakePerp.functions.getPosition(exchangeAddr, traderAddr))[0] return { size: PerpService.fromWei(position.size.d), margin: PerpService.fromWei(position.margin.d), openNotional: PerpService.fromWei(position.openNotional.d), lastUpdatedCumulativePremiumFraction: PerpService.fromWei(position.lastUpdatedCumulativePremiumFraction.d), } } async getPersonalPositionWithFundingPayment(exchangeAddr: string, traderAddr: string): Promise<Position> { const sakePerpViewer = await this.createSakePerpViewer() const position = await sakePerpViewer.getPersonalPositionWithFundingPayment(exchangeAddr, traderAddr) return { size: PerpService.fromWei(position.size.d), margin: PerpService.fromWei(position.margin.d), openNotional: PerpService.fromWei(position.openNotional.d), lastUpdatedCumulativePremiumFraction: PerpService.fromWei(position.lastUpdatedCumulativePremiumFraction.d), } } async getMarginRatio(exchangeAddr: string, traderAddr: string): Promise<Big> { const sakePerp = await this.createSakePerp() return PerpService.fromWei((await sakePerp.functions.getMarginRatio(exchangeAddr, traderAddr))[0].d) } async getPositionNotionalAndUnrealizedPnl( exchangeAddr: string, traderAddr: string, pnlCalcOption: PnlCalcOption, ): Promise<{ positionNotional: Big unrealizedPnl: Big }> { const sakePerp = await this.createSakePerp() const ret = await sakePerp.getPositionNotionalAndUnrealizedPnl(exchangeAddr, traderAddr, pnlCalcOption) return { positionNotional: PerpService.fromWei(ret.positionNotional.d), unrealizedPnl: PerpService.fromWei(ret.unrealizedPnl.d), } } async openPosition( trader: Wallet, exchangePair: string, exchangeAddr: string, side: Side, quoteAssetAmount: Big, leverage: Big, minBaseAssetAmount: Big = Big(0), overrides?: Overrides, ): Promise<TransactionResponse> { const sakePerp = await this.createSakePerp(trader) // if the tx gonna fail it will throw here // const gasEstimate = await sakePerp.estimateGas.openPosition( // exchangeAddr, // side.valueOf(), // { d: PerpService.toWei(quoteAssetAmount) }, // { d: PerpService.toWei(leverage) }, // { d: PerpService.toWei(minBaseAssetAmount) }, // ) const tx = await sakePerp.functions.openPosition( exchangeAddr, side.valueOf(), { d: PerpService.toWei(quoteAssetAmount) }, { d: PerpService.toWei(leverage) }, { d: PerpService.toWei(minBaseAssetAmount) }, { // add a margin for gas limit since its estimation was sometimes too tight // gasLimit: BigNumber.from( // Big(gasEstimate.toString()) // .mul(Big(1.2)) // .toFixed(0), // ), gasLimit: 2_500_000, ...overrides, }, ) this.log.jinfo({ event: "OpenPositionTxSent", params: { exchangePair: exchangePair, trader: trader.address, amm: exchangeAddr, side, quoteAssetAmount: +quoteAssetAmount, leverage: +leverage, minBaseAssetAmount: +minBaseAssetAmount, txHash: tx.hash, gasPrice: tx.gasPrice.toString(), nonce: tx.nonce, }, }) return tx } async closePosition( trader: Wallet, exchangeAddr: string, exchangePair: string, minBaseAssetAmount: Big = Big(0), overrides?: Overrides, ): Promise<TransactionResponse> { const sakePerp = await this.createSakePerp(trader) const tx = await sakePerp.functions.closePosition( exchangeAddr, { d: PerpService.toWei(minBaseAssetAmount) }, { gasLimit: 2_500_000, ...overrides, }, ) this.log.jinfo({ event: "ClosePositionTxSent", params: { trader: trader.address, exchangePair: exchangePair, txHash: tx.hash, gasPrice: tx.gasPrice.toString(), nonce: tx.nonce, }, }) return tx } async removeMargin( trader: Wallet, exchangeAddr: string, marginToBeRemoved: Big, overrides?: Overrides, ): Promise<TransactionResponse> { const sakePerp = await this.createSakePerp(trader) const tx = await sakePerp.functions.removeMargin( exchangeAddr, { d: PerpService.toWei(marginToBeRemoved) }, { gasLimit: 1_500_000, ...overrides, }, ) this.log.jinfo({ event: "RemoveMarginTxSent", params: { trader: trader.address, amm: exchangeAddr, marginToBeRemoved: +marginToBeRemoved.toFixed(), txHash: tx.hash, gasPrice: tx.gasPrice.toString(), nonce: tx.nonce, }, }) return tx } async addMargin( trader: Wallet, exchangeAddr: string, marginToBeAdded: Big, overrides?: Overrides, ): Promise<TransactionResponse> { const sakePerp = await this.createSakePerp(trader) const tx = await sakePerp.functions.addMargin( exchangeAddr, { d: PerpService.toWei(marginToBeAdded) }, { gasLimit: 1_500_000, ...overrides, }, ) this.log.jinfo({ event: "AddMarginTxSent", params: { trader: trader.address, amm: exchangeAddr, marginToBeRemoved: +marginToBeAdded.toFixed(), txHash: tx.hash, gasPrice: tx.gasPrice.toString(), nonce: tx.nonce, }, }) return tx } async getUnrealizedPnl(exchangeAddr: string, traderAddr: string, pnlCalOption: PnlCalcOption): Promise<Big> { const sakePerpViewer = await this.createSakePerpViewer() const unrealizedPnl = (await sakePerpViewer.functions.getUnrealizedPnl(exchangeAddr, traderAddr, BigNumber.from(pnlCalOption)))[0] return Big(PerpService.fromWei(unrealizedPnl.d)) } async getGapForMovingAmm(exchangeAddr: string, sakeperpVaultAddr: string): Promise<Big> { const exchangeReader = await this.createExchangeReader() const gapMM = (await exchangeReader.functions.getGapForMovingAmm(exchangeAddr, sakeperpVaultAddr))[0] return Big(PerpService.fromWei(gapMM.d)) } // noinspection JSMethodCanBeStatic static fromWei(wei: BigNumber): Big { return Big(formatEther(wei)) } // noinspection JSMethodCanBeStatic static toWei(val: Big): BigNumber { return parseEther(val.toFixed(18)) } }