Perpetual Protocol
Searchโ€ฆ
๐Ÿ’พ
Smart Contract JavaScript Development Guide
A brief guide to interacting with our platform smart contracts
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โ€‹
Also check out the Developer FAQโ€‹

Overview

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. โ€Œ

xDai intro - Layer 2 solution

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).

Toolchain

โ€ŒFirst, install the necessary npm packages to interact with the Perpetual Protocol smart contracts.
1
$ npm install cross-fetch ethers @perp/contract
Copied!
  • 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.

Smart contract addresses

The Perpetual Protocol development team stores all contract addresses in a JSON file on the official website:

โ€ŒExample JSON:

1
{
2
"layers": {
3
"layer1": {
4
"contracts": {
5
"RootBridge": {
6
"name": "RootBridge",
7
"address": "0xd9F8a64da87eA0dA27B0bd7CCA619ED48C0e3d19"
8
},
9
"ChainlinkL1": {
10
"name": "ChainlinkL1",
11
"address": "0x8112DFD2A9Ff6Fa34f190275823cB8e7BdB5345a"
12
}
13
},
14
"accounts": [],
15
"network": "rinkeby",
16
"externalContracts": {
17
"foundationGovernance": "0xa230A4f6F38D904C2eA1eE95d8b2b8b7350e3d79",
18
"perp": "0xaFfB148304D38947193785D194972a7d0d9b7F68",
19
"tether": "0x40D3B2F06f198D2B789B823CdBEcD1DB78090D74",
20
"usdc": "0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b",
21
"testnetFaucet": "0x9E9DFaCCABeEcDA6dD913b3685c9fe908F28F58c",
22
"ambBridgeOnEth": "0xD4075FB57fCf038bFc702c915Ef9592534bED5c1",
23
"multiTokenMediatorOnEth": "0x30F693708fc604A57F1958E3CFa059F902e6d4CB"
24
}
25
},
26
"layer2": {
27
"contracts": {
28
"MetaTxGateway": {
29
"name": "MetaTxGateway",
30
"address": "0x3Ef797AaBE73bb4747D82850B97283E77a7CC452"
31
},
32
"ClientBridge": {
33
"name": "ClientBridge",
34
"address": "0x457A42Ee5e05a3e6ec2aD5c6e23E5163da88EE2D"
35
},
36
"InsuranceFund": {
37
"name": "InsuranceFund",
38
"address": "0xbEfbeB2e1c9981771D20412dD072C50Ca2A158a1"
39
},
40
"L2PriceFeed": {
41
"name": "L2PriceFeed",
42
"address": "0x4CCca659abAf29214501E6800c2737BbFB6e4f2C"
43
},
44
"ClearingHouse": {
45
"name": "ClearingHouse",
46
"address": "0x1B54A05488F74984b2825560c6213901fb32B10D"
47
},
48
"ETHUSDC": {
49
"name": "Amm",
50
"address": "0x9aCb03b0d6b971aF7bBebE6fb98C4eB79ff4D102"
51
},
52
"BTCUSDC": {
53
"name": "Amm",
54
"address": "0x53B99173D017322676aCA54Ac079549342138256"
55
},
56
"ClearingHouseViewer": {
57
"name": "ClearingHouseViewer",
58
"address": "0x72238b1e540cE24E0ce36EF3c38d622Da097C8cE"
59
},
60
"AmmReader": {
61
"name": "AmmReader",
62
"address": "0x06760Cad3213BE325669938A81b66025452b3737"
63
}
64
},
65
"accounts": [],
66
"network": "xdai",
67
"externalContracts": {
68
"foundationGovernance": "0x44883405Eb9826448d3E8eCC25889C5941E79d9b",
69
"arbitrageur": "0x68dfc526037E9030c8F813D014919CC89E7d4d74",
70
"testnetFaucet": "0x9E9DFaCCABeEcDA6dD913b3685c9fe908F28F58c",
71
"ambBridgeOnXDai": "0xc38D4991c951fE8BCE1a12bEef2046eF36b0FA4A",
72
"multiTokenMediatorOnXDai": "0xA34c65d76b997a824a5E384471bBa73b0013F5DA",
73
"tether": "0xe0B887D54e71329318a036CF50f30Dbe4444563c",
74
"usdc": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83"
75
}
76
}
77
}
78
}
79
โ€‹
Copied!
This file can be retrieved via cross-fetch in JavaScript:
1
const url = "<https://metadata.perp.exchange/staging.json>"
2
const metadata = await fetch(url).then(res => res.json())
Copied!

โ€ŒAccount setup

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 Rinkeby (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.
1
const { Contract, providers, Wallet } = require("ethers")
2
const xDaiUrl = "<https://rpc.xdaichain.com/>"
3
const infuraProjectId = "REGISTER_INFURA_TO_GET_PROJECT_ID"
4
const rinkebyUrl = "<https://rinkeby.infura.io/v3/>" + infuraProjectId
5
const layer1Provider = new providers.JsonRpcProvider(rinkebyUrl)
6
const layer2Provider = new providers.JsonRpcProvider(xDaiUrl)
7
const layer1Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer1Provider)
8
const layer2Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer2Provider)
Copied!
Your account must have ETH and xDAI tokens to send transactions. 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.
1
const AmmArtifact = require("@perp/contract/build/contracts/src/Amm.sol/Amm.json")
2
const ClearingHouseArtifact = require("@perp/contract/build/contracts/ClearingHouse.sol/ClearingHouse.json")
3
const RootBridgeArtifact = require("@perp/contract/build/contracts/RootBridge.sol/RootBridge.json")
4
const ClientBridgeArtifact = require("@perp/contract/build/contracts/ClientBridge.sol/ClientBridge.json")
5
const ClearingHouseViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.sol/ClearingHouseViewer.json")
6
const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken.sol/TetherToken.json")
7
โ€‹
8
// layer 1 contracts
9
const layer1BridgeAddr = metadata.layers.layer1.contracts.RootBridge.address
10
const usdcAddr = metadata.layers.layer1.externalContracts.tether
11
const layer1AmbAddr = metadata.layers.layer1.externalContracts.ambBridgeOnEth
12
โ€‹
13
const layer1Usdc = new Contract(usdcAddr, TetherTokenArtifact.abi, layer1Wallet)
14
const layer1Bridge = new Contract(layer1BridgeAddr, RootBridgeArtifact.abi, layer1Wallet)
15
const layer1Amb = new Contract(layer1AmbAddr, ABI_AMB_LAYER1, layer1Wallet)
16
โ€‹
17
// layer 2 contracts
18
const layer2BridgeAddr = metadata.layers.layer2.contracts.ClientBridge.address
19
const layer2AmbAddr = metadata.layers.layer2.externalContracts.ambBridgeOnXDai
20
const xUsdcAddr = metadata.layers.layer2.externalContracts.tether
21
const clearingHouseAddr = metadata.layers.layer2.contracts.ClearingHouse.address
22
const chViewerAddr = metadata.layers.layer2.contracts.ClearingHouseViewer.address
23
const ammAddr = metadata.layers.layer2.contracts.ETHUSDC.address
24
โ€‹
25
const layer2Usdc = new Contract(xUsdcAddr, TetherTokenArtifact.abi, layer2Wallet)
26
const amm = new Contract(ammAddr, AmmArtifact.abi, layer2Wallet)
27
const clearingHouse = new Contract(clearingHouseAddr, ClearingHouseArtifact.abi, layer2Wallet)
28
const clearingHouseViewer = new Contract(chViewerAddr, CHViewerArtifact.abi, layer2Wallet)
29
const layer2Amb = new Contract(layer2AmbAddr, ABI_AMB_LAYER2, layer2Wallet)
30
const layer2Bridge = new Contract(layer2BridgeAddr, ClientBridgeArtifact.abi, layer2Wallet)
Copied!
โ€Œ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.

Set up test USDC

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)
1
async function faucetUsdc(accountAddress) {
2
const faucetApiKey = "da2-h4xlnj33zvfnheevfgaw7datae"
3
const appSyncId = "izc32tpa5ndllmbql57pcxluua"
4
const faucetUrl = `https://${appSyncId}.appsync-api.ap-northeast-1.amazonaws.com/graphql`
5
const options = {
6
method: "POST",
7
headers: {
8
"Content-Type": "application/json",
9
"X-Api-Key": faucetApiKey,
10
},
11
body: JSON.stringify({
12
query: `mutation issue {issue(holderAddr:"${accountAddress}"){
13
txHashQuote
14
amountQuote
15
}
16
}`,
17
}),
18
}
19
return fetch(faucetUrl, options)
20
}
Copied!

