The code presented here is for informational purposes only.
Do not use this code in production without testing.
We welcome feedback via GitHub! https://github.com/perpetual-protocol/perp-contract-demo
Perpetual Protocol is a decentralized perpetual contracts protocol based on Ethereum. In addition to interacting with the smart contracts via the official web interface, users can also directly interact with the smart contracts to trade perpetual contracts through JavaScript/Typescript.
Perpetual Protocol is a Layer 1/2 hybrid dApp, therefore it is necessary to perform functions both on Layer 1 (Homestead mainnet or Rinkeby testnet) as well as Layer 2 (xDai network).
This article provides a brief tutorial on how to open a position, query active positions and Profit and Loss (PnL), and close a position through JavaScript.
Perpetual Protocol uses the xDai network scaling solution, so in addition to using Homestead or Rinkeby, Layer 2 commands must be sent to the xDai network. The principal interaction between these two layers occurs when depositing funds from Layer 1 to Layer 2, or withdrawing funds from Layer 2 back to Layer 1. Other functions are done by interacting directly with the Layer 2 (xDai network).
First, install the necessary npm packages to interact with the Perpetual Protocol smart contracts.
$ npm install cross-fetch ethers @perp/contract
cross-fetch
: To download data from the Internet
ethers
: The main library used to interact with the Ethereum blockchain
@perp/contract
: It contains all the functions and artifacts of the Perpetual Protocol smart contracts which are imported into ethers
to construct objects that can be manipulated in JavaScript.
The Perpetual Protocol development team stores all contract addresses in a JSON file on the official website:
production (Homestead/xDai): https://metadata.perp.exchange/production.json
staging (Rinkeby/xDai): https://metadata.perp.exchange/staging.json
{"layers": {"layer1": {"contracts": {"RootBridge": {"name": "RootBridge","address": "0xd9F8a64da87eA0dA27B0bd7CCA619ED48C0e3d19"},"ChainlinkL1": {"name": "ChainlinkL1","address": "0x8112DFD2A9Ff6Fa34f190275823cB8e7BdB5345a"}},"accounts": [],"network": "rinkeby","externalContracts": {"foundationGovernance": "0xa230A4f6F38D904C2eA1eE95d8b2b8b7350e3d79","perp": "0xaFfB148304D38947193785D194972a7d0d9b7F68","tether": "0x40D3B2F06f198D2B789B823CdBEcD1DB78090D74","usdc": "0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b","testnetFaucet": "0x9E9DFaCCABeEcDA6dD913b3685c9fe908F28F58c","ambBridgeOnEth": "0xD4075FB57fCf038bFc702c915Ef9592534bED5c1","multiTokenMediatorOnEth": "0x30F693708fc604A57F1958E3CFa059F902e6d4CB"}},"layer2": {"contracts": {"MetaTxGateway": {"name": "MetaTxGateway","address": "0x3Ef797AaBE73bb4747D82850B97283E77a7CC452"},"ClientBridge": {"name": "ClientBridge","address": "0x457A42Ee5e05a3e6ec2aD5c6e23E5163da88EE2D"},"InsuranceFund": {"name": "InsuranceFund","address": "0xbEfbeB2e1c9981771D20412dD072C50Ca2A158a1"},"L2PriceFeed": {"name": "L2PriceFeed","address": "0x4CCca659abAf29214501E6800c2737BbFB6e4f2C"},"ClearingHouse": {"name": "ClearingHouse","address": "0x1B54A05488F74984b2825560c6213901fb32B10D"},"ETHUSDC": {"name": "Amm","address": "0x9aCb03b0d6b971aF7bBebE6fb98C4eB79ff4D102"},"BTCUSDC": {"name": "Amm","address": "0x53B99173D017322676aCA54Ac079549342138256"},"ClearingHouseViewer": {"name": "ClearingHouseViewer","address": "0x72238b1e540cE24E0ce36EF3c38d622Da097C8cE"},"AmmReader": {"name": "AmmReader","address": "0x06760Cad3213BE325669938A81b66025452b3737"}},"accounts": [],"network": "xdai","externalContracts": {"foundationGovernance": "0x44883405Eb9826448d3E8eCC25889C5941E79d9b","arbitrageur": "0x68dfc526037E9030c8F813D014919CC89E7d4d74","testnetFaucet": "0x9E9DFaCCABeEcDA6dD913b3685c9fe908F28F58c","ambBridgeOnXDai": "0xc38D4991c951fE8BCE1a12bEef2046eF36b0FA4A","multiTokenMediatorOnXDai": "0xA34c65d76b997a824a5E384471bBa73b0013F5DA","tether": "0xe0B887D54e71329318a036CF50f30Dbe4444563c","usdc": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83"}}}}
This file can be retrieved via cross-fetch
in JavaScript:
const url = "<https://metadata.perp.exchange/staging.json>"const metadata = await fetch(url).then(res => res.json())
Next, you need to set up a main working account (ie. wallet). There are many ways to set up an account, one of which is through MNEMONIC
.
Suppose we want to access Perpetual Protocol's staging/testnet environment. Below is the code that connects our dApp to the xDai network and Rinkby (using an Infura node), and sets the MNEMONIC
phrase by using an environment variable. The xDai Layer 2 and Ethereum Layer 1 networks can use a shared set of private keys.
const { Contract, providers, Wallet } = require("ethers")const xDaiUrl = "<https://rpc.xdaichain.com/>"const infuraProjectId = "REGISTER_INFURA_TO_GET_PROJECT_ID"const rinkebyUrl = "<https://rinkeby.infura.io/v3/>" + infuraProjectIdconst layer1Provider = new providers.JsonRpcProvider(rinkebyUrl)const layer2Provider = new providers.JsonRpcProvider(xDaiUrl)const layer1Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer1Provider)const layer2Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer2Provider)
Your account must have ETH and xDAI tokens to send transactions. Please ask us for some xDAI on Discord or convert DAI to xDAI at bridge.xdaichain.com. If you do not have xDAI, you will get an error (Insufficient funds).
Now you have the addresses of the smart contract and wallets with gas for sending transactions. Next, quote the information from @perp/contract
and provide it to ethers
to construct a smart contract with the correct functions and variables for subsequent operations.
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")// layer 1 contractsconst layer1BridgeAddr = metadata.layers.layer1.contracts.RootBridge.addressconst usdcAddr = metadata.layers.layer1.externalContracts.tetherconst layer1AmbAddr = metadata.layers.layer1.externalContracts.ambBridgeOnEthconst layer1Usdc = new Contract(usdcAddr, TetherTokenArtifact.abi, layer1Wallet)const layer1Bridge = new Contract(layer1BridgeAddr, RootBridgeArtifact.abi, layer1Wallet)const layer1Amb = new Contract(layer1AmbAddr, ABI_AMB_LAYER1, layer1Wallet)// layer 2 contractsconst layer2BridgeAddr = metadata.layers.layer2.contracts.ClientBridge.addressconst layer2AmbAddr = metadata.layers.layer2.externalContracts.ambBridgeOnXDaiconst xUsdcAddr = metadata.layers.layer2.externalContracts.tetherconst clearingHouseAddr = metadata.layers.layer2.contracts.ClearingHouse.addressconst chViewerAddr = metadata.layers.layer2.contracts.ClearingHouseViewer.addressconst ammAddr = metadata.layers.layer2.contracts.ETHUSDC.addressconst layer2Usdc = new Contract(xUsdcAddr, TetherTokenArtifact.abi, layer2Wallet)const amm = new Contract(ammAddr, AmmArtifact.abi, layer2Wallet)const clearingHouse = new Contract(clearingHouseAddr, ClearingHouseArtifact.abi, layer2Wallet)const clearingHouseViewer = new Contract(chViewerAddr, CHViewerArtifact.abi, layer2Wallet)const layer2Amb = new Contract(layer2AmbAddr, ABI_AMB_LAYER2, layer2Wallet)const layer2Bridge = new Contract(layer2BridgeAddr, ClientBridgeArtifact.abi, layer2Wallet)
You can build smart contract objects that can interact with Ethereum using new Contract
(Addr
, abi
, Wallet
). amm
is declared in the code above; call amm.quoteAsset()
to get the ERC20 token address corresponding to this AMM and use this address to construct the USDC objects for subsequent operations.
In order to facilitate testing, the test USDC used by smart contracts deployed by Perpetual Protocol on the Rinkeby testnet is a test token created by our team. You can call the faucet API provided by the team to get test USDC If needed. (Once wallet address can only receive test USDC once)
async function faucetUsdc(accountAddress) {const faucetApiKey = "da2-h4xlnj33zvfnheevfgaw7datae"const appSyncId = "izc32tpa5ndllmbql57pcxluua"const faucetUrl = `https://${appSyncId}.appsync-api.ap-northeast-1.amazonaws.com/graphql`const options = {method: "POST",headers: {"Content-Type": "application/json","X-Api-Key": faucetApiKey,},body: JSON.stringify({query: `mutation issue {issue(holderAddr:"${accountAddress}"){txHashQuoteamountQuote}}`,}),}return fetch(faucetUrl, options)}
Before continuing, we need to explain the relationship between the different decimals in the system. Different tokens have different settings for decimal places, and these need to be reconciled.
Our development team created a customized decimals library to prevent errors when converting digits. Many of Perpetual Protocol’s smart contracts require the use of custom-defined decimal formats on incoming parameters.
Simply put, it is a JavaScript object with the d
attribute, and the decimals of d
are all 18 digits. For example, a number that needs to be passed in 100.0 can be expressed in the following way:
const { parseUnits } = require("ethers/lib/utils")const oneHundred = { d: parseUnits("100", 18) } // d is 100000000000000000000
Before transacting on xDai, USDC must be transferred from layer 1 to layer 2. Regardless of which layer is used, funds remain in the control of the user's wallet and private key.
Layer 1 and 2 each have a bridge contract. Transferring between layers can be done simply by calling ERC20.approve()
to approve use of tokens, followed by calling bridge.erc20Transfer()
.
Deposit:
const approveTx = await layer1Usdc.approve(layer1Bridge.address, constants.MaxUint256)await approveTx.wait()const depositAmount = { d: amount }const transferTx = layer1Bridge.erc20Transfer(layer1Usdc.address, layer1Wallet.address, depositAmount)const receipt = await transferTx.wait()
Withdraw:
const approveTx = await layer2Usdc.approve(layer2Bridge.address, constants.MaxUint256)await approveTx.wait()const withdrawAmount = { d: amount }const transferTx = layer2Bridge.erc20Transfer(layer2Usdc.address, layer2Wallet.address, withdrawAmount )const receipt = await transferTx.wait()
Let's look at two examples. Deposit by calling erc20Transfer()
; this transaction ends interaction on layer 1, but the transfer of tokens to layer 2 can be confirmed by listening on xDai's Arbitrary Message Bridge (AMB) contract.
After erc20Transfer()
completes successfully, messageId
can be read from AffirmationCompleted
in the transaction receipt. This variable can be used to confirm the successful transfer of tokens to layer 2.
const methodId = "0x482515ce" // UserRequestForAffirmationconst eventName = "AffirmationCompleted"const [log] = receipt.logs.filter(log => log.topics[0].substr(0, 10) === methodId)const fromMsgId = log.topics[1]layer2Amb.on(eventName, (sender, executor, toMsgId, status, log) => {if (fromMsgId === toMsgId) {amb.removeAllListeners(eventName)resolve(log.transactionHash)}})
The withdraw process differs in terms of messageId
, eventName
, and listens for the layer1Amb
event:
const methodId = "0x520d2afd" // UserRequestForSignatureconst eventName = "RelayedMessage"const [log] = receipt.logs.filter(log => log.topics[0].substr(0, 10) === methodId)const fromMsgId = log.topics[1]layer1Amb.on(eventName, (sender, executor, toMsgId, status, log) => {if (fromMsgId === toMsgId) {amb.removeAllListeners(eventName)resolve(log.transactionHash)}})
Once you have the test USDC on layer 2, you can call the approve()
function to allow the clearingHouse
smart contract to spend your test USDC.
const tx = await layer2Usdc.approve(clearingHouse.address, constants.MaxUint256)await tx.wait()
clearingHouse
provides the function openPosition()
which takes a number of parameters:
amm.address
: AMM contract address to be interacted with
side
: Long or short, 0 or 1 respectively
quoteAssetAmount
: The amount of margin
leverage
: Amount of leverage up to 10x
minBaseAssetAmount
: Minimum size of new position (slippage protection). Transactions will fail if the transaction will result in a position size below this value. Set to 0 to accept any position size.
If a trader wants to open a short position with a margin of 100 and 2x leverage, it can be achieved with the following code:
const { parseUnits } = require("ethers/lib/utils")async function openPosition() {const DEFAULT_DECIMALS = 18const side = 1 // Shortconst quoteAssetAmount = { d: parseUnits("100", DEFAULT_DECIMALS) }const leverage = { d: parseUnits("2", DEFAULT_DECIMALS) }const minBaseAssetAmount = { d: "0" } // "0" can be automatically convertedconst tx = await clearingHouse.openPosition(amm.address,side,quoteAssetAmount,leverage,minBaseAssetAmount,)await tx.wait()}
All values used in Perpetual Protocol are 18 decimal places, so they can be converted into the correct number of digits using parseUnits
provided by ethers.
After opening a position, you can query the current status using two functions: clearingHouseViewer.getPersonalPositionWithFundingPayment()
and clearingHouseViewer.getUnrealizedPnl()
. The former can be used to query the current position held, and the latter can be used to query the currently unrealized PnL. Methods as below:
const position = await clearingHouseViewer.getPersonalPositionWithFundingPayment(amm.address,wallet.address,)const pnl = await clearingHouseViewer.getUnrealizedPnl(amm.address,wallet.address,BigNumber.from(PNL_OPTION_SPOT_PRICE),)
If PNL_OPTION_SPOT_PRICE
is 0, the basis for obtaining unrealized PnL is the current price. You can also set it to 1 to obtain unrealized PnL based on 15-minute TWAP (Time-weighted average price).
Use of closePosition()
is relatively simple. It only requires the AMM contract address.
The user could choose to open an inverse position using openPosition()
to effectively close an existing position, but this may result in a dust position (unspendable remainder). closePosition()
will ensure that the current position will be completely closed.
const minimalQuoteAsset = {d: "0"}const tx = await clearingHouse.closePosition(amm.address, minimalQuoteAsset)await tx.wait()
The following example is a complete JavaScript template that combines all the functions mentioned above.
const fetch = require("cross-fetch")const { Contract, Wallet, BigNumber, constants, providers } = 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 CHViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.json")const Erc20TokenArtifact = require("@perp/contract/build/contracts/ERC20Token.json")const { parseUnits, formatEther, formatUnits } = require("ethers/lib/utils")require("dotenv").config()// const LONG_POS = 0const SHORT_POS = 1const DEFAULT_DECIMALS = 18const PNL_OPTION_SPOT_PRICE = 0const SHORT_AMOUNT = "100"const ACTION_DEPOSIT = 0const ACTION_WITHDRAW = 1const ABI_AMB_LAYER1 = ["event RelayedMessage(address indexed sender, address indexed executor, bytes32 indexed messageId, bool status)","event AffirmationCompleted( address indexed sender, address indexed executor, bytes32 indexed messageId, bool status)",]const ABI_AMB_LAYER2 = ["event AffirmationCompleted( address indexed sender, address indexed executor, bytes32 indexed messageId, bool status)",]async function waitTx(txReq) {return txReq.then(tx => tx.wait(2)) // wait 2 block for confirmation}async function faucetUsdc(accountAddress) {const faucetApiKey = "da2-h4xlnj33zvfnheevfgaw7datae"const appSyncId = "izc32tpa5ndllmbql57pcxluua"const faucetUrl = `https://${appSyncId}.appsync-api.ap-northeast-1.amazonaws.com/graphql`const options = {method: "POST",headers: {"Content-Type": "application/json","X-Api-Key": faucetApiKey,},body: JSON.stringify({query: `mutation issue {issue(holderAddr:"${accountAddress}"){txHashQuoteamountQuote}}`,}),}return fetch(faucetUrl, options)}async function setupEnv() {const metadataUrl = "<https://metadata.perp.exchange/staging.json>"const metadata = await fetch(metadataUrl).then(res => res.json())const xDaiUrl = "<https://rpc.xdaichain.com/>"const infuraProjectId = "04034d1ba6d141b4a5d57f872c0e52bd"const rinkebyUrl = "<https://rinkeby.infura.io/v3/>" + infuraProjectIdconst layer1Provider = new providers.JsonRpcProvider(rinkebyUrl)const layer2Provider = new providers.JsonRpcProvider(xDaiUrl)const layer1Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer1Provider)const layer2Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer2Provider)console.log("wallet address", layer1Wallet.address)// layer 1 contractsconst layer1BridgeAddr = metadata.layers.layer1.contracts.RootBridge.addressconst usdcAddr = metadata.layers.layer1.externalContracts.usdcconst layer1AmbAddr = metadata.layers.layer1.externalContracts.ambBridgeOnEthconst layer1Usdc = new Contract(usdcAddr, Erc20TokenArtifact.abi, layer1Wallet)const layer1Bridge = new Contract(layer1BridgeAddr, RootBridgeArtifact.abi, layer1Wallet)const layer1Amb = new Contract(layer1AmbAddr, ABI_AMB_LAYER1, layer1Wallet)// layer 2 contractsconst layer2BridgeAddr = metadata.layers.layer2.contracts.ClientBridge.addressconst layer2AmbAddr = metadata.layers.layer2.externalContracts.ambBridgeOnXDaiconst xUsdcAddr = metadata.layers.layer2.externalContracts.usdcconst clearingHouseAddr = metadata.layers.layer2.contracts.ClearingHouse.addressconst chViewerAddr = metadata.layers.layer2.contracts.ClearingHouseViewer.addressconst ammAddr = metadata.layers.layer2.contracts.ETHUSDC.addressconst layer2Usdc = new Contract(xUsdcAddr, Erc20TokenArtifact.abi, layer2Wallet)const amm = new Contract(ammAddr, AmmArtifact.abi, layer2Wallet)const clearingHouse = new Contract(clearingHouseAddr, ClearingHouseArtifact.abi, layer2Wallet)const clearingHouseViewer = new Contract(chViewerAddr, CHViewerArtifact.abi, layer2Wallet)const layer2Amb = new Contract(layer2AmbAddr, ABI_AMB_LAYER2, layer2Wallet)const layer2Bridge = new Contract(layer2BridgeAddr, ClientBridgeArtifact.abi, layer2Wallet)console.log("USDC address", usdcAddr)return {amm,clearingHouse,layer1Usdc,layer2Usdc,layer1Wallet,layer2Wallet,clearingHouseViewer,layer1Bridge,layer2Bridge,layer1Amb,layer2Amb,}}async function openPosition(clearingHouse, amm) {const quoteAssetAmount = {d: parseUnits(SHORT_AMOUNT, DEFAULT_DECIMALS),}const leverage = { d: parseUnits("2", DEFAULT_DECIMALS) }const minBaseAssetAmount = { d: "0" }await waitTx(clearingHouse.openPosition(amm.address,SHORT_POS,quoteAssetAmount,leverage,minBaseAssetAmount,),)}async function printInfo(clearingHouseViewer, amm, wallet) {console.log("getting information")const position = await clearingHouseViewer.getPersonalPositionWithFundingPayment(amm.address,wallet.address,)const pnl = await clearingHouseViewer.getUnrealizedPnl(amm.address,wallet.address,BigNumber.from(PNL_OPTION_SPOT_PRICE),)console.log("- current position", formatUnits(position.size.d, DEFAULT_DECIMALS))console.log("- pnl", formatUnits(pnl.d, DEFAULT_DECIMALS))}async function printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc) {// get ETH & USDC balanceconst ethBalance = await layer1Wallet.getBalance()const xDaiBalance = await layer2Wallet.getBalance()let layer1UsdcBalance = await layer1Usdc.balanceOf(layer1Wallet.address)let layer2UsdcBalance = await layer2Usdc.balanceOf(layer1Wallet.address)const layer1UsdcDecimals = await layer1Usdc.decimals()const layer2UsdcDecimals = await layer2Usdc.decimals()const outputs = ["balances",`- layer 1`,` - ${formatEther(ethBalance)} ETH`,` - ${formatUnits(layer1UsdcBalance, layer1UsdcDecimals)} USDC`,`- layer 2`,` - ${formatEther(xDaiBalance)} xDAI`,` - ${formatUnits(layer2UsdcBalance, layer2UsdcDecimals)} USDC`,]console.log(outputs.join("\\n"))}async function waitCrossChain(action, receipt, layer1Amb, layer2Amb) {let methodIdlet eventNamelet ambif (action === ACTION_DEPOSIT) {methodId = "0x482515ce" // UserRequestForAffirmationeventName = "AffirmationCompleted"amb = layer2Amb} else if (action === ACTION_WITHDRAW) {methodId = "0x520d2afd" // UserRequestForSignatureeventName = "RelayedMessage"amb = layer1Amb} else {throw new Error("unknown action: " + action)}return new Promise(async (resolve, reject) => {if (receipt && receipt.logs) {const matched = receipt.logs.filter(log => log.topics[0].substr(0, 10) === methodId)if (matched.length === 0) {return reject("methodId not found: " + methodId)}const log = matched[0]const fromMsgId = log.topics[1]console.log("msgId from receipt", fromMsgId)amb.on(eventName, (sender, executor, toMsgId, status, log) => {console.log("got event", toMsgId)if (fromMsgId === toMsgId) {amb.removeAllListeners(eventName)resolve(log.transactionHash)}})} else {reject("receipt or log not found")}})}async function main() {const {amm,clearingHouse,layer1Usdc,layer2Usdc,layer1Wallet,layer2Wallet,clearingHouseViewer,layer1Bridge,layer2Bridge,layer1Amb,layer2Amb,} = await setupEnv()// get ETH & USDC balancelet layer1UsdcBalance = await layer1Usdc.balanceOf(layer1Wallet.address)// if no USDC, faucet to get more USDCwhile (!layer1UsdcBalance.gt(0)) {console.log("faucet USDC")await faucetUsdc(layer1Wallet.address)layer1UsdcBalance = await layer1Usdc.balanceOf(layer1Wallet.address)}const amount = parseUnits(SHORT_AMOUNT, DEFAULT_DECIMALS)await printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc)// approve USDCconst allowanceForBridge = await layer1Usdc.allowance(layer1Wallet.address, layer1Bridge.address)if (allowanceForBridge.lt(amount)) {console.log("approving all tokens for root bridge on layer 1")await waitTx(layer1Usdc.approve(layer1Bridge.address, constants.MaxUint256))}// deposit to layer 2console.log("depositing to layer 2")const depositAmount = { d: amount }const layer1Receipt = await waitTx(layer1Bridge.erc20Transfer(layer1Usdc.address, layer1Wallet.address, depositAmount),)console.log("waiting confirmation on layer 2")await waitCrossChain(ACTION_DEPOSIT, layer1Receipt, layer1Amb, layer2Amb)await printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc)const allowanceForClearingHouse = await layer2Usdc.allowance(layer2Wallet.address,clearingHouse.address,)if (allowanceForClearingHouse.lt(amount)) {console.log("approving all tokens for clearing house on layer 2")await waitTx(layer2Usdc.approve(clearingHouse.address, constants.MaxUint256))}console.log("opening position")await openPosition(clearingHouse, amm)await printInfo(clearingHouseViewer, amm, layer2Wallet)console.log("closing position")await waitTx(clearingHouse.closePosition(amm.address, { d: "0" }))await printInfo(clearingHouseViewer, amm, layer2Wallet)// withdraw to layer 1console.log("approving all token for client bridge on layer 2")await waitTx(layer2Usdc.approve(layer2Bridge.address, constants.MaxUint256))console.log("withdraw 50 USDC from layer 2 to layer 1")const layer2Receipt = await waitTx(layer2Bridge.erc20Transfer(layer2Usdc.address, layer2Wallet.address, {d: parseUnits("50", DEFAULT_DECIMALS),}),)console.log("waiting confirmation on layer 1")await waitCrossChain(ACTION_WITHDRAW, layer2Receipt, layer1Amb, layer2Amb)await printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc)}main()
If you have test ETH in the account you provided on the Rinkeby testnet and also some xDai on xDai network, the code above will perform several things:
Get test USDC from the faucet
Approve ClearingHouse smart contract to spend your test USDC
Open a 100 USDC short position with 2x leverage
List the trader's positions and unrealized PnL
Close position
The results received may be different depending on your account status. The following output was obtained according to the account used by the author at time of writing:
$ node main.jswallet address 0x88D427c17d1Ac4100b598b15CE65110F2030A7CfUSDC address 0x40D3B2F06f198D2B789B823CdBEcD1DB78090D74balances- layer 1- 0.098113582 ETH- 8950.0 USDC- layer 2- 0.995539288 xDAI- 1048.294207 USDCdepositing to layer 2waiting confirmation on layer 2msgId from receipt 0x000500009e7518627c2468e5f236bfec42b9026816e66b370000000000001ec2got event 0x000500009e7518627c2468e5f236bfec42b9026816e66b370000000000001ec2balances- layer 1- 0.097912091 ETH- 8850.0 USDC- layer 2- 0.995539288 xDAI- 1148.294207 USDCapproving all tokens for clearing house on layer 2opening positiongetting information- current position -0.335740825585871707- pnl -0.000000000000000178closing positiongetting information- current position 0.0- pnl 0.0approving all token for client bridge on layer 2withdraw 50 USDC from layer 2 to layer 1waiting confirmation on layer 1msgId from receipt 0x00050000dd91aecde2ad4ff420b70fff98bad16a14bb881700000000000002fegot event 0x00050000dd91aecde2ad4ff420b70fff98bad16a14bb881700000000000002febalances- layer 1- 0.097912091 ETH- 8900.0 USDC- layer 2- 0.994502962 xDAI- 1097.894206 USDC
The code above is also available on our github. You can clone the code, install all dependencies and then execute node main.js
.
There are a few more numbers that can be calculated based on the properties of the contracts.
Entry price can be calculated by open notional & position size via getPersonalPositionWithFundingPayment()
. Using the big.js
module to deal with Big Numbers is recommended.
const Big = require('big.js')const position = await clearingHouseViewer.getPersonalPositionWithFundingPayment(amm.address,wallet.address,)const openNotional = new Big(formatUnits(position.openNotional.d, DEFAULT_DECIMALS))const size = new Big(formatUnits(position.size.d, DEFAULT_DECIMALS))const entryPrice = openNotional.div(size).abs()
The estimated liquidation price is more complicated to calculate. You can use the code below to get it.
function bigNum2Big(val: BigNumber, decimals: number = 18): Big {return new Big(val.toString()).div(new Big(10).pow(decimals))}function big2BigNum(val: Big, decimals: number = 18): BigNumber {return BigNumber.from(val.mul(new Big(10).pow(decimals)).toFixed(0))}async function estimateLiquidationPrice(position: Position,amm: Amm,clearingHouse: ClearingHouse,): Promise<BigNumber> {const spotPrice = bigNum2Big((await amm.getSpotPrice()).d)const realCloseQuoteAmount = bigNum2Big((await amm.getOutputPrice(position.size.d.gt(0) ? 0 : 1, { d: position.size.d.abs() })).d,)const maintenanceMarginRatio = bigNum2Big(await clearingHouse.maintenanceMarginRatio())const openNotional = bigNum2Big(position.openNotional.d)const positionSizeAbs = bigNum2Big(position.size.d.abs())const margin = bigNum2Big(position.margin.d)const entryPrice = openNotional.div(positionSizeAbs)const reverseLeverage = margin.div(openNotional)const spotCloseQuoteAmount = spotPrice.mul(positionSizeAbs)// Closing long position: negative since pool asset amount increases (price up)// Closing short position: positive since pool asset amount decreases (price down)const closePosPriceSlippage = realCloseQuoteAmount.minus(spotCloseQuoteAmount).div(spotCloseQuoteAmount)const liquidationPrice = position.size.d.gt(0)? entryPrice.mul(new Big(1).minus(reverseLeverage).minus(closePosPriceSlippage).plus(maintenanceMarginRatio)) // Long: entryPrice.mul(new Big(1).plus(reverseLeverage).minus(closePosPriceSlippage).minus(maintenanceMarginRatio)) // Shortreturn big2BigNum(liquidationPrice)}