Nodejs api service for equilibrium decentralized exchange
Please use yarn
as package manager for resolutions section in package.json to work properly
$ yarn
Add seed phrases as array of strings to seeds.json config at package root level. You can use seeds-example.json. Replace seeds and rename to seeds.json.
$ cat seeds.json
[
"shoe salon decrease sudden person estate describe just ozone shrimp lava injury",
"despair grain funny buffalo liar drive eight swift soup work anchor cousin"
]
You can setup server port, host, blockchain node url and history api url using .env file
$ cat .env
PORT=3000
HOST="0.0.0.0"
CHAIN_NODE="wss://parachain-testnet.equilab.io/eq/collator/api/wss"
API_ENDPOINT="https://apiv3.equilibrium.io/api"
PURGE_TIMEOUT=600
PURGE_TIMEOUT
is delay in seconds before internal dictionary entry will be purged.
You will not be able to GET limitOrder
after order data is purged.
If you have account to master pools add it to environment
POOLS_MASTER="cZifcgcutJWjcCnLheB1Zv3LMkB1jLkiREWdE5hYyGZNx97uF"
$ yarn dev
yarn run v1.22.17
$ . .env && nodemon src/server.ts
...
Initializing keyring...
Server started at port 3000
Keyring initialized
Address added 5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK
Address added 5G1U9j8KePj67pUcQaPZuaqvdzuV5tkdnDrZq3wZCGVYBRzb
...
Chain id initialized: 1008
You can check addresses retrieved from your seed phrases. Now they can be used to sign transactions. Chain id is requested automatically on service start and is used for history api requests (such as get trades history).
Use requests.http
to test service in dev mode. VSCode extension REST Client
is very handy for http requests.
Request chain id using GET
request /chainId
GET http://127.0.0.1:3000/chainId HTTP/1.1
Successful response:
{
"success": true,
"chainId": 10006
}
When token
is in request it is validated using schema
src/constants.ts
contains whitelisted tokens
export const TOKENS = ["WBTC", "ETH", "GENS", "EQD", "BNB"];
src/schemas.ts
contains token schema
const token = { type: "string", enum: TOKENS };
If you need more tokens add them to TOKENS
constant
Request token info via GET
request /token/:token
GET http://127.0.0.1:3000/token/WBTC HTTP/1.1
Response should contain array with one element:
[
{
"token": "Wbtc",
"asset": 2002941027,
"lot": "0.01",
"priceStep": "1",
"makerFee": "0.0005",
"takerFee": "0.001"
}
]
Amounts divided by 1e18 for human readability
Request orderbook using GET
request /orderBook/:token?depth=:posInt
GET http://127.0.0.1:3000/orderBook/WBTC?depth=2 HTTP/1.1
Output is orderbook prices with volume
{
"bids": [
[
"43050.0",
"0.68"
],
[
"43041.0",
"0.11"
]
],
"asks": [
[
"43090.0",
"0.1"
],
[
"43100.0",
"0.58"
]
]
}
Request order list for token (e.g. WBTC)
GET http://127.0.0.1:3000/orders/WBTC HTTP/1.1
Response is array of orders currently registered on chain
[
{
"id": 36,
"account": "cZhbYJCcmnJnRTvbx5YVvrUrt45PL9sgwoT4e4HvpVjFKNvsA",
"side": "buy",
"price": "34950.0",
"amount": "1.0",
"createdAt": "1644842586",
"expirationTime": "0"
},
...
]
Request order list for token (e.g. WBTC) filtered by given address
GET http://127.0.0.1:3000/orders/WBTC/cZhbYJCcmnJnRTvbx5YVvrUrt45PL9sgwoT4e4HvpVjFKNvsA HTTP/1.1
Response is array of orders currently registered on chain
[
{
"id": 36,
"account": "cZhbYJCcmnJnRTvbx5YVvrUrt45PL9sgwoT4e4HvpVjFKNvsA",
"side": "buy",
"price": "34950.0",
"amount": "1.0",
"createdAt": "1644842586",
"expirationTime": "0"
},
...
]
Request best bid and ask for token
GET http://127.0.0.1:3000/bestPrices/WBTC HTTP/1.1
Response is object with bid and ask
{
"ask": "35010.0",
"bid": "34970.0"
}
Request trades by token from history API
GET http://127.0.0.1:3000/trades/WBTC HTTP/1.1
Response is array of trades
[
{
"id": 289,
"chainId": 1008,
"eventCounter": 1,
"currency": "WBTC",
"price": 35010,
"amount": 1,
"takerRest": 0,
"makerAccountId": "cZexYhjJa3nh9wWiEVSczvVhneHWXcLBnm3SRP6gY6QGSdNRA",
"takerAccountId": "cZexYhjJa3nh9wWiEVSczvVhneHWXcLBnm3SRP6gY6QGSdNRA",
"makerSide": "Sell",
"makerOrderId": 26,
"blockNumber": 233522,
"exchangeDate": "2022-02-11T15:07:12",
"takerFee": 35.01,
"makerFee": 17.505
},
...
]
Use pagination when possible
GET http://127.0.0.1:3000/trades/WBTC?page=0&pageSize=5 HTTP/1.1
Defaults are page=0
and pageSize=100
To get trades list for address use request
GET http://127.0.0.1:3000/tradesByAddress/WBTC/cZhTPXeT5o3DVgEnRQ95Vi8BNiyPsDoFDovXZLCeWmDKB89WW HTTP/1.1
Response format is the same. Also pagination can be used
GET http://127.0.0.1:3000/tradesByAddress/WBTC/cZhTPXeT5o3DVgEnRQ95Vi8BNiyPsDoFDovXZLCeWmDKB89WW?page=0&pageSize=3 HTTP/1.1
To get trading balances send GET
request balances/:token/:address
GET http://127.0.0.1:3000/balances/WBTC/5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK HTTP/1.1
Response looks like
{
"masterBalance": "1000000899999941400",
"tradingBalance": "100000000000"
}
Balance precision is 10^9. Divide by 10^9 to get token amount.
If you don't have trading balance object key tradingBalance
will be missing. Transfer funds using deposit method below.
To get latest usd prices from oracles on chain send GET
request /rates
GET http://127.0.0.1:3000/rates HTTP/1.1
Response format looks like:
[
{
"price": "426.887",
"token": "Bnb",
"asset": 6450786
},
{
"price": "2.6186",
"token": "Crv",
"asset": 6517366
},
...
]
To get info on account balances locked by orders send GET
request /lockedBalance/:address/
GET http://127.0.0.1:3000/lockedBalance/cZirADTgk9CYy2ed3y1UPh8jTmWoes2SRd1JSUDntkPpiMQ6X HTTP/1.1
You should recieve
{
"collateralUsd": "260154.7442",
"debtUsd": "0",
"lockedUsd": "124787",
"availableUsd": "135367.7442"
}
Where collateralUsd
is your total trading balance in USD, lockedUsd
is amount locked by active orders
If you use the same account to borrow funds debtUsd
is your total debt in USD
To start trading you should deposit funds to trading account. Use deposit request
POST http://127.0.0.1:3000/deposit HTTP/1.1
content-type: application/json
{
"address": "5G1U9j8KePj67pUcQaPZuaqvdzuV5tkdnDrZq3wZCGVYBRzb",
"token": "WBTC",
"amount": 0.01
}
Request is synchronous. Wait until operation is completed.
Funds fill be transferred FROM address in request to trading subaccount.
Successfull response looks like this
[
{
"index": "0x0000",
"data": [
{
"weight": 2580800000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
Failed response can look like this
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "bailsman.WrongMargin: Wrong margin for operation performing"
}
Use withdraw request
POST http://127.0.0.1:3000/withdraw HTTP/1.1
content-type: application/json
{
"address": "5G1U9j8KePj67pUcQaPZuaqvdzuV5tkdnDrZq3wZCGVYBRzb",
"token": "WBTC",
"amount": 0.01
}
Request is synchronous. Wait until operation is completed.
Funds fill be transferred TO address in request rom trading subaccount.
Successfull response looks like this
[
{
"index": "0x0000",
"data": [
{
"weight": 1669051000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
To register market order send request
POST http://127.0.0.1:3000/marketOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "WBTC",
"amount": 1,
"direction": "buy"
}
Service will immediately respond with message id
{
"success": true,
"payload": {
"messageId": "16448487391741"
}
}
Use this message id to request order status
GET http://127.0.0.1:3000/marketOrder/16448489453521 HTTP/1.1
{
"success": true,
"pending": false,
"payload": [
{
"index": "0x0000",
"data": [
{
"weight": 1271400000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
}
This status means that order is successfully registered in chain.
If order creation is in progress (we haven't recieved "success" event)
{
"success": false,
"pending": true,
"payload": {
"message": "Order is creating"
}
}
To register limit order you can send request
POST http://127.0.0.1:3000/limitOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "WBTC",
"limitPrice": 34940,
"amount": 1,
"direction": "buy"
}
You will recieve immediate response with message id.
{
"success": true,
"payload": {
"messageId": "16448490953732",
"nonce": 73,
"tip": 0
}
}
Use this message id to request order status
GET http://127.0.0.1:3000/limitOrder/16448490953732 HTTP/1.1
Successfull response has orderId
. You can also see this order in get order list request (see above)
If you recieve orderId
in payload - order is registered in orderbook
{
"success": true,
"pending": false,
"payload": [
{
"orderId": "39"
},
{
"index": "0x0000",
"data": [
{
"weight": 1271400000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
}
If order is executed immediately on arrival it won't be registered in orderbook and won't have orderId
Then you will recieve success: true
and pending: false
without orderId
inside payload.
{
"success": true,
"pending": false,
"payload": [
{
"index": "0x0000",
"data": [
{
"weight": 1271400000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
}
Unsuccessfull order creation will result in message like this
"success": false,
"pending": false,
"payload": {
"error": {
"registryErrors": [
{
"args": [],
"docs": [
" Order price should be in corridor"
],
"fields": [],
"index": 7,
"method": "OrderPriceShouldBeInCorridor",
"name": "OrderPriceShouldBeInCorridor",
"section": "eqDex"
}
]
}
}
}
To cancel limit order send DELETE
request
DELETE http://127.0.0.1:3000/limitOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "WBTC",
"price": 34940,
"orderId": 1620
}
Successfull response looks like this
[
{
"index": "0x0000",
"data": [
{
"weight": 700300000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
Failed response looks like this
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "eqDex.OrderNotFound: No order found by id and price"
}
You can replace limit order using PUT
limitOrder request. When you sent POST
limitOrder request service responded with messageId
and nonce
. Use them to replace order. Add tip
to raise your transaction priority. It will be used if transaction is waiting for block to finalize. Transactions with same nonce wich has highest tip
will be in block.
There are 4 scenarios:
- Limit order is queued and replaced by new one
- Limit order is queued and cancelled (send
limitPriceNew = 0
) - Limit order is on chain. It is cancelled and new one registered
- Limit order is on chain and
limitPriceNew = 0
. Order is cancelled
PUT http://127.0.0.1:3000/limitOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "WBTC",
"limitPrice": 39002,
"limitPriceNew": 39003,
"amountNew": 0.1,
"direction": "buy",
"messageId": "16454558118451",
"nonce": 71,
"tip": 10
}
If you send limitPriceNew = 0
order will be cancelled.
Order can be replaced and cancelled even before it is finalized in block.
Possible success response looks like this
{
"success": true,
"payload": {
"message": "Limit order is creating",
"messageId": "16454558286312",
"nonce": 73,
"tip": 0
}
}
To get pending createLimitOrder extrinsics send GET
GET http://127.0.0.1:3000/pendingExtrinsics/cZirADTgk9CYy2ed3y1UPh8jTmWoes2SRd1JSUDntkPpiMQ6X
Response is array of eqDex.createOrder
extrinsics
[
{
"isSigned": true,
"method": {
"args": {
"asset": {
"0": "2,002,941,027"
},
"order_type": {
"Limit": {
"price": "38,990,000,000,000",
"expiration_time": "0"
}
},
"side": "Buy",
"amount": "10,000,000,000,000,000"
},
"method": "createOrder",
"section": "eqDex"
},
"era": {
"MortalEra": {
"period": "64",
"phase": "21"
}
},
"nonce": "780",
"signature": "0x94502be17d2155c99532fd12ce80e192524c2c89f948fddfe15cfc1c3f936645051526abda83d0c23eabd31af9410a951a49fc800284367aeda51cbaa7be518f",
"signer": "cZirADTgk9CYy2ed3y1UPh8jTmWoes2SRd1JSUDntkPpiMQ6X",
"tip": "0"
},
...
]
Some actions can be made using mm pools (if you have account which masters them).
Pool is created via user deposits of tokens. One pool has only one token.
You can view pool info via
GET http://127.0.0.1:3000/mmPool/KSM HTTP/1.1
If pool exists respnonse will look like
[
{
"account_id": "cZj5AEB31a33YAQoiRu3TjquxQi13j8GhowFmB6TGxHpaq42H",
"min_amount": 10000000000,
"total_staked": 133000000000000,
"total_deposit": 132758000000000,
"total_borrowed": 242000000000,
"total_pending_withdrawals": {
"last_epoch": 16113,
"available": 0,
"available_next_epoch": 0,
"requested": 0
}
}
]
Amounts has 10^9 decimals. Example above says that KSM pool has 133000 KSM deposited and 242 KSM borrowed
Every pool can have several market makers. Each market maker can borrow X% of the pool.
To find out mm limit use
GET http://127.0.0.1:3000/marketMaker/1/KSM HTTP/1.1
Response will look like
[
{
"weight": 400000000,
"borrowed": 242000000000
}
]
Numbers has 10^9 decimals. Response above tells us that we have 40% of KSM pool and all
manager accounts of mm with id 1
borrowed 242 KSM.
Each market maker can have several manager accounts.
These accounts can be used to sign transactions for opening and cancelling orders.
But manager account does not have direct access to funds.
Special trader account is created and has funds borrowed from pool.
To get this account address you can request
GET http://127.0.0.1:3000/traderAddress/cZfMdXut7KkaRNnBSAAnzwehVwH7gZuLNNMT9D9Xv3bcqMSpQ HTTP/1.1
{
"trader": "cZiKhG57RJy9FASQrLRDnKbpEH5uCgpAREVHREkymzrVjsq76"
}
Use this address just like ordinary acccount to request info such as orders
, balances
, lockedBalance
, trades
etc.
Use your manager address to sign transactions when creating or deleting orders.
For signed actions you should add "isUsingPool": true
to requests and sign them with manager account.
To start trading you should deposit funds to trading account.
Use deposit request with address
of your manager account
POST http://127.0.0.1:3000/deposit HTTP/1.1
content-type: application/json
{
"address": "cZfMdXut7KkaRNnBSAAnzwehVwH7gZuLNNMT9D9Xv3bcqMSpQ",
"token": "KSM",
"amount": 7,
"isUsingPool": true
}
Request is synchronous. Wait until operation is completed.
Funds fill be transferred FROM POOL in request to trading subaccount.
Balance precision is 10^9. Divide by 10^9 to get token amount.
Use withdraw in similar way to return funds to pool
POST http://127.0.0.1:3000/withdraw HTTP/1.1
content-type: application/json
{
"address": "cZfMdXut7KkaRNnBSAAnzwehVwH7gZuLNNMT9D9Xv3bcqMSpQ",
"token": "KSM",
"amount": 7,
"isUsingPool": true
}
To register limit order you can send request with address
of manager
The difference for pool master is "isUsingPool": true
parameter
POST http://127.0.0.1:3000/limitOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "WBTC",
"limitPrice": 34940,
"amount": 1,
"direction": "buy",
"isUsingPool": true
}
To cancel limit order send DELETE
request with address
of manager
The difference for pool master is "isUsingPool": true
parameter
DELETE http://127.0.0.1:3000/limitOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "ETH",
"price": 3091,
"orderId": 58,
"isUsingPool": true
}
You can send DELETE
request with orders
array
to cancel list of limit orders
DELETE http://127.0.0.1:3000/limitOrders HTTP/1.1
content-type: application/json
{
"address": "cZifcgcutJWjcCnLheB1Zv3LMkB1jLkiREWdE5hYyGZNx97uF",
"isUsingPool": true,
"orders": [
{
"token": "WBTC",
"price": 42900,
"orderId": 123415
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123416
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123417
}
]
}
You get response with messageId
{
"success": true,
"payload": {
"message": "Orders are cancelling",
"messageId": "16524431221121",
"orders": [
{
"token": "WBTC",
"price": 42900,
"orderId": 123415
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123416
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123417
}
],
"events": []
}
}
Use it to get orders statuses in events array with GET http://127.0.0.1:3000/limitOrder/16524431221121 HTTP/1.1
If everything is ok you will receive
{
"success": true,
"pending": false,
"payload": {
"message": "Orders are cancelling",
"messageId": "16524431221121",
"orders": [
{
"token": "WBTC",
"price": 42900,
"orderId": 123415
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123416
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123417
}
],
"events": [
{
"success": true,
"pending": false,
"payload": [
{
"orderId": "123415"
},
{
"index": "0x0000",
"data": [
{
"weight": 700300000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
},
{
"success": true,
"pending": false,
"payload": [
{
"orderId": "123416"
},
{
"index": "0x0000",
"data": [
{
"weight": 700300000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
},
{
"success": true,
"pending": false,
"payload": [
{
"orderId": "123417"
},
{
"index": "0x0000",
"data": [
{
"weight": 700300000,
"class": "Normal",
"paysFee": "Yes"
}
]
}
]
}
]
}
}
If error happens you will see them in events array:
{
"success": true,
"pending": false,
"payload": {
"message": "Orders are cancelling",
"messageId": "16524433895092",
"orders": [
{
"token": "WBTC",
"price": 42900,
"orderId": 123415
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123416
},
{
"token": "WBTC",
"price": 42900,
"orderId": 123417
}
],
"events": [
{
"success": false,
"pending": false,
"error": "Error: eqDex.OrderNotFound: No order found by id and price"
},
{
"success": false,
"pending": false,
"error": "Error: eqDex.OrderNotFound: No order found by id and price"
},
{
"success": false,
"pending": false,
"error": "Error: eqDex.OrderNotFound: No order found by id and price"
}
]
}
}
So you can compare orders
array and events
array. If order was successfully cancelled if will have success: true
and payload with { orderId }
You can replace limit order using PUT
limitOrder request.
The difference for pool master is "isUsingPool": true
parameter
PUT http://127.0.0.1:3000/limitOrder HTTP/1.1
content-type: application/json
{
"address": "5GC1gZuBV5YSwgkxjQrPggF2fLhQcAUeAiXnDaBUg6wJPvtK",
"token": "WBTC",
"limitPrice": 39002,
"limitPriceNew": 39003,
"amountNew": 0.1,
"direction": "buy",
"messageId": "16454558118451",
"nonce": 71,
"tip": 10,
"isUsingPool": true
}
To build and run service use start
command
$ yarn start
You can get candle data from our public history api
Request GET /api/v1/udf/chains/{chainId}/history
https://apiv3.equilibrium.io/api/v1/udf/chains/10006/history?symbol=WBTC%2FEQD&resolution=1D&from=1648394458&to=1649258458
Parameters:
chainId: int32, required
symbol: string
from: int64, unix timestamp in seconds
to: int64, unix timestamp in seconds
resolution: string, possible values are 5, 15, 30, 60, 1D, 1W, 1M
Response Schema
{
statusCode UdfApiStatusesinteger($int32)
Enum:
[ 0, 1, 2 ]
barTime [
nullable: true
integer($int32)]
closingPrice [
nullable: true
number($float)]
openingPrice [
nullable: true
number($float)]
highPrice [
nullable: true
number($float)]
lowPrice [
nullable: true
number($float)]
volume [
nullable: true
number($float)]
}
Response example
{"s":"ok","t":[1649116800],"c":[43050.0],"o":[43050.0],"h":[43050.0],"l":[43050.0],"v":[12915.0]}