Handling decimals

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:
1
const { parseUnits } = require("ethers/lib/utils")
2
const oneHundred = { d: parseUnits("100", 18) } // d is 100000000000000000000
Copied!

Deposit & withdraw between Ethereum and xDai

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:
1
const approveTx = await layer1Usdc.approve(layer1Bridge.address, constants.MaxUint256)
2
await approveTx.wait()
3
const depositAmount = { d: amount }
4
const transferTx = layer1Bridge.erc20Transfer(layer1Usdc.address, layer1Wallet.address, depositAmount)
5
const receipt = await transferTx.wait()
Copied!
Withdraw:
1
const approveTx = await layer2Usdc.approve(layer2Bridge.address, constants.MaxUint256)
2
await approveTx.wait()
3
const withdrawAmount = { d: amount }
4
const transferTx = layer2Bridge.erc20Transfer(layer2Usdc.address, layer2Wallet.address, withdrawAmount )
5
const receipt = await transferTx.wait()
Copied!
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.
1
const methodId = "0x482515ce" // UserRequestForAffirmation
2
const eventName = "AffirmationCompleted"
3
const [log] = receipt.logs.filter(log => log.topics[0].substr(0, 10) === methodId)
4
const fromMsgId = log.topics[1]
5
โ€‹
6
layer2Amb.on(eventName, (sender, executor, toMsgId, status, log) => {
7
if (fromMsgId === toMsgId) {
8
amb.removeAllListeners(eventName)
9
resolve(log.transactionHash)
10
}
11
})
Copied!
The withdraw process differs in terms of messageId, eventName, and listens for the layer1Amb event:
1
const methodId = "0x520d2afd" // UserRequestForSignature
2
const eventName = "RelayedMessage"
3
const [log] = receipt.logs.filter(log => log.topics[0].substr(0, 10) === methodId)
4
const fromMsgId = log.topics[1]
5
โ€‹
6
layer1Amb.on(eventName, (sender, executor, toMsgId, status, log) => {
7
if (fromMsgId === toMsgId) {
8
amb.removeAllListeners(eventName)
9
resolve(log.transactionHash)
10
}
11
})
Copied!

Approve Clearing House

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.
1
const tx = await layer2Usdc.approve(clearingHouse.address, constants.MaxUint256)
2
await tx.wait()โ€Œ
Copied!

Open position

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.
  • options: the options for ethers.js, since openPosition() costs significant gas, sometimes the xdai node cannot estimate gas correctly, so you need to set a fixed gas limit to ensure the transaction can be sent.
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:
1
const { parseUnits } = require("ethers/lib/utils")
2
async function openPosition() {
3
const DEFAULT_DECIMALS = 18
4
const side = 1 // Short
5
const quoteAssetAmount = { d: parseUnits("100", DEFAULT_DECIMALS) }
6
const leverage = { d: parseUnits("2", DEFAULT_DECIMALS) }
7
const minBaseAssetAmount = { d: "0" } // "0" can be automatically converted
8
const options = { gasLimit: 3_800_000 }
9
const tx = await clearingHouse.openPosition(
10
amm.address,
11
side,
12
quoteAssetAmount,
13
leverage,
14
minBaseAssetAmount,
15
options,
16
)
17
await tx.wait()
18
}
Copied!
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.

Query account information

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:
1
const position = await clearingHouseViewer.getPersonalPositionWithFundingPayment(
2
amm.address,
3
wallet.address,
4
)
5
const pnl = await clearingHouseViewer.getUnrealizedPnl(
6
amm.address,
7
wallet.address,
8
BigNumber.from(PNL_OPTION_SPOT_PRICE),
9
)
Copied!
โ€Œ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).

Close Position

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.
1
const minimalQuoteAsset = {d: "0"}
2
const tx = await clearingHouse.closePosition(amm.address, minimalQuoteAsset)
3
await tx.wait()
Copied!

Full code demo

