├── .gitignore ├── @types └── rippled-ws-client │ └── index.d.ts ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── samples ├── PlainSample.js ├── Sample.ts ├── TableCompare.ts └── TableCompareSmall.ts ├── src ├── index.ts ├── parser │ └── LiquidityParser.ts └── types │ ├── Reader.ts │ └── XrplObjects.ts ├── test ├── reader-rippledwsclient.test.ts └── reader-xrplclient.test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | /dist 4 | -------------------------------------------------------------------------------- /@types/rippled-ws-client/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rippled-ws-client' { 2 | interface xrplResponse { 3 | [key: string]: any 4 | } 5 | 6 | export default class Client { 7 | constructor (wssEndpoint: string) 8 | on (eventName: string, method: Function): void 9 | close (): void 10 | send (command: object): Promise 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wietse Wind 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XRPL Orderbook Reader [![npm version](https://badge.fury.io/js/xrpl-orderbook-reader.svg)](https://www.npmjs.com/xrpl-orderbook-reader) 2 | 3 | This repository takes XRPL Orderbook (`book_offers`) datasets and requested volume to 4 | exchange and calculates the effective exchange rates based on the requested and available liquidity. 5 | 6 | Optionally certain checks can be specified (eg. `book_offers` on the other side of the book) 7 | to warn for limited (percentage) liquidity on the requested side, and possibly other side 8 | of the order book. 9 | 10 | **Typescript 3.8+ is required** 11 | 12 | ## How to use: 13 | For now: See `samples/Sample.ts` 14 | 15 | ### Call & get results 16 | 17 | Please create one `LiquidityCheck` instance per pair (from/to). 18 | 19 | ``` 20 | const Lc = new LiquidityCheck(Params) 21 | const Lq = await Lc.get() 22 | log(Lq.rate) 23 | ``` 24 | 25 | You can update Params and fetch new data, and get the results based on the new data 26 | with the `refresh` method. When called without input parameter (`refresh()`) existing 27 | Params will be used based on fresh order book information. If a new Params object is 28 | provided, the entire instance will be updated. 29 | 30 | ``` 31 | // Lc instance already exists 32 | // const Lc = new LiquidityCheck(Params) 33 | 34 | Params.trade.amount += 1000 35 | 36 | Lc.refresh(Params) 37 | const newLq = await Lc.get() 38 | ``` 39 | 40 | Available options [here](https://github.com/XRPL-Labs/XRPL-Orderbook-Reader/blob/38be170007366095bd078713ecbb65684420539d/src/types/Reader.ts#L17). 41 | 42 | ### Client 43 | This lib. requires a connection to an XRPL node. There are two supported clients: 44 | 45 | #### Preferred: `xrpl-client` - https://www.npmjs.com/package/xrpl-client 46 | 47 | You can pass the entire class instance of `xrpl-client` to the Params object. You 48 | pass the class as `client` property. 49 | 50 | ``` 51 | new LiquidityCheck({ 52 | <...>, 53 | client: new XrplClient() 54 | }) 55 | ``` 56 | 57 | #### Deprecated: `rippled-ws-client` - https://www.npmjs.com/package/rippled-ws-client 58 | 59 | You can pass the `send` method of `rippled-ws-client` to the Params object as `method` property. 60 | 61 | ``` 62 | new LiquidityCheck({ 63 | <...>, 64 | method: RippledWsClientInstance.send 65 | }) 66 | ``` 67 | 68 | #### Custom WebSocket/... implementation 69 | 70 | You can pass a `send` method to the Params object. The `send` method passed 71 | to the Params should take an object with a `command` for rippled and return a `Promise` 72 | that will resolve to contain requested order book lines. You pass a function as `method` property. 73 | 74 | ``` 75 | new LiquidityCheck({ 76 | <...>, 77 | method: (JsonRequestWithCommand) => { 78 | return new Promise(resolve) { 79 | // Custom implementation to fetch the book results 80 | } 81 | } 82 | }) 83 | ``` 84 | 85 | 86 | ## Sample in JS (not TS) environment(s): 87 | 88 | Please see `/samples/PlainSample.js` 89 | 90 | ## How to... 91 | 92 | ##### Test: 93 | 94 | `npm run test` 95 | 96 | ##### Run development code: 97 | 98 | `npm run dev` (Compiles and runs `/samples/Sample.ts`) 99 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | transform: { 4 | "^.+\\.ts?$": "ts-jest" 5 | }, 6 | testEnvironment: "node", 7 | testRegex: "(.*|(\\.|/)(test|spec))\\.ts?$", 8 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"] 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xrpl-orderbook-reader", 3 | "version": "1.0.0", 4 | "description": "Parse XRPL Order Book results into effective liquidity based exchange prices", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "scripts": { 8 | "prepublish": "npm run clean && npm run lint && npm run test && npm run build", 9 | "clean": "rm -rf dist", 10 | "build": "tsc", 11 | "watch": "tsc -w", 12 | "dev": "clear; npm run build; DEBUG=orderbook* nodemon dist/samples/Sample.js", 13 | "test": "jest --ci --verbose --runInBand --detectOpenHandles", 14 | "lint": "eslint" 15 | }, 16 | "files": [ 17 | "dist/**/*.js", 18 | "dist/**/*.js.map", 19 | "dist/**/*.d.ts" 20 | ], 21 | "directories": { 22 | "test": "test" 23 | }, 24 | "dependencies": { 25 | "@types/debug": "^4.1.5", 26 | "assert": "^2.0.0", 27 | "bignumber.js": "^9.0.0", 28 | "debug": "^4.1.1" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^26.0.12", 32 | "@types/node": "^12.12.47", 33 | "@typescript-eslint/eslint-plugin": "^4.25.0", 34 | "@typescript-eslint/parser": "^4.25.0", 35 | "jest": "^29.4.3", 36 | "rippled-ws-client": "^1.6.1", 37 | "ts-jest": "^29.0.5", 38 | "typescript": "4.3", 39 | "xrpl-client": "^2.0.2" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git://github.com:XRPL-Labs/XRPL-Orderbook-Reader.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/XRPL-Labs/XRPL-Orderbook-Reader/issues" 47 | }, 48 | "homepage": "https://github.com/XRPL-Labs/XRPL-Orderbook-Reader/#readme", 49 | "license": "MIT", 50 | "readmeFilename": "README.md", 51 | "keywords": [ 52 | "xrp", 53 | "xrpl-ledger", 54 | "dex", 55 | "liquidity", 56 | "offers", 57 | "orders" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /samples/PlainSample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Install: 3 | * npm install xrpl-orderbook-reader rippled-ws-client debug 4 | * Run: 5 | * DEBUG=* node index.js 6 | */ 7 | 8 | const Debug = require('debug') 9 | const { XrplClient } = require('xrpl-client') 10 | // const {LiquidityCheck} = require('xrpl-orderbook-reader') // Elsewhere 11 | const {LiquidityCheck} = require('../') // Here (in this folder) 12 | const log = Debug('orderbook') 13 | 14 | const main = async () => { 15 | const Connection = new XrplClient() 16 | Connection.on('error', e => log(`XRPL Error`, e)) 17 | 18 | const Check = new LiquidityCheck({ 19 | trade: { 20 | from: { 21 | currency: 'XRP' 22 | }, 23 | amount: 4500, 24 | to: { 25 | currency: 'USD', 26 | issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq' 27 | } 28 | }, 29 | options: { 30 | // includeBookData: true, 31 | // verboseBookData: false, 32 | rates: 'to', 33 | maxSpreadPercentage: 3, 34 | maxSlippagePercentage: 4, 35 | maxSlippagePercentageReverse: 5 36 | }, 37 | client: Connection 38 | }) 39 | const Liquidity = await Check.get() 40 | 41 | log({Liquidity}) 42 | // if (typeof Liquidity.books !== 'undefined') { 43 | // console.table(Liquidity.books[0]) 44 | // console.table(Liquidity.books[1]) 45 | // } 46 | 47 | Connection.close() 48 | } 49 | 50 | main() 51 | -------------------------------------------------------------------------------- /samples/Sample.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | const { XrplClient } = require('xrpl-client') 3 | import { 4 | LiquidityCheck, 5 | Params as LiquidityCheckParams, 6 | RatesInCurrency 7 | } from '../src/' 8 | 9 | const log = Debug('orderbook:sample') 10 | 11 | const main = async () => { 12 | /** 13 | * XRPL Connection 14 | */ 15 | const Connection = new XrplClient() 16 | Connection.on('error', (e: Error | string) => log(`XRPL Error`, e)) 17 | 18 | /** 19 | * Liquidity Check 20 | */ 21 | const Params: LiquidityCheckParams = { 22 | trade: { 23 | from: { 24 | currency: 'XRP' 25 | }, 26 | amount: 500, 27 | to: { 28 | currency: 'EUR', 29 | issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq' 30 | } 31 | }, 32 | options: { 33 | rates: RatesInCurrency.to, 34 | timeoutSeconds: 10, 35 | maxSpreadPercentage: 4, 36 | maxSlippagePercentage: 2, 37 | maxSlippagePercentageReverse: 3, 38 | maxBookLines: 250, 39 | includeBookData: true, 40 | verboseBookData: true, 41 | }, 42 | client: Connection 43 | } 44 | 45 | log(Params.trade) 46 | 47 | const Check = new LiquidityCheck(Params) 48 | const Liquidity = await Check.get() 49 | 50 | log(Liquidity) 51 | 52 | // log('rate', Liquidity.rate) 53 | // log('safe', Liquidity.safe) 54 | // log('errors', Liquidity.errors) 55 | 56 | /** 57 | * Please note: uses console.table instead of 58 | * Debug (log), as Debug is missing table output 59 | */ 60 | if (typeof Liquidity.books !== 'undefined') { 61 | console.table(Liquidity.books[0]) 62 | console.table(Liquidity.books[1]) 63 | } 64 | 65 | /** 66 | * Sample: get new data every 4 seconds for higher 67 | * requested amount to exchange. 68 | */ 69 | // setInterval(async () => { 70 | // Params.trade.amount += 1000 71 | // log(`Getting new Liquidity Data for (trade amount)`, Params.trade.amount) 72 | // Check.refresh(Params) 73 | // log(await Check.get()) 74 | // }, 4000) 75 | 76 | Connection.close() 77 | } 78 | 79 | main() 80 | -------------------------------------------------------------------------------- /samples/TableCompare.ts: -------------------------------------------------------------------------------- 1 | const { XrplClient } = require('xrpl-client') 2 | 3 | import { 4 | LiquidityCheck, 5 | Params as LiquidityCheckParams, 6 | RatesInCurrency 7 | } from '../src/' 8 | 9 | const options = { 10 | timeoutSeconds: 10, 11 | rates: RatesInCurrency.to, 12 | maxSpreadPercentage: 4, 13 | maxSlippagePercentage: 3, 14 | maxSlippagePercentageReverse: 3 15 | } 16 | 17 | const main = async () => { 18 | const Connection = new XrplClient() 19 | Connection.on('error', (e: Error | string) => console.log(`XRPL Error`, e)) 20 | 21 | const pairs = [ 22 | {issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq', currency: 'EUR', displayName: 'Gatehub EUR'}, 23 | {issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq', currency: 'USD', displayName: 'Gatehub USD'}, 24 | {issuer: 'rchGBxcD1A1C2tdxF6papQYZ8kjRKMYcL', currency: 'BTC', displayName: 'Gatehub BTC'}, 25 | {issuer: 'rcA8X3TVMST1n3CJeAdGk1RdRCHii7N2h', currency: 'ETH', displayName: 'Gatehub ETH'}, 26 | {issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', currency: 'USD', displayName: 'Bitstamp USD'}, 27 | {issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', currency: 'BTC', displayName: 'Bitstamp BTC'}, 28 | { 29 | issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', 30 | currency: '534F4C4F00000000000000000000000000000000', 31 | displayName: 'SOLO' 32 | } 33 | ] 34 | 35 | const data = await Promise.all( 36 | pairs.map( 37 | async p => { 38 | return await Promise.all([100, 1000, 2500, 5000, 10000, 20000].map(async a => { 39 | const Params: LiquidityCheckParams = { 40 | trade: { 41 | from: {currency: 'XRP'}, 42 | amount: a, 43 | to: {currency: p.currency, issuer: p.issuer} 44 | }, 45 | options, 46 | client: Connection 47 | } 48 | 49 | const Check = new LiquidityCheck(Params) 50 | const r = await Check.get() 51 | 52 | return { 53 | name: p.displayName, 54 | amount: a, 55 | rate: r.rate, 56 | errors: r.errors 57 | } 58 | } 59 | ) 60 | ) 61 | })) 62 | 63 | // console.table(data.reduce((a, b) => { 64 | // b.forEach(r => a.push(r)) 65 | // return a 66 | // }, [])) 67 | data.forEach(d => console.table(d)) 68 | 69 | Connection.close() 70 | } 71 | 72 | 73 | main() 74 | -------------------------------------------------------------------------------- /samples/TableCompareSmall.ts: -------------------------------------------------------------------------------- 1 | const { XrplClient } = require('xrpl-client') 2 | 3 | import { 4 | LiquidityCheck, 5 | Params as LiquidityCheckParams, 6 | RatesInCurrency 7 | } from '../src/' 8 | 9 | const options = { 10 | timeoutSeconds: 10, 11 | rates: RatesInCurrency.to, 12 | maxSpreadPercentage: 4, 13 | maxSlippagePercentage: 3, 14 | maxSlippagePercentageReverse: 3 15 | } 16 | 17 | const main = async () => { 18 | const Connection = new XrplClient() 19 | Connection.on('error', (e: Error | string) => console.log(`XRPL Error`, e)) 20 | 21 | const pairs = [ 22 | {issuer: 'rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De', currency: '524C555344000000000000000000000000000000', displayName: 'RLUSD'}, 23 | ] 24 | 25 | const data = await Promise.all( 26 | pairs.map( 27 | async p => { 28 | return await Promise.all([0.01, 0.1, 1, 10, 100, 1000, 10000, 100000, 1000000].map(async a => { 29 | const Params: LiquidityCheckParams = { 30 | trade: { 31 | from: {currency: 'XRP'}, 32 | amount: a, 33 | to: {currency: p.currency, issuer: p.issuer} 34 | }, 35 | options, 36 | client: Connection 37 | } 38 | 39 | const Check = new LiquidityCheck(Params) 40 | const r = await Check.get() 41 | 42 | return { 43 | name: p.displayName, 44 | amount: a, 45 | rate: r.rate, 46 | errors: r.errors 47 | } 48 | } 49 | ) 50 | ) 51 | })) 52 | 53 | // console.table(data.reduce((a, b) => { 54 | // b.forEach(r => a.push(r)) 55 | // return a 56 | // }, [])) 57 | data.forEach(d => console.table(d)) 58 | 59 | Connection.close() 60 | } 61 | 62 | 63 | main() 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import { 3 | Params as LiquidityCheckParams, 4 | Result as LiquidityCheckResult, 5 | RatesInCurrency, 6 | Options, 7 | Errors, 8 | XrplClient, 9 | RippledWsClient 10 | } from './types/Reader' 11 | import {Offer} from './types/XrplObjects' 12 | import { 13 | LiquidityParser, 14 | ParseResult, 15 | ParseResultVerbose 16 | } from './parser/LiquidityParser' 17 | import BigNumber from 'bignumber.js' 18 | 19 | interface BookOffersResponse { 20 | [key: string]: any 21 | offers: Offer[] 22 | } 23 | 24 | const log = Debug('orderbook') 25 | const logRates = log.extend('rates') 26 | 27 | class LiquidityCheck { 28 | private Params: LiquidityCheckParams 29 | private Book: Promise 30 | private BookReverse: Promise 31 | 32 | constructor (Params: LiquidityCheckParams) { 33 | // log('called') 34 | this.Params = Params 35 | 36 | this.Book = this.fetchBook(true) 37 | this.BookReverse = this.fetchBook(false) 38 | 39 | return this 40 | } 41 | 42 | refresh (Params?: LiquidityCheckParams): void { 43 | if (Params) { 44 | this.Params = Params 45 | } 46 | this.Book = this.fetchBook(true) 47 | this.BookReverse = this.fetchBook(false) 48 | } 49 | 50 | async fetchXrplData (request: Record): Promise { 51 | if ((this.Params as RippledWsClient)?.method) { 52 | return (this.Params as RippledWsClient).method(request) as T 53 | } 54 | if ((this.Params as XrplClient)?.client && (this.Params as XrplClient).client?.ready && (this.Params as XrplClient).client?.send) { 55 | return (this.Params as XrplClient).client.send(request) as T 56 | } 57 | 58 | throw new Error('Params missing either `method` (to call, e.g. xrpl-ws-client) or `client` (with send-method, e.g. xrpl-client)') 59 | } 60 | 61 | async fetchBook (requestedDirection: boolean = true): Promise { 62 | log(`Get book_offers for ${requestedDirection ? 'requested' : 'reversed'} direction`) 63 | const book = await this.fetchXrplData({ 64 | command: 'book_offers', 65 | taker_gets: requestedDirection 66 | ? this.Params.trade.to 67 | : this.Params.trade.from, 68 | taker_pays: requestedDirection 69 | ? this.Params.trade.from 70 | : this.Params.trade.to, 71 | limit: this.Params.options?.maxBookLines || 500 72 | }) 73 | 74 | log(` > Got ${book.offers.length} book_offers for ${requestedDirection ? 'requested' : 'reversed'} direction`) 75 | 76 | return book.offers 77 | } 78 | 79 | detectErrors (books: Array): Array { 80 | const errors: Array = [] 81 | 82 | if (books[0].length < 1) { 83 | errors.push(Errors.REQUESTED_LIQUIDITY_NOT_AVAILABLE) 84 | return errors 85 | } 86 | 87 | if (books[1].length < 1) { 88 | errors.push(Errors.REVERSE_LIQUIDITY_NOT_AVAILABLE) 89 | return errors 90 | } 91 | 92 | const tradeAmount = new BigNumber(this.Params.trade.amount) 93 | const book0Amount = new BigNumber(books[0].filter(l => l._Capped !== undefined).slice(-1)[0]._I_Spend_Capped) 94 | const book1Amount = new BigNumber(books[1].filter(l => l._Capped !== undefined).slice(-1)[0]._I_Get_Capped) 95 | 96 | 97 | const firstBookLine = books[0][0] 98 | const finalBookLine = books[0].filter(l => l._Capped !== undefined).slice(-1)[0] 99 | const startRate = new BigNumber( 100 | firstBookLine?._CumulativeRate_Cap || firstBookLine?._CumulativeRate 101 | ) 102 | const finalRate = new BigNumber( 103 | finalBookLine?._CumulativeRate_Cap || finalBookLine?._CumulativeRate 104 | ) 105 | 106 | const firstBookLineReverse = books[1][0] 107 | const finalBookLineReverse = books[1].filter(l => l._Capped !== undefined).slice(-1)[0] 108 | const startRateReverse = new BigNumber( 109 | firstBookLineReverse?._CumulativeRate_Cap || firstBookLineReverse?._CumulativeRate 110 | ) 111 | const finalRateReverse = new BigNumber( 112 | finalBookLineReverse?._CumulativeRate_Cap || finalBookLineReverse?._CumulativeRate 113 | ) 114 | 115 | /** 116 | * Now check for errors 117 | */ 118 | 119 | 120 | // console.log('tradeAmount', tradeAmount.decimalPlaces(16).toFixed(10)) 121 | // console.log('book0Amount', book0Amount.decimalPlaces(16).toFixed(10)) 122 | // console.log('book1Amount', book1Amount.decimalPlaces(16).toFixed(10)) 123 | 124 | if (!book0Amount.decimalPlaces(16).eq(tradeAmount.decimalPlaces(16))) { 125 | errors.push(Errors.REQUESTED_LIQUIDITY_NOT_AVAILABLE) 126 | } 127 | 128 | if (!book1Amount.decimalPlaces(16).eq(tradeAmount.decimalPlaces(16))) { 129 | errors.push(Errors.REVERSE_LIQUIDITY_NOT_AVAILABLE) 130 | } 131 | 132 | if (this.Params.options?.maxSpreadPercentage) { 133 | const spread = new BigNumber(1).minus(startRate.dividedBy(startRateReverse)).abs().times(100) 134 | logRates({ 135 | liquidityCheck: `MAX_SPREAD`, 136 | start: startRate.toNumber(), 137 | startReverse: startRateReverse.toNumber(), 138 | spread: spread.toNumber(), 139 | max: this.Params.options.maxSpreadPercentage 140 | }) 141 | 142 | if (spread.gt(new BigNumber(this.Params.options.maxSpreadPercentage))) { 143 | errors.push(Errors.MAX_SPREAD_EXCEEDED) 144 | } 145 | } 146 | 147 | if (this.Params.options?.maxSlippagePercentage) { 148 | const slippage = new BigNumber(1).minus(startRate.dividedBy(finalRate)).abs().times(100) 149 | logRates({ 150 | liquidityCheck: `MAX_SLIPPAGE`, 151 | start: startRate.toNumber(), 152 | final: finalRate.toNumber(), 153 | slippage: slippage.toNumber(), 154 | max: this.Params.options.maxSlippagePercentage 155 | }) 156 | 157 | if (slippage.gt(new BigNumber(this.Params.options.maxSlippagePercentage))) { 158 | errors.push(Errors.MAX_SLIPPAGE_EXCEEDED) 159 | } 160 | } 161 | 162 | if (this.Params.options?.maxSlippagePercentageReverse) { 163 | const slippage = new BigNumber(1).minus(startRateReverse.dividedBy(finalRateReverse)).abs().times(100) 164 | logRates({ 165 | liquidityCheck: `MAX_REVERSE_SLIPPAGE`, 166 | start: startRateReverse.toNumber(), 167 | final: finalRateReverse.toNumber(), 168 | slippage: slippage.toNumber(), 169 | max: this.Params.options.maxSlippagePercentageReverse 170 | }) 171 | 172 | if (slippage.gt(new BigNumber(this.Params.options.maxSlippagePercentageReverse))) { 173 | errors.push(Errors.MAX_REVERSE_SLIPPAGE_EXCEEDED) 174 | } 175 | } 176 | 177 | return errors 178 | } 179 | 180 | async get (): Promise { 181 | let timeout 182 | const bookData = await Promise.race([ 183 | new Promise(resolve => { 184 | const ms = this.Params.options?.timeoutSeconds || 60 185 | timeout = setTimeout(resolve, ms * 1000) 186 | }), 187 | Promise.all([this.Book, this.BookReverse]) 188 | ]) 189 | 190 | if (!Array.isArray(bookData)) { 191 | throw new Error('Timeout fetching order book data') 192 | } else if (timeout) { 193 | clearTimeout(timeout) 194 | } 195 | 196 | const books = await Promise.all([ 197 | LiquidityParser({ 198 | books: [bookData[0]], 199 | trade: this.Params.trade, 200 | options: { 201 | verbose: this.Params.options?.verboseBookData || false, 202 | rates: this.Params.options?.rates === RatesInCurrency.from 203 | ? RatesInCurrency.from 204 | : RatesInCurrency.to 205 | } 206 | }), 207 | LiquidityParser({ 208 | books: [bookData[1]], 209 | trade: this.Params.trade, 210 | options: { 211 | verbose: this.Params.options?.verboseBookData || false, 212 | rates: this.Params.options?.rates === RatesInCurrency.from 213 | ? RatesInCurrency.to 214 | : RatesInCurrency.from 215 | } 216 | }) 217 | ]) 218 | 219 | const errors = this.detectErrors(books) 220 | 221 | const finalBookLine = books[0].filter(l => l._Capped !== undefined).slice(-1)[0] 222 | const rate = finalBookLine?._CumulativeRate_Cap || finalBookLine?._CumulativeRate 223 | 224 | const response = { 225 | rate, 226 | safe: errors.length < 1, 227 | errors 228 | } 229 | 230 | if (this.Params.options?.includeBookData) { 231 | Object.assign(response, { 232 | books 233 | }) 234 | } 235 | 236 | return response 237 | } 238 | } 239 | 240 | export { 241 | LiquidityCheck, 242 | RatesInCurrency, 243 | LiquidityCheckResult as Result, 244 | Options, 245 | LiquidityCheckParams as Params, 246 | Errors 247 | } 248 | -------------------------------------------------------------------------------- /src/parser/LiquidityParser.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import {Trade, Amount, Offer} from '../types/XrplObjects' 3 | import {RatesInCurrency as Rates} from '../types/Reader' 4 | import BigNumber from 'bignumber.js' 5 | 6 | const log = Debug('orderbook:parser') 7 | const currentRippleEpoch = Math.round(Number(new Date()) / 1000 - 946684800) 8 | 9 | export interface ParseResult { 10 | _ExchangeRate?: number 11 | _I_Spend_Capped: number 12 | _I_Get_Capped: number 13 | _CumulativeRate: number 14 | _CumulativeRate_Cap: number 15 | _Capped: boolean 16 | } 17 | 18 | export interface ParseResultVerbose extends ParseResult { 19 | account?: string 20 | _I_Spend?: number 21 | _I_Get?: number 22 | TakerGets?: number 23 | TakerGetsFunded?: number 24 | TakerPays?: number 25 | TakerPaysFunded?: number 26 | Funds?: number 27 | } 28 | 29 | interface Options { 30 | rates?: Rates 31 | verbose: boolean 32 | } 33 | 34 | interface ParserOptions { 35 | books: Array 36 | trade: Trade 37 | options?: Options 38 | } 39 | 40 | enum BookType { 41 | SOURCE = 'SOURCE', 42 | RETURN = 'RETURN' 43 | } 44 | 45 | function parseAmount (amount?: Amount | string): BigNumber | undefined { 46 | if (typeof amount === 'object' && amount !== null) { 47 | return new BigNumber(amount.value) 48 | } 49 | if (typeof amount === 'string') { 50 | return (new BigNumber(amount)).dividedBy(1000000) 51 | } 52 | return undefined 53 | } 54 | 55 | function LiquidityParser (ParserData: ParserOptions): ParseResult[] | ParseResultVerbose[] { 56 | const bookData = ParserData.books[0] 57 | if (bookData.length < 1) { 58 | return [] 59 | } 60 | 61 | /** 62 | * Determine Book Direction. 63 | * If opposite (RETURN) direction: cap at 64 | * _I_Get_Capped 65 | * instead of 66 | * _I_Spend_Capped 67 | */ 68 | 69 | const fromCurrency = ParserData.trade.from 70 | const fromIsXrp = fromCurrency.currency.toUpperCase() === 'XRP' && (fromCurrency.issuer ?? '') === '' 71 | 72 | let bookType: BookType 73 | 74 | // log({ 75 | // trade: ParserData.trade, 76 | // book0: bookData[0] 77 | // }) 78 | if (typeof bookData[0].TakerPays === 'string') { 79 | // Taker pays XRP 80 | if (fromIsXrp) { 81 | bookType = BookType.SOURCE 82 | } else { 83 | bookType = BookType.RETURN 84 | } 85 | } else { 86 | // Taker pays IOU 87 | if ( 88 | fromCurrency.currency.toUpperCase() === bookData[0].TakerPays.currency.toUpperCase() && 89 | fromCurrency.issuer === bookData[0].TakerPays.issuer 90 | ) { 91 | bookType = BookType.SOURCE 92 | } else { 93 | bookType = BookType.RETURN 94 | } 95 | } 96 | 97 | const tradeAmount = new BigNumber(ParserData.trade.amount) 98 | 99 | let linesPresent = 0 100 | const data: ParseResultVerbose[] = bookData.filter((offer: Offer) => { 101 | if (offer?.Expiration) { 102 | if (offer.Expiration < currentRippleEpoch) { 103 | log('Ignoring expired offer', offer, currentRippleEpoch) 104 | return false 105 | } 106 | } 107 | 108 | // No expiration or expiration and still valid. 109 | return true 110 | }).map((offer: Offer) => { 111 | return Object.assign({}, { 112 | account: offer.Account, 113 | TakerGets: parseAmount(offer.TakerGets), 114 | TakerGetsFunded: parseAmount(offer.taker_gets_funded), 115 | TakerPays: parseAmount(offer.TakerPays), 116 | TakerPaysFunded: parseAmount(offer.taker_pays_funded), 117 | Funds: parseAmount(offer.owner_funds) 118 | }) 119 | }).filter(a => { 120 | const allowed = linesPresent > 0 || ( 121 | (a.TakerGetsFunded === undefined || (a.TakerGetsFunded && a.TakerGetsFunded.toNumber() > 0)) 122 | && 123 | (a.TakerPaysFunded === undefined || (a.TakerPaysFunded && a.TakerPaysFunded.toNumber() > 0)) 124 | ) 125 | 126 | if (allowed) { 127 | linesPresent++ 128 | } else { 129 | // log({suppressed: a}) 130 | } 131 | 132 | return allowed 133 | }).reduce((a: ParseResultVerbose[], b: ParseResultVerbose, i: number) => { 134 | const _PaysEffective = b.TakerGetsFunded === undefined 135 | ? Number(b.TakerGets) 136 | : Number(b.TakerGetsFunded) 137 | 138 | const _GetsEffective = b.TakerPaysFunded === undefined 139 | ? Number(b.TakerPays) 140 | : Number(b.TakerPaysFunded) 141 | 142 | const _GetsSum = _GetsEffective + (i > 0 ? a[i - 1]?._I_Spend || 0 : 0) 143 | const _PaysSum = _PaysEffective + (i > 0 ? a[i - 1]?._I_Get || 0 : 0) 144 | 145 | const _cmpField = bookType === BookType.SOURCE 146 | ? '_I_Spend_Capped' 147 | : '_I_Get_Capped' 148 | 149 | let _GetsSumCapped: BigNumber | undefined = new BigNumber( 150 | i > 0 && a[i - 1][_cmpField] >= ParserData.trade.amount 151 | ? a[i - 1]._I_Spend_Capped 152 | : _GetsSum 153 | ) 154 | 155 | let _PaysSumCapped: BigNumber | undefined = new BigNumber( 156 | i > 0 && a[i - 1][_cmpField] >= ParserData.trade.amount 157 | ? a[i - 1]._I_Get_Capped 158 | : _PaysSum 159 | ) 160 | 161 | let _CumulativeRate_Cap: BigNumber | undefined 162 | let _Capped: boolean | undefined = i > 0 ? a[i - 1]._Capped : false 163 | 164 | if (bookType === BookType.SOURCE) { 165 | if ( 166 | _Capped === false && 167 | _GetsSumCapped !== undefined && 168 | _GetsSumCapped.gt(tradeAmount) 169 | ) { 170 | const _GetsCap = new BigNumber(1).minus(_GetsSumCapped.minus(tradeAmount).dividedBy(_GetsSumCapped)) 171 | _GetsSumCapped = _GetsSumCapped.multipliedBy(_GetsCap) 172 | _PaysSumCapped = _PaysSumCapped.multipliedBy(_GetsCap) 173 | _Capped = true 174 | } 175 | } 176 | 177 | if (bookType === BookType.RETURN) { 178 | if ( 179 | _Capped === false && 180 | _PaysSumCapped !== undefined && 181 | _PaysSumCapped.gt(tradeAmount) 182 | ) { 183 | const _PaysCap = new BigNumber(1).minus(_PaysSumCapped.minus(tradeAmount).dividedBy(_PaysSumCapped)) 184 | _GetsSumCapped = _GetsSumCapped.multipliedBy(_PaysCap) 185 | _PaysSumCapped = _PaysSumCapped.multipliedBy(_PaysCap) 186 | _Capped = true 187 | } 188 | } 189 | 190 | if (_Capped !== undefined) { 191 | // _CumulativeRate_Cap = _GetsSumCapped / _PaysSumCapped 192 | _CumulativeRate_Cap = _GetsSumCapped.dividedBy(_PaysSumCapped) 193 | } 194 | 195 | if (i > 0 && (a[i - 1]._Capped === true || a[i - 1]._Capped === undefined)) { 196 | _GetsSumCapped = undefined 197 | _PaysSumCapped = undefined 198 | _CumulativeRate_Cap = undefined 199 | _Capped = undefined 200 | } 201 | 202 | // log ({_GetsSum, _PaysSum}) 203 | 204 | if (_GetsSum > 0 && _PaysSum > 0) { 205 | Object.assign(b, { 206 | // _PaysEffective, 207 | // _GetsEffective, 208 | _I_Spend: _GetsSum, 209 | _I_Get: _PaysSum, 210 | _ExchangeRate: _PaysEffective === 0 211 | ? undefined 212 | : _GetsEffective / _PaysEffective, 213 | _CumulativeRate: _GetsSum / _PaysSum, 214 | _I_Spend_Capped: _GetsSumCapped?.toNumber(), 215 | _I_Get_Capped: _PaysSumCapped?.toNumber(), 216 | _CumulativeRate_Cap: _CumulativeRate_Cap?.toNumber(), 217 | _Capped 218 | }) 219 | 220 | if (ParserData.options?.rates?.toLowerCase().trim() === 'to') { 221 | if (!isNaN(b?._ExchangeRate || 0)) { 222 | b._ExchangeRate = 1 / (b?._ExchangeRate || 0) 223 | } 224 | if (!isNaN(b._CumulativeRate_Cap)) { 225 | b._CumulativeRate_Cap = 1 / b._CumulativeRate_Cap 226 | } 227 | if (!isNaN(b._CumulativeRate)) { 228 | b._CumulativeRate = 1 / b._CumulativeRate 229 | } 230 | } 231 | } else { 232 | // One side of the offer is empty 233 | return a 234 | } 235 | 236 | return a.concat(b) 237 | }, []).filter(line => { 238 | let _return = true 239 | 240 | if (!ParserData.options?.verbose) { 241 | if (line._Capped === undefined || line._ExchangeRate === undefined) { 242 | _return = false 243 | } 244 | } 245 | 246 | return _return 247 | }).map(line => { 248 | if (!ParserData.options?.verbose) { 249 | delete line.account 250 | delete line._I_Spend 251 | delete line._I_Get 252 | delete line._ExchangeRate 253 | delete line.TakerGets 254 | delete line.TakerGetsFunded 255 | delete line.TakerPays 256 | delete line.TakerPaysFunded 257 | delete line.Funds 258 | } 259 | 260 | return line 261 | }) 262 | 263 | return data 264 | } 265 | 266 | export {LiquidityParser} 267 | -------------------------------------------------------------------------------- /src/types/Reader.ts: -------------------------------------------------------------------------------- 1 | import {Trade, Offer} from '../types/XrplObjects' 2 | 3 | export enum RatesInCurrency { 4 | to = 'to', 5 | from = 'from' 6 | } 7 | 8 | export enum Errors { 9 | REQUESTED_LIQUIDITY_NOT_AVAILABLE = 'REQUESTED_LIQUIDITY_NOT_AVAILABLE', 10 | REVERSE_LIQUIDITY_NOT_AVAILABLE = 'REVERSE_LIQUIDITY_NOT_AVAILABLE', 11 | MAX_SPREAD_EXCEEDED = 'MAX_SPREAD_EXCEEDED', 12 | MAX_SLIPPAGE_EXCEEDED = 'MAX_SLIPPAGE_EXCEEDED', 13 | MAX_REVERSE_SLIPPAGE_EXCEEDED = 'MAX_REVERSE_SLIPPAGE_EXCEEDED' 14 | } 15 | 16 | export interface Options { 17 | timeoutSeconds?: number // Default: 60 18 | includeBookData?: boolean // Default: false 19 | verboseBookData?: boolean // Default: false 20 | rates?: RatesInCurrency 21 | maxSpreadPercentage?: number 22 | maxSlippagePercentage?: number 23 | maxSlippagePercentageReverse?: number 24 | maxBookLines?: number // Default: 500 25 | } 26 | 27 | interface _Params { 28 | trade: Trade, 29 | options: Options 30 | } 31 | 32 | export interface RippledWsClient extends _Params { 33 | method: any 34 | } 35 | 36 | export interface XrplClient extends _Params { 37 | client: any 38 | } 39 | 40 | export type Params = RippledWsClient | XrplClient 41 | 42 | export interface Result { 43 | rate: number 44 | safe: boolean 45 | errors: Array 46 | books?: Offer[] 47 | } 48 | -------------------------------------------------------------------------------- /src/types/XrplObjects.ts: -------------------------------------------------------------------------------- 1 | export interface TradeAmount { 2 | currency: string 3 | issuer?: string 4 | } 5 | 6 | export interface IssuedAmount extends TradeAmount { 7 | value: string 8 | } 9 | 10 | export interface Trade { 11 | from: TradeAmount 12 | amount: number 13 | to: TradeAmount 14 | } 15 | 16 | export type Amount = string | IssuedAmount 17 | 18 | export interface OfferLedgerEntry { 19 | LedgerEntryType: 'Offer' 20 | Flags: number 21 | Account: string 22 | Sequence: number 23 | TakerPays: Amount 24 | TakerGets: Amount 25 | BookDirectory: string 26 | BookNode: string 27 | OwnerNode: string 28 | PreviousTxnID: string 29 | PreviousTxnLgrSeq: number 30 | Expiration?: number 31 | } 32 | 33 | export interface Offer extends OfferLedgerEntry { 34 | quality?: string 35 | owner_funds?: string 36 | taker_gets_funded?: Amount 37 | taker_pays_funded?: Amount 38 | } 39 | -------------------------------------------------------------------------------- /test/reader-rippledwsclient.test.ts: -------------------------------------------------------------------------------- 1 | import Client from 'rippled-ws-client' 2 | 3 | import { 4 | LiquidityCheck, 5 | RatesInCurrency, 6 | Errors 7 | } from '../src' 8 | 9 | let RippledWsClientConnection: Client 10 | 11 | const trade = { 12 | from: { 13 | currency: 'XRP' 14 | }, 15 | amount: 10000, 16 | to: { 17 | currency: 'USD', 18 | issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq' 19 | } 20 | } 21 | const options = { 22 | rates: RatesInCurrency.to 23 | } 24 | 25 | beforeAll(async () => { 26 | RippledWsClientConnection = await new Client('wss://xrplcluster.com') 27 | RippledWsClientConnection.on('error', (e: Error | string) => console.log(`XRPL Error`, e)) 28 | return RippledWsClientConnection 29 | }) 30 | 31 | afterAll(async () => { 32 | await RippledWsClientConnection.close() 33 | return new Promise(resolve => setTimeout(() => resolve(null), 100)) 34 | }) 35 | 36 | describe('XRPL Orderbook Reader', () => { 37 | it('should get rate for XRP to Gatehub USD', async () => { 38 | const Check = new LiquidityCheck({ 39 | trade, 40 | options, 41 | method: RippledWsClientConnection.send 42 | }) 43 | const Liquidity = await Check.get() 44 | 45 | return expect(Liquidity.rate).toBeGreaterThan(0) 46 | }) 47 | 48 | it('should exceed absurd limits for XRP to Gatehub USD', async () => { 49 | const Check = new LiquidityCheck({ 50 | trade, 51 | options: { 52 | ...options, 53 | maxSpreadPercentage: 0.0001, 54 | maxSlippagePercentage: 0.0001, 55 | maxSlippagePercentageReverse: 0.0001 56 | }, 57 | method: RippledWsClientConnection.send 58 | }) 59 | const Liquidity = await Check.get() 60 | 61 | return expect(Liquidity.errors).toEqual([ 62 | Errors.MAX_SPREAD_EXCEEDED, 63 | Errors.MAX_SLIPPAGE_EXCEEDED, 64 | Errors.MAX_REVERSE_SLIPPAGE_EXCEEDED 65 | ]) 66 | }) 67 | 68 | it('should error out with insufficient liquidity', async () => { 69 | const Check = new LiquidityCheck({ 70 | trade, 71 | options: { 72 | ...options, 73 | maxBookLines: 1 74 | }, 75 | method: RippledWsClientConnection.send 76 | }) 77 | const Liquidity = await Check.get() 78 | 79 | return expect(Liquidity.errors).toEqual([ 80 | Errors.REQUESTED_LIQUIDITY_NOT_AVAILABLE, 81 | Errors.REVERSE_LIQUIDITY_NOT_AVAILABLE 82 | ]) 83 | }) 84 | 85 | it('should throw timeout error', async () => { 86 | const Check = new LiquidityCheck({ 87 | trade, 88 | options: { 89 | timeoutSeconds: 0.0001 90 | }, 91 | method: RippledWsClientConnection.send 92 | }) 93 | 94 | return expect(new Promise(resolve => { 95 | resolve(Check.get()) 96 | })).rejects.toThrow('Timeout fetching order book data') 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /test/reader-xrplclient.test.ts: -------------------------------------------------------------------------------- 1 | // import Client from 'rippled-ws-client' 2 | import {XrplClient} from 'xrpl-client' 3 | 4 | import { 5 | LiquidityCheck, 6 | RatesInCurrency, 7 | Errors 8 | } from '../src' 9 | 10 | let XrplClientConnection: XrplClient 11 | 12 | const trade = { 13 | from: { 14 | currency: 'XRP' 15 | }, 16 | amount: 10000, 17 | to: { 18 | currency: 'USD', 19 | issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq' 20 | } 21 | } 22 | const options = { 23 | rates: RatesInCurrency.to 24 | } 25 | 26 | beforeAll(async () => { 27 | XrplClientConnection = new XrplClient() 28 | XrplClientConnection.on('error', (e: Error | string) => console.log(`XRPL Error`, e)) 29 | return XrplClientConnection 30 | }) 31 | 32 | afterAll(async () => { 33 | await XrplClientConnection.close() 34 | return new Promise(resolve => setTimeout(() => resolve(null), 100)) 35 | }) 36 | 37 | describe('XRPL Orderbook Reader', () => { 38 | it('should get rate for XRP to Gatehub USD', async () => { 39 | const Check = new LiquidityCheck({ 40 | trade, 41 | options, 42 | client: XrplClientConnection 43 | }) 44 | const Liquidity = await Check.get() 45 | 46 | return expect(Liquidity.rate).toBeGreaterThan(0) 47 | }) 48 | 49 | it('should exceed absurd limits for XRP to Gatehub USD', async () => { 50 | const Check = new LiquidityCheck({ 51 | trade, 52 | options: { 53 | ...options, 54 | maxSpreadPercentage: 0.0001, 55 | maxSlippagePercentage: 0.0001, 56 | maxSlippagePercentageReverse: 0.0001 57 | }, 58 | client: XrplClientConnection 59 | }) 60 | const Liquidity = await Check.get() 61 | 62 | return expect(Liquidity.errors).toEqual([ 63 | Errors.MAX_SPREAD_EXCEEDED, 64 | Errors.MAX_SLIPPAGE_EXCEEDED, 65 | Errors.MAX_REVERSE_SLIPPAGE_EXCEEDED 66 | ]) 67 | }) 68 | 69 | it('should error out with insufficient liquidity', async () => { 70 | const Check = new LiquidityCheck({ 71 | trade, 72 | options: { 73 | ...options, 74 | maxBookLines: 1 75 | }, 76 | client: XrplClientConnection 77 | }) 78 | const Liquidity = await Check.get() 79 | 80 | return expect(Liquidity.errors).toEqual([ 81 | Errors.REQUESTED_LIQUIDITY_NOT_AVAILABLE, 82 | Errors.REVERSE_LIQUIDITY_NOT_AVAILABLE 83 | ]) 84 | }) 85 | 86 | it('should throw timeout error', async () => { 87 | const Check = new LiquidityCheck({ 88 | trade, 89 | options: { 90 | timeoutSeconds: 0.0001 91 | }, 92 | client: XrplClientConnection 93 | }) 94 | 95 | return expect(new Promise(resolve => { 96 | resolve(Check.get()) 97 | })).rejects.toThrow('Timeout fetching order book data') 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "typeRoots": ["./node_modules/@types", "./@types"] 12 | }, 13 | "include": ["src/**/*","samples/*.ts"], 14 | "exclude": ["node_modules", "test"] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-eslint-rules" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | "node_modules/**" 8 | ] 9 | }, 10 | "rules": { 11 | "ban": [true, ["alert"]], 12 | "no-arg": true, 13 | "no-conditional-assignment": true, 14 | "no-console": false, 15 | "no-constant-condition": true, 16 | "no-control-regex": true, 17 | "no-debugger": true, 18 | "no-duplicate-case": true, 19 | "no-empty": true, 20 | "no-empty-character-class": true, 21 | "no-eval": true, 22 | "no-ex-assign": true, 23 | "no-extra-boolean-cast": true, 24 | "no-extra-semi": true, 25 | "no-switch-case-fall-through": true, 26 | "no-inner-declarations": [true, "functions"], 27 | "no-invalid-regexp": true, 28 | // this rule would cause problems with mocha test cases, 29 | "no-invalid-this": false, 30 | "no-irregular-whitespace": true, 31 | "ter-no-irregular-whitespace": true, 32 | "label-position": true, 33 | "indent": [true, "spaces", 2], 34 | "linebreak-style": [true, "unix"], 35 | "no-multi-spaces": true, 36 | "no-consecutive-blank-lines": [true, 2], 37 | "no-unused-expression": true, 38 | "no-construct": true, 39 | "no-duplicate-variable": true, 40 | "no-regex-spaces": true, 41 | "no-shadowed-variable": true, 42 | "ter-no-sparse-arrays": true, 43 | "no-trailing-whitespace": true, 44 | "no-string-throw": true, 45 | "no-unexpected-multiline": true, 46 | "no-var-keyword": true, 47 | "no-magic-numbers": false, 48 | "array-bracket-spacing": [true, "never"], 49 | "ter-arrow-body-style": false, 50 | "ter-arrow-parens": [true, "as-needed"], 51 | "ter-arrow-spacing": true, 52 | "block-spacing": true, 53 | "brace-style": [true, "1tbs", {"allowSingleLine": true}], 54 | "variable-name": false, 55 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], 56 | "cyclomatic-complexity": [false, 11], 57 | "curly": [true, "all"], 58 | "switch-default": false, 59 | "eofline": true, 60 | "triple-equals": true, 61 | "forin": false, 62 | "handle-callback-err": true, 63 | "ter-max-len": [true, 120], 64 | "new-parens": true, 65 | "object-curly-spacing": [true, "never"], 66 | "object-literal-shorthand": false, 67 | "one-variable-per-declaration": [true, "ignore-for-loop"], 68 | "ter-prefer-arrow-callback": false, 69 | "prefer-const": true, 70 | "object-literal-key-quotes": false, 71 | "quotemark": [true, "single"], 72 | "radix": true, 73 | "semicolon": [true, "never"], 74 | "space-in-parens": [true, "never"], 75 | "comment-format": [true, "check-space"], 76 | "use-isnan": true, 77 | "valid-typeof": true 78 | } 79 | } --------------------------------------------------------------------------------