The following example is a complete JavaScript template that combines all the functions mentioned above.
1
const fetch = require("cross-fetch")
2
const { Contract, Wallet, BigNumber, constants, providers } = require("ethers")
3
const AmmArtifact = require("@perp/contract/build/contracts/Amm.sol/Amm.json")
4
const ClearingHouseArtifact = require("@perp/contract/build/contracts/ClearingHouse.sol/ClearingHouse.json")
5
const RootBridgeArtifact = require("@perp/contract/build/contracts/RootBridge.sol/RootBridge.json")
6
const ClientBridgeArtifact = require("@perp/contract/build/contracts/ClientBridge.sol/ClientBridge.json")
7
const CHViewerArtifact = require("@perp/contract/build/contracts/ClearingHouseViewer.sol/ClearingHouseViewer.json")
8
const Erc20TokenArtifact = require("@perp/contract/build/contracts/ERC20Token.sol/ERC20Token.json")
9
const { parseUnits, formatEther, formatUnits } = require("ethers/lib/utils")
10
require("dotenv").config()
11
โ€‹
12
// const LONG_POS = 0
13
const SHORT_POS = 1
14
const DEFAULT_DECIMALS = 18
15
const PNL_OPTION_SPOT_PRICE = 0
16
const SHORT_AMOUNT = "100"
17
const ACTION_DEPOSIT = 0
18
const ACTION_WITHDRAW = 1
19
โ€‹
20
const ABI_AMB_LAYER1 = [
21
"event RelayedMessage(address indexed sender, address indexed executor, bytes32 indexed messageId, bool status)",
22
"event AffirmationCompleted( address indexed sender, address indexed executor, bytes32 indexed messageId, bool status)",
23
]
24
โ€‹
25
const ABI_AMB_LAYER2 = [
26
"event AffirmationCompleted( address indexed sender, address indexed executor, bytes32 indexed messageId, bool status)",
27
]
28
โ€‹
29
async function waitTx(txReq) {
30
return txReq.then(tx => tx.wait(2)) // wait 2 block for confirmation
31
}
32
โ€‹
33
async function faucetUsdc(accountAddress) {
34
const faucetApiKey = "da2-h4xlnj33zvfnheevfgaw7datae"
35
const appSyncId = "izc32tpa5ndllmbql57pcxluua"
36
const faucetUrl = `https://${appSyncId}.appsync-api.ap-northeast-1.amazonaws.com/graphql`
37
const options = {
38
method: "POST",
39
headers: {
40
"Content-Type": "application/json",
41
"X-Api-Key": faucetApiKey,
42
},
43
body: JSON.stringify({
44
query: `mutation issue {issue(holderAddr:"${accountAddress}"){
45
txHashQuote
46
amountQuote
47
}
48
}`,
49
}),
50
}
51
return fetch(faucetUrl, options)
52
}
53
โ€‹
54
async function setupEnv() {
55
const metadataUrl = "<https://metadata.perp.exchange/staging.json>"
56
const metadata = await fetch(metadataUrl).then(res => res.json())
57
const xDaiUrl = "<https://rpc.xdaichain.com/>"
58
const infuraProjectId = "04034d1ba6d141b4a5d57f872c0e52bd"
59
const rinkebyUrl = "<https://rinkeby.infura.io/v3/>" + infuraProjectId
60
const layer1Provider = new providers.JsonRpcProvider(rinkebyUrl)
61
const layer2Provider = new providers.JsonRpcProvider(xDaiUrl)
62
const layer1Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer1Provider)
63
const layer2Wallet = Wallet.fromMnemonic(process.env.MNEMONIC).connect(layer2Provider)
64
console.log("wallet address", layer1Wallet.address)
65
โ€‹
66
// layer 1 contracts
67
const layer1BridgeAddr = metadata.layers.layer1.contracts.RootBridge.address
68
const usdcAddr = metadata.layers.layer1.externalContracts.usdc
69
const layer1AmbAddr = metadata.layers.layer1.externalContracts.ambBridgeOnEth
70
โ€‹
71
const layer1Usdc = new Contract(usdcAddr, Erc20TokenArtifact.abi, layer1Wallet)
72
const layer1Bridge = new Contract(layer1BridgeAddr, RootBridgeArtifact.abi, layer1Wallet)
73
const layer1Amb = new Contract(layer1AmbAddr, ABI_AMB_LAYER1, layer1Wallet)
74
โ€‹
75
// layer 2 contracts
76
const layer2BridgeAddr = metadata.layers.layer2.contracts.ClientBridge.address
77
const layer2AmbAddr = metadata.layers.layer2.externalContracts.ambBridgeOnXDai
78
const xUsdcAddr = metadata.layers.layer2.externalContracts.usdc
79
const clearingHouseAddr = metadata.layers.layer2.contracts.ClearingHouse.address
80
const chViewerAddr = metadata.layers.layer2.contracts.ClearingHouseViewer.address
81
const ammAddr = metadata.layers.layer2.contracts.ETHUSDC.address
82
โ€‹
83
const layer2Usdc = new Contract(xUsdcAddr, Erc20TokenArtifact.abi, layer2Wallet)
84
const amm = new Contract(ammAddr, AmmArtifact.abi, layer2Wallet)
85
const clearingHouse = new Contract(clearingHouseAddr, ClearingHouseArtifact.abi, layer2Wallet)
86
const clearingHouseViewer = new Contract(chViewerAddr, CHViewerArtifact.abi, layer2Wallet)
87
const layer2Amb = new Contract(layer2AmbAddr, ABI_AMB_LAYER2, layer2Wallet)
88
const layer2Bridge = new Contract(layer2BridgeAddr, ClientBridgeArtifact.abi, layer2Wallet)
89
โ€‹
90
console.log("USDC address", usdcAddr)
91
โ€‹
92
return {
93
amm,
94
clearingHouse,
95
layer1Usdc,
96
layer2Usdc,
97
layer1Wallet,
98
layer2Wallet,
99
clearingHouseViewer,
100
layer1Bridge,
101
layer2Bridge,
102
layer1Amb,
103
layer2Amb,
104
}
105
}
106
โ€‹
107
async function openPosition(clearingHouse, amm) {
108
const quoteAssetAmount = {
109
d: parseUnits(SHORT_AMOUNT, DEFAULT_DECIMALS),
110
}
111
const leverage = { d: parseUnits("2", DEFAULT_DECIMALS) }
112
const minBaseAssetAmount = { d: "0" }
113
const options = { gasLimit: 3_800_000 }
114
await waitTx(
115
clearingHouse.openPosition(
116
amm.address,
117
SHORT_POS,
118
quoteAssetAmount,
119
leverage,
120
minBaseAssetAmount,
121
options,
122
),
123
)
124
}
125
โ€‹
126
async function printInfo(clearingHouseViewer, amm, wallet) {
127
console.log("getting information")
128
const position = await clearingHouseViewer.getPersonalPositionWithFundingPayment(
129
amm.address,
130
wallet.address,
131
)
132
const pnl = await clearingHouseViewer.getUnrealizedPnl(
133
amm.address,
134
wallet.address,
135
BigNumber.from(PNL_OPTION_SPOT_PRICE),
136
)
137
โ€‹
138
console.log("- current position", formatUnits(position.size.d, DEFAULT_DECIMALS))
139
console.log("- pnl", formatUnits(pnl.d, DEFAULT_DECIMALS))
140
}
141
โ€‹
142
async function printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc) {
143
// get ETH & USDC balance
144
const ethBalance = await layer1Wallet.getBalance()
145
const xDaiBalance = await layer2Wallet.getBalance()
146
let layer1UsdcBalance = await layer1Usdc.balanceOf(layer1Wallet.address)
147
let layer2UsdcBalance = await layer2Usdc.balanceOf(layer1Wallet.address)
148
const layer1UsdcDecimals = await layer1Usdc.decimals()
149
const layer2UsdcDecimals = await layer2Usdc.decimals()
150
โ€‹
151
const outputs = [
152
"balances",
153
`- layer 1`,
154
` - ${formatEther(ethBalance)} ETH`,
155
` - ${formatUnits(layer1UsdcBalance, layer1UsdcDecimals)} USDC`,
156
`- layer 2`,
157
` - ${formatEther(xDaiBalance)} xDAI`,
158
` - ${formatUnits(layer2UsdcBalance, layer2UsdcDecimals)} USDC`,
159
]
160
console.log(outputs.join("\\n"))
161
}
162
โ€‹
163
async function waitCrossChain(action, receipt, layer1Amb, layer2Amb) {
164
let methodId
165
let eventName
166
let amb
167
โ€‹
168
if (action === ACTION_DEPOSIT) {
169
methodId = "0x482515ce" // UserRequestForAffirmation
170
eventName = "AffirmationCompleted"
171
amb = layer2Amb
172
} else if (action === ACTION_WITHDRAW) {
173
methodId = "0x520d2afd" // UserRequestForSignature
174
eventName = "RelayedMessage"
175
amb = layer1Amb
176
} else {
177
throw new Error("unknown action: " + action)
178
}
179
โ€‹
180
return new Promise(async (resolve, reject) => {
181
if (receipt && receipt.logs) {
182
const matched = receipt.logs.filter(log => log.topics[0].substr(0, 10) === methodId)
183
if (matched.length === 0) {
184
return reject("methodId not found: " + methodId)
185
}
186
const log = matched[0]
187
const fromMsgId = log.topics[1]
188
console.log("msgId from receipt", fromMsgId)
189
amb.on(eventName, (sender, executor, toMsgId, status, log) => {
190
console.log("got event", toMsgId)
191
if (fromMsgId === toMsgId) {
192
amb.removeAllListeners(eventName)
193
resolve(log.transactionHash)
194
}
195
})
196
} else {
197
reject("receipt or log not found")
198
}
199
})
200
}
201
โ€‹
202
async function main() {
203
const {
204
amm,
205
clearingHouse,
206
layer1Usdc,
207
layer2Usdc,
208
layer1Wallet,
209
layer2Wallet,
210
clearingHouseViewer,
211
layer1Bridge,
212
layer2Bridge,
213
layer1Amb,
214
layer2Amb,
215
} = await setupEnv()
216
โ€‹
217
// get ETH & USDC balance
218
let layer1UsdcBalance = await layer1Usdc.balanceOf(layer1Wallet.address)
219
โ€‹
220
// if no USDC, faucet to get more USDC
221
while (!layer1UsdcBalance.gt(0)) {
222
console.log("faucet USDC")
223
await faucetUsdc(layer1Wallet.address)
224
layer1UsdcBalance = await layer1Usdc.balanceOf(layer1Wallet.address)
225
}
226
โ€‹
227
const amount = parseUnits(SHORT_AMOUNT, DEFAULT_DECIMALS)
228
โ€‹
229
await printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc)
230
โ€‹
231
// approve USDC
232
const allowanceForBridge = await layer1Usdc.allowance(layer1Wallet.address, layer1Bridge.address)
233
if (allowanceForBridge.lt(amount)) {
234
console.log("approving all tokens for root bridge on layer 1")
235
await waitTx(layer1Usdc.approve(layer1Bridge.address, constants.MaxUint256))
236
}
237
โ€‹
238
// deposit to layer 2
239
console.log("depositing to layer 2")
240
const depositAmount = { d: amount }
241
const layer1Receipt = await waitTx(
242
layer1Bridge.erc20Transfer(layer1Usdc.address, layer1Wallet.address, depositAmount),
243
)
244
console.log("waiting confirmation on layer 2")
245
await waitCrossChain(ACTION_DEPOSIT, layer1Receipt, layer1Amb, layer2Amb)
246
await printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc)
247
โ€‹
248
const allowanceForClearingHouse = await layer2Usdc.allowance(
249
layer2Wallet.address,
250
clearingHouse.address,
251
)
252
if (allowanceForClearingHouse.lt(amount)) {
253
console.log("approving all tokens for clearing house on layer 2")
254
await waitTx(layer2Usdc.approve(clearingHouse.address, constants.MaxUint256))
255
}
256
โ€‹
257
console.log("opening position")
258
await openPosition(clearingHouse, amm)
259
await printInfo(clearingHouseViewer, amm, layer2Wallet)
260
โ€‹
261
console.log("closing position")
262
await waitTx(clearingHouse.closePosition(amm.address, { d: "0" }))
263
await printInfo(clearingHouseViewer, amm, layer2Wallet)
264
โ€‹
265
// withdraw to layer 1
266
console.log("approving all token for client bridge on layer 2")
267
await waitTx(layer2Usdc.approve(layer2Bridge.address, constants.MaxUint256))
268
โ€‹
269
console.log("withdraw 50 USDC from layer 2 to layer 1")
270
const layer2Receipt = await waitTx(
271
layer2Bridge.erc20Transfer(layer2Usdc.address, layer2Wallet.address, {
272
d: parseUnits("50", DEFAULT_DECIMALS),
273
}),
274
)
275
console.log("waiting confirmation on layer 1")
276
await waitCrossChain(ACTION_WITHDRAW, layer2Receipt, layer1Amb, layer2Amb)
277
await printBalances(layer1Wallet, layer2Wallet, layer1Usdc, layer2Usdc)
278
}
279
โ€‹
280
main()
Copied!
โ€Œ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:
1
$ node main.js
2
wallet address 0x88D427c17d1Ac4100b598b15CE65110F2030A7Cf
3
USDC address 0x40D3B2F06f198D2B789B823CdBEcD1DB78090D74
4
balances
5
- layer 1
6
- 0.098113582 ETH
7
- 8950.0 USDC
8
- layer 2
9
- 0.995539288 xDAI
10
- 1048.294207 USDC
11
depositing to layer 2
12
waiting confirmation on layer 2
13
msgId from receipt 0x000500009e7518627c2468e5f236bfec42b9026816e66b370000000000001ec2
14
got event 0x000500009e7518627c2468e5f236bfec42b9026816e66b370000000000001ec2
15
balances
16
- layer 1
17
- 0.097912091 ETH
18
- 8850.0 USDC
19
- layer 2
20
- 0.995539288 xDAI
21
- 1148.294207 USDC
22
approving all tokens for clearing house on layer 2
23
opening position
24
getting information
25
- current position -0.335740825585871707
26
- pnl -0.000000000000000178
27
closing position
28
getting information
29
- current position 0.0
30
- pnl 0.0
31
approving all token for client bridge on layer 2
32
withdraw 50 USDC from layer 2 to layer 1
33
waiting confirmation on layer 1
34
msgId from receipt 0x00050000dd91aecde2ad4ff420b70fff98bad16a14bb881700000000000002fe
35
got event 0x00050000dd91aecde2ad4ff420b70fff98bad16a14bb881700000000000002fe
36
balances
37
- layer 1
38
- 0.097912091 ETH
39
- 8900.0 USDC
40
- layer 2
41
- 0.994502962 xDAI
42
- 1097.894206 USDC
43
โ€‹
Copied!
โ€ŒThe code above is also available on our github. You can clone the code, install all dependencies and then execute node main.js.
GitHub - perpetual-protocol/perp-contract-demo
GitHub

Appendix

There are a few more numbers that can be calculated based on the properties of the contracts.

Entry Price

Entry price can be calculated by open notional & position size via getPersonalPositionWithFundingPayment(). Using the big.js module to deal with Big Numbers is recommended.
1
const Big = require('big.js')
2
โ€‹
3
const position = await clearingHouseViewer.getPersonalPositionWithFundingPayment(
4
amm.address,
5
wallet.address,
6
)
7
const openNotional = new Big(formatUnits(position.openNotional.d, DEFAULT_DECIMALS))
8
const size = new Big(formatUnits(position.size.d, DEFAULT_DECIMALS))
9
โ€‹
10
const entryPrice = openNotional.div(size).abs()
Copied!

Estimated Liquidation Price

The estimated liquidation price is more complicated to calculate. You can use the code below to get it.
1
import Big from "big.js"
2
โ€‹
3
export function getLiquidationPrice(
4
leverage: Big,
5
margin: Big,
6
openNotional: Big,
7
positionSize: Big,
8
mmr: Big, // mmr: maintenanceMarginRatio
9
k: Big,
10
): Big {
11
// NOTE: return zero for the case of no liquidation price
12
// set 0.0001 as the deviation value
13
if (leverage.lte(1.0001)) {
14
return new Big(0)
15
}
16
const pn = positionSize.gte(0)
17
? margin.minus(openNotional).div(mmr.minus(1))
18
: margin.add(openNotional).div(mmr.add(1))
19
const x = positionSize.gte(0)
20
? positionSize
21
.mul(-0.5)
22
.add(positionSize.mul(pn).pow(2).add(pn.mul(k).mul(positionSize).mul(4)).sqrt().div(pn.mul(2)))
23
: positionSize
24
.mul(-0.5)
25
.add(positionSize.mul(pn).pow(2).minus(pn.mul(k).mul(positionSize).mul(4)).sqrt().div(pn.mul(-2)))
26
return k.div(x.pow(2))
27
}
28
โ€‹
Copied!
Last modified 1mo